파이썬3 노트

pytorch 공부: [nn.Sequential][nn.ModuleList]

Jonchann 2018. 10. 16. 16:47

10월 안에 CNN-LSTM모델을 짜야 하는데 논문 구현해 놓은 깃허브를 보니 계속 nn.Sequential과 nn.ModuleList가 나와서 정리해야겠다 싶었음.



[nn.Sequential]

이것은 입력값이 하나일 때, 즉 각 레이어를 데이터가 순차적으로 지나갈 때 사용하면 좋은 방법이라는 것 같다.

간단히 말하자면 여러 nn.Module을 한 컨테이너에 집어넣고 한 번에 돌리는 방법이다.

 

구글에서 찾은 예제를 바탕으로 적당히 적어보면 아래와 같다.

import torch
import torch.nn as nn


class CNNModel(nn.Module):
   def __init__(self, class_num, filter_num, num):
     super().__init__():
     self.layer1 = nn.Sequential(nn.Conv2d(in_channels = 1,
                                      out_channels = 32,
                                      kernel_size = 5,
                                      stride = 1,
                                      padding = 2),
                         nn.BatchNorm2d(num_feature = 32),
                         nn.ReLu(),
                         nn.MaxPool2d(kernel_size = 2, stride = 2))
     self.layer2 = nn.Sequential(nn.Conv2d(in_channels = 32,
                                      out_channels = 64,
                                      kernel_size = 5,
                                      stride = 1,
                                      padding = 2),
                         nn.BatchNorm2d(num_features = 64),
                         nn.ReLu(),
                         nn.MaxPool2d(kernel_size = 2, stride = 2))
     self.fc = nn.Linear(out_channels * num * num, class_num)
     self.dropout = nn.Dropout()

   def forward(self, seq):
     seq = self.layer1(seq)
     seq = self.layer2(seq)
     seq = self.fc(seq)
     return seq

class CNNModel(nn.Module):
   def __init__(self, class_num, filter_num, num):
     super().__init__():
     self.layer1 = make_sequential(1, 32, kernel_size = 5, stride = 1, padding = 2)
     self.layer2 = make_sequential(32, 64, kernel_size = 5, stride = 1, padding = 2)
     self.fc = nn.Linear(out_channels * num * num, class_num)
     self.dropout = nn.Dropout()

   def forward(self, seq):
     seq = self.layer1(seq)
     seq = self.layer2(seq)
     seq = self.fc(seq)
     return seq

위 코드를 읽으면 바로 알 수 있겠지만 nn.Sequential은 코드에 적힌 순서대로 값을 전달해 처리한다. 빠르고 간단히 적을 수 있기 때문에 간단한 모델을 구현할 때에 쓰면 된다는 것 같다.

대신 위에서도 말했다시피 여러 입력값을 따로 따로 동시에 돌리는 것은 할 수 없다.


Medium에서 찾은 기사(Pytorch: how and when to use Module, Sequential, ModuleList and ModuleDict)에는 아예 class 앞에 Sequential을 만드는 함수를 만들어 간편하게 쓸 수 있다고 되어있다.

def make_sequential(in_channels, out_channels, *args, **kwargs):
  return nn.Sequential(nn.Conv2d(in_channels, out_channels, *args, **kwargs),
               nn.BatchNorm2d(out_channels),
               nn.ReLu(),
               nn.MaxPool2d(*args, **kwargs))

이 함수를 이용하면 당연하게도 첫 번째 코드가 간결해진다.

 class CNNModel(nn.Module):
   def __init__(self, class_num, filter_num, num):
     super().__init__():
     self.layer1 = make_sequential(1, 32, kernel_size = 5, stride = 1, padding = 2)
     self.layer2 = make_sequential(32, 64, kernel_size = 5, stride = 1, padding = 2)
     self.fc = nn.Linear(out_channels * num * num, class_num)
     self.dropout = nn.Dropout()

   def forward(self, seq):
     seq = self.layer1(seq)


seq = self.layer2(seq) seq = self.fc(seq) return seq


이제까지 적은 것은 내가 하나 하나 in_channel값, out_channel값 등을 지정해 적어줬는데 위 기사에 따르면 배열(array) 크기에 따라 자동으로 지정할 수 있다고 한다.

 class CNNModel(nn.Module):
   def __init__(self, in_channels, out_channels1, out_channels2, class_num, filter_num, num):
     super().__init__():
     self.layer_size = [in_channels, out_channels1, out_channels2]

     layers = [make_sequential(in_channels, out_channels, kernel_size = 5, stride = 1, padding = 2)\
                       for in_channels, out_channels in zip(self.layer_size, self.layer_size[1:])]

     self.encoder = nn.Sequential(*layers)

     self.fc = nn.Linear(out_channels * num * num, class_num)
     self.dropout = nn.Dropout()

   def forward(self, seq):
     seq = self.encoder(seq)
     seq = self.fc(seq)
     return seq

기사에는 encoder와 decode를 전부 nn.Sequential로 묶어서 CNN모델을 만들었는데 아예 class MyEncoder(nn.Module):, class MyDecoder(nn.Module):로 나눠서 위의 for문을 돌리고 class MyCNN(nn.Module):을 만들어서 정말 간결하게 적어놓았다.

정말 알아보기 쉽게 작성하는 방법이라 이런 스타일을 기억해 놨다가 꼭 좀 따라해봐야겠다.



[nn.ModuleList]

이것은 간단히 말해 nn.Module을 리스트로 정리하는 방법이다.

각 레이어를 리스트에 전달하고 레이어의 iterator를 만든다. 덕분에 forward처리를 간단하게 할 수 있다는 듯 하다.

처음으로 적는 것은 아주 무식하게 하나하나 다 적어서 리스트에 넣고 for로 돌리는 방식이다.

class CNNModel(nn.Module):
   def __init__(self, in_channels, out_channels1, out_channels2, class_num, filter_num, num):
     super().__init__():
     self.conv1 = nn.Conv2d(1, 32, kernel_size = 5, stride = 1, padding = 2)
     self.batchnorm1 = nn.BatchNorm2d(32)
     self.relu1 = nn.ReLu()
     self.maxpool1 = nn.MaxPool2d(kernel_size = 2, stride = 2)

     self.conv2 = nn.Conv2d(32, 64, kernel_size = 5, stride = 1, padding = 2)
     self.batchnorm2 = nn.BatchNorm2d(64)
     self.relu2 = nn.ReLu()
     self.maxpool2 = nn.MaxPool2d(kernel_size = 2, stride = 2)

     self.fc = nn.Linear(out_channels * num * num, class_num)
     self.dropout = nn.Dropout()
     layers = [self.conv1, self.batchnorm1, self.relu1,
            self.maxpool1, self.conv2, self.batchnorm2,
            self.relu2, self.maxpool2, self.fc, self.dropout]

     self.module_list = nn.ModuleList(layers)

   def forward(self, seq):
     for layer in self.module_list:
         x = f(x)
     return x


두 번째로는 text classification cnn pytorch라고 검색했을 때 가장 많이 보이던 것인데, 커널의 region size를 for로 받아 nn.Conv2d를 리스트 안에 수납하는 형식이다.

class CNNModel(nn.Module):
  def __init__(self, vocab_size, embedding_dim, kernel_sizes, num_class):
    super().__init__()
    
    self.vocab_size = vocab_size
    self.embedding_dim = embedding_dim
    self.kernel_sizes = kernel_sizes

    self.embed = nn.Embedding(vocab_size, embedding_dim)
    self.conv = nn.ModuleList([nn.Conv2d(in_channels, out_channels, (kernel, embedding_dim))\
                                 for kernel in kernel_sizes])
    self.dropout = nn.Dropout()
    self.fc = nn.Linear(len(kernel_sizes) * out_channels, num_class)

Medium의 또 다른 기사 Understanding how Convolutional Neural Network (CNN) perform text classification with word embeddings에 따르면 여러 region size의 커널(filter)를 사용하는 것은 n-gram을 이용하는 것이라 되어있다.

가장 기본적(?)인지는 모르겠지만 가장 많이 보이는 것은 [2, 3, 4] 즉, 2-gram, 3-gram, 4-gram을 이용하는 것이었다. 그러니 커널 사이즈는 kernel * embedding_size가 되는 것이다.