MacでTensorFlowを使う

2016-04-19 (火) 18:19:26 (370d) | Topic path: Top / 機械学習 / MacでTensorFlowを使う

はじめに

ここでは、Googleが公開しているオープン・ソース・ソフトウェア (OSS) のTensorFlowをMac (OS X) にインストールして使います。

環境

  • OS X Yosemite 10.10.5
  • Python 3.5.1
  • TensorFlow 0.7.1

Python3のインストール(アップデート)

まず、Python3を最新版にアップデートします。 下記のサイトからMac OS X 64-bit/32-bit installerをダウンロードして、インストール。

TensorFlowのインストール

基本的にはここに書いてある通りなんですが、なぜかsixのeasy_installが先にできなかったので、順序を逆にしました。

sudo pip3 install --upgrade https://storage.googleapis.com/tensorflow/mac/tensorflow-0.7.1-cp35-none-any.whl
$ sudo easy_install --upgrade six

Virtualenvのインストール

$ sudo pip3 install --upgrade virtualenv
$ virtualenv --system-site-packages /Users/Shared/tools/tensorflow
$ source /Users/Shared/tools/tensorflow/bin/activate
(tensorflow)$ pip3 install --upgrade https://storage.googleapis.com/tensorflow/mac/tensorflow-0.7.1-cp35-none-any.whl
(tensorflow)$ deactivate

私の研究室では、共有フォルダーのtoolsというフォルダー (/Users/Shared/tools/) に、みんなで使うツールをインストールしています。 /Users/Shared/tools/tensorflow を ~/tensorflow などTensorFlowをインストールしたいフォルダーに変更してください。

TensorFlowを動かす

TesorFlowのサイトに掲載されているHello Worldを動かします。

$ source /Users/Shared/tools/tensorflow/bin/activate
(tensorflow) $ python3
Python 3.5.1 (v3.5.1:37a07cee5969, Dec  5 2015, 21:12:44) 
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import tensorflow as tf
>>> hello = tf.constant('Hello, TensorFlow!')
>>> sess = tf.Session()
>>> print(sess.run(hello))
b'Hello, TensorFlow!'
>>> a = tf.constant(10)
>>> b = tf.constant(32)
>>> print(sess.run(a + b))
42
>>> quit()
(tensorflow) $ deactivate

TensorFlowでカテゴリー分析をやってみる

TensorFlowの最初のチュートリアルは手書き数字認識のMNISTですが、irisデータセットでカテゴリー分析(分類問題)をやってみます。

データの準備

Irisデータセットは、4つの説明変数(花びらの長さ、幅、がく片の長さ、幅)とカテゴリー (setosa, versicolor, virginica) からなるデータセットです。

UCI Machine Learning Repositoryからファイル iris.data をダウンロードします。 中身はこんな感じです。

$ head -5 iris.data
5.1,3.5,1.4,0.2,Iris-setosa
4.9,3.0,1.4,0.2,Iris-setosa
4.7,3.2,1.3,0.2,Iris-setosa
4.6,3.1,1.5,0.2,Iris-setosa
5.0,3.6,1.4,0.2,Iris-setosa

3クラスの分類なので、カテゴリーのラベルを3次元の0/1ベクトルに変換します。 つまり、setosaは 1,0,0、versicolorは 0,1,0、virginicaは 0,0,1 とします。

Irisデータセットには、それぞれ、50個ずつ150個のデータが並んでいるので、次のように変換します。

$ head -50 iris.data | awk -F, '{printf("%s,%s,%s,%s,1,0,0\n",$1,$2,$3,$4);}' > iris.csv
$ head -100 iris.data | tail -50 | awk -F, '{printf("%s,%s,%s,%s,0,1,0\n",$1,$2,$3,$4);}' >> iris.csv
$ head -150 iris.data | tail -50 | awk -F, '{printf("%s,%s,%s,%s,0,0,1\n",$1,$2,$3,$4);}' >> iris.csv

これで、データの準備は完了です。 このデータをdataフォルダーにおいておきます。

カテゴリー分析用ソースコード

TensorFlowのTutorialsについてる最初の手書き文字データセットMNIST用のサンプルをほとんどそのまま使ったソースコードはこんな感じです。

# Copyright 2016 Tohgoroh Matsui All Rights Reserved.
#
# This includes software developed by Google, Inc.
# licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import tensorflow as tf
import numpy as np


# for TensorBoard
def variable_summaries(var, name):
  with tf.name_scope('summaries'):
    mean = tf.reduce_mean(var)
    tf.scalar_summary('mean/' + name, mean)
    with tf.name_scope('stddev'):
      stddev = tf.sqrt(tf.reduce_sum(tf.square(var - mean)))
    tf.scalar_summary('sttdev/' + name, stddev)
    tf.scalar_summary('max/' + name, tf.reduce_max(var))
    tf.scalar_summary('min/' + name, tf.reduce_min(var))
    tf.histogram_summary(name, var)


# パラメーター
flags = tf.app.flags
FLAGS = flags.FLAGS
flags.DEFINE_integer('samples',       120,    'Number of training samples.')
flags.DEFINE_integer('max_steps',     1001,   'Number of steps to run trainer.')
flags.DEFINE_float(  'learning_rate', 0.001,  'Initial learning rate.')
flags.DEFINE_float(  'dropout',       0.9,    'Keep probability for training dropout.')
flags.DEFINE_string( 'data_dir',      'data', 'Directory for storing data.')
flags.DEFINE_string( 'summaries_dir', 'log',  'Summaries directory.')


def train():
  # CSVファイルの読み込み
  data = np.genfromtxt(FLAGS.data_dir + '/iris.csv', delimiter=",")

  # データをシャッフル
  np.random.shuffle(data)

  # 訓練データ (train) とテストデータ (test) に分割
  x_train, x_test = np.vsplit(data[:,0:4].astype(np.float32), [FLAGS.samples])
  y_train, y_test = np.vsplit(data[:,4:7].astype(np.float32), [FLAGS.samples])


  # セッション
  sess = tf.InteractiveSession()

  # 入力
  with tf.name_scope('input'):
    x  = tf.placeholder(tf.float32, shape=[None, 4])

  # 出力
  with tf.name_scope('output'):
    y_ = tf.placeholder(tf.float32, shape=[None, 3])

  # ドロップアウトのキープ率
  with tf.name_scope('dropout'):
    keep_prob = tf.placeholder(tf.float32)
    tf.scalar_summary('dropout_keep_probability', keep_prob)

  # 重み(係数)
  def weight_variable(shape):
    initial = tf.truncated_normal(shape, stddev=0.1)
    return tf.Variable(initial)

  # バイアス(定数項)
  def bias_variable(shape):
    initial = tf.constant(0.1, shape=shape)
    return tf.Variable(initial)

  # for TensorBoard
  def variable_summaries(var, name):
    with tf.name_scope('summaries'):
      mean = tf.reduce_mean(var)
      tf.scalar_summary('mean/' + name, mean)
      with tf.name_scope('stddev'):
        stddev = tf.sqrt(tf.reduce_sum(tf.square(var - mean)))
      tf.scalar_summary('sttdev/' + name, stddev)
      tf.scalar_summary('max/' + name, tf.reduce_max(var))
      tf.scalar_summary('min/' + name, tf.reduce_min(var))
      tf.histogram_summary(name, var)


  # ニューラル・ネットワークの層
  def nn_layer(input_tensor, input_dim, output_dim, layer_name, act=tf.nn.relu):
    with tf.name_scope(layer_name):
      with tf.name_scope('weights'):
        weights = weight_variable([input_dim, output_dim])
        variable_summaries(weights, layer_name + '/weights')
      with tf.name_scope('biases'):
        biases = bias_variable([output_dim])
        variable_summaries(biases, layer_name + '/biases')
      with tf.name_scope('Wx_plus_b'):
        preactivate = tf.matmul(input_tensor, weights) + biases
        tf.histogram_summary(layer_name + '/pre_activations', preactivate)
      activations = act(preactivate, 'activation')
      tf.histogram_summary(layer_name + '/acctivations', activations)
      return activations


  # 入力層
  hidden1 = nn_layer(x, 4, 64, 'layer1')
  dropped1 = tf.nn.dropout(hidden1, keep_prob)

  # 中間層
  hidden2 = nn_layer(dropped1, 64, 64, 'layer2')
  dropped2 = tf.nn.dropout(hidden2, keep_prob)

  # 出力層
  y = nn_layer(dropped2, 64, 3, 'layer3', act=tf.nn.softmax)


  # クロス・エントロピー
  with tf.name_scope('cross_entropy'):
    #diff = y_ * tf.log(y)
    #with tf.name_scope('total'):
    #  cross_entropy = -tf.reduce_mean(diff)
    cross_entropy = -tf.reduce_sum(y_*tf.log(tf.clip_by_value(y,1e-10,1.0)))
    tf.scalar_summary('cross entropy', cross_entropy)

  # 学習
  with tf.name_scope('train'):
    train_step = tf.train.AdamOptimizer(FLAGS.learning_rate).minimize(cross_entropy)

  # 精度
  with tf.name_scope('accuracy'):
    with tf.name_scope('correct_prediction'):
      correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
    with tf.name_scope('accuracy'):
      accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    tf.scalar_summary('accuracy', accuracy)


  # for TensorBoard
  merged = tf.merge_all_summaries()
  train_writer = tf.train.SummaryWriter(FLAGS.summaries_dir + '/train', sess.graph_def)
  test_writer  = tf.train.SummaryWriter(FLAGS.summaries_dir + '/test')
  tf.initialize_all_variables().run()


  # データとドロップアウト・キープ率の切り替え
  def feed_dict(train):
    if train:
      xs, ys = x_train, y_train
      k  = FLAGS.dropout
    else:
      xs, ys = x_test, y_test
      k  = 1.0
    return {x: xs, y_: ys, keep_prob: k}


  # 学習ルーチン
  for i in range(FLAGS.max_steps):
    # 学習
    sess.run(train_step, feed_dict=feed_dict(True))
    # 訓練データに対する評価
    summary, acc, cp = sess.run([merged, accuracy, cross_entropy], feed_dict=feed_dict(True))
    train_writer.add_summary(summary, i)
    # テスト・データに対する評価
    summary, acc, cp = sess.run([merged, accuracy, cross_entropy], feed_dict=feed_dict(False))
    test_writer.add_summary(summary, i)
    if i == 0 or i % np.power(10, np.floor(np.log10(i))) == 0:
      print('Accuracy and Cross-Entropy at step %s: %s, %s' % (i, acc, cp))


def main(_):
  if tf.gfile.Exists(FLAGS.summaries_dir):
    tf.gfile.DeleteRecursively(FLAGS.summaries_dir)
  tf.gfile.MakeDirs(FLAGS.summaries_dir)
  train()

if __name__ == '__main__':
  tf.app.run()

mist_with_summaries.pyからいくつか変更点があります。

  • iris.csvからデータを読み込む
  • データをシャッフルして120個を訓練データに、残りの30個をテスト・データにする
  • 入力は4次元、出力は3次元
  • 中間層(活性化関数はReLU)を追加
  • 入力層は4x64ユニット、中間層は64x64ユニット、出力層は64x3ユニット
  • クロス・エントロピーの定義を変更(元の定義だとNaNになってしまうことがあるため)
  • 10回中9回学習して1回テストを行うのではなく、毎回学習とテストを行う
  • 標準出力に精度とクロス・エントロピーを出力

実行

(tensorflow) $ python3 tfc_iris.py 
Accuracy and Cross-Entropy at step 0: 0.366667, 129.506
Accuracy and Cross-Entropy at step 1: 0.3, 127.23
Accuracy and Cross-Entropy at step 2: 0.333333, 127.76
Accuracy and Cross-Entropy at step 3: 0.375, 125.178
Accuracy and Cross-Entropy at step 4: 0.366667, 122.702
Accuracy and Cross-Entropy at step 5: 0.441667, 121.688
Accuracy and Cross-Entropy at step 6: 0.375, 120.456
Accuracy and Cross-Entropy at step 7: 0.458333, 118.65
Accuracy and Cross-Entropy at step 8: 0.5, 117.239
Accuracy and Cross-Entropy at step 9: 0.525, 115.154
Accuracy and Cross-Entropy at step 10: 0.508333, 113.581
Accuracy and Cross-Entropy at step 20: 0.716667, 97.5319
Accuracy and Cross-Entropy at step 30: 0.716667, 80.913
Accuracy and Cross-Entropy at step 40: 0.833333, 66.6253
Accuracy and Cross-Entropy at step 50: 0.891667, 55.1736
Accuracy and Cross-Entropy at step 60: 0.916667, 45.7833
Accuracy and Cross-Entropy at step 70: 0.941667, 39.9971
Accuracy and Cross-Entropy at step 80: 0.941667, 35.9949
Accuracy and Cross-Entropy at step 90: 0.933333, 30.6227
Accuracy and Cross-Entropy at step 100: 0.925, 26.9075
Accuracy and Cross-Entropy at step 200: 0.983333, 11.3029
Accuracy and Cross-Entropy at step 300: 0.966667, 7.95685
Accuracy and Cross-Entropy at step 400: 0.983333, 5.68508
Accuracy and Cross-Entropy at step 500: 0.983333, 5.62412
Accuracy and Cross-Entropy at step 600: 0.975, 6.91621
Accuracy and Cross-Entropy at step 700: 0.975, 5.36038
Accuracy and Cross-Entropy at step 800: 0.991667, 3.64939
Accuracy and Cross-Entropy at step 900: 0.975, 6.4123
Accuracy and Cross-Entropy at step 1000: 0.991667, 6.80436

TensorBoardによるログの確認

TensorBoardでログを確認します。

次のようにしてTensorBoardを起動すると、6006番ポートでHTTPサーバーが起動します。

(tensorflow) $ tensorboard --logdir=log

Webブラウザーで http://localhost:6006 にアクセスします。

tfc_iris.png

TensorFlowで回帰分析をやってみる

次に、Housingデータセットで回帰分析をやってみます。

データの準備

Housingデータセットは、ボストン近郊の住宅の価格を、地区の犯罪発生率、宅地率など13個の説明変数を用いて予測する問題です。

UCI Machine Learning Repositoryから、 housing.data をダウンロードします。 中身はこんな感じです。

$ head -5 housing.data
 0.00632  18.00   2.310  0  0.5380  6.5750  65.20  4.0900   1  296.0  15.30 396.90   4.98  24.00
 0.02731   0.00   7.070  0  0.4690  6.4210  78.90  4.9671   2  242.0  17.80 396.90   9.14  21.60
 0.02729   0.00   7.070  0  0.4690  7.1850  61.10  4.9671   2  242.0  17.80 392.83   4.03  34.70
 0.03237   0.00   2.180  0  0.4580  6.9980  45.80  6.0622   3  222.0  18.70 394.63   2.94  33.40
 0.06905   0.00   2.180  0  0.4580  7.1470  54.20  6.0622   3  222.0  18.70 396.90   5.33  36.20

これをコンマ区切りのCSVファイルに変換します。

$ sed "s/^ //" < housing.data | sed "s/  */,/g" > housing.csv

これで、データの準備は完了です。 このデータをdataフォルダーにおいておきます。

回帰分析用ソースコード

カテゴリー分析用のソースコードを一部変更して作成した回帰分析用のソースコードはこんな感じです。

# Copyright 2016 Tohgoroh Matsui All Rights Reserved.
#
# This includes software developed by Google, Inc.
# licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import tensorflow as tf
import numpy as np


# for TensorBoard
def variable_summaries(var, name):
  with tf.name_scope('summaries'):
    mean = tf.reduce_mean(var)
    tf.scalar_summary('mean/' + name, mean)
    with tf.name_scope('stddev'):
      stddev = tf.sqrt(tf.reduce_sum(tf.square(var - mean)))
    tf.scalar_summary('sttdev/' + name, stddev)
    tf.scalar_summary('max/' + name, tf.reduce_max(var))
    tf.scalar_summary('min/' + name, tf.reduce_min(var))
    tf.histogram_summary(name, var)


# パラメーター
flags = tf.app.flags
FLAGS = flags.FLAGS
flags.DEFINE_integer('samples',       400,    'Number of training samples.')
flags.DEFINE_integer('max_steps',     1001,   'Number of steps to run trainer.')
flags.DEFINE_float(  'learning_rate', 0.001,  'Initial learning rate.')
flags.DEFINE_float(  'dropout',       0.9,    'Keep probability for training dropout.')
flags.DEFINE_string( 'data_dir',      'data', 'Directory for storing data.')
flags.DEFINE_string( 'summaries_dir', 'log',  'Summaries directory.')


def train():
  # CSVファイルの読み込み
  data = np.genfromtxt(FLAGS.data_dir + '/housing.csv', delimiter=",")

  # データをシャッフル
  np.random.shuffle(data)

  # 学習データ (train) とテストデータ (test) に分割
  x_train, x_test = np.vsplit(data[:, 0:13].astype(np.float32), [FLAGS.samples])
  y_train, y_test = np.vsplit(data[:,13:14].astype(np.float32), [FLAGS.samples])


  # セッション
  sess = tf.InteractiveSession()

  # 入力
  with tf.name_scope('input'):
    x  = tf.placeholder(tf.float32, shape=[None, 13])

  # 出力
  with tf.name_scope('output'):
    y_ = tf.placeholder(tf.float32, shape=[None, 1])

  # ドロップアウトのキープ率
  with tf.name_scope('dropout'):
    keep_prob = tf.placeholder("float")
    tf.scalar_summary('dropout_keep_probability', keep_prob)

  # 重み(係数)
  def weight_variable(shape):
    initial = tf.truncated_normal(shape, stddev=0.1)
    return tf.Variable(initial)

  # バイアス(定数項)
  def bias_variable(shape):
    initial = tf.constant(0.1, shape=shape)
    return tf.Variable(initial)

  # for TensorBoard
  def variable_summaries(var, name):
    with tf.name_scope('summaries'):
      mean = tf.reduce_mean(var)
      tf.scalar_summary('mean/' + name, mean)
      with tf.name_scope('stddev'):
        stddev = tf.sqrt(tf.reduce_sum(tf.square(var - mean)))
      tf.scalar_summary('sttdev/' + name, stddev)
      tf.scalar_summary('max/' + name, tf.reduce_max(var))
      tf.scalar_summary('min/' + name, tf.reduce_min(var))
      tf.histogram_summary(name, var)


  # ニューラル・ネットワークの層
  def nn_layer(input_tensor, input_dim, output_dim, layer_name, relu=True):
    with tf.name_scope(layer_name):
      with tf.name_scope('weights'):
        weights = weight_variable([input_dim, output_dim])
        variable_summaries(weights, layer_name + '/weights')
      with tf.name_scope('biases'):
        biases = bias_variable([output_dim])
        variable_summaries(biases, layer_name + '/biases')
      with tf.name_scope('Wx_plus_b'):
        preactivate = tf.matmul(input_tensor, weights) + biases
        tf.histogram_summary(layer_name + '/pre_activations', preactivate)
      if relu:
        activations = tf.nn.relu(preactivate, 'activation')
      else:
        activations = preactivate
      tf.histogram_summary(layer_name + '/acctivations', activations)
      return activations


  # 入力層
  hidden1 = nn_layer(x, 13, 64, 'layer1')
  dropped1 = tf.nn.dropout(hidden1, keep_prob)

  # 中間層
  hidden2 = nn_layer(dropped1, 64, 64, 'layer2')
  dropped2 = tf.nn.dropout(hidden2, keep_prob)

  # 出力層
  y = nn_layer(dropped2, 64, 1, 'layer3', relu=False)


  # 損失
  with tf.name_scope('loss'):
    loss = tf.reduce_mean(tf.square(y_ - y))
    tf.scalar_summary('loss', loss)

  # 学習
  with tf.name_scope('train'):
    train_step = tf.train.AdagradOptimizer(FLAGS.learning_rate).minimize(loss)


  # for TensorBoard
  merged = tf.merge_all_summaries()
  train_writer = tf.train.SummaryWriter(FLAGS.summaries_dir + '/train', sess.graph_def)
  test_writer  = tf.train.SummaryWriter(FLAGS.summaries_dir + '/test')
  tf.initialize_all_variables().run()


  # データとドロップアウト・キープ率の切り替え
  def feed_dict(train):
    if train:
      xs, ys = x_train, y_train
      k  = FLAGS.dropout
    else:
      xs, ys = x_test, y_test
      k  = 1.0
    return {x: xs, y_: ys, keep_prob: k}


  # 学習ルーチン
  for i in range(FLAGS.max_steps):
    # 学習
    sess.run(train_step, feed_dict=feed_dict(True))
    # 訓練データに対する評価
    summary, train_loss = sess.run([merged, loss], feed_dict=feed_dict(True))
    train_writer.add_summary(summary, i)
    # テスト・データに対する評価
    summary, test_loss = sess.run([merged, loss], feed_dict=feed_dict(False))
    test_writer.add_summary(summary, i)
    if i == 0 or i % np.power(10, np.floor(np.log10(i))) == 0:
      print('Train loss and test loss at step %s: %s, %s' % (i, train_loss, test_loss))


def main(_):
  if tf.gfile.Exists(FLAGS.summaries_dir):
    tf.gfile.DeleteRecursively(FLAGS.summaries_dir)
  tf.gfile.MakeDirs(FLAGS.summaries_dir)
  train()

if __name__ == '__main__':
  tf.app.run()

カテゴリー分析用のソースコードからの変更点は以下のようなものです。

  • housing.csvからデータを読み込む
  • 訓練データを400個、残りをテスト・データに
  • 入力ユニット13個、出力ユニット1個
  • 出力層の活性化関数はなし(線形和をそのまま出力)
  • 目的関数は平均二乗誤差
  • 最適化アルゴリズムはAdagradOptimizer

実行

(tensorflow) $ python3 tfr_housing.py 
Train loss and test loss at step 0: 222.248, 258.198
Train loss and test loss at step 1: 181.053, 217.751
Train loss and test loss at step 2: 176.776, 196.942
Train loss and test loss at step 3: 159.877, 182.96
Train loss and test loss at step 4: 163.091, 172.167
Train loss and test loss at step 5: 143.979, 163.054
Train loss and test loss at step 6: 151.934, 155.645
Train loss and test loss at step 7: 147.31, 149.762
Train loss and test loss at step 8: 136.29, 145.041
Train loss and test loss at step 9: 127.454, 140.468
Train loss and test loss at step 10: 134.063, 137.537
Train loss and test loss at step 20: 109.339, 113.872
Train loss and test loss at step 30: 92.2723, 104.058
Train loss and test loss at step 40: 91.5749, 99.5541
Train loss and test loss at step 50: 88.0288, 96.844
Train loss and test loss at step 60: 76.5585, 94.9638
Train loss and test loss at step 70: 78.7849, 94.9884
Train loss and test loss at step 80: 82.2189, 94.7088
Train loss and test loss at step 90: 77.9839, 94.2624
Train loss and test loss at step 100: 69.8914, 93.1786
Train loss and test loss at step 200: 65.2334, 90.2992
Train loss and test loss at step 300: 62.0393, 88.9723
Train loss and test loss at step 400: 62.4581, 88.091
Train loss and test loss at step 500: 59.1241, 87.2261
Train loss and test loss at step 600: 55.4679, 87.1955
Train loss and test loss at step 700: 55.6161, 85.9775
Train loss and test loss at step 800: 59.5973, 84.6886
Train loss and test loss at step 900: 53.5109, 84.325
Train loss and test loss at step 1000: 54.979, 83.1869

TensorBoardによるログの確認

カテゴリー分析と同じです。

(tensorflow) $ tensorboard --logdir=log
tfr_housing.png
添付ファイル: filetfr_housing.png 34件 [詳細] filetfc_iris.png 29件 [詳細]
トップ   編集 凍結解除 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS