머신 러닝 교과서/ 간단한 분류 알고리즘 훈련

인공 뉴런: 초기 머신러닝의 간단한 역사

  • 1943년 워런 맥컬록과 월터 피츠는 처음으로 간소화된 뇌의 뉴런 개념을 발표함. 이를 맥컬록-피츠(MCP) 뉴런이라고 한다.
    • 맥컬록과 피츠는 신경 세포를 이진 출력을 내는 간단한 논리 회로로 표현했다. 수상 돌기에 여러 신호가 도착하면 세포체에 합쳐지고 합쳐진 신호가 특정 임계값을 넘으면 출력 신호가 생성되고 축삭 돌기를 이용해서 전달 됨
  • 몇 년 후 프랑크 로젠블라트는 MCP 뉴런 모델을 기반으로 퍼셉트론 학습 개념을 처음 발표함.
    • 퍼셉트론 규칙에서 로젠블라트는 자동으로 최적의 가중치를 학습하는 알고리즘을 제안함.
    • 이 가중치는 뉴런의 출력 신호를 낼지 말지를 결정하기 위해 입력 특성에 곱하는 계수.
    • 지도 학습과 분류 개념으로 말하면 이 알고리즘으로 샘플이 한 클래스에 속하는지 아닌지를 예측할 수 있다.

인공 뉴런의 수학적 정의

  • 인공 뉴런(artifical neuron) 아이디어를 두 개의 클래스가 있는 이진 분류(binary classification) 작업으로 볼 수 있다.
    • 두 클래스는 간단하게 1(양성)과 -1(음성)으로 나타낸다. 그 다음 입력값 x 와 이에 상응하는 가중치 벡터 w 의 선형 조합으로 결정함수(\phi(z) )를 정의한다.
    • 최종 입력(net input)인 z z = w_{1} + x_{1} + ... + w_{m} x_{m} 이다.

w = \left[ \begin{array}{rrr} w_{1} \\ ... \\ w_{m} \end{array} \right], x = \left[ \begin{array}{rrr} x_{1} \\ ... \\ x_{m} \end{array} \right]

  • 이제 특정 샘플 x^{(i)} 의 최종 입력이 사전에 정의된 임계 값 \theta 보다 크면 클래스 1로 예측하고 그렇지 않으면 클래스 -1로 예측한다.
    • 퍼셉트론 알고리즘에서 결정함수 \phi(\cdot) 는 단위 계단 함수(unit step function)를 변형한 것이다.

\phi(z) = \begin{cases} 1 & z \geq \theta \\ -1 & else \end{cases}

  • 식을 간단하게 만들기 위해 임계 값 \theta 를 식의 왼조긍로 옮겨 w_{0} = -\theta x_{0} = 1 인 0번째 가중치를 정의한다. 이렇게 하면 z 를 좀 더 간단하게 쓸 수 있다.

z = w_{0}x_{0} + w_{1}x_{1} + ...  + w_{m}x_{m} = w^{T}x

  • 결정 함수는 다음과 같다.

\phi(z) = \begin{cases} 1 & z \geq \theta \\ -1 & else \end{cases}

  • 머신 러닝 분야에서 음수 임계 값 또는 가중치 w_{0} = -\theta 를 절편이라고 한다.
  • 그림 2-2는 퍼셉트론 결정함수로 최종 입력 z = w^{T}x 가 이진 출력 (-1 또는 1)으로 압축되는 방법(왼쪽)과 이를 사용하여 선형 분리가 가능한 두 개의 클래스 사이를 구별하는 방법(오른쪽)을 나타낸다.

퍼셉트론 학습 규칙

  • MCP 뉴런과 로젠블라트의 임계 퍼셉트론 모델 이면에 있는 전반적인 아이디어는 뇌의 뉴런 하나가 작동하는 방식을 흉내내려는 환원주의(reductionism) 접근 방식을 사용한 것이다.
    • 즉 출력을 내거나 내지 않는 두 가지 경우만 있다. 따라서 로젠블라트의 초기 퍼셉트론 학습 규칙을 요약하면 다음과 같다.
      1. 가중치를 0 또는 랜덤한 작은 값으로 초기화 한다.
      2. 각 훈련 샘플 x^{(i)} 에서 다음 작업을 한다.
        1. 출력값 \hat{y} 를 계산한다.
        2. 가중치를 업데이트 한다.
  • 여기서 출력 값은 앞서 정의한 단위 계단 함수로 예측한 클래스 레이블이다. 가중치 벡터 w 에 있는 개별 가중치 w_{j} 가 동시에 업데이트 되는 것을 다음과 같이 쓸 수 있다.

w_{j} := w_{j} + \Delta w_{j}

  • 가중치 w_{j} 를 업데이트 하는데 사용되는 \Delta w_{j} 값은 퍼셉트론 학습 규칙에 따라 계산된다.

\Delta w_{j} = \eta (y^{(i)} - \hat{y}^{(i)}) x_{j}^{(i)}

  • 여기서 \eta (eta)는 학습률(learning rate)이다. (일반적으로 0.0에서 1.0 사이의 값)
    • y^{(i)} i 번째 훈련 샘플의 진짜 클래스 레이블이다.
    • \hat{y}^{(i)} 는 예측 클래스 레이블이다.
  • 가중치 벡터의 모든 가중치를 동시에 업데이트 한다는 점이 중요하다. 즉, 모든 가중치 \Delta w_{j} 를 업데이트 하기 전에 \hat{y}^{(i)} 를 다시 계산하지 않는다.
  • 구체적으로 2차원 데이터셋에서는 다음과 같이 업데이트 된다.

\Delta w_{0} = \eta (y^{(i)} - output^{(i)})

\Delta w_{1} = \eta (y^{(i)} - output^{(i)}) x_{1}^{i}

\Delta w_{2} = \eta (y^{(i)} - output^{(i)}) x_{2}^{i}

  • 퍼셉트론 규칙이 어떻게 작동하는지 알아보자.
  • 퍼셉트론이 클래스 레이블을 정확히 예측한 다음의 두 경우는 가중치가 변경되지 않고 그대로 유지된다.

\Delta w_{j} = \eta (1 - 1) x_{j}^{i} = 0

\Delta w_{j} = \eta ((-1) - (-1)) x_{j}^{i} = 0

  • 잘못 예측했을 때는 가중치를 양성 또는 음성 타깃 클래스 방향으로 이동시킨다.

\Delta w_{j} = \eta (1 - (-1)) x_{j}^{i} = \eta(2)x_{j}^{(i)}

\Delta w_{j} = \eta (-1 - 1) x_{j}^{i} = \eta(-2)x_{j}^{(i)}

  • 곱셈 계수인 x_{j}^{(i)} 를 좀 더 잘 이해하기 위해 다른 예를 살펴보겠다.

y^{(i)} = 1, \hat{y}_{j}^(i) = -1, \eta = 1

  •  x_{j}^{(i)} = 0.5 일 때 이 샘플을 -1로 잘못 분류했다고 가정한다.
    • 이때 가중치가 1 만큼 증가되어 다음 번에 이 샘플을 만났을 때 최종 입력 x_{j}^{(i)} \times w_{j}^{(i)} 가 더 큰 양수가 된다.
    • 단위 계단 함수의 임계 값보다 커져 샘플이 +1로 분류될 가능성이 높아질 것이다.

\Delta w_{j}^{(i)} = (1 - (-1)) 0.5 = (2) 0.5 = 1

  • 가중치 업데이트는 x_{j}^{(i)} 값에 비례한다. 예컨대 다른 샘플 x_{j}^{(i)} = 2 를 -1로 잘못 분류했다면 이 샘플을 다음번에 올바르게 분류하기 위해 더 크게 결정 경게를 움직인다.

\Delta w_{j}^{(i)} = (1 - (-1)) 2 = (2) 2 = 4

  • 퍼셉트론은 두 클래스가 선형적으로 구분되고 학습률이 충분히 작을 때만 수렴이 보장된다.
    • 두 클래스를 선형 결정 경계로 나눌 수 없다면 훈련 데이터셋을 반복할 최대 횟수(에포크(epoch))를 지정하고 분류 허용 오차를 지정할 수 있다. 그렇지 않으면 퍼셉트론 가중치 업데이트를 멈추지 않는다.

  • 그림 2-4는 퍼셉트론이 샘플 x_{j}^{(i)} 를 입력으로 받아 가중치 x_{j}^{(i)} 를 연결하여 최종 입력을 계산하는 방법을 보여준다.
    • 그 다음 최종 입력은 임계 함수로 전달되어 샘플의 예측 클래스 레이블인 -1 또는 +1의 이진 출력을 만든다.
    • 학습 단계에서 이 출력을 사용하여 예측 오차를 계산하고 가중치를 업데이트 한다.

파이썬으로 퍼셉트론 학습 알고리즘 구현

객체 지향 퍼셉트론 API

  • 객체 지향 방식을 사용하여 퍼셉트론 인터페이스를 가진 파이썬 클래스를 정의
    • Perceptron 객체를 초기화한 후 fit 메서드로 데이터에서 학습하고, 별도의 predict 메서드로 예측을 만든다.
    • 관례에 따라 초기화 과정에서 생성하지 않고 다른 메서드를 호출하여 만든 속성에는 밑줄(_)을 추가한다. ex) self.w_
import numpy as np

class Perceptron(object):

    """ 퍼셉트론 분류기

    매개변수
   ----------
   eta : float
       학습률 (0.0과 1.0사이)
    n_iter : int
       훈련 데이터셋 반복 횟수
    random_state : int
       가중치 무작위 초기화를 위한 난수 생성기 시드

   속성
   ---------
   w_ : 1d-array
       학습된 가중치
    errors_ : list
       에포크마다 누적된 분류 오류
    """

   def __init__(self, eta=0.01, n_iter=50, random_state=1):
       self.eta = eta
       self.n_iter = n_iter
       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
        """

       rgen = np.random.RandomState(self.random_state)
        self.w_ = rgen.normal(loc = 0.0, scale = 0.1, size = 1 + X.Shape[1])
        self.errors_ = []

       for _ in range(self.n_iter):
            errors = 0

           for xi, target in zip(X, y):
               update = self.eta * (target - self.predict(xi))
                self.w_[1:] += update * xi
                self.w_[0] += update
                errors += int(update != 0.0)

            self.errors_.append(errors)

        return self

    def net_input(self, X):
       """ 최종 입력 계산 """
        return np.dot(X, self.w_[1:]) + self.w_[0]

    def predict(self, X):
       """ 단위 계단 함수를 사용하여 클래스 레이블을 반환합니다 """
       return np.where(self.net_input(X) >= 0.0, 1, -1)
  • 이 퍼셉트론 구현을 사용하여 학습률 eta와 에포크 횟수(훈련 데이터를 반복하는 횟수) n_iter로 새로운 Perceptron 객체를 초기화 한다.
    • fit 메서드에서 self.w_ 가중치를 벡터 \mathbb{R}^{m + 1} 로 초기화한다.
    • 여기서 m은 데이터셋에 있는 차원(특성) 개수이다.
    • 벡터의 첫 번째 원소인 절편을 위해 1을 더했다. 즉 이 벡터의 첫 번째 원소 self.w_[0]는 앞서 언급한 절편이다.
  • 이 벡터는 rgen.normal(loc = 0.0, scale = 0.01, size = 1 + X.shape[1])을 사용하여 표준 편차가 0.01인 정규 분포에서 뽑은 랜덤한 작은 수를 담고 있다.
    • 여기서 rgen은 넘파이 난수 생성기로 사용자가 지정한 랜덤 시드(seed)로 이전과 동일한 결과를 재현할 수 있다.
  • 가중치를 0으로 초기화 하지 않는 이유는 가중치가 0이 아니어야 학습률 \eta 가 분류 결과에 영향을 주기 때문이다.
    • 가중치가 0으로 초기화되어 있다면 학습률 파라미터 eta는 가중치 벡터의 방향이 아니라 크기에만 영향을 미친다.
  • fit 메서드는 가중치를 초기화한 후 훈련 세트에 있는 모든 개개의 샘플을 반복 순회하면서 이전 절에서 설명한 퍼셉트론 학습 규칙에 따라 가중치를 업데이트 한다.
    • 클래스 레이블은 predict 메서드에서 예측한다.
    • fit 메서드에서 가중치를 업데이트하기 위해 predict 메서드를 호출하여 클래스 레이블에 대한 예측을 얻는다.
    • predict 메서드는 모델이 학습되고 난 후 새로운 데이터의 클래스 레이블을 예측하는데도 사용할 수 있다.
  • 에포크마다 self.errors_ 리스트에 잘못 분류된 횟수를 기록한다. 나중에 훈련하는 동안 얼마나 퍼셉트론을 잘 수행했는지 분석할 수 있다.

붓꽃 데이터셋에서 퍼셉트론 훈련

  • 앞서 만든 퍼셉트론 구현을 테스트 하기 위해 붓꽃 데이터셋에서 Setosa, Versicolor 두 개의 클래스만 사용한다.
    • 퍼셉트론 알고리즘은 다중 클래스 분류로 확장이 가능한데, 일대다(one-versus-all, OvA) 전략 등을 사용하면 된다.
  • pandas 라이브러리를 사용하여 붓꽃 데이터셋을 DataFrame 형식으로 읽고 꽃받침 길이와 꽃잎 길이를 추출하여 2차원 산점도를 그리면 그림 2-6과 같다.
import matplotlib.pyplot as plt
import numpy as np

# setosa와 versicolor를 선택
y = df.iloc[0:100, 4].values
y = np.where(y == 'Iris-setosa', -1, 1)

# 꽃받침 길이와 꽃잎 길이를 추출
X = df.iloc[0:100, [0,2]].values

# 산점도를 그린다.
plt.scatter(X[:50, 0], X[:50, 1], color='red', marker='o', label='setosa')
plt.scatter(X[50:100, 0], X[50:100, 1], color='blue', marker='x', label='versicolor')
plt.xlabel('sepal length [cm]')
plt.ylabel('petal length [cm]')
plt.legend(loc='upper left')
plt.show()

  • 이 데이터셋에서 추출한 일부 데이터를 이용하여 퍼셉트론 알고리즘을 훈련하고 에포크 대비 잘못 분류된 오차를 그래프로 그리면 다음과 같다.
ppn = Perceptron(eta=0.1, n_iter=10)
ppn.fit(X, y)
plt.plot(range(1, len(ppn.errors_) + 1), ppn.errors_, marker='o')
plt.xlabel('Epochs')
plt.ylabel('Number of errors')
plt.show()

  • 퍼셉트론은 6번째 에포크 이후에 수렴했고 훈련 샘플을 완벽하게 분류했다.
  • 최종적으로 데이터셋의 결정 경계를 시각화 하면 아래 그림과 같다.
from matplotlib.colors import ListedColormap

def plot_decision_regions(X, y, classifier, resolution=0.02):
    # 마커와 컬러맵을 설정
   markers = ('s', 'x', '0', '^', '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.ylim(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')

plot_decision_regions(X, y, classifier=ppn)
plt.xlabel('sepal length [cm]')
plt.ylabel('petal length [cm]')
plt.legend(loc = 'upper left')
plt.show()

적응형 선형 뉴런과 학습의 수렴

  • 이 절에서는 단일층 신경망의 또 다른 종류인 적응형 선형 뉴런(Adaptive Linear Neuron, ADALINE)을 살펴보겠다.
    • 버나드 위드로우와 테드 호프는 프랑크 로젠블라트의 퍼셉트론 알고리즘 이 후 몇 년 지나지 않아 아달린(Adaline)을 발표했는데, 이는 퍼셉트론의 향상된 버전으로 볼 수 있다.
    • 아달린은 연속 함수(continous function)으로 비용 함수를 정의하고 최소화하는 핵심 개념을 보여주기 때문에 흥미롭다.
  • 아달린 규칙(위드로우-호프 규칙이라고도 함)과 로젠블라트 퍼셉트론의 가장 큰 차이점은 가중치를 업데이트 하는데 퍼셉트론처럼 단위 계단 함수 대신 선형 활성화 함수를 사용한다는 것이다.
    • 아달린에서 선형 활성화 함수 \phi(z) 는 최종 입력과 동일한 함수이다.

\phi (w^{T}x) = w^{T} x

  • 선형 활성화 함수가 가중치 학습에 사용되지만 최종 예측을 만드는데 여전히 임계 함수를 사용하는데, 이는 앞서 보았던 단위 계단 함수와 비슷하다.
    • 퍼셉트론과 아달린 알고리즘의 차이는 아래 그림과 같다.

  • 아달린 알고리즘은 진짜 클래스 레이블과 선형 활성화 함수의 실수 출력 값을 비교하여 모델의 오차를 계산하고 가중치를 업데이트 한다.
    • 반면 퍼셉트론은 진짜 클래스 레이블과 예측 클래스 레이블을 비교한다.

경사 하강법으로 비용 함수 최소화

  • 지도 학습 알고리즘의 핵심 구성 요소는 학습 과정 동안 최적화 하기 위해 정의한 목적 함수(object function)이다.
    • 종종 최소화하려는 비용 함수가 목적 함수가 된다.
    • 아달린은 계산된 출력과 진짜 클래스 레이블 사이의 제곱 오차합(Sum of Squared Errors, SSE)으로 가중치를 학습할 비용 함수 J 를 정의한다.

J(w) = {1 \over 2} \sum_{i} (y^{(i)} - \phi(z^{(i)}))^{2}

  • 1/2 항은 그래디언트(gradient)를 간소하게 만들려고 편의상 추가한 것이다.
  • 단위 계단 함수 대신 연속적인 선형 활성화 함수를 사용하는 장점은 비용 함수가 미분 가능해진다는 것이다.
    • 이 비용 함수의 또 다른 장점은 볼록 함수라는 것이다. 간단하지만 강력한 최적화 알고리즘인 경사 하강법(gradient descent)을 적용하여 붓꽃 데이터셋의 샘플을 분류하도록 비용 함수를 최소화하는 가중치를 찾을 수 있다.
  • 아래 그림에서는 경사 하강법 이면에 있는 핵심 아이디어를 지역 또는 전역 최솟값에 도달할 때까지 언덕을 내려오는 것으로 묘사한다.
    • 각 반복에서 경사의 반대 방향으로 진행한다.
    • 진행 크기는 경사의 기울기와 학습률로 결정한다.

  • 경사 하강법을 사용하면 비용 함수 J(w) 의 그래디언트 \nabla J(w) 반대 방향으로 조금씩 가중치를 업데이트 한다.

w := w + \Delta w

  • 가중치 변화량 \Delta w 는 음수의 그래디언트에 학습률 \eta 를 곱한 것으로 정의한다.

\Delta w = - \eta \nabla J(w)

  • 비용 함수의 그래디언트를 계산하려면 각 가중치 w_{j} 에 대한 편도 함수를 계산해야 한다.

{\partial J \over \partial w_{j}} = - \sum_{i} (y^{(i)} - \phi(z^{(i)})) x_{j}^{(i)}

  • 따라서 가중치 w_{j} 의 업데이트 공식을 다음과 같이 쓸 수 있다.

\Delta w_{j} = - \eta {\partial J \over \partial w_{j}} = \eta \sum_{i} (y^{(i)} - \phi(z^{(i)})) x_{j}^{(i)}

  • 모든 가중치가 동시에 업데이트 되기 때문에 아달린 학습 규칙은 다음과 같다.

w := w + \Delta w

  • 아달린 학습 규칙이 퍼셉트론 규칙과 동일하게 보이지만 z^{(i)} = w^{T} x^{(i)} \phi(z^{(i)}) 는 정수 클래스로 레이블이 아니고 실수이다.
    • 또 훈련 세트에 있는 모든 샘플을 기반으로 가중치 업데이트를 계산한다.
    • 이 방식을 배치 경사 하강법(batch gradient descent)라고도 한다.

파이썬으로 아달린 구현

  • 퍼셉트론 규칙과 아달린이 매우 비슷하기 때문에 앞서 정의한 퍼셉트론 구현에서 fit 메서드를 바꾸어 경사 하강법으로 비용 함수가 최소화 되도록 가중치를 업데이트 한다.
import numpy as np

class AdalineGD(object):
   """ 적응형 선형 뉴런 분류기
    매개변수
   ----------
   eta : float
       학습률 (0.0과 1.0사이)
    n_iter : int
       훈련 데이터셋 반복 횟수
    random_state : int
        가중치 무작위 초기화를 위한 난수 생성기 시드

    속성
   ---------
   w_ : 1d-array
       학습된 가중치
    errors_ : list
       에포크마다 누적된 분류 오류
    """

    def __init__(self, eta = 0.01, n_iter = 50, random_state = 1):
       self.eta = eta
       self.n_iter = n_iter
        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
        """

       rgen = np.random.RandomState(self.random_state)
        self.w_ = rgen.normal(loc = 0.0, scale = 0.01, size = 1 + X.shape[1])
       self.cost_ = []

       for i in range(self.n_iter):
           net_input = self.net_input(X)
           output = self.activation(net_input)
            errors = (y - output)
           self.w_[1:] += self.eta * X.T.dot(errors)
            self.w_[0] += self.eta * errors.sum()
           cost = (errors**2).sum() / 2.0
           self.cost_.append(cost)       

        return self

   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.net_input(X) >= 0.0, 1, -1)
  • 퍼셉트론처럼 개별 훈련 샘플마다 평가한 후 가중치를 업데이트 하지 않고, 전체 훈련 데이터셋을 기반으로 그래디언트를 계산한다.
    • 절편(0번째 가중치)은 self.eta * errors.sum() 이고 가중치 1에서 m까지는 self.eta * X.T.dot(errors)이다.
    • 여기서 X.T.dot(errors)는 특성 행렬과 오차 벡터 간의 행렬-벡터 곰셈이다.
  • 이 코드의 activation 메서드는 단순한 항등 함수(identity function)이기 때문에 아무런 영향을 미치지 않는다.
    • 단일층 신경망을 통해 정보가 어떻게 흘러가는지를 표시하려고 추가했다.
  • 입력 데이터의 특성에서 최종 입력, 활성화, 출력 순으로 진행된다.
    • 다음 장에서는 항등 함수가 아니고 비선형 활성화 함수를 사용하는 로지스틱 회귀 분류기를 다룰 것인데, 로지스틱 회귀 모델은 활성화 함수와 비용 함수만 다르고 아달린과 매우 비슷하다.
fit, ax = plt.subplots(nrows=1, ncols=2, figsize=(10,4))

ada1 = AdalineGD(n_iter=10, eta=0.01).fit(X, y)

ax[0].plot(range(1, len(ada1.cost_) + 1), np.log10(ada1.cost_), marker='o')
ax[0].set_xlabel('Epochs')
ax[0].set_ylabel('log(Sum-squared-error)')
ax[0].set_title('Adaline - Learning rate 0.01')

ada2 = AdalineGD(n_iter=10, eta=0.0001).fit(X, y)

ax[1].plot(range(1, len(ada2.cost_) + 1), ada2.cost_, marker='o')
ax[1].set_xlabel('Epochs')
ax[1].set_ylabel('Sum-squared-error')
ax[1].set_title('Adaline - Learning rate 0.0001')

plt.show()
  • 출력된 비용 함수 그래프에서 볼 수 있 듯 두 개의 다른 문제가 발생한다.
    • 아래 그림 2-11 왼쪽 그래프는 학습률이 너무 클 때 발생한다.
    • 비용 함수를 최소화 하지 못하고 오차는 에포크마다 점점 더 커진다. 전역 최솟값을 지나쳤기 때문이다.
    • 반면 오른쪽 그래프에서는 비용이 감소하지만 학습률 \eta = 0.0001 은 너무 작기 때문에 알고리즘이 전역 최솟값에 수렴하려면 아주 많은 에포크가 필요하다.

  • 아래 그림 2-12는 비용 함수 J 를 최소화하려고 특정 가중치 값을 바꾸었을 때 어떤 일이 일어나는지 보여준다.
    • 왼쪽 그림은 적절하게 선택한 학습률의 경우로, 비용이 점차 감소하여 전역 최솟값 방향으로 이동한다.
    • 오른쪽 그림은 너무 큰 학습률을 선택하여 전역 최솟값을 지나친다.

특성 스케일을 조정하여 경사 하강법 결과 향상

  • 책에서 살펴볼 머신 러닝 알고리즘들은 최적의 성능을 위해 어떤 식으로든 특성 스케일을 조정하는 것이 필요하다. 이는 3, 4장에서 살펴보겠다.
  • 경사 하강법은 특성 스케일을 조정하여 혜택을 볼 수 있는 많은 알고리즘 중 하나이다. 이 절에서는 표준화(standardization)라고 하는 특성 스케일 방법을 사용하겠다.
    • 데이터에 표준 정규 분포의 성질을 부여하여 경사 하강법 학습이 좀 더 빠르게 수렴되도록 돕는다.
    • 표준화는 각 특성의 평균을 0에 맞추고 특성의 표준 편차를 1로 만든다.
    • 예컨대 j 번째 특성을 표준화 하려면 모든 샘플에서 평균 \mu_{j} 을 빼고 표준 편차 \sigma_{j} 로 나누면 된다.

x'_{j} = {x_{j} - \mu_{j} \over \sigma_{j}}

  • 여기서 x_{j} n 개의 모든 훈련 샘플에서 j 번째 특성 값을 포함한 벡터이다.
    • 표준화 기법을 데이터셋의 각 특성 j 에 적용한다.
    • 표준화가 경사 하강법 학습에 도움이 되는 이유 중 하나는 그림 2-13에 나온 것처럼 더 적은 단계를 거쳐 최적 혹은 좋은 솔루션을 찾기 때문이다.
    • 그림 2-13은 2차원 분류 문제에서 모델의 가중치에 따른 비용 함수의 등고선을 보여준다.

  • 표준화를 적용한 결과는 아래 그림 2-14와 같다.
    • 이 그래프에서 볼 수 있듯이 학습률 \eta = 0.01 을 사용하고 표준화된 특성에서 훈련하니 아달린 모델이 수렴했다.
    • 모든 샘플이 완벽하게 분류되더라도 SSE가 0이 되지는 않는다.
# 표준화
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()

ada = AdalineGD(n_iter=15, eta=0.01)
ada.fit(X_std, y)

plot_decision_regions(X_std, y, classifier=ada)

plt.title('Adaline - Gradient Descent')
plt.xlabel('sepal length [standardized]')
plt.ylabel('petal length [standardized]')
plt.legend(loc='upper left')
plt.tight_layout()
plt.show()

plt.plot(range(1, len(ada.cost_) + 1), ada.cost_, marker='o')
plt.xlabel('Epochs')
plt.ylabel('Sum-squared-error')
plt.show()

대규모 머신 러닝과 확률적 경사 하강법

  • 전체 훈련 세트에서 계산한 그래디언트 반대 방향으로 한 걸음씩 진행하여 비용 함수를 최소화 하는 방식을 배치 경사 하강법이라고도 부른다.
    • 수백 만개의 데이터셋이 존재하는 경우 배치 경사 하강법을 사용하면 계산 비용이 매우 많이 든다. 전역 최솟값으로 나아가는 단계마다 매번 전체 훈련 데이터셋을 다시 평가해야 하기 때문이다.
  • 확률적 경사 하강법(stochastic gradient descent)은 배치 경사 하강법의 다른 대안으로 인기가 높다. 이따금 반복 또는 온라인 경사 하강법이라고도 부른다.
    • 다음 첫 번째 수식처럼 모든 샘플 x^{(i)} 에 대하여 누적된 오차의 합을 기반으로 가중치를 업데이트하는 대신 두 번째 수식처럼 각 훈련 샘플에 대해서 조금씩 가중치를 업데이트 한다.

\Delta w = \eta \sum_{i} (y^{(i)} - \phi(z^{(i)})) x^{(i)}

\Delta w = \eta (y^{(i)} - \phi(z^{(i)})) x^{(i)}

  • 확률적 경사 하강법을 경사 하강법의 근사로 생각할 수 있지만 가중치가 더 자주 업데이트 되기 때문에 수렴 속도가 훨씬 빠르다.
    • 그래디언트가 하나의 훈련 샘플을 기반으로 계산되므로 오차의 궤적은 배치 경사 하강법보다 훨씬 어지럽다.
    • 비선형 비용 함수를 다룰 때 얕은 지역 최솟값을 더 쉽게 탈출할 수 있어 장점이 되기도 한다.
    • 확률적 경사 하강법에서 만족스러운 결과를 얻으려면 훈련 샘플 순서를 무작위하게 주입하는 것이 중요하다.
    • 또 순환되지 않도록 에포크마다 훈련 세트를 섞는 것이 좋다.
  • 확률적 경사 하강법의 또 다른 장점은 온라인 학습(online learning)으로 사용할 수 있다는 것이다.
    • 온라인 학습에서 모델은 새로운 훈련 데이터가 도착하는대로 훈련된다. 많은 양의 훈련 데이터가 있을 때도 유용하다.
    • 예컨대 고객 데이터를 처리하는 웹 애플리케이션에서 온라인 학습을 사용해서 시스템은 변화에 즉시 적응할 수 있다.
  • 확률적 경사 하강법으로 소스를 고치면 다음과 같다.
import numpy as np

class AdalineSGD(object):
   """ 적응형 선형 뉴런 분류기
    매개변수
   ----------
   eta : float
       학습률 (0.0과 1.0사이)
    n_iter : int
       훈련 데이터셋 반복 횟수
    random_state : int
        가중치 무작위 초기화를 위한 난수 생성기 시드

    속성
   ---------
   w_ : 1d-array
       학습된 가중치
    errors_ : list
        에포크마다 누적된 분류 오류
    """

    def __init__(self, eta = 0.01, n_iter = 50, 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.w_initialized_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.net_input(X) >= 0.0, 1, -1)
  • 확률적 경사 하강법을 이용해 분류기를 훈련하고 결과를 그래프로 그리면 아래 그림 2-15와 같다.
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.tight_layout()
plt.show()

plt.plot(range(1, len(ada.cost_) + 1), ada.cost_, marker='o')
plt.xlabel('Epochs')
plt.ylabel('Average Cost')
plt.show()

[ssba]

The author

Player가 아니라 Creator가 되려는 자/ suyeongpark@abyne.com

댓글 남기기

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.