你将使用PyTorch的nn.Module以及相关层,实现卷积神经网络(CNN)和循环神经网络(RNN)这两种架构的基本版本。这种动手实践将巩固你对这些模型如何构建以及数据如何在其中流动的理解。我们将侧重于定义模型结构和理解输入/输出维度,直接基于第4章的nn.Module知识和本章前面部分对层的说明进行构建。请记住,这些是简化示例;将它们集成到完整的训练循环中需要添加数据加载(第5章)、损失函数、优化器以及训练逻辑(第6章)。实现一个基本CNNCNN擅长处理网格状数据,例如图像。让我们构建一个可用于图像分类的简单CNN。我们将定义一个包含卷积层、激活函数、池化层和最终全连接层的网络。定义CNN架构我们创建一个继承自nn.Module的类。在__init__中,我们定义所需的层:用于卷积的nn.Conv2d,用于激活的nn.ReLU,用于池化的nn.MaxPool2d,以及用于最终分类的nn.Linear层。forward方法定义了输入数据如何流经这些层。import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super(SimpleCNN, self).__init__() # 输入形状: (批次, 1, 28, 28) - 假设是像MNIST那样的灰度图像 self.conv1 = nn.Conv2d(in_channels=1, out_channels=16, kernel_size=3, stride=1, padding=1) # conv1后的形状: (批次, 16, 28, 28) -> (28 - 3 + 2*1)/1 + 1 = 28 self.relu1 = nn.ReLU() self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2) # pool1后的形状: (批次, 16, 14, 14) -> 28 / 2 = 14 self.conv2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=3, stride=1, padding=1) # conv2后的形状: (批次, 32, 14, 14) -> (14 - 3 + 2*1)/1 + 1 = 14 self.relu2 = nn.ReLU() self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2) # pool2后的形状: (批次, 32, 7, 7) -> 14 / 2 = 7 # 展平输出以便连接到线性层 # 展平后的尺寸 = 32 * 7 * 7 = 1568 self.fc = nn.Linear(32 * 7 * 7, num_classes) def forward(self, x): # 应用第一个卷积块 out = self.conv1(x) out = self.relu1(out) out = self.pool1(out) # 应用第二个卷积块 out = self.conv2(out) out = self.relu2(out) out = self.pool2(out) # 展平卷积层的输出 # -1 表示推断批次大小 out = out.view(out.size(0), -1) # 应用全连接层 out = self.fc(out) return out 在此示例中:我们假设输入是灰度图像(1通道),类似于MNIST数据集中的图像,尺寸为28x28。nn.Conv2d(in_channels=1, out_channels=16, ...):接收1个输入通道,应用16个滤波器。kernel_size=3、stride=1、padding=1是常见的选择,它们在卷积后保持空间维度。nn.MaxPool2d(kernel_size=2, stride=2):将高度和宽度减半。第二个池化层的输出形状为(批次,32,7,7)。out.view(out.size(0), -1):将张量从形状(批次,32,7,7)展平为(批次,32 * 7 * 7)=(批次,1568),以便可以将其输入到线性层。nn.Linear(32 * 7 * 7, num_classes):最后一层将展平后的特征映射到所需数量的输出类别。使用虚拟数据测试CNN让我们创建一些匹配预期形状(批次大小,通道,高度,宽度)的虚拟输入数据,并将其传入我们的网络以查看输出形状。# 实例化模型 cnn_model = SimpleCNN(num_classes=10) # 创建虚拟输入批次(例如,4张图像,1通道,28x28像素) # requires_grad=False,因为我们只是进行前向传播演示 dummy_input_cnn = torch.randn(4, 1, 28, 28, requires_grad=False) # 执行前向传播 output_cnn = cnn_model(dummy_input_cnn) # 打印输入和输出形状 print(f"Input shape: {dummy_input_cnn.shape}") print(f"Output shape: {output_cnn.shape}")运行这段代码应该输出:Input shape: torch.Size([4, 1, 28, 28]) Output shape: torch.Size([4, 10])这确认了我们的网络接收一个包含4张图像的批次,并为每张图像输出10个类别的预测。请注意forward方法如何决定数据流向,以及我们如何需要根据最终池化层的输出形状来计算线性层的展平尺寸。你可以回顾“理解CNN层的输入/输出形状”一节,练习手动计算这些维度。实现一个基本RNNRNN旨在处理序列数据。例如,让我们构建一个可以处理字符序列或传感器读数的简单RNN。定义RNN架构我们将使用nn.RNN层。请记住,RNN层期望的输入格式是(序列长度,批次大小,输入特征)。import torch import torch.nn as nn class SimpleRNN(nn.Module): def __init__(self, input_size, hidden_size, output_size, num_layers=1): super(SimpleRNN, self).__init__() self.hidden_size = hidden_size self.num_layers = num_layers # RNN层 # batch_first=False是默认值,期望输入格式为: (序列长度, 批次, 输入特征) self.rnn = nn.RNN(input_size, hidden_size, num_layers, batch_first=False) # 全连接层,将RNN输出映射到最终输出尺寸 self.fc = nn.Linear(hidden_size, output_size) def forward(self, x, h0=None): # x 形状: (序列长度, 批次, 输入特征) # 如果未提供,则初始化隐藏状态 # 形状: (层数 * 方向数, 批次, 隐藏尺寸) if h0 is None: h0 = torch.zeros(self.num_layers, x.size(1), self.hidden_size).to(x.device) # 数据通过RNN层 # out 形状: (序列长度, 批次, 隐藏尺寸) -> 包含每个时间步的输出特征 # hn 形状: (层数 * 方向数, 批次, 隐藏尺寸) -> 包含最终隐藏状态 out, hn = self.rnn(x, h0) # 我们可以选择使用最后一个时间步的输出 # out[-1] 形状: (批次, 隐藏尺寸) # 或者,如果需要,处理整个序列 'out' out_last_step = out[-1, :, :] # 将最后一个时间步的输出通过线性层 final_output = self.fc(out_last_step) # final_output 形状: (批次, 输出尺寸) return final_output, hn # 返回最终输出和最后一个隐藏状态在此示例中:input_size:序列中每个步长的特征数量。hidden_size:隐藏状态中的特征数量。num_layers:堆叠的RNN层数。nn.RNN(...):核心RNN层。batch_first=False是默认值,表示序列长度维度在前。forward方法接受输入序列x和一个可选的初始隐藏状态h0。如果未提供h0,则将其初始化为零。nn.RNN层返回out(每个时间步的输出)和hn(最终隐藏状态)。我们经常使用最后一个时间步的输出(out[-1, :, :])进行序列分类或预测任务,并将其通过一个最终的线性层。使用虚拟数据测试RNN让我们创建一个虚拟序列并将其传入我们的RNN。# 定义参数 input_features = 10 # 例如,字符/单词的嵌入维度 hidden_nodes = 20 output_classes = 5 # 例如,根据序列预测5个类别之一 sequence_length = 15 batch_size = 4 # 实例化模型 rnn_model = SimpleRNN(input_size=input_features, hidden_size=hidden_nodes, output_size=output_classes) # 创建虚拟输入批次(序列长度,批次大小,输入特征) # requires_grad=False 用于演示 dummy_input_rnn = torch.randn(sequence_length, batch_size, input_features, requires_grad=False) # 执行前向传播(不提供h0,它将被初始化) output_rnn, final_hidden_state = rnn_model(dummy_input_rnn) # 打印输入和输出形状 print(f"Input sequence shape: {dummy_input_rnn.shape}") print(f"Output prediction shape: {output_rnn.shape}") print(f"Final hidden state shape: {final_hidden_state.shape}") 运行这段代码应该产生类似如下的输出:Input sequence shape: torch.Size([15, 4, 10]) Output prediction shape: torch.Size([4, 5]) Final hidden state shape: torch.Size([1, 4, 20])这表明模型处理一个包含4个序列的批次,每个序列长15步,每步有10个特征。它为批次中的每个序列输出一个大小为5的最终预测向量,以及最终的隐藏状态。隐藏状态的形状反映了(层数,批次大小,隐藏尺寸)。更多实践现在你已经实现了这些架构的基本版本,请尝试进行试验:CNN变体:更改nn.Conv2d层中的kernel_size、stride或padding。在运行代码之前预测输出形状。当步长为1时,padding='same'如何影响输出维度?添加另一个卷积/池化块。请记住重新计算nn.Linear层的输入尺寸。更改卷积层中out_channels的数量。RNN变体:增加SimpleRNN中的num_layers。观察初始隐藏状态h0和最终隐藏状态hn的形状。更改hidden_size。将nn.RNN替换为nn.LSTM或nn.GRU。请注意,nn.LSTM处理一个元组隐藏状态(隐藏状态和单元状态)。你需要相应调整隐藏状态的初始化和处理方式。输入/输出形状大体遵循相同的模式。修改forward方法,以使用所有时间步的输出(out)而不是仅最后一个,例如通过对每个步应用线性层或使用平均等聚合方法。这次实践为构建CNN和RNN提供了扎实基础。通过理解如何定义这些层、在forward方法中连接它们以及管理它们的输入/输出形状,你将具备良好能力,能够使用PyTorch构建和调整这些强大的架构以完成各种深度学习任务。