[2] 아달린(Adaline, 적응형 선형 뉴런) 구현해보기

2020. 12. 31. 18:00머신러닝 교과서 with 파이썬, 사이킷런, 텐서플로

728x90
반응형

본 포스팅은 kaggle에서 bronze 메달을 받은 notebook의 코드를 따왔습니다. 관심 있으신 분은 해당 노트북을 참고해보셔도 좋을 것 같습니다. 

www.kaggle.com/choihanbin/predict-titanic-survival-by-adaptive-linear-neuron

 

이전 포스팅에서는 퍼셉트론에 대해 알아봤습니다. 이번 시간에는 이를 약간 변형한 버전인 아달린에 대해 알아보겠습니다.

2020/12/22 - [머신러닝 교과서 with 파이썬, 사이킷런, 텐서플로] - 퍼셉트론(Perceptron) 구현해보기

 

 

아달린이란?

아달린은 Adaptive linear neuron의 약자입니다. 적응형 선형 뉴런은 퍼셉트론의 향상 버전이라고 생각할 수 있습니다. 퍼셉트론과의 가장 큰 사이는 가중치를 업데이트하는 데 퍼셉트론처럼 단위 계단 함수를 사용하는 대신 선형 활성화 함수를 사용하는 것입니다. 이는 비용 함수가 미분 가능해진다는 장점이 있습니다. 이에 따라 비용 함수를 최소화하는 가중치를 찾을 수 있는 것이죠. 경사 하강법을 최적화 알고리즘으로 활용하여 구현해보도록 하겠습니다.

 

 

경사 하강법의 개념

경사 하강법이란 볼록 함수로 정의할 수 있는 비용 함수에 대해 미분을 취해 전역 최솟값을 찾을 수 있게 하는 방법입니다. 단순하게 생각하면 미분을 활용해 비용 함수의 최솟값을 찾는 것이라고 생각하시면 됩니다. 위에서 적응형 선형 뉴런은 선형 활성화 함수를 활용한다고 언급했었죠? 선형 함수(f(x) = x)인데 왜 굳이 넣어야 하냐는 생각이 드실 수 있지만 이는 바로 미분할 수 있다는 장점 때문에 그렇습니다. 미분을 하게 되면 비용 함수의 최솟값을 쉽게 찾을 수 있죠.

 

 

아달린의 가중치 학습규칙

가중치 변화량 (\(\Delta w\))는 음수의 그래디언트에 학습률 \(\eta \)를 곱한 것으로 정의됩니다. 

$$ \Delta w = -\eta▽J(w)$$

 

여기서 비용 함수의 그래디언트\(\delta\) J(w)를 구하기 위해선 각 가중치 \(w_j\)에 대한 편도 함수를 계산해야 합니다.

$$ \frac {\delta J}{\delta w_j}  = -sum_i(y^{(i)} - \phi(z^{(i)}))x^{(i)}$$

 

이에 따라 가중치\(w_j\)의 업데이트 공식을 다음과 같이 쓸 수 있습니다.

$$ \Delta w = \eta\sum_i(y^{(i)} - \phi(z^{(i)}))x^{(i)}$$

 

엄밀히 말하면 위의 방식은 배치 경사 하강법을 적용한 것입니다.

 

 

 

배치 경사 하강법 vs 확률적 경사 하강법

가중치를 어떤 식으로 업데이트하냐에 따라서 경사 하강법의 방식이 갈립니다. 훈련 세트에 있는 모든 샘플을 기반으로 가중치를 업데이트하는 경우에는 배치 경사 하강법이라고 합니다. 즉, 이전 포스팅에 있는 퍼셉트론에서 하나의 샘플별로 가중치를 업데이트하던 것과는 다른 방식인 것이죠.

$$ \Delta w = \eta\sum_i(y^{(i)} - \phi(z^{(i)}))x^{(i)} $$

배치 경사 하강법의 가중치 업데이트

* \(\eta \) : 학습률

 

 

 

하지만 매우 큰 데이터 셋에 해당 방법을 사용한다면 계산비용이 매우 많이 들겠죠. 이를 보완하기 위해 확률적 경사 하강법이라는 것을 활용합니다. 이는 각 훈련 샘플에 대해서 조금씩 가중치를 업데이트하는 것입니다. 다만 이를 활용할 때는 훈련 샘플의 순서를 무작위 하게 주입하는 것이 매우 중요하고, 매 에포크마다 같은 순서로 주입되지 않도록 섞어주는 것이 중요합니다. 훈련 샘플마다 학습이 가능하기 때문에 이는 온라인 학습으로 활용될 수 있습니다. 

 

$$ \Delta w = \eta(y^{(i)} - \phi(z^{(i)}))x^{(i)} $$

확률적 경사 하강법의 가중치 업데이트

 

 

그리고 이 둘을 절충한 미니 배치 학습이 있습니다. 이는 훈련 데이터의 작은 일부분에 배치 경사 하강법을 적용시키는 방식입니다. 가중치 업데이트가 배치 경사 하강법보다 가중치의 수렴 속도가 빠르게 되는 것이죠.

 

그렇다면 이러한 학습 규칙 하에서 어떤 식으로 아달린을 구현할 수 있는지 확인해보도록 하겠습니다. 

 

 

 

구현

필요한 라이브러리를 불러옵니다.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline

import warnings
warnings.filterwarnings('ignore')

from sklearn.preprocessing import StandardScaler

 

데이터를 로드합니다.

train = pd.read_csv('../input/titanic/train.csv')
test = pd.read_csv('../input/titanic/test.csv')
train.head()

feature를 조정합니다.

(feature engineering는 https://www.kaggle.com/choihanbin/titanic-survival-prediction-eda-ensemble를 기준으로 진행했습니다. 해당 아달린 구현에서는 중요하지 않은 부분이니 넘어가도록 하겠습니다.)

# concate dataset(train, test)
df = pd.concat(objs = [train, test], axis = 0).reset_index(drop = True)

# fill null-values in Age
age_by_pclass_sex = df.groupby(['Sex', 'Pclass'])['Age'].median()
df['Age'] = df.groupby(['Sex', 'Pclass'])['Age'].apply(lambda x: x.fillna(x.median()))

# create Cabin_Initial feature
df['Cabin_Initial'] = df['Cabin'].apply(lambda s: s[0] if pd.notnull(s) else 'M')
df['Cabin_Initial'] = df['Cabin_Initial'].replace(['A', 'B', 'C', 'T'], 'ABCT')
df['Cabin_Initial'] = df['Cabin_Initial'].replace(['D', 'E'], 'DE')
df['Cabin_Initial'] = df['Cabin_Initial'].replace(['F', 'G'], 'FG')
df['Cabin_Initial'].value_counts()

# fill null-values in Embarked
df['Embarked'] = df['Embarked'].fillna('S')

# fill null-values in Fare
df['Fare'] = df['Fare'].fillna(df.groupby(['Pclass', 'Embarked'])['Fare'].median()[3]['S'])


# binding function for Age and Fare
def binding_band(column, binnum):
    df[column + '_band'] = pd.qcut(df[column].map(int), binnum)

    for i in range(len(df[column + '_band'].value_counts().index)):
        print('{}_band {} :'.format(column, i), df[column + '_band'].value_counts().index.sort_values(ascending = True)[i])
        df[column + '_band'] = df[column + '_band'].replace(df[column + '_band'].value_counts().index.sort_values(ascending = True)[i], int(i))
        
    df[column + '_band'] = df[column + '_band'].astype(int)    
    
    return df.head()

binding_band('Age',8)
binding_band('Fare', 6)


# create Initial feature
df['Initial'] = 0
for i in range(len(df['Name'])):
    df['Initial'].iloc[i] = df['Name'][i].split(',')[1].split('.')[0].strip()

Mrs_Miss_Master = []
Others = []

for i in range(len(df.groupby('Initial')['Survived'].mean().index)):
    if df.groupby('Initial')['Survived'].mean()[i] > 0.5:
        Mrs_Miss_Master.append(df.groupby('Initial')['Survived'].mean().index[i])
    elif df.groupby('Initial')['Survived'].mean().index[i] != 'Mr':
        Others.append(df.groupby('Initial')['Survived'].mean().index[i])
    
df['Initial'] = df['Initial'].replace(Mrs_Miss_Master, 'Mrs/Miss/Master')
df['Initial'] = df['Initial'].replace(Others, 'Others')    
    
# create Alone feature
df['Alone'] = 0
df['Alone'].loc[(df['SibSp'] + df['Parch']) == 0] = 1

# create Companinon's survival rate feature
df['Ticket_Number'] = df['Ticket'].replace(df['Ticket'].value_counts().index, df['Ticket'].value_counts())
df['Family_Size'] = df['Parch'] + df['SibSp'] + 1
df['Companion_Survival_Rate'] = 0
for i, j in df.groupby(['Family_Size', 'Ticket_Number'])['Survived'].mean().index:
    df['Companion_Survival_Rate'].loc[(df['Family_Size'] == i) & (df['Ticket_Number'] == j)] = df.groupby(['Family_Size', 'Ticket_Number'])["Survived"].mean()[i, j]
    
comb_sum = df.loc[df['Family_Size'] == 5]['Survived'].sum() + df.loc[df['Ticket_Number'] == 3]['Survived'].sum()
comb_counts = df.loc[df['Family_Size'] == 5]['Survived'].count() + df.loc[df['Ticket_Number'] == 3]['Survived'].count()
mean = comb_sum / comb_counts

df['Companion_Survival_Rate'] = df['Companion_Survival_Rate'].fillna(mean)    

# select categorical features
cate_col = []
for i in [4, 11, 12, 15]:
    cate_col.append(df.columns[i])

cate_df = pd.get_dummies(df.loc[:,(cate_col)], drop_first = True)
df = pd.concat(objs = [df, cate_df], axis = 1).reset_index(drop = True)

df = df.drop(['Name', 'Sex', 'Age', 'Ticket', 'Fare', 'Embarked', 'Cabin_Initial', 'SibSp', 'Parch',
              'Cabin', 'Initial', 'Ticket_Number', 'Family_Size'], axis = 1)

# split data
df = df.astype(float)
train = df[:891]
test = df[891:]

train_X = StandardScaler().fit_transform(train.drop(columns = ['PassengerId', 'Survived']))
train_y = train.iloc[:, 1]
test_X = StandardScaler().fit_transform(test.drop(columns = ['PassengerId', 'Survived']))

 

배치 경사 하강법을 사용한 아달린 구현하기

class AdalineBGD(object):
    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):
        rgen = np.random.RandomState(self.random_state)
        self.w_ = rgen.normal(loc = 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
            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, 1, 0)

이전에 구현한 퍼셉트론의 틀에서 달라진 점은 활성화 함수와 가중치를 업데이트하고 비용 함수를 계산하는 방식이 달라졌다는 점이네요.

 

 

학습률에 따른 비용 그래프

fig, ax = plt.subplots(1, 2, figsize = (10, 4))
PCbgd1 = AdalineBGD(eta = 0.01, n_iter = 10)
PCbgd1.fit(train_X, train_y)
ax[0].plot(range(1, len(PCbgd1.cost_) + 1),
          np.log10(PCbgd1.cost_), marker = 'o')
ax[0].set_xlabel('Epochs')
ax[0].set_ylabel('log(SSE)')
ax[0].set_title('Adaline - learning rate 0.01')

PCbgd2 = AdalineBGD(eta = 0.0001, n_iter = 10)
PCbgd2.fit(train_X, train_y)
ax[1].plot(range(1, len(PCbgd2.cost_) + 1),
          np.log10(PCbgd2.cost_), marker = 'o')
ax[1].set_xlabel('Epochs')
ax[1].set_ylabel('log(SSE)')
ax[1].set_title('Adaline - Learning rate 0.0001')
plt.show()

학습률이 0.01일 때는 너무 커서 전역해를 지나고 학습을 진행할 때마다 점차 비용이 증가하는 것을 볼 수 있습니다. 반면 학습률이 0.0001일 때는 학습률이 너무 작아서 전역해에 도달하지 못한 것을 볼 수 있어요.

 

fig, ax = plt.subplots(figsize = (15, 6))
PCbgd3 = AdalineBGD(eta = 0.0005, n_iter = 10)
PCbgd3.fit(train_X, train_y)
ax.plot(range(1, len(PCbgd3.cost_) + 1),
          np.log10(PCbgd3.cost_), marker = 'o')
ax.set_xlabel('Epochs')
ax.set_ylabel('log(SSE)')
ax.set_title('Adaline - Learning rate 0.0001')
plt.show()

 

적절한 학습률과 에포크를 찾았습니다. 

 

확률적 경사 하강법을 사용한 아달린 구현하기

class AdalineSGD(object):
	def __init__(self, eta = 0.1, 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):
        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_weight(xi, target))
            avg_cost = sum(cost) / len(y)
            self.cost_.append(avg_cost)
        return self
    
    def partial_fit(self, X, y):
        # train data not reset weights
        if not self.w_initialized:
            self._initialize_weight(X.shape[1])
        if y.ravel().shape[0] > 1:
            for xi, target in zip(X, y):
                self._update_weight(xi, target)
        else:
            self._update_weight(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, scale = 0.01,
                                  size = 1 + m)
        self.w_initialized = True
        
    def _update_weight(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 = (error**2) / 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, 1, 0)

다소 복잡해 보입니다. 이는 추가로 학습 데이터가 주어지더라도 이미 만들어진 모델에 추가로 구현할 수 있도록 만들었기 때문입니다. 배치 경사 하강법과는 가중치를 업데이트하는 방식이 다른 것을 확인할 수 있습니다. 위에서 설명드린 것처럼 훈련 샘플의 순서를 무작위로 섞어서 각 에포크마다 순서를 달리하도록 하기 위해 shuffle이라는 함수를 추가했습니다.

 

 

학습률에 따른 비용 그래프

fig, ax = plt.subplots(1, 2, figsize = (10, 4))

for i, eta in enumerate([0.05, 0.0001]):
    AD = AdalineSGD(eta = eta, n_iter = 10)
    AD.fit(train_X, train_y)
    ax[i].plot(range(1, len(AD.cost_) + 1),
              np.log10(AD.cost_), marker = 'o')
    ax[i].set_title('Adaline - Learing rate {}'.format(eta))
    ax[i].set_xlabel('Epochs')
    ax[i].set_ylabel('Log(SSE)')

fig, ax = plt.subplots(figsize = (15, 6))
ADsgd = AdalineSGD(eta = 0.005, n_iter = 10)
ADsgd.fit(train_X, train_y)
ax.plot(range(1, len(PCbgd3.cost_) + 1),
          np.log10(PCbgd3.cost_), marker = 'o')
ax.set_xlabel('Epochs')
ax.set_ylabel('log(SSE)')
ax.set_title('Adaline - Learning rate 0.005')
plt.show()

각 모델별로 적절한 학습률과 에포크를 선정해 kaggle 리더보드에 제출했을 때는 생각보다 낮은 *정확도가 나왔습니다.

* 배치 경사 하강법 : 0.65550

* 확률적 경사 하강법 : 0.69617

이는 아달린 모델이 해당 데이터 셋에는 맞지 않기 때문으로 보입니다.

 

 

다음 포스팅에서는 로지스틱 회귀분석에 해 알아보도록 하겠습니다. 

728x90
반응형