【Deep Learning with Python】ドロップアウト

フランソワ・ショレさんのディープラーニングとKerasの解説本『Deep Learning with Python』の読書メモ。今回で前半部分の第1部は終わり。ここでは、前回に引き続いて正則化の手法のドロップアウトを扱う。

ドロップアウトは、ニューラルネットに対して最も有効であり、最も広く使われる正則化手法で、ニューラルネットの「ゴッドファーザートロント大学のジェフ・ヒントン教授とその教え子によって開発された。ドロップアウトはどのような手法かというと、モデルの訓練時に一定の割合のノードの出力をゼロにする (値をドロップアウトする) というもの。この手法により、過学習を緩和して、汎化性能を向上させることができる。

ヒントン先生がドロップアウトの手法を考案したきっかけについて、本書では本人が語った面白いエピソードが紹介されている。

「私は銀行へ行った。窓口の行員は交代し続けており、私は彼らに何故なのかと聞いた。行員は、自分には分からないと言ったが、彼らはたくさん動き回っていた。私は、窓口係の交代で従業員同士の協力が必要となることによって、銀行詐欺を防げるからに違いないと考えた。」

窓口での顧客対応を、わざわざ複数人で行うのは一見非効率だと思える。しかし、あえて情報共有を必要とする状況を作ると、その過程で意味のない情報は捨てられることになる。そうすることで本質的な情報に対する注意が向けられ、銀行詐欺のような異常事態に気付くことができるのではないか、と言うことなのだろう。

ニューラルネットに対するドロップアウトがうまく働く理由も、これと似ていると言われている。訓練データの偏りによって生じた意味のないパターン (ヒントンが共謀(conspiracies)と呼ぶもの) は、わざとランダムなノイズを入れることでかき消すことができる。そのため、訓練データに含まれる本当に重要な意味のある情報のみが残される。


Kerasでドロップアウトを使う場合には、モデルにドロップアウト用のレイヤーを追加するという形を取る。

dropout_model.add(layers.Dropout(0.5))

ここでDropoutインスタンスに指定している引数 "0.5"はドロップアウト率を表す。ここでは、直前の層からの入力のうち50%がランダムに間引かれ、値がゼロとなることを意味する。

IMDBサンプルのモデルにドロップアウト層を追加したモデルは以下の通り。

dropout_model = models.Sequential()
dropout_model.add(layers.Dense(16, activation='relu', 
                                  input_shape=(10000,)))
dropout_model.add(layers.Dropout(0.5))
dropout_model.add(layers.Dense(16, activation='relu'))
dropout_model.add(layers.Dropout(0.5))
dropout_model.add(layers.Dense(1, activation='sigmoid'))

このモデルは、IMDBサンプル問題で使用したオリジナルのモデルにドロップアウトを足しただけのもの。
訓練時のパラメータは全て同じものを使い、検証誤差を比較したものがこちら。

f:id:liaoyuan:20180219195539p:plain

確かに、過学習が緩和されていることが分かる。

【Deep Learning with Python】過学習への対応

以前に取り上げた3つのサンプルプログラムでは、学習を続けていくと、訓練データの予測精度は向上するものの検証スコアは悪化していった。このように、訓練用データはうまく予測できるものの、逆に一度も入力されたことのないデータに対する予測性能が下がってしまう現象を過学習 (overfitting) と呼ぶ。過学習への対処は機械学習の中心的な課題である。

一般的に、過学習に対して最も効果がある対処法は訓練データを増やすことである。それができない場合は、モデルが保存する情報を減らすことである。

この4.4節では、過学習に対応する方法が記載されている。

ネットワークのサイズ減少

過学習に対する最もシンプルな対処法は、ニューラルネットのサイズ (レイヤー数やノード数) を減らすことである。たとえば、50万ビットのパラメータを持つモデルは、MNISTサンプルの5万個の数字の画像全てに対して、対応した数値を記憶させることができるだろう。しかし、このようなモデルは新しいデータに対しては無力である。このことは常に心に留めておいてほしい。ディープラーニングのモデルは、訓練データのフィッティングに対しては上手く働くことが多い。しかし、真の問題は未知のデータに対する汎化性能の向上である。

一方で、あまりに小さすぎるモデルは、訓練データから有用な特徴量を引き出して記憶することができず、やはり未知のデータへの予測性能は低くなってしまう。この状態を未学習 (underfitting) と呼ぶ。

ディープラーニングのモデルが記憶できるデータの量は、容量・キャパシティ(capacity) と呼ばれることがある。多くもなく少なくもない、ちょうど良いキャパシティを持つモデルを構築するのは難しく、簡単に決められる方法は無い。一般的なワークフローとしては、小さなモデルから始めてだんだん大きくして試していくと良い。

ここでは、IMDBのサンプルに対するモデルをもう一度取り上げる。元々のモデルでは、隠れ層が2層、それぞれが16ノードのモデルを用いた。ここでは、ノード数を4と512に増減させたモデルを用いてみる。

f:id:liaoyuan:20180213225531p:plain
f:id:liaoyuan:20180213225542p:plain

小さいモデルでは過学習が発生し始めるエポックが遅く、また過学習のスピードも穏やかである。一方で、大きなモデルはほとんど即座に過学習が始まっている。

また、訓練誤差を確認すると、大きなモデルでは訓練誤差が即座に減少しその後ゼロ付近で振動している。訓練誤差が即座に減少する状況では、過学習が起こっていることを疑ったほうが良い。(なお、このグラフ、p.107の図4.6は印刷のズレか縦軸の説明が切れてしまっている。これは訓練誤差 (training loss) だと思われる。)

正則化

機械学習では、学習データへの予測に対する何らかの誤差を定式化して、これを最小化するように重み付けを最適化するという問題を解く。しかし、ただ単に誤差関数の値を最小化しようと学習させると、過学習が起こりやすくなってしまう。そこで、パラメータの値の大きさに対して何らかのペナルティを科すことで、過学習を抑えた「ちょうど良い」パラメータを学習させる方法がある。これを正則化と呼ぶ。

抽象的には、これはオッカムの剃刀の原理として説明できる。オッカムの剃刀とは、何かの現象に対する複数の説明があるとするなら、最も前提や仮定の少ないシンプルな説明が正しい可能性が高いという原理である。
機械学習においても同様に、同じ予測ができるモデルならば、シンプルであるほうが汎化性能が高い、と考えられる。

kerasでも以下の2つの正規化が使用できる。

  • L1正則化 ペナルティとして学習モデルのパラメータの絶対値の総和を用いる
  • L2正則化 ペナルティとして学習モデルのパラメータの二乗の総和を用いる

なお、L1正則化はLASSO正則化、L2正則化はRidge正則化、L1とL2両方を使う場合をElasticNetと呼ぶ場合がある。下記はl2正則化を使う例。

from keras import regularizers

model = models.Sequential()
model.add(layers.Dense(16, kernel_regularizer=regularizers.l2(0.001),
                   activation='relu', input_shape=(10000,))) 
model.add(layers.Dense(16, kernel_regularizer=regularizers.l2(0.001),
                   activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

書籍には記載がないけど、正則化については以下の通り説明されることがある。

  • L1正則化 重みを0とすることで不要なデータからの影響を削除する
  • L2正則化 重みの大きさをデータに応じて小さくする

これを確認してみたい。L1/L2正則化を入れて学習済みのモデルを使う。以前のサンプルプログラムでも用いた、モデル各層の重みを出力する関数を使い、各ノードの重みの二乗和をグラフにしてみる。

f:id:liaoyuan:20180213230142p:plainf:id:liaoyuan:20180213230146p:plainf:id:liaoyuan:20180213230149p:plain
オリジナルのモデルの重み (正則化無し)

f:id:liaoyuan:20180213230317p:plainf:id:liaoyuan:20180213230320p:plainf:id:liaoyuan:20180213230323p:plain
L1正則化を使用したモデルの重み

f:id:liaoyuan:20180213230416p:plainf:id:liaoyuan:20180213230419p:plainf:id:liaoyuan:20180213230423p:plain
L2正則化を使用したモデルの重み

特に最初の隠れ層 (上記のグラフの一番左のもの) を見ると顕著に分かるが、確かにL1正則化の場合はゼロとなったパラメータの数が増えており、L2正則化の場合はパラメータの値の絶対値が減少していることが分かる。(縦軸のスケールが揃っていないので若干分かりづらい…)

最後にドロップアウトを取り上げるつもりだったけど、長くなったので今回はここまで。

【Deep Learning with Python】機械学習の基礎

4章では、ディープラーニングに限らず機械学習全般に関わる内容を扱っている。このエントリの内容は、主に4章中盤部の内容をまとめたもの。


4.2 機械学習モデルの評価

訓練(training), 検証(validation), テスト(testing)

機械学習モデルの評価をする際には、データセット訓練データ、検証データ、テストデータの3つに分けて使用する。訓練データを用いて機械学習モデル自体を調整(学習)し、学習に使用していない検証データを用いて、学習の結果を評価する。最後に、モデルの学習が完了した時点で、学習にも検証にも使用していないテストデータを使ってモデルの最終的な予測結果を評価する。

ここで、シンプルに訓練データとテストデータの2つに分ければ十分ではないかと思うかもしれない。それではダメな理由は、一言で言うなら、検証データに対する過学習を避けるため。

機械学習モデルを開発する際には、機械学習モデル自体(ハイパーパラメータ)のチューニングを行う。このチューニングは、実質的にはある種のハイパーパラメータ空間内での学習であると言え、検証データが持っている情報がモデル自体に取り込まれてしまう。その結果、モデルが検証データに対して最適化されてしまい、一度も入力されたことのないデータに対する性能 (汎化性能) はむしろ低下してしまうかもしれない。それを避けるためには、元のデータを訓練、検証とテストの3つに分け、テストデータはモデルの学習時には直接的にも間接的にも一切使用しないことが必要となる。

訓練データと検証データの分離法

テストデータを予め取り分けておいた上で、訓練データと検証データを分離する方法はいくつかある。

  • シンプルなホールドアウト
    MNIST問題やIMDB問題で使用した方法。データセットから単に一部の検証データを取り分ける方法である。データが大量に利用できる場合は有効。
  • K分割交差検証 (KFCV)
    Boston Housing問題で使用した方法。データ数が比較的少ない場合に、検証の精度を上げるために使われる。利用可能なデータセットをk個に等分し、そのうち1セットだけを検証用に取り分けておく。残りのk-1個のデータでモデルを学習させ、取っておいた1セットのデータで検証スコアを算出する。それぞれの分割に対して同様の処理を行ない、最終的な検証スコアはk個のスコアの平均を取ったものとなる。
  • シャッフル付き繰り返しk分割検証
    この手法は、利用可能なデータが少ない状況でモデルの精度を可能な限り上げたいときに有効。ショレさんがKaggleのコンペティションなどで使用した際に有効だったという。基本的にはKFCVを何度か繰り返す手法であるが、それぞれの繰り返しにおいてデータセットをk個に分割する前に、データをランダムにシャッフルするものである。この方法では、(KFCV自体の繰り返し回数) x (KFCVの分割数) 回のモデル訓練と評価が必要であり、計算量が大きくなることに注意。

注意点

機械学習の実践上の注意点。

  • データの代表性
    訓練データもテストデータも両方とも、データセットを正しく代表するものでなければならない。MNISTのように数字の画像データを分類する問題を例に取ると、仮に数字順にデータが並んでいた場合、先頭から8割を訓練データ、残り2割をテストデータとして取ってしまうと、0〜7の数字の画像を使って訓練を行い、8,9のデータに対してテストを行なうことになってしまう。バカバカしい間違いのように思えるかもしれないが、ありふれた間違いなのだそうだ。現実のデータは完全にランダムであることは少なく、普通何らかの形でソート、分類されていることが多いからだと思う。これを防ぐためには、データをランダムにシャッフルした上でテストデータを分離する必要がある。
  • 時系列データ
    上記の例とは逆に、データが時系列で何らかの意味を持っている場合は、データをランダムにシャッフルしてはならない。この種のデータには、天気、株価の変動などがある。もしもデータがシャッフルされていた場合、未来の結果を知っている状態で未来を予測する状態になってしまう。時系列データのテストデータは、訓練データよりも時間的に後のデータを用いる必要がある。
  • データの重複
    仮に、データに重複があり(現実のデータではよくある)、重複したデータが訓練データセットとテストデータセットに分けられてしまった場合、同じデータを用いて学習とテストを行うことになってしまい、テストスコアの妥当性が低下してしまう。データをきちんと前処理して重複データを排除することが必要となる。

特徴量工学 (Feature engineering)

特徴量工学 (Feature engineering) とは、対象データと機械学習アルゴリズムについて人間が持つ知識を使用して、モデルの予測性能が向上するように、データを事前に変換する方法を指す。

ディープラーニングで特徴量工学が不要になったと言われるけど、そんなこともないよという話。

  • 良い特徴量を使えば、問題を少ないリソースでエレガントに解決することができる。たとえば、時計の画像から時刻を読む問題を畳み込みニューラルネットで解くのはバカげている。
  • 良い特徴量を使えば、少ないデータで問題を解くことができる。ディープラーニングモデルが特徴量を学習する能力は、多数のデータが利用できることが前提となる。データが少ない場合は、特徴量として抽出する情報の価値は上がる。

本日はここまで。次回は、過学習を防ぐための手法を取り上げ、「Deep Learning with Python」の前半部分終了の予定。

暗号通貨バブル崩壊中の今こそ読みたい『大暴落 1929』(ジョン・ケネス・ガルブレイス)

古い映画の話で恐縮ですが、『愛と悲しみのボレロ』の冒頭には、「人生には2つか3つの物語しかない。しかしそれらは繰り返されるのだ。その度ごとに初めてのときと同じような残酷さで。」という言葉がありました。
金融市場に眼を向けてみると、バブルとその崩壊、そして忘却という3つの物語が、何度も何度も、初めてのときと同じような残酷さで繰り返されているように見えます。

大暴落1929 (日経BPクラシックス)

大暴落1929 (日経BPクラシックス)

本書は、1929年に発生したアメリカの株式市場のバブルと暴落を扱ったルポタージュです。もともとは1950年代に発売された本ですが、以来60年以上出版され続けています。この本には、単なる経済現象の記録を超えた、時代が変わりテクノロジーが進歩しても変化しない、人間の心理に対する深い洞察が含まれているからなのだろうと思います。

バブルとその崩壊に直面した人間は、今も昔もほとんど変わらない行動をするようです。いったん投機ブームが始まると、皆が群れをなして市場に参加し、懐疑的な警告を発する人間は、好況に水を指すけしからん奴、「反逆者」「破壊工作者」という扱いを受ける。政府の経済政策担当者は、状況を放置しておけば更に酷い結果になるということを理解していても、投機の終了の引き金を引いた「戦犯」と非難されることを恐れ、誰も意味のある介入をすることができない…

特に、株価の崩壊が始まった1929年10月19日から11月初頭までの出来事は、1日1日が時系列に沿って詳細に語られており、陳腐な言い方ですが、金融市場の崩壊を目前にしているような臨場感があります。
すなわち、ひとたび上昇基調が終わり「本当の」下げが来たとしても、崩壊の只中ではそれを認識することができず、皆が「ファンダメンタルズに変化はない」「状況は基本的に健全である」という言葉を口にし続ける。その中でもポジションを持ち続ける人間は、合理的に将来を予測するのではなく、むしろ「政府や大銀行やあるいは何者か利害関係者が集団で買い支えてくれるかもしれない」といった気休めの情報を探し、安心を求めるようになる…

 過去、投機的バブルにおいて全く同じことが繰り返されてきており、現在も、おそらく未来にも、何も変わらず同様の出来事が起こるのだろうと思います。
私が長いレビューを書いて紹介するよりも、皮肉で辛辣な名文家であるガルブレイスの文章をそのまま引用したほうがはるかに良い紹介になると思うので、いくつか印象に残っている箴言を抜粋しておきたいと思います。

・私は予想しない。予想というものは、当たったことは忘れられ、外れたことだけが記憶に残る。それでも、目の前にあるのが昔から繰り返されてきたおなじみのことだとは言える。(p.7)

・時が経つとともに、値が上がるという事実だけに目を奪われ、なぜ上がるのかを考えようとしなくなるのは、投機のもう一つの特徴である。(p.21)

・知識があろうとなかろうと、不況の到来を予想することは誰にもできないのであって、それは当時もいまも変わらない。(p.51)

・経済というものは、毎度のことながらはっきりしたターニング・ポイントは示してくれない。きっかけとなる出来事はいつも曖昧で、どれが発端だったのかわからないことさえある。(p.140)

・この種のことではまちがった根拠で正しい結論に達しても評価されないのが現代社会での通例で、立派な方法でまちがう方がはるかによいとされる。(p.141)

・およそどんなきっかけからでも崩壊するというのが、投機ブームの性質だからである。(中略)何が最初のきっかけになったのか、それはわからない。それにたとえわかったとしても、さほどの意味はないのである。(p.153-154)

・[フーバー大統領が行なっていたことは] 何かをするためでなく、何もしないために開く集まりである。これは現代でもさかんに行われている。(中略) やるべき仕事があるからではなくて、やるべき仕事をやっているという印象を与えるために開く会合というものもある。(p.226-227)

・事態が悪化していると知りながら、人はあの言葉を口にするのだ--状況は基本的に健全であると。(p.308-309)

【Deep Learning with Python】ディープラーニングの基礎と構成要素

前回まで、書籍のサンプルプログラムを動作させてみた。ここでは、ディープラーニングの動作原理の基礎と構成要素を簡単にまとめる。「Deep Learning with Python」本の中では、2章〜4章にまたがって説明されている内容をまとめたもの。

ニューラルネットの計算は一体何をしているかと言うと、「前の層から入力された値の重み付き和を取った後、バイアス項として定数を加える。得られた値に対して何らかの関数 (ほとんどの場合において非線形関数) を適用する」という計算をしている。Pythonのコードで書くと以下の通り。

output = relu(dot(W, input) + b)

各変数はテンソルである。数学の人には怒られそうだけど、実務的にはNumpyの多次元配列と思っておけば良い。inputが入力、Wが重み、bがバイアスを表す。また、関数dotはテンソルのドット積で、reluはランプ関数(活性化関数)を表す。

活性化関数

上記の例では、「relu」が活性化関数。特別な理由が無い限り、隠れ層の活性化関数は非線形関数を用いる。複数の線形演算は合成すれは1つの演算として表現できてしまうため、線形演算を使ってしまうと層を重ねる意味がなくなってしまうから。
Kerasでデフォルトで使える活性化関数は以下の通り。もちろん自分で活性関数を定義することもできるがここでは省略。

  • softmax
  • elu
  • selu
  • softplus
  • softsign
  • relu
  • tanh
  • sigmoid
  • hard_sigmoid
  • linear

代表的な活性化関数をプロットしてみた。順不同。スペースの関係で、0以上の値を取るものと負値を取るものを分けている。

f:id:liaoyuan:20180207214953p:plainf:id:liaoyuan:20180207214956p:plain

「常にこれを選べば正解」というような万能の活性化関数は存在しないらしい… 何となく、ReLUやtanhはよく使われているのを見かける。ただし、問題によって出力層の活性化関数はある程度限定される。MNIST問題のように多クラス分類であれば、出力層にsoftmax関数を用いる。softmax関数の出力は、全てが正値で総和が1となるように正規化される。つまり、softmaxの出力は、それぞれのクラスに分類される確率として解釈できる。あるいは、Boston Housing問題のように任意の正値への回帰を行う場合は、活性化関数を使用しない(線形関数の層を使う)、など。

ニューラルネット

さて、ニューラルネットのモデル全体の模式図は下図の通り。

f:id:liaoyuan:20180207215355p:plain

入力Xとそれに対応するターゲット(真の値)Y が与えられる。モデルの演算による予測値Y'と真の値Yとの差を、損失関数 (loss function)を用いて比較し、損失関数の値が小さくなるよう最適化関数 (optimizer) を使って重み(weights) wを更新する。この計算を繰り返して、損失関数の値が小さくなるような重みを選ぶ。

model.compile(optimizer='rmsprop',
                         loss='binary_crossentropy',
                         metrics=['accuracy'])

最適化手法と損失関数は、compile時に引数として指定する。それぞれ、引数のoptimizerとloss。また、metricsは評価関数を表し、モデルによる予測の正確さを評価する指標を指定する。損失関数と評価関数は似ているが、評価関数はモデルの訓練のために直接使用される値ではないという違いがある。たとえば、分類問題を評価する「正確さ」は、直接的に最適化することができない値であるので、代わりに交差エントロピーなどの損失関数を最適化することで、間接的に正しい分類を実現する。(p.113)

損失関数 (loss)

損失関数は、どんな基準でニューラルネットの予測の精度を測定するかを定める関数。目的関数 (objective) と呼ばれることもある。どの損失関数も、値が小さいほど予測が正確であるということを意味する。
kerasで使える損失関数は以下の通り。活性化関数と同じく、自分で損失関数を定義することもできる。

  • 平均二乗誤差(mse:誤差の二乗和)
  • 平均絶対誤差(msa:誤差の絶対値の和)
  • 平均絶対誤差率(mspa:誤差の正解値による商(誤差率)の絶対値の和)
  • 対数平均二乗誤差(msle:「誤差に1を加えた値の対数」の差の二乗和)
  • ヒンジ損失の和(hinge)
  • ヒンジ損失の二乗和(squared_hinge)
  • バイナリ交差エントロピー(binary_crossentropy)
  • カテゴリ交差エントロピー(categorical_crossentropy)
  • スパースカテゴリ交差エントロピー(sparse_categorical_crossentropy)
  • カルバック・ライブラー情報量(kullback_leibler_divergence)
  • ポアソン (poisson)
  • コサイン類似度(cosine_proximity)

ヒンジ損失は、予測値と正解値の積について、1以上で0、1以下で 1-x となる関数(同符号であれば損失が小さい)。

交差エントロピー相互情報量 (2つの確率の相互依存度) とほぼ同じ。分類問題で使われ、分類が正しければ0に、無秩序であるほど1に近付く。

他はあまり分からないのでもう少し調べたい…

最適化関数 (optimizer)

最適化手法も多数存在するけど、結局のところ、全て損失関数の値が減少するように重みを変化させるための手法である。
どの最適化手法も、損失関数の微分 (勾配) の値が少しずつ小さくなるように重みを変化させるという方法を取る。なぜ勾配を小さくするかというと、勾配がゼロの点で誤差関数が極小を取るため。もちろん、極値が損失関数の最小値とは限らない (=局所最適解に陥る可能性もある) し、手法によって収束までの速さも異なる、あるいはそもそも学習が上手く収束しない可能性もあったりするけど、いろいろな要素を考慮しながら最適化できる手法が考案されていて、kerasで使える最適化関数は、sgd, rsmprop, adagrad, adadelta, adam, adamax, nadamというものがある。

ショレさん曰く、「rmspropオプティマイザはどんな問題に対してもまあまあ良いチョイスで、何を使うかそんなに気にしなくてもいい」(p.77) とのこと。別の記事によると、最近はrmspropよりも新しめの(?) adamなどもよく使われるのだとか。

出力層の活性化関数と損失関数

隠れ層の活性化関数と最適化関数については、明確にどれを選べば良いという基準は無いみたい。ただし、損失関数および出力層の活性化関数は、対象とする問題の種類によって定められる。

表4.1

問題の種類 出力層の活性化関数 損失関数
二値分類 sigmoid バイナリ交差エントロピー
多クラス単一ラベル分類 softmax カテゴリ交差エントロピー
多クラス多ラベル分類 sigmoid バイナリ交差エントロピー
任意値の回帰 なし 平均二乗誤差
0〜1の回帰 (確率) sigmoid 平均二乗誤差、バイナリ交差エントロピー

【Deep Learning with Python】Boston Housing data (番外編)

前回からの続き。前回は、ボストン郊外の不動産物件に関するデータを元に、その地域の住宅価格をディープラーニングで予測するというサンプルを動かしてみた。しかし、結果を見ただけではあまりにもブラックボックスすぎるので、少しだけディープラーニングのモデルとデータそのものを調査してみたい。

学習したニューラルネットのパラメーター(重み)は、次の関数を使って取得することができる。

>>> w = model.get_weights()
>>> print(w)
[array([[  1.89665943e-01,   6.76303506e-02,  -1.39802471e-01,
         -3.92748147e-01,  -1.18188962e-01,   1.32170185e-01,
          7.21744308e-03,  -2.46567175e-01,  -2.09330872e-01,
         -4.52335358e-01,   1.95683718e-01,  -2.12740257e-01,
         -2.55996019e-01,   5.20668104e-02,  -1.04660824e-01,
          1.62942894e-02,  -3.40786546e-01,   5.63649461e-03,
         -4.61874083e-02,  -3.72445881e-02,   5.90579249e-02,
         -1.81242540e-01,   1.27426997e-01,  -8.46400037e-02,
         -7.04784617e-02,  -2.00169012e-01,  -2.92712718e-01,
         -2.11299419e-01,  -3.25609922e-01,  -2.05949292e-01,
          4.42221835e-02,   4.28706445e-02,   1.23807281e-01,
         -1.86553150e-01,  -6.58548903e-03,  -2.20981717e-01,
         -1.25058562e-01,  -1.67351410e-01,  -3.25124830e-01,
         -2.76255041e-01,  -1.35442063e-01,   2.48270392e-01,
         -7.80096650e-02,  -3.53677630e-01,   1.83140397e-01,
         -9.01148394e-02,   3.61068577e-01,   3.04216117e-01,
         -2.11137474e-01,  -1.11838944e-01,   1.49960369e-01,
         -1.57415003e-01,   1.39597610e-01,  -4.57416521e-03,
          4.08897176e-02,  -3.75599861e-01,  -5.54241473e-03,
         -7.18339346e-03,  -2.24389568e-01,   3.27147963e-03,
         -6.52354211e-02,  -1.09974429e-01,   6.16966793e-03,
          3.89479995e-02],
(省略)

こんな値が5000個以上続く。なるほど、何も分からない。

重みの全体的な傾向を見るために、各層のノードごとに重みの二乗和を取ってプロットしてみたものが以下の図。

plt.xkcd() # 手書き風グラフ

for i in range(len(model.layers)):
    # ニューラルネットの各層について、重みの絶対値の総和をプロット
    w1 = model.layers[i].get_weights()[0]
    plt.figure()
    plt.plot(range(1, len(w1)+1), (w1**2).sum(axis=1), 'o-')
    plt.show()

f:id:liaoyuan:20180205212629p:plainf:id:liaoyuan:20180205212634p:plainf:id:liaoyuan:20180205212637p:plain
ニューラルネットの重みの絶対値の総和

この値が大きいほど、入力値が次の層に与える影響が大きくなることを意味するのだそうだ。

隠れ層の重みの意味はあまりよく分からないが、入力層に注目してみると7、10、13番目の項目が比較的大きな寄与をしていることが分かる。

前回の記事を見て、特に重みの値が大きい13番目の説明変数を意味をみると、これはLSTATすなわち低所得者の割合を表したものである。部屋数や築年数といった物件自体のデータよりも、所得の寄与が大きいというのは意外に感じる。低所得者が多い場合、そもそも住居費として支出される金額自体が少なくなるのかもしれない。ちなみに、7番目の説明変数は「1940年よりも前に建てられた家屋の割合」、10番目は「10,000ドルあたりの所得税率」を表すようだ。

そんなわけで、一番寄与の大きい説明変数LSTATと目的変数の関係を調べるために、この2つのデータから散布図を作成してみる。

from keras.datasets import boston_housing
(train_data, train_targets), (test_data, test_targets) = boston_housing.load_data()

# 13番目の説明変数と目的変数を散布図としてプロット
plt.title("Boston House Prices")
plt.ylabel("Price ($1000)")
plt.xlabel("lower status of the population (%)")

plt.scatter(train_data[:,12], train_targets, s=7)
plt.show()

f:id:liaoyuan:20180205213145p:plain

低所得者の割合が増加すると、住宅価格も穏やかに低下していく傾向が見える。

回帰分析を用いたBoston Housing dataの予測

ディープラーニング本からは脱線するけど、LSTATと住宅価格の関係を回帰分析して、結果を比較してみたい。まずは、簡単に1次式で線形回帰する。

from sklearn import linear_model
from sklearn.metrics import mean_absolute_error

LSTAT = train_data[:,12] # 低所得者の割合

# 線形回帰
Liner = linear_model.LinearRegression()
Liner.fit(LSTAT.reshape(-1,1), train_targets)

# 回帰式 y=ax+b
a = Liner.coef_
b = Liner.intercept_
y = a*LSTAT + b

# 散布図と予測グラフをプロット
plt.title("Boston House Prices")
plt.ylabel("Price ($1000)")
plt.xlabel("lower status of the population (%)")
plt.scatter(LSTAT, train_targets, s=7)
plt.plot(LSTAT, y, "r")

# テストデータに対して、回帰式のパラメータa, bを用いて予測
test_pred = a*test_data[:,12] + b

そして、平均絶対誤差(mean absolute error)を計算してみる。テスト誤差は4110ドル程度で、当然だが前回のディープラーニングの誤差2550ドルよりも悪い。

>>> print(mean_absolute_error(train_targets, y)) # 訓練誤差
    4.57378020095 
>>> print(mean_absolute_error(test_targets, test_pred)) # テスト誤差
    4.11751840112

f:id:liaoyuan:20180205213734p:plain

更に、多項式回帰を試してみる。scikit-learnではPolynomialFeaturesという多項式基底を作成できる関数があるので、これを用いる。なお、pipelineを使えば基底の作成とモデルの学習までの処理をまとめることができる。pipelineについてはこちらのブログを参照した。

from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline

LSTAT = LSTAT.reshape(-1,1)

# 多項式回帰
regr = Pipeline([
    ('poly', PolynomialFeatures(degree=5)),
    ('linear', linear_model.LinearRegression())
])
regr.fit(LSTAT, train_targets)

# 回帰曲線を計算
xt = np.linspace(0.0, 40.0, num=1000).reshape((1000, 1))
yt = regr.predict(xt)

# 散布図と回帰曲線をプロット
plt.scatter(LSTAT, train_targets, s=7)
plt.plot(xt, yt, c="r")
plt.title("Boston House Prices")
plt.ylabel("Price ($1000)")
plt.xlabel("lower status of the population (%)")
plt.ylim(0,52)

多項式基底の次元をいくつか試してテスト誤差が小さくなるように選んだところ、5次式を用いた場合で以下の値となった。いくら貧困率が高くても住宅価格が0になってしまうことはないだろうから若干過学習気味だけど、分析のベースラインとしてはそれほど悪くないかもしれない。

>>> print(mean_absolute_error(test_targets, test_pred))
    3.934188344856719

f:id:liaoyuan:20180205213805p:plain

もちろん、ディープラーニングによる予測と同じ条件で比較するなら、全ての説明変数を用いて重回帰分析やらをする必要があるのだろうけど、更に脱線しそうなのでこのあたりで止めておきます。

【Deep Learning with Python】Boston Housing Datasetを用いた回帰問題

Deep Learning with Python」のサンプルプログラム3つ目。

データはBoston Housing Dataを用いる。1970年代のボストン郊外地域の不動産物件に関するデータで、ある地域の平均物件価格と部屋の数や築年数といった物件情報、犯罪率や黒人比率などの人口統計に関する属性が付属している。つまり、ある地域の不動産の属性を元にして、その地域の平均物件価格を予測する問題である。先の2つの分類問題とは異なり、これは回帰問題に属する。つまり、データを分類するものではなく、学習データからある1つの連続量を予測するものである。

どうでもいいけど、最近、核戦争が起きて荒廃した未来のボストンを舞台にしたゲームを遊んでいるので、ボストンという地名に反応してしまう (笑)

例によってkerasのインターフェイスからデータセットをロードする。

from keras.datasets import boston_housing
(train_data, train_targets), (test_data, test_targets) = boston_housing.load_data()

目的変数 (住宅価格) は以下の通り。単位は千ドルで、少し安いと思うかもしれない。これは1970年代の国勢調査のデータで、インフレ調整していないことが原因。

>>> train_targets
        [ 15.2,  42.3,  50. ...  19.4,  19.4,  29.1]

予測の元となる説明変数は全部で13種類あり、それぞれの意味は以下の通り。

  • CRIM・・・犯罪発生率(人口単位)
  • ZN・・・25,000平方フィート以上の住宅区画の割合
  • INDUS・・・非小売業の土地面積の割合(人口単位)
  • CHAS・・・チャールズ川沿いかどうか(1:Yes、0:No)
  • NOX・・・窒素酸化物の濃度(pphm単位)
  • RM・・・1戸あたりの平均部屋数
  • AGE・・・1940年よりも前に建てられた家屋の割合
  • DIS・・・ボストンの主な5つの雇用圏までの重み付き距離
  • RAD・・・幹線道路へのアクセス指数
  • TAX・・・10,000ドルあたりの所得税
  • PTRATIO・・・教師あたりの生徒の数(人口単位)
  • B・・・黒人居住者の割合(人口単位)
  • LSTAT・・・低所得者の割合

ボストンデータセットは、以前に取り上げたMNIST、IMDBデータセットとは次の2点で少々異なる。

1点目は、データの数が506件と比較的少数であることで、検証用データをここから取り分けるため訓練に使用できるデータは更に減る。2点目は、それぞれの説明変数が異なるスケールを持っていることである。上記の通り、割合や人口比といったパーセントで表されるデータもあり、部屋数の平均のように数値で表されるデータもあり、川沿いか否かという0/1のデータもある。もちろんこのままニューラルネットに入力することもできるが、学習が難しくなる可能性がある。

上記2点のデータ特性に対応するため、ちょっとした操作が必要となる。

データの正規化

まずは2点目の特性に対応するため、データの正規化を行う。これは、スケールが揃っていない説明変数を、平均0、分散1となるように変換するもの。

Numpyを使ってこの処理は簡単に書ける。

# 前処理
# データを正規化(平均0、分散1に変換)する
mean = train_data.mean(axis=0)
train_data -= mean
std = train_data.std(axis=0)
train_data /= std

test_data -= mean
test_data /= std

ニューラルネットの定義

続いて、1点目の特性である「データセットの少なさ」に対する対応。一般に、データのサンプル数が少なく学習モデルのパラメータ数が多い場合、過学習が起こりやすい。今回のデータも比較的少数であるため、過学習を避けるために比較的小さなニューラルネットを用いる。ここでは、隠れ層が2層、それぞれ64ノードのモデルを用いる。

# モデル定義
#KFCV用に同じモデルを複数回作成するため、関数にする
def build_model():
    model = models.Sequential()
    model.add(layers.Dense(64, activation='relu',
                           input_shape=(train_data.shape[1],)))
    model.add(layers.Dense(64, activation='relu'))
    model.add(layers.Dense(1))
    model.compile(optimizer='rmsprop', loss='mse', metrics=['mae'])
    
    return model

 
ニューラルネットの出力層は、活性関数の無い単一ノードを用いる。(線形関数の層となる) ここでは、大きな値を取る可能性がある住宅価格を予測するものであるため、活性関数は使用していない。回帰問題の場合でも、出力の範囲を制限したい場合は何らかの活性関数を用いる場合がある。たとえば、確率値として0〜1の値を求めたい場合は、活性関数としてシグモイド関数を用いる。また、損失関数は、回帰問題でよく用いられるmse (mean squared error)を用いる。次に出てくるK分割交差検証のために、新しいモデルを何度も作る必要があるため、後で再利用できるように関数化している。

K分割交差検証

更に続いて、データセットの少なさに対する対応。訓練したニューラルネットの予測を検証する際に使うデータの選び方によって、検証結果の変動が大きくなる (高variance) 可能性がある。これを防ぐために、K分割交差検証 (K-fold cross validation; KFCV) という方法を用いる。

ただし、"Deep Learning with Python"のKFCVを説明した図3.11には大きな誤植がある。Fold 2とFold 3の1つ目の四角は、Trainingでなければならないが、Validationと書かれてしまっている。コードや説明文には誤りはないので丁寧に読んでいれば間違えることは無いと思うけど、混乱する…

f:id:liaoyuan:20180208192457p:plain

(正しい)図を見ればだいたい理解できるはずだけど、KFCVの方法は次の通り。データセットをk個(今回はk=4)に均等に分割し、k番目のデータを検証用に取り分け、それ以外のk-1個のデータでニューラルネットを訓練する。これをk回繰り返して、最終的な検証スコアはk回の平均を取ったものになる。

上記の考え方を素直にコードに落としたものがこちら。

k = 4
num_val_samples = len(train_data) // k
num_epochs = 100
all_scores = []

# K-fold cross validation (K-分割交差検証; KFCV)
for i in range(k):
    print('processing fold #', i)
    val_data = train_data[i * num_val_samples: (i+1) * num_val_samples]
    val_targets = train_targets[i * num_val_samples: (i+1) * num_val_samples]
    
    partial_train_data = np.concatenate(
    [train_data[:i * num_val_samples],
     train_data[(i+1) * num_val_samples:]],
    axis=0)
    
    partial_train_targets = np.concatenate(
    [train_targets[:i * num_val_samples],
     train_targets[(i+1) * num_val_samples:]],
    axis=0)
    
    model = build_model()
    model.fit(partial_train_data, partial_train_targets,
              epochs=num_epochs, batch_size=1, verbose=0)
    
    val_mse, val_mae = model.evaluate(val_data, val_targets, verbose=0)
    all_scores.append(val_mae)

訓練中の検証スコアを記録してプロットする。グラフを見やすくするために、学習過程の最初の10回は無視して、また平滑化を行う。
f:id:liaoyuan:20180203004144p:plain

だいたい80回前後くらいで過学習が始まって検証スコアが悪化を始めているようだ。

終結

上記までが学習回数の検討段階で、次で最終的なモデルの訓練と予測を実施する。学習エポック数を80回に固定して、モデルの学習を行う。

model = build_model()
model.fit(train_data, train_targets,
  epochs=80, batch_size=16, verbose=0)
test_mse_score, test_mae_score = model.evaluate(test_data, test_targets)

最終的な結果は、平均絶対誤差(Mean Absolute Error)の値で2550ドル程度…らしい。平均絶対誤差とは、予測の値と真の値の差を取り、差の絶対値を平均したもの。完璧に予測できていれば0となるので、これは小さければ小さいほど予測が正確であるということを意味する。

>>> test_mae_score
    2.5532484335057877

これで3章の内容は終わりだけど、この結果だけでは予測過程がブラックボックスなので、次回はこの結果を可視化する方法を検討してみたい。

"Deep Learning with Python" ボストンの住宅価格予測のサンプル · GitHub