赞
踩
图像分类是根据图像的语义信息对不同类别图像进行区分,是计算机视觉的核心,是物体检测、图像分割、物体跟踪、行为分析、人脸识别等其他高层次视觉任务的基础。图像分类在许多领域都有着广泛的应用,如:安防领域的人脸识别和智能视频分析等,交通领域的交通场景识别,互联网领域基于内容的图像检索和相册自动归类,医学领域的图像识别等。
上一节主要介绍了卷积神经网络常用的一些基本模块,本节将对图像分类领域的经典卷积神经网络进行剖析,介绍如何应用这些基础模块构建卷积神经网络,解决图像分类问题。按照被提出的时间顺序,涵盖如下卷积神经网络:
LeNet是最早的卷积神经网络之一。1998年,Yann LeCun第一次将LeNet卷积神经网络应用到图像分类上,在手写数字识别任务中取得了巨大成功。LeNet通过连续使用卷积和池化层的组合提取图像特征,其架构如下图所示,这里展示的是用于MNIST手写体数字识别任务中的LeNet-5模型:
【提示】:
卷积层的输出特征图如何当作全连接层的输入使用呢?
卷积层的输出数据格式是[N,C,H,W],在输入全连接层的时候,会自动将数据拉平,也就是对每个样本,自动将其转化为长度为K的向量,其中 K = C × H × W K = C \times H \times W K=C×H×W,一个mini-batch的数据维度变成了 N × K N\times K N×K的二维向量。
LeNet网络的实现代码如下(基于paddlepaddle 2.0):
# 导入需要的包 import paddle import numpy as np from paddle.nn import Conv2D, MaxPool2D, Linear ## 组网 import paddle.nn.functional as F # 定义 LeNet 网络结构 class LeNet(paddle.nn.Layer): def __init__(self, num_classes=1): super(LeNet, self).__init__() # 创建卷积和池化层 # 创建第1个卷积层 self.conv1 = Conv2D(in_channels=1, out_channels=6, kernel_size=5) self.max_pool1 = MaxPool2D(kernel_size=2, stride=2) # 尺寸的逻辑:池化层未改变通道数;当前通道数为6 # 创建第2个卷积层 self.conv2 = Conv2D(in_channels=6, out_channels=16, kernel_size=5) self.max_pool2 = MaxPool2D(kernel_size=2, stride=2) # 创建第3个卷积层 self.conv3 = Conv2D(in_channels=16, out_channels=120, kernel_size=4) # 尺寸的逻辑:输入层将数据拉平[B,C,H,W] -> [B,C*H*W] # 输入size是[28,28],经过三次卷积和两次池化之后,C*H*W等于120 self.fc1 = Linear(in_features=120, out_features=64) # 创建全连接层,第一个全连接层的输出神经元个数为64, 第二个全连接层输出神经元个数为分类标签的类别数 self.fc2 = Linear(in_features=64, out_features=num_classes) # 网络的前向计算过程 def forward(self, x): x = self.conv1(x) # 每个卷积层使用Sigmoid激活函数,后面跟着一个2x2的池化 x = F.sigmoid(x) x = self.max_pool1(x) x = F.sigmoid(x) x = self.conv2(x) x = self.max_pool2(x) x = self.conv3(x) # 尺寸的逻辑:输入层将数据拉平[B,C,H,W] -> [B,C*H*W] x = paddle.reshape(x, [x.shape[0], -1]) x = self.fc1(x) x = F.sigmoid(x) x = self.fc2(x) return x
飞桨会根据实际图像数据的尺寸和卷积核参数自动推断中间层数据的W和H等,只需要用户表达通道数即可。下面的程序使用随机数作为输入,查看经过LeNet-5的每一层作用之后,输出数据的形状。
# 输入数据形状是 [N, 1, H, W] # 这里用np.random创建一个随机数组作为输入数据 x = np.random.randn(*[3,1,28,28]) x = x.astype('float32') # 创建LeNet类的实例,指定模型名称和分类的类别数目 model = LeNet(num_classes=10) # 通过调用LeNet从基类继承的sublayers()函数, # 查看LeNet中所包含的子层 print(model.sublayers()) x = paddle.to_tensor(x) for item in model.sublayers(): # item是LeNet类中的一个子层 # 查看经过子层之后的输出数据形状 try: x = item(x) except: x = paddle.reshape(x, [x.shape[0], -1]) x = item(x) if len(item.parameters())==2: # 查看卷积和全连接层的数据和参数的形状, # 其中item.parameters()[0]是权重参数w,item.parameters()[1]是偏置参数b print(item.full_name(), x.shape, item.parameters()[0].shape, item.parameters()[1].shape) else: # 池化层没有参数 print(item.full_name(), x.shape)
# -*- coding: utf-8 -*- # LeNet 识别手写数字 import os import random import paddle import numpy as np import paddle from paddle.vision.transforms import ToTensor from paddle.vision.datasets import MNIST # 定义训练过程 def train(model, opt, train_loader, valid_loader): # 开启0号GPU训练 use_gpu = True paddle.device.set_device('gpu:0') if use_gpu else paddle.device.set_device('cpu') print('start training ... ') model.train() for epoch in range(EPOCH_NUM): for batch_id, data in enumerate(train_loader()): img = data[0] label = data[1] # 计算模型输出 logits = model(img) # 计算损失函数 loss_func = paddle.nn.CrossEntropyLoss(reduction='none') loss = loss_func(logits, label) avg_loss = paddle.mean(loss) if batch_id % 2000 == 0: print("epoch: {}, batch_id: {}, loss is: {:.4f}".format(epoch, batch_id, float(avg_loss.numpy()))) avg_loss.backward() opt.step() opt.clear_grad() model.eval() accuracies = [] losses = [] for batch_id, data in enumerate(valid_loader()): img = data[0] label = data[1] # 计算模型输出 logits = model(img) pred = F.softmax(logits) # 计算损失函数 loss_func = paddle.nn.CrossEntropyLoss(reduction='none') loss = loss_func(logits, label) acc = paddle.metric.accuracy(pred, label) accuracies.append(acc.numpy()) losses.append(loss.numpy()) print("[validation] accuracy/loss: {:.4f}/{:.4f}".format(np.mean(accuracies), np.mean(losses))) model.train() # 保存模型参数 paddle.save(model.state_dict(), 'mnist.pdparams') # 创建模型 model = LeNet(num_classes=10) # 设置迭代轮数 EPOCH_NUM = 5 # 设置优化器为Momentum,学习率为0.001 opt = paddle.optimizer.Momentum(learning_rate=0.001, momentum=0.9, parameters=model.parameters()) # 定义数据读取器 train_loader = paddle.io.DataLoader(MNIST(mode='train', transform=ToTensor()), batch_size=10, shuffle=True) valid_loader = paddle.io.DataLoader(MNIST(mode='test', transform=ToTensor()), batch_size=10) # 启动训练过程 train(model, opt, train_loader, valid_loader)
(基于torch)
#构建网络 import torch.nn as nn import torch.nn.functional as F class Net(nn.Module): def __init__(self): super(Net, self).__init__() # 卷积层 # i --> input channels # 6 --> output channels # 5 --> kernel size self.conv1 = nn.Conv2d(1, 6, 5) self.conv2 = nn.Conv2d(6, 16, 5) # 全连接层 # 16 * 4 * 4 --> input vector dimensions # 120 --> output vector dimensions self.fc1 = nn.Linear(16 * 4 * 4, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 10) def forward(self, x): # 卷积 --> ReLu --> 池化 x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2)) x = F.max_pool2d(F.relu(self.conv2(x)), (2, 2)) # reshape, '-1'表示自适应 # x = (n * 16 * 4 * 4) --> n : input channels # x.size()[0] == n --> input channels x = x.view(x.size()[0], -1) # 全连接层 x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x
训练(train.py)
import torchvision as tv import torchvision.transforms as transforms import torch import torch.nn as nn from torch import optim from torch.utils.data import DataLoader from torch.autograd import Variable from model import Net if __name__ == '__main__': # 定义对数据的预处理 transform = transforms.Compose([ transforms.ToTensor(), # 转换为Tensor,并归一化至[0, 1] ]) # 训练集 trainset = tv.datasets.MNIST( root='data/', train=True, download=True, transform=transform ) trainloader = DataLoader( dataset=trainset, batch_size=4, shuffle=True ) # MNIST数据集中的十种标签 classes = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9') # 创建网络模型 net = Net() if torch.cuda.is_available(): # 使用GPU net.cuda() # 定义损失函数和优化器 criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(net.parameters(), lr=0.005, momentum=0.9) for epoch in range(5): running_loss = 0.0 for i, data in enumerate(trainloader): # 输入数据 inputs, labels = data if torch.cuda.is_available(): inputs = inputs.cuda() labels = labels.cuda() inputs, labels = Variable(inputs), Variable(labels) # 梯度清0 optimizer.zero_grad() # forward + backward outputs = net(inputs) loss = criterion(outputs, labels) loss.backward() # 更新参数 optimizer.step() # 打印log信息 running_loss += loss.item() # 每2000个batch打印一次训练状态 if i % 100 == 99: print('[{}/{}][{}/{}] loss: {:.3f}'.format(epoch + 1, 5, (i + 1) * 4, len(trainset), running_loss / 100)) running_loss = 0.0 # 保存参数文件 torch.save(net.state_dict(), 'checkpoints/model_{}.pth'.format(epoch + 1)) print('model_{}.pth saved'.format(epoch + 1)) print('Finished Training')
虽然LeNet在手写数字识别数据集上取得了很好的结果,但在更大的数据集上表现却并不好。自从1998年LeNet问世以来,接下来十几年的时间里,神经网络并没有在计算机视觉领域取得很好的结果,反而一度被其它算法所超越。原因主要有两方面,一是神经网络的计算比较复杂,对当时计算机的算力来说,训练神经网络是件非常耗时的事情;另一方面,当时还没有专门针对神经网络做算法和训练技巧的优化,神经网络的收敛是件非常困难的事情。
随着技术的进步和发展,计算机的算力越来越强大,尤其是在GPU并行计算能力的推动下,复杂神经网络的计算也变得更加容易实施。另一方面,互联网上涌现出越来越多的数据,极大的丰富了数据库。同时也有越来越多的研究人员开始专门针对神经网络做算法和模型的优化,Alex Krizhevsky等人提出的AlexNet以很大优势获得了2012年ImageNet比赛的冠军。这一成果极大的激发了产业界对神经网络的兴趣,开创了使用深度神经网络解决图像问题的途径,随后也在这一领域涌现出越来越多的优秀成果。
AlexNet与LeNet相比,具有更深的网络结构,包含5层卷积和3层全连接,同时使用了如下三种方法改进模型的训练过程:
# -*- coding:utf-8 -*- # 导入需要的包 import paddle import numpy as np from paddle.nn import Conv2D, MaxPool2D, Linear, Dropout ## 组网 import paddle.nn.functional as F # 定义 AlexNet 网络结构 class AlexNet(paddle.nn.Layer): def __init__(self, num_classes=1): super(AlexNet, self).__init__() # AlexNet与LeNet一样也会同时使用卷积和池化层提取图像特征 # 与LeNet不同的是激活函数换成了‘relu’ self.conv1 = Conv2D(in_channels=3, out_channels=96, kernel_size=11, stride=4, padding=5) self.max_pool1 = MaxPool2D(kernel_size=2, stride=2) self.conv2 = Conv2D(in_channels=96, out_channels=256, kernel_size=5, stride=1, padding=2) self.max_pool2 = MaxPool2D(kernel_size=2, stride=2) self.conv3 = Conv2D(in_channels=256, out_channels=384, kernel_size=3, stride=1, padding=1) self.conv4 = Conv2D(in_channels=384, out_channels=384, kernel_size=3, stride=1, padding=1) self.conv5 = Conv2D(in_channels=384, out_channels=256, kernel_size=3, stride=1, padding=1) self.max_pool5 = MaxPool2D(kernel_size=2, stride=2) self.fc1 = Linear(in_features=12544, out_features=4096) self.drop_ratio1 = 0.5 self.drop1 = Dropout(self.drop_ratio1) self.fc2 = Linear(in_features=4096, out_features=4096) self.drop_ratio2 = 0.5 self.drop2 = Dropout(self.drop_ratio2) self.fc3 = Linear(in_features=4096, out_features=num_classes) def forward(self, x): x = self.conv1(x) x = F.relu(x) x = self.max_pool1(x) x = self.conv2(x) x = F.relu(x) x = self.max_pool2(x) x = self.conv3(x) x = F.relu(x) x = self.conv4(x) x = F.relu(x) x = self.conv5(x) x = F.relu(x) x = self.max_pool5(x) x = paddle.reshape(x, [x.shape[0], -1]) x = self.fc1(x) x = F.relu(x) # 在全连接之后使用dropout抑制过拟合 x = self.drop1(x) x = self.fc2(x) x = F.relu(x) # 在全连接之后使用dropout抑制过拟合 x = self.drop2(x) x = self.fc3(x) return x
基于torch
#model.py import torch.nn as nn import torch class AlexNet(nn.Module): def __init__(self, num_classes=1000, init_weights=False): super(AlexNet, self).__init__() self.features = nn.Sequential( #打包 nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2), # input[3, 224, 224] output[48, 55, 55] 自动舍去小数点后 nn.ReLU(inplace=True), #inplace 可以载入更大模型 nn.MaxPool2d(kernel_size=3, stride=2), # output[48, 27, 27] kernel_num为原论文一半 nn.Conv2d(48, 128, kernel_size=5, padding=2), # output[128, 27, 27] nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), # output[128, 13, 13] nn.Conv2d(128, 192, kernel_size=3, padding=1), # output[192, 13, 13] nn.ReLU(inplace=True), nn.Conv2d(192, 192, kernel_size=3, padding=1), # output[192, 13, 13] nn.ReLU(inplace=True), nn.Conv2d(192, 128, kernel_size=3, padding=1), # output[128, 13, 13] nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), # output[128, 6, 6] ) self.classifier = nn.Sequential( nn.Dropout(p=0.5), #全链接 nn.Linear(128 * 6 * 6, 2048), nn.ReLU(inplace=True), nn.Dropout(p=0.5), nn.Linear(2048, 2048), nn.ReLU(inplace=True), nn.Linear(2048, num_classes), ) if init_weights: self._initialize_weights() def forward(self, x): x = self.features(x) x = torch.flatten(x, start_dim=1) #展平 或者view() x = self.classifier(x) return x def _initialize_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)
VGG是当前最流行的CNN模型之一,2014年由Simonyan和Zisserman提出,其命名来源于论文作者所在的实验室Visual Geometry Group。AlexNet模型通过构造多层网络,取得了较好的效果,但是并没有给出深度神经网络设计的方向。VGG通过使用一系列大小为3x3的小尺寸卷积核和池化层构造深度卷积神经网络,并取得了较好的效果。VGG模型因为结构简单、应用性极强而广受研究者欢迎,尤其是它的网络结构设计方法,为构建深度神经网络提供了方向。
下图是VGG-16的网络结构示意图,有13层卷积和3层全连接层。VGG网络的设计严格使用3×3的卷积层和池化层来提取特征,并在网络的最后面使用三层全连接层,将最后一层全连接层的输出作为分类的预测。 在VGG中每层卷积将使用ReLU作为激活函数,在全连接层之后添加dropout来抑制过拟合。使用小的卷积核能够有效地减少参数的个数,使得训练和测试变得更加有效。比如使用两层3×3卷积层,可以得到感受野为5的特征图,而比使用5×5的卷积层需要更少的参数。由于卷积核比较小,可以堆叠更多的卷积层,加深网络的深度,这对于图像分类任务来说是有利的。VGG模型的成功证明了增加网络的深度,可以更好的学习图像中的特征模式。
paddlepaddle 实现
# -*- coding:utf-8 -*- # VGG模型代码 import numpy as np import paddle # from paddle.nn import Conv2D, MaxPool2D, BatchNorm, Linear from paddle.nn import Conv2D, MaxPool2D, BatchNorm2D, Linear # 定义vgg网络 class VGG(paddle.nn.Layer): def __init__(self): super(VGG, self).__init__() in_channels = [3, 64, 128, 256, 512, 512] # 定义第一个block,包含两个卷积 self.conv1_1 = Conv2D(in_channels=in_channels[0], out_channels=in_channels[1], kernel_size=3, padding=1, stride=1) self.conv1_2 = Conv2D(in_channels=in_channels[1], out_channels=in_channels[1], kernel_size=3, padding=1, stride=1) # 定义第二个block,包含两个卷积 self.conv2_1 = Conv2D(in_channels=in_channels[1], out_channels=in_channels[2], kernel_size=3, padding=1, stride=1) self.conv2_2 = Conv2D(in_channels=in_channels[2], out_channels=in_channels[2], kernel_size=3, padding=1, stride=1) # 定义第三个block,包含三个卷积 self.conv3_1 = Conv2D(in_channels=in_channels[2], out_channels=in_channels[3], kernel_size=3, padding=1, stride=1) self.conv3_2 = Conv2D(in_channels=in_channels[3], out_channels=in_channels[3], kernel_size=3, padding=1, stride=1) self.conv3_3 = Conv2D(in_channels=in_channels[3], out_channels=in_channels[3], kernel_size=3, padding=1, stride=1) # 定义第四个block,包含三个卷积 self.conv4_1 = Conv2D(in_channels=in_channels[3], out_channels=in_channels[4], kernel_size=3, padding=1, stride=1) self.conv4_2 = Conv2D(in_channels=in_channels[4], out_channels=in_channels[4], kernel_size=3, padding=1, stride=1) self.conv4_3 = Conv2D(in_channels=in_channels[4], out_channels=in_channels[4], kernel_size=3, padding=1, stride=1) # 定义第五个block,包含三个卷积 self.conv5_1 = Conv2D(in_channels=in_channels[4], out_channels=in_channels[5], kernel_size=3, padding=1, stride=1) self.conv5_2 = Conv2D(in_channels=in_channels[5], out_channels=in_channels[5], kernel_size=3, padding=1, stride=1) self.conv5_3 = Conv2D(in_channels=in_channels[5], out_channels=in_channels[5], kernel_size=3, padding=1, stride=1) # 使用Sequential 将全连接层和relu组成一个线性结构(fc + relu) # 当输入为224x224时,经过五个卷积块和池化层后,特征维度变为[512x7x7] self.fc1 = paddle.nn.Sequential(paddle.nn.Linear(512 * 7 * 7, 4096), paddle.nn.ReLU()) self.drop1_ratio = 0.5 self.dropout1 = paddle.nn.Dropout(self.drop1_ratio, mode='upscale_in_train') # 使用Sequential 将全连接层和relu组成一个线性结构(fc + relu) self.fc2 = paddle.nn.Sequential(paddle.nn.Linear(4096, 4096), paddle.nn.ReLU()) self.drop2_ratio = 0.5 self.dropout2 = paddle.nn.Dropout(self.drop2_ratio, mode='upscale_in_train') self.fc3 = paddle.nn.Linear(4096, 1) self.relu = paddle.nn.ReLU() self.pool = MaxPool2D(stride=2, kernel_size=2) def forward(self, x): x = self.relu(self.conv1_1(x)) x = self.relu(self.conv1_2(x)) x = self.pool(x) x = self.relu(self.conv2_1(x)) x = self.relu(self.conv2_2(x)) x = self.pool(x) x = self.relu(self.conv3_1(x)) x = self.relu(self.conv3_2(x)) x = self.relu(self.conv3_3(x)) x = self.pool(x) x = self.relu(self.conv4_1(x)) x = self.relu(self.conv4_2(x)) x = self.relu(self.conv4_3(x)) x = self.pool(x) x = self.relu(self.conv5_1(x)) x = self.relu(self.conv5_2(x)) x = self.relu(self.conv5_3(x)) x = self.pool(x) x = paddle.flatten(x, 1, -1) x = self.dropout1(self.relu(self.fc1(x))) x = self.dropout2(self.relu(self.fc2(x))) x = self.fc3(x) return x
基于torch
import torch import torch.nn as nn import torch.nn.functional as F class VGG16(nn.Module): def __init__(self): super(VGG16, self).__init__() # 3 * 224 * 224 self.conv1_1 = nn.Conv2d(3, 64, 3) # 64 * 222 * 222 self.conv1_2 = nn.Conv2d(64, 64, 3, padding=(1, 1)) # 64 * 222* 222 self.maxpool1 = nn.MaxPool2d((2, 2), padding=(1, 1)) # pooling 64 * 112 * 112 self.conv2_1 = nn.Conv2d(64, 128, 3) # 128 * 110 * 110 self.conv2_2 = nn.Conv2d(128, 128, 3, padding=(1, 1)) # 128 * 110 * 110 self.maxpool2 = nn.MaxPool2d((2, 2), padding=(1, 1)) # pooling 128 * 56 * 56 self.conv3_1 = nn.Conv2d(128, 256, 3) # 256 * 54 * 54 self.conv3_2 = nn.Conv2d(256, 256, 3, padding=(1, 1)) # 256 * 54 * 54 self.conv3_3 = nn.Conv2d(256, 256, 3, padding=(1, 1)) # 256 * 54 * 54 self.maxpool3 = nn.MaxPool2d((2, 2), padding=(1, 1)) # pooling 256 * 28 * 28 self.conv4_1 = nn.Conv2d(256, 512, 3) # 512 * 26 * 26 self.conv4_2 = nn.Conv2d(512, 512, 3, padding=(1, 1)) # 512 * 26 * 26 self.conv4_3 = nn.Conv2d(512, 512, 3, padding=(1, 1)) # 512 * 26 * 26 self.maxpool4 = nn.MaxPool2d((2, 2), padding=(1, 1)) # pooling 512 * 14 * 14 self.conv5_1 = nn.Conv2d(512, 512, 3) # 512 * 12 * 12 self.conv5_2 = nn.Conv2d(512, 512, 3, padding=(1, 1)) # 512 * 12 * 12 self.conv5_3 = nn.Conv2d(512, 512, 3, padding=(1, 1)) # 512 * 12 * 12 self.maxpool5 = nn.MaxPool2d((2, 2), padding=(1, 1)) # pooling 512 * 7 * 7 # view self.fc1 = nn.Linear(512 * 7 * 7, 4096) self.fc2 = nn.Linear(4096, 4096) self.fc3 = nn.Linear(4096, 1000) # softmax 1 * 1 * 1000 def forward(self, x): # x.size(0)即为batch_size in_size = x.size(0) out = self.conv1_1(x) # 222 out = F.relu(out) out = self.conv1_2(out) # 222 out = F.relu(out) out = self.maxpool1(out) # 112 out = self.conv2_1(out) # 110 out = F.relu(out) out = self.conv2_2(out) # 110 out = F.relu(out) out = self.maxpool2(out) # 56 out = self.conv3_1(out) # 54 out = F.relu(out) out = self.conv3_2(out) # 54 out = F.relu(out) out = self.conv3_3(out) # 54 out = F.relu(out) out = self.maxpool3(out) # 28 out = self.conv4_1(out) # 26 out = F.relu(out) out = self.conv4_2(out) # 26 out = F.relu(out) out = self.conv4_3(out) # 26 out = F.relu(out) out = self.maxpool4(out) # 14 out = self.conv5_1(out) # 12 out = F.relu(out) out = self.conv5_2(out) # 12 out = F.relu(out) out = self.conv5_3(out) # 12 out = F.relu(out) out = self.maxpool5(out) # 7 # 展平 out = out.view(in_size, -1) out = self.fc1(out) out = F.relu(out) out = self.fc2(out) out = F.relu(out) out = self.fc3(out) out = F.log_softmax(out, dim=1) return out
GoogLeNet是2014年ImageNet比赛的冠军,它的主要特点是网络不仅有深度,还在横向上具有“宽度”。由于图像信息在空间尺寸上的巨大差异,如何选择合适的卷积核来提取特征就显得比较困难了。空间分布范围更广的图像信息适合用较大的卷积核来提取其特征;而空间分布范围较小的图像信息则适合用较小的卷积核来提取其特征。为了解决这个问题,GoogLeNet提出了一种被称为Inception模块的方案。如 下图所示:
图(a)是Inception模块的设计思想,使用3个不同大小的卷积核对输入图片进行卷积操作,并附加最大池化,将这4个操作的输出沿着通道这一维度进行拼接,构成的输出特征图将会包含经过不同大小的卷积核提取出来的特征,从而达到捕捉不同尺度信息的效果。Inception模块采用多通路(multi-path)的设计形式,每个支路使用不同大小的卷积核,最终输出特征图的通道数是每个支路输出通道数的总和,这将会导致输出通道数变得很大,尤其是使用多个Inception模块串联操作的时候,模型参数量会变得非常大。为了减小参数量,Inception模块使用了图(b)中的设计方式,在每个3x3和5x5的卷积层之前,增加1x1的卷积层来控制输出通道数;在最大池化层后面增加1x1卷积层减小输出通道数。基于这一设计思想,形成了上图(b)中所示的结构。下面这段程序是Inception块的具体实现方式,可以对照图(b)和代码一起阅读。
提示:
paddlepaddle 实现
# GoogLeNet模型代码 import numpy as np import paddle from paddle.nn import Conv2D, MaxPool2D, AdaptiveAvgPool2D, Linear ## 组网 import paddle.nn.functional as F # 定义Inception块 class Inception(paddle.nn.Layer): def __init__(self, c0, c1, c2, c3, c4, **kwargs): ''' Inception模块的实现代码, c1,图(b)中第一条支路1x1卷积的输出通道数,数据类型是整数 c2,图(b)中第二条支路卷积的输出通道数,数据类型是tuple或list, 其中c2[0]是1x1卷积的输出通道数,c2[1]是3x3 c3,图(b)中第三条支路卷积的输出通道数,数据类型是tuple或list, 其中c3[0]是1x1卷积的输出通道数,c3[1]是3x3 c4,图(b)中第一条支路1x1卷积的输出通道数,数据类型是整数 ''' super(Inception, self).__init__() # 依次创建Inception块每条支路上使用到的操作 self.p1_1 = Conv2D(in_channels=c0,out_channels=c1, kernel_size=1) self.p2_1 = Conv2D(in_channels=c0,out_channels=c2[0], kernel_size=1) self.p2_2 = Conv2D(in_channels=c2[0],out_channels=c2[1], kernel_size=3, padding=1) self.p3_1 = Conv2D(in_channels=c0,out_channels=c3[0], kernel_size=1) self.p3_2 = Conv2D(in_channels=c3[0],out_channels=c3[1], kernel_size=5, padding=2) self.p4_1 = MaxPool2D(kernel_size=3, stride=1, padding=1) self.p4_2 = Conv2D(in_channels=c0,out_channels=c4, kernel_size=1) def forward(self, x): # 支路1只包含一个1x1卷积 p1 = F.relu(self.p1_1(x)) # 支路2包含 1x1卷积 + 3x3卷积 p2 = F.relu(self.p2_2(F.relu(self.p2_1(x)))) # 支路3包含 1x1卷积 + 5x5卷积 p3 = F.relu(self.p3_2(F.relu(self.p3_1(x)))) # 支路4包含 最大池化和1x1卷积 p4 = F.relu(self.p4_2(self.p4_1(x))) # 将每个支路的输出特征图拼接在一起作为最终的输出结果 return paddle.concat([p1, p2, p3, p4], axis=1)
GoogLeNet的架构如下图所示,在主体卷积部分中使用5个模块(block),每个模块之间使用步幅为2的3 ×3最大池化层来减小输出高宽。
第一模块使用一个64通道的7 × 7卷积层。
第二模块使用2个卷积层:首先是64通道的1 × 1卷积层,然后是将通道增大3倍的3 × 3卷积层。
第三模块串联2个完整的Inception块。
第四模块串联了5个Inception块。
第五模块串联了2 个Inception块。
第五模块的后面紧跟输出层,使用全局平均池化层来将每个通道的高和宽变成1,最后接上一个输出个数为标签类别数的全连接层。
说明: 在原作者的论文中添加了图中所示的softmax1和softmax2两个辅助分类器,如下图所示,训练时将三个分类器的损失函数进行加权求和,以缓解梯度消失现象。这里的程序作了简化,没有加入辅助分类器。
GoogLeNet的具体实现如下代码所示:
# GoogLeNet模型代码 import numpy as np import paddle from paddle.nn import Conv2D, MaxPool2D, AdaptiveAvgPool2D, Linear ## 组网 import paddle.nn.functional as F # 定义Inception块 class Inception(paddle.nn.Layer): def __init__(self, c0, c1, c2, c3, c4, **kwargs): ''' Inception模块的实现代码, c1,图(b)中第一条支路1x1卷积的输出通道数,数据类型是整数 c2,图(b)中第二条支路卷积的输出通道数,数据类型是tuple或list, 其中c2[0]是1x1卷积的输出通道数,c2[1]是3x3 c3,图(b)中第三条支路卷积的输出通道数,数据类型是tuple或list, 其中c3[0]是1x1卷积的输出通道数,c3[1]是3x3 c4,图(b)中第一条支路1x1卷积的输出通道数,数据类型是整数 ''' super(Inception, self).__init__() # 依次创建Inception块每条支路上使用到的操作 self.p1_1 = Conv2D(in_channels=c0,out_channels=c1, kernel_size=1, stride=1) self.p2_1 = Conv2D(in_channels=c0,out_channels=c2[0], kernel_size=1, stride=1) self.p2_2 = Conv2D(in_channels=c2[0],out_channels=c2[1], kernel_size=3, padding=1, stride=1) self.p3_1 = Conv2D(in_channels=c0,out_channels=c3[0], kernel_size=1, stride=1) self.p3_2 = Conv2D(in_channels=c3[0],out_channels=c3[1], kernel_size=5, padding=2, stride=1) self.p4_1 = MaxPool2D(kernel_size=3, stride=1, padding=1) self.p4_2 = Conv2D(in_channels=c0,out_channels=c4, kernel_size=1, stride=1) # # 新加一层batchnorm稳定收敛 # self.batchnorm = paddle.nn.BatchNorm2D(c1+c2[1]+c3[1]+c4) def forward(self, x): # 支路1只包含一个1x1卷积 p1 = F.relu(self.p1_1(x)) # 支路2包含 1x1卷积 + 3x3卷积 p2 = F.relu(self.p2_2(F.relu(self.p2_1(x)))) # 支路3包含 1x1卷积 + 5x5卷积 p3 = F.relu(self.p3_2(F.relu(self.p3_1(x)))) # 支路4包含 最大池化和1x1卷积 p4 = F.relu(self.p4_2(self.p4_1(x))) # 将每个支路的输出特征图拼接在一起作为最终的输出结果 return paddle.concat([p1, p2, p3, p4], axis=1) # return self.batchnorm() class GoogLeNet(paddle.nn.Layer): def __init__(self): super(GoogLeNet, self).__init__() # GoogLeNet包含五个模块,每个模块后面紧跟一个池化层 # 第一个模块包含1个卷积层 self.conv1 = Conv2D(in_channels=3,out_channels=64, kernel_size=7, padding=3, stride=1) # 3x3最大池化 self.pool1 = MaxPool2D(kernel_size=3, stride=2, padding=1) # 第二个模块包含2个卷积层 self.conv2_1 = Conv2D(in_channels=64,out_channels=64, kernel_size=1, stride=1) self.conv2_2 = Conv2D(in_channels=64,out_channels=192, kernel_size=3, padding=1, stride=1) # 3x3最大池化 self.pool2 = MaxPool2D(kernel_size=3, stride=2, padding=1) # 第三个模块包含2个Inception块 self.block3_1 = Inception(192, 64, (96, 128), (16, 32), 32) self.block3_2 = Inception(256, 128, (128, 192), (32, 96), 64) # 3x3最大池化 self.pool3 = MaxPool2D(kernel_size=3, stride=2, padding=1) # 第四个模块包含5个Inception块 self.block4_1 = Inception(480, 192, (96, 208), (16, 48), 64) self.block4_2 = Inception(512, 160, (112, 224), (24, 64), 64) self.block4_3 = Inception(512, 128, (128, 256), (24, 64), 64) self.block4_4 = Inception(512, 112, (144, 288), (32, 64), 64) self.block4_5 = Inception(528, 256, (160, 320), (32, 128), 128) # 3x3最大池化 self.pool4 = MaxPool2D(kernel_size=3, stride=2, padding=1) # 第五个模块包含2个Inception块 self.block5_1 = Inception(832, 256, (160, 320), (32, 128), 128) self.block5_2 = Inception(832, 384, (192, 384), (48, 128), 128) # 全局池化,用的是global_pooling,不需要设置pool_stride self.pool5 = AdaptiveAvgPool2D(output_size=1) self.fc = Linear(in_features=1024, out_features=1) def forward(self, x): x = self.pool1(F.relu(self.conv1(x))) x = self.pool2(F.relu(self.conv2_2(F.relu(self.conv2_1(x))))) x = self.pool3(self.block3_2(self.block3_1(x))) x = self.block4_3(self.block4_2(self.block4_1(x))) x = self.pool4(self.block4_5(self.block4_4(x))) x = self.pool5(self.block5_2(self.block5_1(x))) x = paddle.reshape(x, [x.shape[0], -1]) x = self.fc(x) return x # 创建模型 model = GoogLeNet() print(len(model.parameters())) opt = paddle.optimizer.Momentum(learning_rate=0.001, momentum=0.9, parameters=model.parameters(), weight_decay=0.001) # 启动训练过程 train_pm(model, opt)
torch实现
#model.py import torch.nn as nn import torch import torch.nn.functional as F 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) 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._initialize_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 _initialize_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) #inception结构 class Inception(nn.Module): 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) # output[batch, 128, 4, 4] self.fc1 = nn.Linear(2048, 1024) self.fc2 = nn.Linear(1024, num_classes) def forward(self, x): # aux1: N x 512 x 14 x 14, aux2: N x 528 x 14 x 14 x = self.averagePool(x) # aux1: N x 512 x 4 x 4, aux2: N x 528 x 4 x 4 x = self.conv(x) # N x 128 x 4 x 4 x = torch.flatten(x, 1) x = F.dropout(x, 0.5, training=self.training) # N x 2048 x = F.relu(self.fc1(x), inplace=True) x = F.dropout(x, 0.5, training=self.training) # N x 1024 x = self.fc2(x) # N x num_classes return x class BasicConv2d(nn.Module): 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
ResNet是2015年ImageNet比赛的冠军,将识别错误率降低到了3.6%,这个结果甚至超出了正常人眼识别的精度。
通过前面几个经典模型学习,我们可以发现随着深度学习的不断发展,模型的层数越来越多,网络结构也越来越复杂。那么是否加深网络结构,就一定会得到更好的效果呢?从理论上来说,假设新增加的层都是恒等映射,只要原有的层学出跟原模型一样的参数,那么深模型结构就能达到原模型结构的效果。换句话说,原模型的解只是新模型的解的子空间,在新模型解的空间里应该能找到比原模型解对应的子空间更好的结果。但是实践表明,增加网络的层数之后,训练误差往往不降反升。
Kaiming He等人提出了残差网络ResNet来解决上述问题,其基本思想如下图所示。
图(b)的结构是残差网络的基础,这种结构也叫做残差块(Residual block)。输入x通过跨层连接,能更快的向前传播数据,或者向后传播梯度。通俗的比喻,在火热的电视节目《王牌对王牌》上有一个“传声筒”的游戏,排在队首的嘉宾把看到的影视片段表演给后面一个嘉宾看,经过四五个嘉宾后,最后一个嘉宾如果能表演出更多原剧的内容,就能取得高分。我们常常会发现刚开始的嘉宾往往表演出最多的信息(类似于Loss),而随着表演的传递,有效的表演信息越来越少(类似于梯度弥散)。如果每个嘉宾都能看到原始的影视片段,那么相信传声筒的效果会好很多。类似的,由于ResNet每层都存在直连的旁路,相当于每一层都和最终的损失有“直接对话”的机会,自然可以更好的解决梯度弥散的问题。残差块的具体设计方案如下图所示,这种设计方案也常称作瓶颈结构(BottleNeck)。11的卷积核可以非常方便的调整中间层的通道数,在进入33的卷积层之前减少通道数(256->64),经过该卷积层后再恢复通道数(64->256),可以显著减少网络的参数量。这个结构(256->64->256)像一个中间细,两头粗的瓶颈,所以被称为“BottleNeck”。
下图表示出了ResNet-50的结构,一共包含49层卷积和1层全连接,所以被称为ResNet-50。
ResNet-50的具体实现如下代码所示(paddlepaddle ):
# -*- coding:utf-8 -*- # ResNet模型代码 import numpy as np import paddle import paddle.nn as nn import paddle.nn.functional as F # ResNet中使用了BatchNorm层,在卷积层的后面加上BatchNorm以提升数值稳定性 # 定义卷积批归一化块 class ConvBNLayer(paddle.nn.Layer): def __init__(self, num_channels, num_filters, filter_size, stride=1, groups=1, act=None): """ num_channels, 卷积层的输入通道数 num_filters, 卷积层的输出通道数 stride, 卷积层的步幅 groups, 分组卷积的组数,默认groups=1不使用分组卷积 """ super(ConvBNLayer, self).__init__() # 创建卷积层 self._conv = nn.Conv2D( in_channels=num_channels, out_channels=num_filters, kernel_size=filter_size, stride=stride, padding=(filter_size - 1) // 2, groups=groups, bias_attr=False) # 创建BatchNorm层 self._batch_norm = paddle.nn.BatchNorm2D(num_filters) self.act = act def forward(self, inputs): y = self._conv(inputs) y = self._batch_norm(y) if self.act == 'leaky': y = F.leaky_relu(x=y, negative_slope=0.1) elif self.act == 'relu': y = F.relu(x=y) return y # 定义残差块 # 每个残差块会对输入图片做三次卷积,然后跟输入图片进行短接 # 如果残差块中第三次卷积输出特征图的形状与输入不一致,则对输入图片做1x1卷积,将其输出形状调整成一致 class BottleneckBlock(paddle.nn.Layer): def __init__(self, num_channels, num_filters, stride, shortcut=True): super(BottleneckBlock, self).__init__() # 创建第一个卷积层 1x1 self.conv0 = ConvBNLayer( num_channels=num_channels, num_filters=num_filters, filter_size=1, act='relu') # 创建第二个卷积层 3x3 self.conv1 = ConvBNLayer( num_channels=num_filters, num_filters=num_filters, filter_size=3, stride=stride, act='relu') # 创建第三个卷积 1x1,但输出通道数乘以4 self.conv2 = ConvBNLayer( num_channels=num_filters, num_filters=num_filters * 4, filter_size=1, act=None) # 如果conv2的输出跟此残差块的输入数据形状一致,则shortcut=True # 否则shortcut = False,添加1个1x1的卷积作用在输入数据上,使其形状变成跟conv2一致 if not shortcut: self.short = ConvBNLayer( num_channels=num_channels, num_filters=num_filters * 4, filter_size=1, stride=stride) self.shortcut = shortcut self._num_channels_out = num_filters * 4 def forward(self, inputs): y = self.conv0(inputs) conv1 = self.conv1(y) conv2 = self.conv2(conv1) # 如果shortcut=True,直接将inputs跟conv2的输出相加 # 否则需要对inputs进行一次卷积,将形状调整成跟conv2输出一致 if self.shortcut: short = inputs else: short = self.short(inputs) y = paddle.add(x=short, y=conv2) y = F.relu(y) return y # 定义ResNet模型 class ResNet(paddle.nn.Layer): def __init__(self, layers=50, class_dim=1): """ layers, 网络层数,可以是50, 101或者152 class_dim,分类标签的类别数 """ super(ResNet, self).__init__() self.layers = layers supported_layers = [50, 101, 152] assert layers in supported_layers, \ "supported layers are {} but input layer is {}".format(supported_layers, layers) if layers == 50: #ResNet50包含多个模块,其中第2到第5个模块分别包含3、4、6、3个残差块 depth = [3, 4, 6, 3] elif layers == 101: #ResNet101包含多个模块,其中第2到第5个模块分别包含3、4、23、3个残差块 depth = [3, 4, 23, 3] elif layers == 152: #ResNet152包含多个模块,其中第2到第5个模块分别包含3、8、36、3个残差块 depth = [3, 8, 36, 3] # 残差块中使用到的卷积的输出通道数 num_filters = [64, 128, 256, 512] # ResNet的第一个模块,包含1个7x7卷积,后面跟着1个最大池化层 self.conv = ConvBNLayer( num_channels=3, num_filters=64, filter_size=7, stride=2, act='relu') self.pool2d_max = nn.MaxPool2D( kernel_size=3, stride=2, padding=1) # ResNet的第二到第五个模块c2、c3、c4、c5 self.bottleneck_block_list = [] num_channels = 64 for block in range(len(depth)): shortcut = False for i in range(depth[block]): # c3、c4、c5将会在第一个残差块使用stride=2;其余所有残差块stride=1 bottleneck_block = self.add_sublayer( 'bb_%d_%d' % (block, i), BottleneckBlock( num_channels=num_channels, num_filters=num_filters[block], stride=2 if i == 0 and block != 0 else 1, shortcut=shortcut)) num_channels = bottleneck_block._num_channels_out self.bottleneck_block_list.append(bottleneck_block) shortcut = True # 在c5的输出特征图上使用全局池化 self.pool2d_avg = paddle.nn.AdaptiveAvgPool2D(output_size=1) # stdv用来作为全连接层随机初始化参数的方差 import math stdv = 1.0 / math.sqrt(2048 * 1.0) # 创建全连接层,输出大小为类别数目,经过残差网络的卷积和全局池化后, # 卷积特征的维度是[B,2048,1,1],故最后一层全连接的输入维度是2048 self.out = nn.Linear(in_features=2048, out_features=class_dim, weight_attr=paddle.ParamAttr( initializer=paddle.nn.initializer.Uniform(-stdv, stdv))) def forward(self, inputs): y = self.conv(inputs) y = self.pool2d_max(y) for bottleneck_block in self.bottleneck_block_list: y = bottleneck_block(y) y = self.pool2d_avg(y) y = paddle.reshape(y, [y.shape[0], -1]) y = self.out(y) return y
torch实现
import torch import torch.nn as nn import torchvision import numpy as np print("PyTorch Version: ",torch.__version__) print("Torchvision Version: ",torchvision.__version__) __all__ = ['ResNet50', 'ResNet101','ResNet152'] def Conv1(in_planes, places, stride=2): return nn.Sequential( nn.Conv2d(in_channels=in_planes,out_channels=places,kernel_size=7,stride=stride,padding=3, bias=False), nn.BatchNorm2d(places), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2, padding=1) ) class Bottleneck(nn.Module): def __init__(self,in_places,places, stride=1,downsampling=False, expansion = 4): super(Bottleneck,self).__init__() self.expansion = expansion self.downsampling = downsampling self.bottleneck = nn.Sequential( nn.Conv2d(in_channels=in_places,out_channels=places,kernel_size=1,stride=1, bias=False), nn.BatchNorm2d(places), nn.ReLU(inplace=True), nn.Conv2d(in_channels=places, out_channels=places, kernel_size=3, stride=stride, padding=1, bias=False), nn.BatchNorm2d(places), nn.ReLU(inplace=True), nn.Conv2d(in_channels=places, out_channels=places*self.expansion, kernel_size=1, stride=1, bias=False), nn.BatchNorm2d(places*self.expansion), ) if self.downsampling: self.downsample = nn.Sequential( nn.Conv2d(in_channels=in_places, out_channels=places*self.expansion, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(places*self.expansion) ) self.relu = nn.ReLU(inplace=True) def forward(self, x): residual = x out = self.bottleneck(x) if self.downsampling: residual = self.downsample(x) out += residual out = self.relu(out) return out class ResNet(nn.Module): def __init__(self,blocks, num_classes=1000, expansion = 4): super(ResNet,self).__init__() self.expansion = expansion self.conv1 = Conv1(in_planes = 3, places= 64) self.layer1 = self.make_layer(in_places = 64, places= 64, block=blocks[0], stride=1) self.layer2 = self.make_layer(in_places = 256,places=128, block=blocks[1], stride=2) self.layer3 = self.make_layer(in_places=512,places=256, block=blocks[2], stride=2) self.layer4 = self.make_layer(in_places=1024,places=512, block=blocks[3], stride=2) self.avgpool = nn.AvgPool2d(7, stride=1) self.fc = nn.Linear(2048,num_classes) for m in self.modules(): if isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') elif isinstance(m, nn.BatchNorm2d): nn.init.constant_(m.weight, 1) nn.init.constant_(m.bias, 0) def make_layer(self, in_places, places, block, stride): layers = [] layers.append(Bottleneck(in_places, places,stride, downsampling =True)) for i in range(1, block): layers.append(Bottleneck(places*self.expansion, places)) return nn.Sequential(*layers) def forward(self, x): x = self.conv1(x) x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.layer4(x) x = self.avgpool(x) x = x.view(x.size(0), -1) x = self.fc(x) return x def ResNet50(): return ResNet([3, 4, 6, 3]) def ResNet101(): return ResNet([3, 4, 23, 3]) def ResNet152(): return ResNet([3, 8, 36, 3]) if __name__=='__main__': #model = torchvision.models.resnet50() model = ResNet50() print(model) input = torch.randn(1, 3, 224, 224) out = model(input) print(out.shape)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。