前言

本文教程均来自b站【小白也能听懂的人工智能原理】,感兴趣的可自行到b站观看。

代码及工具箱

本专栏的代码和工具函数已经上传到GitHub:1571859588/xiaobai_AI: 零基础入门人工智能 (github.com),可以找到对应课程的代码

正文

图片识别

在机器学习和神经网络的世界里,对于不想深入了解复杂原理的初学者来说,有一个非常适合入门的项目,那就是手写体数字识别。这个任务的目标很明确,就是教会计算机识别手写的数字。

手写体数字识别通常会用到一个叫做MNIST的数据集,它包含了许多28×28像素的手写数字图片。这些图片是灰度图,每个像素点的灰度值从0(黑色)到255(白色)不等,不同的灰度值组合起来就形成了我们看到的数字形状。

但是,手写数字并不像打印的字体那样规整,每次写同一个数字,都可能因为各种原因(比如手抖)而略有不同。这就给计算机识别带来了挑战,因为不能简单地根据像素值来判断数字。

为了解决这个问题,我们可以使用神经网络,它具有一定的容错能力。我们知道如何搭建神经网络,现在的问题是如何把图片数据输入到网络中。

神经网络需要的输入是多维向量,而图片是由像素点组成的二维数组。解决的办法是把图片的每一行像素值依次展开,形成一个一维数组。对于28×28像素的图片,这将形成一个784维的向量,或者说一个有784个元素的数组。我们只需要把MNIST数据集中的每张图片都转换成这样的784维数组,然后输入到神经网络中进行训练。

最初,人们使用深度全连接神经网络来处理这个问题,取得了不错的效果,但还有改进的空间。

训练集和测试集 vs 过拟合和欠拟合

在机器学习中,我们用训练集来训练模型,希望模型在这里表现得很好,意味着它学到了数据的规律。但我们更关心的是模型在新的数据上的表现,这就是测试集的作用。一个好的模型应该在测试集上也有高准确率,这表明它不仅能在训练数据上表现好,也能很好地应对它从未见过的新数据,也就是有好的泛化能力。

如果模型在训练集上表现差,那它基本上就是没用的,这种情况叫做欠拟合,可能是因为模型太简单了。

如果模型在训练集和测试集上都表现得很好,那说明模型泛化能力强。

但如果模型只在训练集上表现好,在测试集上表现差,那就是过拟合了。过拟合可能是因为模型太复杂,学到了训练数据中的噪声,而不是真正的规律。比如,如果我们用一个复杂的模型去拟合一个简单的问题,就像豆豆数据集的例子,模型在训练集上可能非常精确,但在测试集上就表现不佳。

这有点像学生平时做习题死记硬背,考试时却考不好,因为他们没有真正理解知识点。

要解决过拟合,我们可以调整模型结构,或者使用一些技术比如L2正则化、Dropout等。这些方法可以帮助模型更好地泛化。

最后,如果一个模型在训练集上表现差,但在测试集上表现好,这种情况几乎不会发生。

Lecun表格

MNIST数据集是一个包含手写数字的图片集合,它由6万张训练图片和1万张测试图片组成。长期以来,研究人员一直在用各种方法尝试识别这些手写数字。

尽管全连接神经网络在图像识别方面取得了不错的进展,但随着网络深度和神经元数量的增加,性能的提升变得越来越困难。Yann LeCun,一个深度学习领域的领军人物,制作了一个详细的表格,展示了不同机器学习方法在MNIST数据集上的表现,包括线性分类器、K最近邻(KNN)、提升方法、支持向量机(SVM),以及不同类型的神经网络。

评价模型好坏的标准是在测试集上的错误率。只有当模型在测试集上的错误率很低时,我们才认为它具有强大的泛化能力。

到目前为止,纯粹的深度神经网络中表现最好的是一个2010年的6层网络,它达到了0.35%的错误率。这个网络的规模非常大,每层有数千个神经元。研究者们坦率地表示,他们之所以能达到这样的效果,是因为他们拥有强大的计算资源,特别是能加速训练的高性能显卡。

在MNIST手写数字识别问题上,使用纯全连接的深度神经网络似乎已经达到了某种极限。即使我们不断增加网络的大小和复杂度,也很难再有显著的性能提升。这种情况,用现在流行的话来说,就是“内卷”了,意味着单纯的数量增加并不能带来质量的飞跃,而且内卷在任何情况下都是不利的。

为了突破这种局限,研究者们开始尝试另一种类型的神经网络——卷积神经网络(CNN)。卷积神经网络在MNIST数据集上取得了显著的成果。例如,早在1998年提出的LeNet-5,一个经典的卷积神经网络,就能达到99.2%的准确率。到了2012年,准确率更是提高到了99.77%。

卷积神经网络之所以有效,是因为它们能够捕捉图像数据中的局部特征并逐层构建更为复杂和抽象的特征表示,这使得它们在图像识别任务上表现得非常出色。

卷积怎么工作

卷积神经网络(CNN)的工作原理可能听起来有点复杂,但让我们用简单的方式理解它。

想象一下,我们把一张图片转换成了一个由很多数字组成的长列表,这些数字代表了图片中每个像素的亮度。如果我们只看这些数字,很难知道图片上是什么,就像一串数字很难让人联想到一个茶杯或口罩。

人类在看图片时,大脑会自动寻找图片中的特征,比如形状、颜色和纹理。这些特征帮助我们快速识别物体。但是,如果我们把图片转换成一维的数字序列,就会丢失这些有用的空间信息。

举个例子,假设有两种虚构的东西,我们称之为“咕叽咕叽”和“呱啦呱啦”。尽管它们看起来完全不同,但如果我们只看到它们转换后的数字序列,就很难区分它们。

这说明,图片中的空间特征对于识别和分类非常重要。卷积神经网络之所以强大,就是因为它能够保留和利用这些空间特征,而不是简单地将图像信息降维成一维。

通过提取图像的关键特征,CNN能够提高模型的泛化能力,即使面对新的、未见过的数据,也能做出准确的判断。这就是为什么卷积神经网络在图像识别任务上如此有效。

人们为了让神经网络更好地处理图像,引入了卷积运算。卷积运算是这样进行的:

假设我们有一张8×8像素的灰度图像,我们可以把它想象成一个8×8的数字矩阵。然后,我们准备一个3×3的小矩阵,这个小矩阵在卷积神经网络中也被称为卷积核或滤波器。

我们把这个3×3的卷积核放在图像矩阵的左上角,让它与图像的一小部分对齐。然后,我们将卷积核中的每个数字与图像对应位置的数字相乘,并将这些乘积相加,得到一个新的数值。

接下来,我们把卷积核向右移动一个像素,重复上述乘法和加法的过程,得到新的数值。我们继续这样水平移动卷积核,直到覆盖了图像的整行。

之后,我们把卷积核移回图像的左侧,向下移动一行,然后再次重复这个过程,从左向右进行卷积运算。我们继续这样垂直移动卷积核,直到覆盖了图像的所有行和列。

通过这种方式,卷积核在图像上滑动,通过数学上的乘法和加法操作提取图像的特征。这个过程就是图像卷积操作的基本步骤,它帮助神经网络识别和理解图像中的细节和模式。

卷积运算通过在图像上滑动一个小矩阵(或卷积核/过滤器)来产生一个新的图像。这个新图像展示了原始图像中某些特征的强调或变化。

以一个8×8像素的灰度图像为例,我们用一个3×3的卷积核在图像上滑动,每次将卷积核覆盖的图像区域的像素值与卷积核对应位置的值相乘并求和,得到一个新的数值。这样,我们一步步移动卷积核,直到覆盖整个图像,最终生成一个6×6的新图像。

在这个过程中,如果计算出的某个像素值超过了255,我们会将它限制在255,因为像素值的取值范围是0到255。这样,我们就得到了一个经过卷积处理后的图像,它可能突出了原始图像中的某些特征,比如边缘、纹理或颜色变化。

卷积核的作用就像是一个过滤器,它能够捕捉图像中的特定模式。不同的卷积核可以提取不同类型的特征,这就是为什么卷积运算能够帮助我们识别图像中的轮廓、花纹或颜色等特征。通过这种方式,卷积神经网络能够在图像识别任务中表现出色。

我们可以用一个比较好理解的卷积和来简单的说明这个问题。我们的图像是这样的,大家想一下用这个卷集合卷完之后会是什么样的效果呢?没错,把垂直的边缘给提取出来的。

这个卷积合的作用实际上是用来做垂直边缘的提取,这个大图比较麻烦,我们用两张8×8的小图来看一下细节,这是他们用这个卷积和卷积的结果,你会发现结果图片都开始显现,垂直条纹的特征。

让我们通过一个简单的例子来理解卷积运算是如何工作的。假设我们有一张小图像,中间有一个明显的垂直边缘,比如一个杯子的侧边。

在卷积过程中,我们会用一个3×3的卷积核覆盖图像的每个部分。假设卷积核的左侧是1,右侧是-1,中间是0。当我们将这个卷积核应用到图像上时,会发现:

  • 图像左侧的像素灰度值都是60,由于卷积核左侧是1,中间是0,右侧是-1,卷积运算结果会相互抵消,导致左侧的卷积结果都是0,看起来很暗。
  • 同理,图像右侧的像素灰度值都是0,卷积结果也是0,同样看起来很暗。
  • 但在中间的边缘部分,由于左侧的像素值大于右侧,当它们与卷积核的1和-1相乘时,不会完全抵消。例如,三个60乘以1再减去三个0乘以-1,结果是180。因此,边缘的卷积结果是180,看起来明亮。

如果我们将卷积后的结果可视化,我们会看到图像中间出现了一个明亮的垂直边界。这是因为卷积核能够捕捉到图像中灰度值变化的区域,这些区域通常是边缘的特征。

卷积核通过其特定的数值排列,能够突出显示图像中的特定特征,如垂直边缘。当卷积核在图像上滑动时,它能够检测到灰度值的局部变化,并将这些变化转化为新图像中的亮点,从而帮助我们识别图像中的重要特征。

卷积运算通过特定的卷积核(或滤波器)来突出图像中的边缘特征。当卷积核设计为左侧值大、右侧值小或相反时,它会强化图像中从暗到亮(或从亮到暗)的过渡,这就是边缘。

想象一下,如果卷积核左侧是正值,右侧是负值,那么当它滑过图像中的边缘时,左侧会捕捉到较暗的像素(正值乘以暗像素),右侧捕捉到较亮的像素(负值乘以亮像素)。这样,乘积累加后的结果会很大,因为左侧的暗像素和右侧的亮像素不会相互抵消,而是相加,从而在新图像中创建一个明亮的边界。

反之,如果图像区域是均匀的,即左侧和右侧的像素亮度相同,那么它们与卷积核相乘后的结果会相互抵消,导致卷积后的图像在这些区域看起来很暗。

通过改变卷积核的形状和值,我们可以检测不同类型的边缘,比如水平或垂直边缘。例如,一个设计用来检测垂直边缘的卷积核会有一列正值和一列负值,而用来检测水平边缘的卷积核则会有一行这样的值。

使用这样的卷积核在图像上滑动,可以清晰地突出图像中的水平或垂直线条,而与检测方向垂直的线条则会在卷积后的效果中消失,因为它们在卷积运算中被抵消了。这就是卷积运算如何帮助我们在图像中检测和强化特定方向的边缘。

实际上卷积是一种在图像处理领域非常常见的操作,现在很多图像处理软件中都会利用到卷积运算给图片加上效果。

让我们通过一个简单的例子来理解卷积如何在手写数字识别中起作用。

假设我们有手写数字1的图片,如果我们稍微向右移动一点再写一个1,它看起来和第一个1很相似。但如果我们在第一个1的上方加一横,它就变成了数字7。

对于全连接的深度神经网络来说,当图片被转换成数字数组后,第二个1(稍微向右移动的)和数字7在数组中的许多特征值可能非常接近,因为它们都包含相似的笔画结构。然而,第二个1和原始的1在数组中的特征值可能会有很大差异,因为第二个1相对原始1发生了位置变化,这导致它们在数组中对应位置的数值不再相似。

这说明,尽管人眼可以轻易识别出第二个1仍然是1,但全连接网络可能因为位置的微小变化而难以识别,因为它依赖于像素值的精确匹配。卷积神经网络通过使用卷积层来捕捉图像中的局部特征和空间关系,可以更有效地处理这种位置变化,从而在手写数字识别任务中表现得更好。

但如果我用这样的一个卷积和,对这两个1和这个7进行卷积便得到这样的结果,很明显这个卷积和提取的是横向的边界,所以此刻到底谁更像谁就发生了令人愉快的改变

编程实验

好的,同学们,我们开始做本节课的编程实验,那本次编程实验呢我们就用凯瑞斯搭建一个深度神经网络,来尝试一下经典的mnist的数据集,那关于卷积神经网络的使用,我们在下一节课完整的讲述卷积神经网络的细节之后再来实现。

  • 加载数据集:mnist数据集,为手写数字的图片数据集,训练数据60000张,测试数据10000张,每张图片是28*28像素的黑白图片

    from keras.datasets import mnist
    (X_train, Y_train), (X_test, Y_test) = mnist.load_data()
    
  • 数据预处理

    • 图像数据抽平到一阶张量,比如mnist数据集是28*28像素的黑白图片,就抽平为$28 \times 28=784$ 维向量。

      X_train = X_train.reshape(X_train.shape[0], -1)
      X_test = X_test.reshape(X_test.shape[0], -1)
      
    • 为了方便梯度下降收敛,需要把像素点的大小归一化到0-1之间

      X_train = X_train/255
      X_test = X_test/255
      
    • 对Y标签进行独热编码,这是为了输出层可以多分类。数据集是0-9的数字手写,所以就是10维向量来存储类别,0用[1, 0, 0, 0, 0, 0, 0, 0, 0, 0]表示,2用[0, 0, 1, 0, 0, 0, 0, 0, 0, 0] 表示。这样输出层也输出10个值,第几个神经元的输出值最大,就可以认为是哪一类了np.argmax(Y_test_pre[i])

      Y_train = to_categorical(Y_train, 10)
      Y_test = to_categorical(Y_test, 10)
      
  • 网络构建

    • 网络拓扑图

    • 三层隐藏层,每层256个神经元,激活函数选择ReLU函数;
      输出层选择Softmax,使得输出的十个值总和为1。

      model = Sequential()
      model.add(Dense(units=256, activation='relu', input_dim=784))
      model.add(Dense(units=256, activation='relu'))
      model.add(Dense(units=256, activation='relu')) 
      model.add(Dense(units=10, activation='softmax'))  # 输出层用softmax函数
      model.compile(loss='categorical_crossentropy', optimizer=SGD(
          learning_rate=0.05), metrics=['accuracy'])  # 多分类问题使用交叉熵做代价函数
      
  • 全部代码

    from keras.utils import to_categorical
    import numpy as np
    import matplotlib.pyplot as plt
    from keras.models import Sequential
    from keras.layers import Dense
    from keras.optimizers import SGD
    from keras.models import load_model
    from keras.datasets import mnist
    
    (X_train, Y_train), (X_test, Y_test) = mnist.load_data()
    X_train = X_train.reshape(X_train.shape[0], -1)/255
    X_test = X_test.reshape(X_test.shape[0], -1)/255
    
    Y_train = to_categorical(Y_train, 10)
    Y_test = to_categorical(Y_test, 10)
    

创建模型

model = Sequential()
model.add(Dense(units=256, activation='relu', input_dim=784))
model.add(Dense(units=256, activation='relu'))
model.add(Dense(units=256, activation='relu')) 
model.add(Dense(units=10, activation='softmax'))  # 输出层用softmax函数
model.compile(loss='categorical_crossentropy', optimizer=SGD(learning_rate=0.05), metrics=['accuracy'])  # 多分类问题使用交叉熵做代价函数

开始训练

model.fit(X_train, Y_train, epochs=5000, batch_size=2000, verbose=1)

训练完毕,查看loss和accuracy

loss, accuracy = model.evaluate(X_test, Y_test)
print(f"{loss=}")
print(f"{accuracy=}")

保存模型

* 识别效果:训练集达到了100%正确率,测试集则只有97.82%
  model.save('./model/10_model.h5')