在前面的文章中,我们已经掌握了MindSpore的基础知识,包括Tensor、nn.Cell、数据处理等。从本篇开始,我们将正式进入激动人心的模型构建部分,首先要学习的就是在计算机视觉(CV)领域大放异彩的------卷积神经网络(Convolutional Neural Network, CNN)。
1. CNN是什么
把CNN想象成一个拥有"火眼金睛"的图像识别专家。它不像我们之前接触的简单网络那样"一视同仁"地看待所有像素,而是通过模仿人类视觉系统的方式,逐层地从图像中提取特征,从最基础的边缘、颜色,到更复杂的纹理、形状,最终识别出图像的内容。
本篇文章将作为您进入CNN世界的第一站,详细拆解构成CNN的几个最核心的"零件"------即MindSpore中的关键神经网络层。
2. CNN的核心"零件"
在MindSpore中,构建一个CNN网络就像搭乐高积木一样,我们需要从mindspore.nn模块中拿出各种功能的"积木块"(神经网络层),然后将它们有序地拼接起来。下面,我们就来逐一认识这些核心"积木块"。
2.1 卷积层 (nn.Conv2d):特征提取器
卷积层是CNN的灵魂,它负责从输入图像中提取特征。
-
工作原理 :想象你有一个"滤镜"(称为卷积核 或滤波器 ),这个滤镜非常小,比如
3x3大小。你将这个滤镜覆盖在输入图像的左上角,计算滤镜覆盖区域的像素与滤镜自身值的加权和,得到一个输出值。然后,你将滤镜向右移动一个"步长"(stride),重复计算,直到扫过整行。接着,下移一个步长,继续扫描,最终生成一张新的、尺寸更小的图像,这张图就叫做特征图(Feature Map)。这个过程就模拟了大脑识别物体边缘、角落等局部特征的方式。 -
关键参数:
in_channels(int): 输入通道数。对于RGB彩色图像,就是3。out_channels(int): 输出通道数,也等于卷积核的数量。每个卷积核可以学习提取一种不同的特征,所以输出通道数越多,提取的特征就越丰富。kernel_size(int or tuple): 卷积核的大小。常用的有3(代表3x3) 或(3, 5)。stride(int): 卷积核移动的步长。默认为1。pad_mode(str): 填充模式。'same'模式会在图像周围自动填充0,使得输出特征图的尺寸与输入大致相同;'valid'模式则不填充,输出尺寸会变小。
-
代码示例:
python
import mindspore
from mindspore import nn, Tensor
import numpy as np
# 假设我们有一个 1x1x5x5 的单通道图像 (N, C, H, W)
# N: 批量大小, C: 通道数, H: 高度, W: 宽度
input_image = Tensor(np.ones([1, 1, 5, 5]), mindspore.float32)
# 定义一个卷积层:输入1通道,输出2通道,卷积核3x3,步长1
# pad_mode='valid'表示不填充
conv_layer = nn.Conv2d(in_channels=1, out_channels=2, kernel_size=3, stride=1, pad_mode='valid')
# 将图像输入卷积层
output_feature_map = conv_layer(input_image)
print("输入图像尺寸:", input_image.shape)
print("输出特征图尺寸:", output_feature_map.shape)
# (5-3)/1 + 1 = 3, 所以输出尺寸是 1x2x3x3
2.2 激活函数 (nn.ReLU):引入非线性
如果只有卷积层,无论叠加多少层,其效果都等同于一个线性变换,这无法让网络学习复杂的数据模式。因此,我们需要激活函数来引入非线性。
-
工作原理 :
ReLU(Rectified Linear Unit) 是目前最常用的激活函数之一。它的规则极其简单:对于输入的每个值,如果大于0,则保持不变;如果小于或等于0,则直接变为0。这个简单的操作却能极大地提升网络的表达能力。 -
代码示例:
python
# 定义一个ReLU激活函数层
relu_layer = nn.ReLU()
# 假设有一个包含正数和负数的Tensor
input_tensor = Tensor(np.array([[-1.0, 4.0, -8.0], [2.0, -5.0, 9.0]]), mindspore.float32)
# 应用ReLU
output_tensor = relu_layer(input_tensor)
print("应用ReLU前:
", input_tensor)
print("应用ReLU后:
", output_tensor)
2.3 池化层 (nn.MaxPool2d):信息浓缩与降维
池化层通常紧跟在卷积层和激活层之后,它有两个主要作用:
- 降维:显著减小特征图的尺寸,从而减少网络后续的参数数量和计算量。
- 保持特征不变性:通过取一个区域内的最大值(Max Pooling)或平均值(Avg Pooling),使得网络对特征的微小位移不那么敏感,增强了模型的鲁棒性。
-
工作原理 :
MaxPool2d(最大池化)与卷积类似,也是用一个窗口在特征图上滑动,但它不进行加权计算,而是简单地取窗口内的最大值作为输出。 -
关键参数:
kernel_size(int): 池化窗口的大小。stride(int): 窗口移动的步长。
-
代码示例:
python
# 假设我们有一个 1x2x4x4 的特征图
feature_map = Tensor(np.arange(1 * 2 * 4 * 4).reshape(1, 2, 4, 4), mindspore.float32)
# 定义一个最大池化层:窗口2x2,步长2
maxpool_layer = nn.MaxPool2d(kernel_size=2, stride=2)
# 应用池化
output_pooled = maxpool_layer(feature_map)
print("池化前尺寸:", feature_map.shape)
print("池化后尺寸:", output_pooled.shape) # (4-2)/2 + 1 = 2, 所以输出尺寸是 1x2x2x2
2.4 展平层 (nn.Flatten):从三维到一维的"压平"
在经过多轮"卷积-激活-池化"操作后,我们得到了一系列高度浓缩的特征图。但在将这些特征用于最终分类之前,需要将它们"压平",变成一个一维的向量。这就是nn.Flatten层的工作。
-
工作原理 :它会保留
batch_size(N),然后将后面的所有维度(C, H, W)的数值全部拉伸成一个长长的一维向量。 -
代码示例:
python
# 假设我们有一个 1x2x2x2 的池化后特征
pooled_map = Tensor(np.ones([1, 2, 2, 2]), mindspore.float32)
# 定义一个展平层
flatten_layer = nn.Flatten()
# 应用展平
output_vector = flatten_layer(pooled_map)
print("展平前尺寸:", pooled_map.shape)
print("展平后尺寸:", output_vector.shape) # 2 * 2 * 2 = 8, 所以输出尺寸是 1x8
2.5 全连接层 (nn.Dense):最终分类器
全连接层(在MindSpore中称为Dense)通常位于CNN的末端。在特征被展平后,这个一维向量会被送入一个或多个全连接层,进行最终的分类或回归。
-
工作原理:它的每一个神经元都与前一层的所有神经元相连接,通过学习到的权重对特征进行加权求和,最终映射到指定的输出维度(例如,在10分类任务中,输出维度就是10)。
-
关键参数:
in_channels(int): 输入神经元的数量(即展平后向量的长度)。out_channels(int): 输出神经元的数量(即分类的类别数)。
-
代码示例:
python
# 假设我们有一个长度为8的展平向量
flatten_vector = Tensor(np.ones([1, 8]), mindspore.float32)
# 定义一个全连接层:输入8个特征,输出到10个类别
dense_layer = nn.Dense(in_channels=8, out_channels=10)
# 应用全连接层
output_logits = dense_layer(flatten_vector)
print("全连接层输入尺寸:", flatten_vector.shape)
print("全连接层输出尺寸:", output_logits.shape)
3. 组装一个简单的CNN
现在我们已经认识了所有的"零件",让我们把它们组装起来,构建一个简单的CNN模型。这个模型将遵循经典的卷积 -> 激活 -> 池化 -> 展平 -> 全连接的结构。
python
import mindspore
from mindspore import nn
class SimpleCNN(nn.Cell):
def __init__(self, num_classes=10):
super(SimpleCNN, self).__init__()
# 定义第一组卷积、激活、池化
self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, pad_mode='valid')
self.relu1 = nn.ReLU()
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
# 定义第二组卷积、激活、池化
self.conv2 = nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5, pad_mode='valid')
self.relu2 = nn.ReLU()
self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
# 定义展平层和全连接层
self.flatten = nn.Flatten()
# 假设输入是32x32的图像,经过两轮卷积池化后,尺寸需要计算
# 这里我们先假设一个值,后续实战中会精确计算
self.fc1 = nn.Dense(in_channels=16 * 5 * 5, out_channels=120)
self.relu3 = nn.ReLU()
self.fc2 = nn.Dense(in_channels=120, out_channels=84)
self.relu4 = nn.ReLU()
self.fc3 = nn.Dense(in_channels=84, out_channels=num_classes)
def construct(self, x):
# 按照顺序将输入x通过各个层
x = self.conv1(x)
x = self.relu1(x)
x = self.pool1(x)
x = self.conv2(x)
x = self.relu2(x)
x = self.pool2(x)
x = self.flatten(x)
x = self.fc1(x)
x = self.relu3(x)
x = self.fc2(x)
x = self.relu4(x)
x = self.fc3(x)
return x
# 实例化网络
net = SimpleCNN()
print(net)
注意 :上述代码中
fc1的in_channels是一个估算值。在实际项目中,你需要根据输入图像的尺寸和卷积/池化层的参数精确计算出展平后的向量长度。我们将在后续的LeNet-5实战文章中详细演示这个计算过程。
4. 总结
在本篇文章中,我们详细学习了构建卷积神经网络(CNN)所需的几个核心层:
nn.Conv2d:用于提取局部特征。nn.ReLU:用于引入非线性,增强模型表达力。nn.MaxPool2d:用于降低维度,减少计算量。nn.Flatten:用于将多维特征"压平"成一维向量。nn.Dense:用于根据提取的特征进行最终分类。
通过将这些"零件"有序地组合,我们成功搭建了一个简单的CNN模型。这为您后续学习更复杂的网络(如LeNet-5、ResNet等)并进行端到端实战打下了坚实的基础。
在下一篇文章中,我们将学习如何构建循环神经网络(RNN),敬请期待!