유진의 코딩스토리

Python DL 실습 4 [학습 관련 기술] 본문

Azure 실습/Azure deep learnig

Python DL 실습 4 [학습 관련 기술]

놀고먹는 유진 2024. 10. 25. 09:07

 

최적화 (Optimizer) 기법

 

확률적 경사 하강법 (SGD, Stochastic Gradient Descent)

 

다변수 미분 이론에 바탕을 둔 가장 기본이 되는 optimizer이다.

매번 전체 데이터셋을 사용하는 대신 무작위로 선택된 mini batch를 사용하여 경사를 계산하고 업데이트한다.

큰 데이터셋을 사용할 때 계산량이 적어 경사방향으로 움직일 때 가장 빠른 최적화를 보이는 단순한 방식으로 학습률에 따라 성능 편차가 크다는 특징이 있다.

 

하지만 SGD의 치명적인 단점으로는 고차원 공간에서 비효율적인 경사 하강이 발생할 수 있다는 점이다.

 

위 사진처럼 고차원의 공간에서는 손실함수의 지형이 매우 복잡할 수 있는데 경사가 특정 방향(x)으로는 매우 작고 특정 방향으로는 매우 큰 경우가 있다. 이 때 SGD는 기울기가 큰 방향으로만 너무 크게 이동하고 작은 방향으로는 거의 이동하지않아 수렴 속도가 매우 느려지거나 학습이 잘 진행되지 않을 수 있으며 평탄한 지역에서 쉽게 멈출 수 있다.

 

 

모멘텀 (Momentum)

 

모멘텀 방식은 단순히 기울기만을 이용하지 않고 이전 기울기들의 속도를 반영하여 가중치를 갱신하는 방식이다.

 

 

위 수식을 보면 이전 기울기들의 움직임을 고려해 관성을 더해준다. 이러한 모멘텀 방식은 SGD와 다르게 경사가 평평한 영역에서도 빠르게 움직이게 하며 최적해 근처에서 진동을 줄여주어 빠르게 최적해에 수렴한다는 장점이 있다.

 

모멘텀 방식의 경우 관성이 작용하여 진행하던 속도를 유지하며 부드럽게 이동한다. local minima에 빠지더라도 쉽게 벗어날 수 있다.

하지만 overshotting문제가 발생할 수 있는데 경사가 가파른 영역의 경우 빠른 속도로 인해 관성을 이기지 못하고 최소 지점을 지나쳐버리는 현상이다. gradient가 완만한 경우 최적해를 잘 찾지만 가파를수록 overshooting의 가능성이 커진다.

 

 

 

 

 

 

 

AdaGrad (Adaptive Gradient Algorithm)

 

 

 

NAG (Nesterov Accelerated Gradient)

RMSProp (Root Mean Square Propagation)

Adam (Adaptive Moment Estimation)

 

 

# 각 Optimizer를 활용하여 최적화를 수행

import os, sys
print(os.getcwd())
current_dir = os.path.dirname(os.getcwd())
print(current_dir)
os.chdir(current_dir)

import numpy as np
import matplotlib.pyplot as plt
from collections import OrderedDict
from common.optimizer import *

# 미분 전의 함수
def f(x, y):
    return x**2 / 20.0 + y**2

def df(x, y):
    return x / 10.0, 2.0 * y

init_pos = (-7.1, 2.0)
params = {}
params['x'], params['y'] = init_pos[0], init_pos[1]
grads = {}
grads['x'], grads['y'] = 0, 0

# Optimizer OrderedDict에 정리해둠
optimizers = OrderedDict()
optimizers['SGD'] = SGD(lr=0.95)
optimizers['Momentum'] = Momentum(lr=0.1)
optimizers['AdaGrad'] = AdaGrad(lr=1.5)
optimizers['Adam'] = Adam(lr=0.3)

idx = 1

for key in optimizers:
    optimizer = optimizers[key]
    x_history = []
    y_history = []
    params['x'], params['y'] = init_pos[0], init_pos[1]

    for i in range(30):
        x_history.append(params['x'])
        y_history.append(params['y'])

        grads['x'], grads['y'] = df(params['x'], params['y']) # 각 좌표에 따른 기울기를 구함
        optimizer.update(params, grads)

    x = np.arange(-10, 10, 0.01)
    y = np.arange(-5, 5, 0.01)

    X, Y = np.meshgrid(x,y)
    Z = f(X, Y)

    # 외곽선 단순화
    mask = Z > 7
    Z[mask] = 0

    # 그래프 그리기
    plt.subplot(2, 2, idx)
    idx += 1
    plt.plot(x_history, y_history, 'o-', color = 'red')
    plt.contour(X, Y, Z)
    plt.ylim(-10, 10)
    plt.xlim(-10, 10)
    plt.plot(0, 0, '+')
    # colorbar()
    # spring()
    plt.title(key)
    plt.xlabel("x")
    plt.ylabel("y")

plt.show()

 

 

 

 

 

import os, sys
print(os.getcwd())
current_dir = os.path.dirname(os.getcwd())
print(current_dir)
os.chdir(current_dir)

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 *


# 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. 실험용 설정
# optimizer 정의
optimizers = {}
optimizers['SGD'] = SGD()
optimizers['Momentum'] = Momentum()
optimizers['AdaGrad'] = AdaGrad()
optimizers['Adam'] = Adam()
# optimizers['RMSprop'] = RMSprop()

networks = {}
train_loss = {}

# optimizer별로 신경망 생성
for key in optimizers.keys():
    networks[key] = MultiLayerNet(
        input_size=784, hidden_size_list=[100, 100, 100, 100], 
        output_size=10)
    train_loss[key] = []

# 2. 훈련 시작
# train data에서 배치 크기만큼 데이터 추출
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 optimizers.keys():
        # 기울기를 추출하여 optimizer별로 가중치를 update함
        grads = networks[key].gradient(x_batch, t_batch)
        optimizers[key].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 optimizers.keys():
                loss = networks[key].loss(x_batch, t_batch)
                print(key + ":" + str(loss))

# 3. 그래프 그리기
markers = {"SGD" : "o", "Momentum" : "x", "AdaGrad" : "s", "Adam" : "D"}
x = np.arange(max_iterations)
for key in optimizers.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, 1)
plt.legend()
plt.show()

 

 

가중치 초기값 설정

가중치의 초기값에 따라 학습의 성능과 속도에 큰 영향을 미치므로 가중치 초기값 설정이 매우 중요하다.

 

초기값을 0이나 균일한 값으로 설정할 경우 학습이 올바르게 이뤄지지 않는데 오차역적파법에서 모든 가중치값이 똑같이 갱신되기 때문이다. 가중치의 대칭적인 구조를 무너뜨리기 위해서는 초기값을 무작위로 설정해야한다.

# 가중치의 초기값과 활성화 함수를 변경해가며 가중치의 히스토그램을 확인

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

# 이전 노드의 출력을 입력값으로 사용
for i in range(hidden_layer_size):
    if i != 0:
        x = activations[i-1]

    # 초기값을 다양하게 바꿔가며 실행해보기
    # w = np.random.randn(node_num, node_num) * 1  # 표준편차가 1인 정규분포
    # w = np.random.randn(node_num, node_num) * 0.01  # 표준편차가 0.01인 정규분포
    w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num)  # Xavier초기값
    # w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num)  # He 초기값
    a = np.dot(x, w)

    # 활성화 함수도 바꿔가며 실행해보기
    z = sigmoid(a)
    # z = ReLU(a)
    # z = tanh(a)

    activations[i] = z

# 히스토그램 그리기
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()

표준편차 1인 경우 / 표준편차 0.01인 경우
Xavier 초기값 / He 초기값

 

 위 코드 결과는 가중치 초기값을 어떻게 설정하느냐에 따라 각 층의 sigmoid 활성화 값의 분포를 그린 그래프이다.

 

가중치를 표준편차가 1인 정규분포로 초기화할 경우

 

가중치가 정규분포 N(0,1)를 따를 경우 가중치가 너무 작거나 크지 않을 상태로 시작할 수 있지만 모델의 깊이가 깊어질수록 기울기 소실 (Gradient Vanishing) 문제가 발생할 수 있다. sigmoid 활성화 값들이 0과 1에 치우쳐 있어 미분값은 0에 가까워지기 때문이다.

 

 

가중치를 표준편차가 0.01인 정규분포로 초기화할 경우 

하지만 이런 경우 가중치가 좁은 범위로 모여 있어 모든 가중치들이 비슷한 값을 같는 

 

 

Xavier 초기값

 

 

He 초기값 : Relu를 사용할 때 가중치 초기값

 

 

# 표준편차 0.01, Xavier, He 초기값 각각에 대해 학습 속도를 측정

import os, sys
print(os.getcwd())
current_dir = os.path.dirname(os.getcwd())
print(current_dir)
os.chdir(current_dir)

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 *

# 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'}  # weight 초기값 종류별 지정
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. 훈련 시작
# train data에서 배치 크기만큼 데이터 추출
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)
    
    # 학습 100회마다 손실값 출력
    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()

가중치를 표준편차가 0.01인 정규분포로 초기화할 경우 

배치 정규화 알고리즘

 

 

오버피팅 현상

훈련 데이터만 너무 적응해버려서 테스트 데이터에 제대로 대응하지 못하는 현상

매개변수에 비해 훈련 데이터 수가 적을 때 발생한다.

 

 예제 코드 설명)

  • MNIST 데이터셋 60000개 중 훈련 데이터로 300개만 사용
  • 7층 네트워크사용하여 모델의 복잡성 증가
  • 각 층의 뉴런은 100개, 활성화 함수 Relu 사용
  • 그 결과 학습 데이터 정확도는 높으나 테스트 데이터 정확도는 낮게 나오는 오버피팅 현상 발생

 

 

가중치 감소(Weight Decay)

 

 

드롭 아웃(Drop-Out)

 

 

 

Parameter

Hyperparameter