先介绍一个简单的例子
要手动实现一个简单的卷积神经网络(CNN)来判断手写数字(1-10),我们可以使用 Python 和 TensorFlow(或其他深度学习框架)。以下是一个简单的实现思路,其中包含了手动构建卷积层、池化层、全连接层等。
假设你已经有了手写数字数据集,比如 MNIST
数据集(0-9的数字),然后可以使用 CNN 来进行分类。
安装依赖
首先,你需要安装 TensorFlow:
pip install tensorflow
手动实现CNN
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import tensorflow as tf from tensorflow.keras import layers, models import numpy as np import matplotlib.pyplot as plt from tensorflow.keras.datasets import mnist# 加载数据 (x_train, y_train), (x_test, y_test) = mnist.load_data()# 数据预处理:将数据标准化为 [0, 1],并且 reshape 为 (batch_size, 28, 28, 1) x_train = np.expand_dims(x_train, -1).astype('float32') / 255.0 x_test = np.expand_dims(x_test, -1).astype('float32') / 255.0# 修改标签,使其适应10个数字分类(0-9) y_train = tf.keras.utils.to_categorical(y_train, 10) y_test = tf.keras.utils.to_categorical(y_test, 10)# 创建模型 model = models.Sequential()# 第一层:卷积层,卷积核大小 3x3,使用 32 个卷积核,激活函数为 ReLU model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1))) # 第二层:池化层,最大池化,池化窗口 2x2 model.add(layers.MaxPooling2D((2, 2)))# 第三层:卷积层,卷积核大小 3x3,使用 64 个卷积核,激活函数为 ReLU model.add(layers.Conv2D(64, (3, 3), activation='relu')) # 第四层:池化层,最大池化,池化窗口 2x2 model.add(layers.MaxPooling2D((2, 2)))# 第五层:卷积层,卷积核大小 3x3,使用 128 个卷积核,激活函数为 ReLU model.add(layers.Conv2D(128, (3, 3), activation='relu')) # 第六层:池化层,最大池化,池化窗口 2x2 model.add(layers.MaxPooling2D((2, 2)))# 将三维数据展平为一维 model.add(layers.Flatten())# 全连接层:128 个神经元,激活函数 ReLU model.add(layers.Dense(128, activation='relu'))# 输出层:10 个神经元,softmax 激活函数用于多分类问题 model.add(layers.Dense(10, activation='softmax'))# 编译模型 model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])# 训练模型 model.fit(x_train, y_train, epochs=5, batch_size=64, validation_data=(x_test, y_test))# 评估模型 test_loss, test_acc = model.evaluate(x_test, y_test) print(f"Test accuracy: {test_acc}")# 使用模型进行预测 predictions = model.predict(x_test)# 显示某一张预测结果 plt.imshow(x_test[0].reshape(28, 28), cmap='gray') plt.title(f"Predicted: {np.argmax(predictions[0])}, True: {np.argmax(y_test[0])}") plt.show()
代码解释:
- 数据预处理:将 MNIST 数据集加载并归一化至 [0, 1] 的范围,同时将输入形状修改为
(28, 28, 1)
,表示每张图片为 28x28 的灰度图。 - 模型结构:
- 卷积层:每一层使用 3x3 的卷积核,第一层有 32 个卷积核,第二层有 64 个,第三层有 128 个卷积核。
- 池化层:使用最大池化,池化窗口为 2x2,目的是减少特征的空间尺寸。
- 全连接层:将卷积层的输出展平,并通过一个全连接层进行分类。
- 训练:使用
Adam
优化器和categorical_crossentropy
损失函数,进行 5 个周期的训练。 - 评估:在测试集上评估准确度,并显示一张预测图及其真实标签。
进一步优化:
- 可以尝试不同的卷积核数量、层数或其他超参数。
- 数据增强(如旋转、缩放等)可以提高模型的泛化能力。
关于类别:
MNIST
数据集只包含数字0-9
,如果要添加其他数字,可以考虑合并数据集,或创建一个新的数据集,标记为 1-10。
问题:池化层是不是必须的?一个卷积层是不是必须跟一个池化层?
不是每个卷积层都必须跟一个池化层。卷积层和池化层的组合通常是为了减少空间维度(即图像的宽度和高度)以及增加模型的特征提取能力,但这并不是强制性的。
解释:
- 卷积层(Conv Layer):负责从输入数据中提取特征(如边缘、纹理等),通常会有多个卷积核(滤波器)来提取不同的特征。
- 池化层(Pooling Layer):通常用于下采样(即减少特征图的空间维度),通过最大池化(Max Pooling)或平均池化(Average Pooling)来减小图像的尺寸,从而减轻计算负担并提高模型的鲁棒性。
什么时候不需要池化层:
- 保持空间分辨率:如果你希望保持图像的空间分辨率,或者如果数据本身的空间信息非常重要,你可能就不需要池化层。
- 增加计算量:池化操作会减少特征图的空间维度,这样在后续的卷积层或全连接层中减少计算量。如果你不希望丧失过多的空间信息,也可以选择不使用池化层。
- 全卷积网络(FCN):在某些网络结构(如全卷积网络)中,可能没有池化层,或者池化层的作用通过其他方法来替代(例如,直接通过卷积步长来减少空间尺寸)。
例如:
你可以将卷积层堆叠起来,并在某些情况下不使用池化层,只是通过控制卷积步长(stride)来减小特征图的尺寸。例如:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
model = models.Sequential()# 第一层:卷积层,卷积核大小 3x3,使用 32 个卷积核,步长为 2 model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1), strides=2))# 第二层:卷积层,卷积核大小 3x3,使用 64 个卷积核,步长为 2 model.add(layers.Conv2D(64, (3, 3), activation='relu', strides=2))# 第三层:卷积层,卷积核大小 3x3,使用 128 个卷积核,步长为 2 model.add(layers.Conv2D(128, (3, 3), activation='relu', strides=2))# 展平层 model.add(layers.Flatten())# 全连接层 model.add(layers.Dense(128, activation='relu')) model.add(layers.Dense(10, activation='softmax'))
在这个例子中,我们没有使用池化层,而是通过卷积层的 strides
参数来减小特征图的尺寸。池化层可以帮助减少计算量和避免过拟合,但并不是必需的,特别是在一些特定的网络结构中。如果你的任务对空间分辨率要求较高,或者使用其他策略来减少空间维度,完全可以省略池化层。
当我们进行图像分类任务时,例如使用卷积神经网络(CNN)对手写数字进行分类,预测过程通常包括以下步骤:
- 数据预处理
- 模型前向传播
- 输出结果解释
我们可以通过一个具体的预测例子,来逐步解释每个阶段。
假设我们有一个训练好的 CNN 模型,目标是预测一张手写数字图片(例如 MNIST 数据集中的数字 3)。
步骤 1:数据预处理
首先,我们需要对输入图像进行适当的预处理。原始图像通常是 28x28 的灰度图像,像素值范围是从 0 到 255。
处理步骤:
- 缩放:将像素值缩放到 [0, 1] 之间。这是因为深度学习模型通常更容易处理小范围的输入。
- 形状调整:为了适应 CNN 输入,我们需要将输入图像的形状调整为
(28, 28, 1)
,即加上一个通道维度(灰度图是单通道)。形状解释(height(高度), width(宽度), channels(通道或深度))
步骤 2:模型前向传播
现在我们将图像输入到 CNN 模型中,进行前向传播,计算出每个类别的预测概率。
-
卷积层:模型的第一层是一个卷积层,它会使用若干个卷积核(如 32 个卷积核,每个卷积核大小为 3x3)对图像进行卷积操作。每个卷积核会扫描整个图像,提取局部特征(如边缘、纹理等)。
-
池化层:卷积层之后,通常会加上池化层来对特征进行下采样(即减小图像的空间维度),使得计算量减少,同时保留最重要的特征。
-
卷积与池化层的堆叠:模型的中间可能还会有更多的卷积层和池化层,这些层会逐步提取越来越复杂的特征。
-
展平层(Flatten):卷积和池化操作后,输出的特征图被展平为一个一维向量,作为全连接层的输入。
-
全连接层:在展平后的特征向量上,我们通常有一两个全连接层,最终的全连接层会将特征映射到每个数字类别的概率。
-
输出层:最后一个全连接层使用 softmax 激活函数,计算每个类别的概率。softmax 输出的是一个长度为 10 的向量,每个值表示该类的预测概率。
假设我们的模型是如下定义的:
# 导入需要的模块 from tensorflow.keras import layers, models# 初始化一个顺序模型(Sequential model) model = models.Sequential()# 添加第一个卷积层:32个3x3的卷积核,激活函数为ReLU,输入形状为28x28x1(灰度图像) model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))# 添加最大池化层:2x2的池化窗口 model.add(layers.MaxPooling2D((2, 2)))# 添加第二个卷积层:64个3x3的卷积核,激活函数为ReLU model.add(layers.Conv2D(64, (3, 3), activation='relu'))# 添加最大池化层:2x2的池化窗口 model.add(layers.MaxPooling2D((2, 2)))# 添加第三个卷积层:128个3x3的卷积核,激活函数为ReLU model.add(layers.Conv2D(128, (3, 3), activation='relu'))# 添加最大池化层:2x2的池化窗口 model.add(layers.MaxPooling2D((2, 2)))# 添加展平层(Flatten):将三维的特征图展平为一维向量 model.add(layers.Flatten())# 添加全连接层:128个神经元,激活函数为ReLU model.add(layers.Dense(128, activation='relu'))# 添加输出层:10个神经元,激活函数为softmax,用于分类任务 model.add(layers.Dense(10, activation='softmax'))
现在,我们可以用模型对测试集中的一张图片进行预测:
# 进行预测 predictions = model.predict(np.expand_dims(image, axis=0))# 输出预测结果 print(predictions[0])
predictions[0]
会是一个包含 10 个值的数组,每个值代表该类别的概率。
步骤 3:输出结果解释
假设 predictions[0]
输出的结果是:
[0.02, 0.05, 0.10, 0.60, 0.03, 0.08, 0.02, 0.01, 0.02, 0.01]
这表示:
- 数字 0 的预测概率是 2%
- 数字 1 的预测概率是 5%
- 数字 2 的预测概率是 10%
- 数字 3 的预测概率是 60%
- 数字 4 的预测概率是 3%
- 数字 5 的预测概率是 8%
- 数字 6、7、8、9 的预测概率很低
由于数字 3 的预测概率最高(60%),所以模型最终预测这张图片是数字 3。
问题:每一个卷积层中的N个卷积核他们是提前设置好的吗?他们是怎么来的?
每一层中的卷积核(filters 或者称为卷积权重)并不是提前设定好的,而是在训练过程中通过反向传播算法不断调整的。具体来说,卷积核的初始值是随机设置的,然后在模型训练过程中,通过优化算法(如梯度下降)来逐步调整这些卷积核的值,使得模型能够更好地从输入数据中学习到有用的特征。
具体过程:
-
初始化卷积核:在模型开始训练时,每个卷积核的值通常是通过某种初始化方法(如高斯分布或均匀分布)随机设置的。通常我们会确保卷积核的初始值不会太大或太小,以避免梯度消失或梯度爆炸的问题。
-
前向传播(Forward Pass):在训练过程中,输入数据(例如手写数字图像)会通过网络进行前向传播,经过每个卷积层时,卷积核会对输入进行卷积运算,提取特征。卷积层的输出会传递到下一层,直到输出层得到最终的预测结果。
-
计算损失(Loss Calculation):在前向传播结束后,网络会根据实际的标签(真实值)和预测结果计算损失函数(例如交叉熵损失)。损失值表示模型的预测与实际结果之间的差异。
-
反向传播(Backward Pass):通过反向传播算法,网络会计算出每个参数(包括卷积核)对损失函数的贡献,也就是计算每个卷积核的梯度。这些梯度表示了卷积核调整的方向和大小,即它们需要如何更新才能更好地减少损失。
-
更新卷积核(Weight Update):根据计算出的梯度,优化算法(例如 Adam 或 SGD)会通过某种学习率来调整卷积核的权重。学习率控制着每次更新的步伐大小。通过反复多次的训练和更新,卷积核的值会逐渐变得更适合当前任务,提取出更有效的特征。
-
迭代过程:这一过程会在多个训练周期(Epochs)中不断重复。每次训练时,卷积核的权重都会根据反向传播和优化步骤进行调整,直到模型收敛,即损失值达到最小值。
卷积核如何学习到特征?
- 低级特征:在神经网络的早期层(靠近输入层的卷积层),卷积核通常会学习到一些基本的、低级的特征,例如边缘、角点、纹理等。这些特征对图像理解非常重要。
- 高级特征:在更深的卷积层(靠近输出层的卷积层),卷积核会逐渐组合低级特征,学习到更复杂、更抽象的特征。例如,网络可能会学习到一个卷积核专门用于识别数字的某个特定部分,如圆形(数字 0)或直线(数字 1)等。
在卷积神经网络中,当我们使用多个卷积核(filters)进行卷积操作时,每个卷积核会生成一个 特征图(feature map)。如果你在卷积层使用了 32 个卷积核,那么经过卷积操作后,你会得到 32 个特征图。
假设:
- 输入图像大小是
(28, 28, 1)
(28x28 像素,单通道灰度图像)。 - 你使用了 32 个卷积核,每个卷积核大小是 3x3,步长(stride)是 1,且没有填充(padding)或使用的是有效填充(valid padding)。
1. 卷积操作之后:
经过卷积操作后,由于使用了 32 个卷积核,所以输出的特征图数量为 32。每个特征图的尺寸会根据卷积操作的设置(卷积核大小、步长、填充方式)来决定。
在没有填充的情况下,卷积操作会将输入的 (28, 28, 1)
图像变成 (26, 26, 32)
的特征图。这里的 26 是通过以下公式计算得到的:
假设卷积核大小是 3x3,步长是 1,输入大小是 28:
2. 池化操作之后:
假设你对卷积层的输出应用了一个最大池化(Max Pooling)操作,通常池化层的窗口大小是 2x2,步长是 2。池化操作会减小每个特征图的尺寸,但不改变特征图的数量。
- 池化操作会将每个 26x26 的特征图缩小一半。因为步长是 2,所以池化后的尺寸会变为:
池化后,特征图的尺寸会变为 13x13。因此,经过池化层之后,你会得到 32 个 13x13 的特征图,形状是 (13, 13, 32)
。
池化过程可能导致一定程度的特征丢失,尤其是在使用较大的池化窗口或进行多次池化时。但是,池化层的主要目的是为了 降低计算复杂度、减少空间维度 和 提高模型的鲁棒性。在大多数情况下,尽管池化可能会丢失一些细节信息,但它能够保留输入数据中的最重要的特征,从而有助于模型的泛化能力。
为什么池化不会太大问题?
-
保留最重要的特征:
- 池化操作,尤其是最大池化(Max Pooling),从每个池化区域中保留最大值,这通常意味着池化后会保留该区域的最显著特征。在图像处理中,显著的特征(如边缘、角点等)通常是分类或识别的关键。
- 即使池化会丢失一些细节信息,它通常会保留最能区分类别的关键信息。
-
降低过拟合:
- 池化通过减少特征图的空间维度,减少了网络中的参数数量和计算量。更少的参数通常意味着模型更难以过拟合训练数据,从而提升模型的泛化能力。
- 池化的这种降维效果有助于模型更好地捕捉高层次的抽象特征,而不是依赖于图像中的低级细节。
-
池化的层级性:
- 在一个深度神经网络中,池化通常应用在卷积层之后,卷积层已经通过滤波器提取了低级特征(如边缘、角落、纹理等)。池化层通过减少这些特征图的尺寸来保留它们的主要信息,而不会影响图像中的重要模式。
- 随着网络逐渐加深,网络将学习到越来越抽象的特征,因此池化不会影响最终的分类效果。
-
非线性层补充特征学习:
- 即便池化会丢失一些细节信息,卷积层和后续的全连接层通常会通过非线性激活函数(如 ReLU)和进一步的卷积操作来弥补这一点。这些层能够帮助模型通过更复杂的计算来提取和补充丢失的特征。
如何平衡池化带来的特征丢失?
-
减小池化窗口:可以使用较小的池化窗口(如 2x2 而不是 3x3),从而减少每次池化对特征图尺寸的减小,并尽可能保留更多信息。
-
减少池化层的数量:有些模型设计中可以减少池化层的使用,或者选择更适合任务的池化方式,例如全局平均池化(Global Average Pooling),这种方式能够减少过多的特征丢失,同时减少参数量。
-
使用卷积替代池化:
- 另一个做法是使用卷积层的 步长(stride) 来代替池化操作,通过增加卷积操作的步长来减小特征图的空间尺寸。这样可以避免池化层的特征丢失问题,但计算量通常会增加。
- 例如,有些网络架构,如 ResNet 或 Inception,通过增加卷积层的步长来替代池化层。
池化层虽然有可能丢失一些信息,但它的主要作用是 提取显著特征、减少计算复杂度 和 提高模型的鲁棒性。通常来说,适当的池化不会对模型的表现产生严重影响,反而有助于防止过拟合。如果担心丢失特征,可以通过调节池化窗口的大小、池化层的数量或使用其他替代方法(如步长卷积)来平衡特征提取和降维的效果。
展平层(Flatten) 的作用就是将 多维的特征图(feature maps) 转换为 一维向量,并将其传递给全连接层(Dense Layer)。具体来说,展平层会把卷积层或池化层的输出的所有通道的特征图展开成一个长向量,而不再保留其空间结构(宽、高、深度)。
详细解释:
假设我们在卷积层和池化层后得到的输出是一个形状为 (height, width, channels)
的张量,展平层会将这个三维张量转换为一个一维向量。
举个例子:
假设我们经过卷积和池化之后,输出的特征图的形状是 (13, 13, 32)
,其中:
13
是特征图的高度,13
是特征图的宽度,32
是特征图的通道数(也就是卷积核的数量)。
展平层会将这个 13x13x32
的三维张量展平为一个 一维向量,该向量的长度为:
这个 5408 长度的向量会被输入到全连接层(Dense Layer)中。
展平过程图示:
假设我们有 2 个通道(channel 1
和 channel 2
),每个通道的特征图大小为 2x2
。经过池化后,我们得到的特征图形状是 (2, 2, 2)
,也就是 2 个 2x2 的特征图。
Input (before Flatten): [ [[1, 2], [3, 4]], # channel 1[[5, 6], [7, 8]] # channel 2 ]
展平后:
Output (after Flatten):
[1, 2, 3, 4, 5, 6, 7, 8]
展平层的作用就是把这些值都展开成一个一维数组,丧失了原有的空间结构,但保留了每个特征图的所有信息,并将其传递给全连接层进行进一步处理。
连接全连接层:
一旦展平层将特征图展平为一维向量,接下来的全连接层(Dense Layer)就可以对这些特征进行更复杂的组合。全连接层的每个神经元都会与展平后的向量中的所有值连接,进行加权求和,然后通过激活函数输出结果。