1. 概述
GoogLeNet模型于2014年提出,在ImageNet-1K数据集上达到了当时的SOTA,核心是Inception结构,既然不知道采用多大的卷积核 ,那就用好几种卷积核来提取特征最后再concat在一起。实现了比AlexNet性能更好,但只用了1/12的参数,因为去掉了全连接层,改用Average-Pool层,大大减小了模型的参数量大小。
在训练时,为了避免梯度消失,引入两个分类辅助头来加速训练,在inference阶段则无需用到这两个辅助分类器。相比于VGG-Net,模型更小,训练更快,且很快收敛。
论文地址 : https://arxiv.org/abs/1409.4842https://arxiv.org/abs/1409.4842
网络结构:
2. pytorch实现GoogLeNet
2.1 模型文件 model.py
import torch
import torch.nn as nn
import torch.nn.functional as F
class BasicConv2d(nn.Module):
"""
conv + ReLU
"""
def __init__(self, in_channels, out_channels, **kwargs):
super(BasicConv2d, self).__init__()
self.conv = nn.Conv2d(in_channels, out_channels, **kwargs)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
x = self.conv(x)
x = self.relu(x)
return x
class Inception(nn.Module):
"""
1个 Inception 结构,丢弃了FC层,改用AveragePool层,大大减小模型参数
"""
def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj):
super(Inception, self).__init__()
self.branch1 = BasicConv2d(in_channels, ch1x1, kernel_size=1)
self.branch2 = nn.Sequential(
BasicConv2d(in_channels, ch3x3red, kernel_size=1),
BasicConv2d(ch3x3red, ch3x3, kernel_size=3, padding=1))
self.branch3 = nn.Sequential(
BasicConv2d(in_channels, ch5x5red, kernel_size=1),
BasicConv2d(ch5x5red, ch5x5, kernel_size=5, padding=2))
self.branch4 = nn.Sequential(
nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
BasicConv2d(in_channels, pool_proj, kernel_size=1))
def forward(self, x):
branch1 = self.branch1(x)
branch2 = self.branch2(x)
branch3 = self.branch3(x)
branch4 = self.branch4(x)
outputs = [branch1, branch2, branch3, branch4]
return torch.cat(outputs, 1)
class InceptionAux(nn.Module):
"""
分类辅助头,为了避免梯度消失
"""
def __init__(self, in_channels, num_classes):
super(InceptionAux, self).__init__()
self.averagePool = nn.AvgPool2d(kernel_size=5, stride=3)
self.conv = BasicConv2d(in_channels, 128, kernel_size=1) # [batch, 128, 4, 4]
self.fc1 = nn.Linear(2048, 1024)
self.fc2 = nn.Linear(1024, num_classes)
def forward(self, x):
# aux1: [N, 512, 14, 14] aux2: [N, 528, 14, 14]
x = self.averagePool(x)
# aux1: [N, 512, 4, 4] aux2: [N, 528, 4, 4]
x = self.conv(x)
# [batch, 128, 4, 4]
x = torch.flatten(x, 1)
x = F.dropout(x, 0.5, training=self.training)
# [batch, 2048]
x = F.relu(self.fc1(x), inplace=True)
# [batch, 1024]
x = F.dropout(x, 0.5, training=self.training)
x = self.fc2(x)
# [batch, num_classes]
return x
class GoogLeNet(nn.Module):
def __init__(self, num_classes=1000, aux_logits=True, init_weights=False):
super(GoogLeNet, self).__init__()
self.aux_logits = aux_logits
self.conv1 = BasicConv2d(3, 64, kernel_size=7, stride=2, padding=3)
# ceil_mode 参数为控制输出结果的形状是否向上取整
self.maxpool1 = nn.MaxPool2d(3, stride=2, ceil_mode=True)
self.conv2 = BasicConv2d(64, 64, kernel_size=1)
self.conv3 = BasicConv2d(64, 192, kernel_size=3, padding=1)
self.maxpool2 = nn.MaxPool2d(3, stride=2, ceil_mode=True)
self.inception3a = Inception(192, 64, 96, 128, 16, 32, 32)
self.inception3b = Inception(256, 128, 128, 192, 32, 96, 64)
self.maxpool3 = nn.MaxPool2d(3, stride=2, ceil_mode=True)
self.inception4a = Inception(480, 192, 96, 208, 16, 48, 64)
self.inception4b = Inception(512, 160, 112, 224, 24, 64, 64)
self.inception4c = Inception(512, 128, 128, 256, 24, 64, 64)
self.inception4d = Inception(512, 112, 144, 288, 32, 64, 64)
self.inception4e = Inception(528, 256, 160, 320, 32, 128, 128)
self.maxpool4 = nn.MaxPool2d(3, stride=2, ceil_mode=True)
self.inception5a = Inception(832, 256, 160, 320, 32, 128, 128)
self.inception5b = Inception(832, 384, 192, 384, 48, 128, 128)
if self.aux_logits:
self.aux1 = InceptionAux(512, num_classes)
self.aux2 = InceptionAux(528, num_classes)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.dropout = nn.Dropout(0.4)
self.fc = nn.Linear(1024, num_classes)
if init_weights:
self._init_weights()
def forward(self, x):
# N x 3 x 224 x 224
x = self.conv1(x)
# N x 64 x 112 x 112
x = self.maxpool1(x)
# N x 64 x 56 x 56
x = self.conv2(x)
# N x 64 x 56 x 56
x = self.conv3(x)
# N x 192 x 56 x 56
x = self.maxpool2(x)
# N x 192 x 28 x 28
x = self.inception3a(x)
# N x 256 x 28 x 28
x = self.inception3b(x)
# N x 480 x 28 x 28
x = self.maxpool3(x)
# N x 480 x 14 x 14
x = self.inception4a(x)
# N x 512 x 14 x 14
if self.training and self.aux_logits: # eval model lose this layer
aux1 = self.aux1(x)
x = self.inception4b(x)
# N x 512 x 14 x 14
x = self.inception4c(x)
# N x 512 x 14 x 14
x = self.inception4d(x)
# N x 528 x 14 x 14
if self.training and self.aux_logits: # eval model lose this layer
aux2 = self.aux2(x)
x = self.inception4e(x)
# N x 832 x 14 x 14
x = self.maxpool4(x)
# N x 832 x 7 x 7
x = self.inception5a(x)
# N x 832 x 7 x 7
x = self.inception5b(x)
# N x 1024 x 7 x 7
x = self.avgpool(x)
# N x 1024 x 1 x 1
x = torch.flatten(x, 1)
# N x 1024
x = self.dropout(x)
x = self.fc(x)
# N x 1000 (num_classes)
if self.training and self.aux_logits: # eval model lose this layer
return x, aux2, aux1
return x
def _init_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)
2.2 训练文件 train.py
import json import sys sys.path.append('./../') import os import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets import torchvision.transforms as transforms from utils import train, test, show_result from model import GoogLeNet from tqdm import tqdm import warnings warnings.filterwarnings("ignore") def main(): data_transform = { "train": transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)) ]), "test": transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)) ]) } data_root = os.path.abspath(os.path.join(os.getcwd(), "../")) image_path = os.path.join(data_root, "data_set", "flower_data") assert os.path.exists(image_path), "{} path does not exist.".format(image_path) batch_size = 32 number_workers = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8]) print('----- Using {} dataloader workers every process. -----'.format(number_workers)) train_dataset = datasets.ImageFolder(root=os.path.join(image_path, "train"), transform=data_transform["train"]) train_num = len(train_dataset) train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=number_workers) test_dataset = datasets.ImageFolder(root=os.path.join(image_path, "val"), transform=data_transform["test"]) test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=number_workers) test_num = len(test_dataset) print('----- Using {} images for training, {} images for test. -----'.format(train_num, test_num)) flower_list = train_dataset.class_to_idx # {0: 'daisy', 1: 'dandelion', 2: 'roses', 3: 'sunflowers', 4: 'tulips'} class_dict = dict((val, key) for key, val in flower_list.items()) # 写入 json 文件,indent 参数为添加几个空格 json_str = json.dumps(class_dict, indent=4) with open('class_indices.json', 'w') as json_file: json_file.write(json_str) # ------------------------ 定义模型 ------------------------------------ net = GoogLeNet(num_classes=5, init_weights=True, aux_logits=True).cuda() loss_func = nn.CrossEntropyLoss().cuda() optimizer = optim.SGD(net.parameters(), lr=0.0001) loss_es = [] acc_es = [] best_acc = 0 epochs = 30 train_steps = len(train_loader) # --------------------- 开始训练 --------------------------------------- for epoch in range(epochs): net.train() running_loss = 0.0 train_bar = tqdm(train_loader, file=sys.stdout) for step, data in enumerate(train_bar): images, labels = data optimizer.zero_grad() logits, aux_logits2, aux_logits1 = net(images.cuda()) loss0 = loss_func(logits, labels.cuda()) loss1 = loss_func(aux_logits1, labels.cuda()) loss2 = loss_func(aux_logits2, labels.cuda()) loss = loss0 + loss1 * 0.3 + loss2 * 0.3 loss.backward() optimizer.step() running_loss += loss.item() train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1, epochs, loss) loss_es.append(running_loss / train_steps) # ---------------------- validate ------------------------------- net.eval() acc = 0.0 # accumulate accurate number / epoch with torch.no_grad(): val_bar = tqdm(test_loader, file=sys.stdout) for val_data in val_bar: val_images, val_labels = val_data outputs = net(val_images.cuda()) # eval model only have last output layer predict_y = torch.max(outputs, dim=1)[1] acc += torch.eq(predict_y, val_labels.cuda()).sum().item() val_accurate = acc / len(test_dataset) acc_es.append(val_accurate) print('--- [epoch %d] train_loss: %.3f val_accuracy: %.3f ---' % ( epoch + 1, running_loss / train_steps, val_accurate)) if val_accurate > best_acc: best_acc = val_accurate torch.save(net.state_dict(), './results/GoogLeNet-flower.pt') print('Finished Training') show_result(loss_es, acc_es, './results/GoogLeNet-flower.jpg') if __name__ == '__main__': main()
3. 训练结果
在flower数据集上训练30轮达到了70的准确率。