神经网络
一个经典的问题:
手写图像识别, 输入28*28的矩阵(手写数字图像), 输出识别数字
思路: 每一层都提取出一个小特征, 然后在下一层合并成一个稍大的特征
我们用两层中间神经元, 每层16个, 神经元的层数和每层的个数是超参数, 需要自己根据效果调试
第一层就是28*28=748个神经元, 我们再来看第二层. 考虑第二层某一个特定的神经元, 它的数值是由第一层748个决定的, 每一个值除了值本身, 还有一个权重w, 相当于有748个权重(需要训练)和748个值(已知)共同决定这一个神经元的激活值, 它的值域是, 我们想将它映射到, 可以采用sigmoid函数(现在sigmoid已经过时, 我们一般用ReLU, 这个函数是模仿神经元受到刺激时, 只有刺激达到一定阈值,才会激活). 但是对于激活值, 我们可能希望它>10的时候才pick, 我们可以在映射之前减去一个bias 10, 然后用sigmoid函数处理
我们还只考虑了其中的一部分, 如果全局地看这个4层神经网络, 我们会发现有多于13000个参数需要训练! 相当于这个网络上有13000个旋钮开关让你调整!所以我们在讨论机器学习的时候, 实际上是在讨论电脑应该如何设置这一大堆参数, 从而得到正确的结果.
思考一下上面的计算, 我们可以请出线性代数帮助我们, 就像下面
但是神经网络是怎么训练出这些权重和偏置的呢?
如果输入的手写图片的tag为4, 我们希望神经网络的最后一层中, 4的激活值为1, 其它数字的激活值为0. 但实际上, 可能2的值为0.6, 5的值为0.543...这样真实值和神经网络的计算值就产生了一个差值, 我们可以从这个差值入手, 尝试最小化差值. 这是一个误差函数(Loss Function)
与误差函数不同, 我们再来看一下神经网络函数
我们现在看一下误差函数
好, 既然是一个误差函数, 我们就有办法尝试求它的最小值(虽然是13000元函数()
我们看看怎么求一元函数的最值: 梯度下降
先随便找一点, 作出斜率, 看往哪边倒, 点就往哪边走; 以此反复循环, 就至少能找一个极小值. 就像一个小球滚下山坡. 但是有一个大问题, 你的选取的初始点, 和你每次移动点的距离matters, 选得好就能让你直接找到最小值, 但是这些又是超参数. 哭哭
我们对13000元函数肯定不可能真的求梯度, 于是我们会用到一种反向传播算法.
当我们真的训练出来的时候, 我们会发现它表现得很好, 能有96%+的正确率. 现在来思考一下它的原理, 是按照我们最初假设的"每一层都提取出一个小特征"吗, 完全不是! 即使我们把一张没有任何数字的随机图片丢给神经网络, 它会非常"自信"地给出一个判断(某一个数的激活值很大,而其它的激活值都很小, 而非感到"困惑". 这是因为神经网络只是找到了一个函数的"极小值", 这对于识别手写数字足够了, 但是对于其它图片, 则不是神经网络的职责范围了. 这样看来, 随着我们深入的学习, 人工智能好像没有我们想得那么智能?
利用tensorflow实现手写数字识别
好了, 我们理解了原理, 现在来实操一下
步骤 1:设置环境
import tensorflow as tf
from tensorflow.keras import layers, models
import matplotlib.pyplot as plt
步骤 2:准备数据集
TensorFlow提供了一个简单的API来下载和加载MNIST数据集。
# 加载数据集
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
# 将图像的像素值归一化至0到1的范围内
train_images, test_images = train_images / 255.0, test_images / 255.0
步骤 3:构建模型
接下来,你需要构建模型的架构。对于手写数字识别,一个简单的卷积神经网络就足够了。
# 定义序贯模型
model = models.Sequential([
# 将输入数据的形状重塑为神经网络所需的格式
layers.Reshape((28, 28, 1), input_shape=(28, 28)),
# 第一个卷积层
layers.Conv2D(32, (3,3), activation='relu'),
layers.MaxPooling2D((2, 2)),
# 第二个卷积层
layers.Conv2D(64, (3,3), activation='relu'),
layers.MaxPooling2D((2, 2)),
# 展平层,将卷积层的3D输出展平为1D
layers.Flatten(),
# 全连接层
layers.Dense(128, activation='relu'),
# 输出层,共有10个神经元对应10个类别,使用softmax激活函数计算类别概率
layers.Dense(10, activation='softmax')
])
这里看起来有点迷, 我们仔细看看
我会逐行解释“构建模型”部分的代码。在这个阶段,我们正在创建模型的架构或“蓝图”,这将定义模型的各个层和元素。
# 定义序贯模型 model = models.Sequential([
我们开始使用Sequential
模型,这是Keras中一种常用的模型类型,允许我们按顺序定义一系列网络层。它是一个线性堆栈的层。
# 将输入数据的形状重塑为神经网络所需的格式
layers.Reshape((28, 28, 1), input_shape=(28, 28)),
这里我们使用Reshape
层将输入图像的形状从(28, 28)
(这意味着28x28像素的灰度图像)更改为(28, 28, 1)
,这是卷积层所期望的输入形状,其中1
表示图像的通道数量(灰度图像只有1个通道,而RGB彩色图像有3个通道)。
# 第一个卷积层
layers.Conv2D(32, (3,3), activation='relu'),
接下来是我们的第一个卷积层Conv2D
。这是一个卷积层,通常用于提取图像中的特征。在这个层中,我们创建了32个滤波器(也称为卷积核),每个滤波器大小为3x3。relu
是一个激活函数,帮助模型学习非线性特征。
2, 2)),
MaxPooling2D
是池化层,用于降低空间维度(宽度和高度)。这通过选取2x2区域内的最大值来实现,从而在保留重要信息的同时减少数据的维度。
# 第二个卷积层
layers.Conv2D(64, (3,3), activation='relu'),
我们又有一个Conv2D
卷积层,这次有64个3x3大小的滤波器,以捕获更多种类的特征。同样使用relu
激活函数。
2, 2)),
接着是另一个MaxPooling2D
层,进一步降低特征图的维度。
# 展平层,将卷积层的3D输出展平为1D layers.Flatten(),
Flatten
层将前面卷积/池化层产生的3D特征图转换(或“展平”)为1D,这样我们就可以将数据传递到传统的全连接层(Dense)。
# 全连接层
layers.Dense(128, activation='relu'),
Dense
层是一个常规的全连接神经网络层,有128个神经元(或单元)。它可以学习基于先前层提取的特征进行分类的模式。
# 输出层,共有10个神经元对应10个类别,使用softmax激活函数计算类别概率
layers.Dense(10, activation='softmax')
])
最后,另一个Dense
层是输出层,有10个神经元,每个对应一个数字类别(0到9)。我们在这里使用softmax
激活函数,它可以将“分数”转换为概率分布,这对于多类分类来说是非常有用的。
在模型被构建后,它还没有看到任何数据,也就没有进行任何学习。接下来的步骤是编译模型(定义损失函数、优化器和评估指标)并在我们的数字图像上训练
步骤 4:编译模型
在训练模型之前,你需要指定损失函数和优化器,并选择你想要监视的指标:
compile(
optimizer='adam', # 优化器
loss='sparse_categorical_crossentropy', # 损失函数
metrics=['accuracy'] # 评估模型性能的指标
)
我来解释一下
在深度学习中,编译模型是准备模型进行训练的过程。这一步涉及指定模型在训练时使用的损失函数、优化器和评估指标。这些设定将定义如何更新模型的权重,如何评估模型的性能,以及训练的最终目标(即,我们正在尝试最小化的损失函数)。
-
optimizer='adam'
: 优化器决定了模型如何更新其权重。Adam是一种有效的梯度下降变体,它调整学习率以改善训练过程。简单来说,它试图每次迭代时都以最佳方式更新模型的权重。 -
loss='sparse_categorical_crossentropy'
: 损失函数定义了模型的错误程度,即模型的预测与真实标签之间的差异。模型的目标是最小化这个函数,这实际上意味着它正在尝试对训练数据做出更好的预测。对于多类别分类问题,sparse_categorical_crossentropy
是一个很好的选择,因为我们的标签是整数格式(如果你的标签是"one-hot"编码的,你应该使用categorical_crossentropy
)。 -
metrics=['accuracy']
: 这定义了我们想要用哪些指标来评估我们的模型。在这种情况下,我们使用“准确性”,它将计算模型正确预测的标签所占的比例。
步骤 5:训练模型
现在,你可以使用训练数据来“拟合”(训练)你的模型:
# 训练模型
history = model.fit(
train_images,
train_labels,
epochs=5, # 迭代次数
validation_data=(test_images, test_labels) # 验证数据
)
train_images, train_labels
是我们的训练数据集和相应的标签。模型将尝试学习图像与其对应标签之间的关系。epochs=5
表示我们将遍历整个数据集5次。一次迭代(或epoch)是模型看过整个训练集一次并进行了一次权重更新的过程。增加迭代次数可能会提高模型性能,但也可能导致过度拟合,即模型过于专注于训练数据,而无法很好地泛化到未见过的数据。validation_data=(test_images, test_labels)
是我们的测试数据集和相应的标签。这些数据不用于训练,而是用来评估模型在未见过数据上的性能。这有助于我们了解模型是否过度拟合。
训练过程结束后,fit
函数会返回一个history
对象,它包含了训练过程中的信息(例如,每个epoch的损失和其他指标),这对于分析和可视化训练过程非常有用。
步骤 6:评估模型
训练模型后,你应评估模型的性能,看看它实际上如何工作:
# 在测试集上评估模型
test_loss, test_acc = model.evaluate(test_images, test_labels, verbose=2)
print('\nTest accuracy:', test_acc)
步骤 7:可视化
可视化训练过程中的性能指标(例如,准确性)也是很有帮助的,可以让你了解模型的训练是否进展顺利:
# 可视化训练过程中的准确率和损失值
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(history.history['accuracy'], label='accuracy')
plt.plot(history.history['val_accuracy'], label = 'val_accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.ylim([0, 1])
plt.legend(loc='lower right')
plt.subplot(1, 2, 2)
plt.plot(history.history['loss'], label='loss')
plt.plot(history.history['val_loss'], label = 'val_loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.ylim([0, 1])
plt.legend(loc='upper right')
plt.show()
一个手写数字识别神经网络就愉快地学习好啦