人工知能に関する断創録

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

Practical Deep Learning for Coders

最近、fast.aiのPractical Deep Learning for CodersというMOOCを受講している。

この講座は

  • 無料
  • 動画形式の講義(1回2時間というボリューム)
  • Jupyter NotebookとKerasを使用
  • CNN、Finetuning、VGG16、ResNet、RNNなどが実践的な例題を通して学べる
  • 実務家がDeep Learningで自分の問題を解決できることが目標

という特徴がある。講義内容は高度で実践的なものが多い印象。例えば、Lesson1でMNISTと思いきや・・・いきなりKaggleのDogs vs. CatsをVGG16 + Finetuningで解いてKaggleに投稿するところまでが課題になっている。これさえできれば画像認識が必要ないろんな課題に同じ技術を適用できるとのこと。

今はまだPart1しかないが、ForumのなかでPart2の動画も公開されており(近日正式公開)、そこではNeural Style Transfer、GAN、Attentionなど高度な話題も実践的に取り上げられている。

講師のJeremy HowardさんはKaggleマスターとのことで、Jupyter Notebookを使ってDeep Learningの課題を解いていく様子が非常に参考になる。Jupyter Notebookでこんなことできんの?というTipsが盛りだくさんなのでそれだけでも受講する価値がありそう。受講者はかなりたくさんいるようでForumに非常に役立つ情報が満載されているのもポイントが高い。

私はようやくLesson1と宿題が終わったところ。キリがよいところでレポートをまとめていこうかなと思ってます。

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

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

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

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