컴퓨터비전(Computer Vision)

파이토치(Pytorch)[4] - 선형 회귀(Linear Regression)

zzoming 2023. 10. 27. 03:37

훈련 데이터셋과 테스트 데이터 셋

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

어떤 학생이 1 , 2 , 3 시간 공부했을 때 각각 2 , 4 , 6 점을 받았다. 그렇다면 4시간을 공부한다면 몇점을 받을 수 있을까?

점수를 예측하기 위해서는 앞서 나온 정보를 이용해야 한다. 여기서 예측을 위해 사용하는 데이터를 훈련 데이터셋 , 학습이 끝난 후 모델이 얼마나 잘 작동하는지 판별하는 데이터 셋을 테스트 데이터셋이라고 한다. 

 

모델을 학습시키기 위해서 데이터는 파이토치의 텐서의 형태를 가지고 있어야 한다. 

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

가설 수립 

선형회귀의 가설(직선의 방정식)은 아래와 같은 형식을 가진다.

x와 곱해지는 W를 가중치라고 하며, b를 편향(bias)라고 한다.

 

비용함수에 대한 이해 

 

비용함수 = 손실함수 = 오차함수  모두 같은 용어라고 생각하면 된다 .

비용이 최소 되는 방향 또는 오차를 최소화 되는 방향

 

왼쪽 그림 처럼 4개의 훈련 데이터가 있고 이를 2차원 그래프에 4개의 점으로 표현한 상태라고 하자. 지금의 목표는 4개의 점을 가장 잘 표현하는 직선을 그리는 일이다.

 

오른쪽 그림은 임의로 그려진 주황색 선(y = 13x + 1) 에 대해서 각 실제값과 직선의 예측값에 대한 값의 차이를 화살표로 표현한 것이다. 각 실제값과 각 예측값의 차이고, 이를 각 실제값에서 오차라고 말할 수 있다.  그렇다면 총 오차를 구해보자 

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

이 결과로 오차를 구하게 되는 경우 덧셈과정에서 오차값이 + , - 를 반복하므로 제대로 된 오차의 크기를 측정할 수 없다.

그래서 오차를 전부 더하는 것이 아니라, 각 오차를 제곱해준 뒤에 전부 더한다 

오차의 제곱합에 데이터 개수(n)을 나누면 오차의 제곱합에 대한 평균을 구할 수 있다. 우리는 이를 평균제곱오차 (MSE)라고 한다. 위 문제에서 평균제곱오차는 52.5이

 

평균 제곱 오차는 회귀문제에서 적절한 W와 b를 찾기 위해서 최적화된 식이다. 평균 제곱 오차 값을 최소값으로 만드는 W와 b를 찾아내는 것이 가장 훈련 데이터를 잘 반영한 직선을 찾아내는 일이기 때문이다. 

 

평균 제곱 오차를 W와 b에 의한 비용함수로 재정의 해보면 다음과 같다 


옵티마이저 - 경사하강법 

 

앞서 정의한 비용함수의 값을 최소로 하는 W와 b를 찾는 방법을 학습할 것이다. 이때 사용되는 것이 옵티마이저(Optimizer) 알고리즘이다. 최적화 알고리즘이라고도 부른다. 옵티마이저 알고리즘을 통해 적절한 W와 b를 찾아내는 과정을 학습(training)이라고 한다. 

cost가 최소화 되는 지점은 접선의 기울기가 0이 되는 지점이며 또한 미분값이 0이 되는 지점이다. 경사하강법의 아이디어는 비용함수를 미분하여 현재 W에서의 접선의 기울기를 구하고, 접선의 기울기가 낮은 방향으로 W의 값을 변경하는 작업을 반복하는 것이다. 

 

선형회귀에서 가장 적합한 비용 함수를 평균제곱오차, 옵티마이저는 경사하강법이다. 

 

🤔경사하강법과 관련한 자세한 내용은 저번 포스팅을 참고하자 

https://zzoming00.tistory.com/38

 

파이토치(Pytorch)[2] - 경사하강법

경사하강법 경사하강법은 미분가능한 복잡한 함수가있을 때 해당 함수의 최소점을 찾기 위한 방법이다. 기본 개념은 함수의 기울기(경사)를 구하고 경사의 반대 방향으로 계속 이동시켜 극값에

zzoming00.tistory.com


가설, 비용 함수를 직접 정의해서 선형회귀모델 구현

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
#파이썬 코드를 재실행해도 같은 결과가 나오도록 랜덤 시드 부여 
torch.manual_seed(1)
x_train = torch.FloatTensor([[1],[2],[3]])
y_train = torch.FloatTensor([[2],[4],[6]])

print(x_train)
print(x_train.shape)
print(y_train)
print(y_train.shape)

#실행결과
tensor([[1.],
        [2.],
        [3.]])
torch.Size([3, 1])
tensor([[2.],
        [4.],
        [6.]])
torch.Size([3, 1])

가중치와 편향의 초기화 

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

#실행결과
tensor([0.], requires_grad=True)

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

#실행결과
tensor([0.], requires_grad=True)

가중치 w 와 편향 b를 0으로 초기화 하고, requires_grad = True가 인자로 주어진 것을 확인할 수 있다. 

 

현 직선의 방정식은 다음과 같다 

가설 세우고 비용함수 선언하기 

h = x_train * w + b 
print(h)

#실행결과
tensor([[0.],
        [0.],
        [0.]], grad_fn=<AddBackward0>)
        
cost = torch.mean((h-y_train)**2)
print(cost)

#실행결과
tensor(18.6667, grad_fn=<MeanBackward0>)

경사 하강법 구현하기 

optimizer = optim.SGD([w,b], lr = 0.01)

# gradient를 0으로 초기화
optimizer.zero_grad() 
# 비용 함수를 미분하여 gradient 계산
cost.backward() 
# W와 b를 업데이트
optimizer.step()

optimizer.zero_grad()을 실행함으로써 미분을 통해 얻은 기울기를 0으로 초기화한다. 기울기를 초기화 해야만 새로운 가중치 편향에 대해서 새로운 기울기를 구할 수 있다. cost.backward()를 호출하면 가중치와 편향에 대한 기울기가 계산된다. 

그 다음 경사 하강법 최적화 함수 optimizer의 step() 함수를 호출하여 인수로 들어갔던 w와 b에서 리턴되는 변수들의 기울기에 학습률을 곱하여 빼줌으로써 업데이트 한다 

epochs = 2000

for epoch in range(epochs+1) :

    h = x_train * w + b 

    cost= torch.mean((h-y_train) ** 2)

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

    # 100번마다 로그 출력
    if epoch % 100 == 0:
        print('Epoch {:4d}/{} W: {:.3f}, b: {:.3f} Cost: {:.6f}'.format(
            epoch, epochs, w.item(), b.item(), cost.item()
        ))
        
        
#실행결과 
Epoch  0/2000 W: 0.353, b: 0.151 Cost: 14.770963
Epoch  100/2000 W: 1.746, b: 0.577 Cost: 0.047939
Epoch  200/2000 W: 1.801, b: 0.453 Cost: 0.029624
Epoch  300/2000 W: 1.843, b: 0.356 Cost: 0.018306
Epoch  400/2000 W: 1.877, b: 0.280 Cost: 0.011312
Epoch  500/2000 W: 1.903, b: 0.220 Cost: 0.006990
Epoch  600/2000 W: 1.924, b: 0.173 Cost: 0.004319
Epoch  700/2000 W: 1.940, b: 0.136 Cost: 0.002669
Epoch  800/2000 W: 1.953, b: 0.107 Cost: 0.001649
Epoch  900/2000 W: 1.963, b: 0.084 Cost: 0.001019
Epoch 1000/2000 W: 1.971, b: 0.066 Cost: 0.000630
Epoch 1100/2000 W: 1.977, b: 0.052 Cost: 0.000389
Epoch 1200/2000 W: 1.982, b: 0.041 Cost: 0.000240
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

최종 훈련 결과를 보면 최적의 기울기는 2에 가깝고 b는 0에 가까움을 알 수 있다.  실제 정답인 y = 2x 에 근접한 셈이다. 


파이토치에서 이미 구현되어져 제공되고 있는 함수들을 불러오는 것으로 더 쉽게 선형 모델을 구현할 수 있다.

  • 선형회귀모델: nn.Linear() 
  • 평균제곱오차: nn.functional.mse_loss()
# 데이터
x_train = torch.FloatTensor([[1], [2], [3]])
y_train = torch.FloatTensor([[2], [4], [6]])

모델을 선언 및 초기화. 단순 선형 회귀 이므로 input_dim = 1 , output_dim  = 1 

model = nn.Linear(1,1)
print(list(model.parameters()))

#실행결과
[Parameter containing:
tensor([[0.5153]], requires_grad=True), Parameter containing:
tensor([-0.4414], requires_grad=True)]

첫번째 값이 W이고, 두번째 값이 b에 해당된다. 두 값 모두 현재는 랜덤 초기화 되어 있다. 그리고 두 값 모두 학습의 대상이므로 requires_grad = True 가 되어져 있다. 

# optimizer 설정. 경사 하강법 SGD를 사용하고 learning rate를 의미하는 lr은 0.01
optimizer = torch.optim.SGD(model.parameters() , lr = 0.01)
n = 2000
for epoch in range(n+1) :
	
    prediction = model(x_train) 
    
    #평균제곱오차함수
    cost = F.mse_loss(predcition , y_train) 
    
    #gradient를 0으로 초기화
    optimizer.zero_grad()
    #비용함수를 미분하여 gradient 계산
    cost.backward()
    #W와b를 업데이트
    optimizer.step() 
    
    if epoch % 100 == 0:
    # 100번마다 로그 출력
      print('Epoch {:4d}/{} Cost: {:.6f}'.format(
          epoch, nb_epochs, cost.item()
      ))
      
#실행결과
Epoch    0/2000 Cost: 13.103541
Epoch  100/2000 Cost: 0.002791
Epoch  200/2000 Cost: 0.001724
Epoch  300/2000 Cost: 0.001066
Epoch  400/2000 Cost: 0.000658
Epoch  500/2000 Cost: 0.000407
Epoch  600/2000 Cost: 0.000251
Epoch  700/2000 Cost: 0.000155
Epoch  800/2000 Cost: 0.000096
Epoch  900/2000 Cost: 0.000059
Epoch 1000/2000 Cost: 0.000037
Epoch 1100/2000 Cost: 0.000023
Epoch 1200/2000 Cost: 0.000014
Epoch 1300/2000 Cost: 0.000009
Epoch 1400/2000 Cost: 0.000005
Epoch 1500/2000 Cost: 0.000003
Epoch 1600/2000 Cost: 0.000002
Epoch 1700/2000 Cost: 0.000001
Epoch 1800/2000 Cost: 0.000001
Epoch 1900/2000 Cost: 0.000000
Epoch 2000/2000 Cost: 0.000000

이제 학습 후 W와 b값을 출력해보자 

print(list(model.parameters()))

#실행결과
[Parameter containing:
tensor([[1.9994]], requires_grad=True), Parameter containing:
tensor([0.0014], requires_grad=True)]

W의 값이 2에 가깝고, b의 값이 0에 가까운 것을 볼 수 있습니다.

  • H(x) 식에 입력 x로 부터 예측된 y를 얻는 것은 forward 연산이다
  • 학습과정에서 비용함수를 미분하여 기울기를 구하는 것은 backward 연산이다

모델을 클래스로 구현하기 

파이토치의 대부분의 구현체들을 대부분 모델을 생성할 때 클래스를 사용한다. 

class LinearRegressionModel(nn.Module) :
    def __init__(self) :
        super().__init__()
        self.linear =nn.linear(1,1) 

    def forward(self ,x ) :
        return self.linear(x)
  • 클래스 형태의 모델을 nn.Module을 상속받는다.
  • __init__() 에서 모델의 구조와 동작을 정의하는 생성자를 정의한다.
  • super() 함수는 현재 클래스가 상속받은 부모 클래스의 초기설정을 가져와서 실행한다. 이렇게 함으로써 nn.Module에서 제공하는 유용한 도구와 설정을 현재 클래스에서도 사용할 수 있다.
  • forward() 함수는 모델이 학습 데이터를 입력받아서 forward 연산을 진행시키는 함수이다. 이 forward() 함수는 model 객체를 데이터와 함께 호출하면 자동으로 실행된다. 

🤔클래스에 대한 자세한 내용은 저번에 포스팅한 글을 참고하자

https://zzoming00.tistory.com/40

 

파이썬에서의 클래스

클래스(Class) 클래스(class)란 똑같은 무언가를 계속 만들어 낼 수 있는 설계 도면(과자, 틀) , 객체(object)란 클래스로 만든 피조물(과자 틀로 찍어낸 과자)를 뜻한다. 클래스로 만든 객체는 객체마

zzoming00.tistory.com

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

torch.manual_seed(1) 

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

class LinearRegressionModel(nn.Module) :
    def __init__(self) :
        super().__init__()
        self.linear =nn.Linear(1,1) 

    def forward(self ,x ) :
        return self.linear(x)

model = LinearRegressionModel() 
optimizer = torch.optim.SGD(model.parameters(), lr = 0.01) 

# 전체 훈련 데이터에 대해 경사 하강법을 2,000회 반복
nb_epochs = 2000
for epoch in range(nb_epochs+1):

    # H(x) 계산
    prediction = model(x_train)

    # cost 계산
    cost = F.mse_loss(prediction, y_train) # <== 파이토치에서 제공하는 평균 제곱 오차 함수

    # cost로 H(x) 개선하는 부분
    # gradient를 0으로 초기화
    optimizer.zero_grad()
    # 비용 함수를 미분하여 gradient 계산
    cost.backward() # backward 연산
    # W와 b를 업데이트
    optimizer.step()

    if epoch % 100 == 0:
    # 100번마다 로그 출력
      print('Epoch {:4d}/{} Cost: {:.6f}'.format(
          epoch, nb_epochs, cost.item()
      ))