본문 바로가기
Drawing (AI)/DeepLearning

딥러닝 직접 구현하기 - (가중치 초기값)

by 생각하는 이상훈 2024. 1. 10.
728x90

가중치의 초기값

신경망 학습에서 가중치의 초기값은 매우 중요하다. 가중치의 초기값 설정이 신경망 학습의 성패를 가르는 일이 실제로 자주 있다.

오버피팅을 억제하여 범용 성능을 높이는 테크닉인 가중치 감소(weight decay) 기법을 살펴볼 건데 이는 말 그대로 가중치 매개변수의 값이 작아지도록 학습하여 오버피팅을 방지하는 것이다.

가중치를 작게 만들려면 초깃값도 최대한 작게 만드는 것이 좋다. 따라서 이전에 사용할 때는 단순히 0.01*np.random.randn(10,100)과 같이 정규분포에서 생성된 값을 0.01배로 만든 작은 값을 사용했다. 이때 가중치의 초기값을 작게 하기 위해 0으로 설정하면 오차역전파 과정에서 모든 가중치의 값이 똑같이 전달되기 때문에 학습이 제대로 이루어지지 않는다. 따라서 초기값은 반드시 무작위로 설정해야한다.


은닉층의 활성화값 분포

은닉층의 활성화값(활성화 함수의 출력 데이터)의 분포를 관찰해보자. 우선 가중치의 초깃값에 따라 은닉층의 활성화 값들이 어떻게 변화하는지 살펴보려한다. CS231n 수업의 실험을 참고하면 아래와 같다.

import numpy as np
import matplotlib.pyplot as plt

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def ReLU(x):
    return np.maximum(0, x)

def tanh(x):
    return np.tanh(x)
    
input_data = np.random.randn(1000, 100)  # 1000개의 데이터
node_num = 100  # 각 은닉층의 노드(뉴런) 수
hidden_layer_size = 5  # 은닉층이 5개
activations = {}  # 이곳에 활성화 결과를 저장

x = input_data

def get_activation(hidden_layer_size, x, w, a_func=sigmoid):
    for i in range(hidden_layer_size):
        if i != 0:
            x = activations[i-1]

        a = np.dot(x, w)

        # 활성화 함수도 바꿔가며 실험해보자!
        z = a_func(a)
        # z = ReLU(a)
        # z = tanh(a)

        activations[i] = z
    return activations
    
# 초깃값을 다양하게 바꿔가며 실험해보자!
w = np.random.randn(node_num, node_num) * 1
# w = np.random.randn(node_num, node_num) * 0.01
# w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num)
# w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num)

z = sigmoid
# z = ReLU
# z = tanh

activations = get_activation(hidden_layer_size, x, w, z)


# 히스토그램 그리기
def get_histogram(activations):
    for i, a in activations.items():
        plt.subplot(1, len(activations), i+1)
        plt.title(str(i+1) + "-layer")
        if i != 0: plt.yticks([], [])
        # plt.xlim(0.1, 1)
        # plt.ylim(0, 7000)
        plt.hist(a.flatten(), 30, range=(0,1))
    plt.show()

get_histogram(activations)

표준편차가 1인 정규분포의 활성화값들의 분포이다.

시그모이드 함수는 그 출력이 0 또는 1에 가까워지면 그 미분이 0에 가까워져서 데이터가 0과 1에 치우쳐 분포하게 되어 기울기 값이 점점 작아지다 사라지는 기울기 소실(gradient vanishing)문제가 발생하였다. 따라서 이번에는 표준편차가 0.01인 정규분포를 이용했다.

Gradient vanishing은 발생하지 않았으나 0.5에 치우쳐 다양한 뉴런이 있는 의미가 없어졌다. 모델의 표현력이 제한되었다는 뜻이다.

 

이번에는 사비에르 글로로트와 요슈아 벤지오의 논문에서 권장하는 가중치 초깃값인 Xavier 초기값을 사용해 보자. 이 초기값은 현재 딥러닝 프레임워크의 표준으로 이용되고 있다. 초기값의 표준편차가 1/√ n이 되도록 설정하는 것이다. 이때 n은 이전 layer의 노드 수 이다.

# Xavier 초기값
w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num)
activations = get_activation(hidden_layer_size, x, w, z)
get_histogram(activations)

위와 같은 방식으로 Xaveir초기값을 설정하면 아래와 같은 결과가 나온다.

층이 깊어지면 형태가 뒤틀리긴하지만 정규분포형태를 띄고 있고 이전 방식들 보다는 넓게 분포되어 있음을 알 수 있다.

 

sigmoid, tanh는 좌우 대칭이라 Xavier 초기값이 적당하나 ReLU에 특화된 초기값인 He 초기값이 있다. 이는 앞 계층의 노드가 n일 때 표준편차가 2 / np.sqrt(n)인 정규분포를 사용한다. ReLU의 경우 각초기값에 대해 비교를 해보자.

# 표준편차가 0.01인 정규분포를 가중치 초기값으로 사용한 경우
w = np.random.randn(node_num, node_num) * 0.01
z = ReLU
activations = get_activation(hidden_layer_size, x, w, z)
get_histogram(activations)

# Xavier 초기값을 사용한 경우
w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num)
activations = get_activation(hidden_layer_size, x, w, z)
get_histogram(activations)

# He 초기값을 사용한 경우
w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num)
activations = get_activation(hidden_layer_size, x, w, z)
get_histogram(activations)

std = 0.01일 때는 각 층의 활성화 값들이 아주 작은 값들. 역전파의 가중치의 기울기 역시 작아지고 실제로 학습이 거의 이뤄지지 않는다.

Xavier 초기값일 때 층이 깊어지면 활성화값들이 치우친다. 학습할 때 '기울기 소실'문제가 발생한다.

He 초기값일 때는 모든 층에서 균일하게 분포하는 것을 볼 수 있다.


MNIST 데이터셋으로 비교

MNIST 데이터로 가중치의 초기값을 주는 방법이 신경망 학습에 얼마나 영향을 주는지 그래프를 그려보자.

import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from common.util import smooth_curve
from common.multi_layer_net import MultiLayerNet
#from common.optimizer import SGD


# 0. MNIST 데이터 읽기==========
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)

train_size = x_train.shape[0]
batch_size = 128
max_iterations = 2000


# 1. 실험용 설정==========
weight_init_types = {'std=0.01': 0.01, 'Xavier': 'sigmoid', 'He': 'relu'}
optimizer = SGD(lr=0.01)

networks = {}
train_loss = {}
for key, weight_type in weight_init_types.items():
    networks[key] = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100],
                                  output_size=10, weight_init_std=weight_type)
    train_loss[key] = []


# 2. 훈련 시작==========
for i in range(max_iterations):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    for key in weight_init_types.keys():
        grads = networks[key].gradient(x_batch, t_batch)
        optimizer.update(networks[key].params, grads)
    
        loss = networks[key].loss(x_batch, t_batch)
        train_loss[key].append(loss)
    
    if i % 100 == 0:
        print("===========" + "iteration:" + str(i) + "===========")
        for key in weight_init_types.keys():
            loss = networks[key].loss(x_batch, t_batch)
            print(key + ":" + str(loss))


# 3. 그래프 그리기==========
markers = {'std=0.01': 'o', 'Xavier': 's', 'He': 'D'}
x = np.arange(max_iterations)
for key in weight_init_types.keys():
    plt.plot(x, smooth_curve(train_loss[key]), marker=markers[key], markevery=100, label=key)
plt.xlabel("iterations")
plt.ylabel("loss")
plt.ylim(0, 2.5)
plt.legend()
plt.show()

층별 뉴런 수가 100개인 5층 신경망에서 활성화 함수로 ReLu를 사용하였다. 표준편차가 0.01일 때 학습이 전혀 이뤄지지 않고 Xavier, He 초기값은 학습이 순조롭다. 그러나 He 초기값이 학습진도가 빠른 것을 볼 수 있다.


 

728x90