이번주에는 alexnet, vggnet, resnet 이라는 세개의 cnn 아키텍쳐를 비교해보고, 코드까지 작성하고 실행해보았습니다.
데이터는 torchvision의 CIFAR10 데이터를 사용했다.
from tqdm.notebook import tqdm
def training_model(model, data_loader, epochs):
since = time.time() # 총 소요 시간 계산을 위해시작 시간을 기록
best_model_wts = copy.deepcopy(model.state_dict()) # 모든 epoch에서 학습 된 모델 중 best model의 가중치를 저장하는 변수
best_acc = 0
for epoch in range(epochs):
avg_cost = 0
for phase in ['train', 'val']: # train mode와 validation mode 순서로 진행
if phase == 'train':
model.train() # model을 training mode로
else:
model.eval() # model을 validation mode로
for X, Y in tqdm(data_loader[phase]):
X, Y = X.to(device), Y.to(device)
optimizer.zero_grad() # 지난 iteration에서 계산했던 기울기 초기화
with torch.set_grad_enabled(phase == 'train'): # training mode에서는 gradient를 기억하여 가중치를 수정해야 함
# validation mode에서는 가중치를 수정하지 않으므로 필요 없음
hypothesis = model(X) # 순전파 과정으로 예측값 도출
cost = criterion(hypothesis, Y) # 예측값과 실제값을 비교한 loss
if phase == 'train':
cost.backward() # 역전파, 기울기 계산
optimizer.step() # optimizer로 가중치 갱신
avg_cost += cost / total_batch[phase]
else: # validation에서는 역전파로 가중치를 학습할 필요 없음
prediction = model(X) # 학습한 모델로 test 데이터의 예측값 도출(각 숫자에 해당할 확률)
prediction = prediction.to(device)
correct_prediction = torch.argmax(prediction, 1) == Y # 모든 확률 중에서 가장 큰 확률을 가진 숫자를 예측값으로 지정하고 이를 실제값과 비교
global accuracy
accuracy = correct_prediction.float().mean().item() # 정확도 계산
if phase == 'train':
print('[Epoch: {:>4}] train cost = {:.4f}'.format(epoch + 1, avg_cost)) # training 과정에서 각 epoch 마다의 cost 출력
else:
print('val Accuracy = {:.4f}', accuracy) # validation 과정에서 각 epoch에서 학습된 모델의 성능 출력
print('='*100)
if phase == 'val' and accuracy > best_acc: # 이번 epoch에서 만들어진 모델이 이전에 만들어진 모델보다 더 성능이 좋을 경우, best accuracy을 수정
best_acc = accuracy
best_model_wts = copy.deepcopy(model.state_dict())
training_time = time.time() - since # 학습 경과 시간
print('total training time for {} epochs: {:.0f}m {:.0f}s'.format(epochs, training_time//60, training_time%60))
print('Best val Accuracy: {:.4f}'.format(best_acc))
model.load_state_dict(best_model_wts)
return model # best model 반환
데이터의 양을 늘리기 위해 원본에 각종 변환을 적용하여 개수를 증강시키는 기법
# Data Augmentation
transform = transforms.Compose([
transforms.Resize(256),
transforms.RandomCrop(227),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) #3채널(R,G,B) 데이터의 평균과 분산을 0.5로 설정
])
# 예시속은 CIFAR-10 dataset을 사용했습니다.
batch_size = 64
cifar_train = datasets.CIFAR10('~/.data', download=True, train=True, transform=transform)
train_loader = torch.utils.data.DataLoader(cifar_train, batch_size=batch_size, shuffle=True) #batch_size는 원하는 크기로 변경 가능.
#단, 2의 제곱수로 하고 test_loader의 batch_size도 동일한 수로 변경할 것.
cifar_test = datasets.CIFAR10('~/.data', download=True, train=False, transform=transform)
test_loader = torch.utils.data.DataLoader(cifar_test, batch_size=batch_size, shuffle=True)
data_loaders = {'train' : train_loader, 'val': test_loader}
total_batch = {'train' : len(train_loader), 'val': len(test_loader)}
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
# 클래스 총 10개 지정
from torchvision import utils
import matplotlib.pyplot as plt
import numpy as np
dataiter = iter(train_loader) # iter함수로 iteration 객체 가져오기
images, labels = next(dataiter)
첫번째 레이어(컨볼루션 레이어)
→ Result: 27 x 27 x 96 size의 특성맵
# 1st Conv layer
nn.Conv2d(in_channels=3,
out_channels=96,
kernel_size=11,
padding=0,
stride=4
),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2), # overlap pooling, 55->27
nn.LocalResponseNorm(size=5, alpha=0.0001, beta=0.75, k=2),
두번째 레이어(컨볼루션 레이어)
→ Result: 13 x 13 x 256 size의 특성맵
# 2nd Conv layer
nn.Conv2d(96, 256, 5, padding=2, stride=1), # 27->27
nn.ReLU(inplace=True),
nn.LocalResponseNorm(size=5, alpha=0.0001, beta=0.75, k=2),
nn.MaxPool2d(kernel_size=3, stride=2), # 27->13
세번째 레이어(컨볼루션 레이어)
→ Result: 13 x 13 x 384 size의 특성맵
# 3rd Conv layer
nn.Conv2d(256, 384, 3, padding=1, stride=1), # 13->13
nn.ReLU(inplace=True),
네번째 레이어(컨볼루션 레이어)
→ Result: 13 x 13 x 384 size의 특성맵
# 4th Conv layer
nn.Conv2d(384, 384, 3, padding=1, stride=1), # 13->13
nn.ReLU(inplace=True),
다섯번째 레이어(컨볼루션 레이어)
→ Result: 6 x 6 x 256 size의 특성맵
# 5th Conv layer
nn.Conv2d(384, 256, 3, padding=1, stride = 1), # 13->13
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2), # 13->6
여섯번째 레이어(Fully connected layer):
# 1st Fully connected layer
nn.Linear(in_features=(256 * 6 * 6), out_features=4096),
nn.ReLU(inplace=True),
nn.Dropout(p=0.5),
일곱번째 레이어(Fully connected layer):
nn.Linear(in_features=4096, out_features=4096),
nn.ReLU(inplace=True),
nn.Dropout(p=0.5),
여덟번째 레이어(Fully connected layer):
nn.Linear(in_features=4096, out_features=num_classes),
representation depth is beneficial for the classification accuracy, and that state-of-the-art performance on the ImageNet challenge dataset can be achieved using a conventional ConvNet architecture with substantially increased depth.
def conv_2_block(in_features, out_features):
net = nn.Sequential(
nn.Conv2d(in_features, out_features, kernel_size = 3, padding = 1),
nn.ReLU(),
nn.Conv2d(out_features, out_features, kernel_size = 3, padding = 1),
nn.ReLU(),
nn.MaxPool2d(2, 2)
)
return net
def conv_3_block(in_features, out_features):
net = nn.Sequential(
nn.Conv2d(in_features, out_features, kernel_size = 3, padding = 1),
nn.ReLU(),
nn.Conv2d(out_features, out_features, kernel_size = 3, padding = 1),
nn.ReLU(),
nn.Conv2d(out_features, out_features, kernel_size = 3, padding = 1),
nn.ReLU(),
nn.MaxPool2d(2, 2)
)
return net
def vgg_block(num_convs, in_channels, out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.Conv2d(in_channels, out_channels, kernel_size = 3, padding = 1))
layers.append(nn.ReLU(inplace = True))
in_channels = out_channels
layers.append(nn.MaxPool2d(kernel_size = 2, stride = 2))
return nn.Sequential(*layers)
1층
1차 Conv2d
64개의 3 x 3 x 3 필터커널, padding=1, stride=1
→ 결과적으로 64장의 224 x 224 특성맵(224 x 224 x 64)들이 생성됩니다.
ReLU 함수 적용
2차 Conv2d
****64개의 3 x 3 x 64 필터커널, padding=1, stride=1
→ 결과적으로 64장의 224 x 224 특성맵들(224 x 224 x 64)이 생성됩니다.
ReLU 함수 적용
Max pooling을 사용하여 특성맵의 사이즈를 112 X 112 X 64로 줄입니다.
conv_2_block(3, 64), # 3->64
2층
1차 Conv2d
256개의 3 x 3 x 128 필터커널, padding=1, stride=1
→ 결과적으로 256장의 56 x 56 특성맵(56 x 56 x 256)들이 생성됩니다.
ReLU 함수 적용
2차 Conv2d
256개의 3 x 3 x 256 필터커널, padding=1, stride=1
→ 결과적으로 256장의 56 x 56특성맵들(56 x 56 x 256)이 생성됩니다.
ReLU 함수 적용
3차 Conv2d (2차와 동일)
256개의 3 x 3 x 256 필터커널, padding=1, stride=1
→ 결과적으로 256장의 56 x 56특성맵들(56 x 56 x 256)이 생성됩니다.
ReLU 함수 적용
Max pooling을 사용하여 특성맵의 사이즈를 28 X 28 X 256로 줄입니다.
conv_3_block(128, 256), # 128->256
3층
1차 Conv2d
512개의 3 x 3 x 256 필터커널, padding=1, stride=1
→ 결과적으로 512장의 28 x 28 특성맵(28 x 28 x 512)들이 생성됩니다.
ReLU 함수 적용
2차 Conv2d
512개의 3 x 3 x 512 필터커널, padding=1, stride=1
→ 결과적으로 512장의 28 x 28특성맵들(28 x 28 512)이 생성됩니다.
ReLU 함수 적용
3차 Conv2d (2차와 동일)
512개의 3 x 3 x 512 필터커널, padding=1, stride=1
→ 결과적으로 512장의 28 x 28특성맵들(28 x 28 512)이 생성됩니다.
ReLU 함수 적용
Max pooling을 사용하여 특성맵의 사이즈를 14 X 14 X 512로 줄입니다.
conv_3_block(256, 512), # 256->512
4층
1차 Conv2d
512개의 3 x 3 x 512 필터커널, padding=1, stride=1
→ 결과적으로 512장의 14 x 14 특성맵(14 x 14 x 512)들이 생성됩니다.
ReLU 함수 적용
2차 Conv2d
512개의 3 x 3 x 512 필터커널, padding=1, stride=1
→ 결과적으로 512장의 14 x 14 특성맵(14 x 14 x 512)들이 생성됩니다.
ReLU 함수 적용
3차 Conv2d (2차와 동일)
512개의 3 x 3 x 512 필터커널, padding=1, stride=1
→ 결과적으로 512장의 14 x 14 특성맵(14 x 14 x 512)들이 생성됩니다.
ReLU 함수 적용
Max pooling을 사용하여 특성맵의 사이즈를 7 X 7 X 512로 줄입니다.
conv_3_block(512, 512), # 512->512
5층(FC 1차)
nn.Linear(512*4*4, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
6층(FC 2차)
nn.Linear(4096, 1000),
nn.ReLU(inplace=True),
nn.Dropout(),
7층(FC 3차)
nn.Linear(1000, num_classes),
def conv_block_1(in_features, out_features, stride=1):
net = nn.Sequential(
nn.Conv2d(in_features, out_features, kernel_size=1, stride = stride),
nn.BatchNorm2d(out_features),
nn.ReLU(),
)
return net
def conv_block_3(in_features, out_features, stride=1):
net = nn.Sequential(
nn.Conv2d(in_features, out_features, kernel_size = 3, stride = stride, padding = 1),
nn.BatchNorm2d(out_features),
nn.ReLU(),
)
return net
class BottleNeck(nn.Module):
def __init__(self, in_features, mid_features, out_features, down=False):
super(BottleNeck, self).__init__()
self.down=down
# feature map 크기가 감소하는 경우
if self.down:
self.layer = nn.Sequential(
conv_block_1(in_features, mid_features, stride=2),
conv_block_3(mid_features, mid_features, stride=1),
conv_block_1(mid_features, out_features, stride=1),
)
self.downsample = nn.Conv2d(in_features, out_features, kernel_size=1, stride=2)
# feature map의 크기가 유지되는 경우
else:
self.layer = nn.Sequential(
conv_block_1(in_features, mid_features, stride=1),
conv_block_3(mid_features, mid_features, stride=1),
conv_block_1(mid_features, out_features, stride=1),
)
self.dim_equalizer = nn.Conv2d(in_features, out_features, kernel_size=1)
def forward(self, x):
if self.down:
downsample = self.downsample(x)
output = self.layer(x)
output = output + downsample
else:
output = self.layer(x)
if x.size() is not output.size():
x = self.dim_equalizer(x)
output = output + x
return output
class ResNet(nn.Module):
def __init__(self, num_classes=10):
super(ResNet, self).__init__()
self.layer_1 = nn.Sequential(
nn.Conv2d(3,64,kernel_size = 7, stride = 2, padding=3),
nn.ReLU(),
nn.MaxPool2d(3,2,1),
)
self.layer_2 = nn.Sequential(
BottleNeck(64,64,256),
BottleNeck(256,64,256),
BottleNeck(256,64,256,down=True),
)
self.layer_3 = nn.Sequential(
BottleNeck(256,128,512),
BottleNeck(512,128,512),
BottleNeck(512,128,512),
BottleNeck(512,128,512,down=True),
)
self.layer_4 = nn.Sequential(
BottleNeck(512,256,1024),
BottleNeck(1024,256,1024),
BottleNeck(1024,256,1024),
BottleNeck(1024,256,1024),
BottleNeck(1024,256,1024),
BottleNeck(1024,256,1024,down=True),
)
self.layer_5 = nn.Sequential(
BottleNeck(1024,512,2048),
BottleNeck(2048,512,2048),
BottleNeck(2048,512,2048),
)
self.avgpool = nn.AvgPool2d(1,1)
self.fc_layer = nn.Linear(2048,num_classes)
def forward(self, x):
output = self.layer_1(x)
output = self.layer_2(output)
output = self.layer_3(output)
output = self.layer_4(output)
output = self.layer_5(output)
output = self.avgpool(output)
output = output.view(batch_size,-1)
output = self.fc_layer(output)
return output