からっぽのしょこ

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

4.2.3:シグモイド関数と交差エントロピー誤差【ゼロつく2のノート(実装)】

はじめに

 『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 本の内容を1つずつ確認しながらゆっくりと組んでいきます。

 この記事は、4.2.3項「シグモイド関数と交差エントロピー誤差」の内容です。Sigmoid with Lossレイヤを説明して、Pythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

4.2.3 シグモイド関数と交差エントロピー誤差

・数式による確認

 多値分類では、活性化関数にSoftmax関数を用いてスコアを正規化し、交差エントロピー誤差を損失としました。二値分類では、活性化関数にSigmoid関数を用いてスコアを正規化し、交差エントロピー誤差を損失とします。Sigmoid関数については1.2.1項または1巻の3.2節を参照してください。

 要素(クラス)数が2つ($H = 2$)のデータが1つ($N = 1$)の場合の交差エントロピー誤差の計算式は、次の式になります(1.3.1項)。

$$ \begin{aligned} L &= - \sum_{k=1}^2 t_k \log y_k \\ &= - ( t_1 \log y_1 + t_2 \log y_2 ) \end{aligned} $$

 このときone-hot表現のターゲット(教師ラベル)$\mathbf{t} = (t_1, t_2)$の2つ目の要素は、$t_2 = 1 - t_1$と表現できます。$t_1$が1のとき$t_2$は0、$t_1$が0のとき$t_2$は1になりますね。スコアを正規化した値$\mathbf{y} = (y_1, y_2)$は$\sum_{k=1}^2 y_k = 1$なので、こちらも2つ目の要素は$y_2 = 1 - y_1$と表現できます。これは$y_1 + y_2 = 1$の$y_1$を右辺に移項したと解釈できます。
 よって上の計算式に代入すると、交差エントロピー誤差は

$$ L = - \{ t_1 \log y_1 + (1 - t_1) \log (1 - y_1) \} \tag{4.3} $$

$t_1$と$y_1$から計算できることが分かります。

 つまりCBOWモデルの2つ目の改良点は、Enbedding Dotレイヤ(出力層)でターゲットの単語に関するスコアを計算し、Sigmoid with Lossレイヤ(活性化層と損失層)で確率に変換して損失を求めることです。

 ここまでの処理までの処理がSigmoid with Lossレイヤの順伝播になります。

 逆伝播については、スコア$\mathbf{s}$に関する勾配$\frac{\partial L}{\partial \mathbf{s}}$を求めます。

$$ \frac{\partial L}{\partial \mathbf{s}} = \begin{pmatrix} \frac{\partial L}{\partial s_0} & \frac{\partial L}{\partial s_1} & \cdots & \frac{\partial L}{\partial s_5} \end{pmatrix} $$

 この$k$番目の要素は、次の式で計算できます。

$$ \frac{\partial L}{\partial s_k} = \frac{\partial L}{\partial y_k} y_k (1 - y_k) \tag{A.4} $$


 ただしバッチデータを入力する場合は、交差エントロピー誤差の計算においてバッチサイズ(ここでは$N$とします)で割る必要があります。そのため勾配においても($\frac{1}{N}$も伝播するため)バッチサイズで割る必要があるため

$$ \frac{\partial L}{\partial s_k} = \frac{1}{N} \frac{\partial L}{\partial y_k} y_k (1 - y_k) \tag{A.4'} $$

となります。詳しくは「ソフトマックス関数と交差エントロピー誤差の逆伝播【ゼロつく1のノート(数学)】 - からっぽのしょこ]」を参考にしてください。

 次項でEmbedding Dotレイヤを、次々項でSigmoid with Lossレイヤを実装します。

・Sigmoid with Lossレイヤの実装

 二値分類で用いるSigmoid with Lossレイヤを実装します。

 Sigmoid with Lossレイヤでは、これまでに実装した関数cross_entropy_error()(1.3.1項)を利用します。そのため関数定義を再実行するか、次の方法で読み込む必要があります。

# 読み込み用の設定
import sys
sys.path.append('C://Users//「ユーザー名」//Documents//・・・//deep-learning-from-scratch-2-master')

# 実装済みの関数をインポート
from common.functions import cross_entropy_error


・処理の確認

 前項で計算したスコアsをSigmoid関数(1.5)により正規化します。

# スコア(Embedding Dotレイヤの出力)
print(np.round(s, 3))

# 確率に変換:式(1.5)
y = 1 / (1 + np.exp(-s))
print(np.round(y, 3))
print(np.sum(y))
[-0.394 -0.356 -0.503  1.145  2.56   0.097]
[0.403 0.412 0.377 0.759 0.928 0.524]
3.4026629629938796

 出力データ(正規化後の値)yの各要素は、それぞれ入力データ(コンテキスト)ごとの正解ラベルに関する要素に対応しています。そのためyの総和は1にはなりません。

 np.c_[]を使って、配列を列方向に結合することができます。

# 配列を作成
a = np.array([1, 3, 5, 7])
b = np.array([2, 4, 6, 8])

# 配列を結合
ab = np.c_[a, b]
print(ab)
[[1 2]
 [3 4]
 [5 6]
 [7 8]]

 この関数で、1 - yyの2列の配列を作成します。

 正解の要素はyの列なので、全ての行(データ)において正解ラベルは1になります。そこでnp.ones_like()を使って、ターゲットtargetと同じサイズで全ての要素が1の配列tを作成して、教師ラベル$\mathbf{t}$とします。

 1.3.1項で実装した関数cross_entropy_errorを使って、交差エントロピー誤差を計算します。

# 二値分類用の教師ラベルを作成
t = np.ones_like(target)
print(t)

# 交差エントロピー誤差を計算:式(1.8)
loss = cross_entropy_error(np.c_[1 - y, y], t)
print(loss)
[1 1 1 1 1 1]
0.6280905395595118

 この値が損失であり、順伝播における最終的な出力です。

 続いて逆伝播の処理を確認します。

 Sigmoid with Lossレイヤの逆伝播の入力は$\frac{\partial L}{\partial L} = 1$です。バッチデータを入力とする場合は、式(A.4)を更にバッチサイズで割ります。

# 逆伝播の入力
dL = 1

# バッチサイズを取得
batch_size = t.shape[0]
print(batch_size)

# 勾配を計算:式(A.4)
ds = (y - t) * dL / batch_size
print(np.round(ds, 3))
6
[-0.1   -0.098 -0.104 -0.04  -0.012 -0.079]

 この値がEmbedding Dotレイヤ(出力層)に伝播します。

・実装

 処理の確認ができたので、Sigmoid with Lossレイヤをクラスとして実装します。

# Sigmoid with Lossレイヤの実装
class SigmoidWithLoss:
    
    # 初期化メソッドの定義
    def __init__(self):
        self.params = [] # パラメータ
        self.grads = []  # 勾配
        self.loss = None # 損失
        self.y = None # 正規化後の値
        self.t = None # 教師ラベル
    
    # 順伝播メソッドの定義
    def forward(self, x, t):
        self.t = t # 教師ラベル
        
        # 確率に変換
        self.y = 1 / (1 + np.exp(-x)) # シグモイド関数:式(1.5)
        
        # 交差エントロピー誤差を計算
        self.loss = cross_entropy_error(np.c_[1 - self.y, self.y], self.t) # 1.3.1項
        
        return self.loss
    
    # 逆伝播メソッドの定義
    def backward(self, dout=1):
        # バッチサイズを取得
        batch_size = self.t.shape[0]
        
        # 勾配を計算
        dx = (self.y - self.t) * dout / batch_size # :式(A.4)
        
        return dx


 実装したクラスを試してみましょう。

 インスタンスを作成して、順伝播の処理を行います。

# Sigmoid with Lossレイヤのインスタンスを作成
loss_layer = SigmoidWithLoss()

# 二値分類用の教師ラベルを作成
t = np.ones_like(target)
print(t)

# 順伝播メソッドを計算
loss = loss_layer.forward(s, t)
print(loss)
[1 1 1 1 1 1]
0.6280905395595118

 損失を出力できました。

 続いて逆伝播の処理を行います。

# 逆伝播の入力
dL = 1

# 逆伝播メソッドを計算
ds = loss_layer.backward(dout=dL)
print(np.round(ds, 3))
[-0.1   -0.098 -0.104 -0.04  -0.012 -0.079]


 以上で活性化層と損失層を実装できました。ただしここまでは「正解の単語(データ)」のみを扱ってきました。次項では、「不正解の単語(データ)」サンプリングして損失に加える方法を説明します。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning 2――自然言語処理編』オライリー・ジャパン,2018年.

おわりに

 Sigmoid with Lossレイヤの逆伝播の導出は、巻末付録にて詳しく書いてあることもあり飛ばしてしましました。後々Sigmoid関数とSoftmax関数の関係と一緒にまとめるつもりではいます。

【次節の内容】

https://www.anarchive-beta.com/entry/2020/10/10/190000www.anarchive-beta.com