end0tknr's kipple - web写経開発

太宰府天満宮の狛犬って、妙にカワイイ

Weight decay (荷重減衰)による過学習の軽減(正則化)

deep-learning-from-scratch/ch06 at master · oreilly-japan/deep-learning-from-scratch · GitHub

「ゼロから作るDeep Learning ① (Pythonで学ぶディープラーニングの理論と実装)」 p.193~195 の写経です。

過学習の主な原因

  • パラメータを大量に持ち、表現力の高いモデルであること
  • 訓練データが少ないこと

上記が原因で、重みパラメータが大きくなり、 結果として、過学習となることが多いらしい。

Weight decay (荷重減衰)とは

重みの2乗ノルム(L2ノルム)を損失関数に加算し、 重みが大きくなることを抑えること

 \large{ L2ノルム : \frac{1}{2} λ W^2 }

ただし

 \large{ W = (w_1,w_2,…,w_n) }

Weight decay の python 実装

# coding: utf-8
import os
import sys

sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import matplotlib.pyplot as plt
import urllib.request
import gzip
from collections import OrderedDict

    
def main():
    max_epochs = 201
    
    mymnist = MyMnist()
    train_test_data = mymnist.load_mnist()

    markers = {'train': 'o', 'test': 's'}
    
    # weight decayは荷重減衰で、0はweight decayを使用しない場合
    for i, weight_decay_lambda in enumerate([0, 0.1]):
        
        (train_acc_list,test_acc_list) = \
            my_train(train_test_data,
                     weight_decay_lambda,
                     max_epochs )

        plt.subplot(1,2,i+1)
        x = np.arange(max_epochs)
        plt.plot(x, train_acc_list,marker='o',label='train',markevery=10)
        plt.plot(x, test_acc_list, marker='s',label='test', markevery=10)
        plt.xlabel("epochs")
        plt.ylabel("accuracy")
        plt.ylim(0, 1.0)
        plt.legend(loc='lower right')
    plt.show()


def my_train(train_test_data,
             weight_decay_lambda, # 荷重減衰の設定
             max_epochs):
    (x_train, t_train, x_test, t_test) = train_test_data
    # 過学習を再現するために、学習データを削減
    x_train = x_train[:300]
    t_train = t_train[:300]

    network = MultiLayerNet(input_size=784,
                            hidden_size_list=[100,100,100,100,100,100],
                            output_size=10,
                            weight_decay_lambda=weight_decay_lambda)
    optimizer = SGD(lr=0.01)

    train_size = x_train.shape[0]
    batch_size = 100

    train_acc_list  = []
    test_acc_list   = []

    iter_per_epoch = max(train_size / batch_size, 1)
    epoch_cnt = 0

    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)

        if i % iter_per_epoch == 0:
            train_acc = network.accuracy(x_train, t_train)
            test_acc = network.accuracy(x_test, t_test)
            train_acc_list.append(train_acc)
            test_acc_list.append(test_acc)

            print("epoch:" ,   str(epoch_cnt) ,
                  "train acc:",str(train_acc) ,
                  "test acc:", str(test_acc))

            epoch_cnt += 1
            if epoch_cnt >= max_epochs:
                break

    return (train_acc_list, test_acc_list)
    

class MyMnist:
    def __init__(self):
        pass

    def load_mnist(self):
        data_files = self.download_mnist()
        # convert numpy
        dataset = {}
        dataset['train_img']   = self.load_img(  data_files['train_img'] )
        dataset['train_label'] = self.load_label(data_files['train_label'])
        dataset['test_img']    = self.load_img(  data_files['test_img']  )
        dataset['test_label']  = self.load_label(data_files['test_label'])

        for key in ('train_img', 'test_img'):
            dataset[key] = dataset[key].astype(np.float32)
            dataset[key] /= 255.0

        for key in ('train_label','test_label'):
            dataset[key]=self.change_one_hot_label( dataset[key] )

        return (dataset['train_img'],
                dataset['train_label'],
                dataset['test_img'],
                dataset['test_label'] )

    def change_one_hot_label(self,X):
        T = np.zeros((X.size, 10))
        for idx, row in enumerate(T):
            row[X[idx]] = 1
        return T
    
    def download_mnist(self):
        url_base = 'http://yann.lecun.com/exdb/mnist/'
        key_file = {'train_img'  :'train-images-idx3-ubyte.gz',
                    'train_label':'train-labels-idx1-ubyte.gz',
                    'test_img'   :'t10k-images-idx3-ubyte.gz',
                    'test_label' :'t10k-labels-idx1-ubyte.gz' }
        data_files = {}
        dataset_dir = os.path.dirname(os.path.abspath(__file__))
        
        for data_name, file_name in key_file.items():
            req_url   = url_base+file_name
            file_path = dataset_dir + "/" + file_name

            request  = urllib.request.Request( req_url )
            response = urllib.request.urlopen(request).read()
            with open(file_path, mode='wb') as f:
                f.write(response)
                
            data_files[data_name] = file_path
        return data_files

    def load_img( self,file_path):
        img_size    = 784 # = 28*28
        
        with gzip.open(file_path, 'rb') as f:
            data = np.frombuffer(f.read(), np.uint8, offset=16)
        data = data.reshape(-1, img_size)
        return data
    
    def load_label(self,file_path):
        with gzip.open(file_path, 'rb') as f:
            labels = np.frombuffer(f.read(), np.uint8, offset=8)
        return labels

# 全結合による多層ニューラルネットワーク
class MultiLayerNet:
    def __init__(self,
                 input_size,            # 入力size (MNISTの場合は784)
                 hidden_size_list,      # 隠れ層のneuron数list 例[100,100,100]
                 output_size,           # shuturyoku 出力size (MNISTの場合は10)
                 activation='relu',     # 活性化関数 relu or sigmoid
                 weight_init_std='relu',# ※
                 weight_decay_lambda=0):# Weight Decay (L2ノルム)の強さ
        # weight_init_std : 重みの標準偏差 ( 例 0.01 )
        #   'relu'または'he'を指定した場合は「Heの初期値」
        #   'sigmoid'または'xavier'を指定した場合は「Xavierの初期値」

        
        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_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]()

        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()

    # 重みの初期値設定
    # weight_init_std : 重みの標準偏差を指定(e.g. 0.01)
    #  'relu'または'he'を指定した場合は「Heの初期値」を設定
    #  'sigmoid'または'xavier'を指定した場合は「Xavierの初期値」を設定
    def __init_weight(self, weight_init_std):
        all_size_list = \
            [self.input_size] + self.hidden_size_list + [self.output_size]
        for idx in range(1, len(all_size_list)):
            scale = weight_init_std
            # ReLUを使う場合
            if str(weight_init_std).lower() in ('relu', 'he'):
                scale = np.sqrt(2.0 / all_size_list[idx - 1])
            # sigmoidを使う場合
            elif str(weight_init_std).lower() in ('sigmoid', 'xavier'):
                scale = np.sqrt(1.0 / all_size_list[idx - 1])

            self.params['W' + str(idx)] = \
                scale * np.random.randn(all_size_list[idx-1], all_size_list[idx])
            self.params['b' + str(idx)] = np.zeros(all_size_list[idx])

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

        return x

    # 損失関数。x:入力データ、t:教師ラベル
    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

    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        if t.ndim != 1 : t = np.argmax(t, axis=1)

        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy

    # 勾配(数値微分)。x:入力データ、t:教師ラベル
    def numerical_gradient(self, x, t):
        loss_W = lambda W: self.loss(x, t)

        grads = {}
        for idx in range(1, self.hidden_layer_num+2):
            grads['W' + str(idx)] = \
                numerical_gradient(loss_W, self.params['W' + str(idx)])
            grads['b' + str(idx)] = \
                numerical_gradient(loss_W, self.params['b' + str(idx)])
        return grads

    # 勾配(誤差逆伝搬法)。x:入力データ、t:教師ラベル
    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
    
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

class Relu:
    def __init__(self):
        self.mask = None

    def forward(self, x):
        self.mask = (x <= 0)
        out = x.copy()
        out[self.mask] = 0

        return out

    def backward(self, dout):
        dout[self.mask] = 0
        dx = dout
        return dx

class Sigmoid:
    def __init__(self):
        self.out = None

    def forward(self, x):
        out = sigmoid(x)
        self.out = out
        return out

    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.out
        return dx
    
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 = self.softmax(x)
        self.loss = self.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
    
    def softmax(self,x):
        x = x - np.max(x, axis=-1, keepdims=True)   # オーバーフロー対策
        return np.exp(x) / np.sum(np.exp(x), axis=-1, keepdims=True)

    def cross_entropy_error(self, y, t):
        if y.ndim == 1:
            t = t.reshape(1, t.size)
            y = y.reshape(1, y.size)

        # 教師データがone-hot-vectorの場合、正解ラベルのインデックスに変換
        if t.size == y.size:
            t = t.argmax(axis=1)

        batch_size = y.shape[0]
        return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

# 確率的勾配降下法(Stochastic Gradient Descent)
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] 

    
if __name__ == '__main__':
    main()

上記を実行すると、以下のように表示されます。

右側がWeight decayありで、 教師dataとテストdataでの精度の差が少ないようです。 また、教師dataが100%に至ってないことから、 過学習が防げているらしい。