nn.Module을 상속받는 클래스에서의 forward() 함수
토치에서 모델을 만들 때, 다음과 같은 형태로 모델을 만든다.
PyTorch 내장 모델과 사용자 정의 모델 모두 이 형태로 만들어야 한다.
1. torch.nn.Module을 상속받아야 한다.
2. __init__()과 forward()를 override 해줘야 한다.
__init__()에서는 사용될 모듈, 활성화 함수 등을 정의한다.
forward()에서는 모델에서 실행되어야 하는 계산을 정의한다. backward 계산은 backward()를 이용하면 알아서 해주기 때문에 forward()만 정의하면 된다. input을 넣고 어떤 과정을 거쳐 output이 나올지를 정의해 준다는 느낌이다.
class GRU(nn.Module):
def __init__(self, n_layers, hidden_dim, n_vocab, embed_dim, n_classes, dropout_p=0.2):
super(GRU, self).__init__()
self.n_layers = n_layers
self.hidden_dim = hidden_dim
self.embed = nn.Embedding(n_vocab, embed_dim)
self.dropout = nn.Dropout(dropout_p)
self.gru = nn.GRU(embed_dim, self.hidden_dim,
num_layers=self.n_layers,
batch_first=True)
self.out = nn.Linear(self.hidden_dim, n_classes)
def forward(self, x):
x = self.embed(x)
h_0 = self._init_state(batch_size=x.size(0)) # 첫번째 히든 스테이트를 0벡터로 초기화
x, _ = self.gru(x, h_0) # GRU의 리턴값은 (배치 크기, 시퀀스 길이, 은닉 상태의 크기)
h_t = x[:,-1,:] # (배치 크기, 은닉 상태의 크기)의 텐서로 크기가 변경됨. 즉, 마지막 time-step의 은닉 상태만 가져온다.
self.dropout(h_t)
logit = self.out(h_t) # (배치 크기, 은닉 상태의 크기) -> (배치 크기, 출력층의 크기)
return logit
def _init_state(self, batch_size=1):
weight = next(self.parameters()).data
return weight.new(self.n_layers, batch_size, self.hidden_dim).zero_()
이렇게 만들어진 클래스를 model = model()과 같이 인스턴스 한 후 input을 넣기만 하면 모델이 동작한다.
model = GRU(1, 256, vocab_size, 128, n_classes, 0.5).to(DEVICE)
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
def train(model, optimizer, train_iter):
model.train()
for b, batch in enumerate(train_iter):
x, y = batch.text.to(DEVICE), batch.label.to(DEVICE)
y.data.sub_(1) # 레이블 값을 0과 1로 변환
optimizer.zero_grad()
logit = model(x)
loss = F.cross_entropy(logit, y)
loss.backward()
optimizer.step()
우리가 흔히 알고 있는 클래스에서의 함수 사용법은 model.forward()와 같이 호출을 해주는 것이었다.
그래서 단순히 model 객체를 데이터와 함께 호출하면 자동으로 forward() 함수가 실행이 된다는 점이 의아했다.
따라서 nn.Module의 소스코드를 한번 들여다보았다.
https://github.com/pytorch/pytorch/blob/master/torch/nn/modules/module.py
1494번째 줄에 __call_impl 함수가 정의되어 있다. (코드가 수정되면 줄의 정확한 위치는 바뀔 수 있다.)
def _call_impl(self, *input, **kwargs):
forward_call = (self._slow_forward if torch._C._get_tracing_state() else self.forward)
# If we don't have any hooks, we want to skip the rest of the logic in
# this function, and just call forward.
if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks
or _global_forward_hooks or _global_forward_pre_hooks):
return forward_call(*input, **kwargs)
# Do not call functions when jit is used
full_backward_hooks, non_full_backward_hooks = [], []
if self._backward_hooks or _global_backward_hooks:
full_backward_hooks, non_full_backward_hooks = self._get_backward_hooks()
if _global_forward_pre_hooks or self._forward_pre_hooks:
for hook in (*_global_forward_pre_hooks.values(), *self._forward_pre_hooks.values()):
result = hook(self, input)
if result is not None:
if not isinstance(result, tuple):
result = (result,)
input = result
bw_hook = None
if full_backward_hooks:
bw_hook = hooks.BackwardHook(self, full_backward_hooks)
input = bw_hook.setup_input_hook(input)
result = forward_call(*input, **kwargs)
if _global_forward_hooks or self._forward_hooks:
for hook in (*_global_forward_hooks.values(), *self._forward_hooks.values()):
hook_result = hook(self, input, result)
if hook_result is not None:
result = hook_result
if bw_hook:
result = bw_hook.setup_output_hook(result)
# Handle the non-full backward hooks
if non_full_backward_hooks:
var = result
while not isinstance(var, torch.Tensor):
if isinstance(var, dict):
var = next((v for v in var.values() if isinstance(v, torch.Tensor)))
else:
var = var[0]
grad_fn = var.grad_fn
if grad_fn is not None:
for hook in non_full_backward_hooks:
grad_fn.register_hook(_WrappedHook(hook, self))
self._maybe_warn_non_full_backward_hook(input, result, grad_fn)
return result
이후
__call__ : Callable[..., Any] = _call_impl
를 통해서 _call_impl 함수를 __call__을 통해 호출해 준다.
여기서 __call__ 이 무엇을 의미하는지 짚고 넘어가자.
__call__이란?
함수를 호출하는 것처럼 클래스의 객체도 호출하게 만들어주는 메서드이다.
즉, __init__은 인스턴스 초기화를 위해, __call__은 인스턴스가 호출되었을 때 실행되는 것이다.
__call__ 함수가 있을 경우, 클래스 객체 자체를 호출하면 __call__함수의 return 값이 반환된다.
__call__을 왜 사용할까?
클래스의 인스턴스를 함수로 취급한 후 다른 함수의 파라미터로 사용하기 위해서이다.
참고로 __call__은 클래스를 선언할 때 기본으로 생성되며, 비어있는 인스턴스가 들어가 있다.
class myclass:
def plus(self,n1,n2):
return n1+n2
__call__ = plus
myinstance = myclass()
myinstance(1,2) # 3
이처럼 활용할 수 있는 것이다.
class BinaryClassifier(nn.Module):
def __init__(self):
super().__init__()
self.linear = nn.Linear(2, 1)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
return self.sigmoid(self.linear(x))
위와 같은 클래스를 사용한 모델 구현 형식은 대부분의 파이토치 구현체에서 사용하고 있는 방식으로 반드시 숙지할 필요가 있다.
클래스(class) 형태의 모델은 nn.Module을 상속받는다. 그리고 __init__()에서 모델의 구조와 동작을 정의하는 생성자를 정의한다.
이는 파이썬에서 객체가 갖는 속성 값을 초기화하는 역할로, 객체가 생성될 때 자동으로 호출된다.
super() 함수를 부르면 여기서 만든 클래스는 nn.Module 클래스의 속성들을 가지고 초기화된다.
forward() 함수는 모델이 학습 데이터를 입력받아서 forward 연산을 진행시키는 함수이다. 이 forward() 함수는 model 객체를 데이터와 함께 호출하면 자동으로 실행이 된다.
예를 들어 model이란 이름의 객체를 생성 후, model(입력 데이터)와 같은 형식으로 객체를 호출하면 자동으로 forward 연산이 수행되는 것이다.
class 형태의 모델은 항상 nn.Module을 상속받아야 하며, super(모델명, self).__init__()을 통해 nn.Module.__init__()을 실행시키는 코드가 필요하다.
forward()는 모델이 학습 데이터를 입력받아서 forward propagation을 진행시키는 함수이고, 반드시 forward라는 이름의 함수이어야 한다.
RNN의 예제를 통해서 위에서 언급한 내용들을 정리해 보자.
class RNN(nn.Module):
def __init__(self, n_layers, hidden_dim, n_vocab, embed_dim, n_classes, dropout_p=0.2):
super(RNN, self).__init__()
self.n_layers = n_layers # RNN을 몇번 돌린건지
self.embed = nn.Embedding(n_vocab, embed_dim) # 워드 임베딩 적용
self.hidden_dim = hidden_dim # 은닉층 뉴런개수
self.dropout = nn.Dropout(dropout_p) # 드롭아웃 적용
self.rnn = nn.RNN(embed_dim, self.hidden_dim, num_layers=self.n_layers, batch_first=True) # embed_dim: 훈련데이터의 특성 개수, hidden_dim: 은닉층의 뉴런 개수, num_layers: rnn 계층 개수
self.out = nn.Linear(self.hidden_dim, n_classes) # 마지막 레이어(클래스 2개)
def forward(self, x):
x = self.embed(x) # 문자를 숫자/벡터로 변환
h_0 = self._init_state(batch_size=x.size(0)) # 최초 은닉 상태의 값을 0으로 초기화
x, _ = self.rnn(x, h_0) # RNN 계층을 의미하며, 파라미터로 입력과 이전 은닉 상태의 값을 받음
h_t = x[:, -1, :] # 모든 네트워크를 거쳐서 가장 마지막에 나온 단어의 임베딩 값(마지막 은닉 상태의 값)
self.dropout(h_t)
logit = torch.sigmoid(self.out(h_t))
return logit
def _init_state(self, batch_size=1):
weight = next(self.parameters()).data # 모델의 파라미터 값을 가져와서 weight 변수에 저장
return weight.new(self.n_layers, batch_size, self.hidden_dim).zero_() # 크기가 (계층의 개수, 배치 크기, 은닉층의 뉴런/유닛 개수)인 은닉 상태(텐서)를 생성하여 0으로 초기화한 후 반환
model = RNN(n_layers=1, hidden_dim=256, n_vocab=vocab_size, embed_dim=128, n_classes=n_classes, dropout_p=0.5) # 모델 객체화
model.to(DEVICE)
loss_fn = nn.CrossEntropyLoss() # 손실함수
optimizer = torch.optim.Adam(model.parameters(), lr=lr) # 파라미터 업데이트를 위한 adam 옵티마이저
# 모델 학습 함수 정의
def train(model, optimizer, train_iter):
model.train()
for b, batch in enumerate(train_iter):
x, y = batch.text.to(DEVICE), batch.label.to(DEVICE)
y.data.sub_(1) # 뺄셈 함수(여기에선 1빼기를 나타냄), 레이블값 변환하기 위해(긍정=1, 부정=0으로 바꿔주려고)
optimizer.zero_grad()
logit = model(x)
loss = F.cross_entropy(logit, y)
loss.backward()
optimizer.step()
if b % 50 == 0: # 훈련 데이터셋의 개수를 50으로 나누어서 나머지가 0이면 출력
print("Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}".format(e,b *
len(x),len(train_iter.dataset),100. * b / len(train_iter),loss.item()))
위의 train 함수의 logit = model(x)을 통해 RNN 클래스의 forward() 함수와 _init_state() 함수가 사용됨을 알 수 있다.