Module

DONGJIN IM·2022년 2월 20일
0
post-thumbnail

Module

딥러닝 Architecture

딥러닝은 여러 개의 Layer로 구성되어 있다. 이런 Layer를 여러 번 반복하는 것으로 딥러닝을 수행하는데, 대부분의 경우 Layer의 형태는 같거나 비슷하다.

이런 의미에서 딥러닝은 블록(1개 Layer의 연산) 반복의 연속이라고 부를 수 있다.

torch.nn.Module

딥러닝을 구성하는 Layer의 Base Class이다.
모든 신경망 모델은 nn.Module의 SubClass라고 생각할 수 있다.

Input과 Output, forward(수행할 함수)를 정의할 수 있으며, backward(Autograd) 또한 메서드를 통해 수행시킬 수 있다.

특히 학습 대상이 되는 Weight(Parameter)들을 정의하는 곳으로, Block의 핵심 라이브러리라고 할 수 있다.

torch.nn.Module에서 Override 무조건 Override해야 할 2가지 메서드가 존재한다.

1. __init__()
모델에서 활용할 module(nn.Linear 등), activation function(활성함수; ReLU, sigmoid 등)을 정의하는 곳으로, 무조건 첫 부분에 super.__init__()으로 상속 Module 접근제어자를 처리해 줘야 한다.

Weight의 초기화도 이 부분에서 시켜주며, forward() 과정에서 활용되는 모든 함수를 선언하여 나중에 활용할 수 있게 만드는 공간이라고 할 수 있다.

2. forward()
순전파 때 실행되어야 할 계산을 정의하는 곳이다.
순전파는 Model의 예측치를 구하는 과정이라는 것은 모두가 알고 있을 것이다.

입력값 x가 들어왔을 때 어떻게 하면 예측치 y값을 도출해낼지 중간 함수를 입력하는 곳으로 1번의 __init__()에서 선언했던 함수를 활용하는 곳이다.

nn.Parameter

학습 대상이 되는 Tensor로 Model Block에 존재하는 Weight 값이라는 것을 컴퓨터에 알리기 위해 활용하는 모듈이다.

Tensor 객체의 상속 객체로써, nn.Module 메서드 내에 Attribute가 될 경우 아무런 설정을 하지 않아도 학습 대상으로 지정해준다.

대부분 Layer를 직접 구현하기 보다는 구현되어 있는 함수를 가지고오는데, 미리 선언되어 있는 Block이나 Model들은 Weight 값들이 지정되어 있어 직접 지정할 일은 많이 없다.

Parameter를 직접 지정하는 방법은 Weight이 될 개겣를 선언할 때 requires_grad=True Parameter값을 주어 설정하면 된다.

코드로 보는 Block

간단하게 Linear Regression Model을 생각해보자.
X라는 데이터가 들어왔을 경우, W라는 가중치행렬과 b라는 절편벡터 값을 활용하여 XW+b를 통해 데이터를 변환한 Y값을 Output으로 받고, 이 Y값과 실제 데이터를 비교하여 오차를 줄인다.
이 Model을 구현해보도록 하자.

import torch
from torch import nn
from torch import Tensor

# nn.Module을 상속받아야지만 Model으로 형성 가능
class MyLinear(nn.Module): 
	def __init__(self, in_features, out_features, bias=True):
		"""
        in_features : 한 개 Input Data에 몇 개의 Feature가 포함되어 있는가
        out_features : Input Data를 변환한 이후 Output에는 몇 개의 
                       Features가 포함되어 있는가
         
        in_features, out_features는 고정되어 있는 것이 아니며,
        해당 모델을 선언할 때 전달해 줄 Parameter를 추가할 수 있다.
        """
        super().__init__()
        self.in_features = in_featuers
        self.out_features = out_features
        
        self.weights=nn.Parameter(torch.randn(in_features, out_features))
        self.bias = nn.Parameter(torch.randn(out_features))
        # nn.Parameter()가 있으니, weights와 bias가 학습 대상이다.
        
    def forward(self, x:Tensor):
        return x@self.weights + self.bias
    """
    어디서 많이 본 형태이지 않은가? 그렇다! XW + b!
    즉, weights는 가중치 행렬, b는 절편 벡터가 되는 것이다.
    이제 weights를 왜 torch.randn(in_features, out_features)로 지정했는지 
    알 수 있다
    
    데이터 1개는 in_features만큼의 데이터를 가지고 있으므로, Input Data는
    N * {in_features} 형태의 벡터이다.
    이를 out_features로 변환해주기 위해서는 {in_featuers} * {out_features} 
    행렬이 필요하다
    
    bias는 절편으로 N * {out_features}만큼의 행렬이 필요하겠지만, 
    {out_features}벡터로만 설정하여, Broadcasting으로 인한 연산이 일어나 
    순전파 과정이 진행되게 된다.
    잊지말자. b절편은 모든 행들의 값이 같다.
    """

위 Model 수행시켜보기

x = torch.randn(5,7) 
# x는 Input Data가 될 것이다. 데이터는 5개 & in_features = 7개

layer = MyLinear(7,12) 
# in_features 7개를 Out_features 12개 Data로 변환하는 Module
layer(x) 
# 변환 수행! -> shape : torch.Size([5,12])

for value in layer.parameters(): 
     print(value)               

# 만약 nn.Parameter()로 설정하지 않았다면, layer.parameters()는 
# 존재하지 않을 것이다
# 순전파 계산 때는 문제가 없지만, 역전파 계산을 통한 학습이 불가해짐!!!!

모듈 형성 시 주의할 점

1. __init__()에는 무조건 super().__init__() 기입
잊지 말자. 모듈은 항상 nn.Module을 상속받아 형성되며, 상속이라는 것은 상위 클래스에 대한 선언이 선행되어야 한다.

2. foward() 공간에 모듈이 수행할 메서드를 입력하고, 도출하고 싶은 값을 return을 통해 반환

3. Data를 변경시켜야 할 경우, self.X를 통해 Data 변형 메서드들을 모아놓고, forward() 메서드에 self.X를 활용해 데이터를 변경시키는 게 좋음
이 부분은 필수적이지는 않다. 하지만 대부분의 Case에서는 init 부분에서 데이터를 건드리지 않고, forward() 과정에서 데이터 변환을 수행시키는 경우가 많다.
아래에서 배우겠지만, nn.Sequential 등 여러 Module을 순차적으로 수행시킬 수 있는 메서드들이 구현되어 있고, 이런 메서드들을 활용하여 forward() 과정에서 데이터 변환을 수행하는 경우가 많다.
다른 말로 하자면, transforms.Compose()에 관한 것은 init 부분에 self.X를 통해 저장하되, forward() 메서드 내부에 self.X(data)를 통해 데이터 변환하는 것을 추천한다는 것이다.

forward()를 위한 유용한 메서드들

  • 가정 : Add() - 모듈
    • Add(x) : Input 값에 대하여 Input + x를 Return 시키는 모듈

nn.Sequential

  • nn.Sequential(Module_1, Module_2,..., Module_N)
    • Sequential에 포함된 모든 Model에 대하여 순차적으로 연산을 수행함
class MyModule(nn.Module):
    def __init__():
    	super.__init__()
        self.model = nn.Sequential(Add(3), Add(4), Add(5))

    def forward(self, x):
        t = self.model(x)
        return t

self.model에 Add(3), Add(4), Add(5) 모듈을 추가시켰다
나중에 MyModule의 연산이 수행될 때, self.model(x)로 Input Data를 Model에 통과시킨다. 이 때 위에서 말했던 것처럼 "순차적으로" 수행시킨다
즉, x -> x + 3(Add(3)이므로) -> (x + 3) + 4(Add(4)이므로) -> ((x+3)+4) + 5 (Add(5)이므로) 과정을 거쳐 결과적으로 x + 3 + 4 + 5 값을 반환하게 될 것이다

nn.MouduleList

  • nn.ModuleList([Module_1, Module_2, ..., Module_N)

저장한 Module 중 일부분만 활용하고 싶을 때 사용하는 방식이다.
Module들을 List에 저장시킨 이후, ModuleList 안에 묶어 놓으면 이후 forward 연산에서 index를 통해 특정 Module에 접근 가능하다.

class MyModule(nn.Module):
    def __init__():
        super.__init__()
        self.model = nn.ModuleList([Add(3), Add(4), Add(5)])

    def forward(self, x):
        t = self.model[1](x)
        t = self.model[0](x)
        return t

self.model[1]을 통해 ModuleList의 1번째 index에 있는 Module인
Add(4)를 불러올 수 있다. 이후 input 값인 x를 넣으면, self.model[1](x) = Add(4)(x)로 생각할 수 있을 것이다. 즉, x + 4 = t가 된다
마찬가지로, self.model[0]을 통해 Add(3)을 불러와 t + 3을 수행하고, 이 값을 반환한다
결과적으로, x -> x + 4(Add(4)) -> (x + 4) + 3(Add(3))의 연산이 수행되는 것이다

nn.MoudleDict

  • nn.ModuleDict(["1":Module_1, "2":Module_2, ..., "N" : Module_N])
    ModuleList와 비슷하지만, List가 아닌 Dict type으로 Module을 저장하기 때문에 Module을 Key값을 통해 불러와야 한다.

Hook

다른 프로그래머가 Custom 코드(Function)을 중간에 실행할 수 있도록 만들어 놓은 Interface이다.

실행로직 분석, 프로그램 추가적 기능 제공을 위해 활용된다.
(개념적으론 이해했지만, 지금까지 짠 코드에서 Hook을 사용해보진 못했다)

Hook은 여러 방법으로 분류할 수 있다.

먼저, 등록 객체에 따라 분류하면 Tensor에 등록하는 Hook & Module에 등록하는 Hook으로 분류할 수 있다.

2번째로 함수를 수행시키는 위치에 따라 분류할 수도 있는데, Backward 과정에서 수행되는 Hook & Forward 과정에서 수행되는 Hook으로 분류할 수 있다.

Forward가 수행될 때, forward() 메서드 수행 전 일어나는 Hook를 Pre-Hoook, forward() 메서드 수행 이후 일어나는 Hook를 Hook로 지정할 수 있다.

  • Tensor에 등록하는 Hook
    • Backward & Hook Function 밖에 설정할 수 없음
    • Built-in Function : tensor.register_hook({추가할 Hook Function})
    • tensor._backward_hooks를 통해 Hook Function 수행 가능
    • 등록해 놓을 시, Gradient 계산을 끝마친 이후 Tensor가 해당 Function의 입력값으로 되어 처리함
  • Module에 등록하는 Hook
    • module.register_forward_pre_hook({추가할 Hook Function})
      • Forward가 수행될 때 실행될 Pre-Hook
    • module.register_forward_hook({추가할 Hook Function})
      • Forward가 수행될 때 실행할 Hook
      • Hook Function의 Parameter는 def forward()가 시작할 때 입력된 Data인 Input Data, Input Data가 forward() 메서드를 거쳐 결과적으로 반환될 return data가 저장될 Output 2개 모두 존재해야 함
    • module.register_full_backward_hook({추가할 Hook Function})
      • Backward 때 수행되는 Hook

코드로 보는 Hook

  • 활용 방법

    # 1. 객체 생성 이후 직접 hook function 추가 시킴
    class MyModel(nn.Module):
      def __init__(self):
         super().__init__()
         self.hooks=[]   // hook function으로 추가 가능
      
      def forward(self, input):
          for hook in self.hooks:
             input = hook(input)
          return input
    # ==========모델 선언=============== #
    
    model = MyModel()
    model.hooks.append({추가할 Function})
    
    # 2. Built-in Function을 사용. 이 방법을 더 많이 활용하는 것 같다.
    newmodel = OtherModel() 
    # OtherModel 클래스에는 self.hooks라는 객체가 없다고 생각하자
    newmodel.register_forward_pre_hook({추가할 Function}) 
    # Forward 때 수행되는 Pre-hook 정해줌

register_forward_hook에 관한 고찰

def forward(self, x):
  y = x + 5
  return y
# ======= Here!! ======= #

def hook1(self, input, output):
    output = output + 5

def hook2(self, input, output):
    output = output + 5
    return output

설명
hook1과 hook2를 forward() 메서드를 가진 Module에 적용시켰다고 하자.
만약 Input이 5였다면, 각각의 결과는 어떻게 될까?
결과부터 말하자면, hook1을 적용시킬 경우 10, hook2를 적용시킬 경우 15가 될 것이다.

hook은 forward의 return y의 아래, 즉 Here!!!!의 위치에 존재한다고 생각하면 된다
단지, input이 forward가 받은 Parameter이고, output이 return값, 즉 y일 뿐이다

hook1 같은 경우 y = y + 5가 수행되었을 것이다. 즉 y에는 15가 저장되어 있을 것이다. 하지만 return 명령어가 없으므로, 이 15라는 값은 그냥 없어지게 된다
따라서, hook1에서는 15값으로 변경되었지만, forward() 메서드는 이 것을 알 도리가 없기 때문에 그냥 자신에게 저장되어 있던 10을 반환한다

하지만 hook2같은 경우 return output을 통해 15라는 값을 forward에게 알렸다. 이 때, forward는 output이 15라는 값으로 바뀌었다는 것을 인지한다
(물론, return "output"이여서 output으로 안게 아니라, 그냥 return한 값을 Update된 Output으로 인지한다)
즉, forward에서는 y = 10을 반환하고 싶었는데, hook2 Function을 통해 y가 15로 Update 되었으니 결과적으로 15를 반환하는 것이다


Backward

Backward란?

Forward의 결과값(Model의 예측치)와 실제 Data간 차이(Loss)에 대한 Autograd 수행하는 함수이다.
역전파는 순전파의 역방향으로 진행되는 학습 과정으로 ,이 부분에 대해서는 따로 공부가 필요하니 여기서는 자세히 설명하진 않겠다.

BackWard 과정에서는 Layer에 존재하는 Parameter들의 미분을 수행한다.

코드로 보는 Backward

대다수의 경우 아래와 같은 코드를 통해 Backward 과정을 수행시킨다.

아래 과정 전체가 Model에 1개 Batch Data가 들어가 1 Epoch이 수행되는 과정이다.

self.optimizer.zero_grad() # Gradient 초기화
output = self.model(data)  # y의 예측값 생성
loss = self.criterion(output, target) 
# y의 예측값인 output과 실제 target 사이 loss값 측정
# criterion : (내가 지정한) Loss Function

loss.backward() # Backward Propagation 수행
self.optimizer.step() # Gradient Update

optimizer.zero_grad()

PyTorch에서는 Gradients 값을 Backward를 수행할 때 마다 "더해준다"
즉, 이전에 계산했던 Gradients 값들이 Optimizer에 저장이 되어버려 내가 원하는 방향으로 학습이 진행되지 않는다
이전에 계산해놓았던 Parameter가 현재 Parameter에 영향을 끼치는 것을 막고 싶기 때문에 optimizer를 초기화 시키는 것이다.

그렇다면 왜 더해줄까? 처음부터 초기화시키면 사용자가 훨씬 쉽게 활용할 수 있지 않을까?

이는 "Gradient Accumulation"이라는 방법 때문이다.

Gradient Accumulation

Batch 여러 개를 활용하여 Optimizer를 Update하는 것으로, Batch Size를 늘리는 것과 동일한 효과를 낼 수 있다.

원래라면 매 Epoch마다 zero_grad()를 활용하여 Optimizer에 저장된
Gradient를 초기화 시켜주었어야 한다.
하지만, Gradient Accumulation에서는 이전 연산으로 인해 저장된 Gradient를 초기화 하지 않는다. 대신, (개발자가 지정해준) 주기마다 저장된 Gradient를 초기화 시켜주는 것이다.

그렇다면 왜 사용할까?
많은 이유가 있겠지만, 가장 큰 이유는 "OOM의 해결"이다.
OOM(메모리 부족 에러)이 일어나는 이유중 대부분의 이유는 높은 batch size이다. 큰 Batch Size를 활용하면 학습시에 정보 노이즈를 제거하고 더 좋은 Gradient Descent를 수행할 수 있다
(물론, 상황에 따라 작은 Batch Size가 더 좋은 경우도 존재하며, Batch Size가 클 때와 작을 때의 장단점이 뚜렷하다. 하지만, 대부분의 경우 Batch Size가 클 때 조금 더 좋은 성능을 보인다)

하지만, 알다시피 DL의 특성상 메모리가 매우 많이 필요하다. 큰 회사 같은 경우가 아니라면(특히 나같은 개인이라면...) 메모리가 많이 부족하게 될 것이다.

이런 상황에서 OOM을 해결하기 위해서는 3가지 방법이 있다.
1. Batch Size를 줄인다.
방법은 방법이지만, 우리는 Batch Size를 크게 하고 싶기 때문에 일단 무시하자

2. 필요없이 수행되는 프로세스를 모두 실행 종료한다.
만약 실행중인 프로세스가 없었다면 이 방법은 소용이 없다.

3. Graident Accumulation
위 1,2 방법은 결국 문제를 간접적으로 해결하는 회피법일 뿐 Batch Size를 크게 함과 동시에 모델을 수행시키고 싶다는 욕망은 해결하지 못한다.
위에서 Gradient Descent는 Batch Size를 크게 늘리는 것과 동일한 효과를 낼 수 있다고 말했다. 즉, Gradient Descent를 활용하면 Batch Size를 크게 함과 동시에 OOM을 발생시키지 않을 수 있는 차선의 방법이라는 것이다
(최선의 방법은 Memory를 사는 것이다)

아래 코드를 보면 더욱 이해가 확실하지만, 한 마디로 이전 연산의 Gradient를 더해주고 특정 주기마다 더하기 연산이 수행된 Graident를 학습 과정에 활용함으로써, (Batch Size)*(Gradient 초기화 주기)만큼의 Batch Size를 형성한 것처럼 학습이 진행 가능하다.

코드로 보는 Gradient Accumulation

term = 2 # Gradient 초기화 주기
optimizer.zero_grad()
for epoch in range(EPOCHS):
	running_loss = 0.0
    for i, data in enumerate(train_loader, 0):
    	inputs, labels = data
        outputs = model(inputs)
        
        """
        뭔가 허전하지 않은가?
        원래라면 여기에 optimizer.zero_grad()가 추가되어 있어야 한다.
        Gradient Accumulation은 특정 주기(term)마다 초기화가 진행되므로
        이 부분에서는 초기화를 시켜줄 필요가 없다.
        """
        
        loss = criterion(outputs, labels) / term
        """
        2번째 특징이다.
        criterion을 활용하여 Loss 값을 구하였다.
        문제는 우리는 term만큼 오차의 값을 "더할" 것이다.
        만약 아무 연산도 수행하지 않고 오차만 더한다면, Loss 값은 항상
        양수이기 때문에 너무 커질 것이다.
        따라서, term으로 나누어주어 매 epoch당 Loss를 동등한 비율로, 그리고
        모든 가중치를 합하면 1이 되도록 해주는 것이다.
        """
        
        loss.backward()
        # Back Propagation은 항상 일어나야 한다. 
        
        if i%term == 0:
        # gardient 초기화가 일어나는 구간
        	optimizer.step()
            optimizer.zero_grad()
        """
        term 동안 더해진 Loss는 loss 변수에 저장되어 있을 것이고, 
        if문이 수행되는 epoch에서는 term동안 구해진 
        모든 Loss를 더한 값이 저장되어 있을 것이다.
        
        따라서 loss.backward()를 수행한 Gradient는 term동안 구해진 
        모든 Loss를 더한 값에 대한 학습 연산이 일어난 것이고,
        이를 활용하여 optimizer.step()을 통해 학습 결과를 적용한다.
        
        마지막으로, 다음 epoch부터는 다시 Loss = 0부터 시작하여
        더해야 하기 때문에 optimizer.zero_grad()로 초기화 한다.
        """

Model에 적용할 수 있는 Built-in Model

위에서 torch.nn.Parameters를 설명할 때 내가 직접 Parameters를 설정할 경우는 많이 없다고 하였는데, 바로 DL에 필요한 알고리즘은 대부분 Built-in Function으로 저장되어 있고, 이런 Built-in Function은 알아서 Parameter들을 정해 놓았기 때문이다.

torch.nn.Linear
대표적인 Built-in Model로 선형 모델을 구할 때 활용했던 y=XWT+by = XW^T + b에 대한 코드를 미리 구현해 놓은 Built-in Function이다.
forward() 메서드 Override 할 때 이런 Built-in Function을 활용하기만 하면 된다.

활용 예시

import torch
from torch.autograd import Variable

class LinearRegression(torch.nn.Module):
    def __init__(self, in_features, out_features):
        """
        위에서 내가 직접 만든 모델. 슬프지만 필요가 없다
        super().__init__()
        self.in_features = in_featuers
        self.out_features = out_features
        
        self.weights=nn.Parameter(torch.randn(in_features, out_features))
        self.bias = nn.Parameter(torch.randn(out_features))
        """ 
        super(LinearRegression, self).__init__()
        self.linear = torch.nn.Linear(in_features, out_features)
        # linear에 모델이 저장될 것
        
    def forward(self, x):
       out = self.linear(x) # 위에서 생성한 모델에 Data를 입력만 하면 끝!
       return out
  • 역전파를 통한 학습 진행은 이전에 설명했던 Optimizer를 통해 수행됨
model = LinearRegression(1,1)
if torch.cuda.is_available():
     model.cuda() # GPU에서 Model을 돌리겠다!

criterion = torch.nn.MSELoss() # Loss Function도 미리 구현되어 있다
optimizer = torch.optim.SGD(model.parameters(), lr={학습율})
# 어떤 방식으로 역전파를 수행할 것이며(SGD), 
# 어떤 Parameter를 학습할 것인가(model.parameters())

for epoch in range(epohcs): # epohcs만큼 역전파를 통한 학습 수행
     inputs = Variable(torch.from_numpy(x_train)) # 학습 Data 
     labels = Variable(torch.from_numpy(y_train)) 
     # Data에 대응되는 실제 결과값

     optimizer.zero_grad()
     outputs = model(inputs)    
     loss = criterion(outputs, labels) 
     # output : Model을 통한 예상치, label : 실제 결과값
     # Loss Function에 대입하여 차이 알아봄
     
     loss.backward() # Loss Function을 활용한 역전파 알고리즘 수행!
     optimizer.step() # backward() 과정의 결과값을 활용하여 W를 Update
  • Warning 문구 띄우지 않게 하기
import warnings
warnings.filterwarnings('ignore')

torch.no_grad() vs model.eval()

Model의 Accuracy를 평가할 때나, 학습 과정이 아닌 순전파 과정을 진행할 때 아래 코드가 입력되어 있는 것을 볼 수 있다.

with torch.no_grad() :
	model.eval()
    ~~~
    model.train()

그렇다면, 도대체 이 둘은 무슨 차이가 존재할까? 또 왜 굳이 2개를 같이 활용하는 것일까?

torch.no_grad()

Autograd Engine을 끄는 메서드이디다.

간단히 말하자면, Autograd 연산을 모두 수행하지 않는 것이다. 이런 연산을 수행하기 위해 모든 Parameter에 대하여 requires_grad 값을 False로 초기화 한다.

Autograd 연산이 모두 수행되지 않으므로, Autograd가 필요하지 않은 연산인 Test나 Accuracy 측정 등에 활용될 경우 데이터가 차지할 공간을 아낄 수 있고, 메모리 확보도 가능해진다.

model.eval()

현재 모델링 시 trainig과 Inference(Test) 시에 다르게 동작해야 하는 Layer들이 존재한다.

대표적으로 Dropout Layer나 BatchNorm Layer가 존재한다.
(Trainig시에는 동작해야 하지만, Inference 시에는 동작해서는 안된다)

model.eval()은 이런 Layer의 동작을 inference(eval) model로 바꿔줌
Autogard 연산에 손을 대는 것은 아니기 때문에 메모리 상 이점을 볼 수는 없고, 단순히 학습 시점과 Inference 시점 때 수행되어야 하는 방식이 다른 함수에 대한 Mode를 바꿔주는 함수이다.

만약 model.eval()로 Test를 진행했으면 Test 마지막 부분에 model.train()을 통해 다시 Train Mode로 model을 변경해 줘야한다.

model.eval()과 torch.no_grad()를 Inference 때 둘 다 활용하는 이유

먼저 Inference 때와 Trainig 때 적용되는 함수가 다른 모듈들 때문에 무조건
model.eval()은 필수적이다.

Inference할 때 Autograd를 할 필요가 있을까? Test는 모델의 성능을 올리는 것이 목적이 아니라 어느 정도의 Accuracy를 가지고 있는지 파악하는 것이 중요하다.
(그리고, Test Data로 Accuracy를 올리는 것은 Cheating이다)

즉, Test 때는 예측값과 실제 값의 차이에 대해서만 계산하면 되므로 Autograd 연산이 필요 없고, 자원을 아끼기 위해 with torch.no_grad()로 코드를 감싸 Autograd 연산을 막는 것이다.

이러한 이유로 2개를 모두 활용하여 코드를 짜는 것이다


Module Save

모듈의 파라미터를 저장

  • 사용 코드 : model.state_dict()

파라미터를 저장하고 활용하는 방식이기 때문에 파라미터를 받는 곳에서 모델이 코드 상으로 구현되어 있을 때만 활용 가능한 방법이다.
여기서 중요한 점은, 코드 상 구현되어 있는 모델이 파라미터를 저장시켰던 모델과 동일해야지만 내가 원하는 모델을 형성할 수 있다.

정확히 말해서는 같지 않아도 수행이 될 경우도 존재하지만, 정확도를 보장하지는 못한다. 예를 들어보자.
y=XWT+by=XW^T+b에서 (W,b)를 저장했는데, 내가 지정한 모델이 y=XbT+Wy=Xb^T+W일 경우 당연히 이전에 학습했던 (W,b)는 정확도를 보장해주지 못할 것이다.

장점으로는 Model의 형식을 저장하지 않아도 되기 때문에 파일 용량이 가벼워진다.

코드로 보는 파라미터 저장

# model의 파라미터를 저장시켜 model2에 적용시키는 코드
# 위에서 말했듯, TheOriginModel과 TheNewModel은 같은 형태의 모델이어야 한다.
model = TheOriginModel()
model2 = TheNewModel()

torch.save(model.state_dict(), model.pt') 
# model의 파라미터를 model.pt에 저장

model2.load_state_dict(torch.load('model.pt')) 
# model.pt에서 Parameter를 불러와서 model2에 적용시키기

모듈 자체를 저장

torch.save를 통해 model 자체를 저장하는 방식이다.
Optimizer, Epoch, Loss function 등 모델 전체에 관한 정보를 저장하는 방식이다.

모델 전체가 파일에 저장되어 있기 때문에 코드 상 모델이 미리 구현되어 있지 않더라도 활용 가능하다는 장점은 존재하지만 파일 용량이 매우 커진다

코드로 보는 모듈 저장

torch.save(model, 'model.pt') 
# 모델 전체를 model.pt에 저장

newModel = torch.load('model.pt')
# newModel은 새로 나온 변수
# model 자체를 불러오기 때문에 다른 설정이 필요 없음

Checkpoints

학습의 중간 결과를 저장하여 최선의 결과를 선택하는 것이다.
특히 EarlyStopping 기법 활용 시 이전 학습의 결과물을 저장하고, Loss 값이 가장 작은 Weight 등 특정 기준을 활용해 결과를 저장한다.

일반적으로 epoch, loss, metric을 함께 저장하는데, 모든 Epoch마다 중간 결과를 저장하지는 않고, Loss나 Accuracy가 이전보다 향상되었을 때만 Parameter를 Update하는 방식으로 checkpoint 활용 가능하다.
(물론, 일정 Epoch이 지날 때마다 저장하게 해도 됨)

코드로 보는 Checkpoint

  • Checkpoint 저장
for e in range(epoch):       # e를 통해 현재 몇 번째 Epoch인지 알 수 있음
    ...                         # model 학습에 관련된 코드가 나올 것

   torch.save({
      'epoch':e,
      'model_state_dict':model.state_dict(),
      'optimizer_state_dict':optimizer.state_dict(),
      'loss':epoch_loss
   }, f'{저장할 파일이름}')

  # Checkpoint를 저장할 때는 OrderedDict type으로 저장하기 때문에, 
  # Key:Value 쌍으로 Checkpoint를 저장한다.
  • 저장한 Checkpoint 불러오기
checkpoint = torch.load({Checkpoint를 저장한 파일 이름})
model.load_state_dict(checkpoint['model_state_dict'])
# Parameter를 불러와 모델에 적용시키기
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])

# Optimizer를 불러와 모델에 적용시키기
epoch = checkpoint['epoch']
loss = checkpoint['loss']

# model, optimizer를 보면 우리는 state_dict()를 저장함을 볼 수 있다.
# 즉, Parameter를 저장하는 것이기 때문에,  
# model과 optimizer는 미리 선언되어 있어야 할 것이다

모델 Parameter 정보 확인

1. {생성 model}.\__dict\__
메서드를 통해 Model에 지정된 Hook 및 Parameter, 변수 들을 모두 볼 수 있다.

2. print({생성 Model})
위 함수는 Parameter 자체를 볼 수는 없지만, 함수의 Layer에 저장된 Block(Layer)를 볼 수 있어 간접적으로 유추 가능하다

2. torchsumamry
torch summary 설치하고 summary(model, {input data Channel})을 통해 볼 수 있음


Transfer Learning

Transfer Leraning이란?

다른 데이터셋(대용량 데이터셋)으로 만든 모델을 (일부분 변경하여) 현재 내가 만들고 싶은 모델에 적용시키는 것으로, 현재 가장 일반적인 학습 기법이다.

Backbone Architecture가 잘 학습된 모델에서 일부분을 변경하여 학습을 수행하는 것으로, NLP에서는 HuggingFace를 잘 학습된 모델 표준으로 활용한다.

코드로 보는 Transfer Learning

  • 1번 방법(가장 일반적인 방법)
    • Model Class를 선언 후 class 안에 모델 넣기
class MyModule(nn.Module):
    def __init__(self):
         super(MyModule, self).__init__()
         self.vgg19 = model.vgg19(pretrained=True) 
         # 원하는 모델(현재 vgg19) 불러오기
         self.linear_layer = nn.Linear(1000,1) 
         # 내가 추가로 수행하고 싶은 모델 함수

    def forward(self,x):
         x = self.vgg19(x)          
         # 먼저 학습된 모델에 x를 입력하여 변형시킴
         return self.linear_layers(x) 
         # 이후 내가 만든 모델 함수에 원래 존재하던(Pretrained된) 모델을 통해
         # 나온 Output을 입력
  • 2번 방법 : 함수 선언 없이 내가 추가로 수행하고 싶은 Layer(Block) 추가
vgg.fc = torch.nn.Linear(1000,1)
"""
vgg : Model
vgg에 fc라는 Layer를 가장 마지막에 Layer로 생성하고 Linear를 
해당 Layer Block으로 지정
"""
  • 3번 방법 : 원래 존재하던 Function을 내가 원하는 함수로 바꿔주는 방법
vgg.X.y = torch.nn.Linear(1000,1)
"""
vgg.X.y에는 불러온 Model에 대한 Function이 저장되어 있었을 것이다
(예를 들면, Conv2D 등)
하지만 Conv2D 대신 Linear를 활용하고 싶을 때, vgg.X.y = torch.nn.Linear()를 
통해 원래 저장된 Function이 아닌 다른 Function을 저장 시키는 것이다
"""

추천하는 방법은 아니다. 쉽게 예상이 가능하겠지만, vgg라는 가지고 온
Model 같은 경우 원래 존재하던 Function과 SubModule에 대해 학습이 진행되었을 것이다.
즉, 원래 존재하던 Function을 바꿔버리면 이전에 학습했던 Model의 Accuracy에 대한 보장이 약해질 것이다.
(원래 존재하던 모델은 많은 데이터를 "원래 존재하던" Function에 적용시켜 학습했을 것이므로)

Freezing

미리 학습된 모델을 활용할 때 모델의 일부분을 Forzen시키는 것으로, 다른 Task로 학습시킨 것 중 새로 학습을 시키지 않아도 되는 부분을 잠궈서 Back Propagation 과정에서 학습이 이뤄지지 않도록 하는 것이다.

한 Step만 풀어주고 나머지를 Frozen 시켜 학습시키는 과정을 Layer마다 진행시켜 단계적 학습을 수행하는 것을 Stepping Frozen이라고 한다.

코드를 통한 Freezing

for param in myModel.parameters():
       param.requires_grad = False 
       # 파라미터를 학습 대상에서 전부 제외 시킴

# myModel에서 linear_layer라는 함수를 추가시켰다고 가정하자
for params in myModel.linear_layer.parameters():
     param.requires_grad = True
# 내가 추가한 linear_layer 함수에 대해서는 학습이 진행되어야 하므로 
# 학습 대상에 추가시켜줌
profile
개념부터 확실히!

0개의 댓글

관련 채용 정보