10 분 소요

본 글은 ‘시작하세요! 텐서플로 2.0 프로그래밍’ 을 바탕으로 작성되었습니다.

Chapter 4. 회귀

선형 회귀

선형 회귀 Linear Regression이란 데이터의 경향성을 가장 잘 설명하는 하나의 직선을 예측하는 것입니다. 간단한 예제 데이터를 살펴보겠습니다. 다음 예제에서 사용될 데이터는 2018년 우리나라의 지역별 인구증가율과 고령인구비율 데이터로, 두 변수 사이에 어떤 경향성이 있는지 선형회귀로 예측해보겠습니다.

데이터 시각화

image

위 그림에서 오른쪽 아래에 치우친 하나의 점을 극단치 outlier라고 부르며 일반적인 경향에서 벗어나는 사례입니다. 저런 데이터가 섞여있으면 데이터의 경향이 왜곡되어 일반적인 경향을 파악하기 힘들기 때문에 저 데이터를 제거하겠습니다.

극단치 제거

image

이제 위의 데이터로 선형 회귀를 하겠습니다. 데이터의 경향성을 가장 잘 설명하는 하나의 직선과 각 데이터의 차이를 잔차 residual 이라고 부르며 이 잔차의 제곱을 최소화하는 알고리즘을 최소제곱법 Least Square Method 라고 합니다. 수식은 다음과 같습니다.

\[\begin{aligned} a &= \frac{\sum^n_{i=1}(y_i-\bar{y})(x_i-\bar{x})}{\sum^n_{i=1}(x_i-\bar{x})^2} \\ b &= \bar{y}-a\bar{x} \end{aligned}\]

여기서 $x_i, x_y$는 각 데이터 값이고 $\bar{x}, \bar{y}$는 데이터의 평균을 의미합니다. 다음 예제를 통해 최소제곱법으로 $a$와 $b$를 직접 계산해서 회귀선을 구하겠습니다.

최소제곱법으로 회귀선 구하기

image

X가 인구증가율, Y가 고령인구비율입니다. 리스트의 총합을 sum()으로 구하고 리스트의 원소 개수로 나눠 평균을 구했습니다. 최소제곱법을 활용하여 $a$, $b$를 구하는 부분에서는 두 개의 리스트를 하나로 묶는 list(zip(list_1, list_2) 코드를 활용했습니다. 이렇게 구한 $a$와 $b$로 X 변화량에 따른 Y 예측값을 구할 수 있습니다.

이렇게 구한 회귀선에서 데이터의 경향을 알 수 있습니다. X 값이 증가할수록 Y 값은 감소하고 있습니다. 인구증가율과 고령인구비율은 음의 상관관계에 있다는 뜻이 됩니다. 이를 수식과 최소제곱법이 아니라 텐서플로를 이용할 수도 있습니다.

텐서플로를 이용해 회귀선 구하기

99 a: 0.1319381 b: 6.475691 loss: 94.10751
199 a: -0.10856395 b: 11.008399 loss: 31.454575
299 a: -0.250664 b: 13.686895 loss: 13.701685
399 a: -0.31882948 b: 14.971804 loss: 10.266201
499 a: -0.34511712 b: 15.467309 loss: 9.821517
599 a: -0.35328943 b: 15.621353 loss: 9.783098
699 a: -0.35534135 b: 15.660046 loss: 9.7808895
799 a: -0.35575682 b: 15.667872 loss: 9.780806
899 a: -0.3558249 b: 15.669138 loss: 9.780804
999 a: -0.35583445 b: 15.669302 loss: 9.780804

image

최소제곱법을 통해 구한 a,b와 거의 같은 회귀선을 보여주고 있습니다. 코드의 흐름을 살펴보겠습니다. 우선 $a$와 $b$를 초기화한 후 잔차의 제곱의 평균을 구하는 함수를 정의했습니다. 앞선 3장에서는 실제값에서 출력값을 빼는 방식으로 error를 구했는데, 여기서도 실제값인 $Y$에서 출력인 y_pred를 뺐으며 이를 잔차라고 부릅니다. 이 잔차의 제곱을 모두 더해서 평균을 낸 값을 loss로 반환합니다.

손실을 최소화하는 것이 딥러닝의 주요 알고리즘입니다. 최적화 함수(optimizer)는 이 과정을 자동으로 진행해줍니다. 여기서는 Adam을 사용했는데, 3장에서 사용한 SGD와 함께 가장 많이 쓰이는 최적화 함수 중 하나입니다. SGD와 마찬가지로 적당한 합승률을 넣어서 안정적이고 효율적으로 학습하도록 할 수 있습니다.

바로 아래 줄의 for문에서는 1000번동안 학습시키면서 optimizer.miminize() 함수를 실행합니다. 여기서 첫번째 인수에는 최소화할 손실을 전달하며 두번째 인수에는 학습시킬 변수 리스트를 전달합니다. 여기서는 $a$와 $b$를 학습시켰습니다.

다항 회귀

비선형 회귀 Nonlinear Regression는 선형 회귀로는 표현할 수 없는 데이터의 경향성을 설명하기 위한 회귀입니다. $\text{x}^2, \text{x}^3$ 등의 다항식을 이용한 회귀를 다항회귀 Polynomial Regression 이라고 하며, 직선이 아닌 곡선 등이 회귀선이 됩니다. 바로 앞 코드를 조금만 고쳐 회귀선을 구해보도록 하겠습니다.

텐서플로를 이용해 2차 함수 회귀선 구하기

99 a: 4.2996693 b: -6.297481 c: 5.9575715 loss: 74.94936
199 a: 3.0530944 b: -4.8570886 c: 9.821734 loss: 35.21347
299 a: 1.4663273 b: -2.5190392 c: 12.574173 loss: 17.576946
399 a: 0.44507557 b: -1.0124742 c: 14.344 loss: 11.468212
499 a: -0.11940058 b: -0.1801447 c: 15.324776 loss: 9.847553
599 a: -0.39081585 b: 0.22002411 c: 15.79661 loss: 9.515776
699 a: -0.5046818 b: 0.38790342 c: 15.994575 loss: 9.463209
799 a: -0.5464271 b: 0.44945085 c: 16.067152 loss: 9.456766
899 a: -0.5597867 b: 0.46914777 c: 16.090378 loss: 9.456157
999 a: -0.5635116 b: 0.47463968 c: 16.096853 loss: 9.456114

image

앞선 선형회귀 예제에 비해 달라진 부분은 $c$를 추가하고 $a$와 $b$의 차수를 바꿨으며 y_pred를 직선 대신 2차 함수식으로 구하도록 바꾼 정도입니다. 차수를 더 높여보겠습니다.

텐서플로를 이용해 3차 함수 회귀선 구하기

99 a: -1.1583056 b: 5.661726 c: -3.4997823 d: 6.5110593 loss: 61.92112
199 a: -1.9153429 b: 6.6573954 c: -2.3461106 d: 10.029525 loss: 30.168638
299 a: -1.6253709 b: 5.025352 c: -0.9990838 d: 12.148955 loss: 19.833675
399 a: -0.91975373 b: 2.7739043 c: -0.579422 d: 13.631649 loss: 14.161332
499 a: -0.22719423 b: 0.73928905 c: -0.4714103 d: 14.747077 loss: 10.921365
599 a: 0.32200366 b: -0.84846884 c: -0.42991704 d: 15.580522 loss: 9.253502
699 a: 0.71815836 b: -1.9904491 c: -0.40558273 d: 16.174746 loss: 8.487072
799 a: 0.9854189 b: -2.7604465 c: -0.38985723 d: 16.574657 loss: 8.172149
899 a: 1.1550666 b: -3.2491457 c: -0.37998247 d: 16.828339 loss: 8.05639
999 a: 1.2565118 b: -3.5413637 c: -0.37409508 d: 16.980003 loss: 8.018354

image

여기서 차원을 더 늘릴 수도 있고, 다항 회귀에서 더 나아가 비선형 회귀를 적용해볼 수도 있습니다. 분수나 지수, $e^x$ 등을 회귀식에 추가하는 것입니다. 하지만 텐서플로를 이용하면 딥러닝 네트워크를 위한 회귀를 적용해볼 수 있습니다.

딥러닝 네트워크를 활용한 회귀

앞서 만들었던 XOR 네트워크등 처럼 여기서도 딥러닝 네트워크를 만들어보겠습니다.

딥러닝 네트워크를 이용한 회귀

Model: "sequential_2"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_4 (Dense)              (None, 6)                 12        
_________________________________________________________________
dense_5 (Dense)              (None, 1)                 7         
=================================================================
Total params: 19
Trainable params: 19
Non-trainable params: 0
_________________________________________________________________

위 예제의 딥러닝 model은 2개의 Dense 레이어로 구성됩니다. 첫번째 Dense 레이어는 활성화 함수로 $tanh$을 사용해습니다. $tanh$는 하이퍼볼릭 탄젠트 함수로 실수 입력을 받아 -1과 1 사이의 출력을 반환하며 시그모이드 함수와 유사하지만 음수 값도 반환합니다.

첫 번째 레이어에는 6개의 뉴런을 할당했습니다. 너무 많으면 학습이 제대로 안되거나 과적합 overfitting 문제가 발생할 수 있습니다. 이 내용은 뒤에서 더 자세히 다루겠습니다. 두 번째 레이어는 각 입력값이 하나의 출력값만 반환해야하기 때문에 뉴런 수가 1개입니다. optimizer의 손실은 mse로, 평균 제곱 오차입니다. 잔차의 제곱의 평균이 되기 때문에 손실을 줄이는 쪽으로 학습하면 잔차를 줄이는 방향으로 학습합니다.

딥러닝 네트워크의 학습

Epoch 1/10
1/1 [==============================] - 0s 125ms/step - loss: 257.0500
Epoch 2/10
1/1 [==============================] - 0s 3ms/step - loss: 100.7648
Epoch 3/10
1/1 [==============================] - 0s 4ms/step - loss: 13.4928
Epoch 4/10
1/1 [==============================] - 0s 3ms/step - loss: 10.2934
Epoch 5/10
1/1 [==============================] - 0s 2ms/step - loss: 9.8708
Epoch 6/10
1/1 [==============================] - 0s 2ms/step - loss: 9.7876
Epoch 7/10
1/1 [==============================] - 0s 3ms/step - loss: 9.7376
Epoch 8/10
1/1 [==============================] - 0s 4ms/step - loss: 9.6930
Epoch 9/10
1/1 [==============================] - 0s 5ms/step - loss: 9.6546
Epoch 10/10
1/1 [==============================] - 0s 4ms/step - loss: 9.6244

X를 입력하면 Y가 정답이 되도록 10회 학습시킵니다. 에포크가 진행될수록 손실에 거의 변화가 없으면 학습이 거의 다 됐다고 볼 수 있습니다. 딥러닝은 학습을 종료시키는 시점도 중요합니다. 학습이 됐는데도 불구하고 계속해서 학습을 시킨다면 과적합되어 새로운 데이터에 잘 대처하지 못 할 수도 있습니다. 뒤에서 더 자세히 설명하도록 하겠습니다.

딥러닝 네트워크의 Y값 예측

array([[15.686909 ],
       [15.778545 ],
       [15.400834 ],
       [15.740183 ],
       [15.455323 ],
       [15.700078 ],
       [15.775257 ],
       [15.787913 ],
       [15.779294 ],
       [15.784465 ],
       [15.772322 ],
       [15.786219 ],
       [15.777848 ],
       [15.741835 ],
       [15.780049 ],
       [14.5131645]], dtype=float32)

이와 같이 딥러닝 네트워크가 실제로 어떤 값을 예측했는지 확인할 수 있습니다. 거의 15 부근의 값을 출력하는 것을 볼 수 있습니다. 그래프로 확인해보겠습니다.

딥러닝 네트워크의 회귀선 확인

image

앞서 다항 회귀에서 구했던 2차함수와 비슷한 곡선이 나왔습니다. 차이점은 직선에 가까운 완만한 형태라는 것입니다. 손실도 2차 함수와 비슷한 크기가 되기 때문에 비슷한 성능이라는 결론을 내릴 수 있습니다. 이처럼 회귀식을 가정하고 변수를 직접 추정하지 않고 회귀선을 예측하는 방법을 알아봤습니다. 이제 ‘보스턴 주택 가격 데이터세트’ 라는 유명한 데이터에 적용시켜 보겠습니다.

데이터 적용

‘보스턴 주택 가격 데이터세트’는 1978년 미국 보스턴 지역의 주택 가격으로, 506개 타운의 주택 가격 중앙값을 1,000달러 단위로 나타냅니다. 변수로는 범죄율, 주택 당 방 개수, 고속도로까지의 거리 등 13가지 속성이 있으며 이 속성을 활용하여 주택 가격을 예측해야 합니다.

데이터 불러오기

404 102
[  1.23247   0.        8.14      0.        0.538     6.142    91.7
   3.9769    4.      307.       21.      396.9      18.72   ]
15.2

keras에 기본적으로 탑재되어 있으므로 load_date를 통해 데이터를 불러왔습니다. 이때 훈련 데이터와 테스트 데이터를 나누게 됩니다. 훈련 데이터는 학습 과정에 사용되는 데이터며 테스트 데이터는 학습 결과를 평가하기 위한 데이터입니다. 학습할 때는 훈련 데이터만 사용하게 됩니다. 그리고 훈련 데이터로 학습할 때 일부 데이터를 떼어내서 검증 데이터로 만들 수 있습니다. 검증 데이터로는 학습 중에 학습이 잘 되고 있는지 검증하는 용도로 쓰입니다. ‘보스턴 주택 가격 데이터세트’는 훈련 데이터가 404개, 테스트 데이터가 102개 입니다. 보통 훈련, 검증, 테스트 데이터의 비율은 약 6:2:2 정도로 하는 경우가 많습니다.

데이터를 살펴보면 데이터의 단위가 다릅니다. 퍼센트인 것도 있고, 0/1로 나타나는 데이터도 있습니다. 이러한 데이터를 전처리해서 정규화 normalize를 하면 대체로 더 학습을 잘 시킬 수 있습니다. 이를 위해 각 데이터에서 평균값을 뺀 다음 표준편차로 나눠줄 수 있습니다. 이를 통해 데이터의 분포를 정규분포로 옮길 수 있습니다. 훈련 데이터의 평균과 표준편차로 정규화한 후 이것으로 테스트 데이터도 정규화하게 됩니다. 실제 상황에 적용할 때는 테스트 데이터의 분포를 활용할 수 없습니다.

데이터 전처리(정규화)

[-0.27224633 -0.48361547 -0.43576161 -0.25683275 -0.1652266  -0.1764426
  0.81306188  0.1166983  -0.62624905 -0.59517003  1.14850044  0.44807713
  0.8252202 ]
-0.7821526033779157

전처리 결과 데이터가 정규분포에 들어가도록 바뀌었습니다. 이제 이 데이터로 딥러닝 네트워크를 학습시키겠습니다.

보스턴 주택 가격 데이터세트 회귀 모델 생성

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 52)                728       
_________________________________________________________________
dense_1 (Dense)              (None, 39)                2067      
_________________________________________________________________
dense_2 (Dense)              (None, 26)                1040      
_________________________________________________________________
dense_3 (Dense)              (None, 1)                 27        
=================================================================
Total params: 3,862
Trainable params: 3,862
Non-trainable params: 0
_________________________________________________________________

앞서 다뤘던 데이터보다 속성의 수도 많고 데이터의 양도 많기 때문에 4개의 레이어를 사용했고 뉴런의 수도 늘렸습니다. 레이어가 깊고 뉴런 수가 많을수록 더 복잡한 내용을 학습시킬 수 있습니다. 첫 번째 레이어에서는 데이터의 속성을 모두 불러오기 위해 input_shape의 첫 번째 차원을 13으로 했습니다. 마지막 레이어는 목표변수인 주택가격을 예측하기 위해 뉴런을 1개로 했습니다. 활성화 함수로는 ReLU를 사용했습니다. 가장 많이 사용되는 활성화 함수라고 볼 수 있습니다.

image

활성화 함수들 비교

이제 model.fit() 함수로 회귀 모델을 학습시켜 보겠습니다.

회귀 모델 학습

Epoch 1/50
10/10 [==============================] - 1s 10ms/step - loss: 1.9500 - val_loss: 1.0258
Epoch 2/50
10/10 [==============================] - 0s 3ms/step - loss: 0.6526 - val_loss: 1.0113
Epoch 3/50
10/10 [==============================] - 0s 3ms/step - loss: 0.5247 - val_loss: 0.6702
Epoch 4/50
10/10 [==============================] - 0s 4ms/step - loss: 0.5535 - val_loss: 0.4628
Epoch 5/50
10/10 [==============================] - 0s 4ms/step - loss: 0.3913 - val_loss: 0.5113
Epoch 6/50
10/10 [==============================] - 0s 3ms/step - loss: 0.3498 - val_loss: 0.5492
...
Epoch 48/50
10/10 [==============================] - 0s 4ms/step - loss: 0.1016 - val_loss: 0.1948
Epoch 49/50
10/10 [==============================] - 0s 4ms/step - loss: 0.1418 - val_loss: 0.1855
Epoch 50/50
10/10 [==============================] - 0s 4ms/step - loss: 0.1013 - val_loss: 0.2090

여기서는 validation_split 라는 인수가 추가됩니다. 훈련 데이터의 25%를 검증 데이터로 떼서 학습 결과를 검증하게 됩니다. 그 결과 각 에포크의 학습 결과 출력에 val_loss도 같이 표시됩니다. 출력의 경향을 보면 loss는 꾸준히 감소하지만 val_lossloss보다 높은 값을 유지하며 항상 감소하지 않습니다. 이 결과를 그래프로 시각화 해보겠습니다.

회귀 모델의 학습 결과 시각화

image

훈련 데이터의 손실은 꾸준히 감소하고 있지만 검증 데이터의 손실은 항상 감소하지 않습니다. 전체적으로는 감소하는 경향을 보이지만 부분적으로는 오히려 증가하는 모습도 보입니다. 학습을 마친 현 상태에서 테스트 데이터를 이용해 회귀 모델을 평가해보겠습니다.

회귀 모델 평가

4/4 [==============================] - 0s 1ms/step - loss: 0.2596
0.2595985531806946

테스트 데이터의 손실이 0.259로, 위의 훈련 데이터의 손실인 0.2090에 비해 높습니다. 얼마나 잘 예측하는지 확인해보기 위해 실제 가격과 예측 가격을 1:1로 비교해보겠습니다.

실제 주택 가격과 예측 주택 가격 시각화

image

이상적이라면 대각선 상에 모든 점이 위치해야합니다. 하지만 실제 주택 가격에 대해 예측 주택 가격은 전체적으로 비슷한 모습을 보이지만 일부 떨어진 값도 눈에 보입니다. 검증 데이터와 테스트 데이터는 학습할 때 사용되지 않으므로 학습에 영향을 미치지 않는다는 공통점이 있으므로 검증 데이터에 대한 성적을 신경쓰면 테스트 데이터의 성능도 끌어올릴 수 있을 것으로 보입니다. 즉 훈련 데이터에 과적합되어 val_loss가 높아지지 않도록 중간에 학습을 멈춰줄 필요가 있습니다.

이를 위해서는 콜백 callback 함수를 사용합니다. 콜백 함수는 모델을 학습할 때 에포크가 끝날 때마다 호출됩니다. model.fit() 함수에 callbacks 인수ㅡ를 사용해 콜백 함수의 리스트를 지정할 수 있습니다.

모델 재정의 및 학습

Epoch 1/50
10/10 [==============================] - 0s 9ms/step - loss: 1.9500 - val_loss: 1.0258
Epoch 2/50
10/10 [==============================] - 0s 4ms/step - loss: 0.6526 - val_loss: 1.0113
Epoch 3/50
10/10 [==============================] - 0s 3ms/step - loss: 0.5247 - val_loss: 0.6702
Epoch 4/50
10/10 [==============================] - 0s 4ms/step - loss: 0.5535 - val_loss: 0.4628
Epoch 5/50
10/10 [==============================] - 0s 4ms/step - loss: 0.3913 - val_loss: 0.5113
Epoch 6/50
10/10 [==============================] - 0s 3ms/step - loss: 0.3498 - val_loss: 0.5492
...
Epoch 27/50
10/10 [==============================] - 0s 4ms/step - loss: 0.1116 - val_loss: 0.1899
Epoch 28/50
10/10 [==============================] - 0s 3ms/step - loss: 0.1164 - val_loss: 0.1998
Epoch 29/50
10/10 [==============================] - 0s 3ms/step - loss: 0.1457 - val_loss: 0.1834

콜백 함수의 리스트에 들어간 함수는 tf.keras.callbacks.EarlyStopping 입니다. 학습을 일찍 멈추는 기능을 하는 함수로 patience는 몇 번의 에포크를 기준으로 삼을 것인지, monitor는 어떤 값을 지켜볼 것인지에 대한 인수입니다. 여기서는 val_loss가 5회의 에포크를 수행하는 동안 최고 기록을 갱신하지 못 하면 학습이 멈추게 됩니다. 여기선 29에 멈췄습니다.

회귀 모델의 학습 결과 시각화

image

검증 데이터의 손실에 뚜렷한 증가세가 없습니다. 이렇게 학습된 모델을 다시 평가해보겠습니다.

회귀 모델 평가

4/4 [==============================] - 0s 1ms/step - loss: 0.4206
0.42063912749290466

앞선 결과에 비해 오히려 손실이 높게 나왔습니다. 데이터의 크기가 크지 않아 테스트 데이터에는 오히려 덜 적합하도록 학습이 된 것 같습니다.

회귀 모델 학습 결과 시각화

image

시각화 결과 역시 눈으로 봤을 때는 큰 차이가 없어보입니다. 비록 이번 예제에서는 데이터의 크기, 모델의 적합도 등의 여러 문제로 early stopping을 사용했을 때 테스트 데이터에 대해 오히려 더 낮은 성능을 보였지만 대체로는 학습 속도도 더 빨라지며 과적합 문제도 어느정도 방지할 수 있는 효과가 있습니다.

댓글남기기