Preprocessing in Data Science (Part 3): Scaling Synthesized Data
You can preprocess the heck out of your data but the proof is in the pudding: how well does your model then perform?
이전 두 글에서는 ML 파이프라인에서 데이터 전처리의 역할을 살펴보았다. 특히, KNN과 logistic regression 알고리즘을 알아보고 수치적 데이터 스케일링이 KNN에 영향을 미쳤지만 로지스틱 회귀에는 영향을 미치지 않았다는 것도 알아보았다. 여기서 진짜 기억해야할 것은 전처리는 진공상태(계속 이 용어가 나오는데 아무래도 데이터가 전혀없는 상태를 말하는것 같다)에서는 발생하지 않는다. 즉, 데이터를 엄청나게 전처리 할 수 있지만 증거는 푸딩에 있다.(??): 그러면 모델이 얼마나 잘 동작할까?
수치적 데이터 스케일링(scaling) 즉, 변수의 범위를 바꾸기 위해 변수의 모든 인스턴스를 상수로 곱하는 것은 두가지 관련된 목적을 갖는다.
- 만약 측정된 데이터가 미터(meter)와 마일(mile)로 된 데이터이고 두 데이터 모두 스케일링한다면 두 데이터는 같아진다.
- 만약 두개의 변수가 상당히 다른 범위를 갖는다면 더 큰 범위를 가진 변수가 비록 더 작은 범위를 갖는 것보다 덜 중요하더라도 범위가 큰 변수가 예측모델에 큰 영향을 미친다. 우리가 본 것은 이 문제가 KNN에서도 발생한다는 것이다. 이는 명시적으로 얼마나 데이터가 다른 데이터에 가까운가를 본다. 하지만, 로지스틱 회귀는 그렇지 않다. 로지스틱 회귀는 훈련시 스케일링의 부족을 설명하기 위해 관련 계수를 축소한다.
이전 글에서 사용한 데이터가 실제 데이터이기 때문에 확인할 수 있는 모든 것은 스케일링 전, 후의 모델 성능이었다. 여기서 귀찮은 변수의 형태(목표 변수에 영향을 미치지 않지만 모델에는 영향을 미칠 수 있는)에서 노이즈가 사전 스케일링과 사후 스케일링 양쪽에서 모델 성능을 얼마나 바꾸는가를 확인하기 위해 성가신 변수의 정확한 특성을 제어할 수 있는 데이터셋을 합성한다. 합성된 데이터에 노이즈가 더 많을수록 KNN의 스킹일링이 더 중요하다는 것을 알 수 있다.
아래 코드에서는 4개의 군집으로 구성된 2000개의 데이터 포인트를 생성하기 위해 scikit-learn의 make_blobs 함수를 사용한다.(각 데이터 포인트는 2개의 예측변수와 1개의 목표 변수를 갖는다.)
# Generate some clustered data (blobs!)
import numpy as np
from sklearn.datasets.samples_generator import make_blobs
n_samples=2000
X, y = make_blobs(n_samples, centers=4, n_features=2,
random_state=0)
Plotting the synthesized data
합성한 데이터를 평면에 그려본다. 각 축(axis)은 예측변수이고 색상은 목표변수의 키(key)이다.
%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('ggplot')
plt.figure(figsize=(20,5));
plt.subplot(1, 2, 1 );
plt.scatter(X[:,0] , X[:,1], c = y, alpha = 0.7);
plt.subplot(1, 2, 2);
plt.hist(y)
plt.show()
Note : 우측 도표에서 목표 변수가 동일하게 나타나고 있음을 볼 수 있다. 이러 경우(또는 거의 동일하게 나타나는 경우라도) 분류 y가 균형잡혔다고 한다.(balanced)
특성(예측 변수)에 대한 히스토그램을 그려보자.
import pandas as pd
df = pd.DataFrame(X)
pd.DataFrame.hist(df, figsize=(20,5));
이번에는 훈련셋과 테스트셋으로 나눈 후 그려본다.
from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
plt.figure(figsize=(20,5));
plt.subplot(1, 2, 1 );
plt.title('training set')
plt.scatter(X_train[:,0] , X_train[:,1], c = y_train, alpha = 0.7);
plt.subplot(1, 2, 2);
plt.scatter(X_test[:,0] , X_test[:,1], c = y_test, alpha = 0.7);
plt.title('test set')
plt.show()
훈련셋으로 KNN을 훈련시켜보자.
from sklearn import neighbors, linear_model
knn = neighbors.KNeighborsClassifier()
knn_model = knn.fit(X_train, y_train)
테스트셋으로 정확도를 산출해보자.
print('k-NN score for test set: %f' % knn_model.score(X_test, y_test))
`k-NN score for test set: 0.935000`
훈련셋으로 정확도를 산출해보자.
print('k-NN score for training set: %f' % knn_model.score(X_train, y_train))
`k-NN score for training set: 0.941875`
scikit-learn에서 기본 채점 방식이 정확도인 것을 반복 할 가치가 있다. 다야한 다른 지표를 확인하기 위해 또한 scikit-learn의 classification report를 사용할 수 있다.
from sklearn.metrics import classification_report
y_true, y_pred = y_test, knn_model.predict(X_test)
print(classification_report(y_true, y_pred))
precision recall f1-score support
0 0.87 0.90 0.88 106
1 0.98 0.93 0.95 102
2 0.90 0.92 0.91 100
3 1.00 1.00 1.00 92
avg / total 0.94 0.94 0.94 400
Now with scaling
예측변수를 스케일링하고 KNN에 적용한다.
from sklearn.preprocessing import scale
Xs = scale(X)
Xs_train, Xs_test, y_train, y_test = train_test_split(Xs, y, test_size=0.2, random_state=42)
plt.figure(figsize=(20,5));
plt.subplot(1, 2, 1 );
plt.scatter(Xs_train[:,0] , Xs_train[:,1], c = y_train, alpha = 0.7);
plt.title('scaled training set')
plt.subplot(1, 2, 2);
plt.scatter(Xs_test[:,0] , Xs_test[:,1], c = y_test, alpha = 0.7);
plt.title('scaled test set')
plt.show()
knn_model_s = knn.fit(Xs_train, y_train)
print('k-NN score for test set: %f' % knn_model_s.score(Xs_test, y_test))
`k-NN score for test set: 0.935000`
스켕일링으로는 더 나은 성능을 보이지 않았다. 이는 두 특성이 이미 동일 범위 주변에 있기 때문일 가능성이 높다. 변수가 폭넓은 다양한 범위를 갖는다면 스케일링하는 것이 맞다. 실제 이것을 보기 위해 다른 특성을 추가할 것이다. 게다가 이 특성은 목표변수에 거의 관련되지 않을 것이다. 이는 단지 노이즈일 뿐이다.
Adding noise to the signal:
평균 0, 가변 표준 편차(variable standard deviation) $\sigma$인 가우시안(Gaussian) 노이즈를 세번째 변수로 추가한다. 여기서 $\sigma$를 노이즈의 세기(strengh of the noise)라 부르고 더 강한 노이즈일수록 KNN이 더 나쁜 성능이라는 것을 확인한다.
# Add noise column to predictor variables
ns = 10**(3) # Strength of noise term
newcol = np.transpose([ns*np.random.randn(n_samples)])
Xn = np.concatenate((X, newcol), axis = 1)
이제 3차원 데이터를 그리기 위해 matplot3d 패키지를 사용한다.
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure(figsize=(15,10));
ax = fig.add_subplot(111, projection='3d' , alpha = 0.5);
ax.scatter(Xn[:,0], Xn[:,1], Xn[:,2], c = y);
새로운 데이터에서 모델의 성능을 보자.
Xn_train, Xn_test, y_train, y_test = train_test_split(Xn, y, test_size=0.2, random_state=42)
knn = neighbors.KNeighborsClassifier()
knn_model = knn.fit(Xn_train, y_train)
print('k-NN score for test set: %f' % knn_model.score(Xn_test, y_test))
k-NN score for test set: 0.400000
굉장히 좋지 않은 모델이다. 스케일일하고 모델을 성능을 확인해 보자.
Xns = scale(Xn)
s = int(.2*n_samples)
Xns_train = Xns[s:]
y_train = y[s:]
Xns_test = Xns[:s]
y_test = y[:s]
knn = neighbors.KNeighborsClassifier()
knn_models = knn.fit(Xns_train, y_train)
print('k-NN score for test set: %f' % knn_models.score(Xns_test, y_test))
`k-NN score for test set: 0.907500`
스케일링 후 모델성능은 거의 노이즈가 없는 모델만큼 잘 동작한다. 이제 노이즈 세기에 대한 함수로 모델 성능을 확인해 보자.
The stronger the noise, the bigger the problem:
노이즈의 세기가 모델 정확도에 얼마나 영향을 미치는지 알아보자. 계속 동일 코드를 사용해야 하기 때문에 주요 부분을 작은 함수로 만들자.
def accu( X, y):
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
knn = neighbors.KNeighborsClassifier()
knn_model = knn.fit(X_train, y_train)
return(knn_model.score(X_test, y_test))
noise = [10**i for i in np.arange(-1,6)]
A1 = np.zeros(len(noise))
A2 = np.zeros(len(noise))
count = 0
for ns in noise:
newcol = np.transpose([ns*np.random.randn(n_samples)])
Xn = np.concatenate((X, newcol), axis = 1)
Xns = scale(Xn)
A1[count] = accu( Xn, y)
A2[count] = accu( Xns, y)
count += 1
이제 노이즈 세기 (log x 축)의 함수로 정확도를 그려보자.
plt.scatter( noise, A1 )
plt.plot( noise, A1, label = 'unscaled', linewidth = 2)
plt.scatter( noise, A2 , c = 'r')
plt.plot( noise, A2 , label = 'scaled', linewidth = 2)
plt.xscale('log')
plt.xlabel('Noise strength')
plt.ylabel('Accuracy')
plt.legend(loc=3);
위 그림에서 골치아픈 변수에 더 많은 노이즈있을 수록 KNN 모델을 위해 데이터를 스케일링하는 것이 더 중요해진다는 것을 볼 수 있다. 앞으로 로지스틱 회귀를 위해 동일한 작업을 할 것이다. 결론을 짓기 위해 전처리로 데이터 과학적인 파이프라인, 스케일링과 센터링이 필수적인 부분이고 우리는 ML 문제에 전체적인 접근을 촉진하기 위해 그렇게 했다.
열심인 독자를 위한 연습 : 위의 합성된 데이터셋에 로지스틱 회귀를 훈련하고 모델 성능을 확인해보자.
아래 코드에서 10의 제곱수를 노이즈의 총량을 바꾸기 위해 변경하고 sc = True로 특성을 스케일일 하도록 만든다.
# Below, change the exponent of 10 to alter the amount of noise
ns = 10**(3) # Strength of noise term
# Set sc = True if you want to scale your features
sc = False
#Import packages
import numpy as np
from sklearn.cross_validation import train_test_split
from sklearn import neighbors, linear_model
from sklearn.preprocessing import scale
from sklearn.datasets.samples_generator import make_blobs
#Generate some data
n_samples=2000
X, y = make_blobs(n_samples, centers=4, n_features=2, random_state=0)
# Add noise column to predictor variables
newcol = np.transpose([ns*np.random.randn(n_samples)])
Xn = np.concatenate((X, newcol), axis = 1)
#Scale if desired
if sc == True:
Xn = scale(Xn)
#Train model and test after splitting
Xn_train, Xn_test, y_train, y_test = train_test_split(Xn, y, test_size=0.2, random_state=42)
lr = linear_model.LogisticRegression()
lr_model = lr.fit(Xn_train, y_train)
print('logistic regression score for test set: %f' % lr_model.score(Xn_test, y_test))
** 위에서 증거는 푸딩에 있다? 이 의미가 무엇인지 아시는 분은 댓글 부탁드립니다.