VGGNet
VGGNet(2014.09)
요약
- VGGNet은 2014년 ILSVRC에서 2등을 차지한 딥 CNN 모델로, 3x3 필터를 사용하여 16~19층의 구조를 가지며, 다양한 구성을 통해 이미지 분류 성능을 개선한다. 손실 함수로 CrossEntropy를 사용하고, 데이터 증강 기법으로 FiveCrop을 활용하여 입력 이미지를 5배 증가시킨다.
등장 배경
- ILSVRC-2014 이미지넷 분류 대회에서 2등을 차지한 최초의 Deep CNN 인공 신경망 모델이다.
- VGGNet 이전에는 CNN을 깊게 만들려는 시도는 없었고 CNN의 개념이 생긴 지 얼마 안된 시기이다.
- 따지고 보면 AlexNet이 첫 시도였지만 8층이 한계였다.
- 같은 시기에 Inception Net (GoogLeNet)이 나왔는데 해당 모델이 ILSVRC-2014에서 1등을 차지했다.
논문 출처
- K. Simonyan and A. Zisserman. Very Deep Convolutional Networks for Large-Scale Image Recognition. arXiv:1409.1556, 2014
초록
- 이 논문의 중점은 CNN의 깊이에 따른 이미지 분류 성능 개선의 여부 확인이다.
- 해당 모델은 가장 작은 사이즈의 필터 (3x3)을 사용하고 16~19층의 CNN 아키텍쳐를 사용하며 SOTA 성능을 가진 모델이다.
기본 구조
- 입력 이미지 데이터는 224x244 RGB 이미지로 한정.
- 필터 사이즈는 가장 작은 3x3으로 통일.
- 해당 논문은 총 5개의 configuration를 소개하며 그 중 하나는 1x1 필터를 포함.
- 입력 정보를 그대로 넘기며 비선형성을 증가시키려는 목적.
- 합성곱 이전과 이후의 피처 맵 사이즈를 동일하게 유지하기 위해 Stride = 1, Padding = 1으로 고정한다.
- 최대 풀링은 총 5번 시행하며 사이즈는 2로 고정하며 풀링을 통과할 때 마다 피처 맵의 사이즈가 반으로 줄어들도록 한다.
- 이후 FC 레이어를 통과하여 4096개의 출력으로 만들고 다시 한번 4096개의 출력 노드를 갖는 FC 레이어를 통과한다.
- 이후 1000개의 출력 노드가 나오게끔 FC 레이어를 통과하고 softmax를 통해 확률 값으로 나타낸다.
- ImageNet 기준이라 1000개로 출력 노드 갯수를 잡았다.
- 각 층 사이에는 ReLU 활성화 함수를 사용한다.

Configurations

- VGGNet은 위와 같이 5개의 다른 구성을 가진 모델을 소개했다.
- A에서 E로 갈수록 층이 더 깊어지고 몇몇 구성은 LRN을 도입하거나 1x1 필터를 사용하였다.
- 참고로 1x1 필터를 사용하여 피처 맵을 건들지 않고 비선형성을 늘리는 아이디어는 ‘Network in Network’라는 논문에서 처음 제시된 개념이다.
- 코드 구현
cfgs = {
'A': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'B': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'D': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
'E': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}
class VGG(nn.Module):
def __init__(self, cfg, batch_norm, num_classes = 10, init_weights = True, drop_p = 0.5, pre_trained_path = pre_trained_path):
super().__init__()
self.features = self.make_layers(cfg, batch_norm)
self.avgpool = nn.AdaptiveAvgPool2d((7, 7))
self.classifier = nn.Sequential(nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(),
nn.Dropout(drop_p),
nn.Linear(4096, 4096),
nn.ReLU(),
nn.Dropout(drop_p),
nn.Linear(4096, num_classes))
if init_weights:
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.normal_(m.weight, 0, 1e-4)
if m.bias is not None:
nn.init.constant_(m.bias, 0)
if isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 1e-4)
nn.init.constant_(m.bias, 0)
else:
pass
def forward(self, x):
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x
def make_layers(self, cfg, batch_norm):
layers = []
in_channels = 3
for v in cfg:
if type(v) == int:
if batch_norm:
layers += [nn.Conv2d(in_channels, v, 3, stride = 1, padding = 1),
nn.BatchNorm2d(v),
nn.ReLU()]
else:
layers += [nn.Conv2d(in_channels, v, 3, stride = 1, padding = 1),
nn.ReLU()]
in_channels = v
else:
layers += [nn.MaxPool2d(2)]
return nn.Sequential(*layers)
- 다음과 같이 딕셔너리 형태로 configurations을 저장하고 make_layers 메서드를 이용하여 cfg에 원하는 구성 값을 입력해주면 자동으로 해당 구성에 맞게 모델이 만들어지도록 코드를 구현하였다.
- A-LRN과 C 같은 경우는 구현하지 않았다.
Training
- 손실함수는 CrossEntropy를 사용.
- 최적화 기법은 Mini-Batch SGD를 사용.
- Momentum = 0.9
- L2 Regularization = 5e-4
- FC Layer에서는 Dropout 기법 사용.
- Drop_p = 0.5
- LR 스케줄링은 validation loss가 더이상 줄어들지 않을 때 LR을 10으로 나누며 진행.
- 가중치 초기화는 $N(0, 0.01)$에서 랜덤하게 뽑아 진행.
- 편향값은 0으로 초기화.
- 빠른 학습과 local minimum에 빠지는 것을 방지하기 위해 pre-initalization 기법도 제안.
- A 모델에서 학습 된 파라미터로 초기화 하는 방법을 제시하였지만 구현은 하지 않았음.
- 논문에서 말하길 학습 속도와 성능 개선이 확실히 있다고 함.
def Train(model, train_DL, val_DL, criterion, optimizer,
EPOCH, BATCH_SIZE, TRAIN_RATIO,
save_model_path, save_history_path, **kwargs):
if "LR_STEP" in kwargs:
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min')
else:
scheduler = None
loss_history = {'train':[], 'val':[]}
acc_history = {'train':[], 'val':[]}
best_loss = 9999
for ep in range(EPOCH):
epoch_start = time.time()
current_lr = optimizer.param_groups[0]['lr']
print(f'Epoch: {ep+1}, current_LR = {current_lr}')
model.train()
train_loss, train_acc, _ = loss_epoch(model, train_DL, criterion, optimizer)
loss_history['train'] += [train_loss]
acc_history['train'] += [train_acc]
model.eval()
with torch.no_grad():
val_loss, val_acc, rcorrect = loss_epoch(model, val_DL, criterion, optimizer)
loss_history['val'] += [val_loss]
acc_history['val'] += [val_acc]
if val_loss < best_loss: #early_stopping
best_loss = val_loss
#optimizer도 같이 save하면 여기서 부터 재학습 가능
torch.save({'model': model,
'ep' : ep,
'optimizier' : optimizer,
'scheduler' : scheduler}, save_model_path)
if 'LR_STEP' in kwargs:
scheduler.step(val_loss)
#print loss
print(f'train loss: {round(train_loss, 5)}, '
f'val loss: {round(val_loss, 5)} \n'
f'train acc: {round(train_acc, 1)} %, '
f'val acc: {round(val_acc, 1)} %, num of valid correct: {rcorrect}, time: {round(time.time()-epoch_start)} s')
print('-'*20)
torch.save({'loss_history': loss_history,
'acc_history' : acc_history,
'EPOCH' : EPOCH,
'BATCH_SIZE' : BATCH_SIZE,
'TRAIN_RATIO' : TRAIN_RATIO}, save_history_path)
return loss_history, acc_history
def loss_epoch(model, DL, criterion, optimizer = None):
N = len(DL.dataset) # number of data
rloss = 0; rcorrect = 0
for x_batch, y_batch in tqdm(DL, leave = False):
#GPU 사용
x_batch = x_batch.to(DEVICE)
y_batch = y_batch.to(DEVICE)
#inference
y_hat = model(x_batch)
#loss
loss = criterion(y_hat, y_batch)
loss.requires_grad_(True)
#update
if optimizer is not None:
optimizer.zero_grad()
loss.backward()
optimizer.step()
#loss accumulation
#shape[0]을 하는 이유는 마지막 배치가 사이즈가 다를수도 있기 때문.
loss_b = loss.item() * x_batch.shape[0] # batch loss
rloss += loss_b
#accuracy accumulation
pred = y_hat.argmax(dim = 1)
corrects_b = torch.sum(pred == y_batch).item()
rcorrect += corrects_b
loss_e = rloss / N #epoch loss
accuracy_e = rcorrect/N * 100
Data Augmentation
- VGGNet의 입력 이미지 사이즈는 224x224이지만 먼저 S라는 크기로 Resize 진행.
- S는 224보다 큰 정수를 일컫음.
- 이후 224 사이즈로 FiveCrop을 진행.
- FiveCrop이란 왼쪽 위, 오른쪽 위, 중앙, 왼쪽 아래, 오른쪽 아래를 기준으로 Crop하는 것.
- 논문에선 Crop 이후 flip도 하며 데이터를 더 늘렸지만 코드 구현에서는 FiveCrop까지만 진행하고 RGB 값의 평균과 분산을 구해 Normalization도 진행을 해주었다.
# To normalize the dataset, calculate the mean and std
train_meanRGB = [np.mean(x.numpy(), axis=(1,2)) for x, _ in train_DS]
train_stdRGB = [np.std(x.numpy(), axis=(1,2)) for x, _ in train_DS]
train_meanR = np.mean([m[0] for m in train_meanRGB])
train_meanG = np.mean([m[1] for m in train_meanRGB])
train_meanB = np.mean([m[2] for m in train_meanRGB])
train_stdR = np.mean([s[0] for s in train_stdRGB])
train_stdG = np.mean([s[1] for s in train_stdRGB])
train_stdB = np.mean([s[2] for s in train_stdRGB])
val_meanRGB = [np.mean(x.numpy(), axis=(1,2)) for x, _ in val_DS]
val_stdRGB = [np.std(x.numpy(), axis=(1,2)) for x, _ in val_DS]
val_meanR = np.mean([m[0] for m in val_meanRGB])
val_meanG = np.mean([m[1] for m in val_meanRGB])
val_meanB = np.mean([m[2] for m in val_meanRGB])
val_stdR = np.mean([s[0] for s in val_stdRGB])
val_stdG = np.mean([s[1] for s in val_stdRGB])
val_stdB = np.mean([s[2] for s in val_stdRGB])
print(train_meanR, train_meanG, train_meanB)
print(val_meanR, val_meanG, val_meanB)
train_transformer = transforms.Compose([
transforms.Resize(256),
transforms.FiveCrop(224),
transforms.Lambda(lambda crops: torch.stack([transforms.ToTensor()(crop) for crop in crops])),
transforms.Normalize([train_meanR, train_meanG, train_meanB], [train_stdR, train_stdG, train_stdB]),
])
test_transformer = transforms.Compose([
transforms.ToTensor(),
transforms.Resize(224),
transforms.Normalize([train_meanR, train_meanG, train_meanB], [train_stdR, train_stdG, train_stdB]),
])
# apply transformation
train_DS.transform = train_transformer
val_DS.transform = test_transformer
test_DS.transform = test_transformer
- 즉, 데이터셋의 크기는 5배 증가하였다.
최종 코드 구현
- 첨부 파일 참고.