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

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

맥컬록-피츠 뉴런 : 간소화된 뇌의 뉴런 개념 -> 신경 세포를 이진 출력을 내는 간단한 논리 회로로 표현
퍼셉트론 : 자동으로 최적의 가중치를 학습하는 알고리즘을 제안

2.1.1 인공 뉴런의 수학적 정의

인공 뉴런 아이디어 - 두 개의 클래스가 있는 이진 분류 작업

결정 함수 : 단위 계단 함수를 변형한 것


2.1.2 퍼셉트론 학습 규칙

☑️과정☑️
1) 가중치를 0 또는 랜덤한 작은 값으로 초기화
2) 각 훈련 샘플 x(i)에서 다음 작업을 함

  • a. 출력 값 y(단위 계단 함수로 예측한 클래스 레이블)를 계산
  • b. 가중치를 업데이트(n은 학습률, y(i)는 i번째 훈련 샘플의 진짜 클래스 레이블)

퍼셉트론은 두 클래스가 선형적으로 구분되고, 학습률이 충분히 작을 때만 수렴이 보장됨 -> 두 클래스를 선형 결정 경계로 나눌 수 없다면 훈련 데이터셋을 반복할 최대 횟수(에포크)를 지정하고 분류 허용 오차를 지정



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

2.2.1 객체 지향 퍼셉트론 API

rgen : 넘파이 난수 생성기로 사용자가 지정한 랜덤 시드로 이전과 동일한 결과 재현 가능
np.across : 역코사인 삼각 함수
np.linalg.norm : 벡터 길이를 계산하는 함수
np.dot : 벡터 점곱을 계산

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.01, size=1 + X.shape[1])  # 가중치를 벡터 R(m+1)로 초기화, m은 데이터셋에 있는 특성 개수
        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)

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

꽃받침 길이와 꽃잎 길이만 사용
이진 분류기(일대다 전략) -> 붓꽃 데이터셋에서 두 개의 꽃만 사용(다중 클래스 분류로 확장 가능)

☑️ pandas 라이브러리를 사용하여 UCI 머신 러닝 저장소에서 붓꽃 데이터셋을 DataFrame 객체로 직접 로드 ☑️ tail 메서드로 마지막 다섯 줄을 출력

import os
import pandas as pd


s = 'https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data'
print('URL:', s)

df = pd.read_csv(s,
                 header=None,
                 encoding='utf-8')

df.tail()

☑️ 50개의 Iris-setoa와 50개의 Iris-verisicolor 꽃에 해당하는 처음 100개의 클래스 레이블을 추출
☑️ 클래스 레이블을 두 개의 정수 클래스 1(verisicollor)와 -1(setosa)로 바꾼 후 벡터 y에 저장
☑️ 100개의 훈련 샘플에서 첫 번째 특성 열(꽃받침 길이)과 세 번째 특성 열(꽃잎 길이)을 추출하여 특성 행렬 x에 저장
☑️ 2차원 산점도로 시각화

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.savefig('images/02_06.png', dpi=300)
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 updates')

# plt.savefig('images/02_07.png', dpi=300)
plt.show()


-> 여섯번째 에포크 이후에 수렴했고 훈련 샘플을 완벽하게 분류함

<2차원 데이터셋의 결정 경계를 시각화>
☑️ colors와 markers를 정의 -> ListendColormap을 사용하여 colors 리스트에서 컬러맵을 만듦
☑️ 두 특성의 최솟값과 최댓값을 찾고 이 벡터로 넘파이 meshgrid 함수로 그리드 배열 xx1과 xx2 쌍을 만듦
☑️ 그리드 배열을 펼치고 훈련 데이터와 같은 개수의 열이 되도록 행렬을 만듦
☑️ predict 메서드로 그리드 각 포인트에 데ㅐ응하는 클래스 레이블 Z을 예측
☑️ 클래스 레이블 Z를 xx1, xx2 같은 차원의 그리드로 크기 변경 -> matplotlib의 contourf 함수로 등고선 그래프 그리기 (결정 영역 나타냄)

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

# plt.savefig('images/02_08.png', dpi=300)
plt.show()



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

아달린 규칙과 로젠블라트 퍼셉트론의 차이점 : 가중치를 업데이트하는 데 선형 활성화 함수(단순한 항등 함수)를 사용

아달린 알고리즘 - 진짜 클래스 레이블 & 선형 활성화 함수의 실수 출력 값 비교 -> 모델 오차 계산하고 가중치 업데이트
퍼셉트론 - 진짜 클래스 레이블 & 예측 클래스 레이블을 비교


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

경사하강법 : 1차 근삿값 발견용 최적화 알고리즘으로, 기울기를 구하고 경사의 절댓값이 낮은 쪽으로 계속 이동시켜 극값에 이를 때까지 이를 반복시키는 것

배치 경사 하강법 : 전체 데이터에 대한 기울기를 한번만 계산하여 가중치를 업데이트하는 방법


2.3.2 파이썬으로 아달린 구현

☑️ fit 메서드를 바꾸어 경사 하강법으로 비용 함수가 최소화되도록 가중치를 업데이트 (최종 입력, 활성화, 출력 순으로 진행)

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)
            # 이 코드의 활성화 함수는 항등 함수(identity function)이기 때문에
            # 아무런 효과가 없습니다.
            # 이 대신 `output = self.net_input(X)`로 바로 쓸 수 있습니다.
            # 이 활성화 함수는 개념적인 목적을 위해 만들었습니다.
            # (잠시 후에 보게 될) 로지스틱 회귀의 경우 이 함수를 시그모이드 함수로 
            # 바꾸어 로지스틱 회귀 분류기를 구현합니다.
            output = self.activation(net_input)
            errors = (y - output)
            # X.T.dot(errors) : 특성 행렬과 오차 벡터 간의 행렬-벡터 곱셈
            self.w_[1:] += self.eta * X.T.dot(errors) # 가중치 1에서 m까지
            self.w_[0] += self.eta * errors.sum() # 절편(0번째 가중치)
            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)

☑️ 두 개의 학습률(0.1, 0.0001)에서 에포크 횟수 대비 비용 그래프 그리기

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.savefig('images/02_11.png', dpi=300)
plt.show()


-> 적절한 학습률로 하는 것이 중요


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

경사 하강법은 표준화라는 특성 스케일 방법을 사용

☑️ 넘파이 내장 함수 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()

☑️ 표준화한 후 다시 아달린 모델을 훈련하고 학습률 0.01에서 몇 번의 에포크만에 수렴하는지 확인

ada_gd = AdalineGD(n_iter=15, eta=0.01)
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.savefig('images/02_14_1.png', dpi=300)
plt.show()

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

plt.tight_layout()
# plt.savefig('images/02_14_2.png', dpi=300)
plt.show()


-> 학습률 0.01을 사용하고 표준화된 특성에서 훈련하여 아달린 모델 수렴됨


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

확률적 경사 하강법 : 추출된 각 훈련 샘플에 대해서 가중치를 계산하고, 경사 하강법을 적용하는 방법

  • 수렴 속도가 훨씬 빠름
  • 온라인 학습으로 사용 가능(새로운 훈련 데이터가 도착하는 대로 모델 훈련 -> 시스템은 변화에 즉시 적응)

☑️ fit 메서드 안에서 각 훈련 샘플에 대해 가중치를 업데이트
☑️ partial_fit 메서드 구현(온라인 학습에서 사용 가능)
☑️ 훈련 후에는 알고리즘이 수렴하는지 확인하기 위해 에포크마다 훈련 샘플의 평균 비용 계산

import numpy as np

class AdalineSGD(object):
    """ADAptive LInear NEuron 분류기

    Parameters
    ------------
    eta : float
      학습률 (0.0과 1.0 사이)
    n_iter : int
      훈련 데이터셋 반복 횟수
    shuffle : bool (default: True)
      True로 설정하면 같은 반복이 되지 않도록 에포크마다 훈련 데이터를 섞습니다
    random_state : int
      가중치 무작위 초기화를 위한 난수 생성기 시드

    Attributes
    -----------
    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):
        """훈련 데이터 학습

        Parameters
        ----------
        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._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):
        """훈련 데이터를 섞습니다"""
        r = self.rgen.permutation(len(y)) # 0에서 100까지 중복되지 않은 랜덤한 숫자 시퀀스를 생성 
        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)

☑️ fit 메서드로 AdalineSGD 분류기 훈련
☑️ plot_deicision_regions로 훈련 결과를 그래프로 그림

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.savefig('images/02_15_1.png', dpi=300)
plt.show()

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

plt.tight_layout()
# plt.savefig('images/02_15_2.png', dpi=300)
plt.show()


-> 평균 비용이 상당히 빠르게 감소
-> 스트리밍 데이터를 사용하는 온라인 학습 방식으로 훈련하려면 개개의 샘플마다 partial_fit 메서드를 호출

# 예시
ada.partial_fit(X_std[0, :], y[0])

좋은 웹페이지 즐겨찾기