표준화 정규화 - pyojunhwa jeong-gyuhwa

딥러닝 실행 전에 데이터 표준화(Normalization)와 정규화(Standardization)가 왜 필요할까?

Kaggle 콘크리트 강도 계산(Calculate Concrete Strength) 데이터를 예시로 들어보자.

콘크리트 강도 계산 feature는 8개로 구성되어 있는데 각각 성질은 아래와 같다.

  • Cement Component : - 시멘트의 혼합량
  • Blast Furnace Slag : - 고로 슬래그 미분말 함유량 (링크 참고)
  • Fly Ash Component : - 플라이애시 혼합량(링크 참고)
  • Water Component : - 물 혼합량
  • Superplasticizer Component : - 가소제 혼합량(링크 참고)
  • Coarse Aggregate Component : - 굵은 골재 함유량
  • Fine Aggregate Component : - 잔 골재 함유량
  • Age In Days : 건조 상태로 남아 있던 날 수
  • Strength(Target) :- 콘크리트의 최종 강도는 무엇이었습니까- (목표)

먼저 데이터가 어떻게 분포 되어있는지 확인해 봐야한다. Pandas와 Seaborn을 이용해 몇줄만으로 차트를 그릴 수 있지만, 추가 라이브러리 없이 Numpy와 Matplotlib만 이용해 보겠다.

import numpy as np
from numpy import genfromtxt
import matplotlib.pyplot as plt

data = genfromtxt('ConcreteStrengthData.csv', delimiter=',', skip_header = 1)

fig, ax = plt.subplots(2, 5)
ax[0][0].plot(data[:, 0])
ax[0][0].set_title('CementComponent', fontsize=10)
ax[0][1].plot(data[:, 1])
ax[0][1].set_title('BlastFurnaceSlag', fontsize=10)
ax[0][2].plot(data[:, 2])
ax[0][2].set_title('FlyAshComponent', fontsize=10)
ax[0][3].plot(data[:, 3])
ax[0][3].set_title('WaterComponent', fontsize=10)
ax[1][0].plot(data[:, 4])
ax[1][0].set_title('SuperplasticizerComponent', fontsize=10)
ax[1][1].plot(data[:, 5])
ax[1][1].set_title('CoarseAggregateComponent', fontsize=10)
ax[1][2].plot(data[:, 6])
ax[1][2].set_title('FineAggregateComponent', fontsize=10)
ax[1][3].plot(data[:, 7])
ax[1][3].set_title('AgeInDays', fontsize=10)
ax[1][4].plot(data[:, 8])
ax[1][4].set_title('Strength', fontsize=10)

plt.show()
표준화 정규화 - pyojunhwa jeong-gyuhwa

마지막 Strength가 콘크리트 굳기로 목표값이다.

각 요소별 데이터값들의 그래프를 그려보았는데 값들의 범위가 너무 천차만별이다.

이렇게 값들의 범위가 다르면 제대로 훈련할 수 없다.

제대로 훈련하기 위해 정규화와 표준화가 필요한 이유다.

예를 들어 첫번째 시멘트 함류량(Cement component)에서 100 수치는 작은 값이지만 그 아래 그래프인 가소제 함유량(Superplasticizer)에서는 엄청나게 많은 양이다. 

이 데이터를 기준을 세우고 통일 시켜야 한다.

이 글 에서도 나와있지만 여러가지 기준을 세우는 방법이 있다. 본인은 일반적으로 3가지 방법을 사용한다.

Zscore가 만능이 아니다. 훈련만 잘 되면 그만 아닌가?

1. 데이터 수를 작게 만들기 → 소수점 곱해주기

데이터에 일정한 소수 (Ex : 0.001)을 곱하여 데이터 자체를 작게 만드는 방법이다. 이렇게 하면 곱하지 않을 때 보다 데이터가 훨씬 안정적으로 인식하고 계산할 수 있게 된다. 본인은 가장 첫 번째로 이 방법을 사용한다.

data = genfromtxt('ConcreteStrengthData.csv', delimiter=',', skip_header = 1)
train_data = data[:1000, :]*0.001
test_data = data[1000:, :]*0.001

각 데이터에 0.001을 곱하고 1030개 데이터중 1000개를 Train Data, 30개를 Test Data로 만든 것이다.

훈련을 마치고 그래프로 그려 확인해 보기 위해 다시 1000을 곱해 주어야 한다.

_, _, _, _, _, pred, test_loss = forward(test_inputs, test_targets, W1, B1, W2, B2, W3, B3)
print('test loss', test_loss)
plt.plot(test_targets*1000, 'ro', label='target')
plt.plot(pred*1000, 'bo', label='pred')
plt.legend()
plt.show()

아래는 훈련 결과이다.

표준화 정규화 - pyojunhwa jeong-gyuhwa

파란색점이 MPL로 구성하고 훈련한 모델이 예측(pred)한 결과이고

빨간색 점이 실제 값(Target)이다. 오차값은 각 그래프 값의 수직값을 봐야 한다. 수직으로 겹칠 수록 오차가 작은 것이다.

2. 정규화(Normalization)

데이터를 정규화 시켜보자. 정규화 방법은 여기 를 참고하면 된다.

norm_data = (data - data.min(axis=0)) \
    / (data.max(axis=0) - data.min(axis=0))

훈련이 끝나고 결과를 확인하기 위해 역정규화를 해주어야 한다.

# 역정규화, 정규화의 연산을 다시 풀어준다. data[:, -1:] 는 마지막 target 값을 의미한다.
test_targets = test_targets * (data[:, -1:].max(axis=0) - data[:, -1:].min(axis=0)) + data[:, -1:].min(axis=0)
pred = pred * (data[:, -1:].max(axis=0) - data[:, -1:].min(axis=0)) + data[:, -1:].min(axis=0)
print('test loss', test_loss)
plt.plot(test_targets, 'ro', label='target')
plt.plot(pred, 'bo', label='pred')
plt.legend()
plt.show()

결과는?

표준화 정규화 - pyojunhwa jeong-gyuhwa

결과가 드라마틱하게 변할 수도 있고 아닐 수도 있다. 아니라고 해서 큰 실망 마시길 바란다.

3. 표준화(Standarization)

가장 많이 사용하는 표준화를 할 것이다. 표준화 방법은 여기 를 참고하길 바란다.

표준화는 마지막에 MPL 풀 코드를 제시하겠다. 아래는 데이터의 표준화이다.

norm_data = (data - np.mean(data, axis=0)) / np.std(data, axis=0)

훈련 후 마지막에 역 표준화를 해 주어야 한다.

# 역표준화, 표준화의 연산을 다시 풀어준다. data[:, -1:] 는 마지막 target 값을 의미한다.
test_targets = test_targets * np.std(data[:, -1:], axis=0) + np.mean(data[:, -1:], axis=0)
pred = pred * np.std(data[:, -1:], axis=0) + np.mean(data[:, -1:], axis=0)
plt.plot(test_targets, 'ro', label='target')
plt.plot(pred, 'bo', label='pred')
plt.legend()
plt.show()

결과는?

표준화 정규화 - pyojunhwa jeong-gyuhwa

드라마틱한 결과는 크게 하지 마시오(...)

아래는 표준화 중 Zscore를 이용한 MPL 코드이다.

import numpy as np
from numpy import genfromtxt
import matplotlib.pyplot as plt
"""
Setup Hyperparameters
np.random.seed(20220214)

W1 = np.random.randn(32, 8)
B1 = np.random.randn(32, 1)
W2 = np.random.randn(8, 32)
B2 = np.random.randn(8, 1)
W3 = np.random.randn(1, 8)
B3 = np.random.randn(1, 1)

learning_rate = 0.004

batch_size = 500
steps=0
epochs = 3000

before loss 417.82988286417947
before loss 0.24167556631433623
test loss 0.2483900751894638
"""
data = genfromtxt('ConcreteStrengthData.csv', delimiter=',', skip_header = 1)

norm_data = (data - np.mean(data, axis=0)) / np.std(data, axis=0)

train_data = norm_data[:1000, :]
test_data = norm_data[1000:, :]

inputs = train_data[:, 0:8]
targets = train_data[:, -1:]

test_inputs = test_data[:, 0:8]
test_targets = test_data[:, -1:]



np.random.seed(20220214)

W1 = np.random.randn(32, 8)
B1 = np.random.randn(32, 1)
W2 = np.random.randn(8, 32)
B2 = np.random.randn(8, 1)
W3 = np.random.randn(1, 8)
B3 = np.random.randn(1, 1)

learning_rate = 0.004

batch_size = 500
steps=0
epochs = 3000

def make_batch(input, target, step, batch_size):
    if len(input) >= step + batch_size:
        input_batch = input[step : step + batch_size]
        target_batch = target[step : step + batch_size]
    else:
        input_batch = input[step : ]
        target_batch = target[step : ]
        
    return input_batch, target_batch

def forward(input, target, W1, B1, W2, B2, W3, B3):
    #G1 operation
    
    X = np.transpose(input, (1, 0)) # (1, batch)
    
    G1 = np.dot(W1, X) + B1 # (8, batch)
    
    R1 = np.maximum(0, G1) # (8, batch)
    
    G2 = np.dot(W2, R1) + B2 # (4, batch)
    
    R2 = np.maximum(0, G2) # (4, batch)
    
    G3 = np.dot(W3, R2) + B3 # (1, batch)
    
    pred = np.transpose(G3, (1, 0)) # (batch, 1)
    
    loss = np.mean(np.power(pred-target, 2)) #(1, 1)
    
    return G1, R1, G2, R2, G3, pred, loss


def loss_gradient(input, target, W1, B1, W2, B2, W3, B3):
    G1, R1, G2, R2, G3, _, _ = forward(input, target, W1, B1, W2, B2, W3, B3)
    
    target = np.transpose(target, (1, 0)) # (1, batch)
    
    dL_dG3 = 2*(G3-target) / len(target[0]) # (1, batch)
    
    dG3_dR2 = np.transpose(W3, (1, 0)) # (4, 1)
    
    dG3_dW3 = np.transpose(R2, (1, 0)) # (batch, 4)
    
    dG3_dB3 = np.ones_like(B3) # (1, 1)

    dR2_dG2 = np.where(R2>0, 1, 0) # (4, batch)
    
    dG2_dR1 = np.transpose(W2, (1, 0)) # (8, 4)
    
    dG2_dW2 = np.transpose(R1, (1, 0)) # (batch, 8)
    
    dG2_dB2 = np.ones_like(B2) # (4, 1)
    
    dR1_dG1 = np.where(R1>0, 1, 0) # (8, batch)
    
    dG1_dW1 = input # (batch, 1) 왜냐면 input의 전치가 연산되었는데 다시 전치하므로 원래 모양이 됨.
    
    dG1_dB1 = np.ones_like(B3) # (8, 1)
    
    # chain rule
    # operation W3, B3
    dL_dW3 = np.dot(dL_dG3, dG3_dW3) # (1, 4)
    
    dL_dB3 = np.sum(dL_dG3, keepdims=True) * dG3_dB3 # (1, 1) 
    
    # operation W2, B2
    dL_dG2 = np.dot(dG3_dR2, dL_dG3) * dR2_dG2 # (4, batch)
    
    dL_dW2 = np.dot(dL_dG2, dG2_dW2) # (4, 8)
    
    dL_dB2 = np.sum(dL_dG2, axis = 1, keepdims=True) * dG2_dB2 # (4, 1)
    
    # operation W1, B1
    dL_dG1 = np.dot(dG2_dR1, dL_dG2) * dR1_dG1 # (8, batch)
    
    dL_dW1 = np.dot(dL_dG1, dG1_dW1) # (8, 1)
    
    dL_dB1 = np.sum(dL_dG1, axis=1, keepdims=True) * dG1_dB1 # (8, 1)

    return dL_dW1, dL_dB1, dL_dW2, dL_dB2, dL_dW3, dL_dB3
    


_, _, _, _, _, pred, loss = forward(inputs, targets, W1, B1, W2, B2, W3, B3)
#print('before pred', pred) 
print('before loss', loss) 


arr_loss = []
for i in range(epochs): 
    while steps <= len(inputs):
        x_batch, y_batch = make_batch(inputs, targets, steps, batch_size)
    
        _, _, _, _, _, _, loss = forward(x_batch, y_batch, W1, B1, W2, B2, W3, B3)
        dL_dW1, dL_dB1, dL_dW2, dL_dB2, dL_dW3, dL_dB3 = loss_gradient(x_batch, y_batch, W1, B1, W2, B2, W3, B3) 
        W1 = W1 + -1*learning_rate * dL_dW1
        B1 = B1 + -1*learning_rate * dL_dB1
        W2 = W2 + -1*learning_rate * dL_dW2
        B2 = B2 + -1*learning_rate * dL_dB2
        W3 = W3 + -1*learning_rate * dL_dW3
        B3 = B3 + -1*learning_rate * dL_dB3
        arr_loss.append(loss)
        steps += batch_size
        if steps > len(inputs):
            steps = 0 
            np.random.shuffle(train_data)
            break
    

_, _, _, _, _, pred, loss = forward(inputs, targets, W1, B1, W2, B2, W3, B3)
#print('before pred', pred) 
print('after loss', loss) 
    
_, _, _, _, _, pred, test_loss = forward(test_inputs, test_targets, W1, B1, W2, B2, W3, B3)
print('test loss', test_loss)

# 역표준화, 표준화의 연산을 다시 풀어준다. data[:, -1:] 는 마지막 target 값을 의미한다.
test_targets = test_targets * np.std(data[:, -1:], axis=0) + np.mean(data[:, -1:], axis=0)
pred = pred * np.std(data[:, -1:], axis=0) + np.mean(data[:, -1:], axis=0)

plt.plot(test_targets, 'ro', label='target')
plt.plot(pred, 'bo', label='pred')
#plt.xlabel('arry', size=15)
#plt.ylabel('value', size=15)
plt.legend()
#plt.plot(arr_loss)
plt.show()

각 Hyperparameter를 변경해 보고 결과를 각각 확인해 보자.

4. 전처리를 안한다면?

표준화 정규화 - pyojunhwa jeong-gyuhwa

이 데이터는 MPL에서는 다행이도 전처리에 큰 영향을 안 받는것 같기도 한데 전체 오타는 전처리보다 확실히 크다. 
(전처리 안하면 평균 오차 9이고, 전처리를 하면 오차가 소수점으로 나온다)

그래서 순서는

전처리 하지 않고 훈련해 보고 이건 아니다 싶으면

1 (소수점 곱하기)

→ 2 (정규화 Normalization)

→ 3 (표준화 Stardarization) 순서대로 전처리

해 보고 결과를 확인 후 가장 괜찮은 전처리 방법을 사용하자.