안녕하세요. 23년 하반기는 정말 바쁜 시기였습니다. 글도 제대로 못 쓸 정도로요… 조만간에 어떤 일들이 있었는지, 앞으로 무엇을 할 건지 등 일상글도 작성해보도록 하겠습니다 ㅎㅎㅎ 마지막 12월에는 파이토치를 이용한 선형 회귀에 대해 소개해드리겠습니다!

3-1. 선형 회귀(Linear Regression)

1. 가설(Hypothesis) 수립

선형 회귀식은 아래와 같이 가정해보겠습니다.

$y=H(x)=Wx+b$

참고로 가설의 $H$를 따서 $y$를 $H(x)$라고도 표현합니다. $W$와 $b$는 각각 가중치와 편향을 뜻합니다.


2. 비용 함수(Cost function)에 대한 이해

딥러닝을 공부하면서 헷갈렸던 부분인데 비용 함수(Cost function), 손실 함수(Loss function), 오차 함수(Error function), 목적 함수(Objective function)는 모두 같은 용어를 뜻합니다. 비용 함수에 대해서 알아보기 전에 오차에 대한 개념을 먼저 알아야 합니다. 오차는 ‘실제값 - 예측값’을 뜻합니다. 오차제곱합을 수식으로 나타내면 아래와 같습니다.

$\sum_{i=1}^{n} \left[y^{(i)} - H(x^{(i)})\right]^2$

오차제곱합을 데이터의 개수인 $n$으로 나눈 값을 평균 제곱 오차(Mean Squared Error ; MSE)라고 부릅니다.

$\frac{1}{n} \sum_{i=1}^{n} \left[y^{(i)} - H(x^{(i)})\right]^2$

선형 회귀의 목적은 이러한 MSE를 최솟값으로 만드는 $W$와 $b$를 찾아내는 것입니다. 즉, 아래의 값을 최소로 만드는 초모수 값을 찾아 내는 것이 비용 함수의 목적입니다.

$cost(W, b) = \frac{1}{n} \sum_{i=1}^{n} \left[y^{(i)} - H(x^{(i)})\right]^2$


3. 최적화(Optimizer) - 경사 하강법(Gradient Descent)

그렇다면 비용 함수의 값을 최소로 만드는 초모수를 찾는 방법에 대해서 알아봅시다. 일반적으로 최적화(Optimizer) 알고리즘이 사용되는데 그 중 가장 기본적인 방법인 경사 하강법에 대해 알아보겠습니다. 예를 들어, $H(x)=Wx$에서 $W$와 비용의 관계를 그래프로 표현하면 아래와 같습니다.

img

머신러닝 모델은 가장 최소의 비용값을 찾아내기 위해 점점 접선의 기울기가 0이 되도록 $W$를 수정하는 것을 볼 수 있습니다. 즉, $W$ 값은 아래와 같은 수식으로 반복적으로 조정됩니다.

$W := W - α\frac{∂}{∂W}cost(W)$

여기서 $α$는 학습률(Learning Rate)을 뜻하며, $W$ 값을 변경할 때, 얼마나 크게 변경할지를 결정합니다. 너무 학습률을 크게 설정하면 접선의 기울기가 $0$이 되는 $W$를 찾는 것이 아니라 비용값을 발산시키게 됩니다. 또한, 너무 작게 설정하면 학습 속도가 느려지므로, 적정한 $α$을 찾아내야 합니다.

img


4. 파이토치로 선형 회귀 구현하기

  • (1) 기본 셋팅
import torch      # 메인 네임스페이스
import torch.nn as nn     # 신경망을 구축하기 위한 다양한 데이터 구조나 레이어
import torch.nn.functional as F     # 함수
import torch.optim as optim     # 파라미터 최적화 알고리즘 구현

torch.manual_seed(1)      # 랜덤 시드 설정
  • (2) 변수 선언

X_train과 y_train 모두 (3, 1) 텐서인 것 확인

X_train = torch.FloatTensor([[1], [2], [3]])      # X_train 데이터 생성
y_train = torch.FloatTensor([[4], [5], [6]])      # y_train 데이터 생성

print(X_train)
print('---------------')
print(X_train.shape)
print('---------------')
print(y_train)
print('---------------')
print(y_train.shape)
--------------------------------------------------------------------------------------------------------------------------------
tensor([[1.],
        [2.],
        [3.]])
---------------
torch.Size([3, 1])
---------------
tensor([[4.],
        [5.],
        [6.]])
---------------
torch.Size([3, 1])
  • (3) 가중치와 편향의 초기화

선형 회귀의 목적은 비용 함수를 최소로 만드는 $W, b$를 찾는 것입니다. 우선은 $W, b$를 $0$으로 초기화합니다. 그리고 requires_grad = True로 인자를 줍니다. requires_grad = True는 학습을 통해 값이 변경되는 변수임을 뜻하며, 앞으로 이 텐서에 대한 기울기를 저장한다는 것을 의미합니다. 또한, 자동미분기능이 설정되어 계산그래프가 생성되며, backward()를 사용할 때, 그래프로부터 자동으로 미분이 계산됩니다.

W = torch.zeros(1, requires_grad = True)      # 가중치 W를 0으로 초기화하고 학습을 통해 값이 변경되는 변수임을 명시함.
b = torch.zeros(1, requires_grad = True)      # 가중치 b를 0으로 초기화하고 학습을 통해 값이 변경되는 변수임을 명시함.

print('W :', W)     # 가중치 W 출력
print('b :', b)     # 가중치 b 출력
--------------------------------------------------------------------------------------------------------------------------------
W : tensor([0.], requires_grad=True)
b : tensor([0.], requires_grad=True)

즉, 현재 비용함수는 아래와 같습니다.

$y = 0 × x + 0$

  • (4) 가설 세우기

$H(x)= Wx+b$ 에 대한 가설을 선언합니다.

hypothesis = X_train * W + b
print(hypothesis)
--------------------------------------------------------------------------------------------------------------------------------
tensor([[0.],
        [0.],
        [0.]], grad_fn=<AddBackward0>)
  • (5) 비용 함수 선언하기

선형 회귀의 비용 함수인 $MSE = cost(W, b) = \frac{1}{n} \sum_{i=1}^{n} \left[y^{(i)} - H(x^{(i)})\right]^2$를 선언합니다.

cost = torch.mean((hypothesis - y_train) ** 2)
print(cost)
--------------------------------------------------------------------------------------------------------------------------------
tensor(25.6667, grad_fn=<MeanBackward0>)
  • (6) 경사 하강법 구현하기

일반적인 경사하강법(Gradient Descent)은 하나 이상의 국소최솟값(Local Minimum)이 존재하거나 안장점(Saddle Point)이 존재할 때, 비용함수를 최소로 만드는 초모수 값을 못 찾을 가능성이 있습니다. 따라서, 이에 대한 대안으로 확률적 경사 하강법인 SGD(Stochastic Gradient Descent)를 사용합니다.

조금 더 자세히 설명드리겠습니다. 손실함수 $H(x)$에 대하여 $h_{i}$를 $i$번째 표본에 대한 손실이라고 할 때, $n$개의 데이터에 대한 총 손실은 $H(x) = \sum_{i=1}^{n} h_{i}$가 됩니다. SGD는 총 손실에 대한 최적화가 아닌 ${h_{1}, h_{2}, \cdots ,h_{n}}$을 차례로 하나씩 최소화하여 궁극적으로 전체 손실함수 $H(x)$를 최소화합니다. 이 방법은 표본의 임의성을 이용하여 최적화 시, 국소최솟값이나 안정점에 빠지는 위험을 줄여줍니다. 다만, 수렴방향이 너무 임의로 움직이는 현상인 잡음(Noise)이 발생할 수 있습니다. 따라서, 하나하나의 표본으로 최적화하지 않고, 전체 표본 $n$을 $k$개의 미니 배치로 나눠서 진행합니다. 전체 표본에 대해 한 바퀴를 돌아 SGD를 수행하고 모수의 최신화가 이루어지면 $1$ 에폭(epoch)이 완성되었다고 합니다.

그렇다면 이제 SGD 경사하강법 코드를 작성해보겠습니다. lr은 학습률을 의미합니다.

optimizer = optim.SGD([W, b], lr = 0.01) # lr : 학습률(learning rate)

optimizer가 만들어졌으면 아래와 같은 절차가 필요합니다.

① 미분을 통해 얻은 기울기(gradient)를 $0$으로 초기화

optimizer.zero_grad()를 통해 이루어지며, 기울기를 초기화해야지만 새로운 가중치 편향에 대해서 새로운 기울기를 구할 수 있습니다.

② 비용 함수를 미분하여 가중치 $W$와 편향 $b$에 대한 새로운 기울기(gradient)를 계산

cost.backward()를 통해 이루어집니다.

③ 새롭게 구해진 기울기(gradient)를 통해 가중치 $W$와 편향 $b$을 업데이트

optimizer.step()를 통해 이루어지며, 각각 가중치 $W$와 편향 $b$에서 리턴되는 변수들의 기울기에 학습률($0.01$)을 곱한 값을 빼줌으로써 업데이트합니다.

# gradient(미분을 통해 얻은 기울기)를 0으로 초기화 -> 기울기를 초기화해야지만 새로운 가중치 편향에 대해서 새로운 기울기 구할 수 있음
optimizer.zero_grad()
# 비용 함수를 미분하여 가중치 W와 편향 b에 대한 기울기 계산
cost.backward()
# W와 b에서 리턴되는 변수들의 기울기에 학습률 0.01 곱한 값을 빼줌으로써 W, b를 업데이트 -> W := W - (양수 기울기) * 0.01
optimizer.step()
  • (7) 전체 코드

따라서 전체 코드는 아래와 같이 만들어집니다.

ⓐ 모델 초기화 : 가중치와 편향을 $0$으로 초기값 생성

ⓑ optimizer 설정

ⓒ epoch을 정하여 원하는 만큼 경사 하강법을 반복

​ – $H(x)$ 계산

​ – $cost$ 계산

​ – $cost$로 $H(x)$ 개선 : 기울기 $0$으로 초기화, 새로운 기울기 계산, 가중치 $W$와 편향 $b$ 업데이트

# 데이터
x_train = torch.FloatTensor([[1], [2], [3]])
y_train = torch.FloatTensor([[4], [7], [10]])

# 모델 초기화 : 가중치와 편향을 0으로 초기값 생성
W = torch.zeros(1, requires_grad = True)
b = torch.zeros(1, requires_grad = True)

# optimizer 설정
optimizer = optim.SGD([W, b], lr = 0.01)

# 원하는만큼 경사 하강법을 반복
# epoch -> 전체 훈련 데이터가 학습에 한번 사용된 주기
nb_epochs = 1999
for epoch in range(nb_epochs + 1) :

  # H(x) 계산
  hypothesis = x_train * W + b

  # cost 계산
  cost = torch.mean((hypothesis - y_train) ** 2)

  # cost로 H(x) 개선 : 기울기 0으로 초기화, 새로운 기울기 계산, 가중치 $W$와 편향 $b$ 업데이트
  optimizer.zero_grad()
  cost.backward()
  optimizer.step()

  # 100번마다 로그 출력 -> d : 정수 자리수, f : 소수 자리수
  # W.item(), b.item(), cost.item() -> 계속 갱신되는 값
  if epoch % 100 == 0 :
    print('Epoch {:4d}/{} W: {:.3f} Cost: {:.6f}'.format(epoch, nb_epochs, W.item(), b.item(), cost.item()))

# 거의 y = 3x + 1 에 가까움

코드 진행에 따라 가중치 $W$와 편향 $b$는 훈련 데이터와 잘 맞는 직선인 $y = 3x + 1$에 가까워지는 것을 볼 수 있습니다.

Epoch    0/1999 W: 0.320 Cost: 0.140000
Epoch  100/1999 W: 2.908 Cost: 1.210055
Epoch  200/1999 W: 2.927 Cost: 1.165129
Epoch  300/1999 W: 2.943 Cost: 1.129806
Epoch  400/1999 W: 2.955 Cost: 1.102039
Epoch  500/1999 W: 2.965 Cost: 1.080213
Epoch  600/1999 W: 2.972 Cost: 1.063054
Epoch  700/1999 W: 2.978 Cost: 1.049567
Epoch  800/1999 W: 2.983 Cost: 1.038964
Epoch  900/1999 W: 2.987 Cost: 1.030629
Epoch 1000/1999 W: 2.989 Cost: 1.024077
Epoch 1100/1999 W: 2.992 Cost: 1.018928
Epoch 1200/1999 W: 2.993 Cost: 1.014879
Epoch 1300/1999 W: 2.995 Cost: 1.011697
Epoch 1400/1999 W: 2.996 Cost: 1.009194
Epoch 1500/1999 W: 2.997 Cost: 1.007228
Epoch 1600/1999 W: 2.998 Cost: 1.005682
Epoch 1700/1999 W: 2.998 Cost: 1.004467
Epoch 1800/1999 W: 2.998 Cost: 1.003512
Epoch 1900/1999 W: 2.999 Cost: 1.002762


5. optimizer.zero_grad()가 필요한 이유

파이토치는 미분을 통해 얻은 기울기를 이전에 계산된 기울기 값에 더하기로 누적시키는 특징이 있습니다.

예를 들어 아래와 같이 초기값이 $2$인 $W$와 $Z = 3 \times W + 1$ 관계인 $Z$가 있습니다. z.backward()를 이용하여 미분값을 구했을 때, w.grad()를 보면 계속하여 기울기인 $3$이 누적하여 더해지는 것을 볼 수 있습니다. 따라서 초기화를 해주는 함수인 optimizer.zero_grad()가 필요합니다.

import torch
w = torch.tensor(2.0, requires_grad = True)

nb_epochs = 10
for epoch in range(nb_epochs + 1) :

  z = 3 * w + 1

  z.backward()
  print('수식을 W로 미분한 값 : {}, w : {}, z : {}'.format(w.grad, w.item(), z.item()))
--------------------------------------------------------------------------------------------------------------------------------
수식을 W로 미분한  : 3.0, w : 2.0, z : 7.0
수식을 W로 미분한  : 6.0, w : 2.0, z : 7.0
수식을 W로 미분한  : 9.0, w : 2.0, z : 7.0
수식을 W로 미분한  : 12.0, w : 2.0, z : 7.0
수식을 W로 미분한  : 15.0, w : 2.0, z : 7.0
수식을 W로 미분한  : 18.0, w : 2.0, z : 7.0
수식을 W로 미분한  : 21.0, w : 2.0, z : 7.0
수식을 W로 미분한  : 24.0, w : 2.0, z : 7.0
수식을 W로 미분한  : 27.0, w : 2.0, z : 7.0
수식을 W로 미분한  : 30.0, w : 2.0, z : 7.0
수식을 W로 미분한  : 33.0, w : 2.0, z : 7.0


Reference

  1. Pytorch로 시작하는 딥러닝 입문


DATA_100%_LOGO_LIGHT



댓글남기기