LoginSignup
6
8

More than 3 years have passed since last update.

春祭り ~Deep Learning(CNN) + Colaboratory~

Last updated at Posted at 2018-03-31

はじめに

春の機械学習祭り 〜Data Engineering & Data Analysis WS#4〜に参加してきました。
発表のタイトルは
- 推薦アルゴリズムの今までとこれから
- 画像認識基盤構築への取り組み
- 大規模分散深層学習と ChainerMN の進歩と課題
で、最初二つはサイバーの秋葉原ラボの発表で、
最後のやつがPreferred Networksの発表でした。
発表の中身は、誰かがまとめてくれる事を期待して、特に述べません。
一個だけ言っておくと、「あぁ、すげぇ楽しそう」

今回は、そんな春の機械学習祭りに触発されて、改めてDeep Learningをやってみようと思ったので、その辺りをまとめます。
中身は、
- 参考にした記事の補足
- ファイルの分割
- 学習結果の使い方
- 実データへの応用方法
- Colaboratoryでの実行方法
- 実データでやって見た結果
です。
- そもそもDeep Learningってなに?
- どうやったら精度あがるの?
- もっと速くなんない?
などは説明しません。チューニング方法は、むしろ、僕が聞きたいです。

対象者

対象者は
- Deep Learningやりたいけど、はじめ方が分からない人
- TensorFlowを触って見たい人
- TensorFlowを触って見たが、途中で挫折した人
- TensorFlowを触って見たが、どうやって使えばいいのか分からなかった人
多分、このくらいのレベルの人です。

参考にした記事の補足

先に、僕が参考にした記事を読んで貰えば、大体分かるかと思います。
それでも、若干詰まったところがあるので、その補足説明をコードの中に詰め込んでいきます。
抽象的な表現をすると、TensorFlowとは、ただのフィルタです。
やることは、最初に入り口とパイプと出口を作っておいて、後からデータを流し込むだけです。
そして、出てきたものをありがたくもらえば大丈夫です。

import tensorflow as tf

# MNISTでは画像のサイズが28×28だから、入力xは28×28となっている。
# また、minibatchで学習させるので、一軸をNoneにすることでminibatchのサイズが可変である事を示している。
x = tf.placeholder(tf.float32, shape=[None, 28 * 28])
# 出力される結果は0~9の10個なので、出力結果のサイズは10になる。
# 実際にやってみると分かるが、推定した結果出てくるのは10次元のベクトルで、
# 一番値の大きいもののインデックスを推定解として使うことが多い模様。
y_ = tf.placeholder(tf.float32, shape=[None, 10])

# ネットワークのモデルで、5×5のサイズで読み込み、32の特徴量を作り出す。(32という数字自体には意味がないが、次の層への接続で使うので、注意は必要)
# 特徴量の作り方としては、truncated_normal(裾を切った正規分布)で乱数を発生させ、
# それの足し合わせで行なっている。
# [読み込み対象の層のx軸のサイズ, 読み込み対象の層のy軸のサイズ, 読み込み対象の層の数, 出力層の数]
# 出力層の数以外は、奇数にしておくことで、対称になる。
W_conv1 = weight_variable([5, 5, 1, 32])
# Deep Learningでは、推定したものの誤差を逆伝播するので、0から始めると上手く計算が進まないらしい。
b_conv1 = bias_variable([32])

# mnistのデータは、画像がflatになって渡されるようなので、ここで正方形に整形している。
# ここで、一つ目の要素が-1になっている理由は、xがminibatchで読み込まれるものだから、
# サイズが可変である事を示している。
# また、最後の要素が1である理由は、mnistが白黒のデータだから。
# 実際の画像でやる場合は、RGBの3層になる。
x_image = tf.reshape(x, [-1, 28, 28, 1])

# ここで畳み込みとプーリングを行う。
# 畳み込みの時にはreluを使っているが、好きなものを使って良い
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)

# ここはさっきと同様
# 違う点があるとすれば、先ほどの層で出力が32層になったので、
# 1->32に変わっていることと、この層では64の特徴量を計算するようにしていること(この64も特に意味はない)
W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])

h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)

# これが最後の層で、バラバラと計算してきたものを、一度まとめる層
# stridesが2のネットワークで二回プーリングを行なったので、画像のサイズ自体は28/4=7となっている。
# そして、一つ前の層で特徴量を64にしたので、この層への入力自体は、7×7×64となっている。
# 出力を1024にしているのは、特に意味がない
W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])

h_pool2_flat = tf.reshape(h_pool2, [-1, 7 * 7 * 64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

# 最後の層に読ませる前に、少し、調整をしている。
# Drop Outと呼ばれる手法があり、ネットワーク自体に欠落を生じさせることで
# 学習の汎化性能が上がるという研究がある。
# 理論的に正しいのかどうかは置いておいて、
# 実際に性能は上がることが多いらしい。
# 感覚的な説明としては、ネットワークを欠落させながら学習させることで、
# 単一の学習器でアンサンブル学習のようなことが行えているから、汎化性能が上がる。
keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

# 最後に、1024の特徴量から、10次元のベクトルを出力する。
W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

# y_convが結果のベクトルで、最大値を持つもののインデックスが答えであるとする。
# 例えば、[0, 1.2, 5, 2, 1, 4.2, 12, 1, 0.9, 1.3]
# のようなベクトルが計算された時、答えは6とする。
y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2
import tensorflow as tf

# ネットワークの重みの初期値を計算する
# 上で述べたとおり、裾を切った正規乱数で初期化している。
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)

# 畳み込みを行う
# stridesは[入力層のずらし方, 読み込み対象の層のx軸のずらし方, 読み込み対象の層のy軸のずらし方, 出力層のずらし方?]を指定する。
def conv2d(x, W):
    return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')


def max_pool_2x2(x):
    return tf.nn.max_pool(x,
                          ksize=[1, 2, 2, 1],
                          strides=[1, 2, 2, 1],
                          padding='SAME')

多分、これで大体分かると思います。

ファイルの分割

上で、細かい説明は追記したので、実際に使うためにちょっと調整していきます。
自分が学習させたい問題に最適なネットワークの構造を見つけ出すのは難しいかと思います。
なので、前もってちょっとファイルを分割して見やすくしておきます。

.
├── main.py
├── methods_for_model.py
└── net_models
    └── tutorial.py

このような形に分けておきます。
main.pyに、データのロードとモデルの選択と実行開始を担当させ、
methods_for_model.pyに、weight_variableやconv2dなど、Deep Learningの計算で必要なものを担当させます。
そして、ネットワーク自体のモデルはnet_models/以下に置き、mainから次のような形で呼び出すことで、切り替えます。

from net_models.tutorial import x, y_, keep_prob, y_conv

例えば、違うネットワーク構造を試して見たいと考えた時には、別のファイルをnet_models/以下に置き、mainから呼び出すmoduleをnet_models.tutorialからnet_models.tutorial2などに切り替えれば終わりです。
これで、複数人でトライするときも、一人で何パターンか切り替える時でも、コードの変更が簡単になるかと思います。

学習結果の使い方

そして、一旦の大詰めで学習結果の使い方です。
先ほど乗せた参考記事では、学習の推定精度までは出していたのですが、
推定精度が知りたいのではなく、答えが知りたい訳なので、そこを出していきます。
まずは、学習の実行と推定精度を調べるための処理です。

# TensorFlowを起動させる前準備
sess = tf.InteractiveSession()

# 目的関数の設定をしている。
# 解く問題によっては、別の指標で行うべき
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y_conv))
# 学習の進め方
# Adamと呼ばれる更新規則を指定しているが、別になんでも良い
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
# 推定値の評価は、どれくらい正解したのか?にしている。
# これも、解く問題によって変えるべきもの
correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

# 用意したネットワークを使って学習させるために、TensorFlowを起動させる
with tf.Session() as sess:
    # 全ての値を初期化している
    sess.run(tf.global_variables_initializer())
    for i in range(20000):
        # minibatchで計算をさせる。
        # ここでは50個ずつ学習させるが、何が最適かは知らない
        batch = data.train.next_batch(50)
        if i % 1000 == 0:
            # 学習させたデータでの適合度を調べる。
            # ここで与える引数は、modelの中で宣言したplaceholderを全て与えないと動かない(たまに忘れて、エラーで悩むハメになる)
            train_accuracy = accuracy.eval(feed_dict={x: batch[0], y_: batch[1], keep_prob: 1.0})
            print('step %d, training accuracy %g' % (i, train_accuracy))
            # 学習させたデータでの汎化性能を調べる。
            # ここでは実際には学習は行わず、性能だけを教えてくれる。
            # placeholderを全て入れるのは、ここでも多分同じ
            print('test accuracy %g' % accuracy.eval(feed_dict={x: data.test.images, y_: data.test.labels, keep_prob: 1.0}))
        # ここが学習の実行部分
        # dropoutの部分を0.5にしている。
        train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})

これを実行すれば、普通に動いてくれて、推定精度が??.??%など出るかと思います。(MNISTだと99%以上の結果になるかと思います)
ただ、先ほども述べたとおり、

いや、推定精度が良いのは分かったから、データ入れた時に推定結果どうなるのか教えてよ

となるはずです。
少なくとも自分は、

良いから、答えはよ

と思いました。
ということで、次のように書き換えていきます。

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    flag = 1
    for i in range(20000):
        batch = data.train.next_batch(50)
        train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})
        if i % 1000 == 0:
            train_accuracy = accuracy.eval(feed_dict={x: batch[0], y_: batch[1], keep_prob: 1.0})
            print('step %d, training accuracy %g' % (i, train_accuracy))
            print('test accuracy %g' % accuracy.eval(feed_dict={x: data.test.images, y_: data.test.labels, keep_prob: 1.0}))

    # 学習が終わった段階で、テスト用のデータを入れていく。
    for test_x, test_y in zip(data.test.images, data.test.labels):
        # sess.run()の中に入れることで、指定した変数の値を取り出すことができる
        # これがTensorFlowがただのフィルタだ感じた部分で、
        # 蛇口をひねって、水を水道管から流さないと値が出てこない
        # また、この時はplaceholderでyを入れなくても動く。
        # まぁ入れないと動かなければ、カンニングを疑わざるを得なくなるから、使いたくなくなる訳だが、
        # もうちょっと説明が欲しかった。
        # 上の方でも述べた通り、一番大きな要素のインデックスを答えとして持つので、argmaxを計算している。
        # これで、answerとtest_valueを比較することで、「実は1だったものを7だと判断した!」とかが分かるようになる。
        answer = sess.run(tf.argmax(y_conv, 1), feed_dict={x: [test_x], keep_prob: 1.0})
        test_value = test_y.argmax(0)
        test_value = 0 if flag == test_value else 1
        if test_value != answer[0]:
            print(test_value, answer[0])

実データへの応用方法

これで、Deep LearningでMNISTの問題は解けるようになっているはずです。
でも、MNISTが解ければ満足する訳ないですよね?
ということで、実データに適応してきます。
deep analytics(日本語版kaggleの様なもの。なんか名前変わるらしい。)というサイトがあって、そこにcookpadが学習用のデータを置いてくれていたので、コンペに参加するついでに拝借しました。(コンペ自体は既に終了しているので、もうデータは手に入らないかと思います。)
手に入れたデータは、画像のzipファイル(train.zip)と、各画像のファイル名と画像に何が書いてあるのかを指定するtsvファイル(train_master.tsv)でした。
このデータを読み込み、TensorFlowに学習させていきます。

def load_data_path_label():
    # tsvファイルは全て読み込んでmemory streamに保存しておく
    with open(dir_path + 'input/train_master.tsv', newline='') as f:
        reader = csv.reader(f, delimiter='\t')
        header = next(reader)
        data = []
        for row in reader:
            data.append([row[0], int(row[1])])
    return data

def load_zip_img_from_path(filename, label):
    # zipのままでも開けるので、zipのままにしておく
    # 全部解凍していたら、file streamが金食い虫になってしまう。
    # ここの実装はどうやってるのか想像つかない
    # なんでzipのままで読めるんだろうか
    zippath = dir_path + 'download_zip/train.zip'
    z = zipfile.ZipFile(zippath)
    # 開いたファイルをPILで開き、numpy.arrayに変換する
    # 学習データのサイズがバラバラだと行けないので、この段階でサイズ変更を咬ましている。
    img = Image.open(io.BytesIO(z.read(filename))).resize((image_size, image_size))
    img_array = np.asarray(img)
    # ラベルは、0~54になっていたので、55次元の特定の一つだけが1を持つベクトルに変換する
    binary_label = np.zeros(label_size)
    binary_label[label] = 1
    # 必要に応じて、どんな画像を読み込んでいるのか見ることも可能
    # plt.imshow(img_array)
    # plt.show()
    # ここでは、単純にデータを返しているが、場合によってはデータの標準化を咬ましても良い
    return img_array, binary_label
train_path_label = load_train_path_label()
print(train_path_label)
for filename, label in train_path_label:
    # これで、img_arrayにデータが入ってくる
    # 画像でDeep Learningをする時に、真面目に全てのデータを読み込んで
    # 全てmemory streamに持っていると、いくらお金をかけても追いつかなくなるので、
    # mini batchで読み込ませる時に必要分だけ抽出し、いらなくなったら捨てるようにすれば良い
    # 一応、そうしやすいように作っている
    img_array, binary_label = load_zip_img_from_path(filename, label)

Colaboratoryでの実行方法

これで、データが用意されていれば、やって行けます。
ここで自分が直面した問題としては、

え、自分のPCでやったらスペック足りなすぎるんだけど。

ということです。
まぁ当然ですね。
今回は、一旦遊びたいだけだったので、お金はかけない方針で行こうかと思います。
そこで採用したのがGoogle Colabというサービスです。
GPUを使わせてくれるみたいです。
細かい使用条件はあるんですが、TeslaのK80というスペックのものがロハで使えるようです。(ちょっと値段が気になったので調べてみたところ7,80万ほどするようでした。)

使用条件でちょっと効いてくるのが、
- 90分触らなければデータが全て消えること
- 12時間経つとデータが消えること
- メモリサイズ
あたりかなと思います。
ちゃんと調べたわけではないですが、多分dockerか何かでインスタンスを立てていて、
resetの条件を決めているのかと思います。
で、おそらく、余裕のあるregionのインスタンスを使うようになっているはずです。(接続に割と時間がかかる事があるから、そんな気がしてるだけです。)

なんにせよ、遊ぶには十分すぎるので、ありがたく使わせてもらいます。
ここで問題になるのが、どうやってデータをアップロードすれば良いのだろうか?という話です。
結論、これで動きます。

!apt-get install -y -qq software-properties-common python-software-properties module-init-tools
!add-apt-repository -y ppa:alessandro-strada/ppa 2>&1 > /dev/null
!apt-get update -qq 2>&1 > /dev/null
!apt-get -y install -qq google-drive-ocamlfuse fuse
from google.colab import auth
auth.authenticate_user()
from oauth2client.client import GoogleCredentials
creds = GoogleCredentials.get_application_default()
import getpass
!google-drive-ocamlfuse -headless -id={creds.client_id} -secret={creds.client_secret} < /dev/null 2>&1 | grep URL
vcode = getpass.getpass()
!echo {vcode} | google-drive-ocamlfuse -headless -id={creds.client_id} -secret={creds.client_secret}

これを実行した段階で、googleでよくあるキーの認証が二回求められるので
突破した段階で、google driveのデータにアクセスできるようになります。
複数のサービスへの接続が簡単なのはありがたいですが、やり方が分からないことが多いので、
知ってる人にとっては簡単で、知らない人にとったら魔境に感じるかと思います。

!mkdir -p drive
!google-drive-ocamlfuse drive
!ls

そして、マウントすれば、ファイルが確認されます。
ここで注意点をあげるとすれば、リアルタイムで更新されるわけではないようなので
多少は待たないと、期待通りの結果にならないかと思います。
実際に、パラメータを複数変えて実行しようとした時、
(一部のファイルだけ更新されていなかったことが原因でエラーを吐いていたことがあります。)

実データでやって見た結果

まぁ普通にダメでした。
テストデータでの推定結果が2%程度?
学習セットに対しても、精々8%程度であったので、ちょっと救いようがなかったです。

まとめ

Deep Learningをやってみることはできました。
ただ、何も考えずにやっただけでは案の定、精度が残念だったので、そこは頭をひねる必要があります。
そこに集中できるように、ファイルを分割したつもりなので、
これから論文を読み漁って、知見をためてみようかと思います。
うまく行けば、続編を書きます。

あとやること

  • 新しいデータセットを手に入れる(cookpadのデータはもう使ってはいけないはず)
  • 論文を読む(知見が足りない)
  • GCPを使えるようになる(スペック不足への対処)
  • TensorFlow以外も触ってみる(せっかくPreferredの人の発表を聞いたので、Chainerからかな)
  • TensorFlowのeagerとやらを使ってみる(流行りのdefine by runを採用したやつ)
  • 解きたい問題を探す
6
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
8