부캠 초반부는 강의를 들으며 주어진 과제를 채워내는 식의 코딩이 전부이다.
멘토링 시간에 ResNet에 관해 리뷰하기도 했고 과제를 하며 ResNet을 부분적으로 구현해보기도 했기에
파이토치 공부 겸 ResNet을 Scratch부터 구현해 보았다.
ResNet code
class ResNet(nn.Module):
def __init__(self, name="resnet", xdim=[3, 224, 224], ksize=3, cdims=[64, 128, 256, 512], layeriter=[3,4,6,3],
hdim=1000, USE_BATCH_NORM=True):
super().__init__()
self.name = name
self.xdim = xdim #means C, W, H
self.cdims = cdims #Channels
self.hdim = hdim #Output dimension
self.ksize = ksize
self.USE_BATCH_NORM = USE_BATCH_NORM
#Convnet
self.layers=[]
prev_cdim = self.xdim[0]
self.layers.append(nn.Conv2d(in_channels=prev_cdim, out_channels=64,
kernel_size=7,
stride=2,
padding=7//2))
self.layers.append(nn.BatchNorm2d(64))
self.layers.append(nn.ReLU(True)) #no maxpool and dropout
self.layers.append(nn.MaxPool2d(2))
prev_cdim=64
# layers=[]
for index, cdim in enumerate(self.cdims):
iteration = layeriter[index]
for i in range(iteration):
if i==0:
init_block=True
if cdims[index]==64:
init_block=False
else:
init_block=False
#잔차계산을 위해 모듈화시킬것
self.layers.append(Resblock(channel_in=prev_cdim, channel_out=cdims[index],
init_block=init_block))
prev_cdim = cdims[index]
self.layers.append(nn.AdaptiveAvgPool2d((1, 1))) #넣어준 인자만큼의 space로 채널을 평균 풀링해주는듯?
self.layers.append(nn.Flatten())
self.layers.append(nn.Linear(prev_cdim, hdim, bias=True))
self.net = nn.Sequential(*self.layers)
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.BatchNorm2d): # init BN
nn.init.constant_(m.weight,1)
nn.init.constant_(m.bias,0)
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)
resnet = ResNet(name="resnet", xdim=[3, 224, 224], ksize=3, cdims=[64, 128, 256, 512], layeriter=[3,4,6,3],
hdim=1000, USE_BATCH_NORM=True).to(device) #hdim으로 출력층 개수 조절
class Resblock(nn.Module):
def __init__(self, ksize=3, channel_in=64, channel_out=64, init_block = False, USE_BATCH_NORM=True):
super().__init__()
self.shortcut = nn.Conv2d(in_channels=channel_in, out_channels=channel_out, kernel_size=1, stride=2)
self.layers = []
self.init_block = init_block
if self.init_block==True:
stride_init = 2
else:
stride_init = 1
#잔차계산을 위해 모듈화시킬것
self.layers.append(nn.Conv2d(in_channels=channel_in, out_channels=channel_out,
kernel_size=ksize,
stride=stride_init,
padding=ksize//2))
self.layers.append(nn.BatchNorm2d(channel_out))
self.layers.append(nn.ReLU(True))
self.layers.append(nn.Conv2d(in_channels=channel_out, out_channels=channel_out,
kernel_size=ksize,
stride=1,
padding=ksize//2))
self.layers.append(nn.BatchNorm2d(channel_out))
# self.layers.append(nn.ReLU(True)) #use relu after residual sum
self.resblock = nn.Sequential(*self.layers)
def forward(self, x):
if self.init_block==True:
return F.relu(self.resblock(x) + self.shortcut(x))
else:
return F.relu(self.resblock(x))
코드 설명하는데 필요한 게 코드를 직접 보여주는 것보다 좋은 게 있을까 싶다.
아마 코드를 보고 거의 뒤로 가기를 눌렀을 것 같긴 하지만 아직 읽고 있는 분들을 위해 조금 더 구현 디테일을 설명해 보겠다.
ResNet detail
위에 코드는 ResNet34를 구현한 것이다.
두 번째 코드블록인 Resblock 클래스는 그중 중간의 한 블록을 떼네온 것이다.
한 블록으로 두 번의 convoloution연산 후 residual연산을 해주기 까지를 구현하고 이를 ResNet class가 반복하는 구조이다.
사실 워낙 구현이 쉬운 논문이라 딱히 설명할 부분은 없다. 굳이 주의해야 할 점이라면
이런 색깔이 바뀌는 부분은 channel수가 바뀌는 부분인데 이러한 경우 projection layer를 이용해 잔차를 넘겨줘야 한다.
self.shortcut = nn.Conv2d(in_channels=channel_in, out_channels=channel_out, kernel_size=1, stride=2)
Resblock에 위와 같이 구현되어 있는데 channel 지정뿐만 아니라
점선 쪽 초반에서 conv연산의 stride를 2로 주므로 잔차 연산 시에도 stride를 2로 주는 부분까지 주의해야 한다.
Forward path
x_numpy = np.random.rand(2,3,224, 224)
x_torch = torch.from_numpy(x_numpy).float().to(device)
y_torch = resnet.forward(x_torch)
y_numpy = y_torch.detach().cpu().numpy()
print(x_torch.shape, y_torch.shape)
torch.Size([2, 3, 224, 224]) torch.Size([2, 1000])
입력 데이터가 원하는 형태로 잘 변경된 걸 확인할 수 있다.
learning rate나 가중치 초기화 부분은 논문처럼 세팅하지는 않았는데 구현 시간이 오래걸릴까봐 스킵했다.(실제로 이 코드를 구현하는데 예상보다 오랜시간이 걸렸다 Tensorflow면 10분컷인데...)
그래도 Tensorflow만 하다가 Pytorch를 본격적으로 하니 뭔가... 재밌는 느낌이 좀든다.
Tensorflow는 이미 Keras문법을 완전지원해서 직접 뭔가를 엔지니어링하는 재미는 없었는데 Pytorch는 하나부터 끝까지 전부 케어해줘야 해서 코딩 과정은 길고 어렵지만 이해하고 짜나가고 원하는대로 변경시켜나가는 과정이 뼈공대인 나한테는 재밌게 느껴졌다.