読者です 読者をやめる 読者になる 読者になる

ゼロから作るDeep Learning 4章 ニューラルネットワークの学習

前回はゼロから作るDeep Learning 3章 ニューラルネットワーク - n3104のブログです。

4章です。3章では学習済みのモデルを使いましたが、4章ではそのモデルの学習方法について学びます。といっても、実際にニューラルネットワークで学習する際は4章で学ぶ数値微分ではなく5章で学ぶ誤差逆伝播法を利用します。なので、モデルの学習方法というよりはモデルを学習する仕組みについて学ぶと言ったほうが適切かもです。

  • 4.1.1 データ駆動
    • 機械学習はデータが命です。”
      • ほんとその通り。
    • もちろん特徴量の抽出は考えなくて良くなるという建前だけど、元のデータが役に立つ情報を含んでいなければ当然いけないし、ネットワークの設計は残ると思うのだけど。。
      • 画像の識別なら同じネットワークが使えるのかなー。でも、画層の種類は問題によって異なるだろうし、画像の大きさとか階調でも違う気がするんだけど。。
  • 4.2 損失関数
    • 4.1.2 訓練データとテストデータでの汎化能力の説明にあるように1節で1つの概念を説明するようにする構成はとてもいいと思うんだけど、「幸せ指標」はメタファとしてはイケてない気が。。テストの点数ぐらいで良かった気がするんだけどw
  • 4.2.2 交差エントロピー誤差
    • 2 乗和誤差と比べて交差エントロピー誤差はクラスラベルの次元のみ利用するから、分類問題にしか使えないんだろうなー。その分、はっきりと結果が出るんだろうけど。
    • y = logxのグラフで 0 - 1 の区間は -inf - 0 に対応してるから -log すると 0 - inf になるって言うわけかー。
    • “中身の実装では、np.log の 計算時に、微小な値である delta を足して計算しています。これは、np.log(0) の ような計算が発生した場合、np.log(0) はマイナスの無限大を表す-inf となり、そ うなってしまうと、それ以上計算を進めることができなくなります。その防止策とし て、微小な値を追加して、マイナス無限大を発生させないようにしています。”
      • なるほどー。プログラムとして実装する上でのノウハウだなー。
      • delta = 1e-7 となってるけど e-7 が妥当というのはどこから来てるんだろう?
  • 4.2.3 ミニバッチ学習
    • “ただし、最後に N で割って正規化しています。こ の N で割ることによって、1 個あたりの「平均の損失関数」を求めることになりま す。そのように平均化すれば、訓練データの数に関係なく、いつでも統一した指標が 得られます。たとえば、訓練データが 1,000 個や 10,000 個の場合であっても、1 個 あたりの平均の損失関数を求められます。”
      • なるほど。
  • 4.2.4 [バッチ対応版]交差エントロピー誤差の実装
    • cross_entropy_error 関数は分かりやすくするためだろうが delta によるエラー回避のコードが省略されているので、実際に実行するとエラーになったw
    • ndim は配列の次元数だった。
    • reshape を使うことで配列の次元数を変換することも可能。なのでここでは入力が一次元配列の場合に二次元配列に変換している。
    • y.shape[0] はレコード数になる。
    • np.log( y[np.arange(batch_size), t] ) の所は正解ラベルが配列のインデックスと一致するラベル名だから動作するわけであって、他のラベルだと動作しないのだから紹介するのはどうなんだろう。。
>>> np.array([1, 2]).ndim
1
>>> np.array([1, 2]).shape
(2,)
>>> np.array([[1, 2],[3,4]]).ndim
2
>>> np.array([[1, 2],[3,4]]).shape
(2, 2)
>>> np.array([[1, 2],[3,4]]).reshape(4,1)
array([[1],
       [2],
       [3],
       [4]])
>>> np.array([[1, 2],[3,4]]).reshape(1,4)
array([[1, 2, 3, 4]])
>>> np.array([[1, 2],[3,4]]).reshape(1,4).ndim
2
>>> np.array([[1, 2],[3,4]]).reshape(4,1).ndim
2
>>> np.array([[1, 2],[3,4]]).reshape(4)
array([1, 2, 3, 4])
  • 4.2.5 なぜ損失関数を設定するのか?
    • “「認識精度」を指標にすべきではないか”
      • 実際に認識精度を指標としてパラメータのチューニングってできるんだっけ?
    • ニューラルネットワークの学習の際に、認識精度を“指標”にしてはいけない。 その理由は、認識精度を指標にすると、パラメータの微分がほとんどの場所で 0 になってしまうからである。”
      • “つまり、パラメータの少しの調整だけでは、認識 精度は改善されず一定のままなのです。もし認識精度が改善されたとしても、その値 は 32.0123…% のような連続的な変化ではなく、33% や 34% のように、不連続のと びとびの値へと変わってしまいます。一方、損失関数を指標とした場合、現在の損失 関数の値は 0.92543…のような値によって表されます。そして、パラメータの値を少 し変化させると、それに反応して損失関数も 0.93432…のように連続的に変化するの です。 ”
      • 書いてあることはその通りなんだろうけど、いまいち腑に落ちないなー。
    • “活性化関数の「ステップ関 数」にも同じ話が当てはまります。”
      • “ステップ関数は「ししおどし」のように、ある瞬間だけ変化を起こす関数でした が、一方、シグモイド関数の微分(接線)は、図4-4 に示すように、出力(縦軸の値) が連続的に変化し、さらに、曲線の傾きも連続的に変化します。つまり、シグモイド 関数の微分はどの場所であっても 0 にはならないのです。これは、ニューラルネット ワークの「学習」において重要な性質になります。この性質――傾きが 0 にはならな い――によって、ニューラルネットワークは正しい学習が行えるようになります。”
      • ここはわりと腑に落ちた。要は極端になるってことなのかなー。。
  • 4.3.1 微分
    • “ここで行っているように、微小な差分によって微分を求めることを数値微分 (numerical differentiation)と言います。一方、数式の展開によって微分を求めることは、解析的(analytic)という言葉を用いて、たとえば、「解析的に 解く」とか「解析的に微分を求める」などと言います。たとえば、y = x2 の微 分は、解析的には、dy = 2x として解くことができます。”
    • 数値微分は中心差分を使うだけでいいんだ。なんて単純なんだ。。!
    • 実際に前方差分のみと中心差分の場合とで比べてみたら、前方差分のみは 0 になってしまったw
def numerical_diff1(f, x):
    h = 10e-50
    return (f(x+h) - f(x)) / h


def numerical_diff2(f, x):
    h = 1e-4 # 0.0001
    return (f(x+h) - f(x-h)) / (2*h)


def function_1(x):
    return 0.01*x**2 + 0.1*x


print(numerical_diff1(function_1, 5))
print(numerical_diff1(function_1, 10))
print(numerical_diff2(function_1, 5))
print(numerical_diff2(function_1, 10))

出力

0.0
0.0
0.1999999999990898
0.2999999999986347
  • 4.3.2 数値微分の例
    • 4.3.1 微分 を確認する際に少し読んでいたので特に気になる点はなかった。
  • 4.3.3 偏微分
    • 説明は分かるんだけど、初めて偏微分見た人は理解が追いつくのかな。。
    • 特に説明してないけど、numerical_diff を使って偏微分を実装している。これで偏微分が出来るのは偏微分する変数以外は定数になってるからだよなー。結果的に偏微分の対象となる変数の微分、つまり微小な変化の極限が出てる。
  • 4.4 勾配
    • “すべての変数の偏微分をベクトルとしてまとめた ものを勾配(gradient)と言います。”
      • そうだったのか!
    • “ひとつ補足として述べるとすれ ば、np.zeros_like(x) は、x と同じ形状の配列で、その要素がすべて 0 の配列を 生成するということです。”
      • いや、そこよりも色々補足必要な気がするw
      • numerical_gradient で勾配が出せるのは、次元毎に中心差分を取ってるから。要は numerical_diff のロジックを再実装している。対象となる次元以外の次元は同じ値になるので結果的に相殺され、対象となる次元の微分、つまり偏微分を出せる。
      • 引数となる x に再代入しているけれど、最後に tmp_val を代入しているのでもとに戻る。とはいえ、処理中に例外発生したら呼び出し元の配列の中身が書き換わるから、この実装方法は結構微妙な気がするんだけど。。機械学習の場合は巨大な行列を扱うこともあるからメモリ節約の観点でわざわざコピーせずにやるのかなー?
    • “この勾配は何を意味しているのでしょうか? そ れを理解するために、f (x0 , x1 ) = x20 + x21 の勾配を図で表してみることにしましょ う。ただし、ここでは勾配の結果にマイナスを付けたベクトルを描画します”
      • マイナスつけてるなら納得。
    • “勾配が示す方向は、各場所において関数の値を最も減らす方向”
      • マイナス付けた場合という但し書きはあってもいい気もするが。要は傾きなので単純な勾配は減らす方向にならない。
  • 4.4.1 勾配法
    • “また、関数が複雑で歪な形をしていると、(ほとんど)平らな土地に入 り込み、「プラトー」と呼ばれる学習が進まない停滞期に陥ることがあります。”
      • なるほどー。確かにこういうケースもありそう。
    • “勾配法は、目的が最小値を探すことか、それとも最大値を探すことかによって 呼び名が変わります。正確には、最小値を探す場合を勾配降下法(gradient descent method)、最大値を探す場合を勾配上昇法(gradient ascent method)と言います。ただし、損失関数の符号を反転させれば、最小値を探す 問題と最大値を探す問題は同じことになるので、「降下」か「上昇」かの違いは 本質的には重要ではありません。一般的に、ニューラルネットワーク(ディー プラーニング)の分野では、勾配法は「勾配降下法」として登場することが多 くあります。 ”
      • 勾配上昇法というものあるんだねー。でも、上昇法だと無限を目指す必要があるから降下法を使うのかなー。
    • ニューラルネットワークの学習においては、学習率の値を変更し ながら、正しく学習できているかどうか、確認作業を行うのが一般的です。”
      • 学習率というハイパーパラメーターも残ってたか。。
      • “学習率のようなパラメータはハイパーパラメータと言います。これは、ニュー ラルネットワークのパラメータ――重みやバイアス――とは性質の異なるパラ メータです。なぜなら、ニューラルネットワークの重みパラメータは訓練デー タと学習アルゴリズムによって“自動”で獲得されるパラメータであるのに対し て、学習率のようなハイパーパラメータは人の手によって設定されるパラメー タだからです。一般的には、このハイパーパラメータをいろいろな値で試しな がら、うまく学習できるケースを探すという作業が必要になります。 ”
    • gradient_method.py はカレントディレクトリが ch04 であることを前提としていた。これをIntelliJでやる手順は以下の通り。
      • File -> Project Structure… (Cmd+;)
      • Modules -> Sources タブで ch01…ch08 を選択状態にして Sources ボタンで追加
    • “この実験の結果が示すように、学習率が大きすぎると、大きな値へと発散してしま います。逆に、学習率が小さすぎると、ほとんど更新されずに終わってしまいます。”
      • gradient_descent 関数は init_x の中身を書き換える実装になってた。なので、実際は書き換えない実装にしないと駄目な気もするけど、機械学習の界隈ではそういう割り切りなのかなー。
      • 実際にいくつか試したが、今回だと 1.0 を超えると大きすぎて発散していく。
# coding: utf-8
import numpy as np
import matplotlib.pylab as plt
from gradient_2d import numerical_gradient


def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x
    x_history = []

    for i in range(step_num):
        x_history.append( x.copy() )

        grad = numerical_gradient(f, x)
        x -= lr * grad

    return x, np.array(x_history)


def function_2(x):
    return x[0]**2 + x[1]**2

init_x = np.array([-3.0, 4.0])    

lr = 0.1
step_num = 20
x, x_history = gradient_descent(function_2, init_x, lr=lr, step_num=step_num)

plt.plot( [-5, 5], [0,0], '--b')
plt.plot( [0,0], [-5, 5], '--b')
plt.plot(x_history[:,0], x_history[:,1], 'o')
print(init_x)

init_x = np.array([3.0, 4.0])
lr = 0.95
x, x_history = gradient_descent(function_2, init_x, lr=lr, step_num=step_num)
plt.plot(x_history[:,0], x_history[:,1], 'o')
print(init_x)

init_x = np.array([3.0, -4.0])
lr = 0.01
x, x_history = gradient_descent(function_2, init_x, lr=lr, step_num=step_num)
plt.plot(x_history[:,0], x_history[:,1], 'o')
print(init_x)

plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
plt.xlim(-5.5, 5.5)
plt.ylim(-5.5, 5.5)
plt.xlabel("X0")
plt.ylabel("X1")
plt.show()

f:id:n-3104:20170320165206p:plain

  • 4.4.2 ニューラルネットワークに対する勾配
    • 項のタイトル通り、本当に勾配を1回出すだけで、学習しない。1項1テーマに絞ってるな、ほんと。
    • ネットワークの形は図示したほうがいい気がするけど、それは次節以降で表記してるのかなー。結局、何を求めたかイメージわかない人もいるのでは。
      • 最後まで確認したが、4章の範囲ではネットワークの形は図示しないんだなー。
    • W は np.random.randn で生成しているので、当然毎回結果が異なるwとはいえ、x と t は固定だからある程度傾向は似るんだねー。
    • 一通りコードを追いかけ直したけど、ここのサンプルソースはすごい分かりにくいのでは。。普通に f の引数 W を利用する損失関数にした方が直感出来だった気がする。
      • W に関する勾配を出すわけだから損失関数は W に関する関数である必要がある。で、loss 関数は内部で predict 関数を実行するので、結果的に loss 関数は W に関する関数であると言える。なので、勾配自体は出せる。
        • numerical_gradient 関数で勾配が出せるのは、第2引数が net.W だから。そのため、predict 関数で利用する W の参照が得られるので、numerical_gradient 関数内で前方差分と後方差分を出す際に W の参照を経由して微小量 h が増減する。。
        • “(ここで定義した f(W) という関数の引数 W は、ダミーとし て設けたものです。これは、numerical_gradient(f, x) が内部で f(x) を実行す るため、それと整合性がとれるように f(W) を定義しました)”
          • そうではなくて、ここで net.W を渡さないとそもそも W を変化させて損失関数 f を実行できないから勾配が取得できない。ここは、途中で筆者が混乱したのかなー。simpleNet のコンストラクタで W を指定するのではなくて、predict と loss 関数の引数に W があってもいい気がするんだけど。まぁ、そのなるとクラスにする意味もなくなるけどねー。。
            • ここについては、ほんとにダミーでもよかった。4.5.1 で self.params でアクセスしてた。。。
  • 4.5 学習アルゴリズムの実装
    • 学習について分かりやすくまとまっている。ただ、機械学習について初めてこの本で学ぶ人はどこまで理解できるんだろう。。
  • 4.5.1 2 層ニューラルネットワークのクラス
    • numerical_gradient を見てて思うが、こういう感じで勾配というか W と b の偏微分出せちゃうんだねー。。実際、loss 関数の中で predict 関数を呼び出していて、その中で y を出すために W と b を使って何度か計算してるけど、これを1つの数式にできちゃうもんなー。で、それぞれの偏微分ができちゃうと考えると、確かに数学そこまで分からなくてもコードレベルで理解できるというか、こう書けばいいというのはわかるなー。。
    • 今後 gradient 関数を使うし、numerical_gradient だと計算が遅いからといって、若干天下り的な感じもするねw
    • t = np.random.rand(100, 10) # ダミーの正解ラベル(100 枚分)
      • これ、正解ラベル自体が確率分布っぽくなっちゃうけど、それは問題ないんだっけ?
        • MNISTのデータは1つだけ 1 で他は 0 だった。だよねー。。
>>> np.random.rand(3, 10)
array([[ 0.65365022,  0.63623433,  0.70117989,  0.89724433,  0.82817134,
         0.31230608,  0.34446204,  0.23931909,  0.13342318,  0.67355462],
       [ 0.85156144,  0.10901498,  0.02327781,  0.69283457,  0.13562442,
         0.09658677,  0.32591785,  0.00592776,  0.38647803,  0.91804246],
       [ 0.86498541,  0.03354259,  0.69663751,  0.01011192,  0.1069335 ,
         0.30273103,  0.8269463 ,  0.4885381 ,  0.20540422,  0.9368264 ]])
  • 4.5.2 ミニバッチ学習の実装
    • train_neuralnet.py のソースが描画するグラフは loss ではなくて訓練データとテストデータの正解率の推移だったw
      • loss を表示するようにコードを書いてみたけど、最初から loss の値は 2 ぐらいからスタートしてた。書籍中のグラフはどこから持ってきたw?
  • 4.5.3 テストデータで評価
    • “エポック(epoch)とは単位を表します。1 エポックとは学習において訓練 データをすべて使い切ったときの回数に対応します。たとえば、10,000 個の 訓練データに対して 100 個のミニバッチで学習する場合、確率的勾配降下法を 100 回繰り返したら、すべての訓練データを“見た”ことになります。この場 合、100 回= 1 エポックとなります。”
      • 分かりやすい。要は訓練データを一通り学習し終えたら1エポック。で、イテレーション数自体はエポックというか訓練データのサイズは考慮しないでバッチサイズと一緒に指定するっぽい。言われてみると、いままでもそんな感じだったかも。
    • train_neuralnet.py は 4.5.3 のソースということか。

個人的には数値微分の実装で興奮しました。こんな簡単に実装できるんですね!