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
반응형

+ Recent posts