AlexNet
CNN卷积网络的发展史
1. LetNet5(1998)
2. AlexNet(2012)
3. ZFNet(2013)
4. VGGNet(2014)
5. GoogLeNet(2014)
6. ResNet(2015)
7. DenseNet(2017)
8. EfficientNet(2019)
9. Vision Transformers(2020)
10. 自适应卷积网络(2021)
上面列出了发展到现在CNN的一些经典的网络模型,我将持续不断更新学习上述神经网络的笔记。共勉!
- 本篇我们将学习AlexNet网络模型。
在上次我们已经详细讲过了LeNet5并且用它实现了手写数字识别,同时也可视化了卷积层。但是在其提出20年的时间里并没有引起学者和各研究机构的重视,随机机器学习的兴起,LeNet网络基本被遗忘了。而真正打破机器学习和传统特征提取方法限制的深度神经网络就是本文要讲述的AlexNet。
参考的AlexNet博客AlexNet网络详解
1、AlexNet之前的思考
在AlexNet网络问世之前,大量的学者在进行图像分类、分割、识别等工作时,主要是通过对图像进行特征提取或是特征+机器学习的方法。但是,这种手工准确提取特征是非常难的事情
,而且即便使用机器学习的方法,整个算法的鲁棒性
依然存在较大的问题。因此,一直都有一种讨论:特征是不是也是可以进行学习的?如果是可以学习的,那么特征的表示是不是也存在层级问题(例如第一层为线或是点特征,第二层为线与点组成的初步特征,第三层为局部特征
)?从这一思想出发,特征可学习且自动组合并给出结果,这是典型的“end-to-end”,而特征学习与自由组合就是深度学习的黑盒子部分。
尽管以上思想在现在看来不能说是绝对正确的,但是至少是图像、语音等研究领域的最重要的研究方向之一。但是20多年来并没有得到充分发展,个人这主要有以下几个原因:
最重要的就是数据问题。从现在的情况看深度学习是需要大量数据进行支撑的,少量样本对传统算法和机器学习模型的优势并不大,甚至可以说效果相当不理想。少量的训练样本在处理实际项目中并不占优势。
硬件条件的限制。深度学习训练需要大量样本,但是大量样本需要大量的内存和计算开销,这种限制的打破也是在2001年以后GPU兴起逐步解决的。
专门研究初始化参数、优化算法等方面的学者相对较少,大量的学者还在研究手工提取特征,即深度学习理论基础并不坚实。
上文中提到,AlexNet出现后打破了原来众多学者的认知,它首次证明了学习到的特征可以超越手工设计的特征,从而越来越多的人开始重新审视深度学习算法并加入到研究的浪潮中
2. AlexNet网络结构
从上图看,在网络设计上其实并非如上图所示,上图包含了GPU通信的部分。这是由当时GPU内存的限制引起的,作者使用两块GPU进行计算,因此分为了上下两部分。但是,以目前GPU的处理能力,单GPU足够了,因此其结构图可以如下所示:
注意:原图输入224224,实际上进行了随即裁剪,实际大小为227227.
2.1 卷积层C1
- C1的基本结构为:卷积–>ReLU–>池化
卷积:输入227 × 227 × 3,96个11×11×3的卷积核,不扩充边缘padding = 0,步长stride = 4,因此其FeatureMap大小为(227-11+0×2+4)/4 = 55,即55×55×96;
激活函数:ReLU;
池化:池化核大小3 × 3,不扩充边缘padding = 0,步长stride = 2,因此其FeatureMap输出大小为(55-3+0×2+2)/2=27, 即C1输出为27×27×96(此处未将输出分到两个GPU中,若按照论文将分成两组,每组为27×27×48);
2.2 卷积层C2
- C2的基本结构为:卷积–>ReLU–>池化
卷积:输入27×27×96,256个5×5×96的卷积核,扩充边缘padding = 2, 步长stride = 1,因此其FeatureMap大小为(27-5+2×2+1)/1 = 27,即27×27×256;
激活函数:ReLU;
池化:池化核大小3 × 3,不扩充边缘padding = 0,步长stride = 2,因此其FeatureMap输出大小为(27-3+0+2)/2=13, 即C2输出为13×13×256(此处未将输出分到两个GPU中,若按照论文将分成两组,每组为13×13×128);
2.3 卷积层C3
- C3的基本结构为:卷积–>ReLU。注意一点:此层没有进行MaxPooling操作。
卷积:输入13×13×256,384个3×3×256的卷积核, 扩充边缘padding = 1,步长stride = 1,因此其FeatureMap大小为(13-3+1×2+1)/1 = 13,即13×13×384;
激活函数:ReLU,即C3输出为13×13×384(此处未将输出分到两个GPU中,若按照论文将分成两组,每组为13×13×192);
2.4 卷积层C4
- C4的基本结构为:卷积–>ReLU。注意一点:此层也没有进行MaxPooling操作。
卷积:输入13×13×384,384个3×3×384的卷积核, 扩充边缘padding = 1,步长stride = 1,因此其FeatureMap大小为(13-3+1×2+1)/1 = 13,即13×13×384;
激活函数:ReLU,即C4输出为13×13×384(此处未将输出分到两个GPU中,若按照论文将分成两组,每组为13×13×192);
2.5 卷积层C5
- C5的基本结构为:卷积–>ReLU–>池化
卷积:输入13×13×384,256个3×3×384的卷积核,扩充边缘padding = 1,步长stride = 1,因此其FeatureMap大小为(13-3+1×2+1)/1 = 13,即13×13×256;
激活函数:ReLU;
池化:池化核大小3 × 3, 扩充边缘padding = 0,步长stride = 2,因此其FeatureMap输出大小为(13-3+0×2+2)/2=6, 即C5输出为6×6×256(此处未将输出分到两个GPU中,若按照论文将分成两组,每组为6×6×128);
2.6 全连接层FC6
- FC6的基本结构为:全连接–>>ReLU–>Dropout
全连接:此层的全连接实际上是通过卷积进行的,输入6×6×256,4096个6×6×256的卷积核,扩充边缘padding = 0, 步长stride = 1, 因此其FeatureMap大小为(6-6+0×2+1)/1 = 1,即1×1×4096;
激活函数:ReLU;
Dropout:全连接层中去掉了一些神经节点,达到防止过拟合,FC6输出为1×1×4096;
2.7 全连接层FC7
- FC7的基本结构为:全连接–>>ReLU–>Dropout
全连接:此层的全连接,输入1×1×4096;
激活函数:ReLU;
Dropout:全连接层中去掉了一些神经节点,达到防止过拟合,FC7输出为1×1×4096;
2.8 全连接层FC8
- FC8的基本结构为:全连接–>>softmax
全连接:此层的全连接,输入1×1×4096;
softmax:softmax为1000,FC8输出为1×1×1000;
在整个过程中,并没有将C1与C2中的Local Response Normalization(局部响应归一化)操作添加在其中,此操作就是将ReLU得到的结果进行归一化,读者可以查看一下原论文
3、AlexNet网络结构的主要贡献
3.1 ReLU激活函数的引入
采用修正线性单元(ReLU)的深度卷积神经网络训练时间比等价的tanh单元要快几倍。而时间开销是进行模型训练过程中很重要的考量因素之一。同时,ReLU有效防止了过拟合现象的出现。由于ReLU激活函数的高效性与实用性,使得它在深度学习框架中占有重要地位。
3.2 层叠池化操作
以往池化的大小PoolingSize与步长stride一般是相等的,例如:图像大小为256*256,PoolingSize=2×2,stride=2,这样可以使图像或是FeatureMap大小缩小一倍变为128,此时池化过程没有发生层叠。但是AlexNet采用了层叠池化操作,即PoolingSize > stride。这种操作非常像卷积操作,可以使相邻像素间产生信息交互和保留必要的联系。论文中也证明,此操作可以有效防止过拟合的发生。
3.3 Dropout操作
Dropout操作会将概率小于0.5的每个隐层神经元的输出设为0,即去掉了一些神经节点,达到防止过拟合。那些“失活的”神经元不再进行前向传播并且不参与反向传播。这个技术减少了复杂的神经元之间的相互影响。在论文中,也验证了此方法的有效性。
3.4 网络层数的增加
与原始的LeNet相比,AlexNet网络结构更深,LeNet为5层,AlexNet为8层。在随后的神经网络发展过程中,AlexNet逐渐让研究人员认识到网络深度对性能的巨大影响。当然,这种思考的重要节点出现在VGG网络
Pytorch实现基于AlexNet的cifar-10分类
1.导入需要的包
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
from sklearn.metrics import f1_score,precision_score,recall_score
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
2. 定义加载数据集的方法
def load_CIFAR10(batch_size, resize=224):
""" 加载数据集到内存 ,迎合Alex改变大小 """
trans = transforms.Compose([
transforms.ToTensor(),
transforms.Resize(resize),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
mnist_train = torchvision.datasets.CIFAR10(root=r"dataset",
train=True,
transform=trans,
download=True)
mnist_test = torchvision.datasets.CIFAR10(root=r"dataset",
train=False,
transform=trans,
download=True)
return (DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=2),
DataLoader(mnist_test, batch_size, shuffle=False,
num_workers=2))
load_CIFAR10(64)
train_data, test_data = load_CIFAR10(64)
Files already downloaded and verified
Files already downloaded and verified
Files already downloaded and verified
Files already downloaded and verified
print(len(test_data.dataset.targets))
10000
注意这里使用的层叠池化,可以使相邻像素间产生信息交互和保留必要的联系
3. 定义AlexNet模型
class My_AlexNet(nn.Module):
def __init__(self):
super(My_AlexNet,self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(3, 96, kernel_size=11, stride=4),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(96, 256, kernel_size=5, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(256, 384, kernel_size=3, padding=1),
nn.ReLU(),
nn.Conv2d(384, 384, kernel_size=3, padding=1),
nn.ReLU(),
nn.Conv2d(384,256,kernel_size=3,padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Flatten(1,-1)
)
self.fc = nn.Sequential(
nn.Linear(6400, 4096),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(4096, 4096),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(4096, 10)
)
def forward(self,x):
x = self.conv(x)
x = x.view(x.size()[0], -1)
x = self.fc(x)
return x
4. 定义训练函数
我们在训练的时候总会出现损失一会大一会小的情况,当梯度下降不断逼近最优值的时候,我们很难在一开始就选择到合适学习率使它适配整个模型的训练。我们总是希望模型在一开始收敛快,在后期收敛慢,这样模型就不会越过最优值。因此我们这里将采用动态调整学习率。
注意这里和之前的不一样,在这里加了一个学习率自更新函数。在下面会详细介绍
# 定义训练的类
def train(model, train_loader, loss_fn, optimizer, schedule,epochs=8):
total_train_step = 0
model.train()
for epoch in range(epochs):
print('------------------------------------第{}轮训练开始了----------------------------'.format(epoch+1))
for images, labels in train_loader:
if torch.cuda.is_available():
images = images.cuda()
labels = labels.cuda()
outputs = model(images)
loss = loss_fn(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_train_step += 1
if total_train_step % 100 == 0:
print("训练次数: {}, Loss: {}".format(total_train_step, loss.item()))
schedule.step()
optm.lr_scheduler是一个学习率更新函数,它可以根据设定的策略来更新学习率。这里采用的是WtepLr策略,没经过step_size个epoch后,学习率乘以gamma。
感兴趣的可以阅读这篇文章pytorch动态调整学习率
net = My_AlexNet()
net = net.to(device)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(net.parameters(), lr=0.0001)
schedule=torch.optim.lr_scheduler.StepLR(optimizer,step_size=4,gamma=0.2,last_epoch=-1)
train = train(net, train_data, loss_fn, optimizer,schedule)
------------------------------------第1轮训练开始了----------------------------
训练次数: 100, Loss: 1.9310390949249268
训练次数: 200, Loss: 1.7103620767593384
训练次数: 300, Loss: 1.5838711261749268
训练次数: 400, Loss: 1.5840641260147095
训练次数: 500, Loss: 1.3199703693389893
训练次数: 600, Loss: 1.3746497631072998
训练次数: 700, Loss: 1.3649054765701294
...
------------------------------------第8轮训练开始了----------------------------
训练次数: 5500, Loss: 0.19987386465072632
训练次数: 5600, Loss: 0.2217329442501068
训练次数: 5700, Loss: 0.2678535282611847
训练次数: 5800, Loss: 0.32842034101486206
训练次数: 5900, Loss: 0.2811841368675232
训练次数: 6000, Loss: 0.24110765755176544
训练次数: 6100, Loss: 0.2992621958255768
训练次数: 6200, Loss: 0.2630188763141632
torch.save(net.state_dict(), './models/alexnet.pth')
5. 定义测试函数
def test(model):
model.eval()
correct = 0
total = 0
prec = 0
f1_s = 0
prec_count = 0
with torch.no_grad():
for images, labels in test_data:
# if torch.cuda.is_available():
# images = images.cuda()
# labels = labels.cuda()
outputs = model(images)
predicted = torch.argmax(outputs, 1)
total += labels.size(0)
prec += precision_score(labels, predicted, average='macro')
prec_count += 1
f1_s += f1_score(labels, predicted, average='macro')
correct += (predicted == labels).sum().item()
print(f"Accuracy: {100 * correct / total}%")
# 打印f1分数
labels = labels.cpu().numpy()
predicted = predicted.cpu().numpy()
print(f"F1 Score: {f1_s/prec_count}")
#打印精确率
print(f"Precision Score: {prec/prec_count}")
net = net.cpu()
test(net)
c:\Users\10766\software\Anaconda\envs\pytorch\lib\site-packages\sklearn\metrics\_classification.py:1327: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.
_warn_prf(average, modifier, msg_start, len(result))
c:\Users\10766\software\Anaconda\envs\pytorch\lib\site-packages\sklearn\metrics\_classification.py:1327: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.
_warn_prf(average, modifier, msg_start, len(result))
c:\Users\10766\software\Anaconda\envs\pytorch\lib\site-packages\sklearn\metrics\_classification.py:1327: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.
_warn_prf(average, modifier, msg_start, len(result))
Accuracy: 81.15%
F1 Score: 0.7929468152039317
Precision Score: 0.8077735578292898
这里我采用学习率自更新后,模型的准确率从60%提升到了80%这是一个非常显著的进步。大家可以尝试往自己的模型中加入学习率自更新以达到更好的效果。
下面我们将可视化AlexNet网络的卷积的特征图。代码如下所示。
6. 卷积特征可视化
import torch
import torchvision.models as models
import matplotlib.pyplot as plt
from PIL import Image
from torchvision import transforms
import torch.nn.functional as F
def load_image(image_path, transform=None):
image = Image.open(image_path)
plt.imshow(image)
plt.axis('off')
if transform is not None:
image = transform(image).unsqueeze(0)
return image
preprocess = transforms.Compose([
transforms.Resize((224,224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] )
])
image = load_image('./images/2.jpg', preprocess)
print(image.shape)
torch.Size([1, 3, 224, 224])
model = My_AlexNet()
model.load_state_dict(torch.load('./models/alexnet.pth'))
<All keys matched successfully>
model_children = list(model.children())
print(len(model_children[0]))
14
def image_conv(image, net):
model_weights = []
conv_layers = []
outputs = []
names = []
processed = []
counter = 0
model_children = list(net.children())
print(model_children)
for i in range(len(model_children[0])):
if type(model_children[0][i]) == nn.Conv2d:
counter += 1
model_weights.append(model_children[0][i].weight.data)
conv_layers.append(model_children[0][i])
for layer in conv_layers:
image = F.conv2d(image,weight=layer.weight,bias=layer.bias,stride=1,padding=0)
outputs.append(image)
names.append(str(layer))
# for featuremap in outputs:
# print(featuremap.shape)
for feature_map in outputs:
feature_map = feature_map.squeeze(0)
gray_scale = torch.sum(feature_map, dim=0)
gray_scale = gray_scale/feature_map.shape[0]
processed.append(gray_scale.data.numpy())
# for fm in processed:
# print(fm.shape)
fig = plt.figure(figsize=(30,30))
for i in range(len(processed)):
a = fig.add_subplot(1, len(processed), i+1)
img_plot = plt.imshow(processed[i])
a.axis('off')
a.set_title(names[i].split('(')[0],fontsize=30)
plt.savefig('conv_layers.png', bbox_inches='tight')
image_conv(image, model)
[Sequential(
(0): Conv2d(3, 96, kernel_size=(11, 11), stride=(4, 4))
(1): ReLU()
(2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
(3): Conv2d(96, 256, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
(4): ReLU()
(5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
(6): Conv2d(256, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(7): ReLU()
(8): Conv2d(384, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(9): ReLU()
(10): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU()
(12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
(13): Flatten(start_dim=1, end_dim=-1)
), Sequential(
(0): Linear(in_features=6400, out_features=4096, bias=True)
(1): ReLU()
(2): Dropout(p=0.5, inplace=False)
(3): Linear(in_features=4096, out_features=4096, bias=True)
(4): ReLU()
(5): Dropout(p=0.5, inplace=False)
(6): Linear(in_features=4096, out_features=10, bias=True)
)]
通过结果我们发现到第5层网络提取出的特征图我们还是能看出来是一个好看的女孩子,这里引发了我的是不是增加卷积层数可以提却更抽象更丰富的特征呢?是不是可以使模型更加强大呢? 下篇我们将介绍ZFNet模型(神经网络可视化鼻祖),以及VGG模型。