又是说在前面:对于卷积层的理解是我目前的理解,可能存在不正确的地方,建议百度去了解原理
代码链接:人工智能实验
文章目录
人工智能实验四:深度学习算法及应用
一、实验目的
- 了解深度学习的基本原理
- 能够使用深度学习开源工具识别图像中的数字
- 了解图像识别的基本原理
二、实验要求
- 解释深度学习原理;
- 对实验性能进行分析;
- 回答思考题;
三、实验的硬件、软件平台
硬件:计算机
软件:操作系统:WINDOWS
应用软件:PyTorch with CUDA, Python, matplotlib
四、实验内容与步骤
安装开源深度学习工具设计并实现一个深度学习模型,它能够学习识别图像中的数字序列。然后使用数据训练它:你可以使用人工合成的数据(推荐),或直接使用现实数据。
五、思考题
深度算法参数的设置对算法性能的影响?
六、实验报告要求
-
对算法原理进行解释;
-
对实验步骤进行详细描述;
-
对实验结果进行分析。
实验步骤
本次实验我使用的是MNIST数据集,使用PyTorch来搭建卷积神经网络实现图像识别。
原理
深度学习是在神经网络的基础上发展而来,其搭建的神经网络的隐层不止有一个,而是有多个。通过将多个线性函数进行组合,并且采用激活函数使其不再仅仅表示线性模型,而是可以做到无限逼近任意的模型,从而完成分类任务。其是在庞大的数据集和强大的计算能力的支持下,学习得到数据内部的关系,从而拟合出相应的能够表示其模型的函数。
本次实验由于是要解决图像识别问题,所以我才用的是卷积神经网络。
卷积神经网络是通过卷积层来对原本图像的特征进行提取,从而得到图像的特征图,再经过全连接神经网络进行学习,进行误差反向传播,更新权值。经过多次训练得到一个效果较好的分类器。
卷积层

考虑一个简单的 4 x 4 的图像(上左图),其每个像素的值即当前像素的灰度值,使用一个 3 x 3 的卷积核(上右图)对其进行卷积操作:使用卷积核覆盖原图像的一个范围,将对应的点的值相乘,最后将这9个像素的值相加,作为一个新的值放入到新的图像中。这一个新的图像就被称为特征图。此步骤之后,移动卷积核,对下一个 3 x 3 的方阵进行同样的操作,直到全部完毕。得到如下的特征图:
卷积核每次移动的范围称为步长,上述的情况中步长为1。
但是对于这种卷积方式,很显然,位于图像边缘的一些点的特征会被丢弃,所以,可以对图像周围用0进行填充:
填充的范围被称作Padding,上图的Padding = 0。
对于一个图像,可以经过不止一个卷积核的卷积,得到多个特征图,特征图的个数与卷积核的个数相等。
记输入图像为 X * X,卷积核为 C * C,步长为 S,Padding为 P ,那么,得到的特征图的尺寸则为 N = W − F + 2 P S + 1 N = \frac{W - F + 2P}{S} + 1 N=SW−F+2P+1
当然,上述的卷积核仅仅是一个最简单的卷积核,除此之外,还有扩张卷积:包含一个叫做扩张率的参数,定义了卷积核内参数间的行(列)间隔数,或者又如转置卷积。
对于要处理的二维图像而言,其不仅有长和宽两个变量,还有一个称为“通道”的变量,如上述的例子,输入的图像可表示为(4, 4, 1),表示长和宽都是4,有1个通道,经过N个上述的 3 x 3 卷积核后,其可以变成(2, 2, N),即有了N个通道。在这里,可以把其看作是N个二维的图像组合而成的一个三维图像。
对于多通道的图像,假定其有N个通道,经过一个 3 x 3 的卷积核,那么这个卷积核也应该为(3, 3, N),使用这一个卷积核对每一个通道相同的位置进行卷积,得到了N个值,将这N个值求和,作为输出图像中的一个值。所以,得到的通道的数目只与卷积核的个数有关。
池化层
从上面卷积层可以看到,我们的目的是对一个图像进行特征提取,最终得到了一个N通道的图像,但是,如果卷积核的数量太多,那么得到的特征图数量也是非常多。这时就需要池化层来降低卷积层输出的特征维度,同时可以防止过拟合现象。
由于图像具有一种“静态性”的属性,也就是在一个图像区域有用的特征极有可能在另一个区域同样有用,所以通过池化层可以来降低图像的维度。
这里只介绍我使用的一般池化的方法。
一般池化包括平均池化和最大池化两种。平均池化即计算图像区域的平均值作为该区域池化后的值;最大池化则是选图像区域的最大值作为该区域池化后的值。
如果选取的池化层为 2 x 2,那么对于一个 4 x 4 的图像能够得到如下的结果:
将一个 4 x 4 的图像降低成了 2 x 2 的图像。其中对应的单元的值,取决于池化的方法。
值得注意的是,池化层不包含需要学习的参数。
学习
最后是学习部分。CNN的学习也是和神经网络一样,先经过前向传播,得到当前的预测值,再计算误差,将误差反向传播,更新全连接层的权值和卷积核的参数。这里存在一个问题,如果卷积核的参数都不同,那么需要更新的参数数量会非常多。为了解决这个问题,采用了参数共享的机制,即对于每一个卷积核,其对每个通道而言权值都是相同的。
这里卷积层的反向传播推导过于复杂,所以我也就不再罗列公式了。
实验代码
本次实验通过PyTorch进行CNN的搭建以及数据的处理。
数据读取
采用了MNIST数据集:
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.1307, ), (0.3081, ))])
# 读入
data_train = datasets.MNIST(root="./data",
transform=transform,
train=True,
download=True)
data_test = datasets.MNIST(root="./data", transform=transform, train=False)
# 按 batch = 128 加载
data_loader_train = torch.utils.data.DataLoader(dataset=data_train,
batch_size=128,
shuffle=True)
data_loader_test = torch.utils.data.DataLoader(dataset=data_test,
batch_size=128,
shuffle=True)
将数据读入后转变为张量,对其进行正则化,正则化的均值和方差的选择则是依据MNIST的标准均值方差。加载之后使用dataloader将其读入,每128个图像作为一个batch加载,使用shuffle将其随机打乱。
CNN的搭建
def __init__(self) -> None:
super(Model, self).__init__()
self.conv1 = nn.Conv2d(1, 8, kernel_size=3, stride=1) # 卷积层1
self.conv2 = nn.Conv2d(8, 32, kernel_size=4, stride=1) # 卷积层2
self.hidden1 = nn.Linear(5 * 5 * 32, 150) # 隐层1 输入5 * 5 * 32(之后会进行解释)
self.hidden2 = nn.Linear(150, 40) # 隐层2 输入150 输出40
self.hidden3 = nn.Linear(40, 10) # 输出层 10分类 输出为10
这里采用了两层卷积层进行特征的提取,第一个卷积层的kernel选择为 3 x 3,步长为1,输出为8通道,对于其输入,由于图像转换为了灰度值为非RGB值,所以输入的图像为1通道,如果是RGB值,则要有3通道。第二个卷积层的kernel则是 4 x 4,步长为1,输出32通道,输入则是8通道。
def forward(self, x): # 28 * 28
x = self.conv1(x) # 26 * 26 * 8
x = F.relu(x)
x = F.max_pool2d(x, 2) # 13 * 13 * 8
x = self.conv2(x) # 10 * 10 * 32
x = F.relu(x)
x = F.max_pool2d(x, 2) # 5 * 5 * 32
x = x.view(-1, 5 * 5 * 32) # 展开 变成一维的张量
# 全连接神经网络
x = self.hidden1(x)
x = F.relu(x)
x = self.hidden2(x)
x = F.relu(x)
x = self.hidden3(x)
return x
这是前向传播的过程,输入的图像是 28 x 28 x 1,经过第一个卷积层之后,由于kernel size = 3,所以其变为
(28 - 3 + 1) x (28 - 3 + 1) x 8 即 26 x 26 x 8,经过一个ReLU激活函数后经过池化层,使用最大池化,size = 2,变为 13 x 13 x 8,在经过第二个卷积层,kernel size = 4,转变为 10 x 10 x 32,同样的,经过ReLU激活函数经过最大池化层,得到 5 x 5 x 32 的特征图,由于之后要进行全连接神经网络,所以将其展开成 5 x 5 x 32 的张量,这也是第一个隐层输入为 5 * 5 * 32 的原因。之后就是经过全连接神经网络。
训练
def __init__(self, lr=0.1, epochs=15): # 默认参数 学习率: 0.1 轮数: 15
self.model = Model() # 加载CNN模型
self.optimizer = optim.SGD(self.model.parameters(), lr=lr) # 随机梯度下降优化器
self.criterion = nn.CrossEntropyLoss() # 交叉熵
self.epochs = epochs # 训练轮数
if torch.cuda.is_available(): # 开启cuda加速计算
self.model.cuda()
self.criterion = self.criterion.cuda()
初始化加载模型,采用SGD作为优化器,依旧由于是分类任务,选择交叉熵作为损失函数。
def fit(self, train: DataLoader, test: DataLoader):
for epoch in range(self.epochs): # 训练轮数
loss = 0.0 # 当前轮数计算得到的损失
acc = 0.0 # 当前轮数的准确率
for i, (data, target) in enumerate(train): # 遍历DataLoader 对每个batch进行训练
X_train, y_train = data, target
if torch.cuda.is_available(): # 转换到cuda
X_train = X_train.cuda()
y_train = y_train.cuda()
X_train, y_train = Variable(X_train), Variable(y_train)
pred = self.model(X_train) # 前向传播
loss_ = self.criterion(pred, y_train) # 计算本次误差
loss += loss_.item() # 总误差更新
self.optimizer.zero_grad() # 优化 导数清0
loss_.backward() # 本次误差反向传播
self.optimizer.step() # 更新权值
for i, (data, target) in enumerate(test): # 测试集 用于计算准确率
X_test, y_test = data, target
if torch.cuda.is_available():
X_test = X_test.cuda()
y_test = y_test.cuda()
X_test, y_test = Variable(X_test), Variable(y_test)
pred = self.model(X_test) # 前向传播
pred = torch.max(pred.data, 1)[1] # 得到预测值 选择最大的一个作为本次的标签
acc += torch.sum(pred == y_test) # 计算标签与实际值相等的个数
# 格式化输出
print('Epoch [{}/{}], Loss: {:.4f}, Acc: {:.4f}'.format(
epoch + 1,
self.epochs,
loss,
100 * acc / (len(data_loader_test) * 128),
))
训练的过程也与神经网络相同。经过每一轮,使用train集的数据进行训练,每次加载一个batch,进行前向传播,得到一个当前的输出值,计算误差,反向传播,更新各部分的权值。计算本次的准确率,并且输出当前轮数训练的进展。
cnn = CNN(lr=0.2, epochs=20)
cnn.fit(data_loader_train, data_loader_test)
在主函数内即可调用训练函数,对CNN进行训练。
预测
while True:
idx = random.randint(0, len(data_test)) # 随机选择一个样本
image, _ = data_test[idx] # 得到当前的值
image = torchvision.utils.make_grid(image)
image = image.numpy().transpose(1, 2, 0) # 转变为可以显示图像
plt.imshow(image) # 显示当前图像
test = torch.utils.data.DataLoader(dataset=data_test[idx], batch_size=1) # 加载当前的图像
ans = cnn.pred(test) # 放入CNN中进行预测
# 将预测值和其标签作为标题进行显示
plt.title("pred = " + str(ans[0]) + " " + "label = " + str(ans[1]))
plt.show() # 展示
在预测部分,则是每次随机的选择一个样本,放入CNN中进行预测,最终同时显示预测的结果和进行图像的展示,用来验证本次实验是否正确完成。
def pred(self, X: DataLoader):
ans = []
if (len(X) == 2): # 只有一个样本
X = list(X) # 将加载的tuple转换为list
train = X[0] # 取出图像的数据 (X[1]为当前图像的标签)
if torch.cuda.is_available: # 转移到gpu
train = train.cuda()
train = Variable(train)
pred = self.model(train) # 前向传播
pred = torch.max(pred.data, 1)[1] # 取出最大的标签作为本次的预测结果
if torch.cuda.is_available:
pred = pred.cpu()
pred = pred.data.numpy()
ans.append(pred[0]) # 添加到返回地数据中
else: # 为了适配不止有一个样本的情况,和上述训练相同,每次加载一个batch 进行预测
for i, (data, target) in enumerate(X):
train = data
if torch.cuda.is_available:
train = train.cuda()
train = Variable(train)
pred = self.model(train)
pred = torch.max(pred.data, 1)[1]
if torch.cuda.is_available:
pred = pred.cpu()
pred = pred.data.numpy()
ans.append(pred[0])
return ans
结果
进行15轮训练,最终在测试集上能够做到98.0123%的准确率。
可见,上述图片都被正确分类。
但是从损失函数来看,最终并未达到收敛,所以还可以继续增大训练轮数,得到更好的分类效果。
思考题
本次实验需要设置的参数具体有:CNN的各部分层数,卷积核的大小,卷积核的数量,卷积层的步长以及Padding,池化层的选择,激活函数的选择。
对于层数,其越深那么得到的模型的准确性越好,但是也会增加训练的时间;
卷积核的大小决定了其提取的特征的数目,但并不是越大越好,和其数量一样,也是需要进行调参才能确定。当然也可以使用 1x1的卷积核进行特征的降维和升维;
卷积层的步长和Padding,对于本次实验而言,采用默认即可以达到较好的效果。但正如之前所说,Padding = 0会使得边缘的特征提取不完全;
为了防止过拟合,可以加入Dropout层,将小于阈值的值赋值为0,进行丢弃。
除此之外,还可以对优化器的学习率进行修改,较小的学习率会使收敛速率较慢,但是较大的学习率又会导致发散和欠拟合。