はじめに
TensorFlowを本格的に活用し始めて感じていることがいくつかあります。
- モジュールが異様に豊富で、どれを使えばいいか分からない
- プログラミングの仕方が独特
などが挙げられます(他にもたくさんありますが)。これらを考慮して、TensorFlowを使う際の自分なりの心構えと、初学者が入門するためのチュートリアル的な記事を書こうと思いました。内容が多くなることが想定されるので、何回かに分けて書いていきます。
TensorFlowの書き方、考え方を抑え今回は順伝播までを正確に記述できるようにしていきます。
したがってほぼ初めて、あるいはサンプルコードしか触ってない人が、自分である程度ネットワークを構成できるようになるための初歩に相当します。
数式を交えて書いていきますが、決して難しい内容ではありません(むしろ数式の方がシンプルで、それを如何にしてコードに落とすかの方が難しい)。
TensorFlowを理解する
計算グラフに対する理解
計算グラフ計算グラフ言いますが、計算グラフを理解しなければならないのではなく、むしろ順伝播や逆伝播を理解する便利なツールとして計算グラフがあります。
計算グラフで考えると、難しい数式を追わなくても誤差逆伝搬法が理解できるようになります。だから計算グラフは嬉しいと言っても、あくまでツールであって目的ではありません。
計算グラフなんてもの勉強したことがないよ、という人は無理にこれを勉強する必要はありません。
誤差逆伝搬法が数式ベースで分かるならそれで十分ですし、それが難しそうなら計算グラフで理解をしてみようかな、と考えればいいだけのことです。そして使う側にとっては、誤差逆伝搬法は要するに勾配法のために微分を効率よく計算するものなんだという程度の理解でもとりあえずは構わないでしょう。
実践的なTensorFlowの理解
TensorFlowはあらゆる処理をすべて計算グラフで表現します。計算はもちろん文字の出力なども計算グラフで実行します(fileの保存すら!)。もしもTensorFlowの開発に参加したければ深い理解が必要ですが、そうでもなければ以下のように理解してしまうほうが良いでしょう。
TensorFlowは
「写像を定義し、写像をメモリに載せて、写像する」
という処理の手順を持っています(実際のところ、ここで言う「写像」がまさしく計算グラフのことになってくるわけですが)。
写像とは?
難しく考える必要はありません。
写像というのは、引数
を
に変える変換装置です。
って書いたりします、
これを意識することにどんな意味があるのかというと、例えば普通の(手続き型)プログラミングではを引数として、以下のコードが呼び出された場合、
に
を足すという処理が直ちに行われ、
は
という値だと考えます。しかし、TensorFlowでこのような式を書いた場合には
という写像を定義したことになります。ここでは計算は一切行っていません。ここでの「+」は具体的に「足す」という手続き実行しているのではなく、単に「和である」と宣言しているだけです。そういう装置を置いただけなのです。
例えば数学でも
って書いたり
って書いたりしますが、TensorFlowは上の方に合致する考えです。下は、いかにも具体的にという値があるように見えます。TensorFlowは「後から
を入れるからとりあえずお前はこういう定義の装置だ」と宣言しているだけなのです。
写像をTensorFlowのコードと合わせて見る
プレースホルダーという概念
以下の写像
にとって、という文字に意味はありません。後から与える仮の姿です。だから別に「□」でも「・」でも良いんです。数学の本でも「
」って見たことないですか。「とりあえず何かが入る場所」を表すのに使い、通常「プレースホルダー」と呼びます。
TensorFlowでもこのアイデアを採用します。
上記の写像を定義する場合、
x = tf.placeholder(tf.float32)
f = x + 1
と書きます。はプレースホルダーです。具体的な値ではありません。最初TensorFlowに触れた人は、なんだかこれが奇妙に見えたかもしれません。しかし、この書き方はすごく数式を書く上で真っ当なやり方にも感じられるくらいです。
更に、引数を2つ取る写像
を書きかたれば簡単です。プレースホルダーを2つ使って、
x1 = tf.placeholder(tf.float32)
x2 = tf.placeholder(tf.float32)
f = x1 - 2 * x2
とするだけです。
写像をメモリに乗せる
今まで話してきたのは、写像の宣言に関することであり、具体的に計算は行われていません(プレースホルダーに値が入っていないのだから当然)。
宣言した写像をメモリに乗せる必要があります。このようにして予めメモリに、これから使う写像を載せてしまうことで、何度も何度も写像を呼び出す必要が無くなります(故に、一度作った物はその後変更ができない)。
実は、TensorFlowのネイティブな処理はC++で実装されており高速に動作します。メモリに乗せるというのは仮の表現で、既にインストールしてあるC++との接続を行います(既にメモリに載っている必要な処理とヒモ付をするわけです)。
そのためのコードは以下です。
sess = tf.Session()
この一行だけで、今まで準備した写像が、メモリに設置されました。あとはプレースホルダーに具体的な値を与えてあげるだけです。これを行って初めてTensorFlowの世界に移行したことになります。実はそれ以前はPythonの世界にいるのであって、今までやってきたのはPythonの世界でTensorFlowの世界で使う写像を考えていたということです。
メモリに載せた写像のプレースホルダーに値を与える
ここまでで、
x1 = tf.placeholder(tf.float32)
x2 = tf.placeholder(tf.float32)
f = x1 - 2 * x2
と写像を定義し、
sess = tf.Session()
によって写像をTensorFlowの世界に置くことができました。あとは、そこに具体的なの値を入れてやるだけです。これを実行するときは完全にTensorFlowの世界のルールに従う必要があり、今まではTensorFlowの世界のルールに従ってPythonで記述してきかというわけです。
「sess」はTensorFlowの世界の写像を全て保持しており、最も基本的なメソッドかつ、覚えておかなければならないのは「run」になります。
仮に以下の処理をTensorFlowに渡したければ
sess.run(f, {x1:
8, x2: 3}))
と書くことになります。sess.runの引数は、ひとつ目が写像で、2つ目がプレースホルダーに入れる値です。プレースホルダーに与える値は辞書型で渡します。
ニューラルネットのアフィン層設計
これまで見てきたとおり、TensorFlowは独特のプログラミングパラダイムを持っています。これを念頭においてニューラルネットワークを記述していきましょう。
数式おさらい
まずはニューラルネットワークの最も基本的な層であるアフィン層を作ります。アフィン層はプレースホルダー(後の入力)を
として
と表記できます。ここに行列の方はP行Q列で、ベクトル
の方がP次元のベクトル(あるいはP行1列の行列と言ってもいい)です。これを
、
などと表記します。これは
を受け取り、
のベクトルを返します。
これが一般的な数式による表現ですが、プログラミングでは多くの場合以下の表記を使います。
上記の右肩にあるは行と列を入れ替える転置と呼ばれる操作を表す記号です。
従って上の式は、これまでの話に置いて全て行と列を入れ替えるだけであり、、
となります。
これは数学の方を基準に書いているだけであり、というのはいつでも横ベクトルなんですと決めてしまえば、
と書くことができます。
実はもっと効率の良い計算方法があります。ニューラルネットに値を計算させるときに上記のようにベクトルを1つ1つ計算させるのではなく、複数のベクトル
を同時に計算させることが可能です。
個のベクトル
を計算させる数式上の表現は
となります。これで一度の計算でたくさんのデータを処理できるようになりました。GPUは元々画像処理のために作られたもので、配列の計算に特化したアーキテクチャを持っています。ニューラルネットの処理を、(メモリを食う代わりに)大きな配列の計算として定義してしまうことでGPUによる高速化が可能になるのです(従って、データを1個ずつ与えるような実装をすればその恩恵は小さくなってしまいます)。
コードで見る
これまでに考えてきた写像は以下のとおりでした。
さて、これをTensorFlowの(擬似)コードで書きましょう。
X = tf.placeholder(dtype = tf.float32, shape = [None, Q])
W = weight(dtype = tf.float32, shape = [Q, P])
b = bias(dtype = tf.float32, shape = [P])
f = tf.matmul(x, W) + b
と書くことができます。数式での表現とほとんど変わりません。
まず、のshapeに注意が必要です。[None, Q]となっています。これはQ次元の横ベクトルを幾つか並べた行列を入れますと宣言しているのです。
Noneの部分は本来ならばデータの数を指定することになりますが、どれくらいのデータを一度に入れるのかは実は学習にある程度影響を及ぼします。後で色々調整して試したいということになるので、Noneでプレースホルダーが任意の数を受け入れるようになっているのです。
次に、をプレースホルダーにとして、ここは具体的な数値は後から入れると言っているわけですが、他の部分は
は予め具体的に決めておいてあげなければなりません。そこでPythonで関数を書く必要が出てくるわけです(別にシコシコ手で書いてもいいですが)。
例えばニューラルネットのアフィン層を作るときは、具体的には以下のようにします。
def weight(shape = []):
initial = tf.truncated_normal(shape, stddev = 0.01)
return tf.Variable(initial)
def bias(dtype = tf.float32, shape = []):
initial = tf.zeros(shape, dtype = dtype)
return tf.Variable(initial)
X = tf.placeholder(dtype = tf.float32, shape = [None, Q])
W = weight(shape = [Q, P])
b = bias(shape = [P])
f = tf.matmul(X, W) + b
weightの方は、行列の要素を特殊な正規分布から発生する値で埋めます。別に普通の正規分布を使っても勿論構いません。これはもともとtf.float32で返ってくるので、dtypeの指定を必要としなくなります。biasの方は通常0で埋めておきます。
今はshapeがP,Qなどの文字で置かれていますが、このコードはPythonの世界の話であり、実際には具体的に値を与えなければいけないことは勿論注意しておいてください。例えば以下のようにします。
def weight(shape = []):
initial = tf.truncated_normal(shape, stddev = 0.01)
return tf.Variable(initial)
def bias(dtype = tf.float32, shape = []):
initial = tf.zeros(shape, dtype = dtype)
return tf.Variable(initial)
Q = 5
P = 2
X = tf.placeholder(dtype = tf.float32, shape = [None, Q])
W = weight(shape = [Q, P])
b = bias(shape = [P])
f = tf.matmul(X, W) + b
これで5次元の横ベクトルを2次元の横ベクトルに変換する写像をTensorFlowに渡す準備ができたということです。実際にこれをTensorFlowに渡すためには
を行うことになります。
ニューラルネットの設計
2層のニューラルネットワーク
さて、1つのアフィン層を実装する手立てを手に入れました。実際のニューラルネットワークは多層構造になっており、これを幾つも準備してあげる必要があります。今回は簡単のため2層のニューラルネットを書いてみましょう。
ここまで理解していれば何も難しいことはありません。
def weight(shape = []):
initial = tf.truncated_normal(shape, stddev = 0.01)
return tf.Variable(initial)
def bias(dtype = tf.float32, shape = []):
initial = tf.zeros(shape, dtype = dtype)
return tf.Variable(initial)
Q = 5
P = 2
R = 1
X = tf.placeholder(dtype = tf.float32, shape = [None, Q])
W1 = weight(shape = [Q, P])
b1 = bias(shape = [P])
f1 = tf.matmul(X, W1) + b1
W2 = weight(shape = [P, R])
b2 = bias(shape = [R])
f2 = tf.matmul(f1, W2) + b2
たったこれだけで、2層のニューラルネットが定義できました。このケースでは層を進む毎にとデータの次元が減っていっています。
例えば5つ指標を元に、1つの銘柄の株価を出力したい場合などにこういう形を取ることになるでしょう。5つの指標を元に、3つの株価を出力したいならばなどとすれば良いわけです。あるいは3クラス分類の問題で、5つの指標から各クラスに属する確率をそれぞれ出力した場合にも同じような設計になります。
中間層におけるユニットの数に相当するは好きなように設定するしかありません。仮に5つの指標から本質的に2つの因子があると思ったならば上記のように書くことになりますし、なんだかよくわからんから増やしとけというのも1つの戦略です。
活性化関数
本来のニューラルネットワークは活性化関数なるものが層と層の受け渡しの間に入り込んでいます。活性化関数には色々な物があります。
今回は、基礎となっているシグモイド関数を使ってみましょう。シグモイド関数は
という式になります(話の一貫性を保つため、写像の形式で書きます)。これをPythonで定義してあげて、中間層の活性化として使うために以下のように書き換えましょう。
def weight(shape = []):
initial = tf.truncated_normal(shape, stddev = 0.01)
return tf.Variable(initial)
def bias(dtype = tf.float32, shape = []):
initial = tf.zeros(shape, dtype = dtype)
return tf.Variable(initial)
def sigmoid(x):
return (1 /(1 + tf.exp(-x)))
Q = 5
P = 2
R = 1
X = tf.placeholder(dtype = tf.float32, shape = [None, Q])
W1 = weight(shape = [Q, P])
b1 = bias(shape = [P])
f1 = tf.matmul(X, W1) + b1
sigm = sigmoid(f1)
W2 = weight(shape = [P, R])
b2 = bias(shape = [R])
f2 = tf.matmul(sigm, W2) + b2
ここでの「def sigmoid(x)」はあくまで、「TensorFlowの計算に使うための写像を作る関数」に相当します。いつでもTensorFlowに写像を乗せる準備をしているということを忘れてはなりません。
そして、写像は写像
を引数に取っています。写像を写像しているというわけです。これを合成写像と言い
などと表現します。ここで
なので、最終的にはからすれば
という写像が行われるということです。
TensorFlow APIの活性化関数
実際には既に基本的な活性化関数はAPIとしてかなり準備されているので、特殊なことをしない限り、ほとんど自分で作る機会は無いでしょう(逆に自分で書けば何でも作れる!)。シグモイド関数も当然準備されているので、
W1 = weight(shape = [Q, P])
b1 = bias(shape = [P])
f1 = tf.matmul(X, W1) + b1
sigm = tf.sigmoid(f1)
と書くことで同じことができます。どのような活性化関数が既に準備されているのかは
Activation Functions. | TensorFlow
で確認できます。
ニューラルネットの順伝播
これまでのコードに関して、写像をTensorFlowに渡し、順伝播の準備を完了するためには以下のコードを書く必要があります。
def weight(shape = []):
initial = tf.truncated_normal(shape, stddev = 0.01)
return tf.Variable(initial)
def bias(dtype = tf.float32, shape = []):
initial = tf.zeros(shape, dtype = dtype)
return tf.Variable(initial)
def sigmoid(x):
return (1 /(1 + tf.exp(-x)))
Q = 5
P = 2
R = 1
X = tf.placeholder(dtype = tf.float32, shape = [None, Q])
W1 = weight(shape = [Q, P])
b1 = bias(shape = [P])
f1 = tf.matmul(X, W1) + b1
sigm = sigmoid(f1)
W2 = weight(shape = [P, R])
b2 = bias(shape = [R])
f2 = tf.matmul(sigm, W2) + b2
sess = tf.Session()
init_op = tf.global_variables_initializer()
sess.run(init_op)
特に青い字に注意してください。「sess = tf.Sesstion()」によって今まで作ったPython
上で作った写像をTensorFlowに渡すことができるというのは説明してきました。青い字の部分は、TensorFlow上に準備された写像を初期化することに相当します。これはTensorFlowに渡した写像の中にtf.Variableの型がある限り、必ず行わなければなりません。
もしもVariableの型が無ければ必要ありません。
これらを踏まえて順伝播を実際に行う全体のコードは以下です。
def weight(shape = []):
initial = tf.truncated_normal(shape, stddev = 0.01)
return tf.Variable(initial)
def bias(dtype = tf.float32, shape = []):
initial = tf.zeros(shape, dtype = dtype)
return tf.Variable(initial)
def sigmoid(x):
return (1 /(1 + tf.exp(-x)))
Q = 5
P = 2
R = 1
X = tf.placeholder(dtype = tf.float32, shape = [None, Q])
W1 = weight(shape = [Q, P])
b1 = bias(shape = [P])
f1 = tf.matmul(X, W1) + b1
sigm = sigmoid(f1)
W2 = weight(shape = [P, R])
b2 = bias(shape = [R])
f2 = tf.matmul(sigm, W2) + b2
sess = tf.Session()
init_op = tf.global_variables_initializer()
sess.run(init_op)
y = sess.run(f2, {X: 入れたいデータ }) #出力が欲しい場合
h = sess.run(sigm, {X: 入れたいデータ}) #隠れ層の値が欲しい場合
sess.run()の1つ目の引数は写像です。従って、実行したい写像を指定してあげれば、その値を好きなように取り出すことができます。
手続き型のイメージだとなんとなくを計算してから、その結果を
に渡して、その結果を
に渡さ無ければいけない感じもするかもしれませんが、TensorFlowは合成写像として、それぞれの写像をすでに認識しているため、引数として与えるのはプレースホルダーである
だけでいいのです。
TensorFlowの実際
tf.InteractiveSession()
これまで、TensorFlowの中に写像を作ってあげるために、Pythonでそれらを記述してきました。そして最後に「sess = tf.Session()」によって写像を渡してあげていました。
今まで作り上げてきた写像たちは、まるまる一気にTensorFlowで合成写像の形として認識されます。しかし、今回作った簡単なネットワークならまだしも、もっと大規模な合成写像(ディープなネットワーク)を構築したいとなった時に問題が生じてきます。
写像の定義に対して人為的なミスが生じる可能性があるのです。例えば中間層から中間層へのデータの受け渡しの際、データのサイズが異なってて上手くいかないということが起こるかもしれません。あるいはプログラム上は大丈夫でも、自分の意図していない構造になっているかもしれません。
そこで「tf.InteractiveSession()」が有効に使えます。「sess = tf.Session」では写像を全て作り終えてからしか渡しませんでしたが、「tf.InteractiveSession」では、Pythonの世界で定義した写像をTensorFlowの世界に順次追加していくことができます。
つまりこれを使えば今までのコードは
def weight(shape = []):
initial = tf.truncated_normal(shape, stddev = 0.01)
return tf.Variable(initial)
def bias(dtype = tf.float32, shape = []):
initial = tf.zeros(shape, dtype = dtype)
return tf.Variable(initial)
def sigmoid(x):
return (1 /(1 + tf.exp(-x)))
Q = 5
P = 2
R = 1
sess = tf.InteractiveSession()
X = tf.placeholder(dtype = tf.float32, shape = [None, Q])
W1 = weight(shape = [Q, P])
b1 = bias(shape = [P])
f1 = tf.matmul(X, W1) + b1
sigm = sigmoid(f1)
W2 = weight(shape = [P, R])
b2 = bias(shape = [R])
f2 = tf.matmul(sigm, W2) + b2
init_op = tf.global_variables_initializer()
sess.run(init_op)
y = sess.run(f2, {X: 入れたいデータ }) #出力が欲しい場合
h = sess.run(sigm, {X: 入れたいデータ}) #隠れ層の値が欲しい場合
とすることができるのです。もっと中途半端な位置にあっても構いませんが、とりあえず先にTensorFlowの世界を開いておいてもいいということです。
こいつのお陰でニューラルネットを随時正しく伝搬できるか確認しながら構築していくことができます。当面はそのような大規模なものは作らないと思われますが、TensorFlowは研究ではなく実用の用途でも使えるように意識されており(Googleでは開発にも使われている)、このような便利な機能も備えているというわけです。
順伝播を試してみる
プレースホルダーに値を入れてみて、出力値を確認しましょう。
def weight(shape = []):
initial = tf.truncated_normal(shape, stddev = 0.01)
return tf.Variable(initial)
def bias(dtype = tf.float32, shape = []):
initial = tf.zeros(shape, dtype = dtype)
return tf.Variable(initial)
def sigmoid(x):
return (1 /(1 + tf.exp(-x)))
Q = 5
P = 2
R = 1
sess = tf.InteractiveSession()
X = tf.placeholder(dtype = tf.float32, shape = [None, Q])
W1 = weight(shape = [Q, P])
b1 = bias(shape = [P])
f1 = tf.matmul(X, W1) + b1
sigm = sigmoid(f1)
W2 = weight(shape = [P, R])
b2 = bias(shape = [R])
f2 = tf.matmul(sigm, W2) + b2
init_op = tf.global_variables_initializer()
sess.run(init_op)
y = sess.run(f2, {X: [1,2,2,5,2] })
実は上記のコードはエラーを吐きます。赤字の入力がリストになっているためです。TensorFlowの世界にプレースホルダーを通して値を渡すときは、「np.float32」あるいは「np.float64」のいずれかに統一されていなければなりません。
従って以下に変更します。
y = sess.run(f2, {X: np.array([1,2,2,5,2]).astype(np.float32) })
実はこれでもエラーを吐いてきます。
実はplaceholderの型に注目してください。
X = tf.placeholder(dtype = tf.float32, shape = [None, Q])
行数はNone(任意のデータ数)であり、列数がQ(データの次元:今回の場合5)の2次元配列です。今numpyの配列は単なる1次元配列になっているので以下のように明示的に、2次元配列にしてあげなければなりません。
y = sess.run(f2, {X: np.array([1,2,2,5,2]).astype(np.float32).reshape(1,5) })
numpyの配列は通常、何も宣言しなければnp.float32になるので、実はこちらは明示しなくてもいいですが、今後、もっと複雑なデータ型を渡すようになると、必ず一度は経験するエラーになるので、予め意識しておきましょう。
また、sess.runから出てくる返り値はnumpyの配列になっています。「sess.run」はいつでもPythonの世界とTensorFlowの世界の架け橋です。
最後に
今回は順伝播までを見てきました。
ニューラルネットの本当の始まりは学習にあります。次回からは学習の方を取り扱いますが、TensorFlowをマスターするための肝は順伝播を正しく記述する方です。
従ってここまでをキッチリ理解できているのとそうでないのとでは全く違います。学習の方は、実を言うとPythonの世界であれこれやる必要がほとんどありません。TensorFlowの学習の仕方というのはある程度決まっているため、ほとんどがモジュール化されており、私達はそれを呼び出す処理を書くだけになります(つまり、各手法に対してしっかり理解することが重要であって、Python上で何か特別に書く必要はあまりない)。
従って私達がやるのは、狙い通りの写像を構築することであって、そちらが実際のコーディングでもメインになるのです。
そういうわけで、次回からは学習について取り扱いますが、特に学習手法に関する理解をした上で、TensorFlowを使いこなすことを狙っていきます。
今回の記事に関する補足
実は「写像」、「関数」という言葉は数学的にはいずれも全く同じ意味であって区別の必要はありません。ただし、そうした場合「Pythonで(プログラミング言語の意味で)関数を定義してTensorFlow上で動く(数学の意味での)関数を定義していく」などという表現になってしまい、ややこしく感じたため避けました。
また、「作用素」と「写像」、「関数」も同じ意味であり、作用素のことを英語で「Operator」と言います。TensorFlowのDocumentでは基本的にOperatorという言葉が利用されています。
計算グラフというのは写像をグラフィカルに扱う体系であって、結局動作は一緒です。数式ベースの写像をグラフィカルに見ようとしたり、あるいはグラフ構造を数式ベースで考えたかったり、数学では色々な場面があります。
一方では難しく見えることが他方では簡単に視えたりするのです。その例が誤差逆伝搬法であり、計算グラフで見たほうが容易です。しかし、一度分かったら、基本的に数式ベースの方がプログラミングとの親和性も高く、頭の省エネになります。そういうわけで計算グラフを意識することは今回は避けました。
ニューラルネットを理解するための関連記事
以下の書籍は、ニューラルネットをnumpyで実装していく本であり、コードベースでニューラルネットの処理を理解していくことができます。恐らく現状、理解を促す上で最も良い本です。ただ、既に数式ベースで理解できている人は、単純にnumpyの練習になるくらいであり、実用的なネットワークはフレームワークを利用することになるので不要だと思われます。
以下の分類を行うニューラルネットやSVMが、どのようにして分類を達成しうるのか、その共通した考えについて述べています。
機械学習で現れる数式について最低限分かるレベルになってほしいものをまとめています。「かなり深くまで学ぶ」というよりは、本当にこれだけは知っておいて欲しいというものについてあっさり触れています。
続きの記事
次は設計したニューラルネットワークを学習させる方法を書いていきます。
これは今まで同様、学習を行う写像を定義していけばよいのですが、学習の写像はバックプロパゲーションとして知られており、非常に難易度の高い数式となっています。順伝播の設計は問題に応じて色々変えるため、自身で設計できる必要がありますが、
学習の写像に関してはニューラルネットは共通の方法を取るため、TensorFlowで与えられたAPIを用いて記述していきます。