Keras に入門する 画像分類 Part.3 独自の分類器を作ってみる

前回準備した画像で学習してみる

https://colab.research.google.com/drive/1NQhm5hCOFqv1v-QZM-AuGr1bDWbz2Mgh

学習結果は

WARNING:tensorflow:Variable *= will be deprecated. Use `var.assign(var * other)` if you want assignment to the variable value or `x = x * y` if you want a new python Tensor object.
Train on 7722 samples, validate on 200 samples
Epoch 1/100
7722/7722 [==============================] - 6s 765us/step - loss: 2.2591 - acc: 0.1444 - val_loss: 2.1802 - val_acc: 0.2200
Epoch 2/100
7722/7722 [==============================] - 4s 508us/step - loss: 2.0826 - acc: 0.2370 - val_loss: 2.0145 - val_acc: 0.2700
Epoch 3/100

...

Epoch 99/100
7722/7722 [==============================] - 4s 500us/step - loss: 0.6649 - acc: 0.7762 - val_loss: 1.5615 - val_acc: 0.5650
Epoch 100/100
7722/7722 [==============================] - 4s 502us/step - loss: 0.6505 - acc: 0.7835 - val_loss: 1.3909 - val_acc: 0.5450
<keras.callbacks.History at 0x7f75c9ba2780>

テストデータの分類精度は55%程度までしかあがらなかった

検証用に2個ずつ残しておいた画像を分類してみる

model.predict_classes(x_valid)
→ array([5, 0, 1, 7, 6, 5, 3, 3, 4, 4, 5, 9, 6, 6, 5, 6, 8, 8, 9, 1])

12/20で60%の正解だった

(すべて正解していれば [0, 0, 1, 1, 2, 2, 3, 3, ... , 9, 9] となるはず)

外れたやつをいくつか見てみる

f:id:uhiaha888:20180930024459p:plain

1枚目はカレーの画像だけど、キャラ弁的に何かが乗っかっているみたいなので分類は難しいのかも

2枚目は餃子の画像だけど32x32に縮小すると何かわからない

f:id:uhiaha888:20180930024512p:plain

3枚目と4枚目はハンバーグの画像のはずだけど僕がみても何かよくわからない

逆に当たっていたのも見てみると

f:id:uhiaha888:20180930025112p:plain

オムライスは黄色が特徴的だからかどちらも正解

f:id:uhiaha888:20180930025238p:plain

カレーやラーメンもそれっぽい画像は当てられていた

ということで、自分で集めた画像データでもそれなりの分類をできた

やったね

Keras に入門する 画像分類 Part.2 画像を用意する

32x32のカラー画像とそのラベルがあれば、前回の方法でそれらを判別するシステムを作れそうなので、今回は32x32の画像を用意してみよう

テーマは料理の画像を判別するシステム、とした

個人的にこのテーマは割と良いものが選べたんじゃないかと自負していて、その理由は

  1. 学習データとして得られる画像が均質になりやすい(大体料理のちょっと上から皿全体が映る写真になる)
  2. 画像の数を集めやすい
  3. 同一料理の画像はそこそこ似てるけど、違う料理の画像はそこそこ違う
  4. 分類できるとそこそこ意味がある

最初は、もっと荒唐無稽なテーマの分類(鉛筆と豚と火星とか)をしようと思っていたけど、料理の画像分類ならなんとなくレシピサイトとかで役立ちそう(少なくともなんかそういうサービスの技術検証とかでどこかがやっててもおかしくなさそう)なので随分マシなテーマが設定できたと思う。

ということで、やることは以下

  1. 料理の種類ごとの画像をあつめる
  2. 画像を正方形にトリミング
  3. 32x32に縮小
  4. nparrayとして保存

早速やっていく

  1. 料理の種類ごとの画像をあつめる

"Google画像検索をスクレイピングしたらいいんでしょ"と思っていたけど

Yahoo、Bing、Googleでの画像収集事情まとめ - Qiita とかみてみるとそんなに簡単でもないみたい

そもそものサンプルは、"10種類のカテゴリに対して、訓練データで50000枚、テストデータで10000枚 だったので各カテゴリで大体6000枚使っていることになる

Googleの画像検索でとりあえず"カレーライス"で検索して"結果をもっと表示"を押して最大限表示させると700枚くらいの画像が表示できた

6000枚には遠く及ばないが、まあまあ現実的な数じゃないかと思うのでこれを使うことにする

具体的な手順は、

  1. Google画像検索で料理の名前を入力
  2. 下にスクロール & "結果をもっと表示"をクリック でできるだけ画像を表示する
  3. Chromeの開発ツールでElementを表示
  4. 全体をコピー
  5. テキストファイルとして保存
  6. 保存したファイルから imgタグ かつ srcが "https://encrypted-tbn0" で始まるものを抜き出す
  7. そのimgタグのsrcを画像ファイルとしてダウンロードする

1~5が手作業になるけど10種類くらいなら対して問題にならないのでこれを実行できるコードを書く

import sys
import urllib.request

from bs4 import BeautifulSoup
from time import sleep
import os

def main(file_path, save_dir):
    html_str = None
    with open(file_path, 'r') as f:
        html_str = f.read()
    soup = BeautifulSoup(html_str, features="html.parser")
    imgs = soup.find_all('img', src=lambda s : s and s.startswith('https://encrypted-tbn0'))
    imgs = [ i['src'] for i in imgs ]

    for idx, url in enumerate(imgs):
        print('{}/{}'.format(idx+1, len(imgs)))
        download(url, './{}/{}'.format(save_dir, idx))

def download(url, dest):
    data = urllib.request.urlopen(url).read()
    with open(dest, mode='wb') as f:
        f.write(data)
        sleep(1)

if __name__ == '__main__':
    menus = [
        ('ramen.txt', 'ramen'),
        ('curry.txt', 'curry'),
        ('spaghetti.txt', 'spaghetti'),
        ('gyoza.txt', 'gyoza'),
        ('omlette-rice.txt', 'omlette-rice'),
        ('sandwich.txt', 'sandwitch'),
        ('sushi.txt', 'sushi'),
        ('hamburg.txt', 'hamburg'),
        ('steak.txt', 'steak'),
        ('miso-soup.txt', 'miso-soup')
    ]
    for menu in menus:
        os.mkdir(menu[1])
        main(menu[0], menu[1])

1~5の手順の結果をramen.txt とかの名前で保存しておくと、ramen とかのフォルダの下に連番で画像を保存してくれる

ちなみに今回検索したのは

ラーメン カレーライス スパゲッティ 餃子 オムライス サンドイッチ 寿司 ハンバーグ ステーキ 味噌汁

の10種類

  1. 画像のトリミング

note.nkmk.me

に画像の中心を最大の正方形でトリミングする方法が書いてあるのでこれをそのまま使う

(例えば100x200の画像から中心の100x100を抜き出すことにしたけど、余白部分を白とか黒とかでパディングして200x200にするという方法もあるとは思う。面倒なので検証はしていないけど精度が上がる可能性はあると思う)

全部で8000枚くらい集められた

cifar10では学習用と検査用に分けてあったけど、最後の確認用に各カテゴリ2枚ずつ残しておこうと思う

というので書いたコードが以下

# coding: utf-8
from PIL import Image
import glob
import numpy as np
import pickle

def crop_center(pil_img, crop_width, crop_height):
    img_width, img_height = pil_img.size
    return pil_img.crop(((img_width - crop_width) // 2,
                         (img_height - crop_height) // 2,
                         (img_width + crop_width) // 2,
                         (img_height + crop_height) // 2))

def crop_max_square(pil_img):
    return crop_center(pil_img, min(pil_img.size), min(pil_img.size))

def to_ary(d):
    files = glob.glob('./{}/*'.format(d))
    train = []
    test = []
    valid = []
    for i, f in enumerate(files):
        im = Image.open(f)
        # トリミング
        sq_im = crop_max_square(im)
        # 32x32へ変形
        im32 = sq_im.resize((32, 32))
        # numpy化
        im_ary = np.asarray(im32)
        if im_ary.shape != (32, 32, 3):
            continue
        if i < 20:
            test.append(im_ary)
        elif i < 22:
            valid.append(im_ary)
        else:
            train.append(im_ary)
    return train, test, valid

if __name__ == '__main__':
    X_train, X_test, X_valid = [], [], []
    Y_train, Y_test, Y_valid = [], [], []
    dirs = [
      'curry',
      'gyoza',
      'hamburg',
      'miso-soup',
      'omlette-rice',
      'ramen',
      'sandwitch',
      'spaghetti',
      'steak',
      'sushi'
    ]
    for idx, d in enumerate(dirs):
        print(d)
        x_train, x_test, x_valid = to_ary(d)
        X_train = X_train + x_train
        X_test = X_test + x_test
        X_valid = X_valid + x_valid
        Y_train = Y_train + [[idx]] * len(x_train)
        Y_test = Y_test + [[idx]] * len(x_test)
        Y_valid = Y_valid + [[idx]] * len(x_valid)
    with open('img32.pickle', mode='wb') as f:
        pickle.dump((np.array(X_train), np.array(Y_train), np.array(X_test), np.array(Y_test), np.array(X_valid), np.array(Y_valid)), f)

次回このデータで分類器を作ってみよう

Keras に入門する 画像分類 Part.1 サンプルを動かしてみる

前回サンプルで扱うデータを眺めてみたので、今回は実際にサンプルを動かして画像の分類をしてみたい

サンプルはこれ

keras/cifar10_cnn.py at master · keras-team/keras · GitHub

colab

https://colab.research.google.com/drive/1oCHanNltHykuY2R89lUFYY90cLyIKBrv

基本的にサンプルをそのまま動かしていくだけ コメントもcolabの方に書いていく

一層目は

# 一層目を追加
model.add(Conv2D(32, (3, 3), padding='same',
                 input_shape=x_train.shape[1:]))
model.add(Activation('relu'))

Conv2Dのドキュメントを見ると

keras.layers.Conv2D(filters, kernel_size, strides=(1, 1), padding='valid', data_format=None, dilation_rate=(1, 1), activation=None, use_bias=True, kernel_initializer='glorot_uniform', bias_initializer='zeros', kernel_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, bias_constraint=None)

32(filters)は出力される画像の枚数、(3, 3) (kernel_size) はフィルタのサイズとのこと

また

x_train.shape[1:]

→ (32, 32, 3)

でinput_shape には画像1枚のデータの形を渡している

あと、現在の出力のshapeはmodel.output_shape で見れるみたい

model.output_shape
→ (None, 32, 32, 32)

2層目もほぼ同じ

# 二層目を追加
model.add(Conv2D(32, (3, 3)))
model.add(Activation('relu'))

このあとMaxPooling2Dで画像のサイズを小さくしている

# pooling で画像を縮小
model.add(MaxPooling2D(pool_size=(2, 2)))

model.output_shape
→ (None, 16, 16, 32)

Dropoutは指定した割合のノードを非活性化させて過学習を抑えるやつ

# Dropout 
model.add(Dropout(0.25))

3層目(と数えるのか? poolingが3層目?)以降も1~2層目と同じ感じ

model.add(Conv2D(64, (3, 3), padding='same'))
model.add(Activation('relu'))
model.add(Conv2D(64, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

つぎにFlattenを挟んでいる これは (50000, 32, 32, 3) とかを (50000, 32323) みたいな形に変形するやつ

model.add(Flatten())

フラットにした出力を512個のノードを持つ層に全連結する

model.add(Dense(512))
model.add(Activation('relu'))
model.add(Dropout(0.5))

最後に、10種類の分類問題なので10個のノードを持つ層に全連結、活性化関数はsoftmax

model.add(Dense(num_classes))
model.add(Activation('softmax'))

ここまででレイヤの設定は終了

次にオプティマイザの設定

# パラメータの更新に使うオプティマイザを指定
opt = keras.optimizers.rmsprop(lr=0.0001, decay=1e-6)

SGDとかAdagradとかは知ってたけど RMSPropって初めて知った

損失関数とかを設定してコンパイル

model.compile(loss='categorical_crossentropy',
              optimizer=opt,
              metrics=['accuracy'])

画像データは0~255の整数値だけど 0~1.0の実数にしたいので変換

# データの準備
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255

ImageDataGenerator が何かよくわかっていないけど、学習データにノイズをのせたりちょっと変形させたりして学習データを増やすやつ?? あとで調べる

データの拡張をしないなら

model.fit(x_train, y_train,
              batch_size=batch_size,
              epochs=epochs,
              validation_data=(x_test, y_test),
              shuffle=True)

で学習できます

今回は拡張しないでやりました

Google Colaboratory の GPU ランタイムで、およそ1時間ほどで学習が完了しました (結構早い段階でloss, accは変わらなくなってたみたいです)

Train on 50000 samples, validate on 10000 samples
Epoch 1/100
50000/50000 [==============================] - 34s 678us/step - loss: 1.7920 - acc: 0.3452 - val_loss: 1.5589 - val_acc: 0.4344
Epoch 2/100
50000/50000 [==============================] - 33s 669us/step - loss: 1.4828 - acc: 0.4602 - val_loss: 1.3297 - val_acc: 0.5235
Epoch 3/100

...

Epoch 99/100
50000/50000 [==============================] - 33s 667us/step - loss: 0.6290 - acc: 0.7919 - val_loss: 0.7397 - val_acc: 0.7645
Epoch 100/100
50000/50000 [==============================] - 33s 667us/step - loss: 0.6290 - acc: 0.7928 - val_loss: 0.6674 - val_acc: 0.7836
<keras.callbacks.History at 0x7f5460e23a20>

最終的に80%程度の精度で判別できるようになったみたいです

本当に判別できるようになっているのか、確認してみます

model.predict(x_train[0:1])
→ array([[5.1839565e-05, 9.5230262e-06, 4.2473815e-02, 1.3438360e-01,
        1.5156801e-02, 4.1686233e-02, 7.6447803e-01, 1.6840984e-03,
        4.3788212e-05, 3.2197495e-05]], dtype=float32)

y_train[0]
→ array([0., 0., 0., 0., 0., 0., 1., 0., 0., 0.], dtype=float32)

学習データに使ったデータで確認するべきではないとは思いますが、とりあえず最初の画像は 7番目のラベルが 0.764で一番大きな値になっていて、y_train[0]と一致しているようです

model.predict(x_train[1:2])
→ array([[3.8416937e-07, 4.2660031e-05, 4.9953406e-09, 4.7760366e-08,
        1.3378058e-10, 6.4784986e-09, 4.5471082e-11, 1.5476447e-08,
        1.4176493e-06, 9.9995542e-01]], dtype=float32)

y_train[1]
→ array([0., 0., 0., 0., 0., 0., 0., 0., 0., 1.], dtype=float32)

2つ目の画像も10番目のラベルが99.99でy_train[1]と一致していますね

ということでサンプルを実際に動かしてみることができました

次回はこれを応用して独自の画像分類器を作って見たいと思っています。

Keras に入門する 画像分類 Part.0 cifar10 を眺める

Kerasのサンプルを1つ理解 → 似たような問題を解決する何かを作ってみる、という流れでいくつか問題を解いていく、ということをしていきたい

最初は画像分類をしてみる。

サンプルはこれ

github.com

まだデータやコードを見ていないけれど、多分

  • 学習データとして犬とか猫とか車みたいなタグ(多分10種類)がつけられた画像が提供される
  • 画像とタグをもとに学習する
  • 新しい画像が猫なのか、犬なのか、車なのか、... を判別できるシステムを作る

みたいなことをするんだと思う。

なので、10種類くらいの画像の教師データから新規画像が何の画像なのか判別する方法をサンプルを通して学ぶ → 自分で似たようなテーマの問題を設定してそれを解くことができるシステムを作ってみる、ということをやってみる

ということで、今回は提供されるデータがどのような形式なのかを調べてみる

実行は基本的にGoogle Colaboratoryでやっていくつもり

今回は https://colab.research.google.com/drive/1T0-GO8EvEcY4qIeb686LTB9e4iwqUz1V

from keras.datasets import cifar10

data = cifar10.load_data()

data

を実行してみると

((array([[[[ 59,  62,  63],
           [ 43,  46,  45],
           [ 50,  48,  43],
           ...,
           [158, 132, 108],
           [152, 125, 102],
           [148, 124, 103]],
  
          [[ 16,  20,  20],
           [  0,   0,   0],
           [ 18,   8,   0],
           ...,
           [123,  88,  55],
           [119,  83,  50],
           [122,  87,  57]],
  
          [[ 25,  24,  21],
           [ 16,   7,   0],
           [ 49,  27,   8],
           ...,
           [118,  84,  50],
           [120,  84,  50],
           [109,  73,  42]],
  
          ...,

と出てくる

想像だけど、[ 59, 62, 63] とかが1pixelのRGBなんじゃないかと思うけど、それ以上のことがわからない

ので、公式ドキュメントを見てみる

データセット - Keras Documentation

from keras.datasets import cifar10

(x_train, y_train), (x_test, y_test) = cifar10.load_data()

戻り値:
2つのタプル:
x_train, x_test: shape (num_samples, 3, 32, 32)または(num_samples, 32, 32, 3)のRGB画像データのuint8配列です.これはバックエンド設定のimage_data_formatがchannels_firstとchannels_lastのいずれなのかによって決まります.
y_train, y_test: shape (num_samples,) のカテゴリラベル(0-9の範囲の整数)のuint8配列.

とのことだ

なので、x_train, y_train, x_test, y_test に分割してみる

(x_train, y_train), (x_test, y_test) = data


len(x_train)

→ 50000

len(y_train)

→ 50000

多分学習用に50000枚の画像があって、x_trainには画像データが、y_trainにはその正解のタグが入ってるんだと思う

np.array(x_train).shape

→ (50000, 32, 32, 3)

画像は32x32のRGB画像なんだと思う とりあえず表示してみよう

f:id:uhiaha888:20180928210141p:plain

...1枚目が何なのかよくわからないけど、2枚目は多分トラックだし多分データの構造の解釈としては間違ってないだろう

これに対して、

np.array(y_train).shape

→ (50000, 1)

なので y_train は各画像のラベルを表すベクトルなんだろう

1枚目がなんなのか気になるので同じラベルがつけられている画像をいくつか見てみる

y_train[0]

→ [6]

np.where( y_train == [6] )

→ (array([    0,    19,    22, ..., 49962, 49966, 49996]),
 array([0, 0, 0, ..., 0, 0, 0]))

なんか思っていたのと違う(1つのarrayが返されると思っていたけど2個返ってきた)けど、とりあえず0, 19, 22, ... 49962, 49966, 49996は多分同じ種類の画像なんだろうからそれらを見てみる

f:id:uhiaha888:20180928211512p:plain

なるほど、カエルだ

ついでにx_test, y_testも一応確認しておこう

np.array(x_test).shape

→ (10000, 32, 32, 3)

np.array(y_test).shape

→ (10000, 1)

ということで、今回のサンプルは32x32のRGB画像とそのラベルになっているんだということがわかった

次回はサンプルを実際に動かしてこれらの画像を分類してみたい

Tensorflow: Threading and Queues 和訳

QueueはTensorFlowを利用した非同期処理のための強力なメカニズムである。

TensorFlowのその他の機能と同じように、queueはTensorFlowのグラフのノードである。 これは変数のように状態をもつノードであり、他のノードがその状態を変更することができる。 特に、他のノードがキューにエンキューしたり、キューからデキューすることができる。

queueについて理解するために、簡単な例について考えてみる。 "first in, first out"なqueueを作り、そのキューを0で満たすとする。 そして、queueから1つ取り出し、取り出したものに1を加え、またqueueに追加する、という計算グラフを考える。 これは、queueの要素を少しずつ増やしていく。

https://www.tensorflow.org/versions/r1.1/images/IncremeterFifoQueue.gif

Enqueue, Enqueue, Dequeueは特別なノードである。 これらは普通の値を受け取らず、変更したいqueueへのポインタを受け取る。 これらはqueueのメソッドのようなものであると考えると良い。 実際、Python APIではqueueオブジェクトのメソッドになっている。

注意 (q.enqueue(...)のような)queueのメソッドはqueueと同じデバイス上で実行しなければならない。

Queue usage overview

tf.FIFOQueueやtf.RandomShuffleQueueのようなqueueは、計算グラフ中で非同期にtensorを計算するのに重要なTensorFlowのオブジェクトである。

例えば、典型的な入力アーキテクチャは、RandomShuffleQueueをモデルを学習する入力を準備するのに利用する

  • 複数のスレッドが学習データを準備し、それをqueueに追加する
  • 学習を実行しているスレッドがqueueからミニバッチを取得する

このようなアーキテクチャは多くの利点がある。 詳細は Reading data  |  TensorFlow を参照のこと。

TensorFlowのSessionオブジェクトはマルチスレッドなので、複数のスレッドで同一のsessionを利用したり、処理を並列に実行したりすることが用意にできる。 しかし、上記のようなスレッドを実行するプログラムをpythonで実装するのは必ずしも簡単ではない。 すべてのスレッドは一緒に停止することができなければならないし、例外がキャッチできなければならない、そして、queueは停止する際に適切にcloseされなければならない。

TensorFlowはこれらを手助けするためにtf.train.Coordinatorとtf.train.QueueRunnerというクラスを提供している。 これらの2つのクラスは一緒に使うようにデザインされている。 Coordinatorクラスは複数のスレッドを一緒に停止したり、スレッドが停止するのを待っているプログラムに例外を渡すのを手助けする。 QueueRunnerクラスは1つのqueueにenqueueする複数のスレッドを作成するのに利用する。

Coordinator

Coordinatorクラスは複数のスレッドを一緒に停止させるのを手助けする。

主要なメソッドは下記

  • tf.train.Coordinator.should_stop: スレッドが停止すべきなときTrueを返す
  • tf.train.Coordinator.request_stop: スレッドが停止すべきであることを伝える
  • tf.train.Coordinator.join: 指定したスレッドたちが停止するまで待つ

まずCoordinatorオブジェクトを作成し、coordinatorを利用するスレッドを複数作成する。 これらのスレッドは通常、should_stop()がtrueを返したら停止するようなループを実行する。

どのスレッドも計算を停止すべきかどうかを決めることができる。 request_stop()を呼び出すだけでよく、他のスレッドはshould_stop()がTrueを返すようになるので停止する

# Thread body: loop until the coordinator indicates a stop was requested.
# If some condition becomes true, ask the coordinator to stop.
def MyLoop(coord):
  while not coord.should_stop():
    ...do something...
    if ...some condition...:
      coord.request_stop()

# Main thread: create a coordinator.
coord = tf.train.Coordinator()

# Create 10 threads that run 'MyLoop()'
threads = [threading.Thread(target=MyLoop, args=(coord,)) for i in xrange(10)]

# Start the threads and wait for all of them to stop.
for t in threads:
  t.start()
coord.join(threads)

明らかに、coordinatorは全く異なる処理を行うスレッドたちを管理することができる。 上記の例のように、すべてのスレッドが同じことをする必要はない。 coordinatorは例外の捕捉とレポートも可能。 詳細は tf.train.Coordinator  |  TensorFlow を参照のこと。

QueueRunner

QueueRunnerクラスはenqueueオペレーションを繰り返し実行する複数のスレッドを生成する。 これらのスレッドは停止するのにcoordinatorを利用できる。 さらに、queue runnerはcloser threadを実行できる。 これは、coordinatorに例外がレポートされるとqueueを自動的にcloseする。

上記のようなアーキテクチャを実装するのにqueue runnerが利用できる。

まず、(tf.RandomShuffleQueueなどの)TensorFlowのqueueを例えばinputに使うような計算グラフを作成する。 入力データを処理しqueueにenqueueするようなオペレーションを追加する。 queueからdequeueし学習を実行するようなオペレーションを追加する。

example = ...ops to create one example...
# Create a queue, and an op that enqueues examples one at a time in the queue.
queue = tf.RandomShuffleQueue(...)
enqueue_op = queue.enqueue(example)
# Create a training graph that starts by dequeuing a batch of examples.
inputs = queue.dequeue_many(batch_size)
train_op = ...use 'inputs' to build the training part of the graph...

Pythonのtraining programでは、いくつかのexampleをenqueueするスレッドを作成するQueueRunnerを作成している。 Coordinatorを作りqueue runnerにcoordinatorを利用してスレッドを開始するよう依頼している。

# Create a queue runner that will run 4 threads in parallel to enqueue
# examples.
qr = tf.train.QueueRunner(queue, [enqueue_op] * 4)

# Launch the graph.
sess = tf.Session()
# Create a coordinator, launch the queue runner threads.
coord = tf.train.Coordinator()
enqueue_threads = qr.create_threads(sess, coord=coord, start=True)
# Run the training loop, controlling termination with the coordinator.
for step in xrange(1000000):
    if coord.should_stop():
        break
    sess.run(train_op)
# When done, ask the threads to stop.
coord.request_stop()
# And wait for them to actually do it.
coord.join(enqueue_threads)

Handling exceptions

queue runnerによって開始されたスレッドは単にenqueueオペレーションを実行するだけではない。 これらのスレッドは、queueによって投げられた例外のキャッチとハンドリングができる。 tf.errors.OutOfRangeErrorの例外は、queueがcloseしたことを通知するのに利用される。

coordinatorを利用するtraining programはメインのループ中で例外のキャッチとレポートをする必要がある。

上記の例を改善した例が下記。

try:
    for step in xrange(1000000):
        if coord.should_stop():
            break
        sess.run(train_op)
except Exception, e:
    # Report exceptions to the coordinator.
    coord.request_stop(e)
finally:
    # Terminate as usual. It is safe to call `coord.request_stop()` twice.
    coord.request_stop()
    coord.join(threads)

rnnlmを作ってみる その2 誤差について

入力された単語に対して何が出力されるのか、ということは前回やりました。

今回は、rnnlmの学習でどのような誤差を評価するかについてです。

早速ですが、tensorflowのチュートリアル Recurrent Neural Networks  |  TensorFlow では下記のように書かれています

We want to minimize the average negative log probability of the target words:

{ \displaystyle   loss = - \frac{1}{N} \sum^{N}_{i = 1} \ln p_{target_i} }

N は文(書)中の単語の数、 { \displaystyle  p_{target_i} } は i番目の出力( これは語彙がM個だったらM個の単語それぞれが次に出現する確率となっている) のうちの正解の単語に対応する要素となります

学習データが、"今日 は 良い 天気 ですね" で、学習が "今日 は 良い" まで進んだとき、rnnlmの出力としては各単語の出現確率になるので、語彙が [今日, 昨日, は, 良い, 天気, ですね, カレーライス] だけであれば、これと同じ長さのベクトル(的なもの)が出力されるはずです。 これが [0.1, 0.1, 0.1, 0.1, 0.4, 0.1, 0.1] だとします。 学習データで "今日 は 良い" のあとに続くのは"天気" なので { \displaystyle  p_{target_i} } は この例だと0.4 となる、みたいな感じです。

チュートリアル本文ではこの計算は sequence_loss_by_example でできる、と書いてますがdeprecatedだったので少し前に tf.contrib.seq2seq.sequence_loss を利用するように書き換えられたようです。

チュートリアルのサンプルコードでこの誤差を計算しているのが下記です。

models/ptb_word_lm.py at master · tensorflow/models · GitHub

    # Use the contrib sequence loss and average over the batches
    loss = tf.contrib.seq2seq.sequence_loss(
        logits,
        input_.targets,
        tf.ones([self.batch_size, self.num_steps], dtype=data_type()),
        average_across_timesteps=False,
        average_across_batch=True)

これで、理論的なところについてはだいたい準備できた感じだと思います。

次回から実装を進めていきます。

rnnlmを作ってみる その1 定義とか

深層学習による自然言語処理 の本によれば、

"言語モデル(Language model: LM)あるいは確率的言語モデル (probabilistic language model)とは、人間が扱う自然言語で書かれた文や文書が生成される確率をモデル化したものです"

となっています。

例えば、"今日はいい天気ですね"という文はよくある表現ですが、"今日はいい確率モデルですね"とは文脈によっては言うことは無いとは言えませんが最初の例よりは生成されにくいでしょうし、"カレーライスがいい確率モデルですね"などはわけがわからないですね。

このように、"文の自然さ"とか"文がよくありそうか"みたいなものはそれぞれ異なるのでそういったものを"文や文書が生成される確率"として表すのが言語モデルなんだと思います。

この場合、 P(今日はいい天気ですね) > P(今日はいい確率モデルですね) > P(レーライスがいい確率モデルですね) みたいな感じになるのだと思います。

{ \displaystyle {\bf y_t} } を単語とし、 { \displaystyle {\bf Y = \left( y_0, y_1, y_2, ... , y_T, y_{T+1} \right) } } が文を表すものとします。

{ \displaystyle {\bf y_t} } はone-hot ベクトルで表されているとします。

また、{ \displaystyle {\bf y_0} } { \displaystyle {\bf y_{T+1}} } はそれぞれ文頭と文末を表す擬似的な単語(BOSとEOS)とします。

再帰ニューラルネットワークを利用した言語モデル(recurrent neural network language model; rnnlm)は下記の様に表せます。

{ \displaystyle P_{rnnlm} ({\bf Y}) = \prod_{t=1}^{T+1} P(  {\bf y}_t | {\bf Y}_{\lbrack 0, t-1 \rbrack }) }

ここで { \displaystyle {\bf Y}_{\lbrack 0, t-1 \rbrack }   }{ \displaystyle {\bf Y = \left( y_0, y_1, y_2, ... , y_{t-1} \right) } } を表すものとします。

{ \displaystyle  P(  {\bf y}_t | {\bf Y}_{\lbrack 0, t-1 \rbrack }) } の計算は下記で行うとのことです。 (1層、活性化関数はtanhとした場合)

埋め込みベクトルの取得

{ \displaystyle \overline{{\bf y}}_t = {\bf E y}_{t-1} }

隠れ層の計算

{ \displaystyle {\bf h}_t = \tanh \left( {\bf W}^{(l)}  \begin{bmatrix}   \overline{{\bf y}}_t  \\  {\bf h}_{t-1}  \end{bmatrix}  + {\bf b}^{(l)}  \right)       }

出力層の計算

{ \displaystyle {\bf o}_t = {\bf W}^{(o)} {\bf h}_t + {\bf b}^{(o)}   }

確率化

{ \displaystyle {\bf p}_t = softmax ({\bf o}_t) }

確率の抽出

{ \displaystyle P(  {\bf y}_t | {\bf Y}_{\lbrack 0, t-1 \rbrack }) = {\bf p}_t \cdot {\bf y}_t  }

単語を入力するたびに、 { \displaystyle P(  {\bf y}_t | {\bf Y}_{\lbrack 0, t-1 \rbrack })   } を計算していき、その結果をすべてかけ合わせたものが { \displaystyle P_{rnnlm} ({\bf Y}) } となります

この出力からなんらかの誤差関数を評価してそれを最小化するようにEとかWを調整していくことになるのですが、それは次回書きます。