영웅은 죽지 않는다

머신러닝 때려 부수기 - 캐글 데이터를 이용한 논리 회귀(Logistic Regression), 전처리(Preprocessing) 본문

Programming/Machine Learning

머신러닝 때려 부수기 - 캐글 데이터를 이용한 논리 회귀(Logistic Regression), 전처리(Preprocessing)

YoungWoong Park 2021. 8. 1. 18:02
논리회귀(Logistic Regression)란

 머신러닝에서 입력값과 범주 사이의 관계를 구하는 것을 논리 회귀라고 합니다.

선형회귀에서 풀기 힘든 문제들을 해결할 수 있는 방법 중 하나인데, 그 예시로

시험 전 날 공부한 시간으로 해당 과목의 이수 여부(Pass or Fail)을 예측하는 문제에서 선형 회귀로 표현하면 다음과 같이 나옵니다.

 한 눈에 보기에도 굉장히 이상한 형태입니다. 이런 경우 이진 논리 회귀(Binary Logistic Regression)을 이용하면 해결할 수 있으며 그 결과는 다음과 같습니다.

 이와 같이 선형이 아닌 S 커브 형태로 특정 변수를 함수로 표현한 것을 논리 함수(Logistic Function)이라고 하며, 딥러닝에서는 시그모이드 함수(Sigmoid Function)이라고 합니다.

 시그모이드 함수는 오른쪽 그림과 같이 생겼으며 입력값이 음수 방향으로 갈 수록 출력값은 0에 가까워지고, 양수 방향으로 갈 수록 1에 가까워지는 형태를 볼 수 있습니다.

☞ 가설과 손실함수

선형 회귀에서 가설은 H(x) = W x + b와 같은 형태로 표현하였습니다. 논리 회귀에서는 시그모이드 함수에 선형 회귀 식을 대입하면 되며, 그 형태는 다음과 같습니다.

이렇듯 계산식이 복잡해져 손실 함수는 어려운 수식으로 나타내야 합니다. 수식보다는 개념으로 이해하기 위해 다음 그래프를 확인해보겠습니다.

위와 같은 그래프에서는 크게 두 종류로 나눌 수 있습니다.

1. 정답 라벨 y가 0이 되어야 하는 경우

ㅡ> 예측한 라벨이 0일 경우 확률이 1(100%)이 되어야 한다.

ㅡ> 예측한 라벨이 1일 경우 확률이 0(0%)이 되어야 한다.

2. 정답 라벨 y가 1이 되어야 하는 경우

ㅡ> 예측한 라벨이 1일 경우 확률이 1(100%)이 되어야 한다.

ㅡ> 예측한 라벨이 0일 경우 확률이 0(0%)이 되어야 한다.

위와 같은 확률 분포 그래프의 차이를 비교할 때 CrossEntropy 함수를 사용하게 되며, 이는 임의의 입력값에 대해 우리가 원하는 확률 분포 그래프를 만들도록 학습시키는 손실 함수입니다.

정답이 0인 경우에서,

우리가 현재 학습 중인 입력값의 확률 분포가 파란색 그래프와 같다고 가정할 때, CrossEntropy는 파란색 그래프를 빨간색 그래프처럼 만들어주기 위해 노력하는 함수입니다. 이는 선형 회귀에서 정답값을 나타내는 점과 우리가 세운 가설 직선의 거리를 최소화하려고 했던 거리 함수와 유사하다고 볼 수 있습니다.

(Keras에서 이진 논리 회귀의 경우 binary_crossentropy 손실 함수를 사용합니다.)

다항 논리 회귀 (Multinomial Logistic Regression)

 위 논리 회귀 예시처럼 이수 여부(Pass or Fail)과 같이 2개로 나누는 것이 아닌, 2개 이상으로 클래스를 나누는 방법을 다항 논리 회귀라고 합니다.

이 때 One-Hot-Encoding이라는 인코딩 방법을 이용하여 각 class를 labeling하게 됩니다.

 One-Hot-Encoding을 만드는 방법은 다음과 같습니다.

1. 각 class의 개수만큼 배열을 0으로 채운다.

2. 각 class의 인덱스 위치를 정한다.

3. 각 class에 해당하는 인덱스에 1을 넣는다.

☞ Softmax 함수와 손실함수

 Softmax 함수는 선형 모델에서 나온 결과를 모두 더하면 1이 되도록, 즉 결과를 확률값으로 만들어주는 함수입니다. 우리가 One-Hot-Encoding을 할 때에도 라빌의 값을 모두 더하면 1(100%)이 되기 때문에, Softmax 함수를 적용하여 확률값으로 바꿔주는 것입니다.

출처: https://www.tutorialexample.com/implement-softmax-function-without-underflow-and-overflow-deep-learning-tutorial/

 마찬가지로 확률 분포의 차이를 계산할 때는 CrossEntropy 함수를 사용합니다. 이진 논리 회귀와 마찬가지로 데이터셋의 정답 라벨과 우리가 예측한 라벨의 확률 분포 그래프를 구해서 CrossEntropy로 두 확률 분포의 차이를 구한 다음, 그 차이를 최소화하는 방향으로 학습을 시킵니다.

 

 

 

(Keras에서 다항 논리 회귀의 경우 categorical_crossentropy 손실 함수를 사용합니다.)

이 외에도 Support Vector Machine(SVM), K-Nearest Neighbors(KNN), Decision Tree, Random Forest 등 다양한 머신러닝 모델이 있지만 이에 대한 구체적인 설명은 생략합니다ㅎㅎ.

전처리 (Preprocessing)

 어쩌면 머신러닝 과정에서 가장 중요하고 많은 부분을 차지하는 것이 데이터 전처리 과정이라고 볼 수 있습니다. 필요 없는 데이터를 제거하고 필요한 데이터를 추출하는 과정, Null값이 있는 행 데이터를 삭제하는 것, 정규화(Normalization), 표준화(Standardization) 등의 과정이 포함됩니다.

 여기서는 대표적인 전처리 작업인 정규화와 표준화에 대해 다룰 예정입니다. 먼저 이 과정들이 왜 필요한지에 대해 살펴보자면, 예를들어 우리가 부동산 데이터를 분석한다고 가정할 때

- 집의 넓이 (단위: 평)

- 준공 연도 (단위: 년)

- 층 수 (단위: 층)

- 지하철 역과의 거리 (단위: km)

 위 데이터를 분석하는 것에 있어 단위와 값의 범위가 다르기에 직접적으로 데이터 비교가 불가능하게 됩니다. 집의 넓이가 큰 집과 지하철 역과의 거리가 가까운 집 중 어느 집의 값어치가 더 비싼 것인지는 알 수 없게 되는 것입니다.

 그렇기에 우리는 정규화 과정을 거쳐, 데이터를 0과 1 사이의 범위를 가지도록 만들어 비교가 가능한 형태로 전처리하는 과정이 필요하게 됩니다.

☞ 정규화 (Normalization)

데이터를 0과 1 사이의 범위로 만드는 정규화 수식은 오른쪽과 같습니다.

예를 들어 A는 수학 시험에서 100점 만점에 50점을 받았고, B는 수학 시험에서 500점 만점에 50점을 받았다고 가정했을 때, 정규화 과정을 거치면 다음과 같이 표현이 가능합니다.

 

그렇기에 실제로는 동일한 50점일지라도 A가 B보다 5배 높은 성적을 받았다는 것을 알 수 있습니다.

☞ 표준화 (Standardization)

 

 표준화는 정규화와는 다르게 데이터의 분포를 정규분포(평균=0, 표준편차=1)로 바꾸어줍니다.

(출처: http://cs231n.stanford.edu/2016/)

 

 데이터의 평균을 0으로 만들어주면 데이터의 중심이 0으로 맞춰지게 되며, 표준편차를 1으로 만들어주면 데이터가 표준화되게 됩니다. 이렇게 표준화를 시키게 되면 일반적으로 학습 속도가 빨라지고, Local minima에 빠질 가능성이 줄어들게 됩니다.

데이터 예측하기 - 이진 논리회귀

 이진 논리회귀 예시에서는, 캐글에서 유명한 데이터 중 하나인 타이타닉 승객 정보를 이용하여 생존자를 예측하는 모델을 만들어 봅니다.

 캐글에 로그인 후 내 프로필 -> Account -> API -> Create New API Token 에서 kaggle.json을 다운로드하여 username과 key값을 입력합니다.

import os
os.environ['KAGGLE_USERNAME'] = 'username' # username
os.environ['KAGGLE_KEY'] = 'key' # key

 

그 후 타이타닉 데이터셋의 API Command를 Copy하여 데이터셋을 불러온 후 압축을 풉니다.

!kaggle datasets download -d heptapod/titanic
!unzip titanic.zip

이제 본격적으로 데이터 모델을 만들어보겠습니다.

- 필요한 패키지 Import

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.optimizers import Adam, SGD
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

 

- 데이터셋 Load

df = pd.read_csv('train_and_test2.csv')

df.head(5)

 출력창에 나와있는 column들을 보게 되면 필요없는 데이터들이 존재하는데, 이 중에서 필요한 데이터만 불러오겠습니다.

- 전처리(필요한 데이터만 추출)

df = pd.read_csv('train_and_test2.csv', usecols=[
  'Age', # 나이
  'Fare', # 승차 요금
  'Sex', # 성별
  'sibsp', # 타이타닉에 탑승한 형제자매, 배우자의 수
  'Parch', # 타이타니게 탑승한 부모, 자식의 수
  'Pclass', # 티켓 등급 (1, 2, 3등석)
  'Embarked', # 탑승국
  '2urvived' # 생존 여부 (0: 사망, 1: 생존)
])

df.head(5)

 필요한 데이터들 중 상위 5개를 출력하여 잘 추출이 된 것을 확인합니다.

이 중에서, 비어있는 행이 있는지에 대한 전처리 과정을 추가로 진행합니다.

- 전처리(비어있는 행 확인)

print(df.isnull().sum())

 여기서 'Embarked' 에서 2개의 Null값이 존재하는 것을 확인했으며, 이 비어있는 행을 없애는 전처리 과정을 진행합니다.

- 전처리(비어있는 행 제거)

print(len(df))

df = df.dropna()

print(len(df))

 2개의 행이 잘 제거된 것을 확인할 수 있습니다.

- X, y 데이터 분할

x_data = df.drop(columns=['2urvived'], axis=1)
x_data = x_data.astype(np.float32)

x_data.head(5)

 우리는 생존자를 예측하는 것이기에, X 데이터에는 승객 정보 중 생존 여부 데이터를 제외한 데이터가 존재해야 합니다. 반대로 y 데이터에는 생존 여부 데이터만이 존재해야 합니다.

y_data = df[['2urvived']]
y_data = y_data.astype(np.float32)

y_data.head(5)

 여기서 astype 함수를 통해 각 X, y 데이터의 type을 np.float32 형태로 바꾸게 되는데, 이 과정은 tenserflow(keras)에서 사용 가능한 형태로 바꿔주는 과정이라고 보면 됩니다.

- 표준화 (Standardization)

scaler = StandardScaler()
x_data_scaled = scaler.fit_transform(x_data)

print(x_data.values[0])
print(x_data_scaled[0])

scikit-learn의 StandardScaler 모듈을 이용하여 빠르게 표준화하는 과정을 거칠 수 있습니다. x_data와 그것을 표준화한 결과를 비교한 출력 결과는 위와 같습니다.

( https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.StandardScaler.html )

 

- 학습 / 검증 데이터 분할

x_train, x_val, y_train, y_val = train_test_split(x_data, y_data, test_size=0.2, random_state=2021)

print(x_train.shape, x_val.shape)
print(y_train.shape, y_val.shape)

 위 과정에서는 편의를 위해 test set을 나누지 않고 train set과 validation set 으로만 분할하였습니다. 실무에서는 test set 으로도 나누는 과정이 필요합니다.

- 모델 학습 시키기

model = Sequential([
  Dense(1, activation='sigmoid')
])

model.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.01), metrics=['acc'])

model.fit(
    x_train,
    y_train,
    validation_data=(x_val, y_val), # 검증 데이터를 넣어주면 한 epoch이 끝날때마다 자동으로 검증
    epochs=20 # epochs 복수형으로 쓰기!
)

 위에서도 언급하였듯이, 이진 논리회귀에서는 특정 변수를 함수 형태로 나타내는 sigmoid를 이용하며, keras에서는 binary_crossentropy 손실 함수(loss function)를 사용합니다.

 optimizer를 Adam으로, learning rate를 0.01로 맞추었고, metrics를 accuracy(정확도)를 나타내는 것으로 설정하였으며 epochs를 20으로 설정했기에 검증하는 과정을 20번을 반복학습하여 loss값, val_loss값과 더불어 그들에 대한 accuracy 정보도 함께 나타냅니다. 아래 결과창을 보면 loss값이 점점 작아지고 acc값이 점점 증가하는 것으로 보아 학습이 잘 되었음을 알 수 있습니다.

데이터 예측하기 - 다항 논리회귀

다항 논리회귀 예시에서는, 와인의 도수, 산도, 색 등을 이용해서 와인을 분류해보는 모델을 만들어 보겠습니다.

- 데이터 다운받기

import os
os.environ['KAGGLE_USERNAME'] = 'kairess' # username
os.environ['KAGGLE_KEY'] = '7d0443b2dfffc57c94271fd797511896' # key

!kaggle datasets download -d brynja/wineuci
!unzip wineuci.zip

- 필요한 패키지 Import

위 이진 논리회귀에서 Import한 패키지를 동일하게 불러오되, One-Hot-Encoder 패키지를 추가로 가져옵니다.

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.optimizers import Adam, SGD
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import OneHotEncoder

- 데이터셋 Load

df = pd.read_csv('Wine.csv')

df.head(5)

 이 데이터에는 header가 없는 것을 볼 수 있습니다. 이 header 부분을 채워주겠습니다. (실무에서 다음과 같은 상황이 발생한다면, 각 column이 어떤 데이터를 의미하는지 분석 후 header명을 직접 생성해야 합니다.)

- 전처리(header 채우기)

df = pd.read_csv('Wine.csv', names=[
  'name'
  ,'alcohol'
  ,'malicAcid'
  ,'ash'
  ,'ashalcalinity'
  ,'magnesium'
  ,'totalPhenols'
  ,'flavanoids'
  ,'nonFlavanoidPhenols'
  ,'proanthocyanins'
  ,'colorIntensity'
  ,'hue'
  ,'od280_od315'
  ,'proline'
])

df.head(5)

- 전처리(비어있는 행 확인)

위 이중 논리회귀에서 진행했던 것과 같이 전처리하는 과정을 거칩니다.

print(df.isnull().sum())

 비어있는 데이터가 없는 것을 확인할 수 있습니다. 다행이네요

- X, y 데이터 분할

 여기서는 와인의 정보로 와인을 분류하는 것이기에, X 데이터에는 와인의 특징 정보가, y 데이터에는 와인의 이름 정보가 있어야 할 것입니다.

x_data = df.drop(columns=['name'], axis=1)
x_data = x_data.astype(np.float32)

y_data = df[['name']]
y_data = y_data.astype(np.float32)

 여기서도 위 이중 논리회귀와 같이 각 데이터의 type을 keras에서 사용 가능한 형태인 np.float32로 바꿔줍니다.

- 표준화 (Standardization)

scaler = StandardScaler()
x_data_scaled = scaler.fit_transform(x_data)

print(x_data.values[0])
print(x_data_scaled[0])

 StandardScaler 함수를 통해 x_data를 표준화하여 잘 되었는지 확인합니다.

- One-Hot-Encoding

encoder = OneHotEncoder()
y_data_encoded = encoder.fit_transform(y_data).toarray()

print(y_data.values[0])
print(y_data_encoded[0])

 위에서도 언급하였듯이, 다항 논리회귀에서는 출력값의 형태를 One-Hot-Encoding을 통해 나타내게 됩니다. 아까 추가로 불러온 패키지인 OneHotEncoder를 통해 y_data의 형태를 바꾸어줍니다.

- 학습 / 검증 데이터 분할

x_train, x_val, y_train, y_val = train_test_split(x_data_scaled, y_data_encoded, test_size=0.2, random_state=2021)

print(x_train.shape, x_val.shape)
print(y_train.shape, y_val.shape)

이진 논리회귀 과정과 동일하게 학습, 검증 데이터를 분할합니다.

- 모델 학습 시키기

model = Sequential([
  Dense(3, activation='softmax')
])

model.compile(loss='categorical_crossentropy', optimizer=Adam(lr=0.02), metrics=['acc'])

model.fit(
    x_train,
    y_train,
    validation_data=(x_val, y_val), # 검증 데이터를 넣어주면 한 epoch이 끝날때마다 자동으로 검증
    epochs=20 # epochs 복수형으로 쓰기!
)

 이진 논리회귀에서는 activation을 sigmoid 함수를, 손실 함수(loss function)는 binary_crossentropy를 입력했습니다. 그러나 다항 논리회귀에서는 activation을 softmax 함수를, 손실 함수는 categorical_crossentropy로 입력합니다. 여기서는 learning rate를 0.02로 입력하며, 위와 같이 정확도를 나타내게끔 20번 반복학습을 시키게 됩니다.

 여기서도 loss값이 점점 감소하고, acc값이 점점 증가하는 것으로 보아 모델이 잘 학습되었다고 할 수 있겠네요.