LoginSignup
3
1

More than 3 years have passed since last update.

「ゼロから作るDeep Learning」自習メモ(その10)MultiLayerNet クラス

Last updated at Posted at 2020-10-12

「ゼロから作るDeep Learning」(斎藤 康毅 著 オライリー・ジャパン刊)を読んでいる時に、参照したサイト等をメモしていきます。 その9 ← → その11

5章でレイヤでの実装を説明した後、6章以降ではプログラム自体の説明をあまりやらなくなる。
プログラム例は、最初にダウンロードしたファイルにあるから、自分で実行して、内容を確認しなさい、ということなのだろうが、初心者にはけっこう大変。

まあ、ぼちぼちいきます。

6章の MultiLayerNet クラスの内容を確認してみる

3章でニューラルネットの基本的な説明があり、4章で2層ニューラルネットワークのクラスTwoLayerNet が実装されました。その後、いろんな説明があって、MultiLayerNetクラスになったわけです。すごく複雑になったように見えますが、基本部分は TwoLayerNet と変わってません。このクラスが参照しているライブラリ layers.py の内容を見ると、TwoLayerNetクラスで使っているものと同じです。
複雑そうに見えるのは、
プログラムの汎用性を高めるためにレイヤ単位の実装にした
活性化関数、パラメータ更新手法、重みの初期値等を選択できるようにした
からのようです。

プログラムを理解したいときは、1行ずつ手動トレースしていくのが確実。

というわけで、P192のプログラムをトレースしてみます。

ニューラルネットオブジェクトnetwork を生成する

weight_decay_lambda = 0.1

network = MultiLayerNet(input_size=784, 
                        hidden_size_list=[100, 100, 100, 100, 100, 100],
                        output_size=10,
                        weight_decay_lambda=weight_decay_lambda)

input_size=784 というのは、要素数784個のMNISTデータを使うということ
output_size=10 というのは、認識した結果は10通りになるということ

hidden_size_list=[100, 100, 100, 100, 100, 100]
によって、networkオブジェクトの中が、どのようになるかというと

multi_layer_net.py の MultiLayerNet の定義にある、初期化で

    def __init__(self, input_size, hidden_size_list, output_size,
                 activation='relu', weight_init_std='relu', weight_decay_lambda=0):
        self.input_size = input_size
        self.output_size = output_size
        self.hidden_size_list = hidden_size_list
        self.hidden_layer_num = len(hidden_size_list)
        self.weight_decay_lambda = weight_decay_lambda
        self.params = {}

        # 重みの初期化
        self.__init_weight(weight_init_std)

オブジェクト生成のところでは省略していたけれど
activation='relu' 活性化関数は relu を使う
weight_init_std='relu' 重みの初期値は relu と相性がいい Heの初期値 を使う
self.hidden_layer_num = len(hidden_size_list) リストhidden_size_listの要素の数だけ隠れ層のレイヤを作る、
ということになっています。

レイヤを生成する

ということで、要素の数だけ forループします

        # レイヤの生成
        activation_layer = {'sigmoid': Sigmoid, 'relu': Relu}
        self.layers = OrderedDict()
        for idx in range(1, self.hidden_layer_num+1):
            self.layers['Affine' + str(idx)] = Affine(self.params['W' + str(idx)],
                                                      self.params['b' + str(idx)])
            self.layers['Activation_function' + str(idx)] = activation_layer[activation]()

これの最後に 出力層last_layer として
SoftmaxWithLoss
が付け加わります。

        idx = self.hidden_layer_num + 1
        self.layers['Affine' + str(idx)] = Affine(self.params['W' + str(idx)],
            self.params['b' + str(idx)])

        self.last_layer = SoftmaxWithLoss()

つまり、隠れ層が6つ+出力層1つで、7層のネットワークになります。
リスト layers の内容は、次のようになります。

OrderedDict([
('Affine1', Affine(params[W1],params[b1])),
('Activation_function1', Relu),
('Affine2', Affine(params[W2],params[b2])),
('Activation_function2', Relu),
('Affine3', Affine(params[W3],params[b3])),
('Activation_function3', Relu),
('Affine4', Affine(params[W4],params[b4])),
('Activation_function4', Relu),
('Affine5', Affine(params[W5],params[b5])),
('Activation_function5', Relu),
('Affine6', Affine(params[W6],params[b6])),
('Activation_function6', Relu),
('Affine7', Affine(params[W7],params[b7]))
])

レイヤ単位で実装したことで、hidden_size_listの要素数で、隠れ層の数を指定できるようになっているのがわかります。6層くらいなら、TwoLayerNetクラスのようにプログラム内で層を増やしていくこともできますが、これが100とかになったら、まずムリ。

学習させてみる

このネットワークオブジェクトにMNISTデータを与えて学習させます。

optimizer = SGD(lr=0.01)

for i in range(1000000000):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    grads = network.gradient(x_batch, t_batch)
    optimizer.update(network.params, grads)

ミニバッチのループの中の
grads = network.gradient(x_batch, t_batch)
で、勾配を求めています
grads の中身は、こんな感じ

{
'W1': array([[-0.00240062, -0.01276378, 0.00096349, ..., 0.0054993 ],
[-0.00232299, -0.0022137 , 0.0036697 , ..., -0.00693252],
...,
[-0.00214929, 0.00358515, -0.00982791, ..., 0.00342722]]),
'b1': array([-4.51501921e-03, 5.25825778e-03, ..., -8.60827293e-03]),
'W2': array([[ 0.00394647, -0.01781943, 0.00114132, ..., 0.0029042 ],
[-0.00551014, 0.00238989, 0.01442266, ..., 0.00171659],
...,
[ 0.00279524, 0.01496588, 0.01859664, ..., -0.02194152]]),
'b2': array([ 2.08738753e-03, -8.28071395e-03, ..., 1.22945079e-02]),
'W3': array([[ ..., ]]),
'b3': array([ ..., ]),
'W4': array([[ ..., ]]),
'b4': array([ ..., ]),
'W5': array([[ ..., ]]),
'b5': array([ ..., ]),
'W6': array([[ ..., ]]),
'b6': array([ ..., ]),
'W7': array([
[ 6.72420338e-02,3.36979669e-04,1.26773417e-02,-2.30916938e-03, -4.84414774e-02,
-2.58458587e-02,-5.26754173e-02,3.61136740e-02,-4.29689699e-03, -2.85799599e-02],
[ ...],
[-1.68008362e-02, 6.87882255e-03, -3.15578291e-03, -8.00362948e-04, 8.81555008e-03,
-9.23032804e-03,-1.83337109e-02, 2.17933554e-02, -6.52331525e-03, 1.50930257e-02]
]),
'b7': array([ 0.11697053, -0.02521648, 0.03697393, -0.015763 , -0.0456317 ,
-0.03476072, -0.05961871, 0.0096403 , 0.03581566, -0.01840983])
}

最後のgrads['W7']の内容には、softmax関数で出力した数字0 ~ 9 のどれであるかの確率を10個の要素のリストにして、読み込ませた​訓練データの行数分並んでいます。
そして

    optimizer.update(network.params, grads)

commonフォルダにあるライブラリ optimizer.py の関数SGDのupdateメソッドで、パラメータparamsの内容からgradsの内容を引いて更新します。上の例では、SGD手法で更新しています。ライブラリにはSGDのほかにMomentum、AdaGrad、Adam、RMSpropとかが定義されています。

更新した結果のparamsは、次のバッチ処理に引き継いで使うので、バッチがループする分、学習が進んでいきます。

gradientメソッドは何をしているか

じゃあ、このgradientメソッドは何をしているかというと、重みパラメータの勾配を誤差逆伝播法で求めています。
まず、順方向で損失関数の値を計算し、それから、networkオブジェクトを創生したときに設定したレイヤを、逆方向に辿って勾配を求めます。

    def gradient(self, x, t):
        # forward
        self.loss(x, t)

        # backward
        dout = 1
        dout = self.last_layer.backward(dout)

        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 設定
        grads = {}
        for idx in range(1, self.hidden_layer_num+2):
            grads['W' + str(idx)] = self.layers['Affine' + str(idx)].dW + self.weight_decay_lambda * self.layers['Affine' + str(idx)].W
            grads['b' + str(idx)] = self.layers['Affine' + str(idx)].db

        return grads

最初に、
self.loss(x, t)
とあるのが、よくわかりませんでした。
関数を実行していますが、その結果を次に利用してるように見えませんから。
なので、その中身をトレースしてみました。
実行しているのは、multi_layer_net.pyで定義している関数 loss です。

損失関数lossをトレースしてみた

network.loss(x_batch, t_batch)

62.09479496490768

    def loss(self, x, t):
        y = self.predict(x)
        weight_decay = 0
        for idx in range(1, self.hidden_layer_num + 2):
            W = self.params['W' + str(idx)]
            weight_decay += 0.5 * self.weight_decay_lambda * np.sum(W ** 2)

        return self.last_layer.forward(y, t) + weight_decay

loss関数の中では、predictで、入力データから結果 y を予測させています。この中で、Affine1からAffine7までのレイヤのforwardメソッドが実行されていきます。

    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        return x

過学習を防ぐために重み(params['W1']等)から weight_decay を計算し、これを加えて
出力します。

weight_decay

59.84568388277881

network.last_layer.forward(y, t_batch)

2.2491110821288687

self.last_layer.forward(y, t) は、MultiLayerNetクラスの初期化のところで、

self.last_layer = SoftmaxWithLoss()

と定義してあるので、 実際に実行しているのはlayers.pyで定義している SoftmaxWithLoss()のforwardメソッドです。

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None
        self.y = None # softmaxの出力
        self.t = None # 教師データ

    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)

        return self.loss

    def backward(self, dout=1):
        batch_size = self.t.shape[0]
        if self.t.size == self.y.size: # 教師データがone-hot-vectorの場合
            dx = (self.y - self.t) / batch_size
        else:
            dx = self.y.copy()
            dx[np.arange(batch_size), self.t] -= 1
            dx = dx / batch_size

        return dx

で、このforwardメソッドの中で、交差エントロピー誤差(cross entropy error)を計算して返しています。

network.last_layer.loss

2.2491110821288687

from common.functions import *
cross_entropy_error(network.last_layer.y, network.last_layer.t)

2.2491110821288687

と言う事で、self.loss(x, t) で何を参照して、何をやっているかはわかりました。

で、

SoftmaxWithLoss関数は、この後、勾配を求めるために誤差逆伝播法で backwardメソッドを使うことになるわけです。その中で self.y とか self.t とかを参照していますが、これらは forwardメソッドを実行したときにセットされる変数です。
つまり、最初にself.loss(x, t)とあるのは、損失関数を求めているのではなく、誤差逆伝播法で backwardメソッドを使うための準備だった、ということです。

後ろに戻るためには、先に前に進んでおかなければいけないと言う、まあ、分かって見れば、当たり前の話なんですが。

backward で勾配を求める

self.loss(x, t) を実行して、入力したデータからの予測値等をセットしたら、誤差逆伝播法で勾配を求めます。

        # backward
        dout = 1
        dout = self.last_layer.backward(dout)

self.last_layer.backward(dout) はSoftmaxWithLoss.backward()のことです。
doutには、予測値y と、教師ラベルt の差のリストが返されます。
[y1 - t1, y2 - t2, y3 - t3, ・・・ , y100 - t100]

        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

layers.reverse()で、積み重ねたレイヤを逆順にして、dout = layer.backward(dout)を繰り返して、勾配を求めています。繰り返しを展開すると、こうなります。

dout = layers[0].backward(dout)  #Affine7
dout = layers[1].backward(dout)  #Activation_function6 Relu
dout = layers[2].backward(dout)  #Affine6
dout = layers[3].backward(dout)  #Activation_function5 Relu
dout = layers[4].backward(dout)  #Affine5
dout = layers[5].backward(dout)  #Activation_function4 Relu
dout = layers[6].backward(dout)  #Affine4
dout = layers[7].backward(dout)  #Activation_function3 Relu
dout = layers[8].backward(dout)  #Affine3
dout = layers[9].backward(dout)  #Activation_function2 Relu
dout = layers[10].backward(dout) #Affine2
dout = layers[11].backward(dout) #Activation_function1 Relu
dout = layers[12].backward(dout) #Affine1

各Affineレイヤ内で参照する self.x self.W は、forwardメソッドを実行したときにセットされたものを使っています。

class Affine:
    def __init__(self, W, b):
        self.W =W
        self.b = b

        self.x = None
        self.original_x_shape = None
        # 重み・バイアスパラメータの微分
        self.dW = None
        self.db = None

    def forward(self, x):
        # テンソル対応
        self.original_x_shape = x.shape
        x = x.reshape(x.shape[0], -1)
        self.x = x

        out = np.dot(self.x, self.W) + self.b

        return out

    def backward(self, dout):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)

        dx = dx.reshape(*self.original_x_shape)  # 入力データの形状に戻す(テンソル対応)
        return dx

各レイヤで求めたdw、dbを使って、各レイヤの重みとバイアスの勾配をセットして、関数の値として返します。

        # 設定
        grads = {}
        for idx in range(1, self.hidden_layer_num+2):
            grads['W' + str(idx)] = self.layers['Affine' + str(idx)].dW + self.weight_decay_lambda * self.layers['Affine' + str(idx)].W
            grads['b' + str(idx)] = self.layers['Affine' + str(idx)].db

返された勾配で、パラメータを更新して、ミニバッチの処理が1回終わります。

    grads = network.gradient(x_batch, t_batch)
    optimizer.update(network.params, grads)
class SGD:
    def __init__(self, lr=0.01):
        self.lr = lr

    def update(self, params, grads):
        for key in params.keys():
            params[key] -= self.lr * grads[key] 

lr はlearning rate( 学習係数)
この例では、0.01が設定されている。

MultiLayerNetExtendクラス

multi_layer_net_extend.py にあるMultiLayerNetExtendクラスは、レイヤ生成のところで Dropout、Batch Normalization に対応していますが、基本的なところはMultiLayerNetと同じです。

 その9 ←  → その10の2  → その11
メモの目次等はこちらから 読めない用語集

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1