読者です 読者をやめる 読者になる 読者になる

人工知能に関する断創録

人工知能、認知科学、心理学、ロボティクス、生物学などに興味を持っています。このブログでは人工知能のさまざまな分野について調査したことをまとめています。最近は、機械学習、Deep Learning、Kerasに関する記事が多いです。



畳み込みニューラルネットワークの可視化

Deep Learning Keras コンピュータビジョン

Deep Learningの学習結果(重み)はブラックボックスで、隠れ層のユニット(特に深い層の!)が一体何を学習したのかがよくわからないと長年言われてきた。しかし、今回紹介する方法を使うとニューラルネットが何を学習したのか目で見える形で表現できる。

畳み込みニューラルネットで学習したフィルタの可視化というと以前やったように学習した第1層のフィルタの重みを直接画像として可視化する方法がある。

しかし、畳み込みフィルタのサイズは基本的に数ピクセル(MNISTの例では5x5ピクセル程度)のとても小さな画像なのでこれを直接可視化しても何が学習されたか把握するのはとても難しい。たとえば、MNISTを学習した畳み込みニューラルネット(2016/11/20)のフィルタを可視化しても各フィルタがどの方向に反応しやすいかがわかる程度だ。

f:id:aidiary:20161120200900p:plain:w300

各フィルタが何を学習したかを可視化する別のアプローチとして各フィルタの出力を最大化するような入力画像を生成する手法が提案された。この生成画像はニューラルネットの入力画像と同じサイズなのでフィルタが最終的に何を学習したのかより把握しやすいというメリットがある。

今回は畳み込みニューラルネットの一種であるVGG16を対象に学習したフィルタを可視化してみたい。あとでMNISTやCIFAR-10を学習したCNNや最新のResNetでもやってみるつもり。

基本的に本家のKeras Blogの How convolutional neural networks see the world を参考にした。しかし、この記事は

  • keras.applications.vgg16モジュールが導入される前に書かれている
  • 正則化などの工夫が入っておらず生成される画像が美しくない

という問題がある。

そこでこの記事ではKerasのvgg16モジュールを使って書き換えるとともに、よりきれいな画像が生成されるようにいくつか工夫した。まあ少しは差別化しないとね(笑)

リポジトリ:dream

ナイーブな方法

基本的なアプローチは、畳み込みニューラルネットの指定したフィルタの出力を最大にする入力画像を勾配法を用いて更新することである。数式で書くと

 x \leftarrow x + \eta \frac{\partial a_i (x)}{\partial x}

となる。ここで、 xは入力画像、 \etaは学習率、 a_i (x)は画像 xを入力したときのi番目のフィルタの出力(activation)だ。

ここで、偏微分がいわゆる勾配を表し、入力画像 xをちょっと変えたときにフィルタの出力 a_i (x)がどれくらい変化するかを表している。入力画像 xをこの勾配方向に徐々に移動する勾配上昇法(gradient ascent)を用いてフィルタの出力最大化する入力画像を求めている。

この式はニューラルネットの重みの更新式と比較するとわかりやすい。ニューラルネットの重みの更新式は

 w \leftarrow w - \eta \frac{\partial L(w)}{\partial w}

これはよく目にする式だ。ここで、 wは重み、 \etaは学習率、 L(w)は損失だ。

ここで、偏微分は勾配を表し、重み wをちょっと変えたときに損失 L(w)がどれくらい変化するかを表している。重み wをこの勾配の負の方向に徐々に移動する勾配降下法(gradient descent)を用いて損失最小化する重みを求めている。

こんな感じで比較してみると両者の基本原理はよく似ていることがわかる。

ニューラルネットの重み更新では重みの初期値としてランダムな値を使った。フィルタの可視化でも同様に入力画像の初期値として下記のようなランダムな画像を使う。一方、次回紹介するDeep Dreamはこの画像の初期値として任意の画像を使う。

f:id:aidiary:20170215230458p:plain:w300

Kerasで書くと下のようになる。

def visualize_filter(layer_name, filter_index, num_loops=200):
    """指定した層の指定したフィルタの出力を最大化する入力画像を勾配法で求める"""
    # 指定した層の指定したフィルタの出力の平均
    activation_weight = 1.0
    if layer_name == 'predictions':
        # 出力層だけは2Dテンソル (num_samples, num_classes)
        activation = activation_weight * K.mean(layer.output[:, filter_index])
    else:
        # 隠れ層は4Dテンソル (num_samples, row, col, channel)
        activation = activation_weight * K.mean(layer.output[:, :, :, filter_index])

    # 層の出力の入力画像に対する勾配を求める
    # 入力画像を微小量変化させたときの出力の変化量を意味する
    # 層の出力を最大化したいためこの勾配を画像に足し込む
    grads = K.gradients(activation, input_tensor)[0]

    # 正規化トリック
    # 画像に勾配を足し込んだときにちょうどよい値になる
    grads /= (K.sqrt(K.mean(K.square(grads))) + K.epsilon())

    # 画像を入力して層の出力と勾配を返す関数を定義
    iterate = K.function([input_tensor], [activation, grads])

    # ノイズを含んだ画像(4Dテンソル)から開始する
    x = np.random.random((1, img_height, img_width, 3))
    x = (x - 0.5) * 20 + 128

    # 勾配法で層の出力(activation_value)を最大化するように入力画像を更新する
    cache = None
    for i in range(num_loops):
        activation_value, grads_value = iterate([x])
        # activation_valueを大きくしたいので画像に勾配を加える
        step, cache = rmsprop(grads_value, cache)
        x += step
        print(i, activation_value)

    # 画像に戻す
    img = deprocess_image(x[0])

    return img

この関数の二つの引数は畳込みニューラルネットのどの層の何番目のフィルタを最大化したいかを指定する。畳み込みニューラルネット内の畳込み層または出力層を指定できる。たとえば、出力層の65番目のフィルタを最大化したいときは下のように指定する。

visualize_filter('predictions', 65)

層の名前はmodel.summary()で表示される。predictionsVGG16の出力層に付けられた名前である。VGG16の出力層は1000ユニットから成るが65番目のユニットはsea snake(ウミヘビ)クラスを意味する。1個だけではつまらないので16個まとめて画像化すると下のようになる。左上にフィルタ番号に対応するクラス名を表示した。

f:id:aidiary:20170215230857p:plain

まあこの可視化画像を見てもはっきり言ってよくわからない。ウミヘビは何かウネウネしてるぞ、ゴリラに何か目があるぞとかそれくらい(笑)

でもこれらの画像をニューラルネットに入力するとウミヘビである確率が99%、ゴリラである確率が99%ととんでもない結果が出てくる・・・人間にはまったくウミヘビやゴリラと認識できない画像なのに、ニューラルネットは99%の確率でウミヘビ・ゴリラと判定してしまうのだ。ここら辺は参考文献で挙げた「ニューラルネットは簡単にだませる」という論文を参照。

このナイーブな方法ではあまりはっきりとした画像が生成できなかったが、いくつかの改良を加えるとよりはっきりとした鮮やかな画像が生成できることが知られている。そのために自然な画像らしさを表す事前知識(natural image priors)を正則化項として導入すればよい。

VGG16の出力をsoftmaxからlinearに

正則化項を導入する前に一つ重要な改善を入れよう。VGG16の出力層の出力は分類のためにsoftmax活性化関数を適用しており、1000クラスの合計が1.0になるように正規化されている。この場合、あるクラスの出力を最大化するためには (1) そのクラスの出力を上げる以外に、(2) 他のクラスの出力を下げるという方法もとれてしまう。この依存関係で最適化が惑わされてうまくいかないという指摘がある(Simonyan 2013)。そこで、最初の工夫としてsoftmaxlinearにしてみた。

VGG16のsoftmax活性化関数をかける前の出力(つまり線形出力)が取れれば簡単なのだがKerasにそのようなプロパティは用意されていない。もしVGG16のsoftmax活性化関数が

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

のように独立した層として追加されていれば、一つ前のsoftmaxをかける前の出力は簡単に取れたのだが、VGG16は下のように層ではなく引数として実装されていた。

model.add(Dense(1000, activation='softmax'))

これだとsoftmaxをかける前の出力は取り出せない・・・仕方ないのでKerasのvgg16.pyをコピーして一部修正したVGG16クラスを用意した。121行目だけ下のように書き換えればOK。学習した重みはそのまま使える。

if include_top:
    # Classification block
    x = Flatten(name='flatten')(x)
    x = Dense(4096, activation='relu', name='fc1')(x)
    x = Dense(4096, activation='relu', name='fc2')(x)
    # ここの活性化関数をsoftmaxからlinearに置き換え
    x = Dense(1000, activation='linear', name='predictions')(x)

この工夫を入れてもう一度可視化すると下のようにコントラストが少し強くなる。だけどまだ何が何だかよくわからない。

f:id:aidiary:20170215231801p:plain

Lpノルム正則化の導入

次の工夫はLpノルム正則化である。Lpノルムの定義は

 ||x||_p = (|x_1|^p + |x_2|^p + \cdots + |x_n|^p)^\frac{1}{p}

である。pを \inftyに発散させると最大ノルムになる。

 ||x||_\infty = \max (|x_1|, |x_2|, \cdots, |x_n|)

Kerasで実装すると下のようになる。

def normalize(img, value):
    return value / np.prod(K.int_shape(img)[1:])

# Lpノルム正則化項
# 今回の設定ではactivationは大きい方がよいため正則化のペナルティ項は引く
p = 6.0
lpnorm_weight = 10.0
if np.isinf(p):
    lp = K.max(input_tensor)
else:
    lp = K.pow(K.sum(K.pow(K.abs(input_tensor), p)), 1.0 / p)
activation -= lpnorm_weight * normalize(input_tensor, lp)

ここで、Lpノルムの xは生成画像であることに注意。またactivationは最大化を目指しているためペナルティである正則化項は差し引くことにも注意。noramlize()は画像のサイズによらないようにする正規化である。

この正則化項は画像のノルムを求めているのでピクセル値が極端に小さな値や大きな値になったときにペナルティをかけるような正則化だと考えられる。そのようなピクセル値が極端な値になる画像は自然ではないという事前知識を表している。

Lpノルムのpや正則化項の重み(lpnorm_weight)はハイパーパラメータなのだが参考文献を参考に設定した。いくつか異なる設定を試してみるとよいかも。Lpノルム正則化を入れると下の画像が生成された。あまり効果ない?

f:id:aidiary:20170215231957p:plain

Total Variation正則化の導入

もう一つTotal Variation正則化を導入しよう。これは効果絶大だった!画像 y のTotal Variation

f:id:aidiary:20170216215852p:plain:w400

で定義される。つまり、Total Variationが大きい画像ほど隣通しのピクセルの差分が大きな画像と解釈できる。Kerasで書くと下のようになる(beta = 1のとき式と一致)。

# Total Variationによる正則化
beta = 2.0
tv_weight = 10.0
a = K.square(input_tensor[:, 1:, :-1, :] - input_tensor[:, :-1, :-1, :])
b = K.square(input_tensor[:, :-1, 1:, :] - input_tensor[:, :-1, :-1, :])
tv = K.sum(K.pow(a + b, beta / 2.0))
activation -= tv_weight * normalize(input_tensor, tv)

配列のスライスがややこしいが具体的な行列(5x5くらいの)で計算してみると隣通しのピクセルの差を取っているのがすぐにわかる。aは画像の行方向の差分でbは画像の列方向の差分を意味する。

ここでは、Total Variationが大きいほどペナルティがかかるようになっているためTotal Variationを小さく(=ピクセル間の差を小さく)する方向に正則化がはたらく。つまり、Total Variation正則化は隣通しのピクセルの輝度差が小さい滑らかな画像にする正則化であると考えられる。

この正則化を導入すると下の画像のようにきれいな画像が出てくる。ここまでくると元のクラスが何なのかうっすらと見えてくる!ナイーブな方法に比べると正則化の導入によってかなり改善しているのがわかる。

f:id:aidiary:20170215232009p:plain

他のクラスもはってみる。なかなか面白い。

f:id:aidiary:20170216220606p:plain f:id:aidiary:20170216220749p:plain

隠れ層のフィルタの可視化

これまでは出力層のフィルタの可視化をしていたが隠れ層のフィルタの可視化も同様にできる。たとえば、block3_conv1層の30番目のフィルタの可視化をしたければ

visualize_filter('block3_conv1', 30)

でOK。ということで浅い層(block1_conv1)から深い層(block5_conv3)に向かってランダムに16個のフィルタを選んで可視化してみよう!

block1_conv1 f:id:aidiary:20170216222141p:plain

block2_conv1 f:id:aidiary:20170216222200p:plain

block3_conv1 f:id:aidiary:20170216222214p:plain

block4_conv1 f:id:aidiary:20170216222233p:plain

block5_conv1 f:id:aidiary:20170216222245p:plain

block5_conv2 f:id:aidiary:20170216222257p:plain

block5_conv3 f:id:aidiary:20170216222324p:plain

こんな感じで浅い層ほど単純な特徴が深い層ほど複雑な特徴が学習されていることがわかる。これを自動獲得できる点が深層学習のすごいところなのだ。

隠れ層の可視化で細かい注意点をいくつか。

  • block1_conv1だけ正規化項を入れるとnanになってしまうので外した
  • 他の層は正則化項を入れたほうが鮮やかな画像が得られる
  • block5_conv3はなかなか難しく右下のようにぼやっとした画像になることが多かった

畳み込みニューラルネットの可視化の原理がようやく理解できた!次は気持ち悪い画像を生成することで一世を風靡したDeep Dreamを実装してみたい。Deep Dreamも基本的に今回紹介した可視化技術に基づいている。

参考

15周年記念

日記

2017年2月21日で人工知能に関するブログ(というか自分のWebサイト)を初めてから15周年を迎えます。基本的に飽きっぽい性格ですが、こんなに続いたのは読んで応援してくださったみなさんのおかげだと思います。ありがとうございます!

ここら辺で15年を振り返ってみるのも面白いんじゃないかと思いインターネットアーカイブを掘り起こしてまとめてみることにしました。個人的な話なのであんまり興味ないかもしれないですけど(^^;

/tmp(2002年~)

全世界に公開できるWebサーバ立てたよ。みんな何か書いてみよう!」みたいな話があって書き始めたのがきっかけです。研究室に配属されたてだったのでこれからやる研究の履歴をまとめていこうかなと思っていました。あと文章を書くのが苦手だったので抵抗をなくそうという趣旨でした。当時はHTMLファイルを書いてFTPでアップロードという時代だったのでとってもシンプルなページでした。タイトルも/tmpとか超適当でした(笑)

f:id:aidiary:20170213223932p:plain

下のような感じで読んだ本や論文、勉強した内容などをつらつら書いていました。当時はバイオインフォマティクスを勉強していたので生物系の内容が多かったです。

f:id:aidiary:20170213224819p:plain

レポートはこんな感じ。このときは、教授や研究室の同僚や後輩向けに書いていて学外からアクセスが来ているとあまり意識していなかったですね・・・

f:id:aidiary:20170213225010p:plain

上の絵でGAの調査というのはこれです。

研究履歴(2003年~)

そこそこ履歴が書き溜まってきたので/tmpという超適当なタイトルはやめて研究履歴に変えました。バイオインフォマティクスは何か違うと思ったので、やっぱり前から興味があった人工知能機械学習やりたいと無理言ってテーマ替えしました。

このときは、ニューラルネットワーク、遺伝的アルゴリズム、強化学習といったいわゆるソフトコンピューティングをやりたいと思っていました。最終的に生物の学習にもっとも近いと惹かれた強化学習の領域を選びました。

強化学習の基本的なアルゴリズムやタスクをJavaアプレットで実装して公開していったのがこの時代です。当時は今ほどネット上に参考資料がなくてSuttonさん(まだある、すごい!)が公開していたLispのプログラムをJavaでグラフィック付きで再実装していました。そのときのアプレットはJavaでゲーム作りますが何か?(2004/9/18)に移植して残してあります。

f:id:aidiary:20170213225528p:plain

このアプレットの公開からカウンターが急上昇してかなり怖気づいた覚えがあります(笑)

f:id:aidiary:20170213225915j:plain

あとこのころに人工知能研究のサーベイを徹底的にしました。図書館にこもって人工知能学会誌を1986年のVol.1、No.1からすべて読破しました。「エキスパートシステムってつまんないなぁ」とか愚痴ってた覚えがあります(笑)朝から晩までこもってたので今でも図書館の本の匂いを思い出します(あとかわいかった司書のお姉さんも)。

サーベイをしながら分野間の関係を把握するために上のような図を書いていました。まあ今見てもけっこう妥当じゃないでしょうか?分野の流行り廃りはありますけど新しくできた分野ってないような・・・

f:id:aidiary:20170213230246p:plain

所属研究室が並列分散処理をやっていたこともあってpthreadやMPIの勉強もしていました。そのせんで強化学習の並列分散化に関する研究を始めました。あまり満足のいく結果は出なかったけれど何とか卒業させてもらいました(笑)

f:id:aidiary:20170213230536j:plain

2005年3月31日の卒業とともにWebサイトの更新は終わりました。卒業式で事前に何にも知らされない状態でいきなり名前を呼ばれて表彰されたのはいい思い出です。Webサイトで役に立つ情報を公開していたことに対する表彰でした。同時に表彰された二人が学内で超優秀な有名人だったので自分だけ「誰だよあいつ」状態で公開処刑されてるみたいですごく恥ずかしかった(笑)

スクリーンショットは2004年3月23日(更新停止)になっているけど2005年の間違えだ・・・

Javaでゲーム作りますが何か?(2004年~)

人工知能でゲームの研究というと当時はチェス・バックギャモン・将棋・碁などの知的ゲームがメインでビデオゲームは人工知能の研究対象ではありませんでした。

人工知能と強化学習のサーベイをしている中で人工知能のビデオゲームへの応用は今後重要になると確信して興味を持ち始めました。これ以上テーマを変えるのは許されなかった(笑)ので片手間で研究していました。かの人工知能の名付け親ジョン・マッカーシーでさえレミングスでゲームAIの研究してたんだよ!これはやるっきゃないでしょ。ファミコン世代ということもありビデオゲーム大好きっ子だったので飛びつきました。

ただ当時はOpenAI Universeのような整った環境があるはずもなく、そもそもゲームってどうやって作るんだ?状態でした。そんなわけでJavaで基本的なゲームを一通り作ってみようと始めたのが「Javaでゲーム作りますが何か?」です。

f:id:aidiary:20170213232953j:plain

当時は(今も?)ゲームはC++で作るのがセオリーでJavaみたいにくそ重い言語で作るなんて馬鹿じゃないの?という風潮があったのでこんなタイトルになりました(笑)ゲームを要素に分解して徐々に作り上げていく解説スタイルが珍しかったのかかなり好評でした。12年前のコンテンツですが、今でもこのブログのアクセス数で断トツ不動の1位です。

当時はこんな感じでHTMLでソースコード書いてました。コメントの色を緑にするのすごく大変だったな(^^;

f:id:aidiary:20170213232833j:plain

人工知能に関する備忘録(2005年3月~)

大学に残してきた研究履歴はいい思い出なので全部消すのはおしいということで、卒業とともに内容をコピーしてはてなダイアリーに引っ越しました。このときのタイトルは今とちょっと違って「人工知能に関する備忘録」というタイトルで始めました。

今も使っているハンドルネームaidiaryはこのときから使っています。人工知能に関する日記だからaidiaryと本当はサイト名でした。これも超適当(笑)最初のブログのスクリーンショットはアーカイブされてなかった。残念。

A.I.に関する備忘録(2005年5月~)

ちょっとタイトル変えてみようということで一時期気まぐれで変えました。

人工知能に関する断想録(2005年12月~)

f:id:aidiary:20170213233732j:plain

なんかしっくりこないということで人工知能に関する断想録に変更しました。

断想録というのはあまり聴かない単語かもしれませんがオリジナルではありません。えらい人が何人か「断想録」というタイトルの本を書いています。

このタイトルは私が人工知能に惹かれたきっかけであるマービン・ミンスキーの「心の社会」にあやかっています。今はまだ関連がはっきりしない断片を積み上げていって、最後に全体として統合されるようなブログにしたいなと壮大な野望を抱いてこんなネーミングにした覚えがあります。まだ道半ばです(^^;

このブログのテーマは一見バラバラだけど上の記事で書いたように一応自分の中では筋を通しています。

Deep Learningを始めたのもこの筋の一環です。Deep Learningで画像・音声認識精度が○○%上がりましたという段階では見向きもしなかったのですが、モーダル(言語・画像・音声)の統合機械による創造が実現できそうだというあたりで非常に興味を持ちました。

人工知能に関する断創録(2012年1月~)

f:id:aidiary:20170213234228j:plain

2012年1月1日はブログを初めて10周年記念ということでタイトルを今も続いている「人工知能に関する断録」に変えました。今までは技術がなくてただ想像していることが多かったけれど、これからは創造のフェーズに入りたいという希望をこめています。まああくまで希望です(^^;

このブログの略歴はこんな感じ。

人工知能は情報科学最後のフロンティアで一生かけても極められない未踏の領域です。今後もライフワークとして興味の赴くままに楽しくやっていこうと思います。

VGG16のFine-tuningによる17種類の花の分類

Deep Learning Keras コンピュータビジョン

前回(2017/1/10)は、VGG16をFine-tuningして犬か猫を分類できる2クラス分類のニューラルネットワークを学習した。今回は、同様のアプローチで17種類の花を分類するニューラルネットワークを学習してみたい。前回の応用編みたいな感じ。この実験はオリジナルなので結果がどう出るかわからなかったけどうまくいったのでまとめてみた。

リポジトリ:17flowers

使用したデータは、VGG16を提案したOxford大学のグループが公開している 17 Category Flower Dataset である。下のような17種類の花の画像データ。とっても美しい。

f:id:aidiary:20170131203715j:plain:h700

前に実験した(2017/1/4)ようにデフォルトのVGG16ではひまわりさえ分類できなかったが、VGG16をFine-tuningすることで果たしてこれら17種類の花(ひまわりもある)を分類できるようになるのだろうか?さっそく試してみよう!

セットアップ

17flowers.tgzというファイルをダウンロードする。これを解凍するとjpgというディレクトリの中にimage_0001.jpgからimage_1360.jpgまで各クラス80枚、計1360枚の画像が含まれている。1360枚は畳み込みニューラルネットワークをスクラッチから学習するには心もとないデータ数であるが、今回はVGG16を少量データでチューニングする転移学習を使うので十分だろう。

各クラス80枚で17クラスなので1360枚なのだが、各画像がどのラベルなのかがわからない。とりあえずサンプル画像と見比べて下のようにラベルを割り振ったlabels.txtというファイルを作成した。たとえば、1行目はimage_0001.jpgからimage_0080.jpgまでがTulipであることを意味する。

1       80      Tulip
81      160     Snowdrop
161     240     LilyValley
241     320     Bluebell
321     400     Crocus
401     480     Iris
481     560     Tigerlily
561     640     Daffodil
641     720     Fritillary
721     800     Sunflower
801     880     Daisy
881     960     ColtsFoot
961     1040    Dandelion
1041    1120    Cowslip
1121    1200    Buttercup
1201    1280    Windflower
1281    1360    Pansy

まずは、Kerasからロードしやすいように以下のsetup.pyで画像ファイルを分割する。各クラスで訓練データが70枚、テストデータが10枚になるように分割した。犬猫分類でやったようにサブディレクトリにクラス名を付けておくと自動的に認識してくれる。このクラス名を付けるために先ほどのlabels.txtを使った。

import os
import shutil
import random

IN_DIR = 'jpg'
TRAIN_DIR = 'train_images'
TEST_DIR = 'test_images'

if not os.path.exists(TRAIN_DIR):
    os.mkdir(TRAIN_DIR)

if not os.path.exists(TEST_DIR):
    os.mkdir(TEST_DIR)

# name => (start idx, end idx)
flower_dics = {}

with open('labels.txt') as fp:
    for line in fp:
        line = line.rstrip()
        cols = line.split()

        assert len(cols) == 3

        start = int(cols[0])
        end = int(cols[1])
        name = cols[2]

        flower_dics[name] = (start, end)

# 花ごとのディレクトリを作成
for name in flower_dics:
    os.mkdir(os.path.join(TRAIN_DIR, name))
    os.mkdir(os.path.join(TEST_DIR, name))

# jpgをスキャン
for f in sorted(os.listdir(IN_DIR)):
    # image_0001.jpg => 1
    prefix = f.replace('.jpg', '')
    idx = int(prefix.split('_')[1])

    for name in flower_dics:
        start, end = flower_dics[name]
        if idx in range(start, end + 1):
            source = os.path.join(IN_DIR, f)
            dest = os.path.join(TRAIN_DIR, name)
            shutil.copy(source, dest)
            continue

# 訓練データの各ディレクトリからランダムに10枚をテストとする
for d in os.listdir(TRAIN_DIR):
    files = os.listdir(os.path.join(TRAIN_DIR, d))
    random.shuffle(files)
    for f in files[:10]:
        source = os.path.join(TRAIN_DIR, d, f)
        dest = os.path.join(TEST_DIR, d)
        shutil.move(source, dest)

1. 小さな畳み込みニューラルネットをスクラッチから学習する

前回と同様に今回もベースラインとしてLeNet相当の小さな畳み込みニューラルネットワークを学習してみよう*1。まずはモデルを構築する。

model = Sequential()
model.add(Convolution2D(32, 3, 3, input_shape=(img_rows, img_cols, channels)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Convolution2D(32, 3, 3))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))

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

model.add(Flatten())
model.add(Dense(64))
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'])

今まで何回もやってきたので特に難しいところはない。今回は17クラスの多クラス分類なので損失関数にcategorical_crossentropyを使う。

画像ファイルしか提供されていないときはデータの読み込みにImageDataGeneratorを使うと便利。今回もデータ拡張(2016/12/12)には前回と同じくshear_rangezoom_rangehorizontal_flipを使ったがデータの特徴を見て慎重に決めるとより精度が向上するかも。

# ディレクトリの画像を使ったジェネレータ
train_datagen = ImageDataGenerator(
    rescale=1.0 / 255,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True)

test_datagen = ImageDataGenerator(rescale=1.0 / 255)

上の設定を使って実際にジェネレータを作成。画像ファイルを含むディレクトリを指定するだけでよい。ここで、classesを自分で指定すると順番にクラスラベルを割り振ってくれる。color_modeclass_modeは何に使われるかいまいち把握できていないがrgbcategoricalでよさそう。

classes = ['Tulip', 'Snowdrop', 'LilyValley', 'Bluebell', 'Crocus',
           'Iris', 'Tigerlily', 'Daffodil', 'Fritillary', 'Sunflower',
           'Daisy', 'ColtsFoot', 'Dandelion', 'Cowslip', 'Buttercup',
           'Windflower', 'Pansy']

train_generator = train_datagen.flow_from_directory(
    directory='train_images',
    target_size=(img_rows, img_cols),
    color_mode='rgb',
    classes=classes,
    class_mode='categorical',
    batch_size=batch_size,
    shuffle=True)

test_generator = test_datagen.flow_from_directory(
    directory='test_images',
    target_size=(img_rows, img_cols),
    color_mode='rgb',
    classes=classes,
    class_mode='categorical',
    batch_size=batch_size,
    shuffle=True)

ジェネレータができたのでジェネレータが生成する画像の4Dテンソルを使ってモデルを訓練する。ここは前回と同じ。

history = model.fit_generator(
    train_generator,
    samples_per_epoch=samples_per_epoch,
    nb_epoch=nb_epoch,
    validation_data=test_generator,
    nb_val_samples=nb_val_samples)
save_history(history, os.path.join(result_dir, 'history_smallcnn.txt'))

学習途中の損失と精度はあとで参照できるようにファイルに保存した。

2. VGG16をFine-tuningする

次に前回と似た感じでVGG16をFine-tuningしてみよう。最後の畳込み層ブロックとフル結合層のみ重みを再調整する。

# VGG16モデルと学習済み重みをロード
# Fully-connected層(FC)はいらないのでinclude_top=False)
input_tensor = Input(shape=(img_rows, img_cols, 3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

# FC層を構築
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(nb_classes, activation='softmax'))

# VGG16とFCを接続
model = Model(input=vgg16.input, output=top_model(vgg16.output))

# 最後のconv層の直前までの層をfreeze
for layer in model.layers[:15]:
    layer.trainable = False

# Fine-tuningのときはSGDの方がよい
model.compile(loss='categorical_crossentropy',
              optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
              metrics=['accuracy'])

前回とほとんど同じ。違いは、FC層の出力が17クラス分類になるので活性化関数にsoftmaxを使っているところと損失関数にcategorical_crossentropyを使っているところくらい。あとはほとんど同じなので省略。

実験結果

損失と精度の履歴は下のようになった。小さな畳み込みニューラルネットでは60%程度の分類精度しか出ないが、VGG16をFine-tuningすると85%くらいまで分類精度が跳ね上がるのが確認できた。

f:id:aidiary:20170131204317p:plain f:id:aidiary:20170131204327p:plain

1000クラスにひまわりがなかったVGG16ではあるが、花の分類に役立つ特徴抽出器は学習できていたようだ。

花の分類例

f:id:aidiary:20170131205422j:plain:w200

% python predict.py test_images/Sunflower/image_0724.jpg
input: test_images/Sunflower/image_0724.jpg
('Sunflower', 0.9999969)
('ColtsFoot', 1.3097971e-06)
('Iris', 8.8473638e-07)
('Tigerlily', 6.5053348e-07)
('Buttercup', 1.2474243e-07)

念願のひまわりもちゃんと分類できている!

f:id:aidiary:20170131205545j:plain:w200

% python predict.py test_images/Iris/image_0410.jpg
input: test_images/Iris/image_0410.jpg
('Iris', 0.99993575)
('Crocus', 1.9791674e-05)
('Sunflower', 1.734318e-05)
('Buttercup', 9.1189122e-06)
('Fritillary', 4.9292394e-06)

統計屋にはおなじみのアイリスもちゃんと分類できる。

f:id:aidiary:20170131205647j:plain:w200

% python predict.py test_images/Dandelion/image_0966.jpg
input: test_images/Dandelion/image_0966.jpg
('Dandelion', 0.99502832)
('ColtsFoot', 0.0034611411)
('Sunflower', 0.0014925624)
('Tigerlily', 5.2598648e-06)
('Buttercup', 4.0573868e-06)

蜂がいてもなんのその。何となくひまわりに似ているけどちゃんと区別できているのがすごい。3番目にひまわりが出ていた。

f:id:aidiary:20170131205854j:plain:w200

% python predict.py test_images/Windflower/image_1219.jpg
input: test_images/Windflower/image_1219.jpg
('Windflower', 0.99989629)
('Pansy', 4.6885198e-05)
('Daisy', 3.5976835e-05)
('LilyValley', 8.592202e-06)
('Snowdrop', 5.5862884e-06)

日本語だとアネモネ。これも正解。

f:id:aidiary:20170131210015j:plain:w200

% python predict.py test_images/Daisy/image_0807.jpg
input: test_images/Daisy/image_0807.jpg
('Daisy', 0.99801069)
('Sunflower', 0.0019772816)
('ColtsFoot', 1.0563146e-05)
('Pansy', 8.2547245e-07)
('Windflower', 4.5572747e-07)

デイジーも正解。

こういうきれいな画像を見ているとテンション上がる。とっても楽しい実験だった。

次は畳込みニューラルネットのフィルタの可視化方法を深堀りしてDeep Dreamへとつなげていきたい。何か今さら感が強いんだけどね(^^;

参考

*1:ちなみにここでVGG16をスクラッチから学習しようとすると全然精度が出ないのは確認済み