AI TECH

Autograd & Optimizer

prefer_all 2022. 9. 27. 08:50
<목차>
1. torch.nn.Module => 기본적으로 PyTorch에 구현되어있는 네트워크 텐서 모듈을 살펴보자!
    nn.Parameter : Tensor VS Parameter => nn.Parameter이 Module 내부에서 어떤 역할을 하는 지 알아보자!
    backward 작동 방식
2. AutoGrad for Linear Regression [code]
3.  AutoGrad 없이 LR[code]

 

하나의 딥러닝은 수 많은 Layer (= Block)의 반복

 

torch.nn.Module
  • Layer의 base class
  • input, output, forward, backward를 정의함 
    • backward는 autograd 이용 -> weight가 학습의 대상이 되는 데, 이를 parameter(tensor)로 정의

가장 일반적인 모듈 구성

 

nn.Parameter

- Tensor 객체의 상속 객체

- nn.Module 내에 attribute가 될 때는 required_grad=True 로 지정되어 학습 대상이 되는 Tensor

- 우리가 직접 지정할 일은 잘 없음 : 대부분의 layer에는 weights 값들이 지정되어 있음

# low level API
import torch
from torch import nn
from torch import Tensor

class MyLiner(nn.Module):
    def __init__(self, in_features, out_features, bias=True):
        super().__init__()
        self.in_features = in_features
        self.out_features = out_features
        
        self.weights = nn.Parameter(
                torch.randn(in_features, out_features))
        
        self.bias = nn.Parameter(torch.randn(out_features))

    def forward(self, x : Tensor):
        return x @ self.weights + self.bias # y hat이라고 생각

weight를 사용해서 n개의 feature를 n보다 작은 개수로

input_feature이 3*7  output_feature이 5*3 이면 weight는 7*5

x = torch.randn(5, 7)
layer = MyLiner(7, 12) # layer은 결과값
layer(x).shape
# torch.Size([5, 12])
# data가 5개이고 feature가 12개
for value in layer.parameters():
    print(value)
# 학습이 되는 paramters들의 weight값과 gradient의 대상이 되는가 출력됨
# backward propagation이 발생하면 미분이 되는 값들임
'''
Parameter containing:
tensor([[ 1.2619,  1.0068, -0.6966,  0.8812, -0.2954,  1.4128,  0.9650, -0.3889,
          0.0773, -1.1098,  2.6498,  0.3121],
        [-0.2253, -0.8406, -1.1356,  0.6612, -1.5472,  1.0067, -0.6821,  0.2058,
          0.0319, -0.1887,  0.6998, -2.1920],
        [-1.4269,  1.0507,  0.2441,  1.9487,  0.4445, -0.6500,  0.0587, -0.8002,
          0.6860, -0.1867,  0.4033, -0.8849],
        [-0.0982,  1.2807,  2.0238,  0.4867,  0.5099, -0.1735, -0.1145,  0.3688,
          0.6835, -1.3324,  0.9078, -0.2085],
        [-0.7083,  1.9173, -0.4545, -0.8862, -1.7419, -1.7519, -0.1240,  0.9257,
          0.3316,  1.6444, -0.8231,  0.4194],
        [ 1.2558,  0.4703,  0.4174, -1.0150,  0.0934, -0.7971,  2.1993,  1.4428,
          0.0990, -0.4921,  0.4155, -0.3314],
        [-0.2510,  0.0473, -0.4451,  0.7581, -0.1752, -0.1194,  1.7346, -0.1266,
         -1.9152,  1.0263,  0.0679,  0.1796]], requires_grad=True)
Parameter containing:
tensor([-1.6029, -0.8523, -0.8441, -0.1401,  0.8572, -0.3957, -3.3626, -1.2080,
        -0.2804, -1.9525, -0.6787, -0.0101], requires_grad=True)
'''

 

Parameter이 아닌 Tensor로 선언하면

class MyLiner(nn.Module):
    def __init__(self, in_features, out_features, bias=True):
        super().__init__()
        self.in_features = in_features
        self.out_features = out_features
        
        self.weights = Tensor(
                torch.randn(in_features, out_features))
        
        self.bias = Tensor(torch.randn(out_features))

    def forward(self, x : Tensor):
        return x @ self.weights + self.bias


layer = MyLiner(7, 12)
layer(x).shape #torch.Size([5, 12])
for value in layer.parameters():
    print(value)
# 아무것도 출력 안됨
# parameter은 미분의 대상이 되는 것만

 

 

Backward

- Layer에 있는 Parameter들의 미분을 수행
- Forward의 결과값 (model의 output = 예측치 = y hat)과 실제값간의 차이(loss) 에 대해 미분을 수행
- 해당 값으로 Parameter 업데이트


Autograd를 할 때 Backward라는 함수가 호출됨
for epoch in range(epochs):
… …
    #Clear gradientbuffers becausewe don'twant anygradient fromprevious epoch tocarry forward
    optimizer.zero_grad() # *이전의 gradient값이 영향을 주지 않도록
    #get output from the model, given thei nputs
    outputs = model(inputs) # *y hat 값이 나옴
    #get loss for the predicted output 
    loss = criterion(outputs,labels) # labels는 y임
    print(loss)
    #get gradients w.r.t to parameters
    loss.backward()
    # update parameters optimize
    optimizer.step()

  • optimizer.zero_grad() : 이전 step에서 각 layer 별로 계산된 gradient 값을 모두 0으로 초기화 시키는 작업입니다. 0으로 초기화 하지 않으면 이전 step의 결과에 현재 step의 gradient가 누적으로 합해져서 계산
  • loss.backward() : 각 layer의 파라미터에 대하여 back-propagation을 통해 gradient를 계산
  • optimizer.step() : 각 layer의 파라미터와 같이 저장된 gradient 값을 이용하여 파라미터를 업데이트. 이 명령어를 통해 파라미터가 업데이트되어 모델의 성능이 개선.

AutoGrad for Linear Regression 최종 코드

 

y = 2*x+1

import numpy as np
# create dummy data for training
x_values = [i for i in range(11)]
x_train = np.array(x_values, dtype=np.float32)
x_train = x_train.reshape(-1, 1)

y_values = [2*i + 1 for i in x_values]
y_train = np.array(y_values, dtype=np.float32)
y_train = y_train.reshape(-1, 1)
import torch
from torch.autograd import Variable
class LinearRegression(torch.nn.Module):
    def __init__(self, inputSize, outputSize):
        super(LinearRegression, self).__init__()
        self.linear = torch.nn.Linear(inputSize, outputSize)

    def forward(self, x):
        out = self.linear(x)
        return out
inputDim = 1        # takes variable 'x' 
outputDim = 1       # takes variable 'y'
learningRate = 0.01 
epochs = 100

model = LinearRegression(inputDim, outputDim)
##### For GPU #######
if torch.cuda.is_available():
    model.cuda()
    
criterion = torch.nn.MSELoss() 
optimizer = torch.optim.SGD(model.parameters(), lr=learningRate) # 대상이 되는 parameter
for epoch in range(epochs): 
# *모든 값을 한 번에 넣고 돌림(보통은 dataloader을 가지고 잘라서 넣음)
    # Converting inputs and labels to Variable
    if torch.cuda.is_available():
        inputs = Variable(torch.from_numpy(x_train).cuda())
        labels = Variable(torch.from_numpy(y_train).cuda())
    else:
        inputs = Variable(torch.from_numpy(x_train))
        labels = Variable(torch.from_numpy(y_train))

    # Clear gradient buffers because we don't want any gradient from previous epoch 
    # to carry forward, dont want to cummulate gradients
    optimizer.zero_grad()

    # get output from the model, given the inputs
    outputs = model(inputs)

    # get loss for the predicted output
    loss = criterion(outputs, labels)
    print(loss)
    # get gradients w.r.t to parameters
    loss.backward() # *위에 작성한 SGD로 미분

    # update parameters
    optimizer.step()

    print('epoch {}, loss {}'.format(epoch, loss.item()))
  • <순서>
    1. loss 계산
    2. loss.backward()로 gradient 계산
    3. optimizer.step()으로 weight 갱신
  • 가중치 갱신 순서 : 뉴럴네트워크의 출력값과 라벨 값을 loss 함수를 이용하여 계산을 하고 그 loss 함수의 backward() 연산을 한 뒤에 optimizer.step()을 통해 weight를 업데이트

 

  • Q. loss와 optimizer은 어떤 관계로 연결되어있어서 loss를 통해 계산한 gradient를 optimizer로 가중치 갱신을 할 수 있을까?
  • A. optimizer와 loss.backward()는 같은 model 객체를 사용함. 그리고 loss.backward()의 출력값이 각 model의 layer의 grad 멤버 변수에 저장되고 이 값을 optimizer의 입력값으로 사용함으로써 두 연산이 연결됨. 
  • ex) loss.backward()가 실행되면 gradient는 model.layer1.weight.grad에 저장된다. 그리고 optimizer 객체는 model.parameter()를 통해 생성됐기 때문에 model.layer1.weight.grad에 저장된 gradient에 바로 접근하여 사용 가능.
with torch.no_grad(): # we don't need gradients in the testing phase
    if torch.cuda.is_available():
        predicted = model(Variable(torch.from_numpy(x_train).cuda())).cpu().data.numpy()
    else:
        predicted = model(Variable(torch.from_numpy(x_train))).data.numpy()
    print(predicted)
    
for p in model.parameters():
    if p.requires_grad:
         print(p.name, p.data)
'''
None tensor([[2.0985]])
None tensor([0.3158])
'''
  • 실제 backward는 module 단계에서 직접 지정 가능하나 Autograd가 있어서 굳이 그럴 필요 없음
  • 직접 지정하려면 backward와 optimizer 오버라이딩해야함

Logistic Regulation

logistic regulation은 sigmoid의 g 대신 x, 세타, weight, x의 linear combination을 넣어준 것임

출처: 부스트캠프 AI TECH 4기

forward: y hat을 구함
backward: 미분이 일어남
optimize: w와 bias의 update가 일어남

 

autograd 없이 Logistic Regression
class LR(nn.Module):
    def __init__(self, dim, lr=torch.scalar_tensor(0.01)):
        super(LR, self).__init__()
        # intialize parameters
        self.w = torch.zeros(dim, 1, dtype=torch.float).to(device)
        self.b = torch.scalar_tensor(0).to(device)
        self.grads = {"dw": torch.zeros(dim, 1, dtype=torch.float).to(device),
                      "db": torch.scalar_tensor(0).to(device)}
        self.lr = lr.to(device)

    def forward(self, x):
        ## compute forward
        z = torch.mm(self.w.T, x) + self.b
        a = self.sigmoid(z)
        return a

    def sigmoid(self, z):
        return 1/(1 + torch.exp(-z))

    def backward(self, x, yhat, y):
        ## compute backward
        self.grads["dw"] = (1/x.shape[1]) * torch.mm(x, (yhat - y).T)
        self.grads["db"] = (1/x.shape[1]) * torch.sum(yhat - y)
    
    def optimize(self):
        ## optimization step
        self.w = self.w - self.lr * self.grads["dw"]
        self.b = self.b - self.lr * self.grads["db"]

## utility functions
def loss(yhat, y):
    m = y.size()[1]
    return -(1/m)* torch.sum(y*torch.log(yhat) + (1 - y)* torch.log(1-yhat))

def predict(yhat, y):
    y_prediction = torch.zeros(1, y.size()[1])
    for i in range(yhat.size()[1]):
        if yhat[0, i] <= 0.5:
            y_prediction[0, i] = 0
        else:
            y_prediction[0, i] = 1
    return 100 - torch.mean(torch.abs(y_prediction - y)) * 100
## model pretesting
x, y = next(iter(train_dataset))

## flatten/transform the data
x_flatten = x.T
y = y.unsqueeze(0) 

## num_px is the dimension of the images
dim = x_flatten.shape[0]

## model instance
model = LR(dim)
model.to(device)
yhat = model.forward(x_flatten.to(device))
yhat = yhat.data.cpu()

## calculate loss
cost = loss(yhat, y)
prediction = predict(yhat, y)
print("Cost: ", cost) #Cost:  tensor(0.6931)
print("Accuracy: ", prediction) #Accuracy:  tensor(50.4098)

## backpropagate
model.backward(x_flatten.to(device), yhat.to(device), y.to(device))
model.optimize()
## hyperparams
costs = []
dim = x_flatten.shape[0]
learning_rate = torch.scalar_tensor(0.0001).to(device)
num_iterations = 100
lrmodel = LR(dim, learning_rate)
lrmodel.to(device)

## transform the data
def transform_data(x, y):
    x_flatten = x.T
    y = y.unsqueeze(0) 
    return x_flatten, y 

## training the model
for i in range(num_iterations):
    x, y = next(iter(train_dataset))
    test_x, test_y = next(iter(test_dataset))
    x, y = transform_data(x, y)
    test_x, test_y = transform_data(test_x, test_y)

    # forward
    yhat = lrmodel.forward(x.to(device))
    cost = loss(yhat.data.cpu(), y)
    train_pred = predict(yhat, y)
        
    # backward
    lrmodel.backward(x.to(device), 
                    yhat.to(device), 
                    y.to(device))
    lrmodel.optimize()
    ## test
    yhat_test = lrmodel.forward(test_x.to(device))
    test_pred = predict(yhat_test, test_y)

    if i % 10 == 0:
        costs.append(cost)

    if i % 10 == 0:
        print("Cost after iteration {}: {} | Train Acc: {} | Test Acc: {}".format(i, 
                                                                                    cost, 
                                                                                    train_pred,
                                                                                    test_pred))

Further Question

  1. 1 epoch에서 이뤄지는 모델 학습 과정을 정리해보고 성능을 올리기 위해서 어떤 부분을 먼저 고려하면 좋을지 같이 논의해보세요
1 epoch에서 각 layer의 파라미터와 같이 저장된 gradient 값을 이용하여 파라미터를 업데이트함. 이 과정을 통해 모델의 성능이 개선됨.

optimizer와 hyper parameter 값을 조정하여 성능을 올린다. 

 

2. optimizer.zero_grad()를 안하면 어떤 일이 일어날지 그리고 매 batch step마다 항상 필요한지 같이 논의해보세요

이전 step에서 각 layer 별로 계산된 gradient 값을 모두 0으로 초기화하지 않으면 이전 step의 결과에 현재 step의 gradient가 누적으로 합해짐. 즉, 올바른 backward가 일어나지 않음. 

매 batch step마다 항상 필요한 과정임.