六本木で働くデータサイエンティストのブログ

元祖「銀座で働くデータサイエンティスト」です / 道玄坂→銀座→東京→六本木

機械学習をやる上で必要な数学とは、どの分野のどのレベルの話なのか(数学が大の苦手な人間バージョン)

しばらく前にこんな記事が出ていたのをお見かけしました。

明らかにこれは僕が某所(笑)で適当に放言したことがきっかけで巻き起こった議論の一旦なのではないかと思うのですが、個人的にはこちらの@さんの仰る通りで大体良いのではないかと考えております。


なのですが、言い出しっぺらしき身としてはもうちょっと何か具体的な話を書いた方が良いのかな?とも思いましたので、常々公言しているように数学が大の苦手な身ながらどの分野のどのレベルの数学が機械学習をやっていく上で必要なのかという点について戯言だらけの駄文を書いてみることにします。


深層学習 (機械学習プロフェッショナルシリーズ)

深層学習 (機械学習プロフェッショナルシリーズ)

ちなみに、以下に並べる戯言は深層学習青本から得られた知識をベースにしています。何度でも書いておきますが、僕は機械学習の理論的な側面についてはほぼど素人なのでそこだけはご承知おきください。。。間違っている部分があったらガンガンご指摘いただけると有難いですm(_ _)m*1


基本:TensorFlowでNNを書いた時にその意味が分かる程度の数学の知識


と言っても、数学が大の苦手な僕が偉そうに書けることなんてほとんどありません。けれども、以下のシリーズ記事を書いた時に気付いたことがあります。

「ああ、これってその辺のDeep Learningのテキストに載ってるような数式をそのまま書けばそのままNNとして動くようになってるんだ、すんげー便利やー」。はい、僕にとってはまさにこれだけです(笑)。


例えば、以下の典型的なMNIST分類のDNNのコードって元々は深層学習青本のpp.7-21辺りに数式で書かれていることを、そのままTensorFlowで表したものですよねという。

x = tf.placeholder(tf.float32, [None, 784])

# 1層目
## 重み付け(パラメータ)の定義
W1 = tf.Variable(tf.truncated_normal([784, 512], mean=0.0, stddev=tf.sqrt(2.0 / (784.0 + 512.0))))
## バイアス(切片)の定義
b1 = tf.Variable(tf.zeros([512]))
## 予測値(学習データに対してフィットさせるもの)を行列計算して与える
y1 = tf.matmul(x, W1) + b1
## 活性化関数(ここではReLU)
y1 = tf.nn.relu(y1)

# 2層目
W2 = tf.Variable(tf.truncated_normal([512, 256], mean=0.0, stddev=tf.sqrt(2.0 / (512.0 + 256.0))))
b2 = tf.Variable(tf.zeros([256]))
y2 = tf.matmul(y1, W2) + b2
y2 = tf.nn.relu(y2)

# 全結合層
W3 = tf.Variable(tf.truncated_normal([256, 10], mean=0.0, stddev=tf.sqrt(2.0 / (256.0 + 10.0))))
b3 = tf.Variable(tf.zeros([10]))
y3 = tf.matmul(y2, W3) + b3

# 勾配降下法(というかモメンタム法)で最適化
y = tf.placeholder(tf.int64, [None, 1])
y_ = tf.one_hot(indices = y, depth = 10)
global_step = tf.Variable(0, trainable=False)
starter_learning_rate = 0.001
learning_rate = tf.train.exponential_decay(starter_learning_rate, global_step,
                                           10000, 1 - 1e-6, staircase=True)
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels = y_, logits = y3))
optimizer = tf.train.MomentumOptimizer(learning_rate, momentum = 0.9, use_nesterov=True).minimize(cost, global_step = global_step)

これは冗談でも何でもなくて、僕にとってはそれに気付いたことはTensorFlowを使う上で密かに物凄く感動したポイントの一つだったのでした。上記シリーズ記事で引用した講談社MLP深層学習本に限らず、例えばarXivなどに日々上がってくるNN系の論文も基本的にはズラリとネットワークを表現する数式が並ぶわけですが、TensorFlowであればその数式を置き換えた関数をネットワーク表現に沿ってただベタベタと書いていけば、うまくいくわけです。多分*2


ということで、実際にどれくらいの数学の知識があればTensorFlowでNNを書けるのか、そしてその意味を理解できるんだろうか、というど素人観点からの説明を以下に書いていきます。


線形代数:線形モデル上でtf.matmul が何をしているのか分かる程度


f:id:TJO:20180424153029p:plain
そもそもNNで言うところの「層」は、それ単体で見るといわゆる重回帰分析(線形回帰モデル)と同じで「パラメータ×説明変数」の線形和で表されるわけです。つまり、入力が沢山あってこれにパラメータを掛け合わせた和を取ると、それが次の「層」の新たな入力になるというものです。これを適当にそれぞれ \mathbf{w}, ~ \mathbf{x}で表してみると、i = 1, 2, 3, \cdots, mはデータの行、 k = 1, 2, 3, \cdots, nは説明変数の列として

 u_i = b_i + w_1 x_{i1} + w_2 x_{i2} + w_3 + x_{i3} + \cdots + w_i x_{in}

というように書けるはずです。でもこの表記は結構まだるっこしいので、深層学習青本p.9と同じ形にになるように以下のように直します。

 \mathbf{u} = \mathbf{Wx} + \mathbf{b}

 \mathbf{u} = \left( \begin{array}{c} u_1 \\ \vdots \\ u_m \end{array} \right) ,  \mathbf{x} = \left( \begin{array}{c} x_1 \\ \vdots \\ x_m \end{array} \right) ,  \mathbf{b} = \left( \begin{array}{c} b_1 \\ \vdots \\ b_m \end{array} \right) ,  \mathbf{W} = \left( \begin{array}{ccc} w_{11} & \cdots & w_{1n} \\ \vdots & \ddots & \vdots \\ w_{m1} & \cdots & w_{mn} \end{array} \right)

単に式として書くだけなら非常にすっきりとした形になりました。線形代数というか、行列計算を使うだけでこんなに簡単になるというわけです。この部分をTensorFlowで書いたのが

y1 = tf.matmul(x, W1) + b1

の1行に当たります。裏を返すと、TensorFlowのtf.matmulを使えばここの計算は3つデータを与えれば自動的に全部やってくれるというわけです。コードを書く人間が考えるべきことは、作りたいNNの「層」をどのような行列計算で表すかということだけ。そのために行列計算を含む線形代数の知識があると便利ですよ、というお話です。


微積分:線形モデルの最適解を求める過程で tf.train.GradientDescentOptimizer(rate).minimize(cost) が何をしているのか分かる程度


NNでは学習データに合わせてパラメータを決める際に、モデルの予測値と学習データとの間の誤差(損失)関数を最小化するために、勾配降下法(もしくはその発展アルゴリズム)を使います。厳密には誤差逆伝播を使ってネットワーク内を遡っていくような最適化をやるのですが、TensorFlowでは最後に使う最適化の関数が自動的にそれをやってくれるので、我々が意識する必要は特にありません。一般に、勾配降下法のアルゴリズムは深層学習青本p.24の式(3.1-2)のように書き表せます。

 \mathbf{w}^{(t+1)} = \mathbf{w}^{(t)} - \lambda \frac{\partial E(\mathbf{w})}{\partial \mathbf{w}}

これだけ見てても「ふーん」と感じるだけで終わってしまうと思うのですが、それでは「何故NNの世界では『勾配消失』とか勾配が云々うるさく言うのか」というのが分かりません。

f:id:TJO:20180424151524p:plain

これは昔パーセプトロンの説明で使った図ですが(これ合ってるのかなぁ)、要は「勾配」と言ったら「微分偏微分)」なわけで、「微分」と言ったら「傾き」なわけです。勾配降下法というものは、パラメータをわずかに変えてやった時の「傾き」を利用して、モデルの予測値と学習データとの間の誤差(損失)をどんどん小さくしていって、最終的に図の中の☆のところに到達することを目指すもの、と言って良いかと思います。ちなみに \lambdaはその瞬間の「傾き」に対してどれくらいパラメータを変えるかという倍率を表す「学習率」です。


例として、ただの重回帰分析(線形回帰モデル)をTensorFlowで表したコードが以下です。

# 行列計算で「層」を作る
x = tf.placeholder(tf.float32, [None, 13])
y = tf.placeholder(tf.float32, [None, 1])
W = tf.Variable(tf.zeros([13,1]))
b = tf.Variable(tf.zeros([1]))

# 勾配降下法の準備
## まずモデル予測値を定義する
y_reg = tf.matmul(x, W) + b
## ここの部分が「層」と学習データから成る誤差(損失)を表す
cost = tf.losses.mean_squared_error(labels = y, predictions = y_reg)
## 学習率を与える
rate = 0.1
## 誤差(損失)と学習率から、具体的に勾配降下法によって最適なパラメータを求める
optimizer = tf.train.GradientDescentOptimizer(rate).minimize(cost)

最後の最後にtf.train.GradientDescentOptimizer(rate).minimize(cost)が出てきますが、これが勾配降下法で誤差(損失)を最小化するTensorFlowのメソッドというわけです。とりあえず「微分」すると「勾配」が得られて、その「勾配」を「傾き」として使って最適なパラメータを探すことができるということがこれで分かったわけで、最低でも「微分偏微分)」の概念が一通り分かるぐらいの微積分の知識は知っておいて損はないですよ、というお話でした。


その他:最低でもΣは分かった方が良いし、できれば数式1行程度なら我慢して読めた方が良い


当たり前ですが、 tf.nn.softmax が何をしているのか分かるためには一応\sum{}ぐらいは知っておいても良いと思うわけです。

y = tf.nn.softmax(tf.matmul(x, W) + b)

と言うのは、一応式としては深層学習青本p.20にもあるように

 P(\cal{C}_k | \mathbf{x}) = \frac{ p(\mathbf{x}, \cal{C}_k) }{ \displaystyle \sum^K_{j = 1} p(\mathbf{x}, \cal{C}_j) }

という多クラス分類で使われるsoftmaxを表しているわけで、これ何だったっけ?ということぐらいは思い出せた方が良いのかなとは個人的には思います。ちなみに「そんなの常識だろ!」とご立腹の方もおられるかと推察しますが、非理系出身の人だと\sum{}を見ただけで頭痛がしてくる*3ということもあったりするので、この辺確認しておくのはかなり重要です。。。


これに限らず、実際には大して難しくも何ともない数式で色々表していることが世の中多くて、例えばargminとかargmaxは数式で見ると「???」となる人も多そうですがコードで書けば「ある値を最小or最大にするパラメータを探索して探すループ文」でしかないんですよね(うっかりするとその辺の関数使えばおしまい)。この辺は我慢強さとかも重要なのかなぁと、数学が大の苦手な身としては思ってます。


そして、機械学習も含めてもっと一般的な「数式をプログラミングで表すためのテクニック」に関しては、ズバリ@さんの名スライド「数式を綺麗にプログラミングするコツ #spro2013」を参照されることをお薦めいたします。これは何回読んでもためになる素晴らしい資料です。特にこの資料の中にある多項ロジットの数式のR, Pythonへの書き換えパートを読むと、非常に参考になるのではないかと思います。


最後に



もちろん、上に挙げた程度の数学では足りないというシチュエーションが沢山あることは承知しております。例えば以前HSICの論文を読んだ時は、再生核ヒルベルト空間とか作用素とか測度論系の用語とかがズラリと出てきて、全力で轟沈したのを覚えています。。。(泣) ということもあるので、もちろん数学に長けているに越したことはないと思います。特に毎週のようにarXivに上がってくる最新の機械学習・数理統計学の論文を読みこなしたいとか、NIPS / KDD / AAAI / ICML / ACL etc.と言ったトップカンファレンスの採択論文を読んで実装してみたいとか思うのであれば、数学の知識が相応の分野と相応のレベルにまたがってあった方が良いのは間違いないでしょう。


ただし、単に実装済みのものが提供されている機械学習の各種手法の「ユーザー」である限りはやはり程度問題でしょうし、TensorFlowでゴリゴリNN書くなら上記のレベルの数学ぐらいは知っておいても損はないのかなと考える次第です。


あとこれは思い出話になりますが、以前非線形カーネルSVMのSMOを生実装で書いた*4時に結構細かいアルゴリズムを書く羽目になった上に、ラグランジュの未定乗数法を幾星霜ぶりかにやったので、その辺の数学も多少は分かった方が無難だと思います。


と、あまりこういうことばかり書くとインターネットの向こう側から「お前の機械学習の数学の理解は全て間違っているので理論書を最初から読み返せ」「測度論とルベーグ積分もっと勉強しろ」「汎関数中心極限定理もっと勉強しろ」とか大量のプレッシャーが降り注いできてその恐怖に夜も眠れなくなってしまうので、戯言はこの辺にしておきます。。。

*1:添え字がおかしいとかは直すのも面倒なので、雰囲気を出しただけということで何卒ご容赦を。。。

*2:ああそう言えばまだGAN生実装書いてない。。。

*3:例えばうちの嫁さんとか

*4:ただしこの業界に来てすぐの頃だったのでMatlabで書いていたのでした