728x90
반응형

 이전 포스트에서 확률적 경사 하강법(SGD)에 대해 알아보았다. 해당 포스트에서 경사 하강법 함수 자체는 단순하므로, 이것만 구현하는 것은 쉬우나, 그 성능을 시각적으로 보기 위해선 학습에 대한 모든 알고리즘을 넣어야 하기 때문에 코드가 꽤 어려워지므로, 시간 낭비라고는 하였다.

 그러나, 이에 대하여 관심 있는 사람이 있을 수 있고, 눈으로 직접 코드가 돌아가는 과정을 본다면, 내용을 이해하기 더 쉬울 수 있으므로, 이를 다룬 책을 찾아 코드를 약간 수정하여, 이해하기 쉽도록 풀어보도록 하겠다. 딥러닝에서 사용되는 다층 퍼셉트론을 사용한 예시는 아니지만, 시각적으로 결과를 볼 수 있으므로 좋은 예시라고 생각한다.

 이번 포스트는 세바스찬 라시카, 바히드 미자리의 "머신러닝 교과서 with 파이썬, 사이킷런, 텐서플로"를 참고하여 작성하였다. 해당 책은 딥러닝을 구성하는 알고리즘에 대해 하나하나 다루고 있는 아주 좋은 책이므로, 꼭 읽어보기 바란다.

 

 

 

아달린 확률적 경사 하강법(AdalineSGD)

1. 아달린(ADAptive LInear NEuron, ADALINE)이란?

  • 스탠퍼드의 Bernard Widrow가 개발한 초기 신경망 모델 중 하나인 아달린은 적응형 선형 뉴런이라고 불리며, 연속 함수(Continous Function)로 손실 함수를 정의하고 최소화한다.
  • 아달린과 퍼셉트론의 차이는 가중치 업데이트를 위한 활성화 함수가 다르다.
    • A. 퍼셉트론: 실제값과 예측값의 활성 함수 출력 값이 다르면, 가중치 업데이트
    • B. 아달린: 실제값과 예측값이 다르면 경사 하강법으로 가중치 업데이트
  • Adaline은 퍼셉트론과 달리 선형 활성화 함수라는 것을 통해, 가중치를 업데이트하는 과정이 들어 있다. 활성화 함수는 초기 퍼셉트론과 마찬가지로 계단 함수를 사용한다.

선형 활성화 함수: $ \phi(w^Tx)=w^Tx $

  • 그러나, Adaline 역시 선형 분리가 가능한 논리 함수(AND, NAND, OR)는 실현할 수 있으나, 비선형 논리 함수(XOR)는 실현 불가능하다.
  • 다층 퍼셉트론처럼 다량의 Adaline으로 네트워크를 구성하는 Madaline을 사용하여 이를 해결하긴 하였으나, 계단 함수를 사용하기 때문에 미분이 불가능해 학습이 불가능하다는 단점이 있어, 다층 퍼셉트론(Multilayer Perceptron)에 밀려 요즘은 쓰지 않는다.
    (선형 분리 문제를 해결한 다층 퍼셉트론이 나오기 전엔 Madaline이 최고의 신경망 모델이었다고 한다.)

  • 아달린은 손실 함수로 앞서 학습하였던 제곱 오차합(SSE)을 사용한다.
    2021/01/29 - [Machine Learning/Basic] - 머신러닝-5.0. 손실함수(1)-제곱오차(SE)와 오차제곱합(SSE)
 

머신러닝-5.0. 손실함수(1)-제곱오차(SE)와 오차제곱합(SSE)

 이전 포스트에서 신경망 학습이 어떠한 원리에 의해 이루어지는지 간략하게 살펴보았다. 이번 포스트에서는 제곱 오차(Square Error)와 제곱 오차를 기반으로 만든 손실 함수 오차제곱합(SSE)에 대

gooopy.tistory.com

 

 

 

 

 

2. 구현해보자!

import numpy as np


class AdalineSGD(object):
    """ADAptive LInear NEuron 분류기
    
    매개변수
    -----------------------
    eta : float
    >>> 학습률 (0.0과 1.0 사이)
    n_iter : int
    >>> 훈련 데이터셋 반복 횟수
    shuffle : bool (default: True)
    >>> True로 설정하면 같은 반복이 되지 않도록 에포크마다 훈련 데이터를 섞는다.
    random_state : int
    >>> 가중치 무작위 초기화를 위한 난수 생성기 시드
    
    속성
    -----------------------
    w_ : 1d-array
    >>> 학습된 가중치
    cost_ : list
    >>> 모든 훈련 샘플에 대해 에포크마다 누적된 평균 비용 함수의 제곱합
    """
    def __init__(self, eta=0.01, n_iter=10, shuffle=True, random_state=None):
        self.eta = eta
        self.n_iter = n_iter
        self.w_initialized = False
        self.shuffle = shuffle
        self.random_state = random_state
        
    def fit(self, X, y):
        """훈련 데이터 학습
        
        매개변수
        -----------------------
        X : {array-like}, shape = [n_samples, n_features]
        >>> n_samples개의 샘플과 n_features개의 특성으로 이루어진 훈련 데이터
        y : array-like, shape = [n_samples]
        >>> 타깃 벡터
        
        
        반환값
        -----------------------
        self : object
        """
        self._initialize_weights(X.shape[1])
        self.cost_ = []
        for i in range(self.n_iter):
            if self.shuffle:
                X, y = self._shuffle(X, y)
            cost = []
            for xi, target in zip(X, y):
                cost.append(self._update_weights(xi, target))
            avg_cost = sum(cost) / len(y)
            self.cost_.append(avg_cost)
        return self
    
    def partial_fit(self, X, y):
        """가중치를 다시 초기화하지 않고 훈련 데이터를 학습"""
        if not self.w_initialized:
            self._initialize_weights(X.shape[1])
        if y.ravel().shape[0] > 1:
            for xi, target in zip(X, y):
                self._update_weights(xi, target)
        else:
            self._update_weights(X, y)
        return self
    
    def _shuffle(self, X, y):
        """훈련 데이터를 섞는다."""
        r = self.rgen.permutation(len(y))
        return X[r], y[r]
    
    def _initialize_weights(self, m):
        """랜덤한 작은 수로 가중치를 초기화합니다."""
        self.rgen = np.random.RandomState(self.random_state)
        self.w_ = self.rgen.normal(loc=0.0, scale=0.01, size=1+m)
        self.w_initialized = True
        
    def _update_weights(self, xi, target):
        """아달린 학습 규칙을 적용해 가중치 업데이트"""
        output = self.activation(self.net_input(xi))
        error = (target - output)
        self.w_[1:] += self.eta * xi.dot(error)
        self.w_[0] += self.eta * error
        cost = 0.5 * error**2
        return cost
    
    def net_input(self, X):
        """최종 입력 계산"""
        return np.dot(X, self.w_[1:]) + self.w_[0]
    
    def activation(self, X):
        """선형 활성화 계산"""
        return X
    
    def predict(self, X):
        """단위 계단 함수를 사용하여 클래스 레이블을 반환"""
        return np.where(self.activation(self.net_input(X)) >= 0.0, 1, -1)
  • 위 코드는 아달린으로 SGD를 구현한 것이다.
  • 아달린은 역전파를 통해 가중치 업데이트가 이루어지는 다층 퍼셉트론과 달리 층 자체에서 가중치를 업데이트하므로, 다층 퍼셉트론에 비해 개념이 단순하므로, 아달린을 사용했다.
# iris Data Import
from sklearn.datasets import load_iris
import pandas as pd

# Data Handling
X = pd.DataFrame(load_iris()["data"]).iloc[0:100, [0,2]].values
y = load_iris()["target"][0:100]
y = np.where(y==0, -1, 1)

# 변수 2개만 분석의 대상으로 사용할 것이므로, 이 2개만 표준화시키자.
X_std = np.copy(X)
X_std[:,0] = (X[:,0] - X[:,0].mean()) / X[:,0].std()
X_std[:,1] = (X[:,1] - X[:,1].mean()) / X[:,1].std()
  • 학습에 사용될 데이터 셋은 붓꽃에 대한 정보가 담긴 iris로 데이터 분석을 해본 사람이라면 꽤 친숙한 데이터일 것이다.
  • 해당 데이터에 대한 자세한 내용을 보고 싶다면, load_iris().keys()를 입력하여, dictionary에 있는 key들을 확인하고, 데이터를 살펴보도록 하자.
# 시각화 함수
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap

def plot_decision_regions(X, y, classifier, resolution=0.02):
    
    # 마커와 컬러맵 설정
    markers = ('s', 'x', 'o', '^', 'v')
    colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
    cmap = ListedColormap(colors[:len(np.unique(y))])
    
    # 결정 경계를 그린다.
    x1_min, x1_max = X[:,0].min() - 1, X[:,0].max() + 1
    x2_min, x2_max = X[:,1].min() - 1, X[:,1].max() + 1
    xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution),
                           np.arange(x2_min, x2_max, resolution))
    Z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
    Z = Z.reshape(xx1.shape)
    plt.contourf(xx1, xx2, Z, alpha=0.3, cmap=cmap)
    plt.xlim(xx1.min(), xx1.max())
    plt.xlim(xx2.min(), xx2.max())
    
    # 샘플의 산점도를 그린다.
    for idx, cl in enumerate(np.unique(y)):
        plt.scatter(x=X[y == cl, 0],
                    y=X[y == cl, 1],
                    alpha = 0.8,
                    c=colors[idx], 
                    marker=markers[idx], 
                    label=cl,
                    edgecolor='black')
  • 위 학습 코드만으로는 그 결과를 인지하기 어려우므로, 그 과정을 시각화해주는 코드를 생성하였다.
ada = AdalineSGD(n_iter=15, eta=0.01, random_state=1)
ada.fit(X_std, y)

plot_decision_regions(X_std, y, classifier=ada)
plt.title('Adaline - Stochastic Gradient Descent')
plt.xlabel('sepal length [standardized]')
plt.ylabel('petal length [standardized]')
plt.legend(loc='upper left')
plt.show()
plt.plot(range(1, len(ada.cost_) + 1), ada.cost_, marker='o')
plt.xlabel('Epochs')
plt.ylabel('Average Cost')
plt.show()

  • 출력된 결과를 보면, 두 집단(-1, 1)을 선으로 잘 분리한 것을 볼 수 있다(아달린은 선형 분리에 특화되어 있다.)
  • Epoch별 평균 비용(미니 배치 손실 함수의 평균값)이 빠르게 최솟값에 수렴하는 것을 볼 수 있다.

 

 

 

 이번 포스트는 어떤 주제에 대해 설명하기보다는 소개를 목적으로 글을 적었다 보니, 내용상 부족함이 많다. 위 코드는 꽤나 복잡하고, 이해하기가 힘든데, 개인적으로는 굳이 이해하려고 노력하지 않기를 바란다.

 머신러닝에서 굉장히 많이 사용되는 프레임워크인 텐서플로우의 케라스를 사용하여 코드를 작성하면, 코드가 보다 직관적이고, 내가 원하는 형태로 수정하기도 쉽기 때문에 굳이 위 코드를 이해하려 시간을 낭비할 필요는 없다.

 다만, 인공지능 역사에서 아달린이 차지했던 비중이 꽤 되고, 확률적 경사 하강법을 가장 손쉽게 실제 학습에 적용하여, 그 효과를 볼 수 있는 예시로는 위 코드가 가장 좋다고 생각되어 소개해보았다. 

 다음 포스트에서는 이전에 말했던 모멘텀(Momentum)에 대해 다뤄보도록 하겠다.

 

 

[ 참고 자료 ]

www.aistudy.com/neural/model_kim.htm#_bookmark_1a77358

 

초기의 신경망 이론과 모델 : 김대수

< 퍼셉트론 학습 과정 > [단계 1] 연결강도들과 임계값을 초기화한다. wi(0)(0 ≤ i ≤ N - 1) 와 θ 를 임의수 (random number) 로 정한다.  여기에서 wi(t) 는 시각 t 일 때 입력 i 로부터의 연결강도를 나타

www.aistudy.com

blog.naver.com/samsjang/220959562205

 

[6편] 아달라인(Adaline)과 경사하강법(Gradient Descent)

​아달라인(Adaline) 단층 인공신경망인 퍼셉트론이 발표된지 몇 년 후인 1960년에 Bernard Widrow와 T...

blog.naver.com

 

728x90
반응형
728x90
반응형

 이전 포스트에서는 학습 단위에 대한 단어인 에포크(Epoch), 배치 크기(Batch size), 이터레이션(Iteration)에 대해 알아보았다. 이번 포스트에서 알아볼 확률적 경사 하강법(SGD)의 키는 배치 크기와 랜덤 추출이다.

 경사 하강법에 다른 식을 붙이지 않고 바로 사용하는 방법은 크게 두 가지인 배치 경사 하강법(BGD)과 확률적 경사 하강법(SGD)이 있는데, 이 둘은 손실 함수의 기울기 계산에 사용되는 데이터 셋의 규모만 제외하고 같다.

 중요한 것은 손실 함수의 경사를 구하는 대상이다!

 

 

1. 배치 경사 하강법(Batch Gradient Descent, BGD)

  • 배치 경사 하강법(BGD)은 경사 하강법의 손실 함수의 기울기 계산에 전체 학습 데이터셋의 크기와 동일하게 잡는 방법이다.
  • 즉, 경사 하강법 대상이 배치 크기와 동일하다는 것이다.
  • 데이터셋 모두를 대상으로 하다 보니 파라미터가 한번 이동할 때마다, 계산해야 할 값이 지나치게 많으므로, 계산 시간도 엄청 길어지고, 소모되는 메모리도 엄청나다.
  • mini batch 안 모든 데이터를 대상으로 경사 하강법을 실시하므로, 안정적으로 수렴한다.

  • 안정적으로 수렴하므로, 수렴까지 발생하는 총 파라미터 업데이트 수는 매우 적다.
  • 안정적으로 수렴하는 것은 좋으나, 안정적으로 움직이기 때문에 지역 최소해(Local Minimum)에 빠지더라도 안정적으로 움직이므로 빠져나오기 힘들다. 즉, Local Optima(minimum) 문제가 발생할 가능성이 높다.
  • 학습 데이터셋이 커지면 커질수록 시간과 리소스 소모가 지나치게 크다.

 

 

 

 

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

  • 전체 훈련 데이터셋을 대상으로 학습하는 것은 한정된 리소스를 가지고 있는 우리의 분석 환경에서 매우 비효율적이며, 파라미터 업데이트 수가 적다는 것은 랜덤 하게 뽑힌 시작 위치의 가중치 수도 적으므로, Local minimum 현상이 발생할 확률도 높다는 것이다.
  • 그래서 나온 방법이 학습 데이터셋에서 무작위로 한 개의 샘플 데이터 셋을 추출하고, 그 샘플에 대해서만 기울기를 계산하는 것이다.
  • 샘플 데이터 셋에 대해서만 경사(Gradient)를 계산하므로, 매 반복에서 다뤄야 할 데이터 수가 매우 적어, 학습 속도가 매우 빠르다.
  • 하나의 샘플만 대상으로 경사를 계산하므로, 메모리 소모량이 매우 낮으며, 매우 큰 훈련 데이터 셋이라 할지라도 학습 가능하다.
  • 그러나, 무작위로 추출된 샘플에 대해서 경사를 구하므로, 배치 경사 하강법보다 훨씬 불안정하게 움직인다.

  • 손실 함수가 최솟값에 다다를 때까지 위아래로 요동치며 움직이다 보니, 학습이 진행되다 보면, 최적해에 매우 근접하게 움직이긴 하겠으나, 최적해(Global minimum)에 정확히 도달하지 못할 가능성이 있다.
  • 그러나, 이렇게 요동치며 움직이므로, 지역 최솟값(Local minimum)에 빠진 다할지라도, 지역 최솟값에서 쉽게 빠져나올 수 있으며, 그로 인해 전역 최솟값(Global minimum)을 찾을 가능성이 BGD에 비해 더 높다.
  • 즉, 확률적 경사 하강법(SGD)은 속도가 매우 빠르고 메모리를 적게 먹는다는 장점이 있으나, 경사를 구할 때, 무작위성을 띄므로 지역 최솟값에서 탈출하기 쉬우나, 전역 최솟값에 다다르기 힘들다는 단점을 가지고 있다.
  • 이 문제를 해결하기 미니 배치 경사 하강법(mini-Batch gradient descent)이 등장하였다.

 

학습률 스케줄(Learning rate schedule)

  • 전역 최솟값에 도달하기 어렵다는 문제를 해결하기 위한 방법으로, 학습률을 천천히 줄여 전역 최솟값에 다다르게 하는 방법이 있다.
  • 학습률은 작아질수록 이동하는 양이 줄어들기 때문에 전역 최솟값에 안정적으로 수렴할 수 있다.
  • 만약 학습률이 너무 급격하게 감소하면, Local Optima 문제나 Plateau 현상이 발생할 가능성이 높아진다.
  • 그렇다고 학습률을 너무 천천히 줄이면 최적해 주변을 맴돌 수 있다.

 

 

 

 

3. 미니 배치 경사 하강법(mini-Batch gradient descent)

  • 앞서 이야기한 배치 경사 하강법(BGD)나 확률적 경사 하강법(SGD)은 모두 배치 크기가 학습 데이터 셋 크기와 동일하였으나, 미니 배치 경사 하강법은 배치 크기를 줄이고, 확률적 경사 하강법을 사용하는 기법이다.
  • 예를 들어, 학습 데이터가 1000개고, batch size를 100으로 잡았다고 할 때, 총 10개의 mini batch가 나오게 된다. 이 mini batch 하나당 한 번씩 SGD를 진행하므로, 1 epoch당 총 10번의 SGD를 진행한다고 할 수 있다.
  • 일반적으로 우리가 부르는 확률적 경사 하강법(SGD)은 실제론 미니 배치 경사 하강법(mini-BGD)이므로, 지금까지 학습했던 차이들은 기억하되, 앞으로 SGD를 말하면, 미니 배치 경사 하강법을 떠올리면 된다.

  • 미니 배치 경사 하강법은 앞서 이야기했던, 전체 데이터셋을 대상으로 한 SGD보다 파라미터 공간에서 Shooting이 줄어들게 되는데, 이는 한 미니 배치의 손실 값 평균에 대해 경사 하강을 진행하기 때문이다.
  • 그로 인해, 최적해에 더 가까이 도달할 수 있으나, Local optima 현상이 발생할 수 있다. 그러나, 앞서 말했듯 Local optima 문제는 무수히 많은 임의의 파라미터로부터 시작되면, 해결되는 문제이며, 학습 속도가 빠른 SGD의 장점을 사용하여, 학습량을 늘리면 해결되는 문제다.
  • 배치 크기는 총 학습 데이터셋의 크기를 배치 크기로 나눴을 때, 딱 떨어지는 크기로 하는 것이 좋다.
  • 만약, 1050개의 데이터에 대하여 100개로 배치 크기를 나누면, 마지막 50개 데이터셋에 대해 과도한 평가를 할 수 있기 때문이다.
  • 그러나, 만약 배치 크기로 나누기 애매한 경우라면, 예를 들어 총 학습 데이터 셋이 1,000,050개가 있고, 배치 크기를 1,000개로 나누고 싶은 경우라면, 나머지인 50개는 버리도록 하자(물론 완전 무작위 하게 50개를 선택해서 버려야 한다.).

 

 

 

  지금까지 확률적 경사 하강법(SGD)에 대해 알아보았다. 본래의 SGD는 "배치 크기 = 학습 데이터 셋 크기"이지만, 일반적으로 통용되는 SGD는 "배치 크기 < 학습 데이터 셋 크기"인 미니 배치를 만들어 학습시키는 미니 배치 경사 하강법이다. 

 경사 하강법의 파이썬 코드화는 경사 하강법 함수 자체는 단순하지만, 학습에서 발생하는 모든 알고리즘이 복합적으로 작동하므로, 코드화시키는 것은 시간 낭비로 판단된다. Optimizer 파트부턴 그 개념과 특징을 이해하고, 텐서플로우로 학습을 해보도록 하자.

 다음 포스트에서는 경사 하강법의 한계점을 보완하기 위한 시도 중 하나인 모멘텀(Momentum)에 대해 학습해보도록 하겠다.

728x90
반응형

+ Recent posts