OneHot 인코딩(Encoding) 및 스케일링(Scaling)

학습 데이터의 특성들은 수치값 뿐만 아니라 ‘크다’, ‘중간’, ‘작다’ 또는 ‘여자’, ‘남자’와 같은 범주값도 존재합니다. 먼저 범주형 값을 처리하기 위해서는 이 범주형 값을 수치값으로 변환해야 합니다. 만약 범주형 값이 ‘A등급’, ‘B등급’, ‘C등급’처럼 그 의미에 순위적 연속성이 존재한다면 그냥 3, 2, 1과 같이 수치값으로 등급을 매칭하면 됩니다. 하지만 ‘여자’, ‘남자’처럼 순위도 연속성도 없다면 반드시 다른 의미로의 수치값으로 변환해야 하는데, 그 변환은 OnHot 인코딩이라고 합니다. 결론을 미리 말하면 ‘여자’라면 벡터 (1,0)으로, 남자라면 (0,1)으로 변경해야 합니다.

샘플 데이터를 통해 이 OneHot 인코딩을 하는 방법에 대해 언급하겠습니다. 샘플 데이터는 아래의 글에서 소개한 데이터를 사용합니다.

분석가 관점에서 데이터를 개략적으로 살펴보기

먼저 샘플 데이터를 불러옵니다.

import pandas as pd

raw_data = pd.read_csv('./datasets/datasets_1495_2672_abalone.data.csv', 
        names=['sex', 'tall', 'radius', 'height', 'weg1', 'weg2', 'weg3', 'weg4', 'ring_cnt'])
    #names=['성별', '키', '지름', '높이', '전체무게', '몸통무게', '내장무게', '껍질무게', '껍질의고리수']

이 중 sex 컬럼은 성별인데, 이 컬럼의 값을 아래의 코드를 통해 출력해 봅니다.

print(raw_data["sex"][:10])
0    M
1    M
2    F
3    M
4    I
5    I
6    F
7    F
8    M
9    F

범주형 값이라는 것을 알수있는데, M은 숫컷, F는 암컷, I는 유충입니다. 이 sex 컬럼에 대해 OneHot 인코딩 처리를 위해 먼저 문자형을 숫자형으로 변환해주는 OrdinalEncoder 클래스를 통해 처리합니다.

raw_data_labels = raw_data["ring_cnt"].copy()
raw_data = raw_data.drop("ring_cnt", axis=1)

raw_data_cat = raw_data[["sex"]]

from sklearn.preprocessing import OrdinalEncoder

ordinal_encoder = OrdinalEncoder()
raw_data_encoded = ordinal_encoder.fit_transform(raw_data_cat)

print(raw_data_encoded[:10])
print(ordinal_encoder.categories_)
[[2.]
 [2.]
 [0.]
 [2.]
 [1.]
 [1.]
 [0.]
 [0.]
 [2.]
 [0.]]
[array(['F', 'I', 'M'], dtype=object)]

OrdinalEncoder는 범주형 데이터를 희소행렬(Sparse Matrix)로 그 결과를 반환합니다. 다시 이 희소행렬을 OneHot 인코딩을 시키기 위해 아래의 코드를 수행합니다.

from sklearn.preprocessing import OneHotEncoder
onehot_encoder = OneHotEncoder()
raw_data_cat_onehot = onehot_encoder.fit_transform(raw_data_cat)
print(raw_data_cat_onehot.toarray()[:10])
print(onehot_encoder.categories_)
[[0. 0. 1.]
 [0. 0. 1.]
 [1. 0. 0.]
 [0. 0. 1.]
 [0. 1. 0.]
 [0. 1. 0.]
 [1. 0. 0.]
 [1. 0. 0.]
 [0. 0. 1.]
 [1. 0. 0.]]
[array(['F', 'I', 'M'], dtype=object)]

이제 범주형 컬럼인 sex 대신 OneHot 인코딩된 값을 데이터에 추가하도록 합니다.

raw_data = raw_data.drop("sex", axis=1)
raw_data = np.c_[raw_data_cat_onehot.toarray(), raw_data]
print(raw_data[:10])
[[0.0 0.0 1.0 0.455 0.365  0.095 0.514  0.2245 0.100999 0.15  4]
 [0.0 0.0 1.0 0.35  0.265  0.09  0.2255 0.0995 0.0485   0.07  2]
 [1.0 0.0 0.0 0.53  0.42   0.135 0.677  0.2565 0.1415   0.21  4]
 [0.0 0.0 1.0 0.44  0.365  0.125 0.516  0.2155 0.114    0.155 4]
 [0.0 1.0 0.0 0.33  0.255  0.08  0.205  0.0895 0.0395   0.055 2]
 [0.0 1.0 0.0 0.425 0.3    0.095 0.3515 0.141  0.0775   0.12  3]
 [1.0 0.0 0.0 0.53  0.415  0.15  0.7775 0.237  0.1415   0.33  4]
 [1.0 0.0 0.0 0.545 0.425  0.125 0.768  0.294  0.1495   0.26  4]
 [0.0 0.0 1.0 0.475 0.37   0.125 0.5095 0.2165 0.1125   0.165 4]
 [1.0 0.0 0.0 0.55  0.44   0.15  0.8945 0.3145 0.151    0.32  4]]

범주형 타입인 sex가 제거되고 이 sex에 대한 추가적인 3개의 컬럼이 추가되었습니다. 바로 이 3개의 컬럼이 OneHot 인코딩된 값입니다.

이제 수치형 데이터에 대한 스케일링입니다. 여기서 스케일링이란 서로 다른 특성들을 일정한 값의 범위로 맞춰주는 것입니다. 흔히 사용하는 방식은 Min-Max 스케일링과 표준화(Standardization)이 있습니다. 먼저 Min-Max 스케일링은 특징의 최소값과 최대값을 먼저 계산하고 이 값을 이용하여 전체 특징값들을 0~1 사이의 값으로 변경시킵니다. 표준화는 먼저 평균과 표준편차를 구하고 전체 데이터 각각에 대해 평균을 뺀 후 표준편차로 나눠 분산이 1이 되도록 데이터를 조정합니다. 각각 sklearn에서 제공하는 MinMaxScaler와 StandardScaler 클래스를 통해 수행이 가능합니다. 아래의 코드는 Min-Max 스케일링을 수행하는 코드입니다.

from sklearn.preprocessing import MinMaxScaler

minmax_scaler = MinMaxScaler()
raw_data = minmax_scaler.fit_transform(raw_data)

print(raw_data[:10])
[[0. 0.  1.   0.51351351 0.5210084  0.0840708   0.18133522 0.15030262 0.1323239  0.14798206 0.75 ]
 [0. 0.  1.   0.37162162 0.35294118 0.07964602  0.07915707 0.06624075 0.06319947 0.06826109 0.25 ]
 [1. 0.  0.   0.61486486 0.61344538 0.11946903  0.23906499 0.17182246 0.18564845 0.2077728  0.75 ]
 [0. 0.  1.   0.49324324 0.5210084  0.11061947  0.18204356 0.14425017 0.14944042 0.15296462 0.75 ]
 [0. 1.  0.   0.34459459 0.33613445 0.07079646  0.07189658 0.0595158  0.05134957 0.0533134  0.25 ]
 [0. 1.  0.   0.47297297 0.41176471 0.0840708   0.12378254 0.09414929 0.10138249 0.1180867  0.5  ]
 [1. 0.  0.   0.61486486 0.60504202 0.13274336  0.27465911 0.15870881 0.18564845 0.32735426 0.75 ]
 [1. 0.  0.   0.63513514 0.62184874 0.11061947  0.27129449 0.19704102 0.1961817  0.25759841 0.75 ]
 [0. 0.  1.   0.54054054 0.52941176 0.11061947  0.17974146 0.14492266 0.14746544 0.16292975 0.75 ]
 [1. 0.  0.   0.64189189 0.64705882 0.13274336  0.31609704 0.21082717 0.19815668 0.31738914 0.75 ]]

결과를 보면 전체 데이터가 모두 0~1 사이의 값으로 변환된 것을 알 수 있습니다.

상관관계 조사(Correlation Surveying)

상관관계는 특정 특성에 대해 다른 특성들이 얼마나 영향을 주는지에 대한 척도라고 할 수 있습니다. 가장 흔히 사용되는 상관관계 조사는 표준 상관계수(Standard Correlation Coefficient)로 판다스의 corr 매서드를 통해 쉽게 얻을 수 있습니다.

글의 진행을 위해 사용한 샘플 데이터에 대한 소개는 아래 글을 참고하기 바랍니다.

분석가 관점에서 데이터를 개략적으로 살펴보기

아래의 코드는 샘플 데이터의 특성 중 ring_cnt에 영향을 주는 다른 특성의 표준 상관계수를 구해 출력합니다.

import pandas as pd

raw_data = pd.read_csv('./datasets/datasets_1495_2672_abalone.data.csv', 
    names=['sex', 'tall', 'radius', 'height', 'weg1', 'weg2', 'weg3', 'weg4', 'ring_cnt'])

corr_matrix = raw_data.corr()
print(corr_matrix["ring_cnt"].sort_values(ascending=False))

결과는 다음과 같습니다.

표준상관계수는 특성간의 선형적인 관계를 추출해줍니다. 즉, 선형적으로 비례하면 기울기 1에 가깝고 반비례하면 기울기 -1에 가깝습니다. 선형적으로 관계가 약하면 0에 가깝게 됩니다. 위의 결과를 보면 ring_cnt 특성 중 weg4가 가장 큰 선형 관계를 가지지만 다른 특성 역시 비슷한 선형적 관계를 가지고 있습니다.

아래의 코드처럼 상관관계 조사를 위해 각 특성들을 1:1로 매칭(x축, y축)으로 분포도를 쉽게 출력하여 시각적으로 상관관계를 파악할 수 있습니다.

from pandas.plotting import scatter_matrix
scatter_matrix(raw_data)
plt.show()

결과는 다음과 같습니다.

동일한 특성에 대한 그래프는 아차피 기울기가 1인 선형이므로 히스트그램으로 표시됩니다. ring_cnt를 X축으로 하는 다른 그래프를 살펴보면 height 특성을 제외하고 매우 밀접한 상관관계를 갖고 있음을 알 수 있습니다. 이는 표준 상관계수에서 파악하지 못한 내용입니다.

TensorFlow2를 이용한 간단한 회귀분석

TensorFlow v2가 정식버전으로 배포된지 몇달이 지났습니다. 필자는 딥러닝 라이브러리로 PyTorch를 주력으로 하고 있으나, TensorFlow로 만들어진 많은 코드 분석 및 협업을 위해 TensorFlow에 대한 API도 관심이 많습니다. 이 글에서는 TensorFlow 버전2에서 sin 함수에 대한 회귀분석에 대한 샘플 코드를 설명합니다.

필요한 패키지에 대한 import 및 훈련 데이터와 테스트 데이터를 아래 코드를 통해 준비합니다.

import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.optimizers import SGD, RMSprop, Adam
from tensorflow.keras import metrics

np.random.seed(0)

train_x = np.linspace(0,np.pi*2,10).reshape(10,1)
train_y = np.sin(train_x)
test_x = np.linspace(0,np.pi*2,100).reshape(100,1)
test_y = np.sin(test_x)

sin 함수에 대한 회귀분석을 위한 신경망 모델은 다음과 같습니다.

sin 함수는 1개의 입력값을 받아 1개의 출력값을 가지므로 입력층과 출력층의 뉴런 개수는 1개입니다. 중간의 은닉층의 뉴런은 임의로 2개로 잡았습니다. 이 신경망 모델을 구성하는 코드는 다음과 같습니다.

input_nodes = 1
hidden_nodes = 2
output_nodes = 1

model = Sequential()

model.add(Dense(hidden_nodes, input_dim=input_nodes, activation='sigmoid'))
model.add(Dense(output_nodes))

print(model.summary())

모델 상세 정보가 콘솔에 표시되는데, 다음과 같습니다.

파라메터의 개수는 가중치 w 뿐만 아니라 편차값인 b 값도 고려해야 합니다.

신경망을 학습할 것인데, 학습에 사용할 최적화 방식으로 4가지를 사용합니다. 아래 코드에 사용할 최적화 방법에 대한 구체적인 코드는 다음과 같습니다.

optimizers = {
    'SGD': SGD(lr=0.1),
    'Momentum': SGD(lr=0.1, momentum=0.9), 
    'RMSProp': RMSprop(lr=0.01),
    'Adam': Adam(lr=0.01)
}

다음 코드는 학습입니다.

train_results = []
train_y_predicted = []
test_y_predicted = []

for optimizer_name, optimizer in optimizers.items():
    print(optimizer_name, 'Training ...')

    model.compile(optimizer=optimizer, loss='mean_squared_error', metrics=['mse'])

    result = model.fit(train_x, train_y, epochs=1000, verbose=0)
    train_results.append(result)

    train_result = model.predict(train_x)    
    train_y_predicted.append(train_result)

    test_result = model.predict(test_x)
    test_y_predicted.append(test_result)

위의 코드 중 8번의 model.compile은 모델 학습하기 위해 먼저 호출해야 하는 코드입니다. 인자로 loss와 metrics가 있는데, 각각 가질 수 있는 값은 ‘mean_squared_error'(주로 회귀용), ‘categorilcal_crossentropy'(주로 다중분류), binary_crossentropy'(주로 이진분류) 등과 ‘mse'(주로 회귀용), ‘accuracy'(주로 분류용) 등입니다. 그리고 실제 학습은 10번 코드를 통해 이뤄집니다. 단 1줄로 말입니다. 이 부분은 케라스의 장점이죠. model.fit 함수의 결과값은 손실값과 정확도에 대한 값을 포함합니다. 13번 코드와 16번 코드는 훈련된 모델을 통해 실제 계산을 수행하는 코드입니다. 각각 학습 데이터와 테스트 데이터로 계산을 수행해 그 결과를 배열에 담습니다.

앞의 코드에서는 중간 결과를 배열에 담았는데요. 이렇게 담은 배열은 최종 결과 그래프를 생성하기 위해 다음처럼 사용됩니다.

fig, axes = plt.subplots(3,1)
axes[0].plot(train_x, train_y, '-o', label = 'sin(x)')
for i, optimizer_name in enumerate(optimizers.keys()):
    axes[0].plot(train_x, train_y_predicted[i], '--', label=optimizer_name)
axes[0].legend()    

axes[1].plot(test_x, test_y, '-o', label = 'sin(x)')
for i, optimizer_name in enumerate(optimizers.keys()):
    axes[1].plot(test_x, test_y_predicted[i], '--', label=optimizer_name)
axes[1].legend()

for i, optimizer_name in enumerate(optimizers.keys()):
    axes[2].plot(train_results[i].history['loss'], '--', label=optimizer_name)
axes[2].legend()

plt.show()

결과는 다음과 같습니다.

이 경우 Momentum와 RMSprop의 장점을 섞은 Adam 최적화 방식이 가장 좋은 결과를 제공하는 것을 알 수 있습니다.

케라스(Keras)의 get_file 함수

신경망 학습을 위한 데이터 준비를 위해, 인터넷 상의 파일을 다운로드 받아 압축을 풀 경우가 있습니다. 이때 내가 원하는 로컬 경로에 다운로드를 받고, 원하는 서브 디렉토리에 압축을 풀기 위해 다음의 코드가 사용될 수 있습니다.

import tensorflow as tf

path = 'D:/GeoAI/20200203'
tf.keras.utils.get_file(path + '/image.zip', 'http://where.net/data.zip', extract=True, cache_subdir='data', cache_dir=path)

결과적으로 인터넷 상에 존재하는 http://where.net/data.zip을 D:/GeoAI/20200203 디렉토리에 image.zip 파일명으로 다운로드 받고, 압축은 D:/GeoAI/20200203/data에 풀게 됩니다.

추가적으로 아래의 코드는 압축이 풀린 파일명 리스트를 얻는 코드입니다.

import pathlib
file_path = pathlib.Path(path + '/data/images')

file_paths = list(file_path.glob('*/*'))
print(file_paths[:10])

이렇게 얻은 파일들의 경로를 통해, 만약 해당 파일이 이미지 형식이라면 실제 화면상에 표시해 확인할 수 있는데, 해당 코드는 아래와 같습니다.

import matplotlib.pyplot as plt
import os

plt.figure(figsize=(12,12))
for i in range(9):
    plt.subplot(3,3,i+1)
    plt.imshow(plt.imread(file_paths[i]))
    plt.title(os.path.basename(file_paths[i]))
    plt.axis('off')
plt.show()

결과는 다음과 같습니다.