【Scala PyTorch深度学习】PyTorch On Scala 系列课程 第七章 14 :常用模型CNN RNN Pooling【AI Infra】[PyTorch Scala 硕士研一课程】

张开发
2026/4/16 12:22:58 15 分钟阅读

分享文章

【Scala PyTorch深度学习】PyTorch On Scala 系列课程 第七章 14 :常用模型CNN RNN Pooling【AI Infra】[PyTorch Scala 硕士研一课程】
PyTorch Scala 高校计算机硕士研一课程章节 7: 常用模型结构介绍您已经掌握了 PyTorch 的核心构成部分比如张量Tensors、使用 Autograd 的自动求导、通过torch.nn定义模型以及实现数据加载和训练步骤。本章将在之前所学知识之上讲解如何构建特定且应用广泛的神经网络模型。我们将着重介绍两种重要的模型类别卷积神经网络CNNs您将了解卷积和池化的核心思想学习为何 CNN 对网格状数据特别是图像表现出色并使用nn.Conv2d和nn.MaxPool2d等层实现一个简单的 CNN 模型。我们还将说明如何处理这些层的输入和输出形状。循环神经网络RNNs您将接触到使用循环连接和隐藏状态处理序列数据的思路。我们将使用nn.RNN层构建一个简单的 RNN并讨论 PyTorch 中序列输入所需的特定数据格式。还会简要提及更高级的变体如 LSTM 和 GRU。到本章结束时您将能够在 PyTorch 中构建这些常用模型的简单版本为您后续处理更复杂的模型做好准备。卷积神经网络 (CNN) 概述标准神经网络层如nn.Linear将输入数据视为一个扁平向量。尽管功能强大但这种方法未能内在理解图像等数据中存在的空间结构。对于图像来说相互靠近的像素通常是关联的它们构成边缘、纹理或物体的一部分。当直接应用于图像时全连接层面临两个主要问题参数效率低下将一张中等大小的图像例如224x224 像素3 个颜色通道展平为向量会导致输入维度非常大。即使将其连接到一个中等大小的隐藏层也需要大量权重使得模型容易过拟合并且计算成本高昂。空间信息丢失展平图像会丢弃像素的 2D或包含通道的 3D排列。网络会丢失关于哪些像素最初是相邻的信息。卷积神经网络 (CNN) 是一种专门设计用于处理具有网格状拓扑数据如图像2D 网格或时间序列数据1D 网格的神经网络。它们通过结合两个主要思想来解决标准网络的局限局部感受野通过卷积和空间下采样通过池化。卷积操作识别局部模式CNN 的核心组成部分是卷积层。卷积层不将每个输入单元连接到每个输出单元而是使用小的过滤器也称为核它们在输入数据上滑动。每个过滤器都是一个小的权重矩阵。想象一个微小的放大镜即过滤器在输入图像上滑动。在每个位置过滤器会与其当前覆盖的图像区域执行元素级乘法并将结果求和以在输出中生成一个单一值。这个过程在整个输入图像上重复进行生成一个输出特征图。输入区域过滤器核输出值wu03a3(输入 u00d7 过滤器)www过滤器对输入的局部区域施加权重以计算输出特征图中的一个值。这种滑动过滤器方法具有两个显著优点局部连接特征图中的每个单元仅连接到输入的一个小区域过滤器大小。这使得网络能够在早期层中学习到局部模式如边缘或角落。参数共享相同的过滤器具有相同的权重集合在输入图像的不同位置重复使用。这与全连接层相比大幅减少了参数数量并使网络对特征的平移具有等变性。如果一个模式如垂直边缘被过滤器学习它可以在图像中任何位置检测到该模式。通常一个卷积层会使用多个过滤器每个过滤器学习识别不同类型的特征例如一个过滤器识别水平边缘另一个识别垂直边缘还有一个识别特定纹理。这些过滤器的输出堆叠在一起形成该层的最终输出体。PyTorch 主要通过nn.Conv2d层来实现图像数据的这一操作。激活函数就像在标准网络中一样非线性激活函数例如 ReLU在 PyTorch 中实现为nn.ReLU通常在卷积操作之后进行元素级应用。这使得网络能够学习特征之间复杂的非线性关系。池化操作下采样与不变性在通过卷积层检测到特征后通常有益于使表示更紧凑并对小的空间变异具有抵抗力。这通过使用池化层来实现。最常见的类型是最大池化。它也涉及在特征图上滑动一个窗口通常小于卷积过滤器且不重叠或带步幅。但是它不应用学习到的权重而只是简单地取出该窗口内的最大值。特征图区域 (2x2)输出值1max(1, 5, 3, 2) 5532最大池化选择特征图局部窗口内的最大值。池化提供多项益处维度降低它减少了特征图的空间维度高度和宽度降低了后续层的计算负担。平移不变性局部通过用其最大激活来概括局部区域池化使表示对特征在该区域内的确切位置更具稳定性。PyTorch 提供了nn.MaxPool2d等池化层。典型 CNN 架构一个典型的 CNN 架构通常会堆叠这些组件一个或多个卷积 - 激活 - 池化层块。早期层倾向于使用较小的过滤器来捕捉精细细节而后期层可能使用较大的过滤器或依赖于早期层的池化特征来捕捉更大空间区域上的更复杂模式。在经过多个卷积和池化层之后得到的特征图通常会展平为一个向量。然后这个向量被馈入一个或多个全连接 (nn.Linear) 层类似于标准前馈网络用于最终的分类或回归。输入图像Conv ReLUMaxPoolConv ReLUMaxPoolFlattenLinear ReLU输出分数一个典型的 CNN 架构流程。CNN运用卷积和池化直接从网格状数据中自动学习特征的分层表示这使得它们在图像识别、物体检测等任务中表现非常出色甚至在文本得到适当表示时也能用于自然语言处理。在下一节中你将看到如何在 PyTorch 中实现像nn.Conv2d和nn.MaxPool2d这样的构建模块以构建你的第一个 CNN。在PyTorch中构建一个简单的CNN将卷积神经网络的核心概念转化为可运行的PyTorch模型。CNN通常通过堆叠卷积层、激活函数和池化层来构建之后通常跟随一个或多个全连接层用于分类或回归。PyTorch的torch.nn模块提供了这些核心组件的预构建实现以便高效构建。我们的目标是构建一个能够处理图像数据的简单CNN。我们将从定义网络结构开始将其作为一个Python类并继承自torch.nn.Module。CNN的核心层在PyTorch中卷积层 (nn.Conv2d)此层对输入应用可学习的滤波器。主要参数有in_channels输入张量的通道数例如灰度图像为1RGB图像为3。out_channels滤波器数量也是输出张量的通道数。每个滤波器学习检测不同的特征。kernel_size滤波器尺寸高 x 宽。单个整数k表示k x k的滤波器。stride滤波器每次移动的像素数默认为1。padding在输入周围添加填充常用于控制输出的空间尺寸默认为0。importtorchimporttorch.nnas nn// 示例一个Conv2d层接收3个输入通道例如RGB图像// 使用5x5滤波器生成16个输出通道。valconv1nn.Conv2d(in_channels3,out_channels16,kernel_size5,stride1,padding2)池化层 (nn.MaxPool2d)此层减小特征图的空间尺寸高和宽使表示更紧凑并对特征位置的变化略微更具鲁棒性。kernel_size取最大值的窗口大小。stride窗口移动的距离。对于非重叠池化通常设为等于kernel_size。// 示例一个MaxPool2d层使用2x2窗口和步长为2。// 这通常会将输入的高度和宽度减半。valpool1nn.MaxPool2d(kernel_size2,stride2)激活函数 (例如nn.ReLU)引入非线性使网络能够学习复杂的模式。ReLU修正线性单元是常用选择。它逐元素应用f(x)max(0,x)f(x)ma**x(0,x)。// ReLU激活函数valrelu1nn.ReLU()线性层 (nn.Linear)一个标准的全连接层。通常用于CNN的末尾在空间特征被提取和展平之后。in_features输入特征的数量需要展平卷积/池化层的输出。out_features输出特征的数量例如分类任务中的类别数。// 示例一个线性层接收一个展平的512个特征向量// 并输出10个值例如用于10个类别。valfc1nn.Linear(in_features512,out_features10)定义CNN结构我们通过继承nn.Module来定义我们的CNN。各层通常在__init__方法中定义而前向传播数据如何流经各层则在forward方法中定义。我们来构建一个具有以下结构的CNN输入[批大小, 1, 28, 28]例如像MNIST那样的灰度图像Conv11个输入通道16个输出通道5x5核步长1填充2ReLU1MaxPool12x2核步长2Conv216个输入通道32个输出通道5x5核步长1填充2ReLU2MaxPool22x2核步长2展平Linear1输入特征取决于MaxPool2的输出128个输出特征ReLU3Linear2128个输入特征10个输出特征例如用于10个类别importtorchimporttorch.nnas nnimporttorch.nn.functionalas F// 通常包含激活函数和其他实用工具classSimpleCNNextendsnn.Module:def__init__(self):super(SimpleCNN,self).__init__()// 层定义// 卷积层1valconv1nn.Conv2d(in_channels1,out_channels16,kernel_size5,stride1,padding2)// 最大池化层1valpool1nn.MaxPool2d(kernel_size2,stride2)// 卷积层2valconv2nn.Conv2d(in_channels16,out_channels32,kernel_size5,stride1,padding2)// 最大池化层2valpool2nn.MaxPool2d(kernel_size2,stride2)// 全连接层// fc1的输入特征取决于池化后的输出形状// 输入28x28 - Conv1 (padding2) - 28x28 - Pool1 (stride2) - 14x14// - Conv2 (padding2) - 14x14 - Pool2 (stride2) - 7x7// 因此展平后的尺寸是 32 个通道 * 7 高度 * 7 宽度 1568valfc1nn.Linear(in_features32*7*7,out_features128)valfc2nn.Linear(in_features128,out_features10)// 用于10个类别的输出defforward(x:Tensor):// 定义数据流经各层的方式// 输入x形状[批大小, 1, 28, 28]// 应用Conv1、ReLU、Pool1xpool1(F.relu(conv1(x)))// pool1后的形状[批大小, 16, 14, 14]// 应用Conv2、ReLU、Pool2xpool2(F.relu(conv2(x)))// pool2后的形状[批大小, 32, 7, 7]// 展平张量以用于全连接层// -1 保持批大小维度不变xx.view(-1,32*7*7)// view后的形状[批大小, 1568]// 应用FC1和ReLUxF.relu(fc1(x))// fc1后的形状[批大小, 128]// 应用FC2输出层此处无激活函数通常与损失函数一起应用xfc2(x)// fc2后的形状[批大小, 10]returnx我们来可视化架构流程输入 (1x28x28)Conv1 (16x5x5, s1, p2)ReLUMaxPool1 (2x2, s2)16x28x28Conv2 (32x5x5, s1, p2)ReLU16x14x14MaxPool2 (2x2, s2)32x14x14展平32x7x7Linear1 (1568 - 128)ReLU1568Linear2 (128 - 10)128输出 (10)数据和张量形状流经SimpleCNN模型。请注意通道数增加而空间维度高/宽减小。使用模型要使用这个模型首先实例化该类。然后您可以将输入数据作为PyTorch张量传入其中。输入张量必须具有预期的形状包括批次维度。对于我们的SimpleCNN这具体为[N, 1, 28, 28]其中N是批次中的样本数量。// 实例化模型valmodelSimpleCNN()println(model)// 创建一个虚拟输入张量4张图像的批次1个通道28x28// 如果您打算训练需要梯度跟踪valdummy_inputtorch.randn(4,1,28,28)// 将输入传入模型前向传播valoutputmodel(dummy_input)// 检查输出形状println(s\nInput shape:${dummy_input.shape})println(sOutput shape:${output.shape})// 预期[4, 10]运行此代码将打印模型的层结构并确认输出张量形状符合我们的预期[4, 10]表示批次中每张图像的10个类别的得分。这个例子展示了如何在nn.Module中组合nn.Conv2d、nn.MaxPool2d、nn.ReLU和nn.Linear层来创建一个基本的CNN。设计CNN时的一个重要细节是正确计算每个层之后张量形状的变化特别是在连接卷积/池化部分和全连接部分时。我们将在下一节更详细地说明这些形状的跟踪。理解CNN层的输入/输出形状当你开始构建卷积神经网络CNN时最常见的实际问题之一是确保一个层的输出形状能正确匹配下一个层的预期输入形状。与只需要考虑一个维度的简单全连接层不同卷积层和池化层对多维网格状数据如图像进行操作涉及高度、宽度和通道维度。了解这些维度如何变化对于构建有效的CNN架构非常重要。让我们看一个用于二维CNN层如nn.Conv2d或nn.MaxPool2d的典型输入张量。它通常有四个维度(N,Cin,Hin,Win)(N,C**in,H**in,W**in)NN: 批大小同时处理的样本数量。CinC**in: 输入通道数例如RGB图像为3灰度图像为1。HinH**in: 输入特征图的高度。WinW**in: 输入特征图的宽度。批维度 NN通常保持不变。主要的变换发生在通道 (CC)、高度 (HH) 和宽度 (WW) 上。卷积层 (nn.Conv2d)torch.nn.Conv2d层对由多个输入平面组成的输入信号应用二维卷积。影响输出形状最重要的参数是in_channels(CinC**in): 必须与输入张量中的通道数匹配。out_channels(CoutCou**t): 决定卷积产生的通道数。这是该层学习的滤波器数量。kernel_size: 卷积核滤波器的大小。可以是一个整数用于方形卷积核例如3表示3x3也可以是一个元组(kH, kW)用于指定高度和宽度。stride: 卷积核在输入特征图上滑动时的步长。默认为1。可以是一个整数或一个元组(sH, sW)。较大的步长会导致输出特征图尺寸更小。padding: 输入边缘添加的零填充量。默认为0。可以是一个整数或一个元组(padH, padW)。填充有助于控制输出的空间维度并能保留边界信息。dilation: 卷积核元素之间的间距。默认为1。较大的空洞扩张允许卷积核覆盖输入更广的区域而不会增加参数数量空洞卷积。输出形状 (N,Cout,Hout,Wout)(N,Cou**t,Hou**t,Wou**t) 如下确定**通道数 (Cout*C**o*u*t*):这由nn.Conv2d层的out_channels参数直接设置。每个滤波器产生一个输出通道特征图。高度 (Hout*H**o**u**t*) 和宽度 (Wout*W*o*u*t*):这些取决于输入维度 (Hin,WinHin*,W**in) 和层的参数。计算输出高度的公式是Hout⌊Hin2×填充[0]−空洞[0]×(卷积核尺寸[0]−1)−1步长[0]1⌋Hou**t⌊步长[0]H**in2×填充[0]−空洞[0]×(卷积核尺寸[0]−1)−11⌋对于宽度 (WoutWou**t) 也是类似的Wout⌊Win2×填充[1]−空洞[1]×(卷积核尺寸[1]−1)−1步长[1]1⌋Wou**t⌊步长[1]W**in2×填充[1]−空洞[1]×(卷积核尺寸[1]−1)−11⌋注意如果padding、dilation、kernel_size或stride被指定为单个整数它们将应用于高度和宽度两个维度例如padding[0] padding[1] padding。符号 ⌊⋅⌋⌊⋅⌋ 表示向下取整函数向下舍入到最接近的整数。让我们看一个当dilation 1的常见情况。公式简化为Hout⌊Hin2×填充[0]−卷积核尺寸[0]步长[0]1⌋Hou**t⌊步长[0]H**in2×填充[0]−卷积核尺寸[0]1⌋Wout⌊Win2×填充[1]−卷积核尺寸[1]步长[1]1⌋Wou**t⌊步长[1]W**in2×填充[1]−卷积核尺寸[1]1⌋示例假设我们有一个形状为(16, 3, 32, 32)的输入张量批16通道3高32宽32。我们将其通过一个定义如下的nn.Conv2d层importtorchimporttorch.nnas nnvalconv_layernn.Conv2d(in_channels3,out_channels64,kernel_size3,stride1,padding1)// 输入: N16, Cin3, Hin32, Win32valinput_tensortorch.randn(16,3,32,32)// 参数: K3, S1, P1, D1 (默认)// H_out floor((32 2*1 - 1*(3-1) - 1)/1 1) floor((32 2 - 2 - 1)/1 1) floor(31/1 1) 32// W_out floor((32 2*1 - 1*(3-1) - 1)/1 1) floor((32 2 - 2 - 1)/1 1) floor(31/1 1) 32// 简化公式 (D1):// H_out floor((32 2*1 - 3)/1 1) floor(31/1 1) 32// W_out floor((32 2*1 - 3)/1 1) floor(31/1 1) 32// 前向传播valoutput_tensorconv_layer(input_tensor)println(sOutput shape:${output_tensor.shape})// 预期[16, 64, 32, 32]在此示例中使用kernel_size3、stride1和padding1是一种常见组合它保持了输入的高度和宽度32x32-32x32同时将通道数从3变为64。这有时被称为“相同”填充尽管PyTorch不像其他一些框架那样有明确的same选项你可以通过正确设置参数来实现。如果我们将步长改为2stride2输出维度将会减小importtorchimporttorch.nnas nnvalconv_layer_s2nn.Conv2d(in_channels3,out_channels64,kernel_size3,stride2,padding1)// H_out floor((32 2*1 - 3)/2 1) floor(31/2 1) floor(15.5 1) floor(16.5) 16// W_out floor((32 2*1 - 3)/2 1) floor(31/2 1) floor(15.5 1) floor(16.5) 16// 前向传播valoutput_tensor_s2conv_layer_s2(input_tensor)println(sOutput shape:${output_tensor_s2.shape})// 预期[16, 64, 16, 16]池化层 (nn.MaxPool2d)池化层例如nn.MaxPool2d用于减小特征图的空间维度下采样使表示更紧凑且对小的平移具有鲁棒性。它们在每个通道上独立操作。影响形状的主要参数与nn.Conv2d相似但没有out_channels这个参数因为池化不会改变通道数量kernel_size: 池化窗口的大小。stride: 窗口的步长。通常设置为与kernel_size相等以实现不重叠的池化默认值是kernel_size。padding: 添加的零填充量。dilation: 控制池化元素之间的间距。输出形状 (N,Cout,Hout,Wout)(N,Cou**t,Hou**t,Wou**t) 中 HoutHou**t和 WoutWou**t的计算遵循与卷积层完全相同的公式Hout⌊Hin2×填充[0]−空洞[0]×(卷积核尺寸[0]−1)−1步长[0]1⌋Hou**t⌊步长[0]H**in2×填充[0]−空洞[0]×(卷积核尺寸[0]−1)−11⌋Wout⌊Win2×填充[1]−空洞[1]×(卷积核尺寸[1]−1)−1步长[1]1⌋Wou**t⌊步长[1]W**in2×填充[1]−空洞[1]×(卷积核尺寸[1]−1)−11⌋重要区别池化层不改变通道数量。因此CoutCinCou**tC**in。示例让我们使用我们第一个conv_layer的输出形状为[16, 64, 32, 32]并将其通过一个常见的最大池化层importtorchimporttorch.nnas nn// 来自前一个卷积层的输入: N16, Cin64, Hin32, Win32valpool_layernn.MaxPool2d(kernel_size2,stride2,padding0)// 常见设置// 参数: K2, S2, P0, D1 (默认)// H_out floor((32 2*0 - 1*(2-1) - 1)/2 1) floor((32 - 1 - 1)/2 1) floor(30/2 1) floor(15 1) 16// W_out floor((32 2*0 - 1*(2-1) - 1)/2 1) floor((32 - 1 - 1)/2 1) floor(30/2 1) floor(15 1) 16// 前向传播valpooled_outputpool_layer(output_tensor)println(sOutput shape:${pooled_output.shape})// 预期[16, 64, 16, 16]在此具有2x2卷积核和步长为2的池化层将高度和宽度维度减半32x32-16x16而通道数量保持不变64。输入(N, 3, 32, 32)nn.Conv2d(输出64, K3, S1, P1)保持高宽(N, 64, 32, 32)nn.MaxPool2d(K2, S2, P0)高宽减半(N, 64, 16, 16)输出(N, 64, 16, 16)张量维度通过一个示例卷积和池化层序列的流向。实际中跟踪形状在构建复杂的CNN时手动计算形状会变得繁琐且容易出错。这里有一些实用建议打印形状在初始开发阶段在层之后添加print(x.shape)语句以验证维度。使用虚拟输入创建一个具有预期形状的虚拟输入张量并将其逐步或逐层地通过你的网络定义以查看形状如何变化。辅助函数编写一个小的辅助函数以层和输入形状作为参数并使用公式计算输出形状。库/工具一些库或工具如torchinfo或pytorch-summary可以自动总结你的模型显示给定输入尺寸下每个层的输出形状。掌握形状计算是在设计和调试CNN时必要的一步。通过理解kernel_size、stride、padding和dilation如何影响空间维度以及out_channels如何决定深度你可以放心地堆叠层来构建有效的深度学习模型。

更多文章