からっぽのしょこ

読んだら書く!書いたら読む!読書読読書読書♪同じ事は二度調べ(たく)ない

5.4:単純なレイヤの実装【ゼロつく1のノート(実装)】

はじめに

 「プログラミング」学習初手『ゼロから作るDeep Learning』民のための実装攻略ノートです。『ゼロつく1』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 関数やクラスとして実装される処理の塊を細かく分解して、1つずつ処理を確認しながらゆっくりと組んでいきます。

 この記事は、5.4節「単純なレイヤの実装」の内容になります。乗算レイヤと加算レイヤの順伝播と逆伝播をPythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

5.4 単純なレイヤの実装

 この節ではリンゴとみかんの買い物を例として、乗算レイヤと加算レイヤの順伝播と逆伝播を実装していきます。

5.4.1 乗算レイヤの実装

 「1個100円のリンゴを2個買うときの支払い金額を求める。ただし消費税率は10%とする。」さていくらでしょうか?これは$100 * 2 * 1.1 = 220$ですぐに計算できますね。この例は簡単な計算式なのでそのまま行えますが、ニューラルネットワークの計算は複雑でしたよね。この項では、この簡単な問を例として乗算レイヤを実装します。

 この計算式を一度に行うのではなく、1要素ごとに分割します。またその計算手順をグラフ化したものが、図5-2になります。

 まずは各変数の値を用意します。

# リンゴの値段
apple = 100

# リンゴの個数
apple_num = 2

# 消費税
tax = 1.1


 リンゴの価格を$x$、リンゴの個数を$y$とすると$t = x y$の計算をします。

# 1つ目の計算
t = apple * apple_num
print(t)
200


 続いてこの計算結果$t$と税率$u$の積$z = t u$を求めます。

# 2つ目の計算(順伝播の出力)
z = t * tax
print(z)
220.00000000000003

 最初の計算通りの結果が出力されました(プログラム上の誤差があるけどね)。

 続いて逆伝播による微分を求めます。

 逆伝播の入力は$\frac{\partial z}{\partial z} = 1$でした。なので最初に、値が1の変数dzを用意します。

# 逆伝播の入力
dz = 1


 1つ前の信号$\frac{\partial z}{\partial z}$と、このレイヤの順伝播の出力$z$を入力$t,\ u$で微分した値$\frac{\partial z}{\partial t},\ \frac{\partial z}{\partial u}$との積$\frac{\partial z}{\partial z} \frac{\partial z}{\partial t},\ \frac{\partial z}{\partial z} \frac{\partial z}{\partial u}$を、このレイヤでは計算(出力)します。

 乗算の偏微分は変数を入れ替えればよいので

$$ \frac{\partial z}{\partial t} = u ,\ \frac{\partial z}{\partial u} = t $$

となります。

 このレイヤの計算では、リンゴの合計金額$t$の他に、消費税率$u$も変数です。こちらの微分をdtaxとします。

# 1つ目の計算
dt = dz * tax
dtax = dz * apple
print(dt)
print(dtax)
1.1
100


 2つ目の計算(出力)は、1つ前の信号$\frac{\partial z}{\partial z} \frac{\partial z}{\partial t}$と、このレイヤの順伝播の出力$t$を入力$x,\ y$で微分した値$\frac{\partial t}{\partial x},\ \frac{\partial t}{\partial y}$との積$\frac{\partial z}{\partial z} \frac{\partial z}{\partial t} \frac{\partial t}{\partial x},\ \frac{\partial z}{\partial z} \frac{\partial z}{\partial t} \frac{\partial t}{\partial y}$になります。

 先ほど同様に、それぞれ

$$ \frac{\partial t}{\partial x} = y ,\ \frac{\partial t}{\partial y} = x $$

となります。

# 2つ目の計算(逆伝播の出力)
dapple = dt * apple_num
dapple_num = dt * apple
print(dapple)
print(dapple_num)
2.2
110.00000000000001

 以上がこのレイヤの処理の流れです。

 それでは、順伝播メソッド、逆伝播メソッドを持つ乗算レイヤをクラスとして実装しましょう。

# 乗算レイヤの実装
class MulLayer:
    
    # インスタンス変数を定義
    def __init__(self):
        # 受け皿を作成
        self.x = None
        self.y = None
    
    # 順伝播メソッドを定義
    def forward(self, x, y):
        # インスタンス変数に値を代入
        self.x = x
        self.y = y
        
        # 乗算
        out = x * y
        
        return out
    
    # 逆伝播メソッドを定義
    def backward(self, dout):
        # 入力と偏微分の積
        dx = dout * self.y
        dy = dout * self.x
        
        return dx, dy # (受け取るときに変数が2つ必要!)


 最初に設定した値を使って動作確認を行います。

 まずは2層分のインスタンスを作成します。

## インスタンスを作成

# 第1層
mul_apple_layer = MulLayer()

# 第2層
mul_tax_layer = MulLayer()


 インスタンス変数の初期値を確認します。

# インスタンス変数を確認
print(mul_apple_layer.x)
print(mul_apple_layer.y)
None
None

 インスタンスを作成した時点では値を持ちません。

 では、それぞれの層の入力(変数)を引数に指定して、順伝播(税込み価格)の計算を行います。

# 1つ目の計算
apple_price = mul_apple_layer.forward(apple, apple_num)
print(apple_price)

# 2つ目の計算
price = mul_tax_layer.forward(apple_price, tax)
print(price)
200
220.00000000000003

 正しく計算できました!

 次は逆伝播(微分)の計算をします。逆伝播の入力の値は1でした。またそれぞれの層の引数には、1つ前の結果を渡します。

# 逆伝播の入力
dprice = 1

# 1つ目の計算
dapple_price, dtax = mul_tax_layer.backward(dprice)
print(dtax)

# 2つ目の計算
dapple, dapple_num = mul_apple_layer.backward(dapple_price)
print(dapple)
print(dapple_num)
200
2.2
110.00000000000001

 こちらも正しく計算できました!

 これで乗算レイヤを実装できました。続いて加算レイヤを実装します。

5.4.2 加算レイヤの実装

 5.4.1項の例に加えて「1個150円のミカンを3個買うときの支払い金額を求める。」場合を考えます。計算自体は$(100 * 2 + 150 * 3) * 1.1 = 715$になりますね。この計算も変数1つずつの計算に分解して行います(図5-17)。

 まずは追加する変数を用意します。その他の変数やインスタンスについては、5.4.1項のものをそのまま使います。

# オレンジの値段
orange = 150

# オレンジの個数
orange_num = 3


 乗算ノードについては、先ほど作ったクラスを使いましょう。

# 第1層のインスタンス
mul_orange_layer = MulLayer()

# みかんの合計金額を計算(第1層の計算)
orange_price = mul_orange_layer.forward(orange, orange_num)
print(orange_price)
450


 加算ノードでは、リンゴの合計金額を$x$、みかんの合計金額を$y$とすると、$t = x + y$の計算をします。

# 加算(第2層の計算)
all_price = apple_price + orange_price
print(all_price)
650


 最後に消費税の計算を行います。

# 乗算(第3層の計算)
price = mul_tax_layer.forward(all_price, tax)
print(price)
715.0000000000001

 この値が順伝播の出力となります。

 続いて逆伝播の計算をします。逆伝播の入力は$\frac{\partial z}{\partial z} = 1$でした。

# 逆伝播の入力
dout = 1

# 乗算レイヤの逆伝播を計算
dprice, dtax = mul_tax_layer.backward(dout)
print(dprice)
print(dtax)
1.1
650

 dpriceの値が次のレイヤに伝播します。

 加算レイヤ$t = x + y$の逆伝播では、$\frac{\partial t}{\partial x} = 1,\ \frac{\partial t}{\partial y} = 1$になります。これを伝播してきた値(1つ前の計算結果)との積を次のノードへ伝播します。

# 加算レイヤの逆伝播を計算
dapple_price = dprice * 1
dorange_price = dprice * 1
print(dapple_price)
print(dorange_price)
1.1
1.1


 (この例では同じ値ですが)それぞれ対応するノードに入力(伝播)します。

## 乗算レイヤの逆伝播を計算

dapple, dapple_num = mul_apple_layer.backward(dapple_price)
print(dapple)
print(dapple_num)

dorange, dorange_num = mul_orange_layer.backward(dorange_price)
print(dorange)
print(dorange_num)
2.2
110.00000000000001
3.3000000000000003
165.0

 各変数の微分が求まりました!

 では、加算レイヤの順伝播と逆伝播の計算処理をメソッドとして持つクラスを実装しましょう。

 今確認した通り、加算レイヤの逆伝播を計算はどちらの変数も1となり変数の値を保存しておく必要がありません。加算レイヤでは、インスタンス変数の必要がないため、passとして何もしません(何もしないことを明示しておきます)。

# 加算レイヤを実装
class AddLayer:
    
    # インスタンス変数を定義
    def __init__(self):
        # 何もしない
        pass
    
    # 順伝播メソッドを定義
    def forward(self, x, y):
        # 加算
        out = x + y
        return out
    
    # 逆伝播メソッドを定義
    def backward(sels, dout):
        # 入力と偏微分の積
        dx = dout * 1
        dy = dout * 1
        return dx, dy # (受け取るときに変数が2つ必要!)


 この節全体の動作確認として、もう一度変数の定義から行います。

## 各変数の値を指定

# 第1層
apple = 100
apple_num = 2
orange = 150
orange_num = 3

# 第3層
tax = 1.1

## レイヤ(インスタンス変数)を作成

# 第1層
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()

# 第2層
add_apple_orange_layer = AddLayer()

# 第3層
mul_tax_layer = MulLayer()

 (勿論値を変えてみて理解の確認をするためでもあります!)

 まずは順伝播の計算をします。

## 順伝播の計算

# 第1層
apple_price = mul_apple_layer.forward(apple, apple_num)
orange_price = mul_orange_layer.forward(orange, orange_num)
print(apple_price, orange_price)

# 第2層
all_price = add_apple_orange_layer.forward(apple_price, orange_price)
print(all_price)

# 第3層
price = mul_tax_layer.forward(all_price, tax)
print(price)
200 450
650
715.0000000000001


 次に逆伝播を計算です。

## 逆伝播を計算

# 入力
dprice = 1

# 第3層
dall_price, dtax = mul_tax_layer.backward(dprice)
print(dall_price, dtax)

# 第2層
dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price)
print(dapple_price, dorange_price)

# 第1層
dapple, dapple_num = mul_apple_layer.backward(dapple_price)
dorange, dorange_num = mul_orange_layer.backward(dorange_price)
print(dapple, dapple_num)
print(dorange, dorange_num)
1.1 650
1.1 1.1
2.2 110.00000000000001
3.3000000000000003 165.0

 正しく出力できていますね。

 以上で掛け算と足し算のレイヤの順伝播と逆伝播を実装できました。わざわざクラスとして実装するのは面倒でしたか?これは簡単な例なので、そう感じるのは当然かもしれません。しかし計算自体とそれを組み合わせたアルゴリズムを分けて実装するのは、メンテナンスの面で重要です。
 この例でも、addなのかmulなのか、fowardなのかbackwardなのか明確ですよね。

 これで簡単な計算の順伝播と逆伝播を実装できました。次は少し複雑な関数を扱いましょう!次節からは活性化関数の順伝播と逆伝播を実装します。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning』オライリー・ジャパン,2016年.

おわりに

 順調です!

【次節の内容】

https://www.anarchive-beta.com/entry/2020/07/31/180000www.anarchive-beta.com