본 장에서는 또 다른 single-layer nerual network: ADAptive Linear NEuron(ADaline)
에 대해 알아본다. Adaline
은 퍼셉트론 이후에 나온 개념이라, 퍼셉트론에 비해 조금 더 발전했다고 생각된다.
Adaline
은 퍼셉트론과 비교해 연속 손실 함수를 정의하고, 이를 최소화하는 개념을 보여주었다. 이는 추후 Logistic Regression
, Support Vector Machine
, Multi-layer Nerual Network
, Linear Regression
등의 기반이 되는 개념이다.
Adaline
과 퍼셉트론의 가장 큰 차이점은, linear activation function
에 기반해 w
를 업데이트 한다는 점이다. 해당 linear activation function
은 Threshold function
전에 위치해서, 이를 통해 w
를 업데이트 한다.
지도학습에서 중요한 목적 함수는, 주로 우리가 최소화하고자 하는 loss function
이나 cost function
을 의미한다. Adaline
에서의 목적 함수는 L
로 정의하며, 이는 모델의 결과 값과 실제 클래스 레이블 사이의 MSE(Mean Square Error)
로 모델 파라미터를 학습시킨다.
손실 함수를 이용해 Gradient Descent
기법을 사용할 수 있다. 이는 가장 최적화된 학습을 가능하게 해주는 기법으로, 손실 함수를 최소화하는 w
를 찾도록한다.
국문으로 경사 하강법이라고 부르는 Gradient Descent
는 각 Iteration
마다 Global Loss Minimum
에 도달하기 위해 노력하며, 내려가는 폭, step size
는 미리 정의한 learning rate
에 의해 결정된다.
loss
가 최소가 되려면 위의 그림에서 표현한 것 처럼, 경사가 하강해야 하기 때문에, 수식으로 나타내면 음의 learning rate
의 방향으로 진행된다.
데이터 셋의 모든 학습 예제를 파악하는 것을 full batch gradient descent
로 표현한다.
Adaline
은 퍼셉트론과 매우 유사하기 때문에, 퍼셉트론 코드에서 fit method
만 수정해 사용한다.
class AdalineGD:
"""ADAptive LInear NEuron classifier.
Parameters
------------
eta : float
Learning rate (between 0.0 and 1.0)
n_iter : int
Passes over the training dataset.
random_state : int
Random number generator seed for random weight initialization.
Attributes
-----------
w_ : 1d-array
Weights after fitting.
b_ : Scalar
Bias unit after fitting.
losses_ : list
Mean squared error loss function values in each epoch.
"""
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):
""" Fit training data.
Parameters
----------
X : {array-like}, shape = [n_examples, n_features]
Training vectors, where n_examples is the number of examples and n_features is the number of features.
y : array-like, shape = [n_examples] Target values.
Returns
-------
self : object
"""
rgen = np.random.RandomState(self.random_state)
self.w_ = rgen.normal(loc=0.0, scale=0.01, size=X.shape[1])
self.b_ = np.float_(0.)
self.losses_ = []
for i in range(self.n_iter):
net_input = self.net_input(X)
output = self.activation(net_input)
errors = (y - output)
self.w_ += self.eta * 2.0 * X.T.dot(errors) / X.shape[0]
self.b_ += self.eta * 2.0 * errors.mean()
loss = (errors**2).mean()
self.losses_.append(loss)
return self
def net_input(self, X):
"""Calculate net input"""
return np.dot(X, self.w_) + self.b_
def activation(self, X):
"""Compute linear activation"""
return X
def predict(self, X):
"""Return class label after unit step"""
return np.where(self.activation(self.net_input(X)) >= 0.5, 1, 0)
위 코드에서는 퍼셉트론처럼 학습 예제 각각을 평가하고 w
를 업데이트 하는 것이 아니라, 전체 트레이닝 데이터셋을 기반으로 gradient
를 계산한다.
모델에 알맞는 학습률을 선정하는 것은 매우 중요한 일이다. 이를 찾기 위해 본 장에서는 0.1, 0.0001 두 가지의 학습률을 plot
으로 나타내보고, 어느 학습률이 좋은지 파악한다.
fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10, 4))
ada1 = AdalineGD(n_iter=15, eta=0.1).fit(X, y) # < 0.1
ax[0].plot(range(1, len(ada1.losses_) + 1), np.log10(ada1.losses_), marker='o')
ax[0].set_xlabel('Epochs')
ax[0].set_ylabel('log(Mean squared error)')
ax[0].set_title('Adaline - Learning rate 0.1')
ada2 = AdalineGD(n_iter=15, eta=0.0001).fit(X, y) # < 0.0001
ax[1].plot(range(1, len(ada2.losses_) + 1), ada2.losses_, marker='o')
ax[1].set_xlabel('Epochs')
ax[1].set_ylabel('Mean squared error')
ax[1].set_title('Adaline - Learning rate 0.0001')
plt.show()
아래는 위 코드를 실행해 두 가지 학습률을 비교한다.
왼쪽 plot
이 0.1의 학습률을, 오른쪽이 0.0001의 학습률을 이용해 학습을 진행한 결과다. 학습률 0.1에서는 global minimum
에 수렴하지 못하고, MSE
값이 더욱 커져 발산하는 형태를 확인할 수 있다. 학습률 0.0001에서는 MSE
값이 작아지고 있긴 하지만, 학습률이 너무 낮은 관계로 global minimum
에 다가가다 멈춰버린 상황이다. 결국 두 학습률 모두 적절하지 않았다.
위는 학습이 잘 이루어지고 있는 왼쪽 그림과, 잘못된 (큰) 학습률을 선택했을 때 나타나는 gradient descent
를 보여준다. 왼쪽 그림의 경우 일정한 w
만큼, global minimum
을 향해 하강하고 있지만, 오른쪽 그림의 경우 너무 큰 학습률로 인해 global minimum
에 수렴하지 못하고 발산하는 형태를 보인다.
본 장에서는 standardization
을 통한 feature scaling
을 알아본다. normalization
의 경우
gradinet descent
의 속도에 가속을 줄 수 있는 장점이 있지만, 데이터 셋의 일반적인 특성의 학습이 어렵다.
standardization
의 경우 각 feature
를 모두 반영할 수 있도록 설계한다. 즉,x_j
가 모든 학습 예 n
의 j
번째 feature
라고 할 때 standardization
은 데이터셋에 포함된 모든 j
를 의미한다.
standardization
은 모든 w
가 잘 동작할 수 있는 학습률을 찾아주는 능력이 있기 때문에,
gradient descent
에서 많이 사용한다. 만약 scale
이 다른 feature
들이 있을 때 학습률에 따라 어떤 w
는 큰 업데이트를, 또 다른 w
는 작은 업데이트를 진행하게 될 텐데, 이때 standardization
이 두 w
의 균형을 맞추며 적절한 학습률을 찾아줄 수 있다.
이는 Numpy
라이브러리의 mean
과 std
메소드로 쉽게 구현할 수 있다.
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()
standardization
을 진행한 후, Adaline
을 20 epochs
, 학습률 0.5로 다시 학습했다.
ada_gd = AdalineGD(n_iter=20, eta=0.5)
ada_gd.fit(X_std, y)
plot_decision_regions(X_std, y, classifier=ada_gd)
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_gd.losses_) + 1), ada_gd.losses_, marker='o')
plt.xlabel('Epochs')
plt.ylabel('Mean squared error')
plt.tight_layout()
plt.show()
그에 대한 결과로 아래와 같은 그림을 확인할 수 있다. 정확히 잘 분류하는 모습이며, 오른쪽 그림에 나와있는 MSE
같은 경우, 아무리 모델이 잘 분류한다고 해도 error
값이 0이 되진 않음을 기억해야 한다.
지금까지는 작은 데이터셋에서의 학습을 고려했다면, 본 장에서는 데이터가 매우 많은 데이터 셋의 경우를 살펴본다. 즉, full-batch gradinet descent
가 힘든 환경에서의 학습을 살펴본다.
일반적으로 stochastic gradient descent(SGD)
는 더 빠른 속도로 minimum
에 수렴하는데, 이때 각 gradient
는 하나의 훈련 예시를 기반으로 계산되기 때문에, error surface
에 noise
가 발생할 가능성이 있고, local minima
에서 더 잘 빠져나올 수 있는 특성이 있다. 또한, 온라인 학습이 가능하기 때문에, 수정이 용이하다.
SGD
를 이용해 좋은 결과를 얻기 위해서는, 데이터를 꼭 랜덤하게 제공해야 하기 때문에, 각 epoch
에 데이터를 shuffle
하도록 한다. 이외에도 Mini-batch gradient descent
는 full-batch gradient descent
를 여러 개의 subset
으로 나누어 학습을 진행하는 방식이다.
이미 앞선 코드에서 GD
를 이용한 Adaline
을 구현했기 때문에, 코드 속 fit()
메소드를 조금 바꿔서, 각 학습 Iteration
이 지나고 w
가 업데이트 되는 식으로 변경하면 SGD
를 적용할 수 있다. 또한 online learning
을 위한 partial_fit
메소드를 추가적으로 구현하고, 앞서 설명한 SGD
의 데이터셋 shuffle
을 위한 코드를 추가로 작성한다.
class AdalineSGD:
"""ADAptive LInear NEuron classifier.
Parameters
------------
eta : float
Learning rate (between 0.0 and 1.0)
n_iter : int
Passes over the training dataset.
shuffle : bool (default: True)
Shuffles training data every epoch if True to prevent
cycles.
random_state : int
Random number generator seed for random weight
initialization.
Attributes
-----------
w_ : 1d-array
Weights after fitting.
b_ : Scalar
Bias unit after fitting.
losses_ : list
Mean squared error loss function value averaged over all
training examples in each epoch.
"""
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):
""" Fit training data.
Parameters
----------
X : {array-like}, shape = [n_examples, n_features]
Training vectors, where n_examples is the number of
examples and n_features is the number of features.
y : array-like, shape = [n_examples]
Target values.
Returns
-------
self : object
"""
self._initialize_weights(X.shape[1])
self.losses_ = []
for i in range(self.n_iter):
if self.shuffle:
X, y = self._shuffle(X, y)
losses = []
for xi, target in zip(X, y):
losses.append(self._update_weights(xi, target))
avg_loss = np.mean(losses)
self.losses_.append(avg_loss)
return self
def partial_fit(self, X, y):
"""Fit training data without reinitializing the weights"""
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):
"""Shuffle training data"""
r = self.rgen.permutation(len(y))
return X[r], y[r]
def _initialize_weights(self, m):
"""Initialize weights to small random numbers"""
self.rgen = np.random.RandomState(self.random_state)
self.w_ = self.rgen.normal(loc=0.0, scale=0.01, size=m)
self.b_ = np.float_(0.)
self.w_initialized = True
def _update_weights(self, xi, target):
"""Apply Adaline learning rule to update the weights"""
output = self.activation(self.net_input(xi))
error = (target - output)
self.w_ += self.eta * 2.0 * xi * (error)
self.b_ += self.eta * 2.0 * error
loss = error**2
return loss
def net_input(self, X):
"""Calculate net input"""
return np.dot(X, self.w_) + self.b_
def activation(self, X):
"""Compute linear activation"""
return X
def predict(self, X):
"""Return class label after unit step"""
return np.where(self.activation(self.net_input(X)) >= 0.5, 1, 0)
코드 속 np.random
메소드를 이용해 shuffle
을 구현한다.
최종적으로 fit
메소드를 이용해 AdalineSGD classifier
를 확인한다.
ada_sgd = AdalineSGD(n_iter=15, eta=0.01, random_state=1)
ada_sgd.fit(X_std, y)
plot_decision_regions(X_std, y, classifier=ada_sgd)
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_sgd.losses_) + 1), ada_sgd.losses_, marker='o')
plt.xlabel('Epochs')
plt.ylabel('Average loss')
plt.tight_layout()
plt.show()
왼쪽 그림으로보아, 완전히 잘 학습 했음을 알 수 있고, 오른쪽 그림에서 2번째 epoch
부터 loss
가 대략 0.02 이하로 수렴하는 것으로 보아, 훨씬 더 빠른 속도로 모델이 수렴함을 알 수 있다.