Search
Duplicate

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

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

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

인공 뉴런의 수학적 정의

인공 뉴런(artifical neuron) 아이디어를 두 개의 클래스가 있는 이진 분류(binary classification) 작업으로 볼 수 있다.
두 클래스는 간단하게 1(양성)과 -1(음성)으로 나타낸다. 그 다음 입력값 xx와 이에 상응하는 가중치 벡터 ww의 선형 조합으로 결정함수 ϕ(z)\phi(z)를 정의한다.
최종 입력(net input)인 zz는 z=w1+x1+...+wmxmz = w_{1} + x_{1} + ... + w_{m} x_{m}이다.
w=[w1...wm],x=[x1...xm]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)x^{(i)}의 최종 입력이 사전에 정의된 임계 값 θ\theta보다 크면 클래스 1로 예측하고 그렇지 않으면 클래스 -1로 예측한다.
퍼셉트론 알고리즘에서 결정함수 ϕ()\phi(\cdot)는 단위 계단 함수(unit step function)를 변형한 것이다.
ϕ(z)={1zθ1else\phi(z) = \begin{cases} 1 & z \geq \theta \\ -1 & else \end{cases}
식을 간단하게 만들기 위해 임계 값 를 식의 왼쪽으로 옮겨 w0=θw_{0} = -\theta고 x0=1x_{0} = 1인 0번째 가중치를 정의한다. 이렇게 하면 zz를 좀 더 간단하게 쓸 수 있다.
z=w0x0+w1x1+... +wmxm=wTxz = w_{0}x_{0} + w_{1}x_{1} + ...  + w_{m}x_{m} = w^{T}x
결정 함수는 다음과 같다.
ϕ(z)={1zθ1else\phi(z) = \begin{cases} 1 & z \geq \theta \\ -1 & else \end{cases}
머신 러닝 분야에서 음수 임계 값 또는 가중치 w0=θw_{0} = -\theta를 절편이라고 한다.
그림 2-2는 퍼셉트론 결정함수로 최종 입력 z=wTxz = w^{T}x가 이진 출력 (-1 또는 1)으로 압축되는 방법(왼쪽)과 이를 사용하여 선형 분리가 가능한 두 개의 클래스 사이를 구별하는 방법(오른쪽)을 나타낸다.

퍼셉트론 학습 규칙

MCP 뉴런과 로젠블라트의 임계 퍼셉트론 모델 이면에 있는 전반적인 아이디어는 뇌의 뉴런 하나가 작동하는 방식을 흉내내려는 환원주의(reductionism) 접근 방식을 사용한 것이다.
즉 출력을 내거나 내지 않는 두 가지 경우만 있다. 따라서 로젠블라트의 초기 퍼셉트론 학습 규칙을 요약하면 다음과 같다.
1.
가중치를 0 또는 랜덤한 작은 값으로 초기화 한다.
2.
각 훈련 샘플 x(i)x^{(i)}에서 다음 작업을 한다.
a.
출력값 y^\hat{y}를 계산한다.
b.
가중치를 업데이트 한다.
여기서 출력 값은 앞서 정의한 단위 계단 함수로 예측한 클래스 레이블이다. 가중치 벡터 ww에 있는 개별 가중치 wjw_{j}가 동시에 업데이트 되는 것을 다음과 같이 쓸 수 있다.
wj:=wj+Δwjw_{j} := w_{j} + \Delta w_{j}
가중치 wjw_{j}를 업데이트 하는데 사용되는 Δwj\Delta w_{j} 값은 퍼셉트론 학습 규칙에 따라 계산된다.
Δwj=η(y(i)y^(i))xj(i)\Delta w_{j} = \eta (y^{(i)} - \hat{y}^{(i)}) x_{j}^{(i)}
여기서 η\eta(eta)는 학습률(learning rate)이다. (일반적으로 0.0에서 1.0 사이의 값)
y(i)y^{(i)}는 ii번째 훈련 샘플의 진짜 클래스 레이블이다.
y^(i)\hat{y}^{(i)}는 예측 클래스 레이블이다.
가중치 벡터의 모든 가중치를 동시에 업데이트 한다는 점이 중요하다. 즉, 모든 가중치 Δwj\Delta w_{j}를 업데이트 하기 전에 y^(i)\hat{y}^{(i)}를 다시 계산하지 않는다.
구체적으로 2차원 데이터셋에서는 다음과 같이 업데이트 된다.
Δw0=η(y(i)output(i))\Delta w_{0} = \eta (y^{(i)} - output^{(i)})
Δw1=η(y(i)output(i))x1i\Delta w_{1} = \eta (y^{(i)} - output^{(i)}) x_{1}^{i}
Δw2=η(y(i)output(i))x2i\Delta w_{2} = \eta (y^{(i)} - output^{(i)}) x_{2}^{i}
퍼셉트론 규칙이 어떻게 작동하는지 알아보자.
퍼셉트론이 클래스 레이블을 정확히 예측한 다음의 두 경우는 가중치가 변경되지 않고 그대로 유지된다.
Δwj=η(11)xji=0\Delta w_{j} = \eta (1 - 1) x_{j}^{i} = 0
Δwj=η((1)(1))xji=0\Delta w_{j} = \eta ((-1) - (-1)) x_{j}^{i} = 0
잘못 예측했을 때는 가중치를 양성 또는 음성 타깃 클래스 방향으로 이동시킨다.
Δwj=η(1(1))xji=η(2)xj(i)\Delta w_{j} = \eta (1 - (-1)) x_{j}^{i} = \eta(2)x_{j}^{(i)}
Δwj=η(11)xji=η(2)xj(i)\Delta w_{j} = \eta (-1 - 1) x_{j}^{i} = \eta(-2)x_{j}^{(i)}
곱셈 계수인 xj(i)x_{j}^{(i)}를 좀 더 잘 이해하기 위해 다른 예를 살펴보겠다.
y(i)=1,y^j(i)=1,η=1y^{(i)} = 1, \hat{y}_{j}^{(i)} = -1, \eta = 1
 xj(i)=0.5x_{j}^{(i)} = 0.5일 때 이 샘플을 -1로 잘못 분류했다고 가정한다.
이때 가중치가 1 만큼 증가되어 다음 번에 이 샘플을 만났을 때 최종 입력 xj(i)×wj(i)x_{j}^{(i)} \times w_{j}^{(i)}가 더 큰 양수가 된다.
단위 계단 함수의 임계 값보다 커져 샘플이 +1로 분류될 가능성이 높아질 것이다.
Δwj(i)=(1(1))0.5=(2)0.5=1\Delta w_{j}^{(i)} = (1 - (-1)) 0.5 = (2) 0.5 = 1
가중치 업데이트는 xj(i)x_{j}^{(i)}값에 비례한다. 예컨대 다른 샘플 xj(i)=2x_{j}^{(i)} = 2를 -1로 잘못 분류했다면 이 샘플을 다음번에 올바르게 분류하기 위해 더 크게 결정 경계를 움직인다.
Δwj(i)=(1(1))2=(2)2=4\Delta w_{j}^{(i)} = (1 - (-1)) 2 = (2) 2 = 4
퍼셉트론은 두 클래스가 선형적으로 구분되고 학습률이 충분히 작을 때만 수렴이 보장된다.
두 클래스를 선형 결정 경계로 나눌 수 없다면 훈련 데이터셋을 반복할 최대 횟수(에포크(epoch))를 지정하고 분류 허용 오차를 지정할 수 있다. 그렇지 않으면 퍼셉트론 가중치 업데이트를 멈추지 않는다.
그림 2-4는 퍼셉트론이 샘플 xj(i)x_{j}^{(i)}를 입력으로 받아 가중치 xj(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)
Python
이 퍼셉트론 구현을 사용하여 학습률 eta와 에포크 횟수(훈련 데이터를 반복하는 횟수) n_iter로 새로운 Perceptron 객체를 초기화 한다.
fit 메서드에서 self.w_ 가중치를 벡터 Rm+1\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()
Python
이 데이터셋에서 추출한 일부 데이터를 이용하여 퍼셉트론 알고리즘을 훈련하고 에포크 대비 잘못 분류된 오차를 그래프로 그리면 다음과 같다.
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()
Python
퍼셉트론은 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()
Python

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

이 절에서는 단일층 신경망의 또 다른 종류인 적응형 선형 뉴런(Adaptive Linear Neuron, ADALINE)을 살펴보겠다.
버나드 위드로우와 테드 호프는 프랑크 로젠블라트의 퍼셉트론 알고리즘 이 후 몇 년 지나지 않아 아달린(Adaline)을 발표했는데, 이는 퍼셉트론의 향상된 버전으로 볼 수 있다.
아달린은 연속 함수(continous function)으로 비용 함수를 정의하고 최소화하는 핵심 개념을 보여주기 때문에 흥미롭다.
아달린 규칙(위드로우-호프 규칙이라고도 함)과 로젠블라트 퍼셉트론의 가장 큰 차이점은 가중치를 업데이트 하는데 퍼셉트론처럼 단위 계단 함수 대신 선형 활성화 함수를 사용한다는 것이다.
아달린에서 선형 활성화 함수 ϕ(z)\phi(z)는 최종 입력과 동일한 함수이다.
ϕ(wTx)=wTx\phi (w^{T}x) = w^{T} x
선형 활성화 함수가 가중치 학습에 사용되지만 최종 예측을 만드는데 여전히 임계 함수를 사용하는데, 이는 앞서 보았던 단위 계단 함수와 비슷하다.
퍼셉트론과 아달린 알고리즘의 차이는 아래 그림과 같다.
아달린 알고리즘은 진짜 클래스 레이블과 선형 활성화 함수의 실수 출력 값을 비교하여 모델의 오차를 계산하고 가중치를 업데이트 한다.
반면 퍼셉트론은 진짜 클래스 레이블과 예측 클래스 레이블을 비교한다.

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

지도 학습 알고리즘의 핵심 구성 요소는 학습 과정 동안 최적화 하기 위해 정의한 목적 함수(object function)이다.
종종 최소화하려는 비용 함수가 목적 함수가 된다.
아달린은 계산된 출력과 진짜 클래스 레이블 사이의 제곱 오차합(Sum of Squared Errors, SSE)으로 가중치를 학습할 비용 함수 JJ를 정의한다.
J(w)=12i(y(i)ϕ(z(i)))2J(w) = {1 \over 2} \sum_{i} (y^{(i)} - \phi(z^{(i)}))^{2}
1/2 항은 그래디언트(gradient)를 간소하게 만들려고 편의상 추가한 것이다.
단위 계단 함수 대신 연속적인 선형 활성화 함수를 사용하는 장점은 비용 함수가 미분 가능해진다는 것이다.
이 비용 함수의 또 다른 장점은 볼록 함수라는 것이다. 간단하지만 강력한 최적화 알고리즘인 경사 하강법(gradient descent)을 적용하여 붓꽃 데이터셋의 샘플을 분류하도록 비용 함수를 최소화하는 가중치를 찾을 수 있다.
아래 그림에서는 경사 하강법 이면에 있는 핵심 아이디어를 지역 또는 전역 최솟값에 도달할 때까지 언덕을 내려오는 것으로 묘사한다.
각 반복에서 경사의 반대 방향으로 진행한다.
진행 크기는 경사의 기울기와 학습률로 결정한다.
경사 하강법을 사용하면 비용 함수 J(w)J(w)의 그래디언트 J(w)\nabla J(w) 반대 방향으로 조금씩 가중치를 업데이트 한다.
w:=w+Δww := w + \Delta w
가중치 변화량 Δw\Delta w는 음수의 그래디언트에 학습률 η\eta를 곱한 것으로 정의한다.
Δw=ηJ(w)\Delta w = - \eta \nabla J(w)
비용 함수의 그래디언트를 계산하려면 각 가중치 wjw_{j}에 대한 편도 함수를 계산해야 한다.
Jwj=i(y(i)ϕ(z(i)))xj(i){\partial J \over \partial w_{j}} = - \sum_{i} (y^{(i)} - \phi(z^{(i)})) x_{j}^{(i)}
따라서 가중치 wjw_{j}의 업데이트 공식을 다음과 같이 쓸 수 있다.
Δwj=ηJwj=ηi(y(i)ϕ(z(i)))xj(i)\Delta w_{j} = - \eta {\partial J \over \partial w_{j}} = \eta \sum_{i} (y^{(i)} - \phi(z^{(i)})) x_{j}^{(i)}
모든 가중치가 동시에 업데이트 되기 때문에 아달린 학습 규칙은 다음과 같다.
w:=w+Δww := w + \Delta w
아달린 학습 규칙이 퍼셉트론 규칙과 동일하게 보이지만 z(i)=wTx(i)z^{(i)} = w^{T} x^{(i)}인 ϕ(z(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)
Python
퍼셉트론처럼 개별 훈련 샘플마다 평가한 후 가중치를 업데이트 하지 않고, 전체 훈련 데이터셋을 기반으로 그래디언트를 계산한다.
절편(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()
Python
출력된 비용 함수 그래프에서 볼 수 있 듯 두 개의 다른 문제가 발생한다.
아래 그림 2-11 왼쪽 그래프는 학습률이 너무 클 때 발생한다.
비용 함수를 최소화 하지 못하고 오차는 에포크마다 점점 더 커진다. 전역 최솟값을 지나쳤기 때문이다.
반면 오른쪽 그래프에서는 비용이 감소하지만 학습률 η=0.0001\eta = 0.0001은 너무 작기 때문에 알고리즘이 전역 최솟값에 수렴하려면 아주 많은 에포크가 필요하다.
아래 그림 2-12는 비용 함수 JJ를 최소화하려고 특정 가중치 값을 바꾸었을 때 어떤 일이 일어나는지 보여준다.
왼쪽 그림은 적절하게 선택한 학습률의 경우로, 비용이 점차 감소하여 전역 최솟값 방향으로 이동한다.
오른쪽 그림은 너무 큰 학습률을 선택하여 전역 최솟값을 지나친다.

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

책에서 살펴볼 머신 러닝 알고리즘들은 최적의 성능을 위해 어떤 식으로든 특성 스케일을 조정하는 것이 필요하다. 이는 3, 4장에서 살펴보겠다.
경사 하강법은 특성 스케일을 조정하여 혜택을 볼 수 있는 많은 알고리즘 중 하나이다. 이 절에서는 표준화(standardization)라고 하는 특성 스케일 방법을 사용하겠다.
데이터에 표준 정규 분포의 성질을 부여하여 경사 하강법 학습이 좀 더 빠르게 수렴되도록 돕는다.
표준화는 각 특성의 평균을 0에 맞추고 특성의 표준 편차를 1로 만든다.
예컨대 jj번째 특성을 표준화 하려면 모든 샘플에서 평균 μj\mu_{j}을 빼고 표준 편차 σj\sigma_{j}로 나누면 된다.
xj=xjμjσjx'_{j} = {x_{j} - \mu_{j} \over \sigma_{j}}
여기서 xjx_{j}는 nn개의 모든 훈련 샘플에서 jj번째 특성 값을 포함한 벡터이다.
표준화 기법을 데이터셋의 각 특성 jj에 적용한다.
표준화가 경사 하강법 학습에 도움이 되는 이유 중 하나는 그림 2-13에 나온 것처럼 더 적은 단계를 거쳐 최적 혹은 좋은 솔루션을 찾기 때문이다.
그림 2-13은 2차원 분류 문제에서 모델의 가중치에 따른 비용 함수의 등고선을 보여준다.
표준화를 적용한 결과는 아래 그림 2-14와 같다.
이 그래프에서 볼 수 있듯이 학습률 η=0.01\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()
Python

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

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