ハードウェアエンジニアの備忘録

電子工学(半導体物性)→応用光学・半導体プロセス→アナログ回路→C/C++→C#/.NETと低レイヤーから順調に(?)キャリアを登ってきているハードウェアエンジニアの備忘録。ブログ開始時点でiOSやサーバーサイドはほぼ素人です。IoTがマイブーム。

TensorFlowで学ぶディープラーニング入門備忘録【第1章】

上記TensorFlowで学ぶディープラーニング入門という本を買ったので、その備忘録。

第1章 TensorFlow入門

1.1 ディープラーニングとTensorFlow

1.1.1 機械学習の考え方

月々の平均気温を予測する場合、月々の平均気温の測定値(データ)になめらかな曲線を描く。与えられたデータの数値をそのまま受け取るのではなく、その背後にある仕組みを考える→データのモデル化

今回の場合、平均気温の測定値を次の四次関数で表すとする。

{
\displaystyle
\begin{equation}
y=w_0+w_1x+w_2x^{2}+w_3x^{3}+w_4x^{4} \tag{1.1}
\end{equation}
}

予想したモデルの正確性の評価にはモデルから予測される値と実際のデータの誤差で判断する。 すなわち、モデル式にx=1,2,...12を代入して得られる予想平均気温をy1,y2,...y12とすれば、

{
\displaystyle
\begin{equation}
E = \frac{1}{2} \sum_{n=0}^{12} (y_n - t_n)^{2} \tag{1.2}
\end{equation}
}

として二乗誤差は表され、これが、月々の予測値と実データの差の二乗を計算した値となっている。(1.2)式の係数w0〜w4を調整することで、それらしい曲線を得られる。(1.2)は係数w0〜w4の関数とみなせるので、誤差関数と呼ぶ。

ここまでを整理すると、

  1. 与えられたデータをもとにして、未知のデータを予測する数式を考える
  2. 数式に含まれるパラメータの良し悪しを判断する誤差関数を用意する
  3. 誤差関数を最小にするようにパラメータの値を決定する

ということになる。これを本書では機械学習モデルの3ステップと呼ぶ。

1.1.2ニューラルネットワークの必要性

あるウイルスに感染しているかしていないかというデータの分類問題を考える。検査結果は2種類の数値(x1,x2)で与えられるものとし、この2つの数値をもとにしてウイルスに感染している確率を求める。本書中の散布図データを見ると、検査結果は大きく直線で分類できそうで、

{
\displaystyle
\begin{equation}
f(x_1,x_2) = w_0 + x_1 x_1 + w_2 x_2 = 0 \tag{1.3}
\end{equation}
}

という数式で表現できそうである。平面上の直線というと、y=ax+bという形式が有名だが、ここではx1とx2を対称に扱うためにこのような形式を用いている。また、この形式の利点として、f(x1,x2)=0が境界となる他に境界から離れるほどに、f(x1,x2)の値が±∞にむかって増加していくという性質がある。 そこで、0から1に向かってなめらかに値が変化するシグモイド関数σ(x)を用意して、これにf(x1,x2)の値を代入すると、検査結果(x1,x2)から感染確率P(x1,x2)を求める関数を作ることができる。ここでシグモイド関数を使う利点としては入力する値がどんなに大きくても小さくても、0〜1の間に出力が収まることである。

{
\displaystyle
\begin{equation}
P(x_1,x_2)=\sigma(f(x_1,x_2)) \tag{1.4}
\end{equation}
}

ところで、今回与えられたデータは直線で分類できる前提条件に従っていたが、これが、折れ曲がった直線あるいは曲線を用いて分類する必要があるデータであったらどうだろう。単純にはより複雑な数式に置き換えれば良さそうだが、現実には難しい。今回は数値が2種類であったが、これが全部で20種類だったらどうだろう。20次元のグラフが必要になってしまう。

そんな中、より柔軟性が高く様々なデータに対応できる数式を考え出す努力が行われてきたが、それの一つがニューラルネットワークになる。

まず最もシンプルなニューラルネットワークを示す。

f:id:tosh419:20161008204143p:plain:w300

左から(x1,x2)という値のペアを入力すると、内部でf(x1,x2)の値が計算されて、それをシグモイド関数σ(x)で0〜1の値に変換したものが、変数zとして出力される。これはニューラルネットワークを構成する最小のユニットでノードと呼ばれる。

このノードを多層に重ねることで、より複雑なニューラルネットワークが得られる。2層のノードからなるニューラルネットワークを以下に示す。

f:id:tosh419:20161008211753p:plain:w300

このニューラルネットワークにはw10,w11,w12,w20,w21,w22,w0,w1,w2の9つのパラメータが含まれており、これらの値を調整することで、単なる直線でない、より複雑な境界線が表現できる。 原理的にはノードの数を増やしていけば、どんな複雑な境界線でも書くことが可能だ。しかしながら、パラメータの数が膨大になってそれではパラメータの最適化が終わらない。機械学習におけるニューラルネットワークの挑戦は実際に計算が可能で、かつデータの特性にあった、ニューラルネットワークを構成するという点にある。この挑戦を続ける中で、出てきた特別な形のニューラルネットワークディープラーニングだ。

1.1.3 ディープラーニングの特徴

ディープラーニングは基本的には多層ニューラルネットワークを用いた機械学習に過ぎないが、単純に層を増やして複雑化するのではなく、解くべき問題に応じて、それぞれのノードに特別な役割を与えたり、ノード間の接続を工夫したりということを行う。

例えば、先程のニューラルネットワークではそれぞれのノードは単純な1次関数とシグモイド関数の組み合わせになっていたが、CNN(畳み込みニューラルネットワーク)では1層目のノードには1次関数ではなく、畳込みフィルターなる関数を用いている。また、その後ろのプーリング層では画像の解像度を落とす処理を行う。これは画像の詳細をあえて消し去り、描かれている物体の本質的な特徴のみを抽出しようとする発想による。このような前処理を行ったデータを更に後段のノードが解析し、なんの画像なのかを判断する。 あるいはRNN(リカレントニューラルネットワーク)という例もある。一般には時系列データを取り扱うものだが、一例として、単語が並んだ文章などの自然言語処理に応用される。

f:id:tosh419:20161009212549p:plain

これはある文章が自然な文章か不自然な文章かを見分ける。This is a penという文章に対して、Thisのあとにisが来る確率、isのあとにaが来る確率などを順番に見ていき、全てが高確率になっていれば自然な文章だと判断する。この時、はじめにThisを入力して、次にisを入力する際、その前にThisを入力した際の中間層の出力値もあわせて入力する。先程の中間層にはその前にThisを入力した際の中間層の出力値も合わせて入力する。中間層には最初の単語がThisであったという情報が、isであるという情報だけよりもさらに自然な予測ができる。このように、上図の仕組みでは、過去の入力値の情報が中間層に蓄積されていくことで、より長い単語列に基づいた判断ができる、つまり過去の中間層の値を次の入力に再利用するニューラルネットワークがRNNだ。

1.1.4 TensorFlowによるパラメータの最適化

これからTensorFlowが何をしているか、を(1.2)の誤差関数Eを例に評価する。 (1.2)式に含まれるynはn月の気温を(1.1)式で予測した結果を表す。つまり、

{
\displaystyle
\begin{equation}
y_n = w_0 + w_1n +w_2n^{2}+w_3n^{3}+w_3n^{4} = \sum_{m=0}^{4} w_mn^{m} \tag{1.7}
\end{equation}
}

(1.7)式を(1.2)式に代入すると、次式が得られる。

{
\displaystyle
\begin{equation}
E(w_0,w_1,w_2,w_3,w_4) = \frac{1}{2} \sum_{n=1}^{12} \ (\sum_{m=0}^{4} w_mn^{m} - t_n)^{2} \tag{1.8}
\end{equation}
}

これで誤差関数がわかったので、(1.8)を最小にするw0〜w4を決定する。つまり、(1.8)をw0〜w4のそれぞれで偏微分した値を0とした、次の連立方程式を解くことになる。

{
\displaystyle
\begin{equation}
\frac{\partial E}{\partial w_m}(w_0,w_1,w_2,w_3,w_4)=0 \ (m=0,...,4) \tag{1.9}
\end{equation}
}

ここで、話を簡単にするために、次の2変数関数を用いて、なぜ上式の条件で、Eが最小になるかを説明する。

{
\displaystyle
\begin{equation}
h(x_1,x_2) = \frac{1}{4}(x_1^{2}+x_2^{2}) \tag{1.11}
\end{equation}
}

これの偏微分は、

{
\displaystyle
\begin{equation}
\frac{\partial h}{\partial x_1}(x_1,x_2)=\frac{1}{2}x_1,\ \frac{\partial h}{\partial x_2}(x_1,x_2)=\frac{1}{2}x_2 \tag{1.12}
\end{equation}
}

またこれらを並べたベクトルを次の記号で表して、関数h(x1,x2)の勾配ベクトルと呼ぶ。

{
\displaystyle
\begin{equation}
\nabla h(x_1,x_2)=\begin{pmatrix} \frac{1}{2}x_1\ \frac{1}{2}x_2 \end{pmatrix}
 \tag{1.13}
\end{equation}
}

勾配ベクトルはすり鉢の壁を登っていく方向に一致し、勾配ベクトルの大きさは壁を登る傾きに一致する。壁の傾きが大きいほど、勾配ベクトルも長くなる。すり鉢の壁を降りていくに従い、傾きは小さくなるので、勾配ベクトルの大きさもだんだん小さくなる。この例の場合、最終的に原点(0,0)に達したところで、h(x1,x2)は最小となるので、勾配ベクトルの大きさも0になる。つまり、h(x1,x2)を最小にする(x1,x2)は∇h(x1,x2)=0という条件で決まることになる。現在の位置をx=(x1,x2)T(注:Tは転置の意味)とすれば、新しい位置は

{
\displaystyle
\begin{equation}
X^{new} = x - \nabla h
 \tag{1.14}
\end{equation}
}

として表され、これを何度も繰り返すと、どこから出発したとしても、次第に原点に近づいていく。このように勾配ベクトルを計算して、反対方向にパラメータを修正するアルゴリズム勾配降下法と呼ぶ。気をつけならないのが、パラメータを修正する分量だ。例として以下の例に適用してみよう。

{
\displaystyle
\begin{equation}
h_1(x_1,x_2)=\frac{3}{4}(x_1^{2}+x_2^{2}) \tag{1.15}
\end{equation}
}

{
\displaystyle
\begin{equation}
h_2(x_1,x_2)=\frac{5}{4}(x_1^{2}+x_2^{2}) \tag{1.16}
\end{equation}
}

それぞれの勾配ベクトルは

{
\displaystyle
\begin{equation}
\nabla h_1 = \frac{3}{2} \begin{pmatrix} x_1\ x_2 \end{pmatrix} \tag{1.17}
\end{equation}
}

{
\displaystyle
\begin{equation}
\nabla h_1 = \frac{5}{2} \begin{pmatrix} x_1\ x_2 \end{pmatrix} \tag{1.18}
\end{equation}
}

例えば、式(1.17)を適用しながら移動すると始点を(1,-1)と仮定し、(1,-1)-3/2(1,-1)=(-1/2,1/2)→(-1/2,1/2)-3/2(-1/2,1/2)=(1/4,-1/4)→(1/4,-1/4)-3/2(1/4,-1/4)=(-1/8,1/8)などと、0に漸近していくことが分かる。一方、式(1.18)を適用していくと、始点を(1,-1)として、(1,-1)-5/2(1,-1)=(-3/2,3/2)→(-3/2,3/2)-5/2(-3/2,3/2)=(3,-3)などと、原点から離れていってしまうことが分かる。このように、うまくパラメータが原点にうまく近づくことを収束する、逆に最小値から遠ざかってしまうことを発散するという。そして、この移動量は学習率εとして表現され、式(1.14)は

{
\displaystyle
\begin{equation}
X^{new} = x - \epsilon \nabla h
 \tag{1.19}
\end{equation}
}

に変形できる。この学習率は一回の更新でパラメータをどの程度修正するかのパラメータになり、小さすぎると最小値に達するまでの時間がかかる一方、大きすぎると、パラメータが発散して最適化ができない。

なお、今回は2変数関数で考えたが、当然、変数が増えても同様の考え方が適用可能だ。式(1.8)の二乗誤差Eを最小にするパラメータw0〜w4を決定する場合であれば、これを並べたベクトルw=(w0,w1,w2,w3)Tとして、

{
\displaystyle
\begin{equation}
w^{new} = w - \epsilon \nabla E(w)
 \tag{1.20}
\end{equation}
}

{
\displaystyle
\begin{equation}
\nabla E(w) =\begin{pmatrix} \frac{\partial E}{\partial w_0} \ . \ . \ . \ \frac{\partial E}{\partial w_4} \end{pmatrix}
 \tag{1.21}
\end{equation}
}

を計算すれば良い。この時(1.20)でパラメータを更新するたびに、その点における勾配ベクトルの値を(1.21)で計算し直す事が必要だ。

1.2 環境準備

TensorFlowの環境準備をCentOS7のDockerコンテナでJupyterを起動して、外部のwebブラウザからアクセスして使用するまでの環境準備が説明されている。私の場合、MacOS(El Capitan)なので、巻末付録を参考に環境構築した。

Macの場合も、DockerコンテナでJupyterを起動して、ローカルのwebブラウザからアクセスして使用する。 ①まず以下の、Docker HPよりDocker for Macをダウンロードする。

Docker | Docker

②インストールは特に難しくないはずなので、設定へ

上部メニューバーのクジラのアイコンからPreferences→MemoryとCPUの使用率をそれぞれ4GB、4個以上にして、再起動する。

Macのターミナルより、以下を打つ。

$ mkdir $HOME/data
$ docker run -itd --name jupyter -p 8888:8888 -p 6006:6006 \
       -v $HOME/data:/root/notebook -e PASSWORD=passw0rd \
       enakai00/jupyter_tensorflow:0.9.0-cp27

実際にうってみた感じは以下のよう。

f:id:tosh419:20161009220337p:plain

-e PASSWORDオプションはブラウザからJupyterにアクセスする際の認証パスワードである。この手順でコンテナを起動した場合、Jupyterで作成したノートブックのファイルは、ユーザのホームディレクトリーの下のdataディレクトリに保存される。ブラウザからJupyterに接続する場合、http://localhost:8888にアクセスし、TensorBoardの画面のアクセスにはhttp://localhost:6006からアクセスする。

1.2.2 Jupyterの使いかた

Jupyterではノートブックと呼ばれるファイルを開いて、その中でPythonのコマンドやプログラムを対話的に実行していく。①つのノートブックは複数のセルに分かれており、1つのセルには実行するべきコマンドとその実行結果がセットで記録される。 早速、http://localhost:8888にアクセスし、ノートブックを作ってみる。

このあたりは本を買っていればサッと飛ばせる話なので省略する。

1.3 TensorFlowクイックツアー

まずは月々の平均気温を予測する問題をTensorFlowを用いて解いていく。

1.3.1 多次元配列によるモデルの実現

TensorFlowでは計算に用いるデータはすべて多次元配列として表現する。例えば、n月の予測気温ynは式(1.7)を行列を用いると、

{
\displaystyle
\begin{equation}
y_n = (n^{0},n^{1},n^{2},n^{3},n^{4}) \begin{pmatrix} w_0 \ w_1 \ w_2 \ w_3 \ w_4 \end{pmatrix}
 \tag{1.22}
\end{equation}
}

として表される。さらに12ヶ月分のデータは

{
\displaystyle
\begin{equation}
y=Xw
 \tag{1.23}
\end{equation}
}

{
\displaystyle
\begin{equation}
y_n = \begin{pmatrix} y_1 \\ y_2 \\ . \\ . \\ . \\ y_{12} \end{pmatrix},X=\begin{pmatrix} 1^{0} &1^{1} &1^{2} &1^{3} &1^{4} \\ 2^{0} &2^{1} &2^{2} &2^{3} &2^{4} \\ . \\ . \\ . \\ 2^{0} &12^{1} &12^{2} &12^{3} &12^{4} \end{pmatrix},w=\begin{pmatrix} w_0 \\ w_1 \\ w_2 \\ w_3 \\ w_4 \end{pmatrix}
 \tag{1.24}
\end{equation}
}

として表される。Xはトレーニングセットとして与えられたデータから構成される。TensorFlowではこのようなトレーニングセットを保存する変数をPlaceholderと呼ぶ。次に、wはこれから最適化を実施するパラメータでありVariableと呼ぶ。そして、yはPlaceholderとVariableから計算される値になる。 パラメータの最適化を実施するには二乗誤差を求める必要があるが、これは予測値yとトレーニングセットのデータtから計算されるものである。tは次のようにn月の平均気温を縦に並べたベクトルである。

{
\displaystyle
\begin{equation}
t=\begin{pmatrix} t_1 \ t_2 \ . \ . \ . \ t_{12} \end{pmatrix}
 \tag{1.25}
\end{equation}
}

ただし、このままでは(1.6)を行列形式にはできないので新しい演算を定義する。一般のベクトルv=(v1,v2,...,v_N)Tに対して、次の演算を定義する。

{
\displaystyle
\begin{equation}
square(v)= \begin{pmatrix} v_1^{2} \ v_2^{2} \ . \ . \ . \ v_N^{2} \end{pmatrix}
 \tag{1.26}
\end{equation}
}

{
\displaystyle
\begin{equation}
reduce\_sum(v)= \sum_{i=1}^{N} v_i
 \tag{1.27}
\end{equation}
}

squareは、ベクトルの各成分を二乗するもので、reduce_sumはベクトルの各成分の和を計算する。これらの演算を利用すると、(1.6)は

{
\displaystyle
\begin{equation}
E = \frac{1}{2}reduce\_sum(square(y-t))
 \tag{1.28}
\end{equation}
}

と表される。これで、誤差関数を行列方式、多次元配列方式で表現できた。

1.3.2 TensorFlowのコードによる表現

ここからは、実際にJupyter上でTensorFlowのコードを実行する。

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt

これはライブラリのインポートなので、問題ないと思う。

x = tf.placeholder(tf.float32, [None, 5])
w = tf.Variable(tf.zeros([5, 1]))
y = tf.matmul(x, w)

トレーニングセットのデータを保持するXを用意するが、[None, 5]と引数を指定している。これは任意の行数と5列の行列で初期化することを意味する。

次に、誤差関数を表現する。

t = tf.placeholder(tf.float32, [None, 1])
loss = tf.reduce_sum(tf.square(y-t))

tは12×1行列に相当するが、データ数の部分を任意に取れるようにするため、[None, 1]というサイズ指定を行っている。 次に、トレーニングアルゴリズムを選択する。

train_step = tf.train.AdamOptimizer().minimize(loss)

tf.train.AdamOptimizer()は与えられたトレーニングセットのデータから誤差関数を計算して、その勾配ベクトルの反対方向にパラメータを修正するものだ。学習率は自動的に調整する機能を持っている。

sess = tf.Session()
sess.run(tf.initialize_all_variables())

まずは実行環境となるセッションを用意する。 次に、トレーニングアルゴリズムの実行をセッション内で行う。

train_t = np.array([5.2, 5.7, 8.6, 14.9, 18.2, 20.4, 25.5, 26.4, 22.8, 17.5, 11.1, 6.6])
train_t = train_t.reshape([12,1])
train_x = np.zeros([12, 5])
for row, month in enumerate(range(1, 13)):
      for col, n in enumerate(range(0, 5)):
            train_x[row][col] = month**n

ここでは、tとXをarrayオブジェクトとして用意した。

i = 0
for _ in range(100000):
     i += 1
     sess.run(train_step, feed_dict={x:train_x, train_t})
     if i % 10000 == 0:
     loss_val = sess.run(loss, feed_dict={x:train_x, train_t})
     print ('Step: %d, Loss: %f' % (i, loss_val))

ここでは、勾配降下法によるパラメータの最適化を実施している。

for _ in range(100000):
     i += 1
     sess.run(train_step, feed_dict={x:train_x, t:train_t})
     if i % 10000 == 0:
        loss_val = sess.run(loss, feed_dict={x:train_x, t:train_t})
        print ('Step: %d, Loss: %f' % (i, loss_val))

更に100000回だけトレーニングを実施している。すると、Lossがあるところから増加に転じる。そこでトレーニングを打ち切って、その時点でのパラメータの値を確認する。

w_val = sess.run(w)
print w_val

続いて、この結果を用いて、予測気温を表す関数を定義する。

def predict(x):
       result = 0.0
       for n in range(0, 5):
             result += w_val[n][0] * x**n
       return result

fig = plt.figure()
subplot = fig.add_subplot(1,1,1)
subplot.set_xlim(1,12)
subplot.scatter(range(1,13), train_t)
linex = np.linspace(1,12,100)
liney = predict(linex)
subplot.plot(linex, liney)

上のコードには関数をプロットするものも入っており、これを実行すると、以下のグラフが得られる。

f:id:tosh419:20161010152546p:plain

ここまでで、第1章は終わりになる。