人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

2016年の振り返り

年末なので年始に立てた目標(2016/1/1)の振り返りでもしようかな。

  • Deep Learning Tutorialの読破とTheanoでの実装を続ける
  • パターン生成・コンテンツ生成(2015/12/30)に関する従来研究のサーベイ
  • × 深層強化学習(Deep Reinforcement Learning)の理解
  • × 1週間に1本は論文を熟読する
  • TensorFlowをさわってみる
  • C­omputer vision: models, learning and inference(2015/12/31)の読破
  • × 情報理論の勉強
  • 多様体学習(manifold learning)の勉強
  • × Sparse Codingの勉強
  • × 上記に関係する数学の習得
  • Python3への移行
  • MOT・イノベーション・マーケティング関係の本を月1冊以上読む
  • × 英語ライティングを鍛える

あれ(^^;; 今年はダメダメ。ちゃんと意識してやってたのはPython3への切り替えとビジネス書読むのくらい?目標未達が多いのは4月から本業がちょっと変わって必要な勉強を優先していたためなのだ。まあすっごく言い訳がましいんだけどさ(^^;;

今年やったことをちょっとまとめておこう。

MEANスタック

JavaScriptとMEAN(MongoDBExpressAngularJSNode.js)とBootstrapの勉強をしていた。Webアプリケーション開発はあまり経験がなかったんだけどやってみるとけっこう楽しくてのめり込んでいた。あとAtomエディタでも使われているElectronという技術も勉強していた。JavaScriptでローカルアプリ作れるなんてすごい時代だな!

ここ二ヶ月はCode Schoolを受講していた。月額29$で高いんだけど動画チュートリアルがよくできていてやっていて楽しい。お金を節約するために早く制覇したい。

今年は人工知能関係のWeb API(機械翻訳、物体認識、感情認識、音声認識、音声合成、対話など)が国内でも海外でもたくさん公開された年だった。来年はこういうAPIの使い方も紹介できたらいいな。アイデア次第で面白いことがいろいろできそう!

Keras

今年はTheanoではなく、Keras(2016/3/28)というライブラリでDeep Learningの勉強をちまちましていた。keras_examplesというGithubのレポジトリを作ってこの中でいろいろ実験している。TODOはIssuesにまとめている。ブログにまとめるのが追いつかないけど来年もちまちま続けていきたい。

EmacsからAtomへ

EmacsからAtomに完全に切り替えた。AWSなどで遠隔ログインする場合もAtomのRemoteFTPというパッケージを使うようにしている。最初はPyCharmを課金して使ってたんだけどPythonだけでなくC言語やJavaScriptも頻繁に書くようになったので最終的にAtomに決めた。今のところまったく不満がない。

その他

  • 沖縄の宮古島を旅行してきた。がっつりした旅行は8年ぶり(^^;; くらいかも。台風がぎりぎりそれたおかげで青い海と満点の星空が見られた。少し疲れたけどとても楽しかったな。

  • 10月から自動車教習所へ通い始めた。学生の頃はバイト代をパソコンと本につぎ込んでいたのでお金がなくて、就職したら時間がなくて今さら(^^;; 土日だけで通っているけどけっこう順調で仮免許まで取れた。こんなに運転が楽しいんだと完全無人運転は流行らないかもね(笑)

  • HuluでGame of Thronesっていう歴史ファンタジー系の海外ドラマにすごくはまっていた。ドラマがすごく面白かったので原作(七王国の玉座)もKindleで買って読み始めた。

  • SteamはWitcher 3、Fallout 4、Dark Souls 3、Hearts of Iron 4、Civilization 6あたりをプレイしていた。時間がないとブログをサボっている割には有名どころは一通り抑えているという(笑)

あれ?こう書くとけっこう楽しいことばっかりだったみたい。ただ実感としては心が折れそうなことばかり続いていた。来年は本当に折れそう(^^;; だけどなんとか前向きに生きていきたい。まあ折れそうだけどなかなか折れないものだなと思った一年だった。

Kerasによるデータ拡張

今回は、画像認識の精度向上に有効な データ拡張(Data Augmentation) を実験してみた。データ拡張は、訓練データの画像に対して移動、回転、拡大・縮小など人工的な操作を加えることでデータ数を水増しするテクニック。画像の移動、回転、拡大・縮小に対してロバストになるため認識精度が向上するようだ。

音声認識でも訓練音声に人工的なノイズを上乗せしてデータを拡張するテクニックがあるらしいのでそれの画像版みたいなものだろう。

ソースコード

ImageDataGenerator

Kerasには画像データの拡張を簡単に行うImageDataGeneratorというクラスが用意されている。今回は、この使い方をまとめておきたい。ドキュメントを調べるとこのクラスにはパラメータが大量にあって目が回る。一気に理解するのは難しいので一つずつ検証しよう。

keras.preprocessing.image.ImageDataGenerator(featurewise_center=False,
    samplewise_center=False,
    featurewise_std_normalization=False,
    samplewise_std_normalization=False,
    zca_whitening=False,
    rotation_range=0.,
    width_shift_range=0.,
    height_shift_range=0.,
    shear_range=0.,
    zoom_range=0.,
    channel_shift_range=0.,
    fill_mode='nearest',
    cval=0.,
    horizontal_flip=False,
    vertical_flip=False,
    rescale=None,
    dim_ordering=K.image_dim_ordering())

まずは、適当な画像を1枚だけ入力し、各パラメータを1つだけ指定してどのような画像が生成されるか確認してみた。

if __name__ == '__main__':
    # 画像をロード(PIL形式画像)
    img = load_img(IMAGE_FILE)

    # numpy arrayに変換(row, col, channel)
    x = img_to_array(img)
    # print(x.shape)

    # 4次元テンソルに変換(sample, row, col, channel)
    x = np.expand_dims(x, axis=0)
    # print(x.shape)

    # パラメータを一つだけ指定して残りはデフォルト
    datagen = ImageDataGenerator(rotation_range=90)

    # 生成した画像をファイルに保存
    draw_images(datagen, x, "result_rotation.jpg")

draw_images()は、ジェネレータオブジェクト、画像の4次元テンソル、出力ファイル名を指定すると生成した画像をファイルに描画する自作関数。

def draw_images(datagen, x, result_images):
    # 出力先ディレクトリを作成
    temp_dir = "temp"
    os.mkdir(temp_dir)

    # generatorから9個の画像を生成
    # xは1サンプルのみなのでbatch_sizeは1で固定
    g = datagen.flow(x, batch_size=1, save_to_dir=temp_dir, save_prefix='img', save_format='jpg')
    for i in range(9):
        batch = g.next()

    # 生成した画像を3x3で描画
    images = glob.glob(os.path.join(temp_dir, "*.jpg"))
    fig = plt.figure()
    gs = gridspec.GridSpec(3, 3)
    gs.update(wspace=0.1, hspace=0.1)
    for i in range(9):
        img = load_img(images[i])
        plt.subplot(gs[i])
        plt.imshow(img, aspect='auto')
        plt.axis("off")
    plt.savefig(result_images)

    # 出力先ディレクトリを削除
    shutil.rmtree(temp_dir)

next()を呼び出すたびにbatch_size個の画像が生成される。今回は、入力画像が1枚の画像なのでbatch_sizeは1にしている。生成した9枚の画像は3x3のグリッドに配置し、ファイルに保存する。

早速やってみよう。入力画像はなんでもいいけど下のを入れた。ぞうさんかわいいね。

f:id:aidiary:20161212214844j:plain:w300

rotation_range

画像を指定角度の範囲でランダムに回転する。目が回るゾウ。

datagen = ImageDataGenerator(rotation_range=90)
draw_images(datagen, x, "result_rotation.jpg")

f:id:aidiary:20161212215418j:plain:w500

width_shift_range

画像を水平方向にランダムに移動する。

datagen = ImageDataGenerator(width_shift_range=0.2)
draw_images(datagen, x, "result_width_shift.jpg")

f:id:aidiary:20161212215510j:plain:w500

height_shift_range

画像垂直方向にランダムに移動する。

datagen = ImageDataGenerator(height_shift_range=0.2)
draw_images(datagen, x, "result_height_shift.jpg")

f:id:aidiary:20161212215528j:plain:w500

shear_range

シアー変換をかける。Wikipediaをざっと見たが斜めに引き延ばすような画像変換みたい。

datagen = ImageDataGenerator(shear_range=0.78)  # pi/4
draw_images(datagen, x, "result_shear.jpg")

f:id:aidiary:20161212215555j:plain:w500

zoom_range

画像をランダムにズームする。

datagen = ImageDataGenerator(zoom_range=0.5)
draw_images(datagen, x, "result_zoom.jpg")

f:id:aidiary:20161212215945j:plain:w500

channel_shift_range

画像のチャンネルをランダムに移動する。RGBの値をランダムに加えるのかな?要調査。

datagen = ImageDataGenerator(channel_shift_range=100)
draw_images(datagen, x, "result_channel_shift.jpg")

f:id:aidiary:20161212220056j:plain:w500

horizontal_flip

画像を水平方向にランダムに反転する。

datagen = ImageDataGenerator(horizontal_flip=True)
draw_images(datagen, x, "result_horizontal_flip.jpg")

f:id:aidiary:20161212220144j:plain:w500

vertical_flip

画像を垂直方向にランダムに反転する。

datagen = ImageDataGenerator(vertical_flip=True)
draw_images(datagen, x, "result_vertical_flip.jpg")

f:id:aidiary:20161212220200j:plain:w500

samplewise_center

サンプル平均を0にする。平均の正規化なんだろうけど平均0にしてimshow()しても大丈夫なのかな?imshow()でとりあえず画像出たけどあとで仕様を確認。

datagen = ImageDataGenerator(samplewise_center=True)
draw_images(datagen, x, "result_samplewise_center.jpg")

f:id:aidiary:20161212220405j:plain:w500

samplewise_std_normalization

サンプルを標準偏差で割る。画像はなんか真っ黒になっちゃった。上のと組み合わせると平均0、標準偏差1になる正規化をかけられるようだ。これもアルゴリズムを要確認。

datagen = ImageDataGenerator(samplewise_std_normalization=True)
draw_images(datagen, x, "result_samplewise_std_normalization.jpg")

f:id:aidiary:20161212220649j:plain:w500

ZCA白色化(ZCA whitening)

ZCA白色化の詳細はまだよくわかっていない。この変換は一枚の画像に対して適用するのではなく、画像集合に対して適用するようだ。なので入力は一枚の画像xではなく、X_trainなど画像集合を渡す必要がある。また、ZCA白色化を使うときはdatagen.fit()を使ってあらかじめ統計量を計算しておく必要があるので注意。試しにCIFAR-10の訓練データ画像に対してZCA白色化を適用し、適用後の画像がどうなるか調べた。

img_rows, img_cols, img_channels = 32, 32, 3
batch_size = 16
nb_classes = 10

# CIFAR-10データをロード
(X_train, y_train), (X_test, y_test) = cifar10.load_data()

# 画素値を0-1に変換
X_train = X_train.astype('float32')
X_train /= 255.0

draw(X_train[0:batch_size], 'zca_whitening_before.png')

# データ拡張
datagen = ImageDataGenerator(zca_whitening=True)

datagen.fit(X_train)
g = datagen.flow(X_train, y_train, batch_size, shuffle=False)
X_batch, y_batch = g.next()
print(X_batch.shape)
print(y_batch.shape)

draw(X_batch, 'zca_whitening_after.png')

適用前: f:id:aidiary:20161212220750p:plain:w500

適用後: f:id:aidiary:20161212220801p:plain:w500

訓練データに加えてこれらのデータ拡張で生成した画像を畳み込みニューラルネットに食わせると精度向上が期待できるようだ。長くなったので畳み込みニューラルネットワークとデータ拡張を組み合わせた実験は次回にまわそう。

参考

KerasでCIFAR-10の一般物体認識

今回は、畳み込みニューラルネットを使ってCIFAR-10(2015/10/14)の一般物体認識をやってみた。以前、Chainerでやった(2015/11/8)のをKerasで再実装した。

これもKerasの例題に含まれている。このスクリプトでは、データ拡張(Data Augmentation)も使っているがこれはまた別の回に取り上げよう。

ソースコード:cifar10.py

CIFAR-10

CIFAR-10は32x32ピクセル(ちっさ!)のカラー画像のデータセット。クラスラベルはairplane, automobile, bird, cat, deer, dog, frog, horse, ship, truckの10種類で訓練用データ5万枚、テスト用データ1万枚から成る。

まずは描画してみよう。

import numpy as np
import matplotlib.pyplot as plt
from scipy.misc import toimage
from keras.datasets import cifar10

if __name__ == '__main__':
    # CIFAR-10データセットをロード
    (X_train, y_train), (X_test, y_test) = cifar10.load_data()
    print(X_train.shape, y_train.shape)
    print(X_test.shape, y_test.shape)

    # 画像を描画
    nclasses = 10
    pos = 1
    for targetClass in range(nclasses):
        targetIdx = []
        # クラスclassIDの画像のインデックスリストを取得
        for i in range(len(y_train)):
            if y_train[i][0] == targetClass:
                targetIdx.append(i)

        # 各クラスからランダムに選んだ最初の10個の画像を描画
        np.random.shuffle(targetIdx)
        for idx in targetIdx[:10]:
            img = toimage(X_train[idx])
            plt.subplot(10, 10, pos)
            plt.imshow(img)
            plt.axis('off')
            pos += 1

    plt.show()

f:id:aidiary:20161127184014p:plain

以前は、CIFAR-10のホームページから直接ダウンロードしたが、Kerasではkeras.datasets.cifar10モジュールを使えば勝手にダウンロードして使いやすい形で提供してくれる。

# 入力画像の次元
img_rows, img_cols = 32, 32

# チャネル数(RGBなので3)
img_channels = 3

# CIFAR-10データをロード
# (nb_samples, nb_rows, nb_cols, nb_channel) = tf
(X_train, y_train), (X_test, y_test) = cifar10.load_data()

# ランダムに画像をプロット
plot_cifar10(X_train, y_train, result_dir)

# 画素値を0-1に変換
X_train = X_train.astype('float32')
X_test = X_test.astype('float32')
X_train /= 255.0
X_test /= 255.0

# クラスラベル(0-9)をone-hotエンコーディング形式に変換
Y_train = np_utils.to_categorical(y_train, nb_classes)
Y_test = np_utils.to_categorical(y_test, nb_classes)

X_trainは (50000, 32, 32, 3) の4次元テンソルで与えられる(image_dim_orderingtfのとき)。画像が50000枚、行数が32、列数が32、チャンネルが3(RGB)であることを意味する。配列には0-255の画素値が入っているため255で割って0-1に正規化する。y_trainは (50000, 1) の配列で与えられる。クラスラベルは0-9の値が入っているのでNNで使いやすいようにone-hotエンコーディング形式に変換する。

CNNの構築

少し層が深いCNNを構成してみた。

INPUT -> ((CONV->RELU) * 2 -> POOL) * 2 -> FC

畳み込み層(CONV)、ReLU活性化関数(RELU)を2回繰り返してプーリング層(POOL)を1セットとしてそれを2セット繰り返した後に全結合層(FC)を通して分類するという構成。

# CNNを構築
model = Sequential()

model.add(Convolution2D(32, 3, 3, border_mode='same', input_shape=X_train.shape[1:]))
model.add(Activation('relu'))
model.add(Convolution2D(32, 3, 3))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

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

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

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

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

# モデルのサマリを表示
model.summary()
plot(model, show_shapes=True, to_file=os.path.join(result_dir, 'model.png'))

モデルの訓練

今回は、Early-stoppingは使わずに固定で100エポック回した。Early-stoppingを使うとすぐに収束扱いされてしまったため。patienceを変更すればいいんだけど使いどころがちょっと難しいかも。

# 訓練
history = model.fit(X_train, Y_train,
                    batch_size=batch_size,
                    nb_epoch=nb_epoch,
                    verbose=1,
                    validation_split=0.1)

# 学習履歴をプロット
plot_history(history, result_dir)

損失と精度をプロットすると下のような感じ。validation dataによるテスト損失は30エポックくらいからほとんど変化ないのでもっと早く止めてもよかったかも。10エポックごとに学習途中のモデルを保存しておきたいところだけどどうやるのかな?callbackで実装できるのだろうか。

f:id:aidiary:20161127184154p:plain:w320 f:id:aidiary:20161127184158p:plain:w320

モデルの保存

CNNは学習にものすごい時間がかかる(GPUを使わないと特に)ので学習結果のモデルはファイルに保存するようにした。Kerasではモデルの形状(model.json)と学習した重み(model.h5)を別々に保存するようになっている。PythonなのにJSONを使うところがナウい。h5というのはHDF5 (Hierarchical Data Format)というバイナリフォーマットのようだ。ときどき見かけるけど使ったことがなかった。

# 学習したモデルと重みと履歴の保存
model_json = model.to_json()
with open(os.path.join(result_dir, 'model.json'), 'w') as json_file:
    json_file.write(model_json)
model.save_weights(os.path.join(result_dir, 'model.h5'))

ちなみにファイルからモデルを読み込むときは

model_file = os.path.join(result_dir, 'model.json')
weight_file = os.path.join(result_dir, 'model.h5')

with open(model_file, 'r') as fp:
    model = model_from_json(fp.read())
model.load_weights(weight_file)
model.summary()

とすればよい。

モデルの評価

テストデータで評価すると 約80% の分類精度が得られた。

# モデルの評価
loss, acc = model.evaluate(X_test, Y_test, verbose=0)
print('Test loss:', loss)
print('Test acc:', acc)

次回は画像データの拡張についてまとめたい。