또르르's 개발 Story

[12-2] Optimizers using PyTorch 본문

부스트캠프 AI 테크 U stage/실습

[12-2] Optimizers using PyTorch

또르르21 2021. 2. 3. 01:12

1️⃣ 설정

 

matplotlib 3.3.0을 설치합니다.

!pip install matplotlib==3.3.0

필요한 모듈을 import 합니다.

import numpy as np

import matplotlib.pyplot as plt

import torch

import torch.nn as nn

import torch.optim as optim

import torch.nn.functional as F

%matplotlib inline

%config InlineBackend.figure_format='retina'

print ("PyTorch version:[%s]."%(torch.__version__))

device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

print ("device:[%s]."%(device))

 

2️⃣ Dataset

 

데이터(n_data)는 10000개로 설정하고 data를 생성합니다.

n_data = 10000

x_numpy = -3+6*np.random.rand(n_data,1)   # uniform 분포 10000 * 1로 만듦

y_numpy = np.exp(-(x_numpy**2))*np.cos(10*x_numpy) + 3e-2*np.random.randn(n_data,1)

plt.figure(figsize=(8,5))   # 가로 8인치, 세로 5인치

plt.plot(x_numpy,y_numpy,'r.',ms=2)   # ms는 markersize(마커크기)

plt.show()

x_torch = torch.Tensor(x_numpy).to(device)    # tensor로 변경

y_torch = torch.Tensor(y_numpy).to(device)

print ("Done.")

 

3️⃣ Optimizer model

 

1) MLP model

 

기본적인 MLP 모델입니다. 다만 layer들을 하나씩 만들어서 self.layers list를 추가한 후, concat 하는 방식을 사용했습니다. 이렇게 사용하는 이유는 concat 할 때 layer이름을 정해서 넣기 위해서이며, 가독성이 올라갑니다.

class Model(nn.Module):

    def __init__(self,name='mlp',xdim=1,hdims=[16,16],ydim=1):
    
        super(Model, self).__init__()
        
        self.name = name
        
        self.xdim = xdim
        
        self.hdims = hdims
        
        self.ydim = ydim


        self.layers = []
        
        prev_hdim = self.xdim
        
        for hdim in self.hdims:
        
            self.layers.append(nn.Linear(prev_hdim, hdim, bias=True))
            
            self.layers.append(nn.Tanh())  # activation
            
            prev_hdim = hdim
            
        # Final layer (without activation)
        
        self.layers.append(nn.Linear(prev_hdim,self.ydim,bias=True))


        # Concatenate all layers 
        
        self.net = nn.Sequential()
        
        for l_idx,layer in enumerate(self.layers):
        
            layer_name = "%s_%02d"%(type(layer).__name__.lower(),l_idx)
            
            self.net.add_module(layer_name,layer)
            

        self.init_param() # initialize parameters
    
    
    def init_param(self):
    
        for m in self.modules():
        
            if isinstance(m,nn.Conv2d): # init conv
            
                nn.init.kaiming_normal_(m.weight)
                
                nn.init.zeros_(m.bias)
                
            elif isinstance(m,nn.Linear): # lnit dense
            
                nn.init.kaiming_normal_(m.weight)
                
                nn.init.zeros_(m.bias)
                
    
    def forward(self,x):
    
        return self.net(x)
        

print ("Done.")        

 

여기서 self.layers에 넣는데 Linear -> Tanh (activation)을 계속 넣어줍니다. model의 default의 경우 hdims은 [16, 16]이므로 "16 layer 1번"과 "16 layer 2번" 두 번이 돌아갑니다.

self.layers = []
        
prev_hdim = self.xdim

for hdim in self.hdims:

	self.layers.append(nn.Linear(prev_hdim, hdim, bias=True))

	self.layers.append(nn.Tanh())  # activation

	prev_hdim = hdim

# Final layer (without activation)

self.layers.append(nn.Linear(prev_hdim,self.ydim,bias=True))

Concat을 할 때는 network를 sequential 하게 하고 layer name을 넣어줍니다. 그런 후, add_module을 사용해서 layer_name과 layer를 모듈에 넣어서 concat 합니다.

# Concatenate all layers 

self.net = nn.Sequential()

for l_idx,layer in enumerate(self.layers):

     layer_name = "%s_%02d"%(type(layer).__name__.lower(),l_idx)
     
     self.net.add_module(layer_name,layer)

 

2) Optimizer 설정

 

모델은 SGD, Momentum, Adam 3가지를 사용합니다. 모델 객체를 만들고 optimizer에 parameter와 LEARNING_RATE를 넣어줍니다.

LEARNING_RATE = 1e-2

# Instantiate models

model_sgd = Model(name='mlp_sgd',xdim=1,hdims=[64,64],ydim=1).to(device)

model_momentum = Model(name='mlp_momentum',xdim=1,hdims=[64,64],ydim=1).to(device)

model_adam = Model(name='mlp_adam',xdim=1,hdims=[64,64],ydim=1).to(device)

# Optimizers

loss = nn.MSELoss()

optm_sgd = optim.SGD(model_sgd.parameters(), lr=LEARNING_RATE)

optm_momentum = optim.SGD(model_momentum.parameters(), lr=LEARNING_RATE, momentum=0.9)

optm_adam = optim.Adam(model_adam.parameters(), lr=LEARNING_RATE)

print ("Done.")

 

SGD는 optim.SGD에 parameters와 learning rate를 넣어줍니다.

optm_sgd = optim.SGD(model_sgd.parameters(), lr=LEARNING_RATE)

Momentum은 optim.SGD에 parameters와 learning rate, momentum을 추가로 넣어줍니다.

optm_momentum = optim.SGD(model_momentum.parameters(), lr=LEARNING_RATE, momentum=0.9)

Adam은 optim.Adam에 parameters와 learning rate를 넣어줍니다.

optm_adam = optim.Adam(model_adam.parameters(), lr=LEARNING_RATE)

 

3) Check Parameters

 

Parameter를 출력합니다. 다만, MLP 모델에서 layer list 방법을 사용해서 name을 넣었기 때문에 훨씬 직관적으로 알 수 있습니다.

np.set_printoptions(precision=3)    # numpy float 출력옵션 변경

n_param = 0

for p_idx,(param_name,param) in enumerate(M.named_parameters()):

    param_numpy = param.detach().cpu().numpy()    # detach() : 기존 Tensor에서 gradient 전파가 안되는 텐서 생성
    
    n_param += len(param_numpy.reshape(-1))		# 한줄로 만들어줌
    
    print ("[%d] name:[%s] shape:[%s]."%(p_idx,param_name,param_numpy.shape))
    
    print ("    val:%s"%(param_numpy.reshape(-1)[:5]))

print ("Total number of parameters:[%s]."%(format(n_param,',d')))		# 총 parameter 수



[0] name:[net.linear_00.weight] shape:[(64, 1)].
    val:[0.348 0.964 0.831 1.934 1.524]
[1] name:[net.linear_00.bias] shape:[(64,)].
    val:[0. 0. 0. 0. 0.]
[2] name:[net.linear_02.weight] shape:[(64, 64)].
    val:[ 0.031 -0.118  0.106  0.028  0.254]
[3] name:[net.linear_02.bias] shape:[(64,)].
    val:[0. 0. 0. 0. 0.]
[4] name:[net.linear_04.weight] shape:[(1, 64)].
    val:[-0.117 -0.249  0.115  0.152  0.088]
[5] name:[net.linear_04.bias] shape:[(1,)].
    val:[0.]
Total number of parameters:[4,353].

 

 

4️⃣ Train

 

MAX_ITER,BATCH_SIZE,PLOT_EVERY = 1e4,64,500


model_sgd.init_param()

model_momentum.init_param()

model_adam.init_param()


model_sgd.train()

model_momentum.train()

model_adam.train()


for it in range(int(MAX_ITER)):

    r_idx = np.random.permutation(n_data)[:BATCH_SIZE]    # permutation은 deep copy shuffle
    
    batch_x,batch_y = x_torch[r_idx],y_torch[r_idx]
    
    
    # Update with Adam
    
    y_pred_adam = model_adam.forward(batch_x)
    
    loss_adam = loss(y_pred_adam,batch_y)
    
    optm_adam.zero_grad()
    
    loss_adam.backward()
    
    optm_adam.step()
    

    # Update with Momentum
    
    y_pred_momentum = model_momentum.forward(batch_x)
    
    loss_momentum = loss(y_pred_momentum,batch_y)
    
    optm_momentum.zero_grad()
    
    loss_momentum.backward()
    
    optm_momentum.step()
    

    # Update with SGD
    
    y_pred_sgd = model_sgd.forward(batch_x)
    
    loss_sgd = loss(y_pred_sgd,batch_y)
    
    optm_sgd.zero_grad()
    
    loss_sgd.backward()
    
    optm_sgd.step()
    
    

    # Plot
    
    if ((it%PLOT_EVERY)==0) or (it==0) or (it==(MAX_ITER-1)):
    
        with torch.no_grad():   # no gradient update
        
            y_sgd_numpy = model_sgd.forward(x_torch).cpu().detach().numpy()
            
            y_momentum_numpy = model_momentum.forward(x_torch).cpu().detach().numpy()
            
            y_adam_numpy = model_adam.forward(x_torch).cpu().detach().numpy()
            
            
            plt.figure(figsize=(8,4))
            
            plt.plot(x_numpy,y_numpy,'r.',ms=4,label='GT')
            
            plt.plot(x_numpy,y_sgd_numpy,'g.',ms=2,label='SGD')
            
            plt.plot(x_numpy,y_momentum_numpy,'b.',ms=2,label='Momentum')
            
            plt.plot(x_numpy,y_adam_numpy,'k.',ms=2,label='ADAM')
            
            plt.title("[%d/%d]"%(it,MAX_ITER),fontsize=15)
            
            plt.legend(labelcolor='linecolor',loc='upper right',fontsize=15)
            
            plt.show()
            

print ("Done.")

                                      (생략)

                                      (생략)


✅ sgd는 왜 못 맞췄을까?

위의 결과를 보면 SGD는 정답을 아직 못 맞힌 것을 볼 수 있습니다.

그렇다면 다른 Optimizer는 어떻게 정답에 빠르게 도달했을까요?

 

Momentum은 이전의 그레디언트를 활용에서 다음에 사용하겠다는 개념입니다.
Mini-batch를 사용했기 때문에 이전의 얻어진 그레이언트 정보를 현재에 반영하면서 데이터를 더 많이 반영하는 효과를 받게 됩니다.

Adam은 답이라고 생각한 파라미터에서는 확 늘리고, 답이라고 생각하지 않은 파라미터에서는 확 줄이는 방법을 사용해서 정답에 빠르게 가까워집니다.
또한, loss는 square loss를 사용했기 때문에 많이 틀리는 곳을 많이 맞추게 되고, 적게 틀리는 곳은 적게 맞추게 되는 효과(제곱이기 때문에)로 빠르게 target에 가까워질 수 있습니다.


 

r_idx는 permutation을 통해 10000 이하의 데이터를 랜덤으로 batch_size만큼의 array로 생성합니다.

이후, x_torch에 "r_idx array 위치"에 해당하는 값을 가지고 옵니다. (y_torch도 마찬가지입니다.)

r_idx = np.random.permutation(n_data)[:BATCH_SIZE]    # permutation은 deep copy shuffle
    
batch_x,batch_y = x_torch[r_idx],y_torch[r_idx]

Optimizer를 forward 후 loss를 구하고 update 합니다.

# Update with Adam

y_pred_adam = model_adam.forward(batch_x)

loss_adam = loss(y_pred_adam,batch_y)

optm_adam.zero_grad()

loss_adam.backward()

optm_adam.step()

model에서 forward 해서 나온 값을 복사해서 numpy 형태로 변경합니다.

(detach() : 기존 Tensor에서 gradient 전파가 안 되는 텐서 생성)

y_sgd_numpy = model_sgd.forward(x_torch).cpu().detach().numpy()

y_momentum_numpy = model_momentum.forward(x_torch).cpu().detach().numpy()

y_adam_numpy = model_adam.forward(x_torch).cpu().detach().numpy()

plt로 출력합니다.

plt.figure(figsize=(8,4))

plt.plot(x_numpy,y_numpy,'r.',ms=4,label='GT')

plt.plot(x_numpy,y_sgd_numpy,'g.',ms=2,label='SGD')

plt.plot(x_numpy,y_momentum_numpy,'b.',ms=2,label='Momentum')

plt.plot(x_numpy,y_adam_numpy,'k.',ms=2,label='ADAM')

plt.title("[%d/%d]"%(it,MAX_ITER),fontsize=15)

plt.legend(labelcolor='linecolor',loc='upper right',fontsize=15)

plt.show()

 

Comments