2020. 12. 31. 18:00ㆍ머신러닝 교과서 with 파이썬, 사이킷런, 텐서플로
요즘 머신러닝 교과서 with 파이썬, 사이킷런, 텐서플로라는 책을 보면서 공부하고 있어요. 사실 전에 전공수업들을 때 교재여서 과제 제출할 때 참고하는 용도로 읽다가 종강한 뒤에는 책을 안폈어요. ㅎㅎ... 그러다가 최근에 텐서플로에 대해서 알아볼 게 있어서 읽어봤는데 생각보다 자세하고 이해하기 쉽게 써있더라고요. 그래서 이번 기회에 정리하면서 제대로 공부하려고 합니다.
우선 첫 챕터는 머신러닝의 종류와 개념들, 그리고 머신러닝의 작업 흐름에 대해서 설명해요. 수업 시간에 많이 다뤘던 내용이라 이 부분은 한 번 읽고 넘겼습니다! 나중에 기회가 된다면 해당 내용도 다뤄보도록 할게요.
두 번째 챕터는 퍼셉트론을 활용해서 간단한 분류 알고리즘을 직접 만들어보는건데, 읽다가 흥미로워서 정리할 겸 코드를 짜봤습니다. 교재에서는 Iris dataset(붓꽃 데이터셋)을 활용한 예제를 작성했어요. 하지만 너무 그대로 따라하는 건 퍼셉트론 알고리즘을 이해하는데 큰 도움이 되지 않을 것 같기도 하고, 만들고자하는 알고리즘이 분류 알고리즘이기에 'Titanic dataset'을 활용해보려합니다! 이전에 Kaggle에 Titanic notebook을 올렸는데 이를 참고해서 feature engineer를 진행한 data를 가지고 진행했습니다.
1. 인공 뉴런의 수학적 정의
우선 Perceptron을 구현하기에 앞서 Perceptron의 수학적 정의가 선행되어야합니다.
저희가 구현하고자하는 Perceptron은 쉽게말해 두 개의 클래스가 있는 이진 분류 작업으로 볼 수 있습니다. 입력값 x와 이에 상응하는 가중치 벡터 w의 선형조합(linear combination)으로 결정함수를 정의합니다. 선형조합은 다른 포스팅에서 개념을 정리했었죠. 클릭하시면 해당 포스팅으로 넘어갑니다.
아무튼 이에 따라 최종입력되는 z는 가중치 벡터 W의 transpose vector와 X의 vector간의 곱으로 정해집니다. 이때 최종 입력되는 z가 특정한 임계값보다 크면 1, 작으면 -1로 할당합니다. 이를 실제값과 비교하면서 가중치를 업데이트할 겁니다. 이 과정을 반복하면서 가중치를 지속적으로 수정하면서 클래스를 예측할 겁니다.
이 과정을 간단하게 요약하자면
1. 가중치를 0 또는 랜덤한 작은 값으로 초기화합니다.
2. 각 훈련 샘플레서 다음 작업을 진행합니다.
a. 출력 값 y hat(예측값)을 계산합니다.
b. 가중치를 업데이트합니다.
로 표현할 수 있습니다.
그러면 이제 퍼셉트론을 한번 구현해보겠습니다.
2. 퍼셉트론 구현하기
필요한 library와 데이터를 불러옵니다.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
train = pd.read_csv('./titanic/train.csv')
test = pd.read_csv('./titanic/test.csv')
train.head()
이전에 Kaggle에서 Titanic project를 진행하면서 Titanic dataset을 개인 폴더에 저장했었습니다. 이를 불러오고 저장했습니다.
그리고 Feature engineering이 굉장히 중요한 단계이지만 이것도 마찬가지로 이전에 feature engineering을 진행했던 것을 그대로 참고했습니다! 궁금하신 분은 제 titanic 포스팅을 참고해주시기 바랍니다.
# 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 = train.drop(['Survived', 'PassengerId'], axis = 1)
train_y = train.iloc[:, 1]
test_X = test.drop(['Survived', 'PassengerId'], axis = 1)
그리고 이제 본격적으로 Perceptron을 구현해봅시다.
class Perceptron(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.errors_ = []
for _ in range(self.n_iter):
errors = 0
for i in range(len(X)):
xi = X.iloc[i].values
target = y[i]
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 predict(self, X):
return np.where(self.net_input(X) >= 0.0, 1, 0)
def net_input(self, X):
return np.dot(X, self.w_[1:]) + self.w_[0]
생각보다 간단하게 알고리즘을 작성할 수 있었습니다. 해당 클래스에서 쓰인 매개변수와 속성에 대해 간단하게 설명드리겠습니다.
- eta : 학습률 (float)
- n_iter : 훈련 데이터 반복 횟수 (int)
- random_state : 난수 (int)
- w_ : 학습된 가중치 (1d-array)
- errors_ : 에포크(반복 횟수)마다 누적된 분류 오류 (list)
- X : 훈련데이터
- y : 타겟 값
위에서 설명한 방식대로 진행한다. 코드의 진행 순서를 보겠습니다.
1. 초기 w_(가중치 값)을 설정한다. input variable의 개수 + 1만큼의 크기로, 이는 0을 기준으로 한 정규분포 값으로 랜덤하게 구성되어있다.
2. Xi와 w_[1:]를 점곱(np.dot)하고 이를 w_[1]과 더해준 값을 최종 입력 값으로 저장한다. 점곱의 개념은 선형 회귀의 기본 구성요소 포스팅에서 확인할 수 있다. 해당 포스팅에서 행렬 간의 곱 계산 방법이 간단하게 설명되어있다.
3. 최종 입력값이 0 이상이면 1, 0이하면 0을 출력한다. 위에서 설명한 바와는 다르게 titanic survival prediction은 target variable이 0과 1의 값으로 이루어져있기 때문이다.
4. 최종적으로 출력된 예측 값(y hat)이 실제값과 동일하다면 update는 0이 되고 가중치를 수정하지 않은 채 첫번 째 예측이 끝나게 된다. 예측 값과 실제 값이 다른 경우에는 update에 eta(학습률)이 저장되고, 이 값을 w_[1]에 추가해주고, eta와 xi을 곱한 값은 w_[1:]에 추가해준다. 그리고 이를 errors에 저장해준다.
5. 2-4를 반복하고 errors_에 errors를 append해준다.
6. 주어진 n_iter만큼 1-5를 반복한다.
7. self를 출력한다.
생각보다 복잡하지 않게 구현할 수 있는 알고리즘이다. 이를 train set에 학습을 시켜보고 반복 횟수에 따른 정확도를 파악해봤습니다.
# 학습
Pc = Perceptron(n_iter = 100)
Pc.fit(train_X, train_y)
# 그래프
fig = plt.subplots(figsize = (15, 8))
plt.plot(range(1, len(Pc.errors_) + 1), Pc.errors_)
plt.legend(['Perceptron errors'], loc = 'upper right')
plt.xlabel('Epoch', fontsize = 10)
plt.ylabel('Number of errors', fontsize = 10)
plt.show()
약 7-8번의 학습을 진행하니 약 200개 정도로 오차 개수가 떨어졌고, 이후에는 오차 개수가 190 ~ 210개에 수렴하는 것을 알 수 있습니다.
3. 조정된 퍼셉트론 구현하기
여기까지가 본 교재에서 나온 내용입니다. 문득 이 방식을 조금 더 보완할 수 있는 방법이 없을까 고민하다가 초기 w_값을 다르게 설정해보는 건 어떨까 궁금해졌습니다. 그래서 초기 w_값을 각 variable과 target variable간의 상관관계 값을 넣기로 결정했습니다. 우선 상관관계는 target variable과 직접적인 관계가 있기 때문에 이 값만 조정해주면 책에 적혀있는 Perceptron보다 더 빠르고 정확하게 예측할 수 있을 것이라고 생각했습니다. 또한 'Survived'(target variable)와의 상관관계는 0에서 최대 1까지의 값을 가지고 있기 때문에 scale의 문제 없이 target variable과의 관계를 잘 반영할 수 있을 것이라 생각했습니다. 다음은 그렇게 만든 Adjusted Perceptron 알고리즘입니다.
class Adjusted_Perceptron(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):
cor_X = X.copy()
cor_y = y.copy()
cor_X['Target'] = cor_y
self.w_ = [cor_X.corr()['Target'].values[i]
for i in range(len(cor_X.corr()['Target'].values))
if cor_X.corr()['Target'].index[i] != 'Target']
self.w_.insert(0, 0)
self.errors_ = []
for _ in range(self.n_iter):
errors = 0
for i in range(len(X)):
xi = X.iloc[i].values
target = y[i]
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 predict(self, X):
return np.where(self.net_input(X) >= 0.0, 1, 0)
def net_input(self, X):
return np.dot(X, self.w_[1:]) + self.w_[0]
Adjusted Perceptron으로 train set을 예측해보고 Perceptron과 어떤 차이를 보이는지 확인하기 위해서 그래프를 그렸습니다.
# 학습
APc = Adjusted_Perceptron(n_iter = 100)
APc.fit(train_X, train_y)
# 그래프
fig = plt.subplots(figsize = (15, 8))
plt.plot(range(1, len(APc.errors_) + 1), APc.errors_)
plt.plot(range(1, len(Pc.errors_) + 1), Pc.errors_)
plt.legend(['Adjusted_Perceptron', 'Perceptron'], loc = 'upper right')
plt.title('Adjusted Perceptron vs Perceptron', fontsize = 20)
plt.xlabel('Epoch', fontsize = 10)
plt.ylabel('Number of errors', fontsize = 10)
plt.show()
생각보다 재미있는 결과가 나왔습니다. Adjusted Perceptron은 1번, 2번만 학습하더라도 그 이상 학습한 것과 정확도가 크게 차이나지 않았습니다. 초기 설정값을 상관관계로 설정하고 예측을 진행하니 나타난 결과이다. 이는 Perceptron과 가장 큰 차이입니다. 만약 학습 데이터가 무수히 많아진다면 학습 시간의 측면에서 Adjusted Perceptron이 훨씬 우수한 알고리즘이 될 수 있다는 의미이기도 합니다. 직접 만든 알고리즘이 시간 비용 대비 더 우수하는 것을 확인하니 생각보다 더 큰 성취감을 얻을 수 있었습니다.
또한 각 알고리즘별 적절한 반복횟수를 선정한 뒤 예측 값을 kaggle에 제출해 public accuracy를 확인했습니다.
Pc = Perceptron(n_iter = 10)
Pc.fit(train_X, train_y)
predict = [Pc.predict(test_X)]
APc = Adjusted_Perceptron(n_iter = 2)
APc.fit(train_X, train_y)
predict.append(APc.predict(test_X))
for i in range(len(predict)):
submission = pd.DataFrame(columns = ['PassengerId', 'Survived'])
submission['PassengerId'] = df['PassengerId'][891:].map(int)
submission['Survived'] = predict[i]
submission.to_csv('my_submission_{}.csv'.format(i + 1), header = True, index = False)
정확도:
Perceptron : 0.75358
Adjusted Perceptron : 0.75837
Ensemble : 0.76794
Ensemble은 이전에 여러 분류 알고리즘을 합쳐서 제출한 결과물입니다. 하지만 이에 비해서 Perceptron, Adjusted Perceptron의 정확도가 크게 나쁘지 않다고 할 수 있습니다. 물론 복잡한 데이터를 분류해야한다면 정확도 차이가 크게 날 수도 있지만 Titanic 분류에는 꽤나 괜찮은 모형같네요.
책에서 나온 Perceptron과 직접 매개변수를 조정해본 Perceptron을 구현해봤습니다. 다음 포스팅에서는 적응형 선형 뉴련 분류기인 아달린을 구현해보겠습니다.
'머신러닝 교과서 with 파이썬, 사이킷런, 텐서플로' 카테고리의 다른 글
[6] 의사결정나무의 정보이득과 불순도 (0) | 2021.01.06 |
---|---|
[5] 커널 SVM을 활용한 비선형 문제 해결하기 (0) | 2021.01.04 |
[4] 서포트 벡터 머신(SVM) 원리 이해하기 (선형) (0) | 2020.12.31 |
[3] 로지스틱 회귀분석 원리 (0) | 2020.12.31 |
[2] 아달린(Adaline, 적응형 선형 뉴런) 구현해보기 (0) | 2020.12.31 |