ゼロから作るDeep Learning 5章 誤差逆伝播法

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

5章です。Deep Learningで有名な(?)誤差逆伝播法(Backpropagation)について学びます。4章で数値微分による勾配降下法を利用してニューラルネットワークで学習する方法について学びました。ただ、数値微分は計算コストが大きいのでより効率的に勾配を算出できる誤差逆伝播法を利用するそうで、この章ではその誤差逆伝播法の仕組みと実装方法について説明しています。

  • まえがき
    • “ひとつは「数式」によって、もうひとつは「計算グラフ(computational graph)」によって理解するというものです。前者のほうが一般的な方法で、特に、機 械学習に関する書籍の多くでは、数式を中心に話を展開していきます。確かに、数式 による説明は、厳密で簡潔になるのでもっともな方法なのですが、いきなり数式を中 心に考えようとしたら、本質的なことを見逃してしまったり、数式の羅列にとまどっ たりすることがあります。そこで本章では、計算グラフによって“視覚的”に誤差逆 伝播法を理解してもらおうと思います。実際にコードに書くことでさらに理解が深ま り「なるほど!」と納得できると思います。”
      • 計算グラフによる理解方法もあるんだー。
  • 5.2.3 連鎖律と計算グラフ
    • zの部分の偏微分を求めているのがちょっとピンとこない。。後の項で出てくるのかなー。 ← 5.3.1 を見た感じ、右端も常に微分する必要はあって、今回は終端だったというだけかな。
  • 5.4 単純なレイヤの実装
  • 5.4.1 乗算レイヤの実装
    • 微分(dout)
      • これ、多分 delta out なのかなー。
        • “また、backward() の引数は、「順伝播の際の出力変数に対する微分」を入力する”
        • あってるっぽい。
  • 5.4.2 加算レイヤの実装
    • こんなに簡単に実装できるんだなー。。
  • 5.5 活性化関数レイヤの実装
    • 読めば分かるし、式変換もぎり出来た。。
    • (5.12) 式は単純に第二項は y そのもので、第三項は (1 - y) に変換できると言うだけ。
  • 5.6.1 Affine レイヤ
    • ニューラルネットワークの順伝播で行う行列の内積は、幾何学の分野では「アフィン変換」と呼ばれます。そのため、ここでは、アフィン変換を行う処理を 「Affine レイヤ」という名前で実装していきます。”
      • なぜAffineレイヤと呼ばれるかというと行列の内積の計算がアフィン変換に含まれるからということらしい。
    • “ただし X、W、B は行列(多次元配列) であるということに注意しましょう。これまで見てきた計算グラフは「スカラ値」が ノード間を流れましたが、この例では「行列」がノード間を伝播します。”
      • そっか。ここからはノード単位からレイヤ単位に拡張されてるのか。 ← 違ってた。ニューラルネットワークの場合は前のレイヤーの全ノードの出力が単一のノードの入力になるので行列に拡張されているだけだった。図3-17 入力層から第 1 層目への信号の伝達みたいな感じ。なお、行列に拡張と言ってもこの時点では W のみで、X と B はベクトルにすぎない。X が行列に拡張されるのは次節の 5.6.2 バッチ版 Affine レイヤで、そこでも B についてはベクトルのまま。
    • “(式 (5.13) が導かれる過程はここでは省略します)”
      • さすがにそろそろ導出過程は省略するようになってきたか。。
      • 導出過程はないけど、要は掛け算なので 5.4.1 乗算レイヤの実装 と同じ考え方でそれぞれ掛けるものが入れ替わり、かつ、行列なので掛ける順序が式の通りになってるって感じかなー。個々の要素を展開して数式眺めれば納得するのかなー。。
      • 式展開してたら X が横ベクトルじゃないと計算できないはずなのに W は行列で shape の値が表示されてて混乱してきた???
        • いろいろ調べた結果、nparray は1次元の場合はベクトルに縦とか横とかの概念がないそうで、np.transpose しても縦/横は変換されず、np.dot の場合は片方が二次元の行列でもう片方が一次元のベクトルなら縦横関係なく計算てくれることが判明した。これは補足に書いてくれてもいい気もするんだけど、3.3 多次元配列の計算 では説明してなかった。。
        • numpyの1d-arrayを2d-arrayに変換 - keisukeのブログ
          • http://kaisk.hatenadiary.com/entry/2014/09/20/185553
          • “numpyはベクトルと行列を分けているので*1,ベクトルの転置が取れなくて困る.
          • n次元ベクトルxは,numpyでは行ベクトルでも列ベクトルでもない.単にn次元ベクトル."
        • numpy.dot — NumPy v1.12 Manual
>>> X = np.random.rand(2)
>>> X
array([ 0.19710846,  0.80155544])
>>> W = np.random.rand(2,3)
>>> W
array([[ 0.82160224,  0.64742996,  0.16274919],
       [ 0.82007676,  0.46175103,  0.96207937]])
>>> np.dot(X, W)
array([ 0.81928174,  0.49773297,  0.8032392 ])
>>> np.dot(W, X)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: shapes (2,3) and (2,) not aligned: 3 (dim 1) != 2 (dim 0)
>>> np.dot(np.transpose(W), X)
array([ 0.81928174,  0.49773297,  0.8032392 ])

# X への np.transpose は意味がない
>>> np.dot(np.transpose(X), W)
array([ 0.81928174,  0.49773297,  0.8032392 ])
>>> np.dot(np.transpose(W), np.transpose(X))
array([ 0.81928174,  0.49773297,  0.8032392 ])

# reshape して行列に変換すれば np.transpose できる
>>> X.shape
(2,)
>>> np.transpose(X).shape
(2,)
>>> X.reshape(1,2).shape
(1, 2)
>>> np.transpose(X.reshape(1,2)).shape
(2, 1)
>>> np.dot(X.reshape(1,2), W)
array([[ 0.81928174,  0.49773297,  0.8032392 ]])
>>> np.dot(np.transpose(X.reshape(1,2)), W)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: shapes (2,1) and (2,3) not aligned: 1 (dim 1) != 2 (dim 0)
  • 5.6.2 バッチ版 Affine レイヤ
    • “これまで説明してきた Affine レイヤは、入力である X はひとつのデータを対象と したものでした。ここでは N 個のデータをまとめて順伝播する場合、つまり、バッ チ版の Affine レイヤを考えます”
      • ??? N 個のデータをまとめてというのは 5.6.1 Affine レイヤ まではレイヤではなくてやはりノードの話で、バッチ版でレイヤにまで拡張されてる? ← これは4.2.3 ミニバッチ学習のバッチという意味だった。
  • 5.6.3 Softmax-with-Loss レイヤ
    • 図5-28 を見るとAffineもReLUもSoftmaxも全てレイヤになっている。。もうちょっと先まで読み進まないと理解できないのかなー。
    • 書いてあることは分かるんだけど、こう、なんというか納得感がないというか、結局どう使うのか分からないというか。。
    • “伝播する値をバッチの個数(batch_size)で割ることで、データ 1 個あたりの 誤差が前レイヤへ伝播する点に注意しましょう。”
      • バッチ処理なので一度に複数のデータを処理するので1回のバッチの変化量のオーダーを揃えようとしてるでいいんだっけ?
      • 4.2.3 ミニバッチ学習 に書かれているとおりで、"ただし、最後に N で割って正規化しています。こ の N で割ることによって、1 個あたりの「平均の損失関数」を求めることになりま す。そのように平均化すれば、訓練データの数に関係なく、いつでも統一した指標が 得られます。たとえば、訓練データが 1,000 個や 10,000 個の場合であっても、1 個 あたりの平均の損失関数を求められます。"だかららしい。ここは式 (4.3) だけなら納得なんだけど、SoftmaxWithLoss の実装では損失を合計してないんだよなー。。
      • 4.5 学習アルゴリズムの実装 に書かれているように1回の学習で更新するパラメータのオーダーをバッチサイズに関係なく一定にするってことかなー。。
      • 合計のオーダーを揃えようとしてるでいいみたい。合計する箇所はAffineレイヤだった。5.6.2 バッチ版 Affine レイヤ の 図5-27 バッチ版 Affine レイヤの計算グラフ を見ると分かるが、W については内積なのでバッチの個数分、つまり N 個が足される。なので、あらかじめ batch_size つまり N で割っておくとオーダーが揃うことになる。実装は 5.7 誤差逆伝播法の実装 の train_neuralnet.py で確認できる。ここで batch_size を減らすと N が変わることを layers.py でデバッグプリント入れると確認できる。

train_neuralnet.py

# batch_size = 100
batch_size = 3

layers.py

    def backward(self, dout):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        # 以下、追加したデバッグプリント
        print(self.x.T.shape)
        print(dout.shape)
        print(self.dW.shape)
        self.db = np.sum(dout, axis=0)

出力

(50, 3)
(3, 10)
(50, 10)
(784, 3)
(3, 50)
(784, 50)
  • 5.7 誤差逆伝播法の実装
    • この節で今まで出てきたものの全体における位置づけが分かるっぽい。
  • 5.7.1 ニューラルネットワークの学習の全体図
    • “これまで説明した誤差逆伝播法が登場するのは、ステップ 2 の「勾配の算出」で す。前章では、この勾配を求めるために数値微分を利用しましたが、数値微分は簡単 に実装できる反面、計算に多くの時間がかかりました。誤差逆伝播法を用いれば、時 間を要する数値微分とは違い、高速に効率良く勾配を求めることができます。”
      • なるほど。結局勾配を求める際の計算の効率化が目的なのか。
  • 5.7.2 誤差逆伝播法に対応したニューラルネットワークの 実装
    • self.params[‘b1’] = np.zeros(hidden_size)
      • バイアス項は初期値 0 で初期化するのかー。
    • self.layers = OrderedDict()
      • 本文にも開設のあるとおり順番付きディクショナリらしい。
      • Python本体に含まれている模様。
      • Javaで言うとこの LinkedHashMap みたいなものか。
    • self.layers[‘Relu1’] = Relu()
      • Relu は 5.5.1 ReLU レイヤ で実装していて、ノードかレイヤか分からなかったが、よくよく見直したらレイヤとして実装されてた。なるほど。そして、Relu レイヤは活性化関数のレイヤなのでプログラミングとしてはニューラルネットワークの1つの層を2つのレイヤ(AffineとRelu)で実装してるってことか。
      • 5.4 単純なレイヤの実装 についても改めて見てみたが、こちらはノードを実現するためのレイヤで合ってた。
        • “計算グラフの乗算ノードを「乗算レイヤ(MulLayer)」、加算ノー ドを「加算レイヤ(AddLayer)」という名前で実装することにします。”
      • レイヤという用語は単なる層ではあるが、ニューラルネットワークとしてのレイヤと計算を行うレイヤ(単ノード/複数ノード)があるので、何というか用語の使い分けなりがあっても良かった気はする。 ← 改めて読み直したらいちおう説明はしていた。うーん。。
        • 5.4 単純なレイヤの実装 の最初に断り書きはあった。
          • “ここで言う「レイヤ」とは、ニューラルネットワー クにおける機能の単位です。たとえば、シグモイド関数のための Sigmoid や、 行列の内積のための Affine など、レイヤ単位で実装を行います。そのため、 ここでも「レイヤ」という単位で、乗算ノードと加算ノードを実装します。”
        • 5.5 活性化関数レイヤの実装 の最初でも以下のように層としてのレイヤと記述してはいた。
          • “ここでは、ニューラルネットワークを構成する「層(レイヤ)」をひとつのクラ スとして実装することにします。まずは、活性化関数である ReLU と Sigmoid レイ ヤを実装していきます。”
    • self.lastLayer = SoftmaxWithLoss()
      • predict メソッドでは利用しない。これは 5.6.3 Softmax-with-Loss レイヤ に書いてある。
        • ニューラルネットワークで行う処理には、推論(inference)と学習の 2 つの フェーズがあります。ニューラルネットワークの推論では、通常、Softmax レ イヤは使用しません。たとえば、図5-28 のネットワークで推論を行う場合、 最後の Affine レイヤの出力を認識結果として用います。なお、ニューラルネットワークの正規化しない出力結果(図5-28 では Softmax の前層の Affine レ イヤの出力)は、「スコア」と呼ぶことがあります。つまり、ニューラルネット ワークの推論で答えをひとつだけ出す場合は、スコアの最大値だけに興味があ るため、Softmax レイヤは必要ない、ということです。一方、ニューラルネッ トワークの学習時には、Softmax レイヤが必要になります。”
    • 実際に動かして動作を確認するには 5.7.4 誤差逆伝播法を使った学習 で登場する train_neuralnet.py を利用する。
  • 5.7.3 誤差逆伝播法の勾配確認
    • “さて、数値微分は計算に時間がかかります。そして、誤差逆伝播法の(正しい)実 装があれば、数値微分の実装は必要ありません。そうであれば、数値微分は何の役に 立つのでしょうか? 実は、数値微分が実践的に必要とされるのは、誤差逆伝播法の 実装の正しさを確認する場面なのです。数値微分の利点は、実装が簡単であるということです。そのため、数値微分の実装 はミスが起きにくく、一方、誤差逆伝播法の実装は複雑になるためミスが起きやすいの が一般的です。そこで、数値微分の結果と誤差逆伝播法の結果を比較して、誤差逆伝 播法の実装の正しさを確認することがよく行われます。”
      • なるほどなー。
    • 数値微分は計算に時間が掛かるということだったけど、確かに数値微分はパラメータ毎の勾配を求めるためにはネットワーク全体を2回順伝播することになるのに対して、誤差逆伝播法の場合は順伝播と逆伝播の合計 2 回だけで演算終わるもんなー。なので、数値微分自体の計算が重いというよりは、数値微分の場合はレイヤ数に比例して計算量が増えるので重いと言った方が適切なんだろうな。
    • x_batch = x_train[:3]
>>> x = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> x[:3]
array([0, 1, 2])
>>> y = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> y[:3]
[0, 1, 2]

5章については最初はレイヤの定義が曖昧に見えて悩みました。結局最後まで読み進めた上でソースにデバッグプリント入れながら動きを確認することで、最終的には理解できてとてもスッキリしました。また4章までの部分も何度か読み直したので、5章は4章までを理解していないと読んでいてスッキリしないかもしれません。