本文是使用VGG-16网络在MNIST 数据集上实现图片分类
一、网络结构可视化(PlotNeuralNet工具)
详细教程可以看这篇文章
面向Python的PlotNeuralNet教程 | 图数据科学实验室 (tcsds.github.io)
VGG-16是一种经典的卷积神经网络(CNN),可以很好地用于图像分类任务。它由13个卷积层和3个全连接层组成,因此得名VGG-16。
当你将一张图像输入到VGG-16模型中时,它会经历以下几个步骤进行处理:
1. 卷积层(Convolutional Layers):
- VGG-16模型包含五个卷积层,每个卷积层都包括多个卷积核。
- 这些卷积核会在输入图像上滑动,并对局部区域进行特征提取。卷积操作将原始输入图像转换为一系列特征图(feature maps),其中每个特征图对应一个卷积核的输出。
- 通过堆叠多个卷积层,网络可以逐渐学习到更加复杂和抽象的特征。
2. ReLU激活函数(Rectified Linear Unit):
- 在每个卷积层和全连接层之后,通常会添加激活函数来引入非线性。VGG-16中使用的是ReLU激活函数,它能够加速网络的收敛速度并且减轻梯度消失问题。
3. 池化层(Pooling Layers):
- 池化层用于降低特征图的空间维度,减少参数数量和计算量。
- VGG-16的池化操作是最大池化(MaxPooling),它在每个区域内选择最大值作为输出。
- 通过池化操作,网络可以保留重要的特征并丢弃不重要的信息,从而增强网络的平移不变性。
4. 全连接层(Fully Connected Layers):
- 经过一系列卷积和池化操作之后,特征图被展平为一维向量,并输入到全连接层。
- 全连接层由多个神经元组成,每个神经元与前一层的所有神经元相连接。
- 全连接层通过学习将图像特征映射到类别标签上,输出每个类别的概率分布。
5. Dropout层:
- 为了减少过拟合,VGG-16中的全连接层之间添加了Dropout层。
- Dropout层在训练过程中随机丢弃一定比例的神经元输出,从而降低模型对训练数据的过度依赖,提高泛化能力。
通过这些层的组合和堆叠,VGG-16模型可以逐渐提取图像的多层次特征,从低级的边缘和纹理到高级的语义信息,并且在大规模图像分类任务中取得了很好的性能, 最终实现对图像的准确分类。
import sys
sys.path.append('../')
from pycore.tikzeng import *
# defined your arch
arch = [
to_head( '..' ), # 生成.tex文件位置
to_cor(),
to_begin(),
#input
to_input( './1.jpg',to='(-10,0,0)', width=30, height=30), # 显示输入图像,比较美观
# 该层的图像大小, 输出通道大小, 表示这一层与上一层分别在x,y,z上的偏移量,一般只需要调整x,表示该层在x,y,z方向上的坐标
# 这一层在pool1层的右边,后三个都是视觉效果
to_ConvConvRelu(name='layer1', s_filer=32, n_filer=(64, 64), offset="(0,0,0)", to="(0,0,0)", width=(5, 5), height=120, depth=120,caption='layer1'),
to_Pool("pool1", offset="(0,0,0)",to="(layer1-east)",width=1, height=60, depth=60,opacity=0.5),
to_ConvConvRelu(name='layer2', s_filer=16, n_filer=(128, 128), offset="(10,0,0)", to="(0,0,0)", width=(5, 5), height=100, depth=100,caption='layer2'),
to_Pool("pool2", offset="(0,0,0)",to="(layer2-east)",width=1, height=50, depth=50,opacity=0.5),
to_connection("pool1", "layer2"),
to_ConvConvRelu("layer3", n_filer=(256, 256), offset="(10,0,0)", to="(pool2-east)", height=80, depth=80, width=(4,4),caption="layer3" ),
to_ConvConvRelu("layer3-1", n_filer=(256, 256), offset="(0,0,0)", to="(layer3-east)", height=80, depth=80, width=(4,0) ),
to_Pool("pool3", offset="(0,0,0)",to="(layer3-1-east)",width=1,height=40, depth=40,opacity=0.5),
to_connection("pool2", "layer3"),
to_ConvConvRelu("layer4", s_filer=4, n_filer=(512, 512), offset="(10,0,0)", to="(pool3-east)", height=60, depth=60, width=(4,4),caption="layer4" ),
to_ConvConvRelu("layer4-1", s_filer=4, n_filer=(512, 512), offset="(0,0,0)", to="(layer4-east)", height=60, depth=60, width=(4,0) ),
to_Pool("pool4", offset="(0,0,0)",to="(layer4-1-east)",width=1,height=30, depth=30,opacity=0.5),
to_connection("pool3", "layer4"),
to_ConvConvRelu("layer5", s_filer=2, n_filer=(512, 512), offset="(10,0,0)", to="(pool4-east)", height=40, depth=40, width=(4,4),caption="layer5" ),
to_ConvConvRelu("layer5-1", s_filer=2, n_filer=(512, 512), offset="(0,0,0)", to="(layer5-east)", height=40, depth=40, width=(4,0) ),
to_Pool("pool5", offset="(0,0,0)",to="(layer5-1-east)",width=1,height=20, depth=20,opacity=0.5),
to_connection("pool4", "layer5"),
to_Conv("fc1", 512, 512, offset="(5,0,0)", to="(pool5-east)", height=30, depth=30, width=30 ),
to_connection("pool5", "fc1"),
to_Conv("fc2", 512, 256, offset="(5,0,0)", to="(fc1-east)", height=30, depth=30, width=30 ),
to_connection("fc1", "fc2"),
to_Conv("fc3", 256, 10, offset="(5,0,0)", to="(fc2-east)", height=30, depth=30, width=30 ),
to_connection("fc2", "fc3"),
to_SoftMax("soft1", 10 ,"(3,0,0)", "(fc3-east)", caption="SOFT" ),
to_connection("fc3", "soft1"),
to_end()
]
def main():
namefile = str(sys.argv[0]).split('.')[0]
to_generate(arch, namefile + '.tex' )
if __name__ == '__main__':
main()
二、Loss - acc curve graph
【分析】:对VGG-16网络模型一共训练十轮,每一轮训练完,将训练好的模型放到验证集上计算准确率。
由图可知,在对模型进行不断训练的过程中,损失值实现了收敛,并最终趋近于0,表明模型的训练效果较好。每一轮,将训练好的模型放在验证集上进行验证,准确率稳定在99%左右,并最终趋近于1,表明模型的图像分类效果好,具有很高的准确率。
import torch
import numpy as np
from matplotlib import pyplot as plt
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision import datasets
import torch.nn.functional as F
from torch import nn
# super parameters
batch_size = 64 # 每次模型更新参数时用到的样本数目
learning_rate = 0.01 # 控制模型参数更新的步长大小
momentum = 0.5 # SGD中的冲量
EPOCH = 10 # 遍历整个训练数据集的次数
# transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
transform = transforms.Compose([
transforms.Resize((32, 32)), # 将图片大小调整为需要的大小以匹配网络输入
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
train_dataset = datasets.MNIST(root='./data/mnist', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data/mnist', train=False, download=True, transform=transform) # train=True训练集,=False测试集
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) # 载入数据集
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
class Vgg16_net(nn.Module):
def __init__(self):
super(Vgg16_net, self).__init__()
self.layer1=nn.Sequential(
nn.Conv2d(in_channels=1,out_channels=64,kernel_size=3,stride=1,padding=1), #(32-3+2)/1+1=32 32*32*64
nn.BatchNorm2d(64),
#inplace-选择是否进行覆盖运算
#意思是是否将计算得到的值覆盖之前的值,比如
nn.ReLU(inplace=True),
#意思就是对从上层网络Conv2d中传递下来的tensor直接进行修改,
#这样能够节省运算内存,不用多存储其他变量
nn.Conv2d(in_channels=64,out_channels=64,kernel_size=3,stride=1,padding=1), #(32-3+2)/1+1=32 32*32*64
#Batch Normalization强行将数据拉回到均值为0,方差为1的正太分布上,
# 一方面使得数据分布一致,另一方面避免梯度消失。
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2,stride=2) #(32-2)/2+1=16 16*16*64
)
self.layer2=nn.Sequential(
nn.Conv2d(in_channels=64,out_channels=128,kernel_size=3,stride=1,padding=1), #(16-3+2)/1+1=16 16*16*128
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=128,out_channels=128,kernel_size=3,stride=1,padding=1), #(16-3+2)/1+1=16 16*16*128
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.MaxPool2d(2,2) #(16-2)/2+1=8 8*8*128
)
self.layer3=nn.Sequential(
nn.Conv2d(in_channels=128,out_channels=256,kernel_size=3,stride=1,padding=1), #(8-3+2)/1+1=8 8*8*256
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=256,out_channels=256,kernel_size=3,stride=1,padding=1), #(8-3+2)/1+1=8 8*8*256
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=256,out_channels=256,kernel_size=3,stride=1,padding=1), #(8-3+2)/1+1=8 8*8*256
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.MaxPool2d(2,2) #(8-2)/2+1=4 4*4*256
)
self.layer4=nn.Sequential(
nn.Conv2d(in_channels=256,out_channels=512,kernel_size=3,stride=1,padding=1), #(4-3+2)/1+1=4 4*4*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(4-3+2)/1+1=4 4*4*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(4-3+2)/1+1=4 4*4*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.MaxPool2d(2,2) #(4-2)/2+1=2 2*2*512
)
self.layer5=nn.Sequential(
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(2-3+2)/1+1=2 2*2*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(2-3+2)/1+1=2 2*2*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(2-3+2)/1+1=2 2*2*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.MaxPool2d(2,2) #(2-2)/2+1=1 1*1*512
)
self.conv=nn.Sequential(
self.layer1,
self.layer2,
self.layer3,
self.layer4,
self.layer5
)
self.fc=nn.Sequential(
#y=xA^T+b x是输入,A是权值,b是偏执,y是输出
#nn.Liner(in_features,out_features,bias)
#in_features:输入x的列数 输入数据:[batchsize,in_features]
#out_freatures:线性变换后输出的y的列数,输出数据的大小是:[batchsize,out_features]
#bias: bool 默认为True
#线性变换不改变输入矩阵x的行数,仅改变列数
nn.Linear(512,512),
nn.ReLU(inplace=True),
nn.Dropout(0.5),
nn.Linear(512,256),
nn.ReLU(inplace=True),
nn.Dropout(0.5),
nn.Linear(256,10)
)
def forward(self,x):
x=self.conv(x)
#这里-1表示一个不确定的数,就是你如果不确定你想要reshape成几行,但是你很肯定要reshape成512列
# 那不确定的地方就可以写成-1
#如果出现x.size(0)表示的是batchsize的值
# x=x.view(x.size(0),-1)
x = x.view(-1, 512)
x=self.fc(x)
return x
# 初始化模型、损失函数和优化器
model = Vgg16_net()
criterion = torch.nn.CrossEntropyLoss() # 交叉熵损失函数
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, momentum=momentum) # 随机梯度下降更新参数,lr学习率,momentum冲量
def train(epoch):
running_loss = 0.0 # 这整个epoch的loss清零
total_loss = 0
total_batches = len(train_loader) # 获取总批次数
# 遍历 train_loader 中的每个批次进行训练
for batch_idx, data in enumerate(train_loader, 0):
# 从数据中获取输入和目标标签
inputs, target = data
# 清除之前参数的梯度
optimizer.zero_grad()
# forward + backward + update
# 用输入数据进行模型的前向传播,得到输出
outputs = model(inputs)
# 计算模型输出与目标标签之间的损失
loss = criterion(outputs, target)
# 执行反向传播和参数更新,以最小化损失
loss.backward()
optimizer.step()
# 把运行中的loss累加起来,为了下面300次一除
running_loss += loss.item()
total_loss += loss.item()
if batch_idx % 300 == 299: # 不想要每一次都出loss,浪费时间,选择每300次出一个平均损失,和准确率
print('[%d, %5d]: loss: %.3f'
% (epoch + 1, batch_idx + 1, running_loss / 300))
running_loss = 0.0 # 这小批300的loss清零
average_loss = total_loss / total_batches # 计算平均损失
print('[%d / %d]: loss on training set: %f ' % (epoch+1, EPOCH, loss)) # 求测试的准确率,正确数/总数
return average_loss
def test():
correct = 0
total = 0
with torch.no_grad(): # 告诉 PyTorch 不需要计算梯度
for data in test_loader: # 遍历测试数据集中的每个批次
images, labels = data
outputs = model(images)
# 预测的类别
_, predicted = torch.max(outputs.data, dim=1) # dim = 1 列是第0个维度,行是第1个维度,沿着行(第1个维度)去找1.最大值和2.最大值的下标
total += labels.size(0) # 总样本数
# 张量之间的比较运算
correct += (predicted == labels).sum().item() # 总样本数中预测正确的个数
acc = correct / total
print('[%d / %d]: Accuracy on test set: %.1f %% ' % (epoch+1, EPOCH, 100 * acc)) # 求测试的准确率,正确数/总数
return acc
if __name__ == '__main__':
acc_list_test = []
loss_list = []
for epoch in range(EPOCH):
loss = train(epoch)
loss_list.append(loss)
# if epoch % 10 == 9: #每训练10轮 测试1次
acc_test = test()
acc_list_test.append(acc_test)
# 保存模型参数
torch.save(model, "./VGGNet_model.pth")
# 绘制损失曲线
plt.plot(loss_list, label='Training Loss')
# 绘制准确率曲线
plt.plot(acc_list_test, label='Accuracy On TestSet')
plt.xlabel('Epoch')
plt.ylabel('Value')
plt.legend() # 添加图例
# 保存图像
plt.savefig('./visualization/loss_and_accuracy.png')
plt.show()
三、Average Gradient visualization(10 epochs)
【分析】:我们将每轮训练中各层的平均梯度进行可视化(不包括激活函数)。平均梯度是指某一层神经网络参数的梯度的平均值。平均梯度在神经网络训练过程中具有重要意义,以下是其主要作用:
- 衡量参数更新的大小:平均梯度可以反映参数更新的幅度。如果某一层的平均梯度较大,说明模型在该层的参数更新较为剧烈;反之,如果平均梯度较小,说明参数更新较为缓慢。通过监控平均梯度的变化,可以调整学习率等超参数,以控制参数更新的速度,避免训练过程中出现梯度爆炸或梯度消失。
- 诊断训练过程中的问题:平均梯度可以帮助诊断神经网络训练过程中的问题。例如,如果某一层的平均梯度持续为零或接近零,可能表示该层的参数未能有效更新,可能存在梯度消失或梯度爆炸的问题;如果某一层的平均梯度过大,可能导致模型不稳定,需要调整网络结构或优化算法。
- 指导模型设计和调参:通过分析不同层的平均梯度,可以指导神经网络的设计和调参。例如,可以根据平均梯度的大小调整层的初始化方法、选择合适的激活函数、调整学习率等,以优化模型的训练效果和泛化性能。
import torch
import numpy as np
from matplotlib import pyplot as plt
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision import datasets
import torch.nn.functional as F
from torch import nn
# super parameters
batch_size = 64 # 每次模型更新参数时用到的样本数目
learning_rate = 0.01 # 控制模型参数更新的步长大小
momentum = 0.5 # SGD中的冲量
EPOCH = 10 # 遍历整个训练数据集的次数
# transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
transform = transforms.Compose([
transforms.Resize((32, 32)), # 将图片大小调整为需要的大小以匹配网络输入
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
train_dataset = datasets.MNIST(root='./data/mnist', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data/mnist', train=False, download=True, transform=transform) # train=True训练集,=False测试集
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) # 载入数据集
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
class Vgg16_net(nn.Module):
def __init__(self):
super(Vgg16_net, self).__init__()
self.layer1=nn.Sequential(
nn.Conv2d(in_channels=1,out_channels=64,kernel_size=3,stride=1,padding=1), #(32-3+2)/1+1=32 32*32*64
nn.BatchNorm2d(64),
#inplace-选择是否进行覆盖运算
#意思是是否将计算得到的值覆盖之前的值,比如
nn.ReLU(inplace=True),
#意思就是对从上层网络Conv2d中传递下来的tensor直接进行修改,
#这样能够节省运算内存,不用多存储其他变量
nn.Conv2d(in_channels=64,out_channels=64,kernel_size=3,stride=1,padding=1), #(32-3+2)/1+1=32 32*32*64
#Batch Normalization强行将数据拉回到均值为0,方差为1的正太分布上,
# 一方面使得数据分布一致,另一方面避免梯度消失。
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2,stride=2) #(32-2)/2+1=16 16*16*64
)
self.layer2=nn.Sequential(
nn.Conv2d(in_channels=64,out_channels=128,kernel_size=3,stride=1,padding=1), #(16-3+2)/1+1=16 16*16*128
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=128,out_channels=128,kernel_size=3,stride=1,padding=1), #(16-3+2)/1+1=16 16*16*128
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.MaxPool2d(2,2) #(16-2)/2+1=8 8*8*128
)
self.layer3=nn.Sequential(
nn.Conv2d(in_channels=128,out_channels=256,kernel_size=3,stride=1,padding=1), #(8-3+2)/1+1=8 8*8*256
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=256,out_channels=256,kernel_size=3,stride=1,padding=1), #(8-3+2)/1+1=8 8*8*256
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=256,out_channels=256,kernel_size=3,stride=1,padding=1), #(8-3+2)/1+1=8 8*8*256
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.MaxPool2d(2,2) #(8-2)/2+1=4 4*4*256
)
self.layer4=nn.Sequential(
nn.Conv2d(in_channels=256,out_channels=512,kernel_size=3,stride=1,padding=1), #(4-3+2)/1+1=4 4*4*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(4-3+2)/1+1=4 4*4*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(4-3+2)/1+1=4 4*4*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.MaxPool2d(2,2) #(4-2)/2+1=2 2*2*512
)
self.layer5=nn.Sequential(
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(2-3+2)/1+1=2 2*2*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(2-3+2)/1+1=2 2*2*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(2-3+2)/1+1=2 2*2*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.MaxPool2d(2,2) #(2-2)/2+1=1 1*1*512
)
self.conv=nn.Sequential(
self.layer1,
self.layer2,
self.layer3,
self.layer4,
self.layer5
)
self.fc=nn.Sequential(
#y=xA^T+b x是输入,A是权值,b是偏执,y是输出
#nn.Liner(in_features,out_features,bias)
#in_features:输入x的列数 输入数据:[batchsize,in_features]
#out_freatures:线性变换后输出的y的列数,输出数据的大小是:[batchsize,out_features]
#bias: bool 默认为True
#线性变换不改变输入矩阵x的行数,仅改变列数
nn.Linear(512,512),
nn.ReLU(inplace=True),
nn.Dropout(0.5),
nn.Linear(512,256),
nn.ReLU(inplace=True),
nn.Dropout(0.5),
nn.Linear(256,10)
)
def forward(self,x):
x=self.conv(x)
#这里-1表示一个不确定的数,就是你如果不确定你想要reshape成几行,但是你很肯定要reshape成512列
# 那不确定的地方就可以写成-1
#如果出现x.size(0)表示的是batchsize的值
# x=x.view(x.size(0),-1)
x = x.view(-1, 512)
x=self.fc(x)
return x
# 初始化模型、损失函数和优化器
model = Vgg16_net()
criterion = torch.nn.CrossEntropyLoss() # 交叉熵损失函数
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, momentum=momentum) # 随机梯度下降更新参数,lr学习率,momentum冲量
# 新增函数用于可视化梯度
def plot_grad_flow(named_parameters):
ave_grads = []
layers = []
for n, p in named_parameters:
if p.requires_grad and "bias" not in n:
layers.append(n.replace('.weight', ''))
ave_grads.append(p.grad.abs().mean())
plt.bar(np.arange(len(ave_grads)), ave_grads, alpha=0.5, lw=1, color="c") # 参数alpha设置了直方图的透明度,lw设置了边框宽度,
plt.hlines(0, 0, len(ave_grads) + 1, lw=2, color="k")
plt.xticks(range(0, len(ave_grads), 1), layers, rotation="vertical")
plt.xlim(left=-1, right=len(ave_grads))
plt.ylim(bottom=0) # 设置y轴的上下限
plt.xlabel("Layers")
plt.ylabel("average gradient")
plt.title("Gradient flow")
plt.tight_layout()
plt.savefig(f'./visualization/gradient_flow_epoch_{epoch}.png')
plt.show()
def train(epoch):
running_loss = 0.0
total_loss = 0
total_batches = len(train_loader)
for batch_idx, data in enumerate(train_loader, 0):
inputs, target = data
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, target)
loss.backward()
optimizer.step()
running_loss += loss.item()
total_loss += loss.item()
if batch_idx % 300 == 299:
print('[%d, %5d]: loss: %.3f'
% (epoch + 1, batch_idx + 1, running_loss / 300))
running_loss = 0.0
average_loss = total_loss / total_batches
print('[%d / %d]: loss on training set: %f ' % (epoch+1, EPOCH, loss))
# 每个epoch结束时调用可视化梯度函数
plot_grad_flow(model.named_parameters())
return average_loss
def test():
correct = 0
total = 0
with torch.no_grad():
for data in test_loader:
images, labels = data
outputs = model(images)
_, predicted = torch.max(outputs.data, dim=1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
acc = correct / total
print('[%d / %d]: Accuracy on test set: %.1f %% ' % (epoch+1, EPOCH, 100 * acc))
return acc
if __name__ == '__main__':
acc_list_test = []
loss_list = []
for epoch in range(EPOCH):
loss = train(epoch)
loss_list.append(loss)
acc_test = test()
acc_list_test.append(acc_test)
四、Convolutional layer visualization
【分析】:VGG-16的共有5个卷积层(Layer1 - Layer5),卷积核的数目分别为64,128,256,512,512,我们将每个卷积核可视化。在卷积层的图像中,可以看到有些部分是暗的,而其他部分是亮的。像素值的范围从 0 到 255,0 对应于全黑,255 对应于白色。这意味着暗色块的权重低于亮色块。当输入图像发生仿射变换时,白块将更负责激活图像的该部分。当对权重与像素值进行元素乘积时,模型将更多地关注权重值更多的图像区域。具体而言:
卷积核的亮和暗部分反映了它对输入图像中不同特征的响应程度。
1. 亮区域:
- 高权重区域:卷积核中亮的部分通常对应于具有较高权重的区域,这意味着这些区域在卷积操作中会对输入图像的特定特征做出更强烈的响应。例如,对于边缘检测卷积核,亮部分可能对应于边缘的位置。
- 重要特征区域:卷积核中的亮部分可能表示在特定任务中具有更重要性的特征。在图像分类任务中,这些亮部分可能对应于能够有效区分不同类别的关键图像特征。
2. 暗区域:
- 低权重区域:卷积核中的暗部分通常对应于权重较低的区域,这意味着这些区域在卷积操作中对输入图像的响应相对较弱。
- 次要特征区域:暗部分可能对应于输入图像中次要的或不太重要的特征,或者对于特定任务并不具有区分性的区域。
import torch
import matplotlib.pyplot as plt
from torch import nn
import numpy as np
class Vgg16_net(nn.Module):
def __init__(self):
super(Vgg16_net, self).__init__()
self.layer1=nn.Sequential(
nn.Conv2d(in_channels=1,out_channels=64,kernel_size=3,stride=1,padding=1), #(32-3+2)/1+1=32 32*32*64
nn.BatchNorm2d(64),
#inplace-选择是否进行覆盖运算
#意思是是否将计算得到的值覆盖之前的值,比如
nn.ReLU(inplace=True),
#意思就是对从上层网络Conv2d中传递下来的tensor直接进行修改,
#这样能够节省运算内存,不用多存储其他变量
nn.Conv2d(in_channels=64,out_channels=64,kernel_size=3,stride=1,padding=1), #(32-3+2)/1+1=32 32*32*64
#Batch Normalization强行将数据拉回到均值为0,方差为1的正太分布上,
# 一方面使得数据分布一致,另一方面避免梯度消失。
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2,stride=2) #(32-2)/2+1=16 16*16*64
)
self.layer2=nn.Sequential(
nn.Conv2d(in_channels=64,out_channels=128,kernel_size=3,stride=1,padding=1), #(16-3+2)/1+1=16 16*16*128
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=128,out_channels=128,kernel_size=3,stride=1,padding=1), #(16-3+2)/1+1=16 16*16*128
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.MaxPool2d(2,2) #(16-2)/2+1=8 8*8*128
)
self.layer3=nn.Sequential(
nn.Conv2d(in_channels=128,out_channels=256,kernel_size=3,stride=1,padding=1), #(8-3+2)/1+1=8 8*8*256
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=256,out_channels=256,kernel_size=3,stride=1,padding=1), #(8-3+2)/1+1=8 8*8*256
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=256,out_channels=256,kernel_size=3,stride=1,padding=1), #(8-3+2)/1+1=8 8*8*256
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.MaxPool2d(2,2) #(8-2)/2+1=4 4*4*256
)
self.layer4=nn.Sequential(
nn.Conv2d(in_channels=256,out_channels=512,kernel_size=3,stride=1,padding=1), #(4-3+2)/1+1=4 4*4*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(4-3+2)/1+1=4 4*4*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(4-3+2)/1+1=4 4*4*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.MaxPool2d(2,2) #(4-2)/2+1=2 2*2*512
)
self.layer5=nn.Sequential(
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(2-3+2)/1+1=2 2*2*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(2-3+2)/1+1=2 2*2*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(2-3+2)/1+1=2 2*2*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.MaxPool2d(2,2) #(2-2)/2+1=1 1*1*512
)
self.conv=nn.Sequential(
self.layer1,
self.layer2,
self.layer3,
self.layer4,
self.layer5
)
self.fc=nn.Sequential(
#y=xA^T+b x是输入,A是权值,b是偏执,y是输出
#nn.Liner(in_features,out_features,bias)
#in_features:输入x的列数 输入数据:[batchsize,in_features]
#out_freatures:线性变换后输出的y的列数,输出数据的大小是:[batchsize,out_features]
#bias: bool 默认为True
#线性变换不改变输入矩阵x的行数,仅改变列数
nn.Linear(512,512),
nn.ReLU(inplace=True),
nn.Dropout(0.5),
nn.Linear(512,256),
nn.ReLU(inplace=True),
nn.Dropout(0.5),
nn.Linear(256,10)
)
def forward(self,x):
x=self.conv(x)
#这里-1表示一个不确定的数,就是你如果不确定你想要reshape成几行,但是你很肯定要reshape成512列
# 那不确定的地方就可以写成-1
#如果出现x.size(0)表示的是batchsize的值
# x=x.view(x.size(0),-1)
x = x.view(-1, 512)
x=self.fc(x)
return x
model = torch.load('./VGGNet_model.pth')
for name, module in model.named_children():
if 'layer' in name: # 层的名字,排除全连接层
filters = module[0].weight.cpu().detach().numpy() # 获取卷积层的权重
num_filters = len(filters)
print("Total number of filters in layer", name, ":", num_filters)
# 计算适合的行数和列数
num_rows = int(np.ceil(np.sqrt(num_filters)))
num_cols = int(np.ceil(num_filters / num_rows))
plt.figure(figsize=(20, 17))
for i in range(num_filters):
plt.subplot(num_rows, num_cols, i+1)
plt.axis('off')
plt.imshow(filters[i][0, :, :], cmap='gray')
plt.savefig(f"./visualization/{name}_kernel.png")
# plt.show()
五、Feature map visualization
【分析】:VGG-16 共有13层卷积,我们得到经过每层卷积的特征图。通过特征图,可以看到不同的卷积层进行卷积操作时关注的不同方面。由于卷积层的相应权重,一些特征图专注于图像的背景,另一些特征图专注于图像的轮廓。
特征图中的亮和暗区域可以提供关于神经网络学习到的特征以及输入图像的一些信息。
1. 亮区域:
- 高激活区域: 亮区域通常表示神经网络对输入图像的某些部分或特征感兴趣,并且在这些区域上有较高的激活值。这可能意味着网络识别到了输入图像中具有重要性的特征或模式。
- 边缘或纹理: 亮区域可能对应于输入图像中的边缘、纹理或其他细节特征。这些特征对于对象识别和分类任务通常是很重要的。
2. 暗区域:
- 低激活区域: 暗区域可能表示神经网络对输入图像的某些部分或特征不太感兴趣,并且在这些区域上有较低的激活值。这可能意味着网络认为这些区域对于当前的任务不太重要。
- 背景或无关部分: 暗区域可能对应于输入图像中的背景或与任务无关的部分。对于对象识别任务,网络可能更关注目标对象的特征,而忽略背景信息。
import torch
from torchsummary import summary
from torch import nn
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt
class Vgg16_net(nn.Module):
def __init__(self):
super(Vgg16_net, self).__init__()
self.layer1=nn.Sequential(
nn.Conv2d(in_channels=1,out_channels=64,kernel_size=3,stride=1,padding=1), #(32-3+2)/1+1=32 32*32*64
nn.BatchNorm2d(64),
#inplace-选择是否进行覆盖运算
#意思是是否将计算得到的值覆盖之前的值,比如
nn.ReLU(inplace=True),
#意思就是对从上层网络Conv2d中传递下来的tensor直接进行修改,
#这样能够节省运算内存,不用多存储其他变量
nn.Conv2d(in_channels=64,out_channels=64,kernel_size=3,stride=1,padding=1), #(32-3+2)/1+1=32 32*32*64
#Batch Normalization强行将数据拉回到均值为0,方差为1的正太分布上,
# 一方面使得数据分布一致,另一方面避免梯度消失。
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2,stride=2) #(32-2)/2+1=16 16*16*64
)
self.layer2=nn.Sequential(
nn.Conv2d(in_channels=64,out_channels=128,kernel_size=3,stride=1,padding=1), #(16-3+2)/1+1=16 16*16*128
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=128,out_channels=128,kernel_size=3,stride=1,padding=1), #(16-3+2)/1+1=16 16*16*128
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.MaxPool2d(2,2) #(16-2)/2+1=8 8*8*128
)
self.layer3=nn.Sequential(
nn.Conv2d(in_channels=128,out_channels=256,kernel_size=3,stride=1,padding=1), #(8-3+2)/1+1=8 8*8*256
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=256,out_channels=256,kernel_size=3,stride=1,padding=1), #(8-3+2)/1+1=8 8*8*256
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=256,out_channels=256,kernel_size=3,stride=1,padding=1), #(8-3+2)/1+1=8 8*8*256
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.MaxPool2d(2,2) #(8-2)/2+1=4 4*4*256
)
self.layer4=nn.Sequential(
nn.Conv2d(in_channels=256,out_channels=512,kernel_size=3,stride=1,padding=1), #(4-3+2)/1+1=4 4*4*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(4-3+2)/1+1=4 4*4*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(4-3+2)/1+1=4 4*4*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.MaxPool2d(2,2) #(4-2)/2+1=2 2*2*512
)
self.layer5=nn.Sequential(
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(2-3+2)/1+1=2 2*2*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(2-3+2)/1+1=2 2*2*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=3,stride=1,padding=1), #(2-3+2)/1+1=2 2*2*512
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.MaxPool2d(2,2) #(2-2)/2+1=1 1*1*512
)
self.conv=nn.Sequential(
self.layer1,
self.layer2,
self.layer3,
self.layer4,
self.layer5
)
self.fc=nn.Sequential(
#y=xA^T+b x是输入,A是权值,b是偏执,y是输出
#nn.Liner(in_features,out_features,bias)
#in_features:输入x的列数 输入数据:[batchsize,in_features]
#out_freatures:线性变换后输出的y的列数,输出数据的大小是:[batchsize,out_features]
#bias: bool 默认为True
#线性变换不改变输入矩阵x的行数,仅改变列数
nn.Linear(512,512),
nn.ReLU(inplace=True),
nn.Dropout(0.5),
nn.Linear(512,256),
nn.ReLU(inplace=True),
nn.Dropout(0.5),
nn.Linear(256,10)
)
def forward(self,x):
x=self.conv(x)
#这里-1表示一个不确定的数,就是你如果不确定你想要reshape成几行,但是你很肯定要reshape成512列
# 那不确定的地方就可以写成-1
#如果出现x.size(0)表示的是batchsize的值
# x=x.view(x.size(0),-1)
x = x.view(-1, 512)
x=self.fc(x)
return x
model = torch.load('./VGGNet_model.pth')
model_weights = [] # append模型的权重
conv_layers = [] # append模型的卷积层本身
# get all the model children as list
model_children = list(model.children())
# counter to keep count of the conv layers
counter = 0 # 统计模型里共有多少个卷积层
# print(model_children[0][0].weight)
# append all the conv layers and their respective wights to the list
for i in range(len(model_children)): # 遍历最表层(Sequential就是最表层)
if type(model_children[i]) == nn.Conv2d: # 最表层只有一个卷积层
counter+=1
model_weights.append(model_children[i].weight)
conv_layers.append(model_children[i])
elif type(model_children[i]) == nn.Sequential:
for child in model_children[i]:
if type(child) == nn.Conv2d:
counter+=1
model_weights.append(child.weight)
conv_layers.append(child)
print(f"Total convolution layers: {counter}")
outputs = []
names = []
image = Image.open('./1.jpg')
transform = transforms.Compose([
transforms.Resize((32, 32)), # 将图片大小调整为需要的大小以匹配网络输入
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
image = image.convert('L')
image = transform(image)
# print(f"Image shape before: {image.shape}")
image = image.unsqueeze(0)
# print(f"Image shape after: {image.shape}")
for layer in conv_layers[0:]: # conv_layers即是存储了所有卷积层的列表
image = layer(image) # 每个卷积层对image做计算,得到以矩阵形式存储的图片,需要通过matplotlib画出
outputs.append(image)
names.append(str(layer))
# print(len(outputs))
# for feature_map in outputs:
# print(feature_map.shape)
# print(outputs[1].shape)
# print(outputs[1].squeeze(0).shape) # 去掉 batch_size 的维度,因为matplotlib绘画,这个第0维没用
# print(torch.sum(outputs[1].squeeze(0),0).shape) # 再次 .squeeze 将颜色通道这个维度去除, sum是把几十上百张灰度图像映射到一张
processed = []
for feature_map in outputs:
feature_map = feature_map.squeeze(0) # torch.Size([1, 64, 112, 112]) —> torch.Size([64, 112, 112]) 去掉第0维 即batch_size维
gray_scale = torch.sum(feature_map,0) # sum是把几十上百张灰度图像映射到一张,从彩色图片变为黑白图片 压缩64个颜色通道维度,否则feature map太多张
gray_scale = gray_scale / feature_map.shape[0] # 除以通道数求平均值
processed.append(gray_scale.data.cpu().numpy()) # .data是读取Variable中的tensor .cpu是把数据转移到cpu上 .numpy是把tensor转为numpy
# for fm in processed:
# print(fm.shape)
fig = plt.figure(figsize=(30, 50))
for i in range(len(processed)): # len(processed) = 17
a = fig.add_subplot(5, 4, i+1)
img_plot = plt.imshow(processed[i])
a.axis("off") # 关闭子图 a 的坐标轴显示
a.set_title(f'Conv2d-{i+1}', fontsize=30) # names[i].split('(')[0] 结果为Conv2d
plt.savefig('./visualization/feature_maps.jpg', bbox_inches='tight') # 若不加bbox_inches='tight',保存的图片可能不完整
六、 Parameters visualization
【分析】:这个表格是关于VGG-16的结构描述。其中:
- Layer (type): 层的类型,例如Conv2d表示卷积层,BatchNorm2d表示批归一化层,ReLU表示激活函数等。
- Output Shape: 层的输出形状,即该层的输出的维度。
- Param #: 该层的参数数量,其中Conv2d的参数数量为卷积核的数量乘以卷积核的大小再加上偏置项的数量。
根据输出我们可以看到:
- 这个神经网络总共有95个层(包括卷积层、批归一化层、ReLU激活函数、池化层和全连接层等)。
- 输入图像的尺寸为32x32,经过一系列的卷积、批归一化、激活函数、池化等操作,最终得到了一个大小为512的特征向量。
- 神经网络的总参数数量为29,840,522个。其中可训练的参数数量(Trainable params)也是29,840,522个,这些是模型通过训练学习到的可调整的参数。
- 神经网络的参数大小为113.83MB。