수리통계 분석 코딩 실습

[torch] Loss 함수를 이용한 학습 모델링 본문

대학원/통계프로그래밍

[torch] Loss 함수를 이용한 학습 모델링

얼려먹는 요구르트 2023. 10. 18. 00:43

1. 목적함수란?

data(X,Y)가 주어졌을 때, ✔ 최적화하고싶은 함수를 정한 뒤, 추정하고 싶은 값을 구하는 근사치(using X)를 분류해 알아둬야한다. 

 

❗ 다시말해, 최적화하고 싶은 함수는 추정치인($Y_{hat}$)과 $Y$값의 차이를 계산할 수 있는 방법(mse, rmse, 유클리디안거리 등)이 되는 것이고 추정하고 싶은 값($Y_{hat}$)은 $X$를 이용해 만드는 우리의 모델이 되는 것이다. 

 

즉, $loss$에 사용되는 것이 최적화하고 싶은 함수(opitm function) 가 되는 것이고 이 loss의 미분을 계산해서 $Y_{hat}$의 weight(linear regression에선 coeff)를 update 시켜주는 것이다. 

 

이해하기 쉽게 데이터를 만들어 구성해 아래 Y의 추정치가 의미하는 바를 이해하고, 최적화하고 싶은 함수를 왜 정의하는 지를 알아보자. 결론적으로 $Y_{hat}$이 어떻게 모델을 사용해 $Y$에 근접해지도록 코드가 구성되는 지를 파악하자. 

 

[ 데이터를 이용한 최적함수 이해 ]

우리가 추정하고자 하는 $\hat{y} $이 데이터 $x$를 이용해 $\hat{y}=wx+b$와 같이 구성되어 있다고 하자.

이때, 실제 비교하는 $y = w*x + b +\epsilon$ 이라고 하자. 

 

그럼 우린 $\hat{y}$를 계산하기 위해서 초기값 w,b(i.e $w_0, b_0$)를 주고 데이터 $x$ 값을 이용해 계산할 것이다.

이를 실제 값 $y$와 비교해 어느정도 차이가 나는지 비교해볼 것이다. (그래프를 이용하든..)

 

🎈 파이썬 코드

[1] data generation:

$b=-1,w=2$
$\hat{y}=xw+b$

import torch
torch.manual_seed(2021)
X = torch.arange(-3,3, 0.3).view(-1, 1)

Y = w0 * X + b0 + torch.randn(X.shape) # torch.randn(X.shape)은 random하게 주는 값 즉 eps이라고 볼수 있음

 

[2] calculate $\hat{y}$

w0 = torch.tensor(2.0) # w의 초기값은 2, b의 초기값은 -1로 줌
b0 = torch.tensor(-1.0)

yhat = w0*X+b0

 

[3] compare $\hat{y}$ & $y$

import matplotlib.pyplot as plt
plt.scatter(X.numpy(),Y.numpy())
plt.plot(X.numpy(),yhat.numpy())

우리는 $\hat{y}$는 $x$값을 이용해 추정되는 값임을 알았고 그 값은 $y$와 거의 유사한 값을 갖길 바란다. 그렇다면, 우리는 $\hat{y}$를 $y$와 근접하게 할 수 있을까? 방법은 $\hat{y} = X \cdot w + b$이므로 $w$와 $b$ 값을 update해 $yhat$을 y와 근접하게 해주면 된다!

그렇다면 어떻게 $w$와 $b$의 값을 update시켜 $\hat{y}$를 $y$에 근접하게 만들 수 있을까? 

이때 필요한 것이 최적화하고자 하는 함수 즉, $loss$함수 이다. 

 

▪ loss함수를 정의해 $\hat{y}$와 $y$의 차이를 계산한 뒤, 이 함수를 $\hat{y}$에 대해 미분하여, $w$, $b$값을 update시켜 줄 수 있다. 이는 학습률(lr)을 이용해 미분값을 곱해 기존값에서 빼줌으로써 다음 스텝의 $w$와 $b$를 update해준다. 이게 바로 모델 학습의 과정이다!!

최종적으로 모델 학습이 끝나면, $\hat{y}$와 $y$를 비교해 제대로 추정이 된건지를 판단한다.


2. $y_{hat}$이 linear regression인 모델 with 생성 데이터

$y_{hat}$이 linear regression이라는 것은 $y_{hat} = x \cdot w + b$위의 식과 같은 추정치임을 알 수 있다.

이때, $y_{hat}$의 coeff인 즉 $y_{hat}$의 값을 $y$로 근접하게 만들어 주기 위해 우리가 바꿀 수 있는 값은 $w$와 $b$일 것이다.

 

그렇다면 $w$와 $b$가 잘 만들어졌는지를 평가하는 함수, 즉, 최적화하고자 하는 함수인 $loss$를 정의해줘야할 것이다. 

이때, 이 $loss$를 우리는 $Loss(w, b)=\sum\limits_{i=1}^{n}(Y -Y_{hat}(w,b))^2$를 이용하게 되는 것이다. 

※ $Y_{hat} = x \cdot w + b $로 w와 b에 대한 함수이므로 $ Y_{hat}(w,b)$로 표현했으니 착오 없길 바란다. 

 

▪ gradient descent 

따라서, 학습 모델은 이 $ Loss(w, b)$를 각각 $w$, $b$에 대해 미분해서 기존의 $w$와 $b$값을 update하는데 학습률 lr을 곱해 기존값에서 빼는 방법을 gradient descent 방식이라고 부른다.

GD 방식

❔ 왜 학습률을 곱해 빼야할까?

이는 무작정 $ Loss(w, b)$를 미분해서 기존값에서 빼버리면 휙휙 값이 크게 빠져 $w$와 $b$를 어느정도 조정할 건지를 정한다. 이 조정되는 값을 학습률(learning rate, lr)로 부른다. 

 

이때, $w_{grad}$와 $b_{grad}$의 계산방식은 아래와 같다. 

 

X,Y가 데이터로 주어져있을 때 Loss ftn을 미분해 계산함.

 

 

[1] 데이터 생성

import torch
torch.manual_seed(2021)
X = torch.arange(-3,3, 0.3).view(-1, 1)

Y = w0 * X + b0 + torch.randn(X.shape) # torch.randn(X.shape)은 random하게 주는 값 즉 eps이라고 볼수 있음

 

[2] yhat 추정 모델 및 loss 함수 생성

 

앞서는 $\hat{y}=xw+b$로 직접 정의했으나, 우린 $w$와 $b$를 학습시켜 $Y_{hat}$을 $Y$와 유사해지도록 update시켜줄 것이므로 $Y_{hat}$을 $X$에 대한 모델로 정의한다. 

def forward(x):
      return x*w+b

w = torch.tensor(torch.randn([1,1]), requires_grad=True)
b = torch.tensor(torch.randn([1,1]), requires_grad=True)
# Loss(w,b) 함수 정의
def criterion(yhat, y):
      return torch.mean((yhat-y)**2)

 

[3] 모델 학습

▪ 학습(m1)

history=[]
lr=0.1
for epoch in range(100):
    # X에 대한 추정치
    Yhat = forward(X)
    # Yhat과 Y를 근사 시켜줄 최적화 함수 계산
    loss = criterion(Yhat,Y)
    
    # 해당 모델에 대한 w,b update를 위한 loss함수 미분
    loss.backward()
    # w,b update using Gradient descent method
    w.data = w.data-lr*w.grad.data
    b.data = b.data-lr*b.grad.data
    
    # w_grad, b_grad값이 쌓이므로 이를 제거
    w.grad = None
    b.grad = None
    
    # 모델 학습 epoch별 최적화 함수 loss 계산
    history.append(loss.item())

학습의 방식은 $Y_{hat}$이 $w$와 $b$로 이뤄진 함수여서 위와 같이 $w,b$를 lr을 활용해 update했으나, torch.optim.SGD를 이용해서도 구할 수 있다. 

즉, my_optimizer = torch.optim.SGD([w,b], lr=0.1) 를 이용해 모델의 Gradient desent 방식과 Loss(w,b)를 다시 미분하기 전 grad를 지워주는 방법을 아래와 같이 정의할 수 있다. 

my_optimizer = torch.optim.SGD([w,b], lr=0.1)

1> Gradient descent

my_optimizer.step()

which is equivalent with

w.data = w.data-lr*w.grad.data
b.data = b.data-lr*b.grad.data

2> Making zero-grad before differentiation

my_optimizer.zero_grad()

which is equivalent with

w.grad = None
b.grad = None

▪ 학습(m2)

my_optimizer = torch.optim.SGD([w,b], lr=0.1)

history=[]
lr=0.1
for epoch in range(100):
    # X에 대한 추정치
    Yhat = forward(X)
    # Yhat과 Y를 근사 시켜줄 최적화 함수 계산
    loss = criterion(Yhat,Y)
    
    # 해당 모델에 대한 w,b update를 위한 loss함수 미분
    loss.backward()
    # w,b update using Gradient descent method with optimizer
    my_optimizer.step()
    
    # w_grad, b_grad값이 쌓이므로 이를 제거
    my_optimizer.zero_grad()
    
    # 모델 학습 epoch별 최적화 함수 loss 계산
    history.append(loss.item())

 

[4] 학습 결과 확인

import matplotlib.pyplot as plt
plt.plot(history)

 

loss값이 수렴하는 것으로 보아 model(forward(X))이 잘 y로 근사함을 알 수 있음.


3. $y_{hat}$이 linear regression인 모델 with dataload

※ dataload를 이용하면, data(X,Y)를 Boston, Catseats pandas를 이용해서 생성할 수 있음. 

이때도 학습 순서는 똑같다. 다만 train시키는 X와 Y값이 dataload를 이용해 저장해주는 것뿐..

 

[1] 데이터 생성 및 torch로 dataload

# Import libraries
import torch
from torch.utils.data import Dataset, DataLoader, TensorDataset
from torch import optim

앞선 X와 Y를 이용해 dataload함. 

dataset = TensorDataset(X,Y)
trainloader=DataLoader(dataset = dataset, batch_size=5, shuffle=True)

 

[2] yhat 추정하는 model과  loss function에 대한 계산 함수 생성

# 아래 모델은 torch 내장 함수인 x @ beta는 torch.nn.Linear(1, 1, bias=False)와 같음. 
def model(x):
    return x @ beta + beta0


def criterion(yhat, y):
      return torch.mean((yhat-y)**2)

▪ beta와 beta0의 초기값 설정

beta  = torch.tensor(torch.randn([1,1]), requires_grad=True) 

beta0 = torch.tensor(torch.randn([1,1]), requires_grad=True) 

my_optimizer = torch.optim.SGD([beta, beta0], lr=0.1)

▪ 학습 with optimizer

 

trainloader란 학습 데이터가 (X,Y)로 생성했을 때 이를 쪼개서 학습시킬 수 있다. 즉, 앞서 dataload를 사용하지 않았을 땐 forward(X)로 X를 통으로 넣어 $Y_{hat}$을 계산했는데, dataload를 사용함으로써, 앞서 정의한 batch_size만큼 학습하는 데이터의 수를 줄여 (xx,yy)로 데이터를 학습시킬 수 있다.

 

이때, 계산된 학습과정을 살펴보고 싶으면, trainloader를 다 돈 뒤에 history에 저장해주어야한다. 

그러므로, trainloader안에 xx,yy가 for문을 돌고 있으면, LOSS_sum을 새롭게 정의해 

epochs=100
history=[]
n=len(X)

for epoch in range(epochs):
    LOSS_sum = 0
    for xx, yy in trainloader:
        yhat = model(xx) + beta0 # 주의해야할 것은 mode(x)는 x *beta로 이뤄져있으므로, beta0를 더해서 yhat을 추정해야한다.
        loss = criterion(yhat, yy)
        LOSS_sum = LOSS_sum + loss * len(yy)
        my_optimizer.zero_grad()
        loss.backward()
        my_optimizer.step()
    history.append(LOSS_sum.item()/n)
plt.plot(history)

model(xx) + beta0를 이용해 추정한 yhat

 

❔ 더 나은 추정방법은 무엇이 있을까? → X_design을 사용, 즉, model이 x @ beta로 정의되어 있으므로, yhat을 추정할 때 beta0를 더 해주지말고 coeff가 계산될 수 있도록 [1,1,...,1] colum을 X에 추가해 yhat 모델을 만들어 주자!

ones = torch.ones([X.shape[0],1])
XX = torch.concat([ones, X], axis=1)

dataset_xx = TensorDataset(XX,Y)
trainloader_xx=DataLoader(dataset = dataset_xx, batch_size=5, shuffle=True)
beta  = torch.tensor(torch.randn([2,1]), requires_grad=True) ### coeff항이 추가 됐으므로 shape은 [2,1]

#my_optimizer = torch.optim.SGD(model.parameters(), lr=0.1)도 아래와 같음. 
my_optimizer = torch.optim.SGD([beta], lr=0.1)

▪ 모델 학습

epochs=100
history=[]
n=len(X)

for epoch in range(epochs):
    LOSS_sum = 0
    for xx, yy in trainloader_xx:
        yhat = model(xx)
        loss = criterion(yhat, yy)
        LOSS_sum = LOSS_sum + loss * len(yy)
        my_optimizer.zero_grad()
        loss.backward()
        my_optimizer.step()
    history.append(LOSS_sum.item()/n)
plt.plot(history)

X가 design_matrix일때 학습 결과

[3] yhat 추정하는 model을 torch.nn.Linear를 이용해 생성 및  loss function에 대한 계산 함수 생성

 

앞서, yhat의 학습시키는 function 즉, model(x)를 정의해 yhat을 추정했는데 사실 이건 torch 내장 함수인 torch.nn.Linear(param_num, outlayer, bias = T/F)를 이용해 정의할 수 있다. 

 

※ bias = True는 coeff를 추가로 주는 것을 의미한다. 즉, design_matrix가 아닌 X자체로 linear reg을 할 경우 bias = True로 설정하면 자동적으로 $\beta_0$를 계산할 수 있게 된다. 

'''def model(x):
    return x @ beta + beta0
와 같은 기능을 하는 함수''' 


model = torch.nn.Linear(1,1, bias = True)
list(model.parameters())

'''
# output
[Parameter containing:
 tensor([[0.2632]], requires_grad=True),
 Parameter containing:
 tensor([0.7931], requires_grad=True)]
 
 beta, beta0 둘 다 있음을 알 수 있음.
'''

 

 

▪ 모델 학습

모델 학습을 위한 optimizer 함수 적용

my_optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
epochs=100
history=[]
n=len(X)
lr = 0.1
for epoch in range(epochs):
    LOSS_sum = 0
    for xx, yy in trainloader:
        yhat = model(xx)
        loss = criterion(yhat, yy)
        
        LOSS_sum = LOSS_sum + loss * len(yy)
        
        loss.backward()
        
        my_optimizer.step()
        my_optimizer.zero_grad()
    
    history.append(LOSS_sum.item()/n)
   
  
  plt.plot(history)