5기(210102~)/C팀

모두의 딥러닝 2 Lec 6 ~ Lec 9 review

KAU-Deeperent 2021. 1. 22. 15:39

 

 

 

 

 

 

 

C팀 : 김경태 남서아 김태현 백윤성 이상민

발표자 : 이상민

 

유튜브 채널 Deep Learning Zero To All 님의 '모두를 위한 딥러닝 시즌2 - Pytorch' 6~9 번째 강의를 듣고 리뷰하며 이전 학기 수업에서 사용한 데이터를 통해 직접 네트워크를 구성하여 모델 훈련 과정까지 보여드립니다. 

 

모든 코드는 Google Colab을 이용하였으며 Pytorchscikit-learn, pandas 모듈을 사용하였습니다.


1. Multivariate Linear Regression

이전 강의에서는 Input이 하나인 Single Linear Regression을 배웠습니다. Input $x$ 하나로부터 가설

 

$H(x)=wx+b$

 

를 세워 Output을 유추 하고 이를 실제 output $y$ 와 비교하여 오차를 역전파시켜 네트워크의  weight를 업데이트하였습니다.

 

Multivariate Linear Regression(이하 MLR)은 Input이 여러개인 것만 다르고 나머지 과정은 똑같습니다.

 

Single Linear Regression과 Multivariable Linear Regression의 차이

 

 

직전 학기(2020-2) '머신러닝 입문' 수업의 기말 텀프로젝트로 진행한 데이터를 통해서 MLR의 학습 결과를 알아보겠습니다. 데이터 사용을 허락해 주신 고상호 교수님께 감사의 말씀 올립니다.

 

 

example_dataset_final.csv
0.00MB
예시 데이터

 

해당 데이터가 어떤 의미를 가지는지는 알려주지 않습니다. 단순히 4개의 feature들이 있고 이를 통해 하나의 target을 구하는 문제입니다. 이 50개의 데이터를 통해 선형회귀를 진행하여 보겠습니다.

 

처음부터 문제가 생겼습니다. 우리가 배운 선형회귀는 단순히 '숫자'만을 취급했습니다. 하지만 feature4를 보면 숫자형 변수(Numerical Variable)가 아닌 범주형 변수(Categorical Variable)인것을 확인 할 수 있습니다. 그렇다면 저 Area를 어떻게 하는것이 좋을까요?

 

이때 범주형 변수를 숫자형 변수로 바꾸는 Encoding이 사용됩니다. 그렇다면 단순히 Area 1,2,3을 숫자 1,2,3으로 바꾸는 Label Encoding을 사용하였습니다.

 

 

 

이렇게 바꾸는것이 맞을까요?

 

정답은 No 입니다.

 

지역(Area)은 상하 혹은 대소 관계를 가지는 변수가 아닙니다. 만약 하나의 데이터가 어느 한 사람의 출생지를 나타내는 것이라면 컴퓨터가 계산 시 Area3에서 온 사람이 Area1에서 온 사람보다 더 큰 값(가치)을 가진다고 생각 할 수 있습니다.

 

 

 

따라서 이러한 Label Encoding은 이렇게 숫자의 차이가 모델에 영향을 미치는 선형 계열 모델(로지스틱회귀, SVM, 신경망)에는 사용하면 안됩니다.

 

이러한 경우에는 각 범주에 속하는 지를 확인하는 One-hot encoding을 사용함으로써 범주형 변수를 숫자형으로 바꿀 수 있습니다.

 

Encoding을 통해 범주형 변수였던 feature4가 3개의 숫자형 변수로 나뉘어져 총 6개의 feature가 되었습니다. 이제는 이 50개의 데이터를 통해 네트워크를 어떻게 구성하였는지 알아보겠습니다. 

 

1. 데이터 불러오기

import pandas as pd

# Load example dataset
df = pd.read_csv("/content/drive/MyDrive/Colab Notebooks/Examples/example_dataset_final.csv", 
                  header = 0 , 
                  skiprows = [1])

# Remove first column
df = df.drop('Unnamed: 0', axis = 1)

 

먼저 데이터 전처리를 위해 pandas를 import하여 example_dataset_final.csv 파일을 읽어줍니다. 또한 첫 열은 비어있는 열이므로 제거해줍니다.

 

2. 원-핫 인코딩

# One-hot encoding categorical data
df = pd.get_dummies(df)

이전 One-hot encoding을 pandas에서 구현해줍니다. get_dummies라는 함수에 데이터 전체를 넣음으로 쉽게 구현이 가능합니다.

 

3. 훈련/테스트 데이터 분리

# Split into training/test dataset
from sklearn.model_selection import train_test_split
train, test = train_test_split(df, test_size = 0.3, random_state = 1)

X_train = train.drop(['target'], axis = 1).values
y_train = train['target'].values
X_test = test.drop(['target'], axis = 1).values
y_test = test['target'].values

다음은 모델 훈련을 위한 Training dataset와 Test dataset을 나눠줍니다. 이는 scikit-learn을 통해 진행합니다. Training dataset과 Test dataset의 비율은 7:3으로, 35개의 Training data와 15개의 Test data로 랜덤하게 나뉘어줍니다. 나뉘어진 데이터 속에서 Input이 되는 $X_{train}$ 과 $X_{test}$ 그리고 Output이 되는 $y_{train}$ 과 $y_{test}$ 를 정의해줍니다.

 

4. 표준화

# Standardization
from sklearn.preprocessing import StandardScaler
sc = StandardScaler()
X_train_scaled = sc.fit_transform(X_train)
X_test_scaled = sc.transform(X_test)

모델 전처리 중 하나인 표준화입니다. 이 또한 scikit-learn을 통해서 진행하였습니다. 이 과정에서 주의해야 할 사항이 scaler는 모델 훈련에 사용되어야 할 $X_{train}$ 을 통해 정해져야 합니다. sc.fit_transform으로 $X_{train}$ 에 맞게 scale 범위를 잡고 sc.transform을 통해 $X_{test}$ 의 표준화를 진행합니다.

 

 


표준화 예시


왜 훈련 데이터셋에 맞춰서 표준화를 진행해야 하는지 간단한 예시를 들어서 설명하겠습니다.

train data가 0~10까지의 데이터일때 전처리 방법 중 하나인 정규화(Normalization)인 MinMaxScaler를 사용하였습니다.

정규화는 다음과 같은 공식을 사용하여 특성 값의 범위를 [0, 1]로 변환합니다.

 

$X^{\prime}=\frac{X-X_{min}}{X_{max}-X_{min}}$

 

import numpy as np
from sklearn.preprocessing import MinMaxScaler
train = np.array([0.,1.,2.,3.,4.,5.,6.,7.,8.,9.,10.]).reshape(-1,1)

scaler=MinMaxScaler()
scaler.fit(train)
train_scaled=scaler.transform(train).reshape(1,-1)

print("Train data:       " , train.reshape(1,-1))
print("Scaled_train data:" ,train_scaled)
Train data:        [[ 0.  1.  2.  3.  4.  5.  6.  7.  8.  9. 10.]]
Scaled_train data: [[0.  0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]]

0부터 1까지의 데이터이기 때문에 0은 0으로 10은 1로 0.1 단위로 데이터 스케일링이 완료되었습니다.

test data가 0에서 5까지의 범주를 가진다면 어떻게 될까요?

 

test = np.array([0., 1., 2., 3., 4., 5. ]).reshape(-1,1)

scaler_test = MinMaxScaler()
scaler_test.fit(test)

test_scaled       = scaler.transform(test).reshape(1,-1)
test_scaled_wrong = scaler_test.transform(test).reshape(1,-1)

print("Test data:            " , test.reshape(1,-1))
print("Scaled test data:     " ,test_scaled)
print("Wrong Scaled test data:" , test_scaled_wrong)
Test data:             [[0. 1. 2. 3. 4. 5.]]
Scaled test data:      [[0.  0.1 0.2 0.3 0.4 0.5]]
Wrong Scaled test data: [[0.  0.2 0.4 0.6 0.8 1. ]]

train data로 훈련된 네트워크에는 5라는 숫자 데이터가 0.5로 입력되어야 할겁니다. 하지만 scaling을 test 데이터셋으로 하게 되면 5라는 숫자형 데이터는 scaling 후 1로 입력되어 잘못된 결과를 가져올 수 있습니다.

 

이러한 이유로 네트워크 모델 훈련에 사용되는 $X_{train}$으로만 scaling이 진행되어야 합니다.

 

예시로는 MinMaxScaler를 사용하는 정규화를 들었으나 표준화 역시 마찬가지입니다. 하지만 표준화와 정규화 중에 어떤것이 더 좋은가에 대한 답은 없습니다. 데이터마다 다르므로 표준화 혹은 정규화는 직접 해보고 더 나은 것을 채택하는 것이 좋습니다.

 

 


5. 텐서 변환

# Convert array to tensor
X_train_scaled = torch.FloatTensor(X_train_scaled)
y_train = torch.FloatTensor(y_train)
X_test_scaled = torch.FloatTensor(X_test_scaled)
y_test = torch.FloatTensor(y_test)

Array로 정의된 각 데이터들을 pytorch로 이루어진 네트워크에서 작업 할 수 있도록 Tensor로 변환해줍니다.

 

X_train_scaled
tensor([[-1.2537, -1.0762, -0.5965,  1.2247, -0.7687, -0.5443],
        [-1.7804, -0.2633, -1.4732,  1.2247, -0.7687, -0.5443],
        [-0.1022,  0.7982, -0.7155, -0.8165,  1.3009, -0.5443],
        [-0.1320, -0.0366,  0.7277, -0.8165,  1.3009, -0.5443],
        [ 0.2690,  0.4604,  0.2958,  1.2247, -0.7687, -0.5443],
        [-1.1590, -1.5250, -0.4561, -0.8165,  1.3009, -0.5443],
        [ 0.3284,  0.8350,  0.5508, -0.8165, -0.7687,  1.8371],
        [-1.7581,  0.0170, -1.8421, -0.8165, -0.7687,  1.8371],
        [-0.0640, -0.9554, -0.6599, -0.8165, -0.7687,  1.8371],
        [-0.2759, -0.7024,  0.7400, -0.8165,  1.3009, -0.5443],
        [ 1.3855, -1.2638,  1.2635, -0.8165,  1.3009, -0.5443],
        [-0.5441, -0.8077, -0.0284, -0.8165,  1.3009, -0.5443],
        [ 0.8917,  1.2834,  0.3286, -0.8165,  1.3009, -0.5443],
        [-0.3993, -0.3158, -1.0814, -0.8165,  1.3009, -0.5443],
        [ 0.4900, -0.5131,  0.0954, -0.8165,  1.3009, -0.5443],
        [-0.3089,  2.3037, -0.8511, -0.8165,  1.3009, -0.5443],
        [-1.2863,  1.2153, -1.6168,  1.2247, -0.7687, -0.5443],
        [-0.7461,  1.3282, -0.0611,  1.2247, -0.7687, -0.5443],
        [ 0.2636, -0.3731,  0.6561, -0.8165,  1.3009, -0.5443],
        [-0.0816, -0.3851,  0.6880,  1.2247, -0.7687, -0.5443],
        [-0.3398,  0.6190, -0.6821,  1.2247, -0.7687, -0.5443],
        [ 1.2185,  0.9179, -0.7695,  1.2247, -0.7687, -0.5443],
        [ 1.1224,  0.8527,  0.9029, -0.8165,  1.3009, -0.5443],
        [-1.7804,  0.4577, -1.8584,  1.2247, -0.7687, -0.5443],
        [ 1.8419,  1.0813,  1.9262,  1.2247, -0.7687, -0.5443],
        [-0.0424, -0.0829,  0.3954,  1.2247, -0.7687, -0.5443],
        [ 1.9032,  0.5152,  2.1640, -0.8165, -0.7687,  1.8371],
        [ 0.7709, -0.0431,  0.3735, -0.8165, -0.7687,  1.8371],
        [ 1.1575, -0.9345,  1.2353, -0.8165, -0.7687,  1.8371],
        [ 0.4624, -1.2482,  0.2709,  1.2247, -0.7687, -0.5443],
        [ 0.9672, -0.5880,  0.7418,  1.2247, -0.7687, -0.5443],
        [ 0.9050,  0.9773,  0.7984, -0.8165, -0.7687,  1.8371],
        [ 0.3107,  0.1408,  0.2717, -0.8165,  1.3009, -0.5443],
        [-1.4350,  0.1432, -1.5554, -0.8165, -0.7687,  1.8371],
        [-0.7986, -2.8318, -0.1785,  1.2247, -0.7687, -0.5443]])

 

6. 모델 훈련

표준화된 $X_{train}$입니다. 이 35개의 데이터를 통해 네트워크를 훈련시켜보겠습니다.

 

class MLRModel(nn.Module):
  def __init__(self):
    super().__init__()
    self.linear = nn.Linear(6,1)
  
  def forward(self, x):
    y_pred = self.linear(x)
    return y_pred

feature의 개수가 6개이므로 torch.nn.Linear을 사용하여 6개의 Input을 통해 하나의 Output을 출력하도록 클래스를 정의하였습니다. nn.Linear을 사용하면 자동으로 bias도 정의합니다.

 

model = MLRModel()

optimizer = optim.SGD(model.parameters(), lr = 0.005)

cost_list = []
n_epochs = 2000

for epoch in range(1,n_epochs + 1):

  y_pred = model(X_train_scaled).flatten()

  cost = F.mse_loss(y_pred, y_train)

  optimizer.zero_grad()
  cost.backward()
  optimizer.step()
  cost_list.append(cost)

  if epoch % 100 ==0:
    print('Epoch {:4d}/{} Cost: {:.6f}'.format(
        epoch, n_epochs, cost.item()
    ))
Epoch  100/2000 Cost: 2025092608.000000
Epoch  200/2000 Cost: 369067904.000000
Epoch  300/2000 Cost: 143457248.000000
Epoch  400/2000 Cost: 107922904.000000
Epoch  500/2000 Cost: 99025264.000000
Epoch  600/2000 Cost: 94669816.000000
Epoch  700/2000 Cost: 91670208.000000
Epoch  800/2000 Cost: 89423216.000000
Epoch  900/2000 Cost: 87713152.000000
Epoch 1000/2000 Cost: 86407944.000000
Epoch 1100/2000 Cost: 85411328.000000
Epoch 1200/2000 Cost: 84650240.000000
Epoch 1300/2000 Cost: 84069032.000000
Epoch 1400/2000 Cost: 83625192.000000
Epoch 1500/2000 Cost: 83286208.000000
Epoch 1600/2000 Cost: 83027328.000000
Epoch 1700/2000 Cost: 82829632.000000
Epoch 1800/2000 Cost: 82678640.000000
Epoch 1900/2000 Cost: 82563352.000000
Epoch 2000/2000 Cost: 82475344.000000
plt.plot(range(epoch), cost_list , label = "cost")
plt.xlabel("Epochs")
plt.ylabel("Cost")
plt.legend()
plt.show()

model을 새로 만든 클래스로 정의해주고 Optimizer는 SGD로, Learning rate는 0.005로 설정하였습니다. 총 2000회의 Epoch를 통해 학습된 결과를 보여줍니다.

 

Cost의 단위가 매우 큰데 이는 유추하고자 하는 값(Target Variable)의 값 자체가 커서 오차의 스케일 자체가 커서 생기는 결과입니다. 우리의 목적은 Cost 값이 작아지도록 하는 것이므로 이는 훈련이 잘 되었음을 보여줍니다.

 

7. 회귀 결과

이제 네트워크가 유추한 값과 실제 정답과의 비교를 해보도록 하겠습니다. 먼저 Training Data입니다.

 

y_pred = model(X_train_scaled).flatten().data

Training_results = pd.DataFrame({'True Value' : y_train, 'Predictions' : y_pred}, index = range(1,y_train.shape[0]+1))
                    
print('\nTraining Results')
print(Training_results)

fig1 = plt.figure(figsize= (6,6))
plt.scatter(y_train, y_pred)
plt.xlabel('True Values',size = 15)
plt.ylabel('Predictions',size = 15)
plt.axis('equal')
plt.axis('square')
plt.xlim([0,200000])
plt.ylim([0,200000])
_ = plt.plot([0, 1000000], [0, 1000000],
            color = 'black', 
            ls = '--')
plt.show()
Training Results
       True Value    Predictions
1    71498.492188   71898.289062
2    14681.400391   49641.808594
3   105733.539062  108806.859375
4   110352.250000  115282.609375
5   134307.343750  127429.085938
6    77798.828125   74898.406250
7   125370.367188  130500.093750
8    64926.078125   48285.039062
9   108552.039062  111320.304688
10  108733.992188  110582.976562
11  166187.937500  169104.453125
12   96778.921875   97656.687500
13  132602.656250  147470.375000
14   99937.593750   97078.515625
15  146121.953125  132981.265625
16  103282.382812  101013.398438
17   65200.328125   65339.175781
18   96712.796875   91425.945312
19  124266.898438  128232.601562
20  118474.031250  117753.289062
21  107404.343750  101966.382812
22  156122.515625  153811.718750
23  155752.593750  158200.796875
24   42559.730469   47594.281250
25  191792.062500  188520.140625
26  126992.929688  117539.812500
27  192261.828125  191667.750000
28  129917.039062  144546.421875
29  156991.125000  162031.968750
30  144259.406250  133975.812500
31  149759.953125  153267.718750
32  152211.765625  151114.484375
33  141585.515625  127792.898438
34   69758.976562   60589.187500
35   89949.140625   89505.226562

 

그래프에서 점선은 $y=x$ 값을 나타내는 선입니다. $x$ 축은 정답, 실제 Target Variable 값을 나타내며 $y$ 축은 네트워크를 통해 유추한 값입니다. 여러개의 Outlier들이 보이지만 여러 Epoch를 더 진행해보아도 크게 변하는 것은 없었습니다.

 

y_pred_test = model(X_test_scaled).flatten().data

Test_results = pd.DataFrame({'True Value' : y_test, 'Predictions' : y_pred_test}, index = range(1,y_test.shape[0]+1))
print('\nTest Results')
print(Test_results)

fig1 = plt.figure(figsize= (6,6))
plt.scatter(y_test, y_pred_test)
plt.xlabel('True Values',size = 15)
plt.ylabel('Predictions',size = 15)
plt.axis('equal')
plt.axis('square')
plt.xlim([0,200000])
plt.ylim([0,200000])
_ = plt.plot([0, 1000000], [0, 1000000],
            color = 'black', 
            ls = '--')
plt.show()
Test Results
       True Value    Predictions
1   105008.312500  116784.882812
2    96479.507812   91002.007812
3    78239.906250   76711.976562
4    81229.062500   70910.703125
5   191050.390625  179364.203125
6   182901.984375  172187.718750
7    35673.410156   48160.277344
8   101004.640625  101081.867188
9    49490.750000   60675.878906
10   97483.562500   96914.820312
11   97427.843750   97061.429688
12   81005.757812   84289.640625
13  111313.023438  119044.281250
14   90708.187500   76898.218750
15  122776.859375  111974.843750

 

Test Dataset에 대한 유추 결과 및 정답과의 비교입니다. 훈련된 모델을 통해 Test 데이터도 어느정도 잘 회귀 하였음을 보여줍니다.

 

회귀모델에 대한 정확도 판단은 보통 Residue나 R2 score를 사용합니다. 이는 따로 링크를 통해 확인하였습니다.

 

8. 결론

  • 실제 데이터를 사용하여 회귀 구현
  • 전처리를 통한 데이터 Scaling
  • nn.Linear 모듈을 사용하여 네트워크 구성
  • Training data를 통해 모델 훈련 및 결과 확인

 

2. Normal Equation

두번째로는 정규방정식에 대해서 알아보겠습니다.

 

정규방정식은 방정식의 해를 구하는 방법 중 하나입니다. 기계공학과 과목인 로봇공학에서, 전자과 과목인 영상처리에서도 배우며 공학에서 전반적으로 많이 사용되는 선형대수입니다.

 

그렇다면 머신러닝에서의 정규방정식은 어떻게 사용될까요?

 

 

우리가 어떤 연립 방정식을 풀때 행렬로 나타내고 역행렬을 통해 원하는 미지수의 값을 구할 수 있습니다.

이때 우리가 알게 모르게 정규방정식을 사용하고 있던것입니다.

 

같은 방식으로 우리는 Training data를 통해 35개의 방정식을 가지고 있던 것입니다. 단순히 $X_{train}$ 을 역행렬하여 정답인 $y_{train}$ 앞에 곱해주면 우리가 원하는 가중치인 $w$ 를 구할 수 있을 것입니다.

 

이렇게 쉽게 풀 수 있다고? 

 

두가지 의문점이 있습니다.

  1. Bias는 어떻게 하지?
  2. $X_{train}$ 은 정사각행렬이 아니라서 역행렬이 없는데?

1. Bias

우리는 네트워크를 구성할 때 Input의 feature 개수에 맞게 가중치 $w$를 개수를 정하였습니다. 또한 편향인 bias르 추가하였죠.

 

그렇다면 식은 위 그림처럼 나올 것입니다. 이때 행렬 $b$를 Training Data인 $X$ 안으로 넣어줍니다.

 

그렇다면 $X$ 안에 1이라는 열이 하나 맨 앞에 생기고 bias는 가중치의 일부로 들어가게 됩니다. 만약

 

$b=w_0$

 

라고 한다면 이 방정식을

 

$Xw=Y$

 

로 나타낼 수 있습니다. 이제 bias는 문제가 없겠죠?

 

2. 역행렬

역행렬이 없는것은 의사역행렬(Pseudo Inverse Matrix)로 해결 할 수 있습니다. 이를 처음 접하게 된것은 4학년 2학기 로봇공학을 통해 처음 배웠습니다. 그때는 다자유도 로봇 팔이 더 적은 자유도를 어떻게 표현해야 하는가에 대한 해를 찾기 위해 사용하였습니다.

 

그때는 로봇의 자유도(n) > 표현하고자 하는 자유도(m) 이었기에 Right Inverse를 사용하였습니다. 하지만 머신러닝에서는 대부분 feature의 개수(n) < data의 개수(m) 이기에 Left Inverse를 주로 사용합니다.

 

요지는 간단합니다. 어떤 행렬의 전치행렬(Transpose) 과 그 행렬의 곱은 정사각행렬을 만듭니다. 이를 다시 전치행렬과 곱해주면 역행렬과 비슷한 결과를 낼 수 있습니다.

 

이렇게 데이터의 역행렬에 대한 의문점을 해결 할 수 있습니다.

 


 

그렇다면 이렇게 구한 값이 우리가 네트워크를 구성해서 Gradient Descent를 통해 몇 천번의 Epoch로 구한 값과 같을까? 라는 생각을 해보았습니다. 

 

 

우리는 기본적으로 cost function을 MSE(Mean Square Error)로 잡았습니다. 이때 $H(x)$ 는 예상한 결과이며 bias를 포함하여 $Xw$ 라는 행렬로 나타낼 수 있습니다.

 

$H(x) = Xw$

 

네트워크를 통해 cost가 가장 작은 값을 가지는 가중치를 찾았습니다. 이때 cost가 2차함수의 포물선 형태이며 가장 작은 값은 cost의 미분값이 0일때입니다.

 

따라서 cost를 행렬미분을 통해 0이 되는 값을 구해주면

 

$X^TXw-X^Ty=0$

 

$X^TXw=X^Ty$

 

$w=(X^TX)^{-1}X^Ty$

 

의 결과를 얻을 수 있으며 이는 이전 우리가 보았던 정규방정식의 결과와 같은 것을 볼 수 있습니다.

 

이를 통해 우리는 Gradient Descent와 정규방정식의 결과는 동일하다는 것을 확인 할 수 있습니다.

 

그렇다면 왜 우리는 이 간단한 정규방정식을 사용하지 않을까요?

 

당연하게도 장단점이 있습니다.

 

Gradient Descent 특징

  • 반복적으로 계산한다
  • 학습률(learning rate)를 정해주어야 한다.
  • feature의 개수가 많을 때도 잘 작동한다.

 

Normal Equation 특징

  • 반복하지 않는다
  • 학습률을 정하지 않는다
  • feature의 전처리를 하지 않아도 된다
  • feature의 개수가 많을수록 역행렬을 구하는 과정이 오래걸린다.

 

이러한 특징이 있으며 데이터에 따라 잘 사용하는것이 중요합니다.