畳み込みニューラルネットワークの可視化
Deep Learningの学習結果(重み)はブラックボックスで、隠れ層のユニット(特に深い層の!)が一体何を学習したのかがよくわからないと長年言われてきた。しかし、今回紹介する方法を使うとニューラルネットが何を学習したのか目で見える形で表現できる。
畳み込みニューラルネットで学習したフィルタの可視化というと以前やったように学習した第1層のフィルタの重みを直接画像として可視化する方法がある。
しかし、畳み込みフィルタのサイズは基本的に数ピクセル(MNISTの例では5x5ピクセル程度)のとても小さな画像なのでこれを直接可視化しても何が学習されたか把握するのはとても難しい。たとえば、MNISTを学習した畳み込みニューラルネット(2016/11/20)のフィルタを可視化しても各フィルタがどの方向に反応しやすいかがわかる程度だ。
各フィルタが何を学習したかを可視化する別のアプローチとして各フィルタの出力を最大化するような入力画像を生成する手法が提案された。この生成画像はニューラルネットの入力画像と同じサイズなのでフィルタが最終的に何を学習したのかより把握しやすいというメリットがある。
今回は畳み込みニューラルネットの一種であるVGG16を対象に学習したフィルタを可視化してみたい。あとでMNISTやCIFAR-10を学習したCNNや最新のResNetでもやってみるつもり。
基本的に本家のKeras Blogの How convolutional neural networks see the world を参考にした。しかし、この記事は
keras.applications.vgg16
モジュールが導入される前に書かれている- 正則化などの工夫が入っておらず生成される画像が美しくない
という問題がある。
そこでこの記事ではKerasのvgg16
モジュールを使って書き換えるとともに、よりきれいな画像が生成されるようにいくつか工夫した。まあ少しは差別化しないとね(笑)
リポジトリ:dream
ナイーブな方法
基本的なアプローチは、畳み込みニューラルネットの指定したフィルタの出力を最大にする入力画像を勾配法を用いて更新することである。数式で書くと
となる。ここで、は入力画像、は学習率、は画像を入力したときのi番目のフィルタの出力(activation)だ。
ここで、偏微分がいわゆる勾配を表し、入力画像をちょっと変えたときにフィルタの出力がどれくらい変化するかを表している。入力画像をこの勾配方向に徐々に移動する勾配上昇法(gradient ascent)を用いてフィルタの出力を最大化する入力画像を求めている。
この式はニューラルネットの重みの更新式と比較するとわかりやすい。ニューラルネットの重みの更新式は
これはよく目にする式だ。ここで、は重み、は学習率、は損失だ。
ここで、偏微分は勾配を表し、重みをちょっと変えたときに損失がどれくらい変化するかを表している。重みをこの勾配の負の方向に徐々に移動する勾配降下法(gradient descent)を用いて損失を最小化する重みを求めている。
こんな感じで比較してみると両者の基本原理はよく似ていることがわかる。
ニューラルネットの重み更新では重みの初期値としてランダムな値を使った。フィルタの可視化でも同様に入力画像の初期値として下記のようなランダムな画像を使う。一方、次回紹介するDeep Dreamはこの画像の初期値として任意の画像を使う。
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()
で表示される。predictions
はVGG16
の出力層に付けられた名前である。VGG16の出力層は1000ユニットから成るが65番目のユニットはsea snake(ウミヘビ)クラスを意味する。1個だけではつまらないので16個まとめて画像化すると下のようになる。左上にフィルタ番号に対応するクラス名を表示した。
まあこの可視化画像を見てもはっきり言ってよくわからない。ウミヘビは何かウネウネしてるぞ、ゴリラに何か目があるぞとかそれくらい(笑)
でもこれらの画像をニューラルネットに入力するとウミヘビである確率が99%、ゴリラである確率が99%ととんでもない結果が出てくる・・・人間にはまったくウミヘビやゴリラと認識できない画像なのに、ニューラルネットは99%の確率でウミヘビ・ゴリラと判定してしまうのだ。ここら辺は参考文献で挙げた「ニューラルネットは簡単にだませる」という論文を参照。
このナイーブな方法ではあまりはっきりとした画像が生成できなかったが、いくつかの改良を加えるとよりはっきりとした鮮やかな画像が生成できることが知られている。そのために自然な画像らしさを表す事前知識(natural image priors)を正則化項として導入すればよい。
VGG16の出力をsoftmaxからlinearに
正則化項を導入する前に一つ重要な改善を入れよう。VGG16の出力層の出力は分類のためにsoftmax
活性化関数を適用しており、1000クラスの合計が1.0になるように正規化されている。この場合、あるクラスの出力を最大化するためには (1) そのクラスの出力を上げる以外に、(2) 他のクラスの出力を下げるという方法もとれてしまう。この依存関係で最適化が惑わされてうまくいかないという指摘がある(Simonyan 2013)。そこで、最初の工夫としてsoftmax
をlinear
にしてみた。
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)
この工夫を入れてもう一度可視化すると下のようにコントラストが少し強くなる。だけどまだ何が何だかよくわからない。
Lpノルム正則化の導入
次の工夫はLpノルム正則化である。Lpノルムの定義は
である。pをに発散させると最大ノルムになる。
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ノルムのは生成画像であることに注意。またactivationは最大化を目指しているためペナルティである正則化項は差し引くことにも注意。noramlize()は画像のサイズによらないようにする正規化である。
この正則化項は画像のノルムを求めているのでピクセル値が極端に小さな値や大きな値になったときにペナルティをかけるような正則化だと考えられる。そのようなピクセル値が極端な値になる画像は自然ではないという事前知識を表している。
Lpノルムのpや正則化項の重み(lpnorm_weight
)はハイパーパラメータなのだが参考文献を参考に設定した。いくつか異なる設定を試してみるとよいかも。Lpノルム正則化を入れると下の画像が生成された。あまり効果ない?
Total Variation正則化の導入
もう一つTotal Variation正則化を導入しよう。これは効果絶大だった!画像 y のTotal Variationは
で定義される。つまり、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正則化は隣通しのピクセルの輝度差が小さい滑らかな画像にする正則化であると考えられる。
この正則化を導入すると下の画像のようにきれいな画像が出てくる。ここまでくると元のクラスが何なのかうっすらと見えてくる!ナイーブな方法に比べると正則化の導入によってかなり改善しているのがわかる。
他のクラスもはってみる。なかなか面白い。
隠れ層のフィルタの可視化
これまでは出力層のフィルタの可視化をしていたが隠れ層のフィルタの可視化も同様にできる。たとえば、block3_conv1
層の30番目のフィルタの可視化をしたければ
visualize_filter('block3_conv1', 30)
でOK。ということで浅い層(block1_conv1)から深い層(block5_conv3)に向かってランダムに16個のフィルタを選んで可視化してみよう!
block1_conv1
block2_conv1
block3_conv1
block4_conv1
block5_conv1
block5_conv2
block5_conv3
こんな感じで浅い層ほど単純な特徴が深い層ほど複雑な特徴が学習されていることがわかる。これを自動獲得できる点が深層学習のすごいところなのだ。
隠れ層の可視化で細かい注意点をいくつか。
- block1_conv1だけ正規化項を入れるとnanになってしまうので外した
- 他の層は正則化項を入れたほうが鮮やかな画像が得られる
- block5_conv3はなかなか難しく右下のようにぼやっとした画像になることが多かった
畳み込みニューラルネットの可視化の原理がようやく理解できた!次は気持ち悪い画像を生成することで一世を風靡したDeep Dreamを実装してみたい。Deep Dreamも基本的に今回紹介した可視化技術に基づいている。
参考
- Kerasで学ぶ転移学習
- Neural Style Transfer: Prismaの背景技術を解説する
- How convolutional neural networks see the world
- Visualizing CNN filters with keras
- Understanding Neural Networks Through Deep Visualization
- Visualizing Deep Neural Networks Classes and Features
- Meaning of Weight Gradient in CNN
- Erhan, D. et al. Visualizing Higher-Layer Features of a Deep Network (2009)
- Simonyan, K. et al. Deep Inside Convolutional Networks: Visualising Image Classification Models and Saliency Maps (2013)
- Nguyen, A. et al. Deep Neural Networks are Easily Fooled: High Confidence Predictions for Unrecognizable Images (2014)
- Yosinski, J. et al. Understanding Neural Networks Through Deep Visualization (2015)
15周年記念
2017年2月21日で人工知能に関するブログ(というか自分のWebサイト)を初めてから15周年を迎えます。基本的に飽きっぽい性格ですが、こんなに続いたのは読んで応援してくださったみなさんのおかげだと思います。ありがとうございます!
ここら辺で15年を振り返ってみるのも面白いんじゃないかと思いインターネットアーカイブを掘り起こしてまとめてみることにしました。個人的な話なのであんまり興味ないかもしれないですけど(^^;
/tmp(2002年~)
「全世界に公開できるWebサーバ立てたよ。みんな何か書いてみよう!」みたいな話があって書き始めたのがきっかけです。研究室に配属されたてだったのでこれからやる研究の履歴をまとめていこうかなと思っていました。あと文章を書くのが苦手だったので抵抗をなくそうという趣旨でした。当時はHTMLファイルを書いてFTPでアップロードという時代だったのでとってもシンプルなページでした。タイトルも/tmpとか超適当でした(笑)
下のような感じで読んだ本や論文、勉強した内容などをつらつら書いていました。当時はバイオインフォマティクスを勉強していたので生物系の内容が多かったです。
レポートはこんな感じ。このときは、教授や研究室の同僚や後輩向けに書いていて学外からアクセスが来ているとあまり意識していなかったですね・・・
上の絵でGAの調査というのはこれです。
研究履歴(2003年~)
そこそこ履歴が書き溜まってきたので/tmpという超適当なタイトルはやめて研究履歴に変えました。バイオインフォマティクスは何か違うと思ったので、やっぱり前から興味があった人工知能や機械学習やりたいと無理言ってテーマ替えしました。
このときは、ニューラルネットワーク、遺伝的アルゴリズム、強化学習といったいわゆるソフトコンピューティングをやりたいと思っていました。最終的に生物の学習にもっとも近いと惹かれた強化学習の領域を選びました。
強化学習の基本的なアルゴリズムやタスクをJavaアプレットで実装して公開していったのがこの時代です。当時は今ほどネット上に参考資料がなくてSuttonさん(まだある、すごい!)が公開していたLispのプログラムをJavaでグラフィック付きで再実装していました。そのときのアプレットはJavaでゲーム作りますが何か?(2004/9/18)に移植して残してあります。
このアプレットの公開からカウンターが急上昇してかなり怖気づいた覚えがあります(笑)
あとこのころに人工知能研究のサーベイを徹底的にしました。図書館にこもって人工知能学会誌を1986年のVol.1、No.1からすべて読破しました。「エキスパートシステムってつまんないなぁ」とか愚痴ってた覚えがあります(笑)朝から晩までこもってたので今でも図書館の本の匂いを思い出します(あとかわいかった司書のお姉さんも)。
サーベイをしながら分野間の関係を把握するために上のような図を書いていました。まあ今見てもけっこう妥当じゃないでしょうか?分野の流行り廃りはありますけど新しくできた分野ってないような・・・
所属研究室が並列分散処理をやっていたこともあってpthreadやMPIの勉強もしていました。そのせんで強化学習の並列分散化に関する研究を始めました。あまり満足のいく結果は出なかったけれど何とか卒業させてもらいました(笑)
2005年3月31日の卒業とともにWebサイトの更新は終わりました。卒業式で事前に何にも知らされない状態でいきなり名前を呼ばれて表彰されたのはいい思い出です。Webサイトで役に立つ情報を公開していたことに対する表彰でした。同時に表彰された二人が学内で超優秀な有名人だったので自分だけ「誰だよあいつ」状態で公開処刑されてるみたいですごく恥ずかしかった(笑)
スクリーンショットは2004年3月23日(更新停止)になっているけど2005年の間違えだ・・・
Javaでゲーム作りますが何か?(2004年~)
人工知能でゲームの研究というと当時はチェス・バックギャモン・将棋・碁などの知的ゲームがメインでビデオゲームは人工知能の研究対象ではありませんでした。
人工知能と強化学習のサーベイをしている中で人工知能のビデオゲームへの応用は今後重要になると確信して興味を持ち始めました。これ以上テーマを変えるのは許されなかった(笑)ので片手間で研究していました。かの人工知能の名付け親ジョン・マッカーシーでさえレミングスでゲームAIの研究してたんだよ!これはやるっきゃないでしょ。ファミコン世代ということもありビデオゲーム大好きっ子だったので飛びつきました。
ただ当時はOpenAI Universeのような整った環境があるはずもなく、そもそもゲームってどうやって作るんだ?状態でした。そんなわけでJavaで基本的なゲームを一通り作ってみようと始めたのが「Javaでゲーム作りますが何か?」です。
当時は(今も?)ゲームはC++で作るのがセオリーでJavaみたいにくそ重い言語で作るなんて馬鹿じゃないの?という風潮があったのでこんなタイトルになりました(笑)ゲームを要素に分解して徐々に作り上げていく解説スタイルが珍しかったのかかなり好評でした。12年前のコンテンツですが、今でもこのブログのアクセス数で断トツ不動の1位です。
当時はこんな感じでHTMLでソースコード書いてました。コメントの色を緑にするのすごく大変だったな(^^;
人工知能に関する備忘録(2005年3月~)
大学に残してきた研究履歴はいい思い出なので全部消すのはおしいということで、卒業とともに内容をコピーしてはてなダイアリーに引っ越しました。このときのタイトルは今とちょっと違って「人工知能に関する備忘録」というタイトルで始めました。
今も使っているハンドルネームaidiaryはこのときから使っています。人工知能に関する日記だからaidiaryと本当はサイト名でした。これも超適当(笑)最初のブログのスクリーンショットはアーカイブされてなかった。残念。
A.I.に関する備忘録(2005年5月~)
ちょっとタイトル変えてみようということで一時期気まぐれで変えました。
人工知能に関する断想録(2005年12月~)
なんかしっくりこないということで人工知能に関する断想録に変更しました。
断想録というのはあまり聴かない単語かもしれませんがオリジナルではありません。えらい人が何人か「断想録」というタイトルの本を書いています。
このタイトルは私が人工知能に惹かれたきっかけであるマービン・ミンスキーの「心の社会」にあやかっています。今はまだ関連がはっきりしない断片を積み上げていって、最後に全体として統合されるようなブログにしたいなと壮大な野望を抱いてこんなネーミングにした覚えがあります。まだ道半ばです(^^;
このブログのテーマは一見バラバラだけど上の記事で書いたように一応自分の中では筋を通しています。
Deep Learningを始めたのもこの筋の一環です。Deep Learningで画像・音声認識精度が○○%上がりましたという段階では見向きもしなかったのですが、モーダル(言語・画像・音声)の統合と機械による創造が実現できそうだというあたりで非常に興味を持ちました。
人工知能に関する断創録(2012年1月~)
2012年1月1日はブログを初めて10周年記念ということでタイトルを今も続いている「人工知能に関する断創録」に変えました。今までは技術がなくてただ想像していることが多かったけれど、これからは創造のフェーズに入りたいという希望をこめています。まああくまで希望です(^^;
このブログの略歴はこんな感じ。
人工知能は情報科学最後のフロンティアで一生かけても極められない未踏の領域です。今後もライフワークとして興味の赴くままに楽しくやっていこうと思います。
VGG16のFine-tuningによる17種類の花の分類
前回(2017/1/10)は、VGG16をFine-tuningして犬か猫を分類できる2クラス分類のニューラルネットワークを学習した。今回は、同様のアプローチで17種類の花を分類するニューラルネットワークを学習してみたい。前回の応用編みたいな感じ。この実験はオリジナルなので結果がどう出るかわからなかったけどうまくいったのでまとめてみた。
リポジトリ:17flowers
使用したデータは、VGG16を提案したOxford大学のグループが公開している 17 Category Flower Dataset である。下のような17種類の花の画像データ。とっても美しい。
前に実験した(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_range
とzoom_range
とhorizontal_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_mode
とclass_mode
は何に使われるかいまいち把握できていないがrgb
とcategorical
でよさそう。
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%くらいまで分類精度が跳ね上がるのが確認できた。
1000クラスにひまわりがなかったVGG16ではあるが、花の分類に役立つ特徴抽出器は学習できていたようだ。
花の分類例
% 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)
念願のひまわりもちゃんと分類できている!
% 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)
統計屋にはおなじみのアイリスもちゃんと分類できる。
% 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番目にひまわりが出ていた。
% 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)
日本語だとアネモネ。これも正解。
% 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をスクラッチから学習しようとすると全然精度が出ないのは確認済み