모두의 딥러닝 2 Lec 6 ~ Lec 9 review
C팀 : 김경태 남서아 김태현 백윤성 이상민
발표자 : 이상민
유튜브 채널 Deep Learning Zero To All 님의 '모두를 위한 딥러닝 시즌2 - Pytorch' 6~9 번째 강의를 듣고 리뷰하며 이전 학기 수업에서 사용한 데이터를 통해 직접 네트워크를 구성하여 모델 훈련 과정까지 보여드립니다.
모든 코드는 Google Colab을 이용하였으며 Pytorch와 scikit-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이 여러개인 것만 다르고 나머지 과정은 똑같습니다.
직전 학기(2020-2) '머신러닝 입문' 수업의 기말 텀프로젝트로 진행한 데이터를 통해서 MLR의 학습 결과를 알아보겠습니다. 데이터 사용을 허락해 주신 고상호 교수님께 감사의 말씀 올립니다.
해당 데이터가 어떤 의미를 가지는지는 알려주지 않습니다. 단순히 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$ 를 구할 수 있을 것입니다.
이렇게 쉽게 풀 수 있다고?
두가지 의문점이 있습니다.
- Bias는 어떻게 하지?
- $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의 개수가 많을수록 역행렬을 구하는 과정이 오래걸린다.
이러한 특징이 있으며 데이터에 따라 잘 사용하는것이 중요합니다.