DeepLearningは最近ブームであり,その有名なライブラリとしてTensorflowがあります.
この記事ではDeepLearningの基本的な部分を数式を使って書き下すこととTensorflowの使い方を紹介します.

今更っていう気もしますが…,そこは気にしないでおくことにします

主な対象はベクトル空間やテンソル積等をある程度知っているけれど,DeepLearningは知らない人です.
なので表記も大学の数学でよく出てくるものしています.

なおニューラルネットワークの積分表現には触れません.

三層パーセプトロン

ニューラルネットワークの基本的な形の一つである三層パーセプトロンを定義します.

定義 (三層パーセプトロン)

行列W1Mn0n1(R),W2Mn1n2(R)とベクトルb1Rn1,b2Rn2σ,τ:RRが存在し,

F=τ¯fW2,b2σ¯fW1,b1

と書ける時,関数F:Rn0Rn2三層パーセプトロン という.
ただし,fWi,bi:Rni1RnixWix+biで定める写像であり,σ¯,τ¯はそれぞれ成分毎にσ,τで写した写像とする.

三層パーセプトロンはよく下の図のように描かれます.
この図の◯一つがRで→が◯と◯の間の写像です.

この画像は人工知能であそぶで公開されていたものを使わせていただきました.

数学に慣れがあれば、多層に拡張するのは簡単だと思うので,一般的な定義は読者の演習問題にします.
なお,ここでは三層パーセプトロンを層の数を一般化したものとCNNとRNNをニューラルネットワークと呼ぶこととします.

問題設定

三層パーセプトロンを定義したところで,機械学習の数学的な問題設定を述べておきます.

問題

ある関数f:RnRmに対し,
有限集合ARn上の関数の値,すなわち{(a,f(a))aA}を用いてfが求められるか?

上の問題が想定しているのは,fは具体的にどんな関数かはわかっていないが,その一部分のデータAからfあるいは人間の体感的にはfと思ってもよいだろうという関数を求めたいというものです.

例えば,画像から犬と猫を分類したいとします.
この時,(誰も書いたことがない画像が存在するので)全ての犬画像と猫画像は当然得られないのですが、適当に何枚か犬と猫の画像データを使って,可能な限り精度の高い犬か猫かを判定する判定機を作れるかという問題になります.

余談

定義域や値域がRの直和に制限する必要はないでしょう.その(ユークリッド位相での)コンパクト部分集合で十分な気もしますし,そもそもQの直和の方が自然な気もします.

三層パーセプトロンを含むニューラルネットワークでは上の問題の解答をニューラルネットワークを使って求めようとします.

つまり与えられたデータから,もともとのfに"近い"ニューラルネットワークを求める問題となります.
そうするとそもそもの疑問として以下が思い浮かびます.

疑問

ニューラルネットワークには元々知りたいfに近いものが存在するのか?

この問いに対して,定義域がコンパクトな場合はよい答えが知られています.

定理(普遍性定理)

定義域がコンパクトな連続関数全体の空間上,σ,τにシグモイド関数を取った三層パーセプトロン全体のなす集合はsupノルムが定める位相に対して稠密である

例えばRIMSの講究録に詳しい記述があります.

ニューラルネットワークの学習アルゴリズム

実際に関数fに近いニューラルネットを求めるアルゴリズムを説明します.
三層パーセプトロンでいうと,W1,W2,b1,b2を定めるアルゴリズムになります.

ニューラルネットワークを定めるアルゴリズムに従い計算する行為を学習といいます.

機械学習では,データの特徴を学習して関数を作るという意識があるので,学習という名前がついています.

アルゴリズム

一意にアルゴリズムが定まっているわけではありませんが、ニューラルネットワークの学習アルゴリズムはおおよそ、以下のように行われます.

  1. ニューラルネットワークの構造を決定
  2. ニューラルネットワークを構成する行列等に初期値を与える
  3. ニューラルネットワークに入力データを代入して結果を得る
  4. 計算結果と正解との誤差を求める
  5. 誤差を減らすように計算グラフの値を変更
  6. 関数がいい感じになるまで,3.~5.を繰り返す

これらの処理を具体的に見ていきます.

ニューラルネットワークの構造を決定

三層パーセプトロンの場合で言う, n0,n1,n2およびσ,τを何にするか決める行為です.

これらはよりよい結果が得られるように定めたいわけですが,どうすればよりよくなるのかは非常に難しく,

層の数を単に増やせばいいのか,あるいはn1を増やすべきなのか,はたまた,CNN等の別の構造を考えるべきなのか…

データをもとにヒューリスティックに試行錯誤するしかないのかなあと思っています.

ニューラルネットワークを構成する行列等に初期値を与える

初期値が何にするべきかは結構難しい問題で,何も考えない時は適当な乱数で定めます.

工夫をする時はその問題の特徴を見て決めるのか,あるいは転移学習のように別で学習したデータを適用するという方法もあります.

ニューラルネットワークに入力データを代入して結果を得る

ある関数が定義されているので,それを使って入力データに対する出力を計算します.
コンピュータ上で計算を高速化する場合は
行列の計算をうまく並列にする等の工夫がいります.

計算結果と正解との誤差を求める

誤差を計算するにはその前に誤差関数を定義する必要があります.
わかりやすいのは二乗平均誤差です.
求めたい関数をf:RmRn,ニューラルネットワークをfとします.
有限集合ARmでのデータを使ってよい場合,

xA|f(x)f(x)|2

で定めます.

ここにはいろいろな方法があって例えばAを全て使うのではなく適当にAの部分集合だけで計算する場合もあります.また,出力を確率的に扱うとうまくいく場合が多いので,確率同士の誤差の場合はlogを使う場合もあります.ロジットはここでは本質ではないので詳しく説明しませんが,ここここ等に記載されています.

誤差を減らすように行列やベクトルの値を変更

誤差が少なくなるように行列やベクトルの値を変更します.
基本的な発想はGradient Descnetです.

つまり,W,bの成分で方向微分を計算し,誤差が減る方向にW,bの値をずらすという方法です.

ただ,毎回全てのデータに対して計算をすると,計算時間が足りなくなる場合があるので,ランダムにAの部分集合Bをとって誤差の計算をします.これをstocastic gradient descentと言います.

W,bを減らす方法は他にもAdam等多数あるのですが,一つ一つの具体例を書くのもおかしいのでここでは割愛します.

DeepLearningの教科書ではこの計算を求めるアルゴリズムとしてbackpropagationが強調されています.

backpropagationは微分をW,bを全て計算するために層の個数nが大きい方,つまり出力の近い側から計算しようというものです.
どの順番で計算しようと計算結果は変わりませんが,計算時間は大きく変わります.

関数がいい感じになるまで,3.~5.を繰り返す

何をもっていい感じかは非常に難しいのですが,適当に回数を繰り返すか等で終わりにしている事が多いです.

評価

ここまでアルゴリズムを説明しましたが,我々が知りたいのはA以外の部分の定義域でfに近いニューラルネットワークが得られたかです.
上のアルゴリズムはA内に閉じています.なので,学習が終わった後,A以外の有限集合C上で誤差を評価します.

Tensorflow

DeepLearningの基本的な部分を説明しました.
ここからは,Tensorflowの説明をします.
とりあえず学習をかけられることを目指します.

Tensorflowはニューラルネットワークの計算をするプログラムなので,上で書いた1.~6.を実施しています.

典型的となる部分を裏で自動的に計算する仕組みを持っており,おかげで人間がソースコードを書く量を減らしてくれてます.

具体的にいうと5.はニューラルネットワークの構成と誤差を表す関数(損失関数)と入力さえわかれば,計算できるので、プログラムで自動で計算してくれます.

また,計算する毎にW,bの値が変わっていくので,5.を実現するために,以前の計算結果を覚えています.
計算を覚えておくためにTensorflowではSessionという機能があります.

Tensorflowで使われるクラス

Tensorflowで実際に使われるクラスを説明します.

その前にプログラムをあまり知らない人向けなので,クラスを雰囲気だけ説明します.
数学に例えると,クラスとは、構造の一種です.
ものの集まりとその集まりが持つ構造(定数や関数)を定めたものです.
例えば,開集合の公理をイメージするのが自然かもしれません.

プログラムではクラスをふんだんに活用します.
なぜ使うかは例えばここここ
を参照してみてください.

Variable

W,b等が使われるクラスです.このクラスの元は単に計算できるだけでなく,以下の特徴があります.

  • ランダムに初期値な設定をしやすくしてある.
  • backpropagationで値を変更できる.

placeholder

関数の入力と出力等に使われるクラス.
実際の際に人間が設定して,動的に値を設定できるものです.
例えば二次元の配列を与えてforwardpropagation→backpropagation.
その後別のデータで同じ処理をする.

余談

本来はVariableやplaceholderクラスの定義をしないといけないと思うのですが,複雑なこともあって、いつ使うかと何がうれしいかだけを書きました.
気になる場合はTensorflowの公式サイトやソースをみてください.

Session

Sessionは計算結果を覚えておくクラスです.

tensorflowの処理

Tensorflowで学習する際、プログラムが実施する処理は以下になります.

  1. データの取得
  2. Sessionの作成
  3. 計算グラフの作成
  4. Variableの初期化
  5. 計算の実行
  6. Sessionの終了

この処理の順に何をするか記載します.

データの取得

fileやDBからデータを取得します.
また、とても有名なデータセットに対しては読み込みするメソッドが用意されています.

例えば,mnistの場合だと以下のようにすればデータセットを取得できます.

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)

Sessionの作成

sess = tf.Session() #Sessionの作成

計算グラフの作成

プログラムの処理として計算グラフの作成自体が厳密にこのタイミングで実行されているのかわかりませんが,
ソースとしては次に書くことになります.

今まで計算グラフという言葉を使っていませんでしたが、
計算グラフは演算の処理をグラフとして定義したものです.
詳細はここ等を参考にしてください.

ソースコードとしては計算を記載するだけです.
ソースのコメントにどういう処理かを記載します.

初めて見たときに僕が難しかったのは入力のところです.
関数を定義するのですが、普通、関数fだとxmapstof(x)を定めます.
ですがDeepLearningでは複数のデータをまとめて計算した方が都合がいいので,
x1,,xnf(x1),,f(xn)を定めています.
また、このnは計算を代入する時に定めた方が都合がいいことが多いです.
そのため,計算グラフを作成する場合はnに依存しないNoneと記載して動くようになっています.

# 定義域の元を定義
x = tf.placeholder(tf.float32, [None, 784])
# 行列の定義
W = tf.Variable(tf.zeros([784, 10]))
# ベクトルの定義
b = tf.Variable(tf.zeros([10]))
# sigmaとしてsoftmax関数を設定し、一つの層として計算
y = tf.nn.softmax(tf.matmul(x, W) + b)
# xの正解の関数fの値を定義
y_ = tf.placeholder(tf.float32, [None, 10])
# 誤差を定義
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y), reduction_indices=[1]))
# 誤差を最小化するようにbackpropagationwを実施
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)

損失関数は具体的に定義しましたが,いくつかの典型的なパターンについてはtf.losses配下のメソッドになっています.
例えば以下があります.

  • tf.losses.softmax_cross_entropy()
  • tf.losses.mean_squared_error()

Variableの初期化

sess.run(tf.global_variables_initializer()) # Varibleの初期化

計算の実行

for _ in range(1000):
  batch_xs, batch_ys = mnist.train.next_batch(100)
  sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

Sessionの終了

sess.close() #SessionのClose

注意

これで計算自体はできているのですが,実際にW,bがどういう値になったかや,汎化誤差がどのようになったのかがわかりません.
そのためには別のデータに対して評価します.

変数の値の確認

Tensorflowの計算はSession中に値がかわっていくものなので,計算結果を知りたい場合は途中で知りたくなります.
例えばyの値がSessionをCloseする前に以下を実施すればみれます.

y_output = y.eval(sess, feed_dict={x: batch_xs, y_: batch_ys})
print(y_output)

微分の実装は浮動小数点の評価等も含め面倒なイメージがあるのですが,Tensorflowを使えばこの計算は一瞬でできるわけですね.
微分のことは自分でしろと言われる時代じゃないわけです.

データの保存

W,b等を保存しておかないと後で活用できません,そのために学習では覚えておく必要がありますそれは以下のようにすればよいです

saver = tf.train.Saver()
saver.save(sess, "model.ckpt")

余談

今回は説明の通りに実装しました。そのため、プログラムにとって処理しやすい順序にしているわけではありません.例えばsessionはcloseするのではなく,withを使うとか、namescopeの話,データを可視化してくれるtensorboardの話もしていません.また精度の評価もしていません.

最後に全体のソースを添付しておきます.

import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data

mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
sess = tf.Session() #Sessionの作成
# 定義域の元を定義
x = tf.placeholder(tf.float32, [None, 784])
# 行列の定義
W = tf.Variable(tf.zeros([784, 10]))
# ベクトルの定義
b = tf.Variable(tf.zeros([10]))
# sigmaとしてsoftmax関数を設定し、一つの層として計算
y = tf.nn.softmax(tf.matmul(x, W) + b)
# xの正解の関数fの値を定義
y_ = tf.placeholder(tf.float32, [None, 10])
# 誤差を定義
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y), reduction_indices=[1]))
# 誤差を最小化するようにbackpropagationwを実施
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)
sess.run(tf.global_variables_initializer()) # Varibleの初期化

for _ in range(10):
  batch_xs, batch_ys = mnist.train.next_batch(100)
  sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})


y_output = sess.run(y, feed_dict={x: batch_xs, y_: batch_ys})
print(y_output)

sess.close()

CNN

Deep LearningはCNNを用いた一般物体検出で一気有名になりました.
CNNはConvolutional Neural Networkの略です.

Convolutionとは

数学だと,ConvolutionはFourier変換等を勉強すると現れます.

定義

f,g:RnRに対し,fgのConvolutionfgを以下で定義する.

fg=Rnf(xy)g(y)dy

Convolutionでは,例えば以下の定理が有名です.

定理

f,gL1(Rn)の時,fgL1(Rn)であり,F(f)fL1(Rn)のフーリエ変換を表すとする.この時以下が成り立つ.

F(fg)(z)=F(f)(z)F(g)(z)

これは関数同士の畳み込みはフーリエ変換した世界での積になっているということです.
送った先でとても自然な演算になっているものは数学でよく出てきます.

本当は上の意味でのConvolutionとの関係を調べて一般的に記述しようと思ったのですが,よくわかなかった上に調べてる時間がなくなったので、読者の演習問題とします.

画像データ

CNNでは主に画像を対象にしています.
画像がどうデータとして扱われるかは例えばここをみてください.
よくあるものだと,縦×横×チャンネル(RGB)個の0~255の整数として表現されます.
画像は近いもの同士には関係があるだろうということで,縦,横の前後関係保持できる形の多次元配列として定めている物が多いです.

Tensorflowでも画像を多次元配列のようなものとして扱います.
数学的にはテンソル積を取っているだけです.
ただしプログラムで扱うために基底をFixしています.
例えばRnRmR上のテンソル積の元を標準基底同士から誘導される基底を使って表しています.
縦10ピクセル,横16ピクセル、チャンネル3の画像は
aR10R16R3
の元として扱います.
これをei.fj.gjをそれぞれの標準基底として取ることで,a=αijkeifjgkと書くことができます
多次元配列としては,αijkを並べたものとして記載されます.

CNNにおけるConvolutionの処理

実際にCNNでConvolutionがどうやって定義されているかを説明します.

行列に対する操作の説明

m1>m2,n1>n2とし,(xij)ijMm1n1(R)(wij)ijMm2n2(R)に対し、(aij)ijMm1m2,n1n2(R)を以下で定義する.

aij=k=0m2l=0n2xi+k,j+lwkl

この処理の気持ちは,画像データだと一点に関係あるのは近くの情報だけだろうから,全体で行列を書けるような演算はむしろ無駄で近くだけで演算すればいいよねというものです.

padding

計算の都合上Convolutionをした後の元(aij)Mm1n1(R)の元とみなした方が都合がいい場面があります.
この時そのために0で埋め込む操作をpaddingといいます.

Max pooling

Max poolingという操作もCNNではします.これは近くの中で値が一番大きいものだけを取るというものです.
説明量が膨大になってきたので今回は省きます.
気になる箇所は仕様を調べてみてください.

余談

CNNの定義はここでは述べていません.Convolution,Maxpoolingを少なくとも一度は含むとするべきなのかもしれませんが,ResNetのようにConvolution,Maxpooling,全結合以外の操作もするNNも含めようとすると,関数として定義しにくいものになっています.
おそらくニューラルネットを考えている人の思考やプログラムが数学の関数と対応していないからだと思いますが.このあたりをうまく定式化する方法はないものかなあと悩みます.

TensorflowにおけるConvolutionのメソッド

Tensorflowでもいろんなところで定義されていますが,
例えば以下のメソッドで定義されています.
tf.nn.conv2d
詳細は仕様に記載されていますが、僕はこれだけでもなかなかわからなかったので、動作のメモを記載しておきます.
先程のConvolutionの定義をみると,二次元配列が二つあればいいだけに感じますが,引数はそれよりも多いものです.

conv2d(
    input,
    filter,
    strides,
    padding,
    use_cudnn_on_gpu=True,
    data_format='NHWC',
    name=None
)

引数の説明

  • input:
    4次元配列です.これは画像の場合は、画像の枚数、縦、横、チャンネル(RGB)の4次元配列になります.

  • filter
    4次元配列です.縦,横,入力のチャンネル数,出力のチャンネル数の4次元配列です.
    私はoutputのチャンネルを変更する理由が理解できていないのですが,そのほうがいいのでしょうか.

  • stride
    画像をどれだけずらすかを表します.

  • padding
    "SAME", "VALID"のどちらかを表し.どうやって0を埋めるか等を表します.

  • use_cudnn_on_gpu
    数式だけを見ているのでは関係ないですが,GPUを使うか否かの指定を表します.

  • data_format
    入力データのフォーマットを表します.チャンネルがどの次元に入るかを意味しています.
    "NHWC", "NCHW"のどちらかを減らべます。特に指定意思ないと"NHWC"になります.

  • name
    演算に対して名前を定義します.
    計算グラフ上名前をつけて区別できるようにしておくのは重要なので,設定されています.

この関数の挙動をいくつか調べます.

調べる時は有名なものを使いたいので,TensorFlowのTutorialをベースに実験します.

strideの挙動

  • strides=[2, 1, 1, 1]で実行
InvalidArgumentError (see above for traceback): Current implementation does not yet support strides in the batch and depth dimensions.
[[Node: conv1/Conv2D = Conv2D[T=DT_FLOAT, data_format="NHWC", padding
="SAME", strides=[2, 1, 1, 1], use_cudnn_on_gpu=true, _device="/job:localhost/
replica:0/task:0/gpu:0"](reshape/Reshape, conv1/Variable/read)]]

どうやら,batch方向,ch方向が1でないとエラーになるようです.
いずれサポートされるのか、そもそもサポートしないという方針なのかどちらなんでしょう.

  • stdies=[1,2,2,1]で実行すると,
h_conv = h_conv1.eval(session=sess,feed_dict={x: batch[0], y_: batch[1], keep_prob: 1.0})
print(h_conv.shape)

(50, 14, 14, 32)となりました.

  • padding="VALID"の時
def conv2d(x, W):                                                                    
 """conv2d returns a 2d convolution layer with full stride."""                      
 return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='VALID')  

の時,

print(h_conv.shape)
(50, 24, 24, 32)

となり,0 paddingされていないことがわかります.

  • チャンネル数が実際のどの程度違いをうむのか?

チャンネル数を落としたら精度が落ちることが確認できました.

このあたりを実験しておけば,動作がなんとなくわかってくるのではないでしょうか.
早く使い慣れたいものです.

注意

shapeが合わない行列の計算もエラーになるので,一つを変えて残りも正しく動作させるためには影響箇所を全てサイズを変更する必要があります.

まとめ

量が増えてきたので,今回はこのあたりで終わりにします.
ここでは機械学習の最低限の数式とTensorflowの使い方を紹介しました.
新しいことを学ぶ時は最初の入り口が大変だったりするので,参考になれば嬉しいです.

今後時間が取れたらRNNの紹介と数学や機械学習でお世話になっているArxivから論文を使って遊ぶToy Problemを載せようと思います

P.S.
Arxivで遊んでいるものがありました.しかも,コンピュータと数学のアドベントカレンダーの記事です.