深度学习入门系列:VGG、NiN、GoogleNet
深度学习中的经典卷积神经网络架构解析
今天,我们将通过三个经典的 CNN 架构——VGG、NiN 和 GoogLeNet,来了解如何设计强大的深度学习模型。
使用重复元素的网络(VGG)
1 VGG网络
VGG块:

VGG块的组成规律是:连续使⽤数个相同的填充为1、窗口形状为3 ×3的卷积层后接上⼀个步幅为2、窗口形状为2 ×2的最⼤池化层。卷积层保持输⼊的⾼和宽不变,而池化层则对其减半。
从李沐大神的《动手学深度学习》中有提到,VGG块的实现为:
import d2lzh as d2l
from mxnet import gluon, init, nd
from mxnet.gluon import nn
def vgg_block(num_convs, num_channels):
    blk = nn.Sequential()
    for _ in range(num_convs):
        blk.add(nn.Conv2D(num_channels, kernel_size=3, padding=1, activation='relu'))
    blk.add(nn.MaxPool2D(pool_size=2, strides=2))
    return blk
而我们经常用torch的话,可以这样实现:
import torch
import torch.nn as nn
def vgg_block(in_channels, out_channels, num_convs, kernel_size=3, stride=1, padding=1):
    layers = []
    for _ in range(num_convs):
        layers.append(nn.Conv2d(in_channels, out_channels, kernel_size=kernel_size, stride=stride, padding=padding))
        layers.append(nn.ReLU(inplace=True))
        in_channels = out_channels
    layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
    return nn.Sequential(*layers)
# 创建一个包含2个卷积层的VGG块
vgg_block_example = vgg_block(in_channels=64, out_channels=128, num_convs=2)
# 打印VGG块的结构
print(vgg_block_example)
而VGG网络则是通过多个 VGG 块堆叠而成,常见的结构是 VGG-16 和 VGG-19,分别表示包含 16 和 19 层可训练参数的网络。
VGG16的网络结构如图所示:

网络的具体设计如下:
- • 前几层主要用于提取低级特征(如边缘和纹理)。
 - • 后几层则关注更高级的特征(如物体的形状和轮廓)。
 - • 最后通过全连接层和 Softmax 层实现分类。
 
VGGNet-16由13个卷积层和3个全连接层组成。下面我会列出每一层的具体信息,可能有点长,这个其实作为了解就够了:
第一层卷积的输入图像大小为224×224×3,使用64个大小为3×3、步长为1、填充为1的卷积核,输出特征图大小为224×224×64,接着应用ReLU激活函数。
第二层卷积的输入为224×224×64,使用64个大小为3×3、步长为1、填充为1的卷积核,输出特征图大小为224×224×64,再应用ReLU激活函数,随后进行最大池化,使用2×2大小的池化核、步长为2、填充为0,最终输出112×112×64。
第三层卷积的输入为112×112×64,使用128个大小为3×3、步长为1、填充为1的卷积核,输出特征图大小为112×112×128,随后应用ReLU激活函数。
第四层卷积的输入为112×112×128,使用128个大小为3×3、步长为1、填充为1的卷积核,输出特征图大小为112×112×128,应用ReLU后进行最大池化,池化核大小为2×2、步长为2、填充为0,最终输出56×56×128。
第五层卷积的输入为56×56×128,使用256个大小为3×3、步长为1、填充为1的卷积核,输出特征图大小为56×56×256,随后应用ReLU激活函数。
第六层卷积的输入为56×56×256,使用256个大小为3×3、步长为1、填充为1的卷积核,输出特征图大小为56×56×256,再应用ReLU激活函数。
第七层卷积的输入为56×56×256,使用256个大小为3×3、步长为1、填充为1的卷积核,输出特征图大小为56×56×256,经过ReLU激活后进行最大池化,池化核大小为2×2、步长为2、填充为0,最终输出28×28×256。
第八层卷积的输入为28×28×256,使用512个大小为3×3、步长为1、填充为1的卷积核,输出特征图大小为28×28×512,随后应用ReLU激活函数。
第九层卷积的输入为28×28×512,使用512个大小为3×3、步长为1、填充为1的卷积核,输出特征图大小为28×28×512,接着应用ReLU激活函数。
第十层卷积的输入为28×28×512,使用512个大小为3×3、步长为1、填充为1的卷积核,输出特征图大小为28×28×512,应用ReLU后进行最大池化,池化核大小为2×2、步长为2、填充为0,最终输出14×14×512。
第十一层卷积的输入为14×14×512,使用512个大小为3×3、步长为1、填充为1的卷积核,输出特征图大小为14×14×512,随后应用ReLU激活函数。
第十二层卷积的输入为14×14×512,使用512个大小为3×3、步长为1、填充为1的卷积核,输出特征图大小为14×14×512,再应用ReLU激活函数。
第十三层卷积的输入为14×14×512,使用512个大小为3×3、步长为1、填充为1的卷积核,输出特征图大小为14×14×512,应用ReLU后进行最大池化,池化核大小为2×2、步长为2、填充为0,最终输出7×7×512。
VGG16中的13个卷积层均采用大小为3×3、步长为1、填充为1的卷积核,而5次最大池化操作均使用大小为2×2、步长为2、填充为0的池化核。
VGG 的显著特点是结构简单,所有卷积层的参数大小都相同。这种一致性让它易于理解和实现。
2 VGG网络的实现
接下来我基于torch来实现VGG网络:
import torch
import torch.nn as nn
# 定义VGG16和VGG19的配置,数字代表输出通道,M代表池化层
cfgs = {
    'VGG16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
    'VGG19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}
# 添加模型层
def make_layers(cfg, batch_norm=False):
    layers = []
    in_channels = 3
    for v in cfg:
        if v == 'M':
            layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
        else:
            conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
            if batch_norm:
                layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
            else:
                layers += [conv2d, nn.ReLU(inplace=True)]
            in_channels = v
    return nn.Sequential(*layers)
# 定义VGG模型
class VGG(nn.Module):
    def __init__(self, features, num_classes=1000):
        super(VGG, self).__init__()
        self.features = features
        self.classifier = nn.Sequential(
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(4096, num_classes),
        )
    def forward(self, x):
        x = self.features(x)
        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x
# 创建VGG16和VGG19模型实例
def vgg_model(model_name='VGG16', num_classes=1000, batch_norm=False):
    cfg = cfgs[model_name]
    model = VGG(make_layers(cfg, batch_norm=batch_norm), num_classes=num_classes)
    return model
# 创建VGG16模型实例
model_vgg16 = vgg_model('VGG16')
print(model_vgg16)
# 创建VGG19模型实例
model_vgg19 = vgg_model('VGG19')
print(model_vgg19)
在训练 VGG 网络时,有几个常用的处理方式:
- 1. 数据预处理
- • 对图像进行归一化处理,使像素值在 0 到 1 之间。
 - • 数据增强,如随机裁剪和水平翻转。
 
 
transform = transforms.Compose([
    transforms.RandomResizedCrop(224),  # 随机裁剪到224x224
    transforms.RandomHorizontalFlip(),  # 随机水平翻转
    transforms.ToTensor(),  # 将图像转换为张量
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),  # 归一化处理
])
- 1. 训练技巧:
- • 使用小批量梯度下降法(Mini-batch SGD)优化。
 - • 设置较低的学习率,并配合动量提升优化效果。
 
 - 2. 硬件需求:
- • VGG 网络的参数量较大,需要显存较高的 GPU。
 
 
训练代码可以参考我下面的这部分代码:
# 加载数据集
train_dataset = ImageFolder(root='./data/train', transform=transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4)
val_dataset = ImageFolder(root='./data/val', transform=transform)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4)
# 初始化模型、损失函数和优化器
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = vgg_model(model_name='VGG16', num_classes=1000).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
# 定义训练函数
def train_model(model, train_loader, val_loader, criterion, optimizer, num_epochs=25, device='cuda'):
    since = time.time()
    best_acc = 0.0
    writer = SummaryWriter()
    for epoch in range(num_epochs):
        print(f'Epoch {epoch}/{num_epochs - 1}')
        print('-' * 10)
        # 训练阶段:计算损失、反向传播、更新权重。
        model.train()
        running_loss = 0.0
        running_corrects = 0
        for inputs, labels in train_loader:
            inputs = inputs.to(device)
            labels = labels.to(device)
            optimizer.zero_grad()
            with torch.set_grad_enabled(True):
                outputs = model(inputs)
                _, preds = torch.max(outputs, 1)
                loss = criterion(outputs, labels)
                loss.backward()
                optimizer.step()
            running_loss += loss.item() * inputs.size(0)
            running_corrects += torch.sum(preds == labels.data)
        epoch_loss = running_loss / len(train_loader.dataset)
        epoch_acc = running_corrects.double() / len(train_loader.dataset)
        writer.add_scalar('Loss/train', epoch_loss, epoch)
        writer.add_scalar('Accuracy/train', epoch_acc, epoch)
        print(f'Train Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}')
        # 验证阶段:计算验证集上的损失和准确率。
        model.eval()
        val_running_loss = 0.0
        val_running_corrects = 0
        for inputs, labels in val_loader:
            inputs = inputs.to(device)
            labels = labels.to(device)
            with torch.set_grad_enabled(False):
                outputs = model(inputs)
                _, preds = torch.max(outputs, 1)
                loss = criterion(outputs, labels)
            val_running_loss += loss.item() * inputs.size(0)
            val_running_corrects += torch.sum(preds == labels.data)
        val_epoch_loss = val_running_loss / len(val_loader.dataset)
        val_epoch_acc = val_running_corrects.double() / len(val_loader.dataset)
        writer.add_scalar('Loss/val', val_epoch_loss, epoch)
        writer.add_scalar('Accuracy/val', val_epoch_acc, epoch)
        print(f'Val Loss: {val_epoch_loss:.4f} Acc: {val_epoch_acc:.4f}')
        # 保存最佳模型
        if val_epoch_acc > best_acc:
            best_acc = val_epoch_acc
            torch.save(model.state_dict(), 'best_model.pth')
        print()
# 开始训练
train_model(model, train_loader, val_loader, criterion, optimizer, num_epochs=25, device=device)
尽管 VGG 的性能不错,但它的计算成本和存储需求较高,并且现在有了性能更好的其他模型作为替代,VGG现在还是作为学习多了解即可。
网络中的网络(NiN)
1 NiN块

NiN全名叫做Network in Network,通过引入全局思维解决了传统 CNN 模型中局部性强的问题。NiN 块的核心是用 1×1 卷积层替代全连接层:
- • 局部特征处理:使用普通卷积层提取局部特征。
 - • 非线性组合:通过 1×1 卷积实现通道间的特征重组,相当于给每个像素点添加一个“小型的全连接网络”。
 - • 参数减少:相比大尺寸卷积层,1×1 卷积显著降低了参数数量。
 
在花书中,nin块的实现是这样的:
import d2lzh as d2l
from mxnet import gluon, init, nd
from mxnet.gluon import nn
def nin_block(num_channels, kernel_size, strides, padding):
    blk = nn.Sequential()
    blk.add(nn.Conv2D(num_channels, kernel_size,strides, padding, activation='relu'),
            nn.Conv2D(num_channels, kernel_size=1, activation='relu'),
            nn.Conv2D(num_channels, kernel_size=1, activation='relu'))
    return blk
NiN 块一般由三个主要部分组成:
- 1. 常规卷积层(提取特征)。
 - 2. 1×1 卷积层(非线性组合)。
 - 3. ReLU 激活函数(引入非线性)。
 
2 NiN模型
NiN 模型是由多个 NiN 块堆叠而成,通常在块之间插入最大池化层来压缩特征:
- 1. 全局平均池化(Global Average Pooling):
- • 在 NiN 网络中,用全局平均池化替代了全连接层。
 - • 每个类别的得分由对应特征图的平均值直接给出,减少了参数量。
 
 - 2. 网络结构:
- • 前几层为标准的 NiN 块。
 - • 中间插入池化层减少特征图尺寸。
 - • 最后一层是全局平均池化。
 
 
通过这种设计,NiN 不仅提升了计算效率,还减轻了过拟合风险。
因此我们可以得到NiN模型的简单实现如下:
net = nn.Sequential()
net.add(nin_block(96, kernel_size=11, strides=4, padding=0),
        nn.MaxPool2D(pool_size=3, strides=2),
        nin_block(256, kernel_size=5, strides=1, padding=2),
        nn.MaxPool2D(pool_size=3, strides=2),
        nin_block(384, kernel_size=3, strides=1, padding=1),
        nn.MaxPool2D(pool_size=3, strides=2), nn.Dropout(0.5),
        # 标签类别数是10
        nin_block(10, kernel_size=3, strides=1, padding=1),
        # 全局平均池化层将窗⼝形状⾃动设置成输⼊的⾼和宽
        nn.GlobalAvgPool2D(),
        # 将四维的输出转成⼆维的输出,其形状为(批量⼤⼩, 10)
        nn.Flatten())
3 训练模型
训练 NiN 模型时与 VGG 相似,但 NiN 由于参数更少,对硬件要求稍低:
- 1. 数据预处理:
- • 将输入数据归一化到标准范围内。
 - • 数据增强以提升泛化能力。
 
 - 2. 训练注意事项:
- • NiN 对初始参数较敏感,可以尝试多种初始化方式。
 - • 控制学习率的衰减,避免梯度爆炸或消失。
 
 - 3. 适用场景:
- • NiN 更适合轻量化模型需求,比如移动端和嵌入式设备。
 
 
NiN 的创新点在于将全局信息和局部信息结合,但受限于设计思想,NiN 的表达能力与后续更复杂的模型相比还是有所不足。当然,作为经典的网络模型架构,还是值得我们一学,作为了解即可
含并行连接的网络(GoogLeNet)
1 Inception块

GoogLeNet 的核心是 Inception 块,通过多分支的并行计算从多种尺度提取特征。每个 Inception 块包括:
- 1. 多种卷积核:
- • 使用不同尺寸(如 1×1、3×3、5×5)的卷积核提取特征。
 - • 小卷积核捕捉细节,大卷积核捕捉全局信息。
 
 - 2. 降维处理:
- • 在 3×3 和 5×5卷积前加入 1×1 卷积进行降维,减少计算量。
 
 - 3. 最大池化分支:
- • 使用最大池化提取空间信息,补充卷积分支的局部特征。
 
 
通过以上设计,Inception 块实现了高效的多尺度特征提取。下面是使用torch对Inception块的实现:
class Inception(nn.Module):
    def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj):
        super(Inception, self).__init__()
        # 1x1卷积路径
        self.branch1 = nn.Sequential(
            nn.Conv2d(in_channels, ch1x1, kernel_size=1),
            nn.ReLU(inplace=True)
        )
        # 1x1卷积 + 3x3卷积路径
        self.branch2 = nn.Sequential(
            nn.Conv2d(in_channels, ch3x3red, kernel_size=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(ch3x3red, ch3x3, kernel_size=3, padding=1),
            nn.ReLU(inplace=True)
        )
        # 1x1卷积 + 5x5卷积路径
        self.branch3 = nn.Sequential(
            nn.Conv2d(in_channels, ch5x5red, kernel_size=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(ch5x5red, ch5x5, kernel_size=5, padding=2),
            nn.ReLU(inplace=True)
        )
        # 3x3最大池化 + 1x1卷积路径
        self.branch4 = nn.Sequential(
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
            nn.Conv2d(in_channels, pool_proj, kernel_size=1),
            nn.ReLU(inplace=True)
        )
    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)
2 GoogLeNet模型

GoogLeNet 是由多个 Inception 块堆叠而成的深度网络,并结合一些特殊设计:
- 1. 深度更深:
- • GoogLeNet 包括 22 层,比 VGG 深得多。
 - • 利用 Inception 块保持计算效率,使得深度增加不会显著提高计算成本。
 
 - 2. 辅助分类器:
- • 在中间层加入两个辅助分类器,用于缓解梯度消失问题。
 - • 辅助分类器的损失与主分类器损失加权求和。
 
 - 3. 减少参数:
- • Inception 块的设计减少了全连接层中的参数。
 - • GoogLeNet 参数量仅为 VGG 的 1/12。
 
 
因此googlenet的实现也比刚才提到的两个模型架构要稍微复杂一丢丢,用torch的实现方式如下:
class GoogLeNet(nn.Module):
    def __init__(self, num_classes=1000):
        super(GoogLeNet, self).__init__()
        # 初始卷积层
        self.conv1 = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1),
            nn.LocalResponseNorm(size=5, alpha=0.0001, beta=0.75, k=1)
        )
        # 第二卷积层
        self.conv2 = nn.Sequential(
            nn.Conv2d(64, 64, kernel_size=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 192, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.LocalResponseNorm(size=5, alpha=0.0001, beta=0.75, k=1),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        )
        # Inception模块
        self.inception3a = Inception(192, 64, 96, 128, 16, 32, 32)
        self.inception3b = Inception(256, 128, 128, 192, 32, 96, 64)
        self.maxpool3 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        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(kernel_size=3, stride=2, padding=1)
        self.inception5a = Inception(832, 256, 160, 320, 32, 128, 128)
        self.inception5b = Inception(832, 384, 192, 384, 48, 128, 128)
        # 辅助分类器
        self.aux1 = nn.Sequential(
            nn.AvgPool2d(kernel_size=5, stride=3),
            nn.Conv2d(512, 128, kernel_size=1),
            nn.ReLU(inplace=True),
            nn.Flatten(),
            nn.Linear(2048, 1024),
            nn.ReLU(inplace=True),
            nn.Dropout(0.7),
            nn.Linear(1024, num_classes)
        )
        self.aux2 = nn.Sequential(
            nn.AvgPool2d(kernel_size=5, stride=3),
            nn.Conv2d(528, 128, kernel_size=1),
            nn.ReLU(inplace=True),
            nn.Flatten(),
            nn.Linear(2048, 1024),
            nn.ReLU(inplace=True),
            nn.Dropout(0.7),
            nn.Linear(1024, num_classes)
        )
        # 最终分类器
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.dropout = nn.Dropout(0.4)
        self.fc = nn.Linear(1024, num_classes)
    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = self.inception3a(x)
        x = self.inception3b(x)
        x = self.maxpool3(x)
        x = self.inception4a(x)
        if self.training:
            aux1 = self.aux1(x)
        x = self.inception4b(x)
        x = self.inception4c(x)
        x = self.inception4d(x)
        if self.training:
            aux2 = self.aux2(x)
        x = self.inception4e(x)
        x = self.maxpool4(x)
        x = self.inception5a(x)
        x = self.inception5b(x)
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.dropout(x)
        x = self.fc(x)
        if self.training:
            return x, aux1, aux2
        else:
            return x
model = GoogLeNet(num_classes=1000)
print(model)
看着很长,其实没有那么复杂(你别忘了现在的模型可比这些复杂多了)。GoogLeNet 的训练过程更复杂,但效率较高:
- 1. 数据预处理:
- • 和前述方法类似,主要针对输入图像进行归一化和数据增强。
 
 - 2. 训练优化:
- • 使用辅助分类器的损失调整梯度传播。
 - • 采用更高效的优化算法(如 Adam 或 RMSprop)。
 
 
尽管 GoogLeNet 的设计独特,但其复杂性较高,后来被更现代的架构(如 ResNet)所取代。
写在最后
VGG、NiN 和 GoogLeNet 是深度学习发展过程中具有里程碑意义的模型。它们的设计理念各有侧重:
- • VGG:通过堆叠小卷积核构建深度网络,强调一致性。
 - • NiN:利用 1×1 卷积进行特征重组,简化参数。
 - • GoogLeNet:通过 Inception 块实现多尺度特征提取和高效计算。
 
这些网络的诞生不仅提升了图像分类的精度,还为后续的深度学习模型奠定了基础。虽然现在有了更好的模型更好的架构,但是无论是学习经典架构还是设计新模型,理解这些网络的设计思想都是至关重要的。
本文章转载微信公众号@Chal1ceAI
热门API
- 1. AI文本生成
 - 2. AI图片生成_文生图
 - 3. AI图片生成_图生图
 - 4. AI图像编辑
 - 5. AI视频生成_文生视频
 - 6. AI视频生成_图生视频
 - 7. AI语音合成_文生语音
 - 8. AI文本生成(中国)
 
最新文章
- 如何实现Mock API以进行API测试 | Zuplo博客
 - 解读 TaskMatrix.AI
 - API协议设计的10种技术
 - ComfyUI API是什么:深入探索ComfyUI的API接口与应用
 - 从架构设计侧剖析: MCP vs A2A 是朋友还是对手?
 - Kimi Chat API入门指南:从注册到实现智能对话
 - 免费查询公司注册信息API的使用指南
 - 防御 API 攻击:保护您的 API 和数据的策略
 - 香港支付宝实名认证:是什么?怎么用?
 - 如何获取 Coze开放平台 API 密钥(分步指南)
 - 如何保护您的API免受自动化机器人和攻击 | Zuplo博客
 - ASP.NET Core Minimal APIs 入门指南 – JetBrains 博客