2장. 간단한 분류 알고리즘 훈련

김선재·2021년 11월 19일
0
post-thumbnail

이 장에서 다를 주제

  • 머신 러닝 알고리즘을 직관적으로 이해하기
  • 판다스, 넘파이, 맷플롯립으로 데이터를 읽고 처리하고 시각화하기
  • 파이썬으로 선형 분류 알고리즘 구현하기

💡 파이썬을 사용해서 단계적으로 퍼셉트론을 구현하고 붓꽃 데이터셋에서 훈련하여 꽃 품종을 분류

인공 뉴런

  • 뉴런들은 뇌의 신경 세포와 서로 연결되어 있다
  • 1943년 워렌 맥컬록( Warren McCulloch )과 월터 피츠( Walter Pitts )는 처음으로 간소화된 뇌의 뉴런 개념을 발표
    • 이를 맥컬록-피츠( MCP ) 뉴런이라고 한다.

      신경세포를 이진 출력을 내는 간단한 논리 회로롤 표현

  • 몇 년 후 프랑크 로젠블라트( Frank Rosenblatt )는 MCP 뉴런 모델을 기반으로 퍼셉트론 학습 개념을 처음 발표

    퍼셉트론 규칙에서 자동으로 최적의 가중치를 학습하는 알고리즘을 제안

📌 퍼셉트론에 대한 내용은 이쪽을 참고!!
https://velog.io/@ksj5738/%ED%8E%98%EC%85%89%ED%8A%B8%EB%A1%A0Perceptron

퍼셉트론 학습 알고리즘 구현

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.01, 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 메서드는 가중치를 초기화한 후 훈련 데이터셋에 있는 모든 개체의 샘플을 반복 순회하면서 퍼셉트론 학습 규칙에 따라 가중치를 업데이트
  • 클래스 레이블은 predict 메서드에서 예측
  • 에폭마다 self.errors_ 리스트에 잘못 분류된 횟수를 기록
    • 나중에 훈련하는 동안 얼마나 퍼셉트론을 잘 수행했는지 확인 할 수 있다.
  • net_input 메서드에서 사용한 np.dot 함수는 벡터 점곱 wTxw^Tx를 계산

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

💡 산점도에 훈련 모델의 결정 경계를 그리기 위해 꼬받침 길이와 꽃잎 길이만 사용
💡 퍼셉트론이 이진 분류기이기 때문에 붓꽃 데이터셋에서 두개의 꽃 Setosa, Versicolor만 사용

📍 붓꽃 데이터셋 불러오기

import os
import pandas as pd

s = os.path.join('http://archive.ics.uci.edu', 'ml',
                'machine-learning-databases', 'iris', 'iris.data')
print('url:', s)

💡 책에서는 url로 접근해 데이터를 가져왔지만, url을 통해 데이터를 다운받아 사용했다.

df = pd.read_csv('./input_data/iris.data', header=None, encoding='utf-8')

df.tail()

📍 붓꽃 데이서 시각화

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()

📍 퍼셉트론 알고리즘으로 붓꽃데이터 Training

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 updates')

plt.show()

📍 결정 경계 그리기

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.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('sepal length [cm]')
plt.legend(loc='upper left')

plt.show()

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

아달린( Adaline )

  • 버나드 위드로우( Bernard Widrow )와 테드 호프( Tedd Hoff )가 발표
  • 퍼셉트론의 향상된 버전
  • 연속 함수( Continuous function )로 비용 함수를 정의하고 최소화하는 핵심 개념을 보여준다.
  • 가중치를 업데이트하는 데 선형 활성화 함수를 사용

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

  • 목적 함수 : 지도 학습 알고리즘 에서 학습 과정 동안 최적화하기 위해 정의한 함수
  • 아달린은 계산된 출력과 진짜 클래스 레이블 사이의 제곱 오차합으로 가중치를 학습하기 위핸 비용 함수 JJ를 정의
  • 단위 계단 함수 대신 연속적인 선형 활성화 함수를 사용해 비용 함수가 미분이 가능해 진다.

📍 경사하강법에 대한 내용 참고!!
https://velog.io/@ksj5738/%EC%88%98%EC%B9%98%EB%AF%B8%EB%B6%84%EA%B3%BC-%EA%B2%BD%EC%82%AC%ED%95%98%EA%B0%95%EB%B2%95

아달린 구현

class AdalineGD(object):
    '''적응형 선형 뉴런 분류기
    
    매개 변수
    ----------------------
    eta : float
        학습률 ( 0.0과 1.0 사이 )
    n_iter : int
        훈련 데이터셋 반복 횟수
    random_state : int
        가중치 무작위 초기화를 위한 난수 생성기 시드
        
    속성
    ----------------------
    w_ : 1d-array
        학습된 가중치
    cost_ : 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.activation(self.net_input(x)) >= 0.0, 1, -1)
  • 퍼셉트론 구현에서 fit 메서드를 바꾸어 경사 하강법으로 비용 함수가 최소화되도록 가중치를 업데이트
  • 개별 훈련 샘플마다 평가한 후 가중치를 업데이트 하지 않고 전체 훈련 데이터셋을 기반으로 그레이디언트를 계산

📍 에폭 횟수 대비 비용 그래프 그리기

fig, 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()

  • 왼쪽 그래프는 학습률이 너무 커 전역 최솟값을 지나쳐버렸다.
  • 오른쪽 그래프는 학습률이 너무 작아 전역 최솟값에 수렴하려면 오래걸린다.

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

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.tight_layout()

plt.show()

확률적 경사 하강법 적용

  • 배치 경사 하강법의 다른 대안으로 인기가 높다
  • 모든 샘플 x(i)x^(i^)에 대하여 누적된 오차의 합을 기반으로 가중치를 업데이트하는 대신 각 훈련 샘플에 대해 조금씩 가중치를 업데이트
class AdalineSGD(object):
    '''ADAaptive 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_sample, n_features ]
            n_samples개의 샘플과 n_features개의 특성으로 이루어진 훈련 데이터
        y : array-like, shape = [ n_smaples ]
            타깃 벡터
            
        반환값
        --------------
        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.shapep[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)
  • _shuffle 메서드는 np.random 모듈의 permutation 함수로 0에서 100까지 중복되지 않은 랜덤한 숫자 시권스를 생성

📍 훈련 결과 시각화

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.tight_layout()

plt.show()

  • 평균 비용이 상당히 빠르게 감소
profile
data science!!, data analyst!! ///// hello world

0개의 댓글