湖南大学人工智能实验四:深度学习算法及应用

又是说在前面:对于卷积层的理解是我目前的理解,可能存在不正确的地方,建议百度去了解原理

代码链接:人工智能实验

人工智能实验四:深度学习算法及应用

一、实验目的

  1. 了解深度学习的基本原理
  2. 能够使用深度学习开源工具识别图像中的数字
  3. 了解图像识别的基本原理

二、实验要求

  1. 解释深度学习原理;
  2. 对实验性能进行分析;
  3. 回答思考题;

三、实验的硬件、软件平台

硬件:计算机

软件:操作系统:WINDOWS

应用软件:PyTorch with CUDA, Python, matplotlib

四、实验内容与步骤

安装开源深度学习工具设计并实现一个深度学习模型,它能够学习识别图像中的数字序列。然后使用数据训练它:你可以使用人工合成的数据(推荐),或直接使用现实数据。

五、思考题

深度算法参数的设置对算法性能的影响?

六、实验报告要求

  1. 对算法原理进行解释;

  2. 对实验步骤进行详细描述;

  3. 对实验结果进行分析。

实验步骤

本次实验我使用的是MNIST数据集,使用PyTorch来搭建卷积神经网络实现图像识别。

原理

深度学习是在神经网络的基础上发展而来,其搭建的神经网络的隐层不止有一个,而是有多个。通过将多个线性函数进行组合,并且采用激活函数使其不再仅仅表示线性模型,而是可以做到无限逼近任意的模型,从而完成分类任务。其是在庞大的数据集和强大的计算能力的支持下,学习得到数据内部的关系,从而拟合出相应的能够表示其模型的函数。

本次实验由于是要解决图像识别问题,所以我才用的是卷积神经网络。

卷积神经网络是通过卷积层来对原本图像的特征进行提取,从而得到图像的特征图,再经过全连接神经网络进行学习,进行误差反向传播,更新权值。经过多次训练得到一个效果较好的分类器。

卷积层
img

考虑一个简单的 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=SWF+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,进行丢弃。

除此之外,还可以对优化器的学习率进行修改,较小的学习率会使收敛速率较慢,但是较大的学习率又会导致发散和欠拟合。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值