勾配ブースティング決定木を理解する

本記事では、機械学習コンペなどでよく見られる勾配ブースティング決定木(gradient boosting decision tree)を説明します。勾配ブースティング決定木は、MNISTデータに対して、ニューラルネットの最高精度と同等の精度を出したり、また高速な実装xgboostなどで有名な手法です。ライブラリを使用している方も多いと思いますが、意外とどのような構造になっているかを知らない人もいるかもしれません。

そこで、本記事では、決定木とは何か、というところから始めて、アンサンブル学習、勾配ブースティング決定木について見ていきます。

決定木

決定木(decision tree)は、データに対して一連の質問を与えることによって、目標に対する決定を下す予測モデルです。決定木の優れた点として、意味解釈性(interpretability)が挙げられます。予測モデルには、与えられたデータから得られる結果の過程がブラックボックス、すなわちどうしてその結果が導かれたのかがわかりづらいものが多く存在します。一方、決定木では、その木構造からどうやってその結果が得られたかを解釈しやすい、という人にとって非常に嬉しい性質を持ちます。

f:id:hiyoko9t:20171203024359p:plain
決定木の例

この図では、天気予報が晴れなら海に行く、曇りならランニング、雨なら映画を見るというように与えられたデータ(=天気情報)から次の行動(=その日の予定)を予測しています。これは、分類問題の例ですが、決定木は回帰問題、すなわち連続値を扱うような問題にも適用できます。例えば、身長であれば170cm以上、もしくはそれより下、というようにわけることで分類問題のように木を成長させることができます。分類問題と回帰問題、それぞれに対する決定木を分類木(classification tree)回帰木(regression tree)と呼びます。

決定木は、その根から始めて、後述する情報利得(information gain) が最大となるようにデータを分割し、その葉が純粋(=葉の要素が全て同じクラスに属する)となるまで分割を行います。ただし、実際は葉が純粋になるまで決定木を成長させると、非常に構造が深くなり、過学習に陥りがちです。そこで、最大の深さを設定するなどにより、木が複雑になりすぎるのを防ぎます。

情報利得

決定木を分割する上で、はじめに考えなければいけないのはデータをどのように分割するか、ということです。この問いに一つの答えを与えてくれるのが情報利得であり、情報利得は分割された要素のばらつきの減少をあらわします。

例えば、クラスAに属するデータが40個、クラスBに属するデータが40個、計80個のデータを決定木を用いて分割することを考えます。

f:id:hiyoko9t:20171203024446p:plain
分割の例

さて、ここで決定木1と決定木2では、どちらの分割が、(語弊を恐れずに言えば)優れているのでしょうか。直感的には、決定木2であれば、右側の葉の要素はすべてクラスAに属しており、これ以上葉を増やさなくて済むので、良さそうに見えます。これを、式で裏付けていきます。

まず、決定木を二分決定木、すなわち親ノードは二つの子ノードまでしか持たない、を仮定します。このとき、$I$を不純度、すなわち異なるクラスがどれだけ混じっているか、とすると、情報利得$IG$は以下の式であらわされます。

$$ IG(D_p) = I(D_p) - \frac{N_{left}}{N_p} I(D_{left}) - \frac{N_{right}}{N_p} I(D_{right}) $$

ここで、それぞれの変数は以下で定義されます。

すなわち、情報利得とは、「親の不純度」と「子の不純度の総和」の差であると言えます。

不純度として、代表的なものに以下が挙げられます。

  • ジニ不純度(gini impurity) : $I_G = \sum_{i=1}^c p(i|t) (1 - p(i|t))$
  • エントロピー(entropy) : $I_H = - \sum_{i=1}^c p(i|t) log_2 p(i|t)$
  • 分類誤差(classification error) : $I_E = 1 - max( p(i|t) )$

ここで$p(i|t)$は、ノード$t$においてクラス$i$に属するサンプルの割合、$c$はクラス数をあらわします。例えばクラス数が二つのときのあるノードtにおけるエントロピーは次のようになります。 $$ I_H = - \sum_{i=1}^c p(i|t) log_2 p(i|t) = - (p(1|t) log_2 p(1|t) + p(0|t) log_2 p(0|t)) $$ ここで、$t$におけるデータがすべてクラス1に属するとすると、エントロピーは次のようになります。 $$ I_H = - \sum_{i=1}^c p(i|t) log_2 p(i|t) = - (1 log_2 1 + 0 log_2 0) = 0 $$ ただし、$0 log_2 0=0$とします。

これより、あるノードにおいて、全ての要素が一つのクラスに属しているとき、エントロピーは最小となります。一方、各クラスが一様に分布している時エントロピーは最大となります。

これに基づいて先ほどの例の情報利得を求めてみます。

f:id:hiyoko9t:20171203024209p:plain
各決定木の情報利得

これより、決定木2の方が情報利得が大きい、すなわち"良い"分割であることがわかりました。

上の例では、不純度としてエントロピーを用いましたが、どれを用いるのが良いのでしょうか。それを確認するために、ニクラス分類における三つの不純度を比較してみます。図の横軸はあるノードにおける要素のクラス1に属する割合、縦軸は不純度をあらわします。

f:id:hiyoko9t:20171203024558p:plain
不純度の比較

上図からわかるように、ジニ不純度とエントロピーが近く、分類誤差が少し異なることがわかります。実際、先ほどの例で分類誤差を計算して見ると、面白いかもしれません。結論としては、不純度は特に悩まずジニ不純度かエントロピーを用いるのが無難だと思います。

以上より、"良い"分割方法がわかったので、情報利得が最大となるような質問を、葉が純粋になるまで繰り返すことで、決定木が得られます。

アンサンブル学習

アンサンブル学習とは、分類/回帰問題に対して、複数の予測器を組み合わせる手法を指します。アンサンブル学習の根幹にある考え方は、弱い学習器を組み合わせることにより、複雑な問題に対する強い学習器が得られる、というものです。アンサンブル学習には、代表的な手法としてバギングとブースティングが挙げられます。

バギング

訓練データからランダムに復元抽出をM回繰り返すことにより、M組の新たなデータ集合を得る手法をブートストラップ法(bootstrap)と呼びます。

f:id:hiyoko9t:20171203102038p:plain
復元抽出と非復元抽出

バギング(bagging)は、こうして得られたM組のデータに対して、それぞれ予測器を学習させて、予測器の重み付け和を用いて予測を行う手法です。 訓練データ集合$D$からブートストラップ法で得られたM組のデータセットを$D_1$, ..., $D_{M}$とし、データ集合$D_i$で学習した予測器を$h_i$とします。 このとき、回帰問題では次のように予測を行います。 $$ H(x) = \frac{1}{M} \sum_{i=1}^{M} h_i(x) $$ 分類問題では最多票を得たクラスを分類結果とします。 $$ H(x) = argmax \sum_{i=1}^{M} I(h_i(x) = t) $$

ブースティング

バギングは各予測器を並列に学習できるという利点がありますが、独立に予測器を学習させるため多様性が生まれにくいという欠点があります。そこで、独立に学習するのではなく、逐次的に予測器を学習させる方法が提案されました。これをブースティング(boosting)と呼びます。すなわち、前段の分類器$h_i(x)$で誤分類されたデータに大きな重みが与えられ、後段の分類器$h_{i+1}(x)$の学習に利用されます。また、各分類器では、誤り率$\epsilon_{i}$が計算され、それに応じた信頼度$\alpha_{i}$が計算されます。この信頼度により、重み付けされた各学習器の和を用いて分類を行います。

勾配ブースティング決定木

ここまでの話から、勾配ブースティング決定木は、決定木を用いてアンサンブル学習、特にブースティングを用いる手法だということがわかると思います。では、どのように前段の決定木から後段の決定木を生成するのでしょうか。

そこで、まずは次の目的関数を考えます。 $$ obj(\theta ) = L(\theta ) + \Omega (\theta ) $$ ここで、$L$はロス関数、$\Omega $は正則化項をあらわします。ロス関数の例としては、次のようなものが挙げられます。

  • 二乗誤差(squared error): $L(\theta ) = \sum_{i} (y_i - \hat{y}_i)^2$
  • ロジスティックロス(logistic loss): $L(\theta ) = \sum_{i} [(y_i ln(1+e^{-\hat{y}_i}) +(1-y_i) ln(1+e^{\hat{y}_i})]$

ただし、$y_i$は真の値、$\hat{y}_i$は予測値をあらわします。

いま、K個の決定木$f_k$を用いて予測を行うとき、$i$番目のデータ$x_i$に対する予測値は次のようになります。 $$ \hat{y}_i = \sum_{k=1}^{K} f_k(x_i) $$ このとき、目的関数はロス関数を$l$とすると、次のようにあらわされます。 $$ obj(\theta ) = \sum_{i=1}^n l(y_i, \hat{y}_i) + \sum_{k=1}^{K} \Omega (f_k ) $$

各反復での訓練

$t$番目の決定木$f_t$を求めるために、$t$番目の予測値$\hat{y}^{(t)}_{i}$について考えます。 $$ \hat{y}^{(0)}_{i} = 0 \\ \hat{y}^{(1)}_{i} = f_1(x_i) = \hat{y}^{(0)}_{i} + f_1(x_i) \\ ...\\\ \hat{y}^{(t)}_{i} = \sum_{k=1}^{t} f_k(x_i) = \hat{y}^{(t-1)}_{i} + f_t(x_i) $$ これより、$t$番目の目的関数は次のようになります。 $$ obj^{(t)} = \sum_{i=1}^n l(y_i, \hat{y}^{(t-1)}_{i} + f_t(x_i) ) + \Omega (f_t ) + Const $$ ここで、$t-1$番目までの決定木はfixなので、正則化項は$t$番目の決定木のみ考えています。 ここで、ロス関数をテイラー展開し、二次の項までで打ち切った目的関数を考えます。 $$ obj^{(t)} = \sum_{i=1}^n \left[ l(y_i, \hat{y}^{(t-1)}_{i}) + g_i f_t(x_i) + \frac{1}{2} h_i f_t^2(x_i) \right] + \Omega (f_t ) + Const $$ ただし、$g_i$、$h_i$はそれぞれロス関数の$\hat{y}_i^{(t-1)}$における一階微分と二階微分をあらわします。

以上より、$t$番目の目的関数の定数部分を除いた式は、次のとおりです。 $$ \sum_{i=1}^n \left[ g_i f_t(x_i) + \frac{1}{2} h_i f_t^2(x_i) \right] + \Omega (f_t ) $$ これを最小化するように$t$番目の決定木$f^{(t)}$を構築します。 上式より、$g_i$、$h_i$が与えられれば、$f^{(t)}$を求めることができ、これがまさにXGBoostがサポートしているロス関数の条件となります。

モデルの複雑性

次に正則化項の定義を与えます。正則化項の役割としては、モデルが複雑になりすぎることを防ぐ、すなわち汎化性能を高めることにあります。

まず、葉の数が$T$個であると仮定します。ここで$x$が与えられたとき、それをどの葉に割り当てるかを決定する関数を$q:R^d \rightarrow${$1, 2, ..., T$}とします。さらに、葉のスコアのベクトルを$w$とすると、$t$番目の決定木のスコアは次のようにあらわされます。 $$ f_{t}(x) = w_{q(x)} $$ XGBoostでは、これを用いて正則化項を次のように定義します。 $$ \Omega(f) = \gamma T + \frac{1}{2} \lambda \sum_{j=1}^{T} w_{j} $$ これにより、葉の数(木構造の深さ)$T$と各ノードの値$w$が過剰に大きくなりすぎることにペナルティを与えることができます。

最適解の導出

ロス関数と正則化項より、目的関数は次のようにあらわされます。($Const$は無視) $$ Obj^{(t)} = \sum_{i=1}^n \left[ g_i w_{q(x_i)} + \frac{1}{2} h_i w_{q(x_i)}^2 \right] + \gamma T + \frac{1}{2} \lambda \sum_{j=1}^{T} w_{j} $$ これを整理すると、 $$ \sum_{j=1}^T \left[ (\sum_{i \in I_j} g_i) w_{j} + \frac{1}{2} (\sum_{i \in I_j} h_i + \lambda) w_{j}^2 \right] + \gamma T $$ ただし、$I_{j} =${$i|q(x_i)=j$}となるインデックス集合です。 さらに、 $$ G_j = \sum_{i \in I_j} g_i, H_j = \sum_{i \in I_j} h_i $$ とおくと、以下のようになります。 $$ Obj^{(t)} = \sum_{j=1}^T \left[ G_j w_{j} + \frac{1}{2} (H_j + \lambda) w_{j}^2 \right] + \gamma T $$ ここで、$w_j$は独立であり、二次関数の形から最適解とそのときの値は次のように与えられます。 $$ w^{*}_j = - \frac{G_j}{H_j + \lambda} $$ $$ Obj^{*} = - \frac{1}{2} \sum_{j=1}^{T} \frac{G_j^2}{H_j + \lambda} + \gamma T $$ それぞれの値のイメージは以下となります。

f:id:hiyoko9t:20171203172530p:plain
決定木の構造(引用[3])

実装例

以上のアルゴリズムを実装した例がこちら( GitHub - nyk510/gradient-boosted-decision-tree: 勾配ブースティングのpythonによる実装 )にあるので、実行してみます。実装もわかりやすいので、アルゴリズムを細かく追いたい人は、オススメです。

分類問題の訓練データは、$[1, 1]$、$[-1, -1]$を中心としたガウス分布からのサンプリングとなっています。

f:id:hiyoko9t:20171203172954p:plain
分類問題に対する予測

回帰問題の訓練データはsin関数にガウス分布からのノイズを付与したものとなります。

f:id:hiyoko9t:20171203173033p:plain
回帰問題に対する予測

分類、回帰ともに訓練データに対する予測ができていることがわかります。

まとめ

少し飛ばし気味のところもありますが、勾配ブースティング決定木について見てきました。より詳しく知りたい方は、参考の方をどうぞ。

この記事を書くきっかけとなったのが、現在開催中のkaggleの某コンペなので、そちらも参加される人は頑張りましょう。

不定期更新ですがよろしくお願いします。 何かあればコメントやツイッター(@hiyoko9t)でお気軽にお声がけください。

参考

[1] Sebastian Raschka, "Python 機械学習プログラミング", インプレス, 2016.
[2] nyk510, GitHub - nyk510/gradient-boosted-decision-tree: 勾配ブースティングのpythonによる実装
[3] Introduction to Boosted Trees — xgboost 0.6 documentation, 2015.

TensolFlowのチュートリアルを全部やってまとめてみました

本記事では、機械学習ライブラリとして有名なTensorFlowの、公式ドキュメントのチュートリアルをまとめて紹介します。TensorFlowのチュートリアルは、それなりに量があるので、必要なものを参照しやすくするために本記事を書きました。

本題に入る前に、今回なぜライブラリとしてTensorFlowを選んだかを簡単にお話ししたいと思います。

f:id:hiyoko9t:20171008163500p:plain
Googleが開発したTensorFlow

と言っても筆者は、機械学習ライブラリを触ったのは、この記事を投稿する一週間ほど前からになるので、他の有名なライブラリ(ChainerやTheano、kerasなど)との違いはほとんどわかりません。

ただ、TensorFlowのマニュアルを流し見していた時に、計算グラフという概念とその可視化が魅力的に映ったので、TensorFlowを選びました。また、世界的にTensorFlowがトップクラスに使用されていることも、導入としてはいいかなと感じ選びました。(ドキュメントの多さなど)

実際、チュートリアルをすべて追ってみて、難しいと感じた部分もありますが、モデルの設計など非常に便利だと感じたので、これから始める人には十分オススメです。

それでは、以下にそれぞれのチュートリアルのリンクと概要、キーワードを示します。本記事では、これらの一つ一つについて、ざっくりとどんなことをしているか、どんなものができあがるか、などを紹介していこうと思います。

チュートリアル 概要 キーワード
Getting Started With TensorFlow  |  TensorFlow tensorflowのインポートから線形モデルを用いた学習 テンソル、計算グラフ、tf.train、tf.estimator
MNIST For ML Beginners  |  TensorFlow MNISTのデータのロード、ソフトマックス回帰を用いた分類 MNIST、ソフトマックス回帰
Deep MNIST for Experts  |  TensorFlow MNISTに対して、ディープな畳み込み層を用いた分類 多層畳み込みネットワーク、畳み込み、プーリング、密結合層、ドロップアウト
TensorFlow Mechanics 101  |  TensorFlow 順伝播型ニューラルネットワークを用いた手書き文字分類器の訓練と評価 順伝播型ニューラルネットワーク
tf.estimator Quickstart  |  TensorFlow high-levelなAPIを用いた分類器の構築 tf.estimator、Iris、DNN
Building Input Functions with tf.estimator  |  TensorFlow 入力関数(input_fn)の作り方 tf.estimator、input_fn、numpy、pandas
TensorBoard: Visualizing Learning  |  TensorFlow TensorBoardを用いた学習の可視化 TensorBoard、学習の可視化、シリアライズ
TensorBoard: Graph Visualization  |  TensorFlow TensorBoardを用いた計算グラフの可視化 TensorBoard、グラフの可視化、tf.name_scope
チュートリアル 概要 キーワード
Using GPUs  |  TensorFlow GPUの使用 CPU、GPU
Image Recognition  |  TensorFlow Inception-v3を用いた画像認識 画像認識、ImageNet、Inception-v3
How to Retrain Inception's Final Layer for New Categories  |  TensorFlow 新しいカテゴリに対するInceptionの最終層の再訓練 Inception、再訓練、最終層
A Guide to TF Layers: Building a Convolutional Neural Network  |  TensorFlow layersモジュールを用いたCNNの構築 CNN、layers、畳み込み層、プーリング層、密結合層
Convolutional Neural Networks  |  TensorFlow CIFAR-10に対するCNNの構築 CNN、CIFAR-10、GPU
Vector Representations of Words  |  TensorFlow 単語のベクトル表現 ベクトル表現、word2vec、embedding、t-SNE
Recurrent Neural Networks  |  TensorFlow 再帰ニューラルネットワーク RNN、LSTM、Pen Tree Bank
Sequence-to-Sequence Models  |  TensorFlow Sequence-to-Sequence Modelsを用いた翻訳 Sequence-to-Sequence Models、ニューラル翻訳
Large-scale Linear Models with TensorFlow  |  TensorFlow tf.estimatorを用いた大規模な線形モデルの構築 大規模な線形モデル、FeatureColumn
TensorFlow Linear Model Tutorial  |  TensorFlow 人口調査データに対する線形モデルを用いた二値分類 線形モデル、二値分類、Census Income Dataset、正則化、ロジスティック回帰
TensorFlow Wide & Deep Learning Tutorial  |  TensorFlow wideモデルとdeepモデルの結合 wideモデル、deepモデル、wide&deepモデル
Improving Linear Models Using Explicit Kernel Methods  |  TensorFlow カーネル法を用いた線形モデルの改善 カーネル法
Mandelbrot Set  |  TensorFlow マンデルブロ集合の可視化 マンデルブロ集合、フラクタル
Partial Differential Equations  |  TensorFlow 偏微分方程式を用いたシミュレーション 偏微分方程式ラプラシアンカーネル、シミュレーション

なお、TensorFlowのチュートリアルは、あくまで実装を中心とした解説になっているので、機械学習の各手法やオプティマイザなどの数学的な背景は前提知識として持っていないと理解しづらい部分もあるかと思います。

以前の記事で、CNNの考え方や最適化アルゴリズムの考え方を紹介しているので、興味のある方はそちらもどうぞ。

hiyoko9t.hatenadiary.jp

hiyoko9t.hatenadiary.jp

Getting Started With TensorFlow

以降のチュートリアルすべての基本となる章なので、前回の記事で、TensorFlowのインストールからライブラリ内で用いられる基本的な概念をすべて翻訳して、紹介しています。

hiyoko9t.hatenadiary.jp

MNIST For ML Beginners

このチュートリアルでは、手書き文字データとして有名なMNISTに対して、ソフトマックス回帰を適用し、適切なラベルを与えることを目的としています。

MNISTは、$28\times 28$のグレイスケール画像で次のようなものが挙げられます。また、各データにはそれが数字の何をあらわしているか、というラベルが与えられています。("5"の画像には"5"というラベルが与えられます)

f:id:hiyoko9t:20171008014018p:plain
MNIST

このMNISTを入力として、以下のネットワークであらわされるソフトマックス回帰を行います。

f:id:hiyoko9t:20171008014113p:plain
ソフトマックス回帰

結果として、92%程度の正解率をもつモデルを構築します。

Deep MNIST for Experts

このチュートリアルでは、前半は「MNIST For ML Beginners」で紹介されている内容とほぼ同様の内容を紹介しており、後半では多層畳み込みネットワークの構築を行います。

ここでは、畳み込み層とプーリング層を重ねたものを二層用意し、その後、密結合層を通して、最終的な出力の層へと遷移します。

f:id:hiyoko9t:20171008030155p:plain
多層畳み込みネットワーク

畳み込み層、プーリング層を経て、入力画像のテンソルがどのように変化していくかに注目すると理解しやすいです。また、工夫としてオプティマイザにAdamを利用していたり、ドロップアウトを行うことで過学習を防いでいます。

結果として、99.2%程度の正解率をもつモデルを構築します。

TensorFlow Mechanics 101

このチュートリアルでは、MNISTに対して順伝播型ニューラルネットワークの訓練と評価方法を紹介します。

f:id:hiyoko9t:20171008110859p:plain
部分グラフ

コード自体は、「Deep MNIST for Experts」と共通する部分も多く、それを機能ごとに関数化し、解説しています。
また、あくまで訓練と評価が中心のチュートリアルとなっていますが、可視化やチェックポイント、実行時間の計測など、実践的な機能の実装方法も確認できます。

tf.estimator Quickstart

このチュートリアルでは、モデルの設定や訓練、評価を簡単にするhigh-levelなAPIであるtf.estimatorの使い方を紹介します。ここでは、Irisデータセットを用いて、花の品種を分類するニューラルネットワークを構築します。

Irisデータセットは、次の表に示すように4つの特徴量と品種をあらわすラベルからなります。

がく片の長さ がく片の幅 花びらの長さ 花びらの幅 ラベル
5.1 3.5 1.4 0.2 0
... ... ... ... ...
5.9 3.0 5.1 1.8 2

f:id:hiyoko9t:20171008112331j:plain
Iris(左からsetosa、versicolor、virginica)

チュートリアルのコード内に、Irisデータを取得するために

raw = urllib.urlopen(IRIS_TRAINING_URL).read()

という一行があるのですが、これはpython2.x系の記述なので、python3.x系を使用される方は次のように修正してください。

raw = urllib.request.urlopen(IRIS_TRAINING_URL).read()

得られたIrisデータの特徴量から、品種を分類するニューラルネットワークを構築しますが、「Deep MNIST for Experts」では、層ごとに自分で重みやバイアスを作成していました。

しかし、tf.estimatorを用いれば、隠れ層を3つもつネットワークを次のように簡単に記述できます。

# Build 3 layer DNN with 10, 20, 10 units respectively.
classifier = tf.estimator.DNNClassifier(feature_columns=feature_columns,
                                        hidden_units=[10, 20, 10],
                                        n_classes=3,
                                        model_dir="/tmp/iris_model")

結果として、96%程度の正解率を持つ分類器が構築されます。

Building Input Functions with tf.estimator

このチュートリアルでは、tf.estimatorにおいて、前処理やモデルにデータを与えるための入力関数(input_fn)の作り方を紹介します。

用意した特徴量/ラベルデータがpythonの配列やpandasデータフレームで格納されている場合、次のようにinput_fnを記述することができます。

import numpy as np
# numpy input_fn.
my_input_fn = tf.estimator.inputs.numpy_input_fn(
    x={"x": np.array(x_data)},
    y=np.array(y_data),
    ...)
import pandas as pd
# pandas input_fn.
my_input_fn = tf.estimator.inputs.pandas_input_fn(
    x=pd.DataFrame({"x": x_data}),
    y=pd.Series(y_data),
    ...)

具体例として、Boston House Valueデータをpandasデータフレームで格納し、input_fn関数を通して、回帰問題として、訓練と評価、予測を行なっています。すべて、同一のinput_fn関数を通して実行できることに注意してコードを追ってみてください。

TensorBoard: Visualizing Learning

このチュートリアルでは、DNNのような複雑になりがちなモデルにおいて、理解、デバッグ、最適化を進めるための、データの可視化ツールであるTensorBoardを紹介します。TensorBoardでは、計算グラフや行列、画像データなどの可視化を行うことができます。

実際に、学習を可視化すると、以下のようになります。

f:id:hiyoko9t:20171008152349p:plain
TensorBoardを用いた学習の可視化

TensorBoardは、TensorFlowの実行時に生成することのできるサマリーデータを含むイベントファイルを読み込むことによって、そのデータを可視化します。例として、CNNの訓練を行うとき、学習率とロス関数の値を確認したいとき、そのノードにtf.summary.scalarを取り付けます。

公式のソースコードを実行した後に端末で、次のコマンドを実行してください。

tensorboard --logdir=/tmp/tensorflow/mnist

コマンドを実行した後に、localhost:6006へブラウザでアクセスすると、可視化されたものが見れます。

実際に「MNIST For ML Beginners」で実装したコードをロギングし、可視化したものが次のようになります。

f:id:hiyoko9t:20171008154255p:plain
MNISTに対する学習の可視化

また、チュートリアル内にはありませんが、画像を可視化したものもTensorBoard上で見ることができます。

f:id:hiyoko9t:20171008170139p:plain
MNISTにおける訓練中の画像

TensorBoard: Graph Visualization

このチュートリアルでは、複雑になりがちな計算グラフの理解、デバッグをTensorBoardを用いて可視化します。

学習の可視化と同様に、「MNIST For ML Beginners」で実装したコードをロギングし、計算グラフを可視化したものが次のようになります。

f:id:hiyoko9t:20171008165813p:plain
MNISTに対する学習の計算グラフの可視化

グラフの中で隠れ層、ソフトマックス、クロスエントロピーの計算などが行われていることがわかります。

ここで、計算グラフ上の各シンボルは次のような意味を持っています。

f:id:hiyoko9t:20171008170523p:plain
計算グラフ上における各シンボルの意味

これにより、複雑な計算グラフを視覚的に確認することができます。

Using GPUs

TensorFlowでは、CPUとGPU、二つのデバイスをサポートしています。TensorFlowでは、CPUとGPUで同時に実装が行われているとき、GPUが優先的に用いられます。

実際に、簡単な計算でGPUが使用されていることを確認してみます。

# Creates a graph.
a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a')
b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b')
c = tf.matmul(a, b)
# Creates a session with log_device_placement set to True.
sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
# Runs the op.
print(sess.run(c))

セッションを起動する時に、log_device_placement=Trueを指定することで、使用されたデバイスを確認することができます。

Device mapping:
/job:localhost/replica:0/task:0/gpu:0 -> device: 0, name: Tesla K40c, pci bus
id: 0000:05:00.0
b: /job:localhost/replica:0/task:0/gpu:0
a: /job:localhost/replica:0/task:0/gpu:0
MatMul: /job:localhost/replica:0/task:0/gpu:0
[[ 22.  28.]
 [ 49.  64.]]

このようにGPUが使用されていることがわかります。

さらに、このチュートリアルでは、GPUの使用するメモリの制御、複数のGPUの使用方法などが紹介されています。(GPU環境がないので、この実装は試せていません。GPU環境がある方は、ぜひ試してみてください)

Image Recognition

画像認識は、近年盛んに研究が行われている分野です。画像認識の基本的な考え方は以前の記事で紹介しているので、よければそちらをどうぞ。

f:id:hiyoko9t:20171009094228p:plain
画像認識の例

このチュートリアルでは、Googleの開発したInception-v3の使い方を中心に解説しています。Inception-v3は、画像分類問題のベンチマークであるImageNetに対して訓練されています。これは、「ゼブラ」や「ダルメシアン」といったように画像からそのクラスを1000個に分類する問題となっています。

このようなモデルを評価するための指標として、予測のトップ5に真に求めたいラベルが含まれているかどうか(トップ5エラー率)、という指標が存在します。有名なアルゴリズムとそのトップ5エラー率を次の表に示します。

アルゴリズム トップ5エラー率
AlexNet 15.3%(2012年)
Inception(GoogleNet) 6.67%
BN-Inception-v2 4.9%
Inception-v3 3.46%

表からInception-v3のトップ5エラー率が非常に小さいことがわかります。

Inception-v3は、Python API/C++ APIとして提供されています。ここでは、Python APIに関してのみ紹介します。

Python APIの利用方法は、非常にシンプルです。

  1. GitHub - tensorflow/models: Models built with TensorFlowをgit cloneする
  2. 以下のコマンドを実行する
cd models/tutorials/image/imagenet
python classify_image.py

これにより、パンダの画像が与えられたとき、次のような出力結果が得られます。

f:id:hiyoko9t:20171009101558j:plain
パンダ

giant panda, panda, panda bear, coon bear, Ailuropoda melanoleuca (score = 0.88493)
indri, indris, Indri indri, Indri brevicaudatus (score = 0.00878)
lesser panda, red panda, panda, bear cat, cat bear, Ailurus fulgens (score = 0.00317)
custard apple (score = 0.00149)
earthstar (score = 0.00127)

出力結果から、パンダのスコアが高くなっていることがわかります。

How to Retrain Inception's Final Layer for New Categories

このチュートリアルでは、新しいカテゴリに対するInceptionの最終層の再訓練方法を紹介します。

Inceptionは、ImageNetを用いて訓練されているため、ImageNetに存在しないカテゴリを予測することはできません。そこで、新しいカテゴリを結果として得たいときには、ネットワークを更新する必要があります。しかし、現代の物体認識モデルは何百万、何千万というパラメータを持つので、そのすべてを更新するのは非常に大きな計算量がかかります。そこで、途中の層はそのままで、出力に直接影響のある最終層だけを更新する手法が提案されています。(詳細はこちら) これにより、すべてのパラメータを更新するときと比べて精度は落ちるものの、GPUの無い環境でも30分程度で再訓練することが可能となります。

この手法がうまく動くことは、ImageNetに含まれる1000クラスから訓練した、入力層に近い特徴を取り出す層は、未知のカテゴリに対してもその特徴を取り出すことに一定の効果が見られることを示唆しています。

このチュートリアルでは、新しいカテゴリとして、花の画像を使用します。

f:id:hiyoko9t:20171009111632j:plain
新しいカテゴリ:花

また、自分のデータセットを用いて学習するときの注意点も紹介されています。 例としていくつか下に挙げます。

  • 認識したいカテゴリの画像を少なくとも100枚は用意する
  • 認識したいカテゴリに応じた画像を用意する(アウトドアな物体を認識したいのに、インドアな物体を訓練してはいけない)
  • できるだけ色々な状況における物体画像を用意する(背景が青の部屋で撮った物体と背景が緑の部屋で撮った物体から正しく認識したいとき、背景を除くため)
  • カテゴリの粒度を意識する (「乗り物」という粒度。「車」、「モーターバイク」、「トラック」という粒度。)

また、Inceptionに精度は劣るものの高速で動くアーキテクチャも紹介されています。アプリなどで、物体認識を行うときはこちらを使用すればいいかもしれません。

A Guide to TF Layers: Building a Convolutional Neural Network

このチュートリアルでは、MNISTデータセットからラベル分類を行うため、layersモジュールを用いた畳み込みニューラルネットワーク(Convolutional Neural Network; CNN)の構築方法を説明します。

f:id:hiyoko9t:20171009122402p:plain
MNIST

CNNは主に以下の3つの層から構成されます。

  1. 畳み込み層
    特徴マップを作るために、画像の小領域に対してフィルターを畳み込んで生成します。

  2. プーリング層
    畳み込み層の次元をダウンサンプリングします。例えば、$2\times 2$のフィルターを$28\times 28$の画像に適用することで$14\times 14$の特徴に減らすことができます。

  3. 密結合(全結合)層
    畳み込み層、プーリング層から得られた特徴から分類を行います。

このチュートリアルでは、以下の構造のCNNを実装します。

  1. 畳み込み層 #1:$32$特徴マップ、$5\times 5$フィルター、ReLu
  2. プーリング層 #1:最大値プーリング、$2\times 2$フィルター(ストライド$2$)
  3. 畳み込み層 #2:$64$特徴マップ、$5\times 5$フィルター、ReLu
  4. プーリング層 #2:最大値プーリング、$2\times 2$フィルター(ストライド$2$)
  5. 密結合層 #1:1024ユニット(ドロップアウト率$0.4$)
  6. 密結合層 #2:10ユニット(クラスラベル0から9をあらわす)

これを実装したものが次のようになります。

def cnn_model_fn(features, labels, mode):
  """Model function for CNN."""
  # Input Layer
  input_layer = tf.reshape(features["x"], [-1, 28, 28, 1])

  # Convolutional Layer #1
  conv1 = tf.layers.conv2d(
      inputs=input_layer,
      filters=32,
      kernel_size=[5, 5],
      padding="same",
      activation=tf.nn.relu)

  # Pooling Layer #1
  pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2)

  # Convolutional Layer #2 and Pooling Layer #2
  conv2 = tf.layers.conv2d(
      inputs=pool1,
      filters=64,
      kernel_size=[5, 5],
      padding="same",
      activation=tf.nn.relu)
  pool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=[2, 2], strides=2)

  # Dense Layer
  pool2_flat = tf.reshape(pool2, [-1, 7 * 7 * 64])
  dense = tf.layers.dense(inputs=pool2_flat, units=1024, activation=tf.nn.relu)
  dropout = tf.layers.dropout(
      inputs=dense, rate=0.4, training=mode == tf.estimator.ModeKeys.TRAIN)

  # Logits Layer
  logits = tf.layers.dense(inputs=dropout, units=10)

  predictions = {
      # Generate predictions (for PREDICT and EVAL mode)
      "classes": tf.argmax(input=logits, axis=1),
      # Add `softmax_tensor` to the graph. It is used for PREDICT and by the
      # `logging_hook`.
      "probabilities": tf.nn.softmax(logits, name="softmax_tensor")
  }

  if mode == tf.estimator.ModeKeys.PREDICT:
    return tf.estimator.EstimatorSpec(mode=mode, predictions=predictions)

  # Calculate Loss (for both TRAIN and EVAL modes)
  onehot_labels = tf.one_hot(indices=tf.cast(labels, tf.int32), depth=10)
  loss = tf.losses.softmax_cross_entropy(
      onehot_labels=onehot_labels, logits=logits)

  # Configure the Training Op (for TRAIN mode)
  if mode == tf.estimator.ModeKeys.TRAIN:
    optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001)
    train_op = optimizer.minimize(
        loss=loss,
        global_step=tf.train.get_global_step())
    return tf.estimator.EstimatorSpec(mode=mode, loss=loss, train_op=train_op)

  # Add evaluation metrics (for EVAL mode)
  eval_metric_ops = {
      "accuracy": tf.metrics.accuracy(
          labels=labels, predictions=predictions["classes"])}
  return tf.estimator.EstimatorSpec(
      mode=mode, loss=loss, eval_metric_ops=eval_metric_ops)

ここで、入力層が次の引数をとっていることに注意してください。

[batch_size, image_width, image_height, channels]

MNISTにこれをあてはめると、特徴から入力への変換は次のようになります。

input_layer = tf.reshape(features["x"], [-1, 28, 28, 1])

ここで、batch_sizeが-1となっているのは、動的に値をとることを示しています。これにより、バッチザイズをハイパーパラメータとして扱うことができます。 例として、バッチサイズが100のとき、入力の要素数は$100\times 28\times 28\times 1 = 78400$となります。

畳み込み層、プーリング層がそれぞれこの引数を受け取っていることが確認できます。

conv1 = tf.layers.conv2d(
    inputs=input_layer,
    filters=32,
    kernel_size=[5, 5],
    padding="same",
    activation=tf.nn.relu)

pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2)

全結合層では、上記の実装で得られた特徴量をflattenして入力として受け取ります。

pool2_flat = tf.reshape(pool2, [-1, 7 * 7 * 64])

dense = tf.layers.dense(inputs=pool2_flat, units=1024, activation=tf.nn.relu)

さらに、訓練時のみドロップアウトを行い、最後にクラスラベルの数の層へ変換します。

dropout = tf.layers.dropout(
    inputs=dense, rate=0.4, training=mode == tf.estimator.ModeKeys.TRAIN)

logits = tf.layers.dense(inputs=dropout, units=10)

このように、直感的に畳み込み層、プーリング層、全結合層が実装できることがわかります。非常に便利ですね。

これにより97%程度の正解率を持つモデルを構築できます。

Convolutional Neural Networks

ここまで、画像分類のベンチマークとして、MNISTを用いてきました。MNIST同様、有名な画像分類問題のデータセットとしてCIFAR-10 and CIFAR-100 datasetsが挙げられます。これは、$32\times 32$のRGBの画像で、飛行機や鳥、猫といった10個のクラスの画像からなるデータセットとなります。

f:id:hiyoko9t:20171009141422p:plain
CIFAR-10 and CIFAR-100 datasetsより引用

ここでは、あらかじめ準備されているコード(models/tutorials/image/cifar10 at master · tensorflow/models · GitHub)について、その特徴を見ていきます。

  • モデルの入力
    CIFAR-10はもともと$32\times 32$の画像ですが、入力として与えるとき、ここでは$24\times 24$の画像に変換しています。訓練のときは、ランダムな位置から取得し、評価のときは、中心から取得します。また、ランダムな位置から取得するため、その影響をなくすため白色化を行います。さらに、データセットの数を増やすため、明るさやコントラストを変化させます。
    そうして、得られた画像が次のようになります。

f:id:hiyoko9t:20171009144142p:plain
変換されたCIFAR-10の例

  • 予測モデル
    ここで使用するモデルは次のようになります。

f:id:hiyoko9t:20171009144207p:plain
ネットワークの構成

  • 訓練モデル
    ここでは、ネットワークの出力にソフトマックス関数、ロスとして、正規化された予測結果とone-hotラベルに対するクロスエントロピーを用います。

以上より、86%程度の正解率を持つモデルを構築できます。

また、このチュートリアルでは、複数のGPUを用いた訓練方法も紹介されています。(GPU環境がないので、この部分は試せていません。) GPU環境がある方は、ぜひ試してみてください。

Vector Representations of Words

ここまでは、画像を中心に扱ってきましたが、ここからは自然言語について扱います。自然言語を扱う上で有名なモデルとしてword2vecが挙げられます。このモデルでは、単語のベクトル表現を用います。(word embedding)

それでは、なぜ単語のベクトル表現を用いるのでしょうか。これまで見てきたような物体認識や音声認識では、その情報が密なためすべての情報を使用する必要がありました。しかし、自然言語では、伝統的に単語を離散的なシンボル、例えば「犬」を「Id537」、「猫」を「Id143」、といった風に扱ってきました。そのため、データがスパースになり、統計的なモデルを訓練するためにはよりデータが必要となります。そこで、これらを解決するために考えられたのが単語のベクトル表現です。

f:id:hiyoko9t:20171009163635p:plain
情報のイメージ

word2vecは、連続的Bag-of-Words(CBOW)とSkip-Gramモデルという二つのアーキテクチャを持ちます。CBOWは、もととなるコンテクスト (e.g. 'the cat sits on the')からターゲットとなる言葉 ('mat')を予測する一方、Skip-Gramは、ターゲットからもととなるコンテクストを推測します。CBOWは、比較的小さなデータセットに対してうまくいくことが知られており、Skip-Gramは、比較的大きなデータセットに対してうまくいくことが知られています。このチュートリアルではSkip-Gramモデルを中心に扱います。Skip-Gramモデルに関しては、こちらのブログでわかりやすく解説されているので興味のある方はどうぞ。

こうして得られた単語のベクトル表現をt-SNEなどの次元削減を用いて、表現したものが次の図になります。次元削減には、他にも主成分分析などがあり、次元削減の基本的な考え方については、以前のブログで述べているのでそちらをどうぞ。

f:id:hiyoko9t:20171009170354p:plain
単語の関係性

図より、「man」から「woman」、「king」から「queen」へ向かうベクトルがほぼ同じ方向を向いていることがわかります。これは、単語間の関係性をこのベクトル空間上において、うまく表現できていることを示しています。

さらに多くの単語をベクトル表現化し、t-SNEで次元削減をおこなったものが次の図となります。

f:id:hiyoko9t:20171009171738p:plain
単語の関係性

図より、似た意味を持つ単語が近くにプロットされていることが確認できます。

Recurrent Neural Networks

このチュートリアルでは、言語モデルに対する再帰ニューラルネットワーク(Recurrent Neural Networks; RNN)を訓練します。目的としては、与えられた言葉のヒストリーから次の単語を予測することとなります。ここでは有名なベンチマークであるPenn Tree Bankデータセットを用います。

このチュートリアルでは、RNNの特殊な構造を持つLong Short Term Memory(LSTM)を用います。LSTMの詳しい解説はここでは行いませんが、その名前にある通り、従来のRNNではできなかった長期的なデータの記憶を行うことができます。LSTMについて、詳しく知りたい方は、わかるLSTM ~ 最近の動向と共に - QiitaもしくはLSTMネットワークの概要 - Qiitaをご覧ください。

バッチ内における各単語は、以下のように自動的に時間$t$と結び付けられます。

 t=0  t=1    t=2  t=3     t=4
[The, brown, fox, is,     quick]
[The, red,   fox, jumped, high]

words_in_dataset[0] = [The, The]
words_in_dataset[1] = [fox, fox]
words_in_dataset[2] = [is, jumped]
words_in_dataset[3] = [quick, high]
num_batches = 4, batch_size = 2, time_steps = 5

LSTMの主要な実装は、以下の部分となります。

words_in_dataset = tf.placeholder(tf.float32, [num_batches, batch_size, num_features])
lstm = tf.contrib.rnn.BasicLSTMCell(lstm_size)
# Initial state of the LSTM memory.
hidden_state = tf.zeros([batch_size, lstm.state_size])
current_state = tf.zeros([batch_size, lstm.state_size])
state = hidden_state, current_state
probabilities = []
loss = 0.0
for current_batch_of_words in words_in_dataset:
    # The value of state is updated after processing each batch of words.
    output, state = lstm(current_batch_of_words, state)

    # The LSTM output can be used to make next word predictions
    logits = tf.matmul(output, softmax_w) + softmax_b
    probabilities.append(tf.nn.softmax(logits))
    loss += loss_function(probabilities, target_words)

これらをのコードは、こちらから入手することができます。これを実行することで、Penn Tree Bankデータセットに対して訓練を行うことができます。

提供されているコードを実行する時は、次のことに気をつけてください。

  • GPU環境がない方
    GPU環境がない状態でptb_word_lm.pyを実行しようとすると、エラーとなるので、オプションとして--num_gpus=0をつけてください。
python ptb_word_lm.py --data_path=$HOME/simple-examples/data/ --model=small --num_gpus=0
  • Python 3.x系を使っている方
    ptb_word_lm.pyのコード255行目、487行目にdict.iteritems()というメソッドがありますが、これをdict.items()に置き換えてください。
for name, op in ops.items():

Sequence-to-Sequence Models

このチュートリアルでは、Sequence-to-Sequenceモデルを用いたニューラル翻訳を行います。

Sequence-to-Sequenceモデルを用いて、英語の文章をフランス語へ変換した結果は次のようになります。

>  Who is the president of the United States?
 Qui est le président des États-Unis ?

フランス語はわかりませんがすごいですね。

これを実現するSequence-to-Sequenceモデルは、入力を処理するエンコーダと出力を生成するデコーダという二つのRNNから構成されます。

f:id:hiyoko9t:20171009185007p:plain
Sequence-to-Sequenceモデルのアーキテクチャ

図の中のボックスは、RNNのセルをあらわしており、GRUセルやLSTMセルがよく用いられます。

基本的なモデルは、上の図の通りですが、デコーダがより直接的に入力にアクセスすることのできる多層Sequence-to-Sequenceモデルも提案されています。

f:id:hiyoko9t:20171009185448p:plain
多層Sequence-to-Sequenceモデル

TensorFlowで、上に挙げた基本的なSequence-to-Sequenceモデルは次のように実装されます。

outputs, states = basic_rnn_seq2seq(encoder_inputs, decoder_inputs, cell)

ここで、最初に挙げた図においてencoder_inputsにあたるものがA、B、C、decoder_inputsにあたるものがGO、W、X、Y、Zになります。cellはGRUCellもしくはLSTMCellを用います。

これにより、返り値(outputs)として、最初の図のW、X、Y、Z、EOSが返ります。

ここでは、Sequence-to-Sequenceモデルの基本的な構造しか紹介しませんが、さらに精度を高める工夫として、ソフトマックスサンプル、バケッティングやパディングなども紹介されています。

Large-scale Linear Models with TensorFlow

tf.estimator APIは、線形モデルに関する様々なツールを提供しています。

線形モデルは、特徴量に対して一つの重み付けされた和を用います。
ディープニューラルネットワークが非常に高い精度を誇る中、線形モデルを用いる利点は次のようなものが挙げられます。

  • 訓練が速い
  • 非常に大きな特徴量に対してうまく動く
  • 学習率など多くの面倒な処理を行わずに済む
  • 解釈やデバッグがしやすい、また、大きな意味のある特徴量がわかる
  • 機械学習の導入としてわかりやすい
  • 産業で広く用いられている

このチュートリアルでは、tf.estimatorを用いた線形分類/回帰モデルの実装、離散値、連続値の特徴量の扱い方、線形モデルとディープニューラルネットワークの結合方法が紹介されています。

TensorFlow Linear Model Tutorial

このチュートリアルでは、tf.estimator APIを用いて、二値分類を行います。

入力データとして、年齢、性別、教育、職業などが与えられたとき、年収が50000ドルを超えているかどうかを判別します。

また、ここではロジスティック回帰モデルを訓練することとします。

このチュートリアルでは、以下のような構成でそれぞれの実装を行なっています。

  1. 人口調査データの読み込み
  2. データをテンソルへ変換
  3. 特徴量の選択
  4. ロジスティック回帰モデルの定義
  5. モデルの訓練と評価
  6. 過学習を避けるための正則化

ざっくりと各トピックで注目すべきポイントを挙げます。

  • 人口調査データの読み込み
    データには、categoricalなものとcontinuousなもの、離散値と連続値、が存在します。例えば、出身国(日本、アメリカ、インドなど)はcategoricalなデータであり、身長(170.1、159.2、160.1など)はcontinuousなデータとなります。

  • データをテンソルへ変換
    tf.estimator.Estimatorのtrainやevaluateメソッドを使用するためにデータをテンソルへ変換する必要があります。入力関数は、TensorもしくはSparseTensors型の特徴量feature_colusとTensor型のlabelを返さなければなりません。

  • 特徴量の選択
    特徴量は、データをそのまま特徴量とすることもあれば、もとの特徴量から新しい特徴量を作ることもあります。例として、continuousなデータを、連続値の境界を定めることによって、categoricalなデータへ変換することができます。(bucketization) 具体的には、年齢はそのままのデータはcontinuousですが、18歳以下、18歳〜25歳以下など境界を定めることによって、categoricalな特徴量を作ることができます。

age_buckets = tf.feature_column.bucketized_column(
    age, boundaries=[18, 25, 30, 35, 40, 45, 50, 55, 60, 65])

他にも、複数の特徴量を組み合わせて、新たな特徴量を作ることができます。 (線形モデルだと特徴量間の相関が考慮できないため)

  • 過学習を避けるための正則化
    モデルが複雑になればなるほど、訓練データに過剰に適合してしまい、未知のデータへの汎化性能が落ちてしまうことを過学習(overfiting)と言います。これを避けるための手法が、正則化(regularization)であり、ロス関数に足された正則化項は、モデルの複雑さを制御します。
    また、正則化には種類があり、$l_1$正則化はより解がスパースになりやすいといった性質があったり、$l_2$正則化微分可能であるといった特徴があります。(数式で確認するとよくわかると思います。)

以上を実装することにより、83%程度の正解率を持つモデルを構築することができます。

TensorFlow Wide & Deep Learning Tutorial

このチュートリアルでは、wideなモデルとdeepな順伝播型ニューラルネットワークのtf.estimator APIを用いた結合方法を説明します。

wideなモデルとは、スパースな幅広の集合と交差した特徴量(二つ以上の特徴量から作られた特徴量)を持つ線形モデルを指します。

一方、deepなモデルは、これまでのチュートリアルでみてきたような順伝播型ニューラルネットワークを指します。特徴としては、はじめに高次元のスパースなベクトルを低次元の密な実数値ベクトルに変換することが挙げられます。(これをembeddingと言います) こうして作成されたベクトルともともと連続なベクトルを結合して、入力とします。

deepなモデルにより、まだ見ていないデータに対する汎化性能が上がることが知られています。しかし、もともとの特徴量が高次元でスパースであるとき、密な低次元ベクトルを学習するのは難しく、ここで線形モデルがもとの高次元のベクトルの情報を保持するのに役立ちます。手法の詳しい説明は論文をどうぞ([1606.07792] Wide & Deep Learning for Recommender Systems)。

f:id:hiyoko9t:20171009030542p:plain
WideなモデルとDeepなモデルの結合

結果として、84%程度の正解率を持つモデルを構築できます。

Improving Linear Models Using Explicit Kernel Methods

このチュートリアルでは、劇的に精度を上げるための線形モデルとカーネル法の結合を紹介します。

「MNIST For ML Beginners」で、MNISTに対してシンプルなソフトマックス回帰を行なった結果、その正解率はおよそ92%程度でした。また、ロス関数を最小化するためのアルゴリズムは降下勾配法でしたが、それを$l_2$正則化を用いたアルゴリズム、Follow-The-Regularized-Leader(FTRL)に置き換えても正解率はおよそ93%程度です。

これより、パラメータの値に関わらず、この線形モデルの限界はおおよそ93%程度であることがわかります。すなわち、データセットが線形分離不可能であることを示唆しています。

この7%程度のエラーを小さくするためにカーネル法を用います。カーネル法は、線形分離が不可能な特徴を線形分離が可能となるような高次元に特徴を写像する手法です。

f:id:hiyoko9t:20171009093011p:plain
高次元空間への写像

ここでは、MNISTの次元$28\times 28 = 784$を$2000$次元の空間へ写像することにより、(より)線形分離を可能としています。

これにより、正解率97%程度のモデルが構築できます。

では、線形分離を可能とするためにとにかく高次元に写像すれば良いのでしょうか。実際は、一定の閾値を越えるとほとんど正解率が変わらなくなることが知られています。次の図は、横軸が写像後の次元、縦軸が正解率を示しています。

f:id:hiyoko9t:20171009093406p:plain
写像後の次元に対する正解率

次元を大きくしていっても、一定の値を越えるとほとんど正解率が変わらなくなることが確認できます。

Mandelbrot Set

TensorFlowは、機械学習に特化したライブラリですが、他の数学の問題に対しても使用することができます。

その一つの例として、マンデルブロ集合の可視化を行います。マンデルブロ集合の詳細についてはこちら

実際に、TensorFlowを用いて、マンデルブロ集合を可視化したものが次のようになります。

f:id:hiyoko9t:20171008182748j:plain
マンデルブロ集合

Partial Differential Equations

このチュートリアルでは、偏微分方程式のふるまいをシミュレーションした様子を紹介します。

まず、$500\times 500$の正方形の画像に雨粒を落とします。(ランダムで生成した点をプロットします)

f:id:hiyoko9t:20171008185413j:plain
雨粒を落とした画像

ここで、ラプラシアンカーネルを作成して、畳み込み計算を行います。そして、適当な重みを与えて、もとの画像と足し合わせることで、次のさざなみが立ったようなシミュレーションが得られます。

f:id:hiyoko9t:20171008190913g:plain
さざなみが立っていく様子

綺麗ですね。

まとめ

長い記事になりましたが、目を通された方はお疲れ様です。(ありがとうございます。)

本記事では、TensorFlowのチュートリアルを紹介してきました。筆者は3日ほどかけて、チュートリアルを読みながらコードを実行してきましたが、正直「じゃあ自分のモデルを書くぞ!」ってスラスラ書ける気はあんまりしません。何回も参照し直して、憶えていくしかなさそうですね。

今回でチュートリアルを一通り終えることができたので、今後は論文の実装なども行なっていければなと思います。

不定期更新ですがよろしくお願いします。 何かあればコメントやツイッター(@hiyoko9t)でお気軽にお声がけください。

はじめてのTensorFlow

本記事では、機械学習ライブラリとして有名なTensorFlow(以下、TF)をはじめて使う人向けに、公式のドキュメント(Getting Started  |  TensorFlow)に基づいて、TFの基本的な使い方を説明していきます。

TFのインストール

まずは、TFをインストールしましょう。インストールは、こちら(Installing TensorFlow  |  TensorFlow)でそれぞれのOSに沿ったインストール方法が紹介されているので、詳細はこちらをご覧ください。

ここでは、MAC OS XのAnacondaを用いたCPUのみを使用する環境を構築します。以下の4つの手順を実行してください。

1.Anacondaをインストールします。こちら(Downloads | Anaconda)からOSを選択すればインストールすることができます。

2.TFを実行する仮想環境を作ります。

conda create -n tensorflow

3.上で作成した仮想環境に移ります。

source activate tensorflow

4.TFをインストールします。

pip install --ignore-installed --upgrade TF_PYTHON_URL

ここで、TF_PYTHON_URLはPython2.7系を用いる場合、

https://storage.googleapis.com/tensorflow/mac/cpu/tensorflow-1.3.0-py2-none-any.whl

Python3.4、3.5、3.6系を用いる場合、

https://storage.googleapis.com/tensorflow/mac/cpu/tensorflow-1.3.0-py3-none-any.whl

に置き換えてください。

これで、準備は終了です。

TFとは

TFは様々なAPIを提供していますが、主に機械学習エンジニアが使用することを推奨しています。TFの最も低いレベルのAPIである、TensorFlow Core(以下、TFC)はプログラミングの基本的な機能を提供します。一方、高いレベルのAPIは、データセットや推定、訓練などを容易にする機能を提供します。

TFにおけるデータの中心的な単位として、テンソル(Tensor)というものが挙げられます。テンソルは、配列の形で格納された値の集合から成ります。テンソルのランクは、配列の次元の数と一致します。以下にテンソルの例を見てみます。

3 # ランク0のテンソル; 型が[]のスカラー
[1., 2., 3.] # ランク1のテンソル; 型が[3]のベクトル
[[1., 2., 3.], [4., 5., 6.]] # ランク2のテンソル; 型が[2, 3]の行列
[[[1., 2., 3.]], [[7., 8., 9.]]] # ランク3のテンソル; 型[2, 1, 3]

TFCチュートリアル

TFのインポート

TFのクラスやメソッドを利用するためには、まずはコード内でTFをインポートしなければなりません。

import tensorflow as tf

計算グラフ

計算グラフ(Computational Graph)は、TFの一連の操作をグラフのノードへアレンジしたものです。

簡単な計算グラフのビルド例を挙げます。まず2つのfloat型のテンソル、node1とnode2を作成します。

node1 = tf.constant(3.0, dtype=tf.float32)
node2 = tf.constant(4.0) # 暗にtf.float32を宣言
print(node1, node2)

この結果は、以下のようになります。

Tensor("Const:0", shape=(), dtype=float32) Tensor("Const_1:0", shape=(), dtype=float32)

ここで、ノードをprintしたとき、内部に格納されている値である$3.0$、$4.0$が出力されていないことに注意してください。代わりに、ノードが評価された時に、値が返ります。ノードを評価するためには、セッションの中で計算グラフを走らせなければなりません。

以下のコードでは、1行目でセッションオブジェクトを作成し、2行目でnode1とnode2を評価するための計算グラフを走らせるrunメソッドを用います。

sess = tf.Session()
print(sess.run([node1, node2]))

これにより、内部に格納されている値が表示されることが確認できます。

[3.0, 4.0]

より複雑な例を見てみましょう。上記で作成した二つの定数ノードを組み合わせて、新しいグラフを作ります。

node3 = tf.add(node1, node2)
print("node3:", node3)
print("sess.run(node3):", sess.run(node3))

実行結果はこのようになります。

node3: Tensor("Add:0", shape=(), dtype=float32)
sess.run(node3): 7.0

TFは、TensorBoardという計算グラフを可視化することのできる機能を提供しています。上の例では、次のような図が得られます。

f:id:hiyoko9t:20171004161647p:plain

このグラフは、常に定数の結果を出力する簡単なグラフです。

グラフは、外部の入力をパラメータ化することもできます。これを、プレースホルダー(placeholders)と呼びます。プレースホルダーは、値を後で返すことを保証します。言葉で書くとよくわかりませんね。実際に、例を見ていきましょう。

a = tf.placeholder(tf.float32)
b = tf.placeholder(tf.float32)
adder_node = a + b  # tf.add(a, b)のショートカット

このコードでは、$a$、$b$という値の殻を作っておき、adder_nodeで$a$、$b$に対する操作を定義しています。実際に、プレースホルダーに具体的な値を与え、runメソッドを実行することにより、このグラフを評価することができます。

print(sess.run(adder_node, {a: 3, b: 4.5}))
print(sess.run(adder_node, {a: [1, 3], b: [2, 4]}))

この実行結果は、以下のようになり、足し算ができていることがわかります。

7.5
[ 3.  7.]

この計算グラフは、TensorBoardにより、次のようにあらわされます。

f:id:hiyoko9t:20171004163014p:plain

他の操作を加えて、もう少し複雑な計算グラフを見ていきましょう。

add_and_triple = adder_node * 3
print(sess.run(add_and_triple, {a: 3, b: 4.5}))

これは、二つの引数を足し合わせた後、3倍する操作を定義しています。

よって、結果としては、以下が出力されます。

22.5

この計算グラフは、TensorBoardを用いると、次のようにあらわされます。 f:id:hiyoko9t:20171004163338p:plain

機械学習では、上記のように任意の入力が可能なモデルを得たい場合があります。モデルを訓練可能とするために、同じ入力に対して新しい出力を得ることができるようにグラフを修正する必要があります。そこで、変数(Variables)を用いることにより、訓練可能なパラメータをグラフに加えることができます。変数を用いた例を挙げます。

W = tf.Variable([.3], dtype=tf.float32)
b = tf.Variable([-.3], dtype=tf.float32)
x = tf.placeholder(tf.float32)
linear_model = W * x + b

定数は、tf.constantが呼ばれたときに初期化され、その値が変更されることはありませんでした。一方、変数はtf.Variableが呼ばれたときには初期化されません。TFのプログラムの中で変数を初期化するには、次の操作が必要となります。

init = tf.global_variables_initializer()
sess.run(init)

ここで、initはすべてのグローバル変数を初期化するTFの部分グラフを扱っていることに注意してください。sess.runが呼ばれるまでは、変数は初期化されません。

ここで、$x$はプレースホルダーなので、ある特定の値に対して、linear_modelを評価することができます。

print(sess.run(linear_model, {x: [1, 2, 3, 4]}))

出力結果は、以下のとおりです。

[ 0.          0.30000001  0.60000002  0.90000004]

ここで、線形モデルを作ることができましたが、このモデルが良いかどうかはわかりません。訓練データに対してモデルを評価するために、目的値を与えるためのプレースホルダー$y$が必要となります。また、ロス関数を与える必要もあります。

ロス関数は、現在のモデルが与えられたデータからどれだけ離れているかを測ります。ここでは、線形回帰に対する標準的なロスモデル、すなわち現在のモデルと与えられたデータの差の二乗和を用います。linear_model - yは、それぞれの要素が対応するエラーをあらわすベクトルとなります。tf.squareを用いて、この二乗誤差を得ることができます。さらに、tf.reduce_sumを用いて、すべてのサンプルのエラーを足し合わせて、スカラーにすることができます。

y = tf.placeholder(tf.float32)
squared_deltas = tf.square(linear_model - y)
loss = tf.reduce_sum(squared_deltas)
print(sess.run(loss, {x: [1, 2, 3, 4], y: [0, -1, -2, -3]}))

この結果は、以下のとおりとなります。

23.66

ここで$W$と$b$の値を適切な値、$-1$と$1$、を定めることで、このモデルを改善することができます。変数は、tf.Variableで初期値を与えられますが、tf.assignを用いることで、変更することができます。以下で、変数を変更してみます。

fixW = tf.assign(W, [-1.])
fixb = tf.assign(b, [1.])
sess.run([fixW, fixb])
print(sess.run(loss, {x: [1, 2, 3, 4], y: [0, -1, -2, -3]}))

結果は、次のようになります。

0.0

ここでは、「完全な」$W$と$b$の値を予測しましたが、機械学習ではこれらのモデル(を決定するパラメータ)を自動的に求めます。次の章では、その方法を見ていきます。

tf.train API

機械学習の詳細な議論は、このページでは行いませんが、TFは、ロス関数を最小化するようにゆっくりと変数を変化させるオプティマイザを提供しています。もっともシンプルなオプティマイザは、勾配降下法であり、これはロス関数の微分に基づいて変数を修正します。一般に微分を計算することは、面倒であり、また誤差が生じやすい部分となります。TFでは、tf.gradientsという関数を用いることにより、この微分を自動で得ることができます。簡単なオプティマイザの例を挙げます。

optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss)
sess.run(init) # 変数のリセット
for i in range(1000):
  sess.run(train, {x: [1, 2, 3, 4], y: [0, -1, -2, -3]})

print(sess.run([W, b]))

これにより、モデルのパラメータは以下のように求められます。

[array([-0.9999969], dtype=float32), array([ 0.99999082],
 dtype=float32)]

これで、実際の機械学習を行ったこととなります。これは、非常に簡単な例であり、TFのコアなコードを使用していませんが、より複雑なモデルやメソッドを実装するときには、必要に応じてコードを追加しなければなりません。TFでは、より抽象化されたAPIが提供されているので、次章では、その使い方を見ていきます。

完全なコード

線形回帰モデルの、完全なコードは次の通りです。

import tensorflow as tf

# モデルパラメータ
W = tf.Variable([.3], dtype=tf.float32)
b = tf.Variable([-.3], dtype=tf.float32)
# モデルの入出力
x = tf.placeholder(tf.float32)
linear_model = W * x + b
y = tf.placeholder(tf.float32)

# ロス
loss = tf.reduce_sum(tf.square(linear_model - y)) # sum of the squares
# オプティマイザ
optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss)

# 訓練データ
x_train = [1, 2, 3, 4]
y_train = [0, -1, -2, -3]
# 訓練ループ
init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init) # reset values to wrong
for i in range(1000):
  sess.run(train, {x: x_train, y: y_train})

# 訓練精度の評価
curr_W, curr_b, curr_loss = sess.run([W, b, loss], {x: x_train, y: y_train})
print("W: %s b: %s loss: %s"%(curr_W, curr_b, curr_loss))
W: [-0.9999969] b: [ 0.99999082] loss: 5.69997e-11

ここで、ロス関数の値が非常に小さくなっていることに注目してください。実際に、このコードを実行したときは、ランダムに初期化された変数のために異なった結果が得られるかもしれません。

これを、TensorBoardで可視化したものが、次の図となります。

f:id:hiyoko9t:20171005181309p:plain

tf.estimator

tf.estimatorは、機械学習の仕組みを簡単にした高いレベルのTFのライブラリです。
機能としては、次のものを含みます。

  1. 訓練ループを回す
  2. 評価ループを回す
  3. データセットを管理する

基本的な使い方

tf.estimatorを用いて、線形回帰問題がどれぐらいシンプルにかけるかを見てみましょう。

import tensorflow as tf
# NumPy はデータのロード、計算、前処理によく用いられます。
import numpy as np

# 特徴量のリストの宣言。ここでは、一つの特徴量だけですが、他にも多くの、より複雑で有用な型が存在します。
feature_columns = [tf.feature_column.numeric_column("x", shape=[1])]

# 推定器による訓練と評価。線形回帰や線形分類器、ニューラルネットワーク回帰、分類器など多くの準備された型が存在します。(ここでは線形回帰を用います)
estimator = tf.estimator.LinearRegressor(feature_columns=feature_columns)

# TFは、多くのデータセットの読み込みやセットアップに役立つメソッドが準備されています。ここでは、訓練データと評価データを使用します。
x_train = np.array([1., 2., 3., 4.])
y_train = np.array([0., -1., -2., -3.])
x_eval = np.array([2., 5., 8., 1.])
y_eval = np.array([-1.01, -4.1, -7, 0.])
input_fn = tf.estimator.inputs.numpy_input_fn(
    {"x": x_train}, y_train, batch_size=4, num_epochs=None, shuffle=True)
train_input_fn = tf.estimator.inputs.numpy_input_fn(
    {"x": x_train}, y_train, batch_size=4, num_epochs=1000, shuffle=False)
eval_input_fn = tf.estimator.inputs.numpy_input_fn(
    {"x": x_eval}, y_eval, batch_size=4, num_epochs=1000, shuffle=False)

# 訓練データを用いて、訓練を1000ステップ実行
estimator.train(input_fn=input_fn, steps=1000)

# モデルの性能を評価
train_metrics = estimator.evaluate(input_fn=train_input_fn)
eval_metrics = estimator.evaluate(input_fn=eval_input_fn)
print("train metrics: %r"% train_metrics)
print("eval metrics: %r"% eval_metrics)

これを実行した結果が、次のようになります。

train metrics: {'loss': 1.2712867e-09, 'global_step': 1000}
eval metrics: {'loss': 0.0025279333, 'global_step': 1000}

訓練と比較して、評価のロスが大きいことに気づかれるかもしれません。しかし、ロスが$0$に近づいていることから、正しく学習できたことがわかります。

カスタムモデル

tf.estimatorは、先の準備された形でしか使えないものではありません。今、TFでビルドしないモデルを作りたいとします。ここでは、低いレベルのTF APIを使用することにより、先の線形回帰モデルと等価なモデルを作成します。

カスタムされたモデルを使うには、tf.estimator.Estimatorを用います。tf.estimator.LinearRegressorは、tf.estimator.Estimatorのサブクラスとなります。ここでは、model_fnという関数を用いて、予測、訓練、ロスの評価をどう行うかを定義します。

import numpy as np
import tensorflow as tf

# 特徴量の宣言
def model_fn(features, labels, mode):
  # 線形モデルと予測変数のビルド
  W = tf.get_variable("W", [1], dtype=tf.float64)
  b = tf.get_variable("b", [1], dtype=tf.float64)
  y = W * features['x'] + b
  # ロス
  loss = tf.reduce_sum(tf.square(y - labels))
  # 訓練
  global_step = tf.train.get_global_step()
  optimizer = tf.train.GradientDescentOptimizer(0.01)
  train = tf.group(optimizer.minimize(loss),
                   tf.assign_add(global_step, 1))
  # 推定器との接続
  return tf.estimator.EstimatorSpec(
      mode=mode,
      predictions=y,
      loss=loss,
      train_op=train)

estimator = tf.estimator.Estimator(model_fn=model_fn)
# データセットの定義
x_train = np.array([1., 2., 3., 4.])
y_train = np.array([0., -1., -2., -3.])
x_eval = np.array([2., 5., 8., 1.])
y_eval = np.array([-1.01, -4.1, -7, 0.])
input_fn = tf.estimator.inputs.numpy_input_fn(
    {"x": x_train}, y_train, batch_size=4, num_epochs=None, shuffle=True)
train_input_fn = tf.estimator.inputs.numpy_input_fn(
    {"x": x_train}, y_train, batch_size=4, num_epochs=1000, shuffle=False)
eval_input_fn = tf.estimator.inputs.numpy_input_fn(
    {"x": x_eval}, y_eval, batch_size=4, num_epochs=1000, shuffle=False)

# 訓練
estimator.train(input_fn=input_fn, steps=1000)
# モデルの評価
train_metrics = estimator.evaluate(input_fn=train_input_fn)
eval_metrics = estimator.evaluate(input_fn=eval_input_fn)
print("train metrics: %r"% train_metrics)
print("eval metrics: %r"% eval_metrics)

これを実行することにより、次の結果が得られます。

train metrics: {'loss': 1.227995e-11, 'global_step': 1000}
eval metrics: {'loss': 0.01010036, 'global_step': 1000}

model_fn()関数に注目すると、本記事で作成した訓練ループと非常によく似ていることがわかります。

まとめ

本記事では、Tensorflowのインストールから線形モデルに対する訓練、評価という機械学習の一連の流れを見てきました。 公式のドキュメントでは、MNISTを用いた画像分類やTensorBoardを用いたグラフの可視化、CNNの構築など中々のボリュームのチュートリアルがあるので、興味のある人はぜひいっしょに頑張りましょう。

イラストレーターの絵から学ぶ画像認識 - 基礎からGANを用いた画像生成まで -

本記事では、画像認識(image recognition)の概要を紹介していきます。

画像認識は、手書き文字の認識、ガン診断、自動運転技術などその応用が非常に多岐にわたることで知られています。しかしながら、画像認識は画像の情報量の多さ、また3次元の情報を2次元に射影していることによる幾何学的な縮退などのため、多くの課題を抱えており、とっつきづらい分野でもあります。そこで、本記事では、できるだけ基本的なアプローチを紹介して、そういった敷居を下げることを目的としています。

本記事では、イラストをもとに様々な画像認識のトピックを紹介していきます。
今回は、Twitterやpixivで活躍されているイラストレーターの花かんざらしさん(@kore099)から許可を頂き、絵を使用させて頂くこととなりました。
実際に使用させて頂く絵は以下になります。

f:id:hiyoko9t:20170921021259j:plain
図1. しぶりん

画像認識の基本

画像認識と一口に言っても、その目的によって様々な方法に分かれます。例として、写っている物体に対して適切なラベルを与える物体認識、複数の物体から意味のある状況を読み取るシーン認識、さらに目標となる対象が存在する領域を検出する物体検出などが挙げられます(図2)。

f:id:hiyoko9t:20170924153308j:plain
図2. 画像認識の分類

本記事では、(その他の手法とも共通する部分が多いため)主にクラス認識を対象として、画像認識の基本を説明していきます。例として、図1が入力画像として与えられたとき、人というラベルを付与することが目的となります。

画像認識といえば畳み込みニューラルネットワーク(Convolutional Neural Network; CNN)だ!という方もいるかもしれません。実際、CNNの利点として、局所特徴抽出などに詳しくなくても分類ができるという点もあり、あながち間違いではないと思います。しかし、伝統的な画像処理手法を知っておくことは、柔軟な画像処理を行い、無駄な計算を減らすことやネットワークの適切な構成方法を構築することに繋がります。

クラス認識の手順

クラス認識の手順は主に以下の3つとなります。([5]では主に2. と3. が取り上げられています)

  1. 前処理
  2. 特徴抽出
    1. サンプリング
    2. 局所記述
    3. 統計的特徴抽出
    4. コーディング
    5. プーリング
  3. 分類

以降では、これらのトピックを一つづつ紹介していきます。また、実装にはopencvを利用していますが、こちらの導入方法などは公式ドキュメントの方でお願いします。

前処理

機械学習においてデータの前処理は重要ですが、画像認識においても重要となります。
データの前処理では、主にノイズの除去やコントラスト、輝度の標準化を行います[11]。

まず、ノイズの載った画像を考えるため、図1にガウシアンノイズを加えた画像を見てみましょう。 ガウシアンノイズは、以下の確率密度関数$p$から得られます。 $$ p(x) = \frac{1}{\sqrt{2 \pi v^2}} exp \left( - \frac{(x-m)^2}{2v^2} \right) $$ ここで, $m, v^2$はそれぞれ平均、分散をあらわします。

実際にガウシアンノイズを乗せてみます。

# coding:utf-8
import cv2
from matplotlib import pyplot

# 画像の読み込み
img = cv2.imread('001.jpg')

# BGRからRBGへ
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# ガウシアンノイズの付与
img_gaussian_noise = img
row, col, d = img.shape

# ノイズの作成
gaussian_noise = np.zeros((row, col, d), np.int8)
cv2.randn(gaussian_noise, np.zeros(3), np.ones(3)*10)

# ノイズの追加
img_gaussian_noise = cv2.add(img_gaussian_noise, gaussian_noise, dtype=cv2.CV_8UC3)


#  描画の設定
pyplot.figure(figsize=(16,9))
pyplot.xticks([]), pyplot.yticks([])

# 描画
pyplot.imshow(img_gaussian_noise)

# 保存
filename = "001_gauss.png"
pyplot.savefig(filename)

pyplot.show()

今回は、視覚的にわかりやすいようにノイズの標準偏差の値を大きくしました。標準偏差の値が大きい方が、ノイズが大きいことがわかると思います。ちなみに、わざわざ画像にノイズを乗せることなんてあるのか?と疑問に思われる方もいるかもしれません。これに関しては、学習データが少ない時に学習データの水増しのため、ノイズを乗せた画像を別の画像として利用することがあります。その他にも平行移動や回転で画像の枚数を増やす方法もあります。

f:id:hiyoko9t:20170925202334p:plain
図3. 標準偏差10のガウシアンノイズ

f:id:hiyoko9t:20170925202410p:plain
図4. 標準偏差30のガウシアンノイズ

さて、話がそれましたが、このようなノイズを除去する方法として、平滑化という手法がよく知られています。 代表的なものとして、平均値フィルタが挙げられます。$3\times 3$の平均値フィルタは以下のようにあらわされます。 $$ \frac{1}{9} \begin{pmatrix} 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \end{pmatrix} $$ 式から、この平均値フィルタは注目画素の周囲8マスとの画素の和をとって、それを画素数で割っていることがわかります。これにより、ノイズを低減させることができます。
また、平均値フィルタでは注目画素とその周囲の画素が同じ重みですが、実際には注目画素から近ければ近いほど重要であると考えられます。それを実現したのがガウシアンフィルタとなります。 $$ G_v(x, y) = \frac{1}{2 \pi v^2} exp \left( - \frac{x^2 + y^2}{2v^2} \right) $$ 実際に適用してみます。今回は、視覚的にわかりやすくするためにカーネルサイズを$15\times 15$と非常に大きくとっていますが、通常は$3\times 3$, $5\times 5$程度だと思います。

# 平滑化
img_smoothing = cv2.GaussianBlur(img_gaussian_noise, (15, 15), 0)

f:id:hiyoko9t:20170925202455p:plain
図5. 図3に対して平滑化

f:id:hiyoko9t:20170925202528p:plain
図6. 図4に対して平滑化

図5, 6から、画像全体がぼけたようになっており、ノイズが小さくなったことがわかります。

特徴抽出

ここでは、簡単に特徴抽出の手法とそれぞれの役割を見ていきます。

サンプリング

画像は情報量が非常に大きいので、それそのものを扱うのは計算コストが高く、また(分類に)必要のない点も考慮しなければなりません。そこで、画像から特徴的な点を抽出するサンプリングを行います。(サンプリングに関して詳しく知りたい方は[8]をどうぞ)

まず、簡単に思いつく方法として、一定間隔ごとに画像の代表点を取得する方法が挙げられます。以下の図7に$10\times 10$の画像のサンプリングの例を示します。図中の次元は、今回だとRGBなので3次元となります。パッチサイズは注目画素のまわりをどれだけ考えるかを示しています。

f:id:hiyoko9t:20170925011928j:plain
図7. サンプリングの例

また、一定ではなく画像の特徴的な点、例えば人と背景の境界など、をサンプリングする方法も提案されています。

最も代表的なエッジ検出を紹介します。

今、画像における$(x, y)$の画素を$I(x, y)$としたとき、その$x$軸方向の微分は、離散画像なので次のような差分であらわされます。 $$ \Delta I_x (x, y) = I(x+1, y) - I(x,y) $$ また, これは前進差分と呼ばれ、以下であらわされる微分が後進差分となります。 $$ \Delta I_x (x, y) = I(x, y) - I(x-1,y) $$ さらに平均をとることもできます。 $$ \Delta I_x (x, y) = \frac{I(x+1, y) - I(x-1,y)}{2} $$ $y$方向についても同様であり、これにより、勾配が大きい点(=色の変化が大きい点=何らかの境界となる点)が得られます。

さらに、一階微分だけでなく二回微分を用いたラプラシアンオペレータが存在します。 ここでは、ラプラシアンオペレータを図1に適用した結果を示します。

img_laplacian = cv2.Laplacian(img, cv2.CV_64F) #x方向, y方向の2回微分の和

f:id:hiyoko9t:20170925202638p:plain
図8. しぶりんにラプラシアンオペレータを適用した結果

また同作者が描かれた初音ミクについてもラプラシアンオペレータを適用してみます。

f:id:hiyoko9t:20170925005656p:plain
図9. 初音ミクラプラシアンオペレータを適用した結果

このように、人物と背景、人物と衣類の境界などをきちんと分離できていることがわかります。

局所記述

サンプリングで選んだ代表点だけでは、ノイズの混じった点を選んでしまった時に、誤った特徴となりやすいので、その周囲(図7における青)のパッチ領域からも情報を取得します。これを局所記述と言います。

局所記述子として有名なものにSIFT記述子が挙げられます。SIFT記述子は異なるスケールや回転に対してロバスト(頑健)になることが知られています。 SIFT記述子に関しては、こちらのスライド( MIRU2013チュートリアル:SIFTとそれ以降のアプローチ) に詳しく載っていますが、簡単に触れると平滑化された画像を$L$として、その$(x, y)$において勾配強度mと勾配方向$\theta$を求めます。 $$ m(x,y)=\sqrt{L_x^2(x,y) + L_y^2(x,y)}, \theta (x,y) = tan^{-1}\frac{L_y(x,y)}{L_x(x,y)} $$ これを、あらかじめ分割した空間に割り当てて、特徴量を求めます。例えば、勾配方向を8分割(8ビン)すれば、横軸に$0°, 45°, 90°, …$といった$8$次元のヒストグラムが得られます。

統計的特徴抽出

サンプリングと局所記述で得られた情報には、ノイズなどが含まれている可能性があります。 そこで、これらのノイズを削減する方法として、統計的特徴抽出が用いられます。

具体的には、主成分分析などがよく用いられます。これは、前記事で行列の特異値分解と絡めて紹介しているのでそちらをご覧ください。 hiyoko9t.hatenadiary.jp

コーディング

サンプリングと局所記述で得られた特徴を分類が容易になるような高次元空間へ写像する手法をコーディングと言います。

有名な手法の一つとして、自然言語処理の分野で用いられているBag of Words(BoW)の考え方から生まれたBag of Visual Wordsという手法があります。 BoWの例を以下に示します。

import numpy as np
from sklearn.feature_extraction.text import CountVectorizer

count = CountVectorizer()

strings = np.array(["This is a pen", "This is a pen and that is a pencil"])
bag = count.fit_transform(strings)

print(count.vocabulary_)
print(bag.toarray())

ここで、"This is a pen"と"This is a pen and that is a pencil"という二つの文章が存在する時、BoWはそれぞれの文書中における単語の出現回数をあらわします。 以下が、出力結果であり、1行目でそれぞれの単語とインデックスが結びついており、2, 3行目にそれぞれの単語の出現回数が表示されています。3行目の2つ目の要素が2となっているのは、文中に"is"が2回あらわれたことを意味しています。

{'pen': 2, 'is': 1, 'and': 0, 'this': 5, 'pencil': 3, 'that': 4}
[[0 1 1 0 0 1]
 [1 2 1 1 1 1]]

BoWでは出現した単語の数をカウントしますが、BoVWでは特徴を代表するベクトルの数を求めます。(その後、全体の数で割って生起確率を求めます。) もちろん特徴を代表するベクトルを適当に選んだとき、すべての特徴ベクトルがそれに一致することはないので、最近傍の代表ベクトルに特徴ベクトルを割り振ります。

プーリング

プーリングは、コーディングで得られた特徴ベクトルを一本にまとめる作業を行います。

よく使用されるプーリングとして平均値プーリングが用いられます。領域$R$内の$N$本のコーディング後の特徴ベクトルを$c_n$とすると、平均値プーリングは以下であらわされます。 $$ \frac{1}{N} \sum_{n=1}^{N} c_n $$ これにより、コーディング後の局所特徴の数が異なっている場合でも、プーリング後の次元は領域ごとに一致することがわかります。

分類

分類手法については、一般的な機械学習の手法を用います。例としては、SVMやロジスティック回帰、決定木などが挙げられます。 これらの分類手法については、すでに多くの優れた参考書や論文がありますので、ここでは割愛させていただきます。

畳み込みニューラルネットワーク(CNN)

ニューラルネットワークは、これまで紹介してきたサンプリング、局所記述、統計的特徴抽出、コーディング、プーリングをネットワークとして表現することができます。

しかし、画像はその情報量の多さ(次元の大きさ)から学習するべきパラメータが膨大になりがちです。そこで画像の特徴を活かして、学習コストを抑えたものが畳み込みニューラルネットワークとなります。

まず、一般のユニット(ここでは一つ一つの画素をイメージしてください)が全結合している例を挙げます。
ここで、$l$層から$l+1$層の重みを$w_{ij}$とします。今、$l$層が$5$つのユニットから構成され、$l+1$層が$3$つのユニットから構成されるとき、学習すべきパラメータ$w_{ij}$は$5\times 3 = 15$個となります(図10)。

f:id:hiyoko9t:20170924234614j:plain
図10. 全結合層の例

ここで、画像は注目している画素から近い画素の影響が大きく、遠い画素の影響は小さいという特徴を持つので、ニューラルネットワークにおいてもユニットを全結合させるのではなく、近傍のユニットだけを結合させることができます。 また画像は、局所的に近い構造を持つという特徴があると考えると、それぞれのパラメータが共通であると考えられます。
この考え方を適用したものが、畳み込み層となります。

f:id:hiyoko9t:20170924235019j:plain
図11. 畳み込み層の例

図11からわかるように、レイヤー間で全結合せず、また重みを共有しているため学習すべきパラメータは$3$個と大幅に削減されていることがわかります。

このようにネットワーク間の計算コストを下げたものをCNNと呼びます。

計算コストが下がることにより、様々なフィルターを適用した特徴を画像から得ることができ、そうして得た特徴に対して畳み込み層とプーリング層を適用することでより良い特徴が得られます。

アニメ絵から顔の位置を検出

物体検出は、目標となる物体が存在する領域を推定する手法です。対象の物体が存在する領域を四角の枠で囲むことが多いですが、きっちりと境界を求める手法も存在します。
物体検出については後日、別記事で詳しく触れたいと思うので、ここでは概要だけ紹介します(詳しくは[10]など)。

大まかな手順は以下の通りです。

  1. 入力画像から物体が存在しそうな領域を抽出する
  2. 1.で得た領域に対して、分類問題を解く
  3. 2.で得た結果が重複している領域などを除去して整える

今回、物体検出について調べている時にアニメの顔検出を既にこちら( GitHub - nagadomi/lbpcascade_animeface: The face detector for anime/manga using OpenCV.) で実装されていたので、試しに以下の画像に対して、実行してみました。物体検出のイメージだけでも伝われば幸いです。

f:id:hiyoko9t:20170924191903p:plain
図12. ベンチに座るしぶりん

f:id:hiyoko9t:20170924191919p:plain
図13. ステージ上のアイドル

なぜかりーなだけ認識されていません。こういうこともあります。

敵対的生成ネットワーク(Generative Adversarial Network; GAN)

ここまでは、画像を認識することを考えてきました。

近年、GANと呼ばれる画像を生成する技術が盛んに研究されています。GANのはじまりは、Goodfellowらの論文[3]となります。GANに関してははじめてのGANがよくまとまっており、また Chainerで顔イラストの自動生成 - Qiitaでは、クオリティの高い顔イラストの生成が実現されています。機械が絵を描く日も近いかもしれないですね。

ここでは、簡単にGANの概念だけを紹介します。 GANでは、生成器(generator)と識別器(discriminator)という二つの要素が重要となります。 生成器が、訓練データを見て最終的に得たい画像を生成し、識別器は入力された画像が本物か偽物かを判定します。生成器は、識別器に本物だと思わせるように学習を行い、画像を生成します。識別器は、本物の画像なのか生成器が作った画像なのかを見破るように学習します。こうして、お互い学習し合うことで、よりリアルな画像を生成することができます(図14)。

f:id:hiyoko9t:20170925201456j:plain
図14. GANの仕組み

また、以下がもとの論文から引用したアルゴリズムとなっています。アルゴリズム中の$G$が生成器、$D$が識別器をあらわしており、これらが交互に更新されていくことがわかります。(詳細な解き方は、別記事でやるかもしれません)

f:id:hiyoko9t:20170925122147p:plain
図15. GANのアルゴリズム([3]より引用)

さらに、近年注目されている技術として、Deep Convolutional GAN(DCGAN)が挙げられます。DCGANは、GANにCNNを適用したものであり、より鮮明な画像を得ることができます。その生成器の概要が以下の図16となります。

f:id:hiyoko9t:20170925115625p:plain
図16. DCGAN([1]より引用)

ここでは、入力として100次元のベクトルが与えられ、出力として$64\times 64$の画像が生成されます。これは、画像認識の逆のプロセスを辿っていることがわかります。(画像認識では、入力が画像、出力がクラス認識であれば所属するクラスのラベルでした。)中間の層の詳細な説明はここでは行いませんが、このネットワークを用いて、画像を生成します。

DCGANはこちら(GitHub - carpedm20/DCGAN-tensorflow: A tensorflow implementation of "Deep Convolutional Generative Adversarial Networks")で実装されているので、今回はそれを利用しました。また使用するデータとしては、手書き文字認識で有名なMNISTを用いました。(本当は、イラストやアニメ画像の生成を行いたかったのですが、マシンパワーやデータの取得などの観点から今回は見送りました。後日、リベンジしたいと思います。)

以下に、DCGANを用いて生成された数字の画像を示します。

f:id:hiyoko9t:20170925171607p:plain
図17. エポック0で生成された画像

f:id:hiyoko9t:20170925171607p:plain
図18. エポック1で生成された画像

f:id:hiyoko9t:20170925171740p:plain
図19. 最終的に生成された画像

図17、18は訓練の初期で生成された画像でまだ数字らしくない画像が生成されています。しかし、学習を続けて得られた図19では、かなり鮮明に数字として読み取れる画像が生成されていることがわかります。これが、画像生成の一例です。

正直GANに関しては、まだほとんど追えていないのですが、画像を生成することができる、というのが非常に興味深く響いたのでここでは取り上げてみました。これから論文を追っていこうと思うので、気が向けばまた記事を書くと思います。

まとめ

本記事では、画像認識で知っておくべき基礎的な内容を紹介してきました。直感的な理解を優先したため、複雑な数式などはほとんど使いませんでしたが、実際に数式を追えば、より理解が進むと思います。さらに詳しいことが知りたい方は、画像認識 (機械学習プロフェッショナルシリーズ), 原田達也 を一読されることをオススメします。一週間ほど前、画像認識に関する知識がほぼゼロの状態から、本書を三日ほどかけて一読させて頂きましたが、数式なども追いやすく、また構成も理解しやすいものだったのでぜひ興味のある方はぜひどうぞ。

最後に、今回イラストをお借りする許可を頂いた花かんざらしさん(@kore099)に改めて感謝致します。

参考文献

[1] Alec Radford, Luke Metz, Soumith Chintala, “Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks”, ICLR 2016, 2015.
[2] Carpedm20, “DCGAN-tensorflow”, https://github.com/carpedm20/DCGAN-tensorflow, 2017.
[3] Goodfellow, Ian J., Pouget-Abadie, Jean, Mirza, Mehdi, Xu, Bing, Warde-Farley, David, Ozair, Sherjil, Courville, Aaron C., and Bengio, Yoshua., “Generative adversarial nets”, NIPS, 2014.
[4] 花かんざらし, Available at https://twitter.com/kore099, 2017.
[5] 原田達也, “画像認識 (機械学習プロフェッショナルシリーズ)”, 講談社, 2017.
[6] 藤吉弘亘, “画像局所特徴量SIFTとそれ以降のアプローチ”, Available at https://www.slideshare.net/hironobufujiyoshi/miru2013sift, 2013.
[7] Mattya, “Chainerで顔イラストの自動生成”, Available at http://qiita.com/mattya/items/e5bfe5e04b9d2f0bbd47, 2015.
[8] 美濃導彦, “画像処理論 - Web情報理解のための基礎知識 -” , 昭晃堂, 2010.
[9] Nagadomi, “lbpcascade_animeface”, Available at https://github.com/nagadomi/lbpcascade_animeface, 2011.
[10] Ross Girshick, Jeff Donahue, Trevor Darrell, Jitendra Malik, “Rich feature hierarchies for accurate object detection and semantic segmentation”, UC Berkeley and ICSI, 2014.
[11] Satya Mallick, “Image Recognition and Object Detection : Part 1”, Available at http://www.learnopencv.com/image-recognition-and-object-detection-part1/, 2017.
[12] Shinya Yuki, “はじめてのGAN”, Available at https://elix-tech.github.io/ja/2017/02/06/gan.html, 2017.

固有値、固有ベクトルの幾何的な意味を考える

本記事では、簡単な例を用いて固有値(eigenvalue)、固有ベクトル(eigenvector)の幾何的な意味を考えます。
固有値固有ベクトルと言えば、理系の大学生なら4年間付き合うことになる概念なので、式だけでなく幾何的な意味も捉えておきましょう。

はじめに固有値固有ベクトルの定義を与えます。

$Definition.$ $n$次正方行列$A$に対して $$ Ax=\lambda x $$ を満たすベクトル$x\neq 0$が存在するとする。このとき、スカラー$\lambda$を$A$の固有値、$x$を固有値$\lambda$に対する$A$の固有ベクトルという。

まず、非常に簡単な例として、行列$A$を以下で与えます。 $$ A= \begin{pmatrix} 2 & 0 \\ 0 & 3 \end{pmatrix} $$ これは、対角成分以外の要素が$0$であるような対角行列となっています。

では、この固有値固有ベクトルを求めます。今回は固有値固有ベクトルを求める部分がメインではないのでPythonでサクッと求めてしまいます。(もちろん定義に則って計算すれば同様の解が得られます。)

import numpy as np

A = np.array([[2,0],[0,3]])
eigval_A, eigvec_A = np.linalg.eig(A)

これより、行列$A$の固有値は、$\lambda_1 = 2, \lambda_2 = 3$となり、それぞれに対応する固有ベクトルは $$ x_1 = \begin{pmatrix} 1 \\ 0 \end{pmatrix}, x_2 = \begin{pmatrix} 0 \\ 1 \end{pmatrix} $$ となります。次の図が、固有ベクトルをあらわします。 f:id:hiyoko9t:20170914202727p:plain

ここからは、あるベクトル$v=(v_1, v_2)$に左から$A$をかけたときの線形写像を考えます。 今、$v=(1, 1)$というベクトルに左から$A$をかけると $$ Av= \begin{pmatrix} 2 & 0 \\ 0 & 3 \end{pmatrix} \begin{pmatrix} 1 \\ 1 \end{pmatrix}=\begin{pmatrix} 2 \\ 3 \end{pmatrix} $$ より、もとのベクトル$v$と写像後のベクトル$Av$は次のようにあらわされます。

f:id:hiyoko9t:20170914202945p:plain

これより、行列$A$をかけることによって$v$は$x$軸方向に$2$倍、$y$軸方向に$3$倍引き伸ばされたことがわかります。
このように、行列$A$によりもとのベクトルは、固有ベクトルの方向に固有値の大きさだけ引き伸ばされることがわかります。

任意のベクトル$v=(a,b)$($a,b$は任意のスカラー)に対して、これが成り立つことを確認します。まず、$Av$は $$ Av=A\left( a\times \begin{pmatrix} 1 \\ 0 \end{pmatrix}+ b\times \begin{pmatrix} 0 \\ 1 \end{pmatrix}\right)=a \times Ax_1 +b\times Ax_2 $$ とあらわされます。ここで、固有値固有ベクトルの定義から$Ax_1 = \lambda_1 x_1$, $Ax_2 = \lambda_2 x_2$であるので $$ Av= a \times \lambda_1 x_1 + b\times \lambda_2 x_2 = \begin{pmatrix} 2a \\ 3b \end{pmatrix} $$ となることがわかります。これより、任意のベクトルに対しても同様のことが言えると確認できました。

上の例では、対角行列という特別な場合でしたので、対角成分=固有値となる簡単な例でしたが、実際はもう少し複雑です。
しかし、行列$A$の固有値が分かっている時にあるベクトル$v$を適当な固有ベクトル$(x_1, x_2)$の線形和であらわすことができれば、$Av$は簡単に計算することができます。すなわち、 $$v = \alpha x_1 + \beta x_2$$ であるとき、 $$Av = \alpha Ax_1 + \beta Ax_2 = \alpha \lambda_1 x_1 + \beta \lambda_2 x_2$$ となり、計算できることがわかります。また、式から行列計算を行わずに、$Av$の結果を求めることができたことも確認できます。

次に、対角成分が$0$でないような次の行列$B$を考えてみます。 $$ B= \begin{pmatrix} 1 & 1 \\ -2 & 4 \end{pmatrix} $$ この固有値固有ベクトルを求めると、固有値は$\lambda_1 = 2, \lambda_2 = 3$となり、固有ベクトルはそれぞれ $$ x_1=\frac{1}{\sqrt{2}} \begin{pmatrix} -1 \\ -1 \end{pmatrix}, x_2 = \frac{1}{\sqrt{5}} \begin{pmatrix} -1 \\ -2 \end{pmatrix} $$ となります。

f:id:hiyoko9t:20170914211051p:plain

ここでベクトル$v$として$v=(1,0)$を考えると、このベクトルは $$ v = -2\sqrt{2} x_1 + \sqrt{5} x_2 $$ とあらわされるので、 $$ Av = -2\sqrt{2} Ax_1 + \sqrt{5} Ax_2 = -4\sqrt{2} x_1 + 3\sqrt{5} x_2 = -4\begin{pmatrix} -1 \\ -1 \end{pmatrix} + 3\begin{pmatrix} -1 \\ -2 \end{pmatrix} = \begin{pmatrix} 1 \\ -2 \end{pmatrix} $$ と計算できます。

f:id:hiyoko9t:20170914213138p:plain

本記事では、簡単な例を通して固有値固有ベクトルの幾何的な意味を見てきました。 前記事の主成分分析 hiyoko9t.hatenadiary.jp

においても、固有値の大きさが寄与率に影響することに触れましたが、その意味が理解できたかと思います。 行列を扱う上で必ず出てくる固有値固有ベクトルですが、その意味を考えれば、理解しやすいと思います。

不定期更新ですがよろしくお願いします。
何かあればコメントやツイッター(@hiyoko9t)でお気軽にお声がけください。

主成分分析とは何か

本記事では、多変量解析の代表的な手法の一つである主成分分析(Principal Component Analysis; PCA)について紹介します。 機械学習の文脈では、しばしば次元削減のために用いられますが、その他にもノイズの除去や多変量データの可視化などに用いられます。

本記事は大いに、以下の論文を参考にしています。詳細が知りたい方はそちらへ。

Hervé Abdi, Lynne J.Williams, "Principal Component Analysis", Available at https://www.researchgate.net/profile/Lynne_Williams/publication/227644862_Principal_Component_Analysis/links/00b7d51657d5ad0d15000000/Principal-Component-Analysis.pdf, 2010.

主成分分析の目的は、高次元のデータから分散が最大となる方向を求め、それをもとの次元と同じかそれ以下に射影することとなります。

イメージを掴むため、2次元のデータを1次元へ次元削減する非常に簡単な例を挙げます。
今、二つの点$$(x,y)=(-1, 1), (1,1)$$があるとき、これをもとの点の情報を保ったまま1次元へ次元を減らすことを考えます。 図中での横方向の実線をx軸、縦方向の実線をy軸としたとき、まずy軸への射影を考えてみます。

f:id:hiyoko9t:20170912200151p:plain

上図のように$(-1, 1)\mapsto (1)$, $(1, 1)\mapsto (1)$と二つの点が重なりもとの情報が失われてしまいます。一方、x軸への射影を考えると、

f:id:hiyoko9t:20170912195703p:plain

上図より、$(-1, 1)\mapsto (-1)$, $(1, 1)\mapsto (1)$となり、もとのデータの情報を保ったまま、次元を削減することができます。このように高次元データ(例では2次元ですが)から分散が最大となる方向へ射影することにより次元を削減することができます。 実際には、例えば100次元から2次元へ次元削減を行ったとき、一般にもとのデータの情報を100%保持することはできません。しかし、どの程度それを保持できているか(寄与率)は求めることができます。

以下では、主成分分析の数式的な解釈を与えます。

まず、$(n\times m)-$行列$X$において、$rank(X)=k$とします。 このとき、$X$は特異値分解を用いて、次のようにあらわされます。 $$X = P\Delta Q^T$$ ただし、$P$は$(n\times k)$の左特異値ベクトルを並べた行列、$Q$は$(m\times k)-$の右特異値ベクトルを並べた行列となります。また、$\Delta$は対角に特異値をもつ行列となります。ここで、$X^T X, XX^T$の非ゼロな(これらは半正定値行列なので正の)固有値を対角に並べた行列を$\Lambda$とすると、$\Delta ^2=\Lambda $となります。($\because XX^T = P \Delta Q^T Q \Delta P^T = P \Delta ^2 P^T $)

ここでは、特異値分解については詳しく触れないので、このような分解が存在するということを知っていただければ大丈夫です。

上式において、$F=P \Delta$(得点行列と呼ばれます)とあらわすと、以下の式が成り立ちます。 $$ F= P \Delta = P \Delta Q Q^T = XQ $$ 上式より、$Q$は 射影行列とみなすことができます。幾何的な解釈に関しては、先の論文内に例とともに挙げられています。

ここで、$F$は$(n\times k)$の得点行列でしたが、$l<k$を満たす$l$に対して、$(n\times l)$に次元削減された得点行列$F_l$を考えます。対応する射影行列を$Q_l$とすると、以下が成り立ちます。 $$ F_l = XQ_l $$ このとき、射影行列$Q_l$、得点行列$F_l$を用いて求められる行列を$X_l$とすると $$ \| X - X_l \| = \| FQ^T - F_l Q_l^T \| $$ を最小化するような、階数$l$の行列を選ぶと良いことがわかります。ただし、行列$A$に対して、$\| A \|$はフロベニウスノルムをあらわします。 これを求めると、寄与の大きい$l$個の特異値に対応する特異ベクトルが射影行列として残ります。(よく見る寄与が大きい順=固有値が大きい順にベクトルを選択するという結果との関連もわかると思います)

最後に、射影後の寄与率について少し触れます。 上記の括弧書きで書いたように、特徴量の分散共分散行列の固有値が大きいものから寄与が大きいという情報を目にしたことがあるかもしれません。 分散共分散行列を$\sum$とすると、その固有値$\lambda$、固有ベクトル$v$は以下の式を満たします。 $$ \sum v= \lambda v $$ ここで、データに含まれる多くの情報(分散)を含む固有ベクトルを選びますが、固有値固有ベクトルの大きさをあらわすため、固有値が大きい順にそれに対応する軸を選びます。 ここで、固有値$\lambda_i$の寄与率は、以下の式であらわされます。 $$ \frac{\lambda_i}{\sum_{i} \lambda_i} $$ 例として、固有値が$\lambda = 3,2,1$のとき、3次元から2次元への次元削減は $$ \frac{3+2}{3+2+1} = 0.83333... $$ より、83%程度の情報を保持します。

本記事では、特異値分解を用いて主成分分析と紐づけてきました。 最後に少しだけ書きましたが、分散共分散行列との関係性も非常に興味深いものなのでぜひ調べて見てください。

不定期更新ですがよろしくお願いします。
何かあればコメントやツイッター(@hiyoko9t)でお気軽にお声がけください。  

これだけは知っておきたい最適化アルゴリズムの基礎

本記事では、最適化アルゴリズムの基礎を最急降下法(steepest descent)を用いて紹介します。

機械学習をメインにされている人も、確率的降下勾配法(Stochastic Gradient Descent; SGD)の基礎になるテーマなので知っておいて損はありません。

 

今回は、以下の無制約最小化問題を考えます。

{min:f(x)}

まず、この問題に対する解の定義を与えます。

  • 大域的最適解(global minimizer)

{x^*\in R^n}が任意の{x\in R^n}に対して、{f(x^*)\leq f(x)}が成り立つとき、{x^*}を大域的最適解という。

  • 局所的最適解(local minimizer)

{x^*\in R^n}の適当な近傍を{N(x^*)}とする。このとき、任意の点{x \in N(x^*)}に対して、{f(x^*)\leq f(x)}が成り立つとき、{x^*}を局所的最適解という。

簡単に言えば、大域的最適解は一番小さい点、局所的最適解はまわりと比較して一番小さい点となります。

 

準備が出来たので、ここから反復法の説明を行います。

反復法では、適当な初期点{x^0 \in R^n}からスタートして点を以下のように更新します。
{x^{k+1} = x^{k} + \alpha^k d^k}

ここで{d^k \in R^n}は、探索方向と呼ばれ、この方向に進むことで{k}回目の反復点より、{k+1}回目の反復点の方が解に近づくようになります。{\alpha^k}スカラーで探索方向にどれぐらい進むかを制御します。

この更新を繰り返すことで、{x^k}が最適解{x^*}に十分近づいたとき、アルゴリズムを終了します。(イメージとしては、十分小さい{\epsilon}に対して{| x^k - x^* | \leq \epsilon}を満たしたとき。真の解がわからないから反復法を使っているのにどうやって判定するんだ、という鋭いツッコミは別記事で...)

これが反復法の基礎となります。連続最適化における様々なアルゴリズムは、この探索方向{d^k}の求め方が異なります。

 

さて、探索方向{d^k}の決定方法ですが、これは関数が減少する方向となることが期待されます。すなわち、

{f(x^{k+1}) - f(x^{k}) =f(x^{k} +\alpha ^k d^k) - f(x^{k})}

が負となるような{d^k}を求めたい、ということに他なりません。

ここで関数{f}微分可能なとき、

{lim_{t\rightarrow +0} \displaystyle \frac{f(x^{k} +t d^k) - f(x^{k})}{t} = \nabla f(x^k)^T d^k}

より、この右辺が負となれば良いことがわかります。(イメージしづらい方は、{f(x^{k} +\alpha ^k d^k))}テイラー展開して、一次の項までで打ち切ってみてください)
この右辺を、方向微係数と呼び、反復法ではこれが負となるような{d^k}を選びます。

 

さて、ここから最急降下法の紹介に入ります。

上述より、方向微係数は次のようにあらわされます。

{\nabla f(x^k)^T d^k = \|\nabla f(x^k) \| \| d^k \| cos \gamma}

ただし、 {\gamma}{f(x^k)}{d^k}のなす角です。この式より、方向微係数を最小にするのは {\gamma = \pi}のときであり、探索方向は

{d^k = -\nabla f(x^k)}

として、求められます。

以上より、アルゴリズムは現在の反復点{x^k}が、解に十分近づくまで以下の更新を行います。

{x^{k+1} = x^k + \alpha^k (-\nabla f(x^k)) =x^k - \alpha^k \nabla f(x^k)}

このように、方向微係数を最小とする方向を用いて、解を探索するために最急降下法と呼ばれます。

 

では、探索方向を常に方向微係数を最小にする {d^k = -\nabla f(x^k)}とすれば良いのでしょうか。実際には、これでは上手くいかない場合(振動など)があります。また、式を見てわかる通り、今回は一回微分までの情報しか使っていませんが、二回微分の情報を用いるアルゴリズム(ニュートン法)もあります。

 

さらに、ステップ幅{\alpha^k}も適当に取れば良いということではなく、解にたどり着くための条件が存在します。収束性(アルゴリズムが解に本当に収束するのか)や収束率 (どれぐらいの速さで解に収束するのか)などを解析するときに、重要となってきます。

 

今回は、最適化アルゴリズムの基礎を最急降下法を用いて紹介してきました。

これらは、機械学習でよく用いられる最適化手法の理解の助けになると思います。解を求めるときに、このようなアルゴリズムが基礎になっていることを想像していただければ幸いです。

 

不定期更新ですがよろしくお願いします。
何かあればコメントやツイッター(@hiyoko9t)でお気軽にお声がけください。