2.3 常用层 (Common Layers) PyTorch 模型构建基石:常用层 (Common Layers) 详解与实践 在深度学习的广阔天地中,PyTorch 以其灵活性和强大的功能,成为了研究人员和工程师构建复杂模型的首选框架之一。模型构建的核心在于选择和组合各种神经网络层,而 常用层 (Common Layers) 正是构成这些模型的基石。掌握这些常用层的原理、应用以及在 PyTorch 中的实现,对于深入理解和有效构建神经网络模型至关重要。 2.3.1 线性层 (Linear Layer) 线性层,也称为全连接层 (Fully Connected Layer) 或 Dense 层,是神经网络中最基础且应用最广泛的层之一。
在深度学习的广阔天地中,PyTorch 以其灵活性和强大的功能,成为了研究人员和工程师构建复杂模型的首选框架之一。模型构建的核心在于选择和组合各种神经网络层,而 常用层 (Common Layers) 正是构成这些模型的基石。掌握这些常用层的原理、应用以及在 PyTorch 中的实现,对于深入理解和有效构建神经网络模型至关重要。
线性层,也称为全连接层 (Fully Connected Layer) 或 Dense 层,是神经网络中最基础且应用最广泛的层之一。其核心作用是对输入数据进行线性变换,实现特征的线性组合和空间变换。在数学上,线性层可以表示为矩阵乘法和偏置项的加法:
其中:
\mathbf{x} 是输入向量。
\mathbf{W} 是权重矩阵,其维度决定了输入和输出特征空间的维度变换。
\mathbf{b} 是偏置向量,为每个输出特征提供一个可学习的偏移量。
\mathbf{y} 是输出向量。
线性层通过学习权重矩阵 \mathbf{W} 和偏置向量 \mathbf{b},能够将输入数据从一个特征空间映射到另一个特征空间。它在神经网络中扮演着至关重要的角色,常用于:
特征提取的最后阶段: 将前面层提取的高级特征进行线性组合,最终用于分类或回归任务。
不同维度空间之间的转换: 调整特征向量的维度,以适应后续层的输入要求。
构建简单的线性模型: 单独使用线性层即可构建简单的线性回归或线性分类模型。
nn.Linear在 PyTorch 中,线性层通过 torch.nn.Linear 类实现。其构造函数接受三个主要参数:
in_features: 输入特征的维度。
out_features: 输出特征的维度。
bias: 布尔值,指示是否使用偏置项,默认为 True。
代码实践:
import torch import torch.nn as nn # 定义一个输入维度为 10,输出维度为 5 的线性层 linear_layer = nn.Linear(in_features=10, out_features=5) # 创建一个随机输入张量 (batch_size, in_features) input_tensor = torch.randn(64, 10) # 通过线性层进行前向传播 output_tensor = linear_layer(input_tensor) # 打印输出张量的形状 print("Input Tensor Shape:", input_tensor.shape) print("Output Tensor Shape:", output_tensor.shape) # 查看线性层的权重和偏置参数 print("Linear Layer Weights:", linear_layer.weight.shape) # (out_features, in_features) print("Linear Layer Bias:", linear_layer.bias.shape) # (out_features)
代码详解:
nn.Linear(in_features=10, out_features=5): 我们创建了一个线性层实例 linear_layer,指定输入特征维度为 10,输出特征维度为 5。PyTorch 会自动初始化权重矩阵 linear_layer.weight 的形状为 (5, 10),偏置向量 linear_layer.bias 的形状为 (5)。
torch.randn(64, 10): 我们创建了一个形状为 (64, 10) 的随机输入张量 input_tensor,其中 64 代表 batch size,10 代表输入特征维度。
output_tensor = linear_layer(input_tensor): 将输入张量 input_tensor 传递给线性层 linear_layer 进行前向传播,得到输出张量 output_tensor。
形状输出: 可以看到,输入张量的形状为 (64, 10),输出张量的形状为 (64, 5)。线性层将每个样本的特征维度从 10 变换到了 5。权重矩阵的形状为 (5, 10),偏置向量的形状为 (5),这与线性层的数学公式和 PyTorch 的实现一致。
Mermaid 图示:
卷积层 是深度学习中,特别是计算机视觉领域中至关重要的层。它通过卷积运算提取输入数据中的局部特征,并具有平移不变性的特性,使其能够有效地处理图像、语音等具有空间或时间结构的数据。
卷积运算的核心是卷积核 (Kernel) 或 滤波器 (Filter)。卷积核是一个小的权重矩阵,它在输入数据上滑动 (convolve),并在每个位置执行元素乘积求和的操作。这个过程可以形象地理解为使用卷积核扫描输入数据,提取局部模式。
对于二维卷积 (2D Convolution),常用于图像处理,其运算过程可以描述如下:
定义卷积核: 选择一个大小为 k \times k 的卷积核,其中 k 通常为奇数,例如 3 或 5。
滑动窗口: 将卷积核在输入图像上滑动,从左上角开始,每次滑动一定的步长 (stride)。
卷积运算: 在每个滑动位置,将卷积核与输入图像对应区域进行元素乘积,并将所有乘积结果求和,得到输出特征图 (Feature Map) 的一个像素值。
重复操作: 重复步骤 2 和 3,直到卷积核滑遍整个输入图像,生成完整的输出特征图。
卷积层通常包含多个卷积核,每个卷积核可以学习到不同的局部特征。通过堆叠多个卷积层,网络可以逐步提取图像中越来越高级、抽象的特征,例如边缘、纹理、形状、物体部件等。
nn.Conv2d在 PyTorch 中,二维卷积层通过 torch.nn.Conv2d 类实现。其构造函数接受以下主要参数:
in_channels: 输入特征图的通道数。对于 RGB 图像,in_channels 为 3。
out_channels: 输出特征图的通道数,即卷积核的数量。
kernel_size: 卷积核的大小,可以是一个整数 (正方形卷积核) 或一个元组 (height, width)。
stride: 卷积核滑动的步长,可以是一个整数或一个元组 (stride_height, stride_width),默认为 1。
padding: 输入图像边缘填充的层数,可以是整数或字符串 'valid' (不填充) 或 'same' (保持输出尺寸与输入尺寸相同,需要根据 kernel_size 和 stride 自动计算填充)。
bias: 布尔值,指示是否使用偏置项,默认为 True。
代码实践:
import torch import torch.nn as nn # 定义一个卷积层:输入通道为 3,输出通道为 16,卷积核大小为 3x3,步长为 1,padding 为 1 conv_layer = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, stride=1, padding=1) # 创建一个随机输入图像张量 (batch_size, channels, height, width) input_image = torch.randn(64, 3, 32, 32) # 假设输入图像大小为 32x32 # 通过卷积层进行前向传播 output_feature_map = conv_layer(input_image) # 打印输出特征图的形状 print("Input Image Shape:", input_image.shape) print("Output Feature Map Shape:", output_feature_map.shape) # 查看卷积层的权重和偏置参数 print("Conv Layer Weights:", conv_layer.weight.shape) # (out_channels, in_channels, kernel_height, kernel_width) print("Conv Layer Bias:", conv_layer.bias.shape) # (out_channels)
代码详解:
nn.Conv2d(...): 我们创建了一个卷积层实例 conv_layer,配置了输入通道数、输出通道数、卷积核大小、步长和 padding 等参数。PyTorch 会自动初始化卷积核权重 conv_layer.weight 的形状为 (16, 3, 3, 3),偏置向量 conv_layer.bias 的形状为 (16)。
torch.randn(64, 3, 32, 32): 我们创建了一个形状为 (64, 3, 32, 32) 的随机输入图像张量 input_image,其中 64 是 batch size,3 是通道数 (RGB),32x32 是图像的高度和宽度。
output_feature_map = conv_layer(input_image): 将输入图像张量 input_image 传递给卷积层 conv_layer 进行前向传播,得到输出特征图 output_feature_map。
形状输出: 可以看到,输入图像的形状为 (64, 3, 32, 32),输出特征图的形状为 (64, 16, 32, 32)。由于我们设置了 padding=1 和 stride=1,并且卷积核大小为 3x3,因此输出特征图的尺寸与输入图像尺寸保持一致 (32x32)。输出通道数变为 16,这是因为我们设置了 out_channels=16,卷积层使用了 16 个不同的卷积核来提取特征。权重矩阵的形状为 (16, 3, 3, 3),偏置向量的形状为 (16),符合卷积层的参数定义。
Mermaid 图示:
池化层,也称为汇聚层,通常紧跟在卷积层之后,用于降低特征图的空间分辨率 (减小尺寸) 和增强特征的平移不变性。池化操作在每个特征图上独立进行,它将输入特征图划分为一系列不重叠或重叠的区域 (通常是正方形),并对每个区域内的像素值进行聚合统计,例如取最大值 (Max Pooling) 或平均值 (Average Pooling)。
常见的池化操作包括:
最大池化 (Max Pooling): 取每个池化窗口内的最大值作为输出。最大池化能够有效地保留区域内的最显著特征,并对小的平移和形变具有更强的鲁棒性。
平均池化 (Average Pooling): 计算每个池化窗口内像素值的平均值作为输出。平均池化能够平滑特征图,降低噪声的影响,并提供区域内的平均特征。
池化层的主要作用包括:
降维 (Dimensionality Reduction): 减小特征图的尺寸,降低后续计算的复杂度,并减少模型参数数量,防止过拟合。
平移不变性 (Translation Invariance): 通过聚合局部区域的特征,使得模型对于输入数据中目标位置的微小变化更加鲁棒。
特征抽象 (Feature Abstraction): 池化操作可以提取更高层次的特征,例如从边缘特征到纹理特征,再到物体部件特征。
nn.MaxPool2d在 PyTorch 中,二维最大池化层通过 torch.nn.MaxPool2d 类实现。其构造函数接受以下主要参数:
kernel_size: 池化窗口的大小,可以是一个整数 (正方形窗口) 或一个元组 (height, width)。
stride: 池化窗口滑动的步长,可以是一个整数或一个元组 (stride_height, stride_width),默认为 kernel_size。
padding: 输入特征图边缘填充的层数,默认为 0。
代码实践:
import torch import torch.nn as nn # 定义一个最大池化层:池化窗口大小为 2x2,步长为 2 max_pool_layer = nn.MaxPool2d(kernel_size=2, stride=2) # 假设输入特征图是卷积层的输出 (batch_size, channels, height, width) input_feature_map = torch.randn(64, 16, 32, 32) # 通过最大池化层进行前向传播 output_feature_map_pooled = max_pool_layer(input_feature_map) # 打印输出特征图的形状 print("Input Feature Map Shape:", input_feature_map.shape) print("Output Pooled Feature Map Shape:", output_feature_map_pooled.shape)
代码详解:
nn.MaxPool2d(kernel_size=2, stride=2): 我们创建了一个最大池化层实例 max_pool_layer,设置池化窗口大小为 2x2,步长为 2。这意味着池化窗口在特征图上以步长 2 滑动,且每个 2x2 区域内只保留最大值。
torch.randn(64, 16, 32, 32): 我们假设输入特征图 input_feature_map 是卷积层的输出,形状为 (64, 16, 32, 32)。
output_feature_map_pooled = max_pool_layer(input_feature_map): 将输入特征图传递给最大池化层 max_pool_layer 进行前向传播,得到池化后的输出特征图 output_feature_map_pooled。
形状输出: 可以看到,输入特征图的形状为 (64, 16, 32, 32),输出池化特征图的形状为 (64, 16, 16, 16)。由于池化窗口大小为 2x2,步长为 2,因此特征图的高度和宽度都缩小了一半 (从 32 变为 16),通道数保持不变。
Mermaid 图示:
循环层,也称为循环神经网络 (Recurrent Neural Network, RNN),专门用于处理序列数据,例如文本、语音、时间序列等。与前馈神经网络不同,循环层具有记忆能力,它可以将之前的输入信息传递到当前时刻,从而捕捉序列数据中的时序依赖关系。
RNN 的核心思想是在网络中引入循环连接,使得网络在处理序列的每个元素时,不仅考虑当前的输入,还考虑之前的状态信息。最基础的 RNN 结构可以表示为:
其中:
\mathbf{x}_t 是时刻 t 的输入向量。
\mathbf{h}_t 是时刻 t 的隐藏状态向量,它存储了网络在时刻 t 的记忆信息。
\mathbf{h}_{t-1} 是上一时刻 t-1 的隐藏状态向量,RNN 通过循环连接将上一时刻的状态传递到当前时刻。
\mathbf{y}_t 是时刻 t 的输出向量。
\mathbf{W}_{ih}、\mathbf{W}_{hh}、\mathbf{W}_{ho} 是权重矩阵。
\mathbf{b}_h、\mathbf{b}_o 是偏置向量。
f 和 g 是激活函数,通常使用 tanh 或 ReLU 等。
然而,基础 RNN 在处理长序列时容易出现梯度消失或梯度爆炸问题,导致难以学习长距离依赖关系。为了解决这个问题,出现了更高级的循环层结构,例如 长短期记忆网络 (Long Short-Term Memory, LSTM) 和 门控循环单元 (Gated Recurrent Unit, GRU)。
LSTM 通过引入门控机制 (输入门、遗忘门、输出门和细胞状态) 来更有效地控制信息的流动和长期记忆的存储,从而克服了梯度消失问题,在处理长序列数据上表现出色。
nn.LSTM在 PyTorch 中,LSTM 层通过 torch.nn.LSTM 类实现。其构造函数接受以下主要参数:
input_size: 输入特征的维度。
hidden_size: 隐藏状态的维度,也决定了 LSTM 单元的记忆容量。
num_layers: LSTM 层的层数,可以堆叠多层 LSTM。
batch_first: 布尔值,如果为 True,则输入张量的第一个维度为 batch size,否则第二个维度为 batch size,默认为 False (即时间步维度在第一个维度)。
bidirectional: 布尔值,如果为 True,则使用双向 LSTM,可以同时考虑序列的前向和后向信息。
代码实践:
import torch import torch.nn as nn # 定义一个 LSTM 层:输入特征维度为 20,隐藏状态维度为 50,单层 LSTM lstm_layer = nn.LSTM(input_size=20, hidden_size=50, num_layers=1) # 创建一个随机输入序列张量 (seq_len, batch_size, input_size) input_sequence = torch.randn(30, 64, 20) # 假设序列长度为 30,batch size 为 64 # 初始化 LSTM 的隐藏状态和细胞状态 (h_0, c_0) # 形状为 (num_layers * num_directions, batch_size, hidden_size) h0 = torch.randn(1, 64, 50) # 单层单向 LSTM, num_directions=1 c0 = torch.randn(1, 64, 50) # 通过 LSTM 层进行前向传播 output_sequence, (hn, cn) = lstm_layer(input_sequence, (h0, c0)) # 打印输出序列和最终隐藏状态的形状 print("Input Sequence Shape:", input_sequence.shape) print("Output Sequence Shape:", output_sequence.shape) # (seq_len, batch_size, num_directions * hidden_size) print("Final Hidden State Shape (hn):", hn.shape) # (num_layers * num_directions, batch_size, hidden_size) print("Final Cell State Shape (cn):", cn.shape) # (num_layers * num_directions, batch_size, hidden_size)
代码详解:
nn.LSTM(input_size=20, hidden_size=50, num_layers=1): 我们创建了一个单层 LSTM 层实例 lstm_layer,输入特征维度为 20,隐藏状态维度为 50。
torch.randn(30, 64, 20): 我们创建了一个形状为 (30, 64, 20) 的随机输入序列张量 input_sequence,其中 30 是序列长度 (seq_len),64 是 batch size,20 是输入特征维度 (input_size)。
h0 = torch.randn(1, 64, 50), c0 = torch.randn(1, 64, 50): 我们初始化了 LSTM 的初始隐藏状态 h0 和初始细胞状态 c0。对于单层单向 LSTM,它们的形状都为 (1, 64, 50),分别代表 (num_layers * num_directions, batch_size, hidden_size)。如果未显式提供初始状态,LSTM 会默认使用零初始化。
output_sequence, (hn, cn) = lstm_layer(input_sequence, (h0, c0)): 将输入序列 input_sequence 和初始状态 (h0, c0) 传递给 LSTM 层 lstm_layer 进行前向传播,得到输出序列 output_sequence 和最终的隐藏状态 hn 及细胞状态 cn。
形状输出: 可以看到,输入序列的形状为 (30, 64, 20),输出序列的形状为 (30, 64, 50)。输出序列包含了每个时间步的 LSTM 输出,其维度为 (seq_len, batch_size, num_directions * hidden_size)。最终的隐藏状态 hn 和细胞状态 cn 的形状都为 (1, 64, 50),它们代表了整个序列处理完毕后 LSTM 的最终状态。
Mermaid 图示 (简化版,仅展示单时间步 LSTM):
嵌入层 (Embedding Layer) 用于将离散的符号 (例如单词、ID、类别标签等) 转换为连续的低维向量,也称为词向量或嵌入向量。嵌入层在自然语言处理 (NLP) 和推荐系统等领域中广泛应用。
在处理文本数据时,通常需要将文本中的单词转换为数值表示,以便输入到神经网络中。One-hot 编码是一种常见的表示方法,但它会产生高维稀疏的向量,难以捕捉单词之间的语义关系。嵌入层则能够学习到单词的分布式表示,将每个单词映射到一个低维稠密的向量空间中,使得语义相似的单词在向量空间中也彼此靠近。
嵌入层本质上是一个查找表 (Lookup Table)。它维护一个嵌入矩阵,矩阵的每一行对应一个符号的嵌入向量。当输入一个符号的索引时,嵌入层会根据索引在嵌入矩阵中查找对应的行向量,并将其作为输出。
nn.Embedding在 PyTorch 中,嵌入层通过 torch.nn.Embedding 类实现。其构造函数接受以下主要参数:
num_embeddings: 嵌入词汇表的大小,即不同符号的总数。
embedding_dim: 每个嵌入向量的维度,即低维空间的维度。
padding_idx: 可选参数,指定一个 padding 索引,该索引的嵌入向量在训练过程中保持固定为零向量。
代码实践:
import torch import torch.nn as nn # 定义一个嵌入层:词汇表大小为 10000,嵌入维度为 100 embedding_layer = nn.Embedding(num_embeddings=10000, embedding_dim=100) # 创建一个符号索引的张量 (batch_size, seq_len) input_indices = torch.randint(0, 10000, (64, 50)) # 假设 batch_size 为 64,序列长度为 50 # 通过嵌入层进行前向传播 output_embeddings = embedding_layer(input_indices) # 打印输出嵌入向量的形状 print("Input Indices Shape:", input_indices.shape) print("Output Embeddings Shape:", output_embeddings.shape) # (batch_size, seq_len, embedding_dim) # 查看嵌入层的嵌入矩阵 print("Embedding Matrix Shape:", embedding_layer.weight.shape) # (num_embeddings, embedding_dim)
代码详解:
nn.Embedding(num_embeddings=10000, embedding_dim=100): 我们创建了一个嵌入层实例 embedding_layer,词汇表大小为 10000,嵌入维度为 100。PyTorch 会自动初始化嵌入矩阵 embedding_layer.weight 的形状为 (10000, 100)。
torch.randint(0, 10000, (64, 50)): 我们创建了一个形状为 (64, 50) 的随机整数张量 input_indices,作为输入符号的索引。索引的取值范围在 [0, 10000) 之间,模拟词汇表中的单词索引。
output_embeddings = embedding_layer(input_indices): 将输入索引 input_indices 传递给嵌入层 embedding_layer 进行前向传播,得到输出嵌入向量 output_embeddings。
形状输出: 可以看到,输入索引的形状为 (64, 50),输出嵌入向量的形状为 (64, 50, 100)。嵌入层将每个索引转换为一个 100 维的嵌入向量,因此输出张量的维度增加了一维,变为 embedding_dim。嵌入矩阵的形状为 (10000, 100),与我们定义的词汇表大小和嵌入维度一致。
Mermaid 图示:
激活函数 (Activation Function) 在神经网络中扮演着至关重要的角色。它们被应用于神经网络的输出,引入非线性,使得神经网络能够学习和逼近复杂的非线性函数。如果没有激活函数,无论神经网络有多少层,都只能进行线性变换,表达能力将受到限制。
常用的激活函数包括:
ReLU (Rectified Linear Unit): f(x) = \max(0, x)。ReLU 是目前最常用的激活函数之一,其优点是计算简单、收敛速度快,并且在正区间内梯度为常数,有助于缓解梯度消失问题。但 ReLU 在负区间内梯度为 0,可能导致神经元 "死亡"。
Sigmoid: f(x) = \frac{1}{1 + e^{-x}}。Sigmoid 函数将输入值压缩到 (0, 1) 之间,常用于二分类问题的输出层,表示概率值。但 Sigmoid 函数在输入值较大或较小时容易出现梯度饱和,导致梯度消失。
Tanh (Hyperbolic Tangent): f(x) = \tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}。Tanh 函数将输入值压缩到 (-1, 1) 之间,与 Sigmoid 函数类似,也容易出现梯度饱和问题。