본문 바로가기

그 땐 AI했지/그 땐 DeepLearning했지

[TAVE/study] ch03 선형 회귀 | 01 선형 회귀

728x90

참고자료: https://wikidocs.net/book/2788

 

PyTorch로 시작하는 딥 러닝 입문

이 책은 딥 러닝 프레임워크 PyTorch를 사용하여 딥 러닝에 입문하는 것을 목표로 합니다. 이 책은 2019년에 작성된 책으로 비영리적 목적으로 작성되어 출판 ...

wikidocs.net

(모든 사진의 출처는 https://wikidocs.net/53560입니다.)

데이터에 대한 이해

1️⃣훈련 데이터셋과 테스트 데이터셋

더보기

💡 어떤 학생이 1시간을 공부했더니 2점, 다른 학생이 2시간을 공부했더니 4점, 또 다른 학생이 3시간을 공부했더니 6점을 맞았다. 그렇다면, 내가 4시간을 공부한다면 몇점을 맞을 수 있을까?

Hours (x) Points (y)
1 2
2 4
3 6
4 ?

👉🏻이 질문에 대답하기 위해 위의 표의 정보를 이용해야 한다. 이때 예측을 위해 사용하는 데이터를 훈련 데이터셋(training dataset)이라고 한다.

👉🏻학습이 끝난 수 얼마나 잘 작동하는지 판별하는 데이터셋을 테스트 데이터셋(test dataset)이라고 한다.

 

2️⃣훈련 데이터셋의 구성

x_train = torch.FloatTensor([[1], [2], [3]])
y_train = torch.FloatTensor([[2], [4], [6]])

👉🏻모델을 학습시키기 위해 데이터는 파이토치의 텐서의 형태를 가지고 있어야 한다. 이때 보편적으로 입력은 x, 출력은 y를 사용해 표기한다.

 

가설 수립

📌머신러닝에서 식을 세울 때 이 식을 가설이라고 한다. 선형 회귀의 가설(직선의 방정식)은 아래와 같다.

\(y = Wx + b\) 혹은 \(H(x) = Wx + b\) 이다.

  • W: 가중치
  • b: 편향

 

비용함수에 대한 이해

📌비용 함수(cost function)⭐️ = 손실 함수(loss function)⭐️ = 오차 함수(error function) = 목적 함수(objective function)

🐰비용 함수 예시를 살펴보자!

출처: https://wikidocs.net/53560

👉🏻4개의 훈련 데이터를 2차원 그래프에 점으로 표현하였다.

👉🏻4개의 점을 표현하는 직선을 그렸는데 이 때 오차(error)라는 개념에 대한 이해가 필요하다.

📌오차: 각 실제값과 각 예측값과의 차이다. 해당 그래프에서는 빨간색으로 표시된 화살표가 오차다. 총 오차를 구하는 식은 다음과 같다.

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

  • n: 데이터의 개수

✍🏻왜 (실제값 - 예측값)에 제곲을 하는걸까? 이유는 다음과 같다.

👉🏻주황색 직선은 \(y = 13x + 1\)이므로 오차는 다음 표와 같다.

hours(x) 2 3 4 5
실제값 25 50 42 61
예측값 27 40 53 66
오차 -2 10 -9 -5

✍🏻만약 예측값이 실제값보다 큰 경우 오차가 음수가 나온다. 이 때 제곱을 하지 않고 모든 오차를 더한다면 제대로 된 오차의 크기를 측정할 수 없다. 때문에 (실제값 - 예측값)에 제곱을 한 후 데이터의 개수로 나눈다. 이 식을 평균 제곱 오차라고 한다.

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

 

옵티마이저 - 경사 하강법

🐰앞선 비용 함수의 값을 최소로 하는 W와 b를 찾는 방법을 알아보자!

📌옵티마이저 알고리즘: 최적화 알고리즘으로 경사 하강법이 가장 기본적이다.

👉🏻시작 전 다음과 같은 요소를 명심하고 설명을 보도록 하자!

  • 편향 b = 0으로 설정한다
  • 가중치 W는 기울기 이므로 이제 기울기라고 언급되면 W임을 떠올리자.

  • 주황색 선의 기울기 = 20 → \(y = 20x\)
  • 초록색 선의 기울기 = 1 → \(y = x\)
  • 빨간 화살표: 오차값

👉🏻\(y = 13x\)와 비교해 기울기가 지나치게 크거나 작으면 오차가 커진다는 것을 확인할 수 있다.

 

🐰가설을 \(H(x) = Wx\)로 단순화하고 경사 하강법을 알아보자! 비용함수는 cost라고 표현하겠다.

W와 cost의 관계

👉🏻기울기 W가 무한대로 커지면 커질수록 cost의 값도 무한대로 커지고, 반대도 마찬가지이다. cost가 가장 작을 때는 맨 아래 볼록한 부분이다. 즉 기계가 할 일은 맨 아래 볼록한 부분의 W를 찾아야 한다.

👉🏻임의의 초기값 W를 정해 이를 점차 수정해간다(그래프에서 점이 점점 아래로 내려가는 모습). 초록색 선은 수정되어가는 점들의 기울기이다. W가 낮아질수록 기울기가 작아지고 결국 cost의 최솟값을 찾는 지점에 다다르면 기울기는 0이 된다. 이는 미분값 또한 0이 되는 지점이다.

📌경사 하강법: 비용함수를 미분해 현재 W에서의 접선의 기울기를 구하고, 접선의 기울기가 낮은 방향으로 W의 값을 변경하는 작업을 반복한다.

\(기울기 = \frac{\partial cost(W)}{\partial W}\)

 

✅기울기에 따라 W값이 조정되는 방법(기울기를 0으로 만드는 방법)

  • 음수일 때: W의 값이 증가
  • 양수일 때: W의 값이 감소

 \(W := W - \propto \times\)(양수의 기울기)

👉🏻위의 식이 접선의 기울기가 음수이거나, 양수일 때 모두 접선의 기울기가 0인 방향으로 W값을 조정한다.

 

학습률인 \(\propto\)는 어떤 의미인가?

👉🏻 \(\propto\)는 W의 값을 변경할 때, 얼마나 크게 변경하는지 결정한다. 혹은 얼마나 큰 폭으로 이동할지 결정한다.

✍🏻그렇다면 \(\propto\)의 값이 클 수록 기울기가 최솟값이 되는 W를 빠르게 찾을 수 있을까?

    NO! 그렇지 않다! 위의 그림을 보면 \(\propto\)의 값이 크지만 접선의 기울기가 0이 되는 곳을 찾지 못 하고 W의 값이 발산하고 있다.

    때문에 적절한 \(\propto\)을 찾는 것도 중요하다.

 

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

 

1️⃣기본 셋팅

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

# 현재 실습하고 있는 파이썬 코드를 재실행해도 다음에도 같은 결과가 나오도록 랜덤 시드(random seed)를 줍니다.
torch.manual_seed(1)

 

2️⃣변수 선언

x_train = torch.FloatTensor([[1], [2], [3]])
y_train = torch.FloatTensor([[2], [4], [6]])

print(x_train)
print(x_train.shape)

#tensor([[1.],
#        [2.],
#        [3.]])
#torch.Size([3, 1])

👉🏻훈련 데이터를 선언한다. x_train의 크기가 (3x1)이고 y_train도 마찬가지이다.

 

3️⃣가중치와 편향의 초기화

📌선형 회귀의 목표: 가장 잘 맞는 직선을 정의하는 W와 b의 값을 찾는 것이다.

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

#tensor([0.], requires_grad=True)

👉🏻가중치 W가 0으로 초기화 되었다.

✍🏻requires_grad=True: 이 변수는 학습을 통해 계속 값이 변경되는 변수임을 의미한다.

b = torch.zeros(1, requires_grad=True)
print(b)

#tensor([0.], requires_grad=True)

👉🏻편향 b도 0으로 초기화했다.

👉🏻현재 직선의 방적식은 \(y = 0x + 0\)이다.

 

4️⃣가설 세우기

hypothesis = x_train * W + b
print(hypothesis)

👉🏻\(H(x) = Wx + b\)를 다음과 같이 표현할 수 있다.

 

5️⃣비용 함수 선언하기

# 앞서 배운 torch.mean으로 평균을 구한다.
cost = torch.mean((hypothesis - y_train) ** 2) 
print(cost)

#tensor(18.6667, grad_fn=<MeanBackward1>)

👉🏻\(cost(W, b) = \frac{1}{n}\sum_{i=1}^{n}[y^{(i)} - H(x^{(i)})]^{2}\)를 다음과 같이 선언한다.

 

6️⃣경사 하강법 구현하기

optimizer = optim.SGD([W, b], lr=0.01)
  • SGD: 경사 하강법의 일종
  • lr: 학습률(learning rate)
# gradient를 0으로 초기화
optimizer.zero_grad() 
# 비용 함수를 미분하여 gradient 계산
cost.backward() 
# W와 b를 업데이트
optimizer.step()

👉🏻미분을 통해 얻은 기울기를 0으로 초기화해야만 새로운 가중치 편향에 대해서 새로운 기울기를 구할 수 있다.

👉🏻cost.backward(): 가중치 W와 편향 b에 대한 기울기가 계산된다.

👉🏻opimizer의 step(): 인수로 들어갔던 W와 b에서 리턴되는 변수들의 기울기에 학습률 0.01을 곱하여 빼줘서 업데이트한다.

 

7️⃣전체 코드✅

# 데이터
x_train = torch.FloatTensor([[1], [2], [3]])
y_train = torch.FloatTensor([[2], [4], [6]])
# 모델 초기화
W = torch.zeros(1, requires_grad=True)
b = torch.zeros(1, requires_grad=True)
# optimizer 설정
optimizer = optim.SGD([W, b], lr=0.01)

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) 개선
    optimizer.zero_grad()
    cost.backward()
    optimizer.step()

    # 100번마다 로그 출력
    if epoch % 100 == 0:
        print('Epoch {:4d}/{} W: {:.3f}, b: {:.3f} Cost: {:.6f}'.format(
            epoch, nb_epochs, W.item(), b.item(), cost.item()
        ))
Epoch    0/2000 W: 0.187, b: 0.080 Cost: 18.666666
Epoch  100/2000 W: 1.746, b: 0.578 Cost: 0.048171
Epoch  200/2000 W: 1.800, b: 0.454 Cost: 0.029767
Epoch  300/2000 W: 1.843, b: 0.357 Cost: 0.018394
Epoch  400/2000 W: 1.876, b: 0.281 Cost: 0.011366
Epoch  500/2000 W: 1.903, b: 0.221 Cost: 0.007024
Epoch  600/2000 W: 1.924, b: 0.174 Cost: 0.004340
Epoch  700/2000 W: 1.940, b: 0.136 Cost: 0.002682
Epoch  800/2000 W: 1.953, b: 0.107 Cost: 0.001657
Epoch  900/2000 W: 1.963, b: 0.084 Cost: 0.001024
Epoch 1000/2000 W: 1.971, b: 0.066 Cost: 0.000633
Epoch 1100/2000 W: 1.977, b: 0.052 Cost: 0.000391
Epoch 1200/2000 W: 1.982, b: 0.041 Cost: 0.000242
Epoch 1300/2000 W: 1.986, b: 0.032 Cost: 0.000149
Epoch 1400/2000 W: 1.989, b: 0.025 Cost: 0.000092
Epoch 1500/2000 W: 1.991, b: 0.020 Cost: 0.000057
Epoch 1600/2000 W: 1.993, b: 0.016 Cost: 0.000035
Epoch 1700/2000 W: 1.995, b: 0.012 Cost: 0.000022
Epoch 1800/2000 W: 1.996, b: 0.010 Cost: 0.000013
Epoch 1900/2000 W: 1.997, b: 0.008 Cost: 0.000008
Epoch 2000/2000 W: 1.997, b: 0.006 Cost: 0.000005

👉🏻W와 b가 훈련 데이터와 잘 맞는 직선을 표현하기 위해 적절한 값을 변화해간다. 결과를 보면 W는 2, b는 0에 가깝다.

✍🏻에포크(Epoch): 전체 훈련 데이터가 학습에 한 번 사용된 주기

 

optimizer.zero_grad()가 필요한 이유
import torch
w = torch.tensor(2.0, requires_grad=True)

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

  z = 2*w

  z.backward()
  print('수식을 w로 미분한 값 : {}'.format(w.grad))
수식을 w로 미분한 값 : 2.0
수식을 w로 미분한 값 : 4.0
수식을 w로 미분한 값 : 6.0
수식을 w로 미분한 값 : 8.0
수식을 w로 미분한 값 : 10.0
수식을 w로 미분한 값 : 12.0
수식을 w로 미분한 값 : 14.0
수식을 w로 미분한 값 : 16.0
수식을 w로 미분한 값 : 18.0
수식을 w로 미분한 값 : 20.0
수식을 w로 미분한 값 : 22.0
수식을 w로 미분한 값 : 24.0
수식을 w로 미분한 값 : 26.0
수식을 w로 미분한 값 : 28.0
수식을 w로 미분한 값 : 30.0
수식을 w로 미분한 값 : 32.0
수식을 w로 미분한 값 : 34.0
수식을 w로 미분한 값 : 36.0
수식을 w로 미분한 값 : 38.0
수식을 w로 미분한 값 : 40.0
수식을 w로 미분한 값 : 42.0

👉🏻파이토치는 미분을 통해 얻은 기울기를 이전에 계산된 기울기 값에 누적시키는 특징이 있다. 때문에 미분값을 계속 0으로 초기화시켜야 한다.

 

torch.manual_seed()를 하는 이유
import torch
torch.manual_seed(3)
print('랜덤 시드가 3일 때')
for i in range(1,3):
  print(torch.rand(1))
#랜덤 시드가 3일 때
#tensor([0.0043])
#tensor([0.1056])

👉🏻torch.manual_seed(): 난수 발생 순서와 값을 동일하게 보장한다. 이후 랜덤시드 값을 5로 변경시킨 후 다시 3으로 설정해도 똑같은 난수 0.0043, 0.1056이 나온다.

728x90