『Python数値計算ノート』ではアフィリエイトプログラムを利用して商品を紹介しています。

バックプロパゲーション

損失関数の勾配

ニューラルネットワークは、ある層の出力値の線形結合を次の層に渡すことを繰り返すので、ネットワークからの出力ベクトル y には、入力ベクトル x と使用されたすべての重み情報が含まれることになります。損失関数 E は出力値 y と正解値 t を使って計算します。ネットワークは E が最小になるように重みを調整することで学習を進めます。十分に学習(訓練)を重ねたネットワークは、未知のデータが入力されたときに信頼性の高い予測値を示すことができるようになります。
 
ネットワークに学習させるためには、使用されたすべての重み w について、損失関数の偏微分 Ew を求める必要があります。

コンピュータで微分を計算する方法として、数値微分という手法が広く知られています。これは非常に小さな値 ϵ を設定して近距離にある 2 点の傾きを微分の近似値とする方法です。
 
Ew=E(w+ϵ)E(wϵ)2ϵ
しかしながら、実用的なニューラルネットワークでは、重みの総数が数百~数千、場合によっては数万というオーダーに達します。いくらコンピュータを使っていても、重み1つ1つについて数値微分で Ew を計算するのは大変な負荷がかかります。
 
そこで、数値微分よりも効率的に偏微分を処理するために、バックプロパゲーション(誤差逆伝播法)という手法を用います。

バックプロパゲーション(誤差逆伝播法)

バックプロパゲーション は、ネットワークからの出力値と正解値(目標変数)の差分を上層へ伝えることによって順次重みを調整していく手法です。
 
Python Backpropagation 1
深さ d 層の分類用ニューラルネットワークのオンライン学習を考えます。すなわち、バッチサイズ は 1 であり、一度に 1 個のデータを与えて重みの更新を行ないます。

損失関数は交差エントロピー誤差 E=ktklogyk です。
 
出力層の活性化関数は ソフトマックス関数 とします。
 
話を具体的にするために、出力層(第 d 層)は 3 個、その1つ上の層(第 d1 層)と2つ上の中間層(第 d2 層)は、それぞれ 2 個のニューロンで構成されるとします(下図参照)。
 
Python バックプロパゲーションの説明図2

出力層における損失関数の勾配

d1 層からの出力を x、重み行列を A とすると、第 d 層の入力総和 λ
 (1)λ=Ax
で表されます。成分表示すると次のようになります。
 (2)[λ0λ1λ2]=[a00a01a02a10a11a12a20a21a22][x0x1x2]
出力層における損失関数の勾配を求めてみましょう。
偏微分の連鎖律によって、Eaji は出力層の入力総和 λj を使って
 (3)Eaji=Eλjλjaji
と表すことができます。まず最初に、Eλj を求めてみましょう。正解値(目標変数)を
 t=[t0t1t2]
とすると、交差エントロピー誤差は
 (4)E=t0logy0t1logy1t2logy2
なので、Eλ0 で微分すると
 (5)Eλ0=t0y0y0λ0t1y1y1λ0t2y2y2λ0
出力層の活性化関数はソフトマックス関数
 (6)yi=exp(xi)i=1nexp(xi)
に設定しています。ソフトマックス関数の微分は
 (7)yixj={yiyj(ij)yi(1yi)(i=j)
で与えられます。式 (5) の続きを計算すると、
 (8)Eλ0=t0y0(1y0)y0t1(y1y0)y1t2(y2y0)y2=(t0+t1+t2)y0t0
正解値の合計は t0+t1+t2=1 となるので、
 (9)Eλ0=y0t0
が得られます。同様に
 (10)Eλ1=y1t1,Eλ2=y2t2
となるので、
 (11)Eλj=yjtj
となります。すなわち、交差エントロピー誤差の入力総和による微分は出力値 yj と目標変数 tj の差分となっています。この差分を δj と表すことにすると、出力層における勾配は
 (12)Eλjδj
となります。

次は λjaji の部分を求めてみます。たとえば j=0 のとき、
 (13)λ0=a00x0+a01x1+a02x2
なので、
 (14)λ0a00=x0,λ0a01=x1,λ0a02=x2
となります。j=1, 2 のときも同様なので、
 (15)λjaji=xi
が得られます。式 (3) に (12) と (15) を代入すると、
 (16)Eaji=δjxi
となります。交差エントロピー誤差の勾配が
 
 [出力値と正解値の差] × [1つ上の層からの入力値]
 
となっています。この計算はコンピュータにとっては単純な処理ですから、数千個の重みについて実行したとしても一瞬で終えることができます。

中間層における損失関数の勾配

次は第 d1 層の勾配 Ebji を求めます。
d2 層からの出力を z、重み行列を B とすると、第 d1 層の入力総和 μ
 (17)μ=Bz
と表されます。成分表記すると以下のようになります。
 (18)[μ0μ1]=[b00b01b02b10b11b12][z0z1z2]
偏微分の連鎖律によって、
 (19)Ebji=Eμjμjbji
と表すことができます。(18) を bji で微分すると、
 (20)μjbji=zi
となるので、(19) は
 (21)Ebji=Eμjzi
と表せます。

次に (19) の Eμj に連鎖律を適用すると
 (22)Eμj=Exjxjμj
と表せます。xjμj の部分は、xj が活性化関数 f(u) によって調整された値であることから、
 (23)xjμj=f(μj)μj=f(μj)
と表しておくことにすると(具体的な表式は設定した活性化関数によって異なります)、(22) は
 (24)Eμj=f(μj)Exj
となります。

Exj の計算はやや複雑です。Eλ0(x0, x1, x2), λ1(x0, x1, x2), λ2(x0, x1, x2) の関数なので、Exj で偏微分すると、これも連鎖律によって、
 (25)Exj=Eλ0λ0xj+Eλ1λ1xj+Eλ2λ2xj
と表せます。Eλj は先に求めた出力値と正解値の差分 δj なので(この項を通して δj の情報が中間層まで伝えられていることに着目してください)、(25) は
 (26)Exj=δ0λ0xj+δ1λ1xj+δ2λ2xj
となります。
 
λrxj (r=0,1,2) の部分は行列形式
 (27)[λ0λ1λ2]=[a00a01a02a10a11a12a20a21a22][x0x1x2]
の形のまま微分すると簡単です。たとえば x0 で微分すると、
 (28)x0[λ0λ1λ2]=[a00a01a02a10a11a12a20a21a22][100]=[a00a10a20]
となって、A0 列目が抜き出されます。同様に x1, x2 で微分すると 1 列目、2 列目が抜き出されるので、
 (29)xj[λ0λ1λ2]=[a0ja1ja2j]
と表せます。したがって、(26) は
 (30)Exj=a0jδ0+a1jδ1+a2jδ2
となります。(24) に代入すると、
 (31)Eμj=f(μj)(a0jδ0+a1jδ1+a2jδ2)
この式を一般化すると、
 (32)Eμj=f(μj)rarjδr
となります。r は出力層のニューロンの個数だけ和をとることを意味します(たとえば、10 個のニューロンがある場合は r=0 から r=9 まで和をとります)。
 
式 (21) より、この層の重みによる損失関数の勾配は
 (33)Ebji=zif(μj)rarjδr
となります。

重みの更新式

最後に層同士の関係を明確にするために、以下のように変数を置き換えます。
 (34)xj=yj(d1),zj=yj(d2)(35)λj=uj(d),μj=uj(d1)(36)aji=wji(d),bji=wji(d1)
右上の添字は層番号を意味します (たとえば、uj(d)d 層、すなわち出力層のニューロンの入力総和であることを表しています)。 ただし、最終的な出力値 yj、および正解値と出力値の差分 δj=tjyjについては、層番号を表示しないことにします(下図参照)。
 

新しい変数で Eaji を書き直すと次のようになります。
 
(37)Ewji(d)=yi(d1)δj
同様に、Ebji を書き直すと、
 (38)Ewji(d1)=yi(d2)f(uj(d1))rwrj(d)δr
となります。ここで、
 (39)f(uj(d1))rwrj(d)δr=δj(d1)
と定義すると、
 (40)Ewji(d1)=yi(d2)δj(d1)
となります。(37) と (40) より、出力層および、その1つ手前の中間層の重みの更新は
 (41)wji(d)(t+1)=wji(d)(t)αEwji(d)=wji(d)(t)αδjyi(d1)wji(d1)(t+1)=wji(d1)(t)αEwji(d1)=wji(d1)(t)αδj(d1)yi(d2)
で与えられます (α は学習率で正の値をとります)。同様に、さらに上の層も
 (42)wji(d2)(t+1)=wji(d2)(t)αδj(d2)yi(d3)
というように遡って計算してくことができます。これでバックプロパゲーションを Python に実装することができますが、次回記事では、更新式の意味するところをもう少し掘り下げて解説したいと思います。

【おすすめ書籍】ゼロから作る Deep Learning

ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装

中古価格
¥2,818から
(2019/7/30 20:24時点)

『ゼロから作る Deep Learning』は既製品を使わずに実際に動くコードをゼロから書きながら、ディープラーニングの仕組みを基礎から学ぶことができる本です。「ブラックボックスは使いたくない!」という硬派なプログラマーさんにおすすめの一冊です。

ゼロから作るDeep Learning ? ?自然言語処理編

中古価格
¥2,979から
(2020/8/20 11:36時点)

シリーズ二冊目は自然言語処理の話がメインとなります。私たちの話す言葉をコンピュータに理解させることは非常に困難ですが、ディープラーニングの手法で追求する動きが広まっています。Google の機械翻訳もその一例です。表題にある通り既製品を使わず『ゼロから作る』という方針で記述されているので、本書を読み通すことで、ディープラーニングの基礎理論の土台を築くことができます。

ゼロから作るDeep Learning ? ?フレームワーク編

中古価格
¥3,500から
(2020/8/20 11:38時点)

シリーズ三冊目では、なんと、ディープラーニングのフレームワークをゼロから作ります。TensorFlow や Chainer のようなインターフェースを自分で組み立てながら、フレームワークの構造について学ぶのです。本書では DeZero と名づけられたオリジナル・フレームワークを小さなステップに分けて作成していきます (DeZero は小規模なフレームワークですが十分に複雑です)。本書は読み通すことで、上級エンジニアとして、自身でフレームワークを設計する土台を築くことができます。

コメント

  1. HNaito より:

    下記は誤植と思われますので、ご確認ください。
    微分の近似式で、E(w+ϵ)−(w−ϵ) → E(w+ϵ)−E(w−ϵ)
     
    上から 2 番目の図で灰色のニューロンはバイアス ( =1 ) だと思うので、x2に入ってくる矢印は
    不要だと思います。

    • あとりえこばと より:

      ありがとうございます。記事を修正し、2番目の図も差し替えておきました m(_ _)m

  2. HNaio より:

    下記は誤植と思われますので、ご確認ください。
     
    式(6)で、-t2(−y2y0)/y1 → -t2(−y2y0)/y2

  3. HNaito より:

    下記は誤植と思われますので、ご確認ください。
     
    (15) 式で、z = Bμ → μ = Bz
    (17) 式の右辺で、∂μj/∂aji → ∂μj/∂bji
    (17) 式の下の文で、∂E/∂uj → ∂E/∂μj
    (19) 式の下の行列形式で、λ= は不要。そのまま残すのであれば太字にする。

  4. HNaito より:

    下記は誤植と思われますので、ご確認ください。
     
    (23)式で、∂μ/∂bji → ∂μj/∂bji

  5. HNaito より:

    下記は誤植と思われますので、ご確認ください。
     
    (21) 式の下の文で、
     
    式(18), (19), (21) より、
    ∂E/∂μj = ~
    となる。一般化すると、

    式 (19), (20), ∂E/∂λr=δr (r=0,1,2) より、
    ∂E/∂xj = ~
    となる。上の式と式 (21) を式 (18) に代入して一般化すると、

    • あとりえこばと より:

      ありがとうございます。記事全体を見直して、いくつかの式を追加し、数式の順序も変更しました(それに伴って式番号も変更されています)。これで重み更新式を導く過程が少しわかりやすくなったと思います。ぜひご確認ください。

      • HNaito より:

        式を整理していただいてずいぶん見通しがよくなりました。ありがとうございました。

        下記は誤植と思われますので、ご確認ください。
        (26) 式の上の文で、(23) は → (25) は

  6. HNaito より:

    下記は誤植と思われますので、ご確認ください。
    一番最後の式の左辺で、wji^(d-2) → wji^(d-2)(t+1)
    式(32)の α (学習率:正の数) の説明が必要と思いました。