ニューラルネットワークの内部では何が行われている?作って試そう! ディープラーニング工作室(2/3 ページ)

» 2020年04月21日 05時00分 公開
[かわさきしんじDeep Insider編集部]

重みとバイアスを使った計算の実際

 その前に、このニューラルネットワークの構造について振り返りましょう。以下は入力層から出力層までに、このニューラルネットワークで計算される値の流れを示したものです。特に入力層のノードから隠れ層の最初のノード(y1)へと渡される(伝播される)値に着目して、その計算に必要な「重み」と「バイアス」を書き込んであります。

入力層から隠れ層の先頭のノードへ出力される値を決定する処理 入力層から隠れ層の先頭のノードへ出力される値を決定する処理

 ここで注目してほしいことは幾つかあります。まず、隣り合う層の全てのノードがエッジ(ノードをつなぐ線)で接続(結合)されていることです。このような結合のことを「全結合」などと呼びます。

 また、入力層には5つのノードがありますが、そのx1〜x4は入力変数です。つまりこのニューラルネットワークが受け取るがく片の長さ/幅、花弁の長さ/幅はこれらの変数に渡されます。x0は、次の層に伝播される値を計算するときに使用されるバイアスに対応したものです。この値は通常「1」と考えます(バイアスがある場合)。この値が「1」ならば、バイアスをw11などの重みと同様に扱おうとしたときに、バイアスの値が常に出力値の計算に使われるようになります(各層からの出力値を計算する際に重みとバイアスを1つの行列にまとめられるという効果もありますが、これについてはここでは説明しません)。隠れ層と出力層も同様です。前の層から伝播される値を受け取るノード(と隠れ層ではバイアスに対応するノード)があります。

 上の図におけるwijは、「重み」を意味する「w」に、値を受け取る側のノードの番号(y1なら「1」)と値を出力する側ノードの番号(x2なら「2」)を表す2つの数値が付加されたものです。つまり、x2からy1へ出力される値を計算するときに使用する値はw12のように表されます。

 このように表現すると、隠れ層の最初のノード(y1)が(活性化関数を介して)受け取る値は次のように計算できます(先に述べましたが、多くの場合、x0は通常1であり、「b1×x0」ではなく、単に「b1」のように記述することもよくあります)。なお、実際にはこの計算結果を活性化関数でさらに変換したものが次の層への入力となることには注意してください。

 w11 × x1 + w12 × x2 + w13 × x3 + w14 × x4 + b1 × x0

 この計算は、隠れ層の1つのノードについてのみ行ったものであることにも注意が必要です。実際には、残る4つのノードについても同様な計算を行うことで、入力層から出力される値が決まります。隠れ層からの出力についても同様です。

 今までに見てきたNetクラスのインスタンス変数fc1とfc2は、ある層と次の層がどのように結合されるかを表しています。以下に__init__メソッドのコードを示します。

def __init__(self):
    super().__init__()
    self.fc1 = nn.Linear(INPUT_FEATURES, HIDDEN)
    self.fc2 = nn.Linear(HIDDEN, OUTPUT_FEATURES)

__init__メソッド

 例えば、fc1は「self.fc1 = nn.Linear(INPUT_FEATURES, HIDDEN)」のように初期化されています。これは「前の層(入力層)のノード数がINPUT_FEATURES(4)で、次の層(隠れ層)のノード数がHIDDEN(5)であり、これらが全結合される」ことを意味しています。fc2であれば「前の層(隠れ層)のノード数はHIDDEN(5)で、次の層(出力層)のノード数がOUTPUT_FEATURES(1)であり、これらが全結合される」となります。ある層と次の層が「全結合」であることは、Linearクラス(torch.nn.Linearクラス)のインスタンスを生成しているところから分かります(というか、PyTorchでは一般に2つの層が全結合する場合にはこのクラスを使用するということです)。

 なお、2つのインスタンス変数には何気なく「fc1」「fc2」という名前を付けていましたが、これらの「fc」は「全結合」(full connected)を意味しています。

 第2回でも述べましたが、実際の計算がどのように行われるかは、forwardメソッドに記述されています。

def forward(self, x):
    x = self.fc1(x)
    x = torch.sigmoid(x)
    x = self.fc2(x)
    return x

forwardメソッドではニューラルネットワーク内部で行う計算が定められる

 ここでは、__init__メソッドで定義したインスタンス変数fc1とfc2を、「self.fc1(x)」「self.fc2(x)」のように、それがあたかもメソッドであるかのように呼び出しています。これはLinearクラスのインスタンスが「呼び出し可能オブジェクト」だからです(詳細については本稿の末尾に掲載するコラムを参照のこと)。ここでは、Linearクラス(に限らず、PyTorchのニューラルネットワークモジュールクラス)のインスタンスは関数やメソッドのように呼び出せることだけを覚えておいてください。

 forwardメソッドの最初の行だけに注目してみましょう。実は、PyTorchでは「self.fc1(x)」のような呼び出しを行うと、多くの場合は回り回って、そのクラスで定義されているforwardメソッドが呼び出されるようになっています。Linearクラスのforwardメソッドでは、何らかの計算(上で述べたのと同様な計算)をして、その結果を返します。そして、その値が活性化関数を経由して、次の層へと渡されます。このように入力層から出力層の方向へとニューラルネットワークによる計算結果が渡されていくことを「順伝播」(forward propagation)と呼びます。forwardメソッドはこうした処理を実行することから、このような名前が付いたのでしょう。

 それはともかく、ここでいいたいのは、1行目と「net.fc1.forward(x)」では(この場合は)同じ処理が行われるということです。そこで、先ほど分割したデータセットの訓練データから先頭の要素だけを取り出して、それをLinearクラスのforwardメソッドに渡してみましょう(もちろん、「net.fc1(x)」でも同様です。が、ここではあえて明示的にforwardメソッドを呼び出してみます)。

x = X_train[0]
print('x:', x)

result = net.fc1.forward(x)
print(result)

Linearクラスで定義されているforwardメソッドを呼び出してみるコード

 実行結果を以下に示します。

実行結果 実行結果

 net.fc1.forwardメソッドに4つの数値からなる訓練データを1個渡すことで、5つの要素からなる配列(テンソル)が得られました。これらを活性化関数に通したものが、ノードを5つ(とバイアスに対応する1つのノードを)持つ隠れ層への入力となります。

 では、これと同じことを自分の手でやってみましょう。その前に、もう一度、以下のコードを実行して、入力層の重みとバイアスを確認しておきます。

print('weight')
print(net.fc1.weight)
print('bias')
print(net.fc1.bias)

入力層の重みとバイアスを確認

 以下の実行結果で注目してほしいのは、重み(weight)が5行4列の行列(2次元配列)になっていることと、バイアスが5つの要素からなるベクトル(1次元配列)となっているところです。重みをまとめた行列では、隠れ層の個々のノードが受け取る値の計算に使われる重みが、各行に(4つの数値を要素とする配列として)まとめられています。

 隠れ層の最初のノード(先ほどの図ならy1)に関する計算では、この行列の最初の要素である配列が使われて、その配列の先頭要素が入力層の最初のノード(先ほどの図ならx1)から出力される値を計算するのに使われるといった具合です(つまり、w11に相当します)。一方、バイアスを含む配列では、隠れ層の各ノードが受け取る値を計算する際に、対応する要素が使われます(最初のノードに関する計算では、バイアスを格納している配列の最初の要素が使われるといった具合です)。

重みの使われ方 重みの使われ方

 そこで、ここではweight属性の値を変数wに代入しておきましょう。バイアスの値は変数bに代入しておきます。入力層に与えられた値は、既に見た通り変数xに代入されています。

w = net.fc1.weight
b = net.fc1.bias
# x = X_train[0]

入力層の重みを変数wに、入力層のバイアスを変数bに代入しておく

 すると、隠れ層の最初のノード(y1)が受け取る値は次のように計算できます。

 w[0][0] * x[0] + w[0][1] * x[1] + w[0][2] * x[2] + w[0][3] * x[3] + b[0]

 この式と先ほど示した、隠れ層の最初のノードが受け取る値の式を比べてみてください(x0は通常「1」であり、b1と書いても構わないことに注意)。

 w11 × x1 + w12 × x2 + w13 × x3 + w14 × x4 + b1 × x0

 これまでに示してきた図では重みや入力変数などを1始まりの添字で表現していました。しかし、Pythonではインデックスは0で始まります。そのため、インデックスと添字がズレてしまっていますが、それを除けば、同じ形式になっていることが分かります。他のノードへの出力を計算するときも同様です。では、実際に先ほどforwardメソッドを呼び出したときに得られた結果と上記の式を使った計算結果が等しくなるかを見てみましょう。

o0 = w[0][0] * x[0] + w[0][1] * x[1] + w[0][2] * x[2] + w[0][3] * x[3] + b[0]
o1 = w[1][0] * x[0] + w[1][1] * x[1] + w[1][2] * x[2] + w[1][3] * x[3] + b[1]
o2 = w[2][0] * x[0] + w[2][1] * x[1] + w[2][2] * x[2] + w[2][3] * x[3] + b[2]
o3 = w[3][0] * x[0] + w[3][1] * x[1] + w[3][2] * x[2] + w[3][3] * x[3] + b[3]
o4 = w[4][0] * x[0] + w[4][1] * x[1] + w[4][2] * x[2] + w[4][3] * x[3] + b[4]
print(o0.data, o1.data, o2.data, o3.data, o4.data)

「net.fc1.forward」メソッドの呼び出しと同じことを手作業で行うコード

 以下に実行結果を示します。

実行結果 実行結果

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。