2. 분류를 위한 간단한 학습 알고리즘들 - 2

KangMyungJoe·2022년 9월 15일
0
post-thumbnail

Adaptive linear neurons and the convergence of learning

본 장에서는 또 다른 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 functionThreshold function 전에 위치해서, 이를 통해 w를 업데이트 한다.

Minimizing loss functions with gradient descent

지도학습에서 중요한 목적 함수는, 주로 우리가 최소화하고자 하는 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로 표현한다.

Implementing Adaline in Python

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에 수렴하지 못하고 발산하는 형태를 보인다.

Improving gradient descent through feature scaling

본 장에서는 standardization을 통한 feature scaling을 알아본다. normalization의 경우
gradinet descent의 속도에 가속을 줄 수 있는 장점이 있지만, 데이터 셋의 일반적인 특성의 학습이 어렵다.

standardization의 경우 각 feature를 모두 반영할 수 있도록 설계한다. 즉,x_j가 모든 학습 예 nj번째 feature라고 할 때 standardization은 데이터셋에 포함된 모든 j를 의미한다.

standardization은 모든 w가 잘 동작할 수 있는 학습률을 찾아주는 능력이 있기 때문에,
gradient descent에서 많이 사용한다. 만약 scale이 다른 feature들이 있을 때 학습률에 따라 어떤 w는 큰 업데이트를, 또 다른 w는 작은 업데이트를 진행하게 될 텐데, 이때 standardization이 두 w의 균형을 맞추며 적절한 학습률을 찾아줄 수 있다.

이는 Numpy 라이브러리의 meanstd 메소드로 쉽게 구현할 수 있다.

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이 되진 않음을 기억해야 한다.

Large-scale machine learning and stochastic gradient descent

지금까지는 작은 데이터셋에서의 학습을 고려했다면, 본 장에서는 데이터가 매우 많은 데이터 셋의 경우를 살펴본다. 즉, full-batch gradinet descent가 힘든 환경에서의 학습을 살펴본다.

일반적으로 stochastic gradient descent(SGD)는 더 빠른 속도로 minimum에 수렴하는데, 이때 각 gradient는 하나의 훈련 예시를 기반으로 계산되기 때문에, error surfacenoise가 발생할 가능성이 있고, local minima에서 더 잘 빠져나올 수 있는 특성이 있다. 또한, 온라인 학습이 가능하기 때문에, 수정이 용이하다.

SGD를 이용해 좋은 결과를 얻기 위해서는, 데이터를 꼭 랜덤하게 제공해야 하기 때문에, 각 epoch에 데이터를 shuffle 하도록 한다. 이외에도 Mini-batch gradient descentfull-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 이하로 수렴하는 것으로 보아, 훨씬 더 빠른 속도로 모델이 수렴함을 알 수 있다.

profile
소통을 잘하는 개발자가 되고 싶습니다.

0개의 댓글