PyTorch Scala 高校计算机 硕士研一课程
章节 4: 使用torch.nn搭建模型
在熟悉了PyTorch张量和用于梯度计算的Autograd系统后,我们现在开始构建神经网络本身。本章主要介绍torch.nn包,它是PyTorch用于高效构建网络结构的专用库。
你将学习如何使用核心nn.Module类作为模型的设计蓝图。我们将使用PyTorch提供的常用构建块来组装网络,包括线性层(nn.Linear)、卷积层(nn.Conv2d)和循环层(nn.RNN)。我们还将整合像激活函数(例如ReLU、Sigmoid)这样的重要组成部分,以引入非线性处理能力。此外,你将学习如何使用torch.nn中的损失函数(如MSELossMSELoss或CrossEntropyLossCrossEntropyL**oss)来定义目标衡量标准,以及如何从torch.optim中选择合适的优化算法(如SGD或Adam)在训练期间迭代地优化模型参数。在本章结束时,你将能够在PyTorch中定义并实例化自己的基本神经网络。
torch.nn.Module基类
收藏
构建神经网络在PyTorch中围绕着一个主要的理念:torch.nn.Module。可以将nn.Module视为一个基础蓝图或基类,所有的神经网络模型、层,甚至复杂的复合结构都是以此为基础构建的。它提供了一种标准化的方式来封装模型参数、管理这些参数的辅助函数(例如在CPU和GPU之间移动它们),以及定义输入数据在网络中流动的逻辑。
无论何时,当你在PyTorch中定义自定义神经网络时,通常会通过创建一个继承自nn.Module的Python类来完成。这种继承为你的自定义类提供了大量内置功能,这些功能对于深度学习工作流程来说非常必要。
nn.Module的核心结构
本质上,使用nn.Module需要在你的自定义类中实现两个主要方法:
__init__(self):构造函数。你在这里定义和初始化网络的组件,例如层(卷积层、线性层等)、激活函数,甚至其他nn.Module实例(子模块)。这些组件通常被作为类实例(self)的属性进行赋值。forward(self, input_data):此方法定义了网络的前向传播。它规定了输入数据(input_data)如何流经在__init__中定义的层和组件。forward方法接收一个或多个输入张量,并返回一个或多个输出张量。PyTorch的Autograd系统会根据此forward方法中执行的操作自动构建计算图,从而实现自动微分。
以下是一个自定义模块的骨架:
importtorchimporttorch.nnas nnimporttorch.nn.functionalas Fimporttorch.nnclassMySimpleNetwork[D<:]extendsnn.Module:def__init__(self):super(MySimpleNetwork,self).__init__()# 在 __init__ 中定义层或组件 # 示例:一个线性层self.layer1=nn.Linear(in_features=10,out_features=5)# 示例:一个激活函数实例self.activation=nn.ReLU()defforward(x:Tensor[D]):# 定义数据流经组件的方式 x=self.layer1(x)x=self.activation(x)returnx// 实例化网络valmodel=MySimpleNetwork()println(model)执行此代码将打印出网络结构的表示,展示nn.Module如何帮助组织你的组件。
参数和子模块
nn.Module的一个重要特点是其自动注册和管理可学习参数的能力。当你在__init__方法中将一个PyTorch层(如nn.Linear、nn.Conv2d等)的实例作为属性赋值时,nn.Module会识别该层的内部参数(权重和偏置)。
这些参数是torch.nn.Parameter类的实例,它是torch.Tensor的一个特殊子类。主要区别在于Parameter对象默认自动设置requires_grad=True,并且它们会注册到父nn.Module中。这种注册使得PyTorch可以轻松收集模型的所有可学习参数,这对于在训练期间将它们传递给优化器来说非常重要。
你也可以直接使用nn.Parameter定义你自己的自定义可学习参数:
classCustomModuleWithParameterextendsnn.Module:def__init__(self):super().__init__()# 一个可学习的参数张量valmy_weight=nn.Parameter(torch.randn(5,2))// 一个普通的张量属性(不会自动跟踪用于优化)valmy_info=torch.tensor([1.0,2.0])defforward(x:Tensor[D]):// 示例用法returntorch.matmul(x,my_weight)valmodule=CustomModuleWithParameter()# 访问模块跟踪的参数forname,param<-module.named_parameters():println(f"Parameter name: {name}, Shape: {param.shape}, Requires grad: {param.requires_grad}")注意my_weight被列为参数,而my_info则没有。这种自动跟踪简化了管理深度网络中可能数千或数百万参数的过程。
nn.Module的主要功能
除了定义结构和管理参数之外,nn.Module还提供了一些有用的方法,可供你的自定义类继承:
parameters(): 返回模块内(包括子模块中的)所有nn.Parameter对象的迭代器。这通常用于将模型的参数提供给优化器。named_parameters(): 类似于parameters(),但会生成(参数名,参数对象)的元组。这有助于检查或有选择地修改特定参数。children(): 返回直接子模块(定义为属性的子模块)的迭代器。modules(): 返回网络中所有模块的迭代器,从模块自身开始,然后递归遍历所有子模块。state_dict(): 返回一个Python字典,该字典包含模块的完整状态,主要将每个参数和缓冲区名称映射到其对应的张量。这对于保存模型权重非常重要。load_state_dict(): 将状态(通常来自保存的文件)加载回模块,恢复参数和缓冲区。to(device): 将模块的所有参数和缓冲区移动到指定设备(例如,GPU的'cuda'或CPU的'cpu')。这对于硬件加速非常重要。train(): 将模块及其子模块设置为训练模式。这会影响像Dropout和BatchNorm这样的层,它们在训练和评估期间表现不同。eval(): 将模块及其子模块设置为评估模式。
理解nn.Module非常重要,因为它建立了在PyTorch中定义任何神经网络架构的标准模式。在接下来的章节中,我们将使用这个基类来构建包含各种层、激活函数和损失函数的网络。
定义自定义网络架构
许多网络架构需要比简单的线性层堆叠更复杂的设计。尽管torch.nn.Sequential对于线性模型很方便,但更复杂的设计常常是必需的。你可能需要跳跃连接(如在 ResNet 中)、多输入/输出路径,或以非顺序方式使用的层。在这种情况下,通过继承torch.nn.Module来定义你自己的自定义网络架构就变得必不可少。这种方法在指定数据如何通过模型流动方面提供了最大的灵活性。
这个基本过程包含两个主要步骤:
- 在构造函数 (
__init__) 中定义层:创建一个继承自torch.nn.Module的 Python 类。在其__init__方法中,你必须首先调用父类的构造函数 (super().__init__())。然后,实例化网络所需的所有层(例如nn.Linear、nn.Conv2d、nn.ReLU等),并将它们作为类实例的属性(使用self)进行分配。这些层就成为你的自定义模块的子模块。 - 在
forward方法中定义数据流:为你的类实现forward方法。此方法将输入张量作为参数,并定义输入数据如何通过你在__init__中定义的层进行传播。此方法的输出是你的网络对于给定输入的最终输出。PyTorch 的 Autograd 系统会根据此forward方法中执行的操作自动构建计算图。
我们从一个基本例子开始:一个实现为自定义模块的简单线性回归模型。
importtorchimporttorch.nnas nnclassSimpleLinearModelextendsnn.Module:def__init__(self,input_features:Int,output_features:Int):// 调用父类构造函数super().__init__()// 定义单个线性层vallinear_layer=nn.Linear(input_features,output_features)// 打印初始化信息println(f"已初始化 SimpleLinearModel,输入特征数={input_features},输出特征数={output_features}")println(f"已定义层: {linear_layer}")defforward(x:Tensor[D]):// 定义前向传播:将输入通过线性层println(f"前向传播输入形状: {x.shape}")valoutput=linear_layer(x)println(f"前向传播输出形状: {output.shape}")returnoutput// --- 使用示例 ---// 定义输入和输出维度valin_dim=10valout_dim=1// 实例化自定义模型valmodel=SimpleLinearModel(in_dim,out_dim)// 创建一些模拟输入数据(batch_size=5,特征数=10)valdummy_input=torch.randn(5,in_dim)println(f"\n模拟输入张量形状: {dummy_input.shape}")// 将数据通过模型valoutput=model(dummy_input)println(f"模型输出张量形状: {output.shape}")// 检查参数(自动注册)println("\n模型参数:")for(name,param)<-model.named_parameters():ifparam.requires_grad then println(s" 名称: {name}, 形状: {param.shape}")在此示例中:
SimpleLinearModel继承自nn.Module。__init__调用super().__init__()并定义self.linear_layer = nn.Linear(...)。此层现在是一个已注册的子模块。forward(self, x)接收输入x并将其通过self.linear_layer,然后返回结果。
PyTorch 会自动追踪nn.Linear层的参数(权重和偏置),因为它们被作为属性分配在nn.Module子类中。我们可以通过查看model.parameters()或model.named_parameters()来验证这一点。
构建多层感知机 (MLP)
现在,我们来构建一个稍微复杂一点的模型,一个两层 MLP,在层之间带有一个 ReLU 激活函数。
importtorchimporttorch.nnas nnimporttorch.nn.functionalas F//通常用于函数式 API,如激活函数classSimpleMLPextendsnn.Module:def__init__(input_size:Int,hidden_size:Int,output_size:Int):super().__init__()# 定义层vallayer1=nn.Linear(input_size,hidden_size)valactivation=nn.ReLU()// 将激活函数定义为层vallayer2=nn.Linear(hidden_size,output_size)println(f"已初始化 SimpleMLP: 输入={input_size}, 隐藏层={hidden_size}, 输出={output_size}")println(f"层 1: {layer1}")println(f"激活函数: {activation}")println(f"层 2: {layer2}")defforward(x:Tensor[D]):// 定义前向传播序列println(f"前向传播输入形状: {x.shape}")valx=layer1(x)println(f"经过层 1 后的形状: {x.shape}")valx=activation(x)// 应用 ReLU 激活println(f"经过激活函数后的形状: {x.shape}")valx=layer2(x)println(f"经过层 2(输出)后的形状: {x.shape}")returnx// --- 使用示例 ---// 定义维度valin_size=784// 示例:展平的 28x28 图像valhidden_units=128valout_size=10// 示例:用于分类的 10 个类别// 实例化 MLPvalmlp_model=SimpleMLP(input_size=in_size,hidden_size=hidden_units,output_size=out_size)// 创建模拟输入(批大小=32)valdummy_mlp_input=torch.randn(32,in_size)println(f"\n模拟 MLP 输入形状: {dummy_mlp_input.shape}")// 前向传播valmlp_output=mlp_model(dummy_mlp_input)println(f"MLP 输出形状: {mlp_output.shape}")// 检查参数println("\nMLP 模型参数:")for(name,param)<-mlp_model.named_parameters():ifparam.requires_grad then println(f" 名称: {name}, 形状: {param.shape}")这里,forward方法明确规定了序列:输入 ->layer1->activation->layer2-> 输出。请注意,nn.ReLU等激活函数通常也在__init__中定义为层,并在forward中调用。另外,你也可以直接在forward方法中使用其函数式等效项(例如,导入torch.nn.functional as F后使用F.relu(x)),特别是对于没有可学习参数的激活函数。
可视化 MLP 结构
我们可以可视化SimpleMLP的forward方法中定义的数据流。
输入 (x)层1 (线性)激活函数 (ReLU)层2 (线性)输出
数据流经
SimpleMLP模型,如其forward方法中所定义。
继承nn.Module的优点
- 灵活性:这是主要优势。你可以实现任何架构,包括具有多个输入/输出、残差连接(其中输入被加回到后续层的输出)、共享层或
forward传递中自定义操作的架构。nn.Sequential仅限于严格的线性层序列。 - 可读性和组织性:复杂的架构通常在类结构中组织时更容易理解,其中层在
__init__中定义,它们的交互方式在forward中定义。 - 参数管理:PyTorch 会自动发现并注册在
__init__方法中作为属性分配的任何nn.Module(例如self.layer1 = nn.Linear(...))。这意味着model.parameters()将正确地给出所有子模块的所有可学习参数(权重、偏置),使其可以轻松传递给优化器。 - 嵌套:自定义模块可以包含其他模块(包括
nn.Sequential或其他自定义模块),从而允许你构建分层和可重用的组件。
通过继承nn.Module,你可以完全控制网络的结构,从而实现针对特定任务的复杂深度学习模型。这种方法是构建更复杂的前馈网络的标准做法。
常见层:线性、卷积、循环
PyTorch 提供神经网络模型的基本构成单元,即层。torch.nn包提供了多种预构建层,它们执行神经网络中常见的操作。这些层将可学习参数(权重和偏置)和操作本身都包含在内。这里将介绍三种主要类型:线性层、卷积层和循环层。
线性层 (nn.Linear)
神经网络层最基本的类型是线性层,也称为全连接层或密集层。它对输入数据进行线性变换。如果输入张量的形状为 (∗,Hin)(∗,H**in),其中 ∗∗ 代表任意数量的前导维度(如批大小),HinH**in是输入特征的数量,那么nn.Linear层会将其转换为形状为 (∗,Hout)(∗,Hou**t) 的输出张量,其中 HoutHou**t是为该层指定的输出特征数量。
在数学上,此操作表示为 y=xWT+by=xWT+b,其中:
- xx是输入
- WW是权重矩阵
- bb是偏置向量
- yy是输出
您可以通过指定输入特征和输出特征的数量来创建线性层。
importtorchimporttorch.nnas nn// 示例:创建一个线性层,输入特征大小为 20,输出特征大小为 30vallinear_layer=nn.Linear(in_features=20,out_features=30)// 创建一个示例输入张量(批大小 64,20 个特征)valinput_tensor=torch.randn(64,20)// 将输入通过该层valoutput_tensor=linear_layer(input_tensor)println(f"Input shape: {input_tensor.shape}")println(f"Output shape: {output_tensor.shape}")// 预期输出:// Input shape: torch.Size([64, 20])// Output shape: torch.Size([64, 30])// 检查层的参数(自动创建)println(f"\nWeight shape: {linear_layer.weight.shape}")println(f"Bias shape: {linear_layer.bias.shape}")// 预期输出:// Weight shape: torch.Size([30, 20])// Bias shape: torch.Size([30])线性层是许多架构中的基本组成部分,包括简单的多层感知机(MLP),并且在像 CNN 和 RNN 这样更复杂的模型中,它们通常作为最终的分类或回归头部。
卷积层 (nn.Conv2d)
卷积层是现代计算机视觉模型的核心。与对扁平特征向量进行操作的线性层不同,卷积层设计用于处理网格状数据(如图像),并保留空间关系。用于二维数据(如图像)的主要层是nn.Conv2d。
它的工作原理是通过在输入空间维度(高和宽)上滑动小型滤波器(卷积核)。对于滤波器的每个位置,它计算滤波器权重与滤波器下输入图像块的点积,从而在输出特征图中生成一个元素。这个过程有助于检测边缘、角点和纹理等空间模式。
nn.Conv2d的主要参数包括:
in_channels:输入图像中的通道数量(例如,RGB 图像为 3)。out_channels:要应用的滤波器数量。每个滤波器生成一个输出通道(特征图)。kernel_size:滤波器的大小(高,宽)。可以是单个整数用于方形卷积核,或一个元组(H, W)。stride(可选,默认 1):滤波器在每一步移动的像素数。padding(可选,默认 0):添加到输入边界的零填充量。
// 示例:处理一批 16 张图像,3 通道(RGB),32x32 像素// 应用 6 个滤波器(输出通道),每个大小为 5x5valconv_layer=nn.Conv2d(in_channels=3,out_channels=6,kernel_size=5)// 创建一个示例输入张量(批大小,通道,高,宽)// PyTorch 通常期望通道优先的格式 (N, C, H, W)valinput_image_batch=torch.randn(16,3,32,32)// 将输入通过卷积层valoutput_feature_maps=conv_layer(input_image_batch)println(f"Input shape: {input_image_batch.shape}")println(f"Output shape: {output_feature_maps.shape}")// 没有填充/步幅时,输出大小会减小:32 - 5 + 1 = 28// 预期输出:// Input shape: torch.Size([16, 3, 32, 32])// Output shape: torch.Size([16, 6, 28, 28])// 检查参数println(f"\nWeight (filter) shape: {conv_layer.weight.shape}")// (输出通道,输入通道,卷积核高,卷积核宽)println(f"Bias shape: {conv_layer.bias.shape}")// (输出通道)// 预期输出:// Weight (filter) shape: torch.Size([6, 3, 5, 5])// Bias shape: torch.Size([6])nn.Conv2d及其变体(nn.Conv1d、nn.Conv3d)对涉及空间层次的任务是不可或缺的,主要用于图像和视频分析,但有时也应用于序列数据。我们将在第 7 章更详细地了解如何构建 CNN。
循环层 (nn.RNN)
循环神经网络(RNN)设计用于处理序列数据,其中元素的顺序很重要。示例包括文本、时间序列数据或音频信号。RNN 层的核心思想是维护一个隐藏状态,该状态捕捉序列中先前元素的信息,并影响当前元素的处理。
PyTorch 中基本的nn.RNN层逐步处理输入序列。在每一步 tt,它接收输入 xtx**t和前一个隐藏状态 ht−1h**t−1,以计算输出 oto**t(可选,通常只使用最终隐藏状态)和新的隐藏状态 hth**t。
nn.RNN的主要参数:
input_size:每个时间步输入中的特征数量。hidden_size:隐藏状态中的特征数量。num_layers(可选,默认 1):堆叠 RNN 层的数量。batch_first(可选,默认 False):如果为 True,输入和输出张量将以(batch, seq_len, features)形式提供,而不是默认的(seq_len, batch, features)。
// 示例:处理一批 10 个序列,每个序列长 20 步,每步有 5 个特征。// 使用大小为 30 的隐藏状态。// 设置 batch_first=True 以便更方便地处理数据。valrnn_layer=nn.RNN(input_size=5,hidden_size=30,batch_first=true)// 创建一个示例输入张量(批,序列长度,输入特征)valinput_sequence_batch=torch.randn(10,20,5)// 初始化隐藏状态(层数,批,隐藏状态大小)// 如果未提供,默认为零。valinitial_hidden_state=torch.randn(1,10,30)// 层数=1// 将输入序列和初始隐藏状态通过 RNN// 输出包含所有时间步的输出// final_hidden_state 包含最后一个时间步的隐藏状态val(output_sequence,final_hidden_state)=rnn_layer(input_sequence_batch,initial_hidden_state)println(f"Input shape: {input_sequence_batch.shape}")println(f"Initial hidden state shape: {initial_hidden_state.shape}")println(f"Output sequence shape: {output_sequence.shape}")// (批,序列长度,隐藏状态大小)println(f"Final hidden state shape: {final_hidden_state.shape}")// (层数,批,隐藏状态大小)// 预期输出:// Input shape: torch.Size([10, 20, 5])// Initial hidden state shape: torch.Size([1, 10, 30])println(f"Output sequence shape: {output_sequence.shape}")// (批,序列长度,隐藏状态大小)println(f"Final hidden state shape: {final_hidden_state.shape}")// (层数,批,隐藏状态大小)// 预期输出:// Output sequence shape: torch.Size([10, 20, 30])// Final hidden state shape: torch.Size([1, 10, 30])虽然nn.RNN展示了基本思想,但简单的 RNN 通常因梯度消失而难以处理长序列。在实际应用中,通常更偏好nn.LSTM(长短期记忆)和nn.GRU(门控循环单元)等更高级的循环层,因为它们包含门控机制,能更好地管理长距离依赖中的信息流。这些将在第 7 章再次提及。
这三种层类型(线性层、卷积层、循环层)代表了针对不同数据和任务的基本操作。torch.nn提供了这些层以及许多其他层(如池化层、归一化层、dropout 层),它们可以在nn.Module子类中组合起来,以构建复杂的深度学习模型。在接下来的部分中,我们将看到如何将这些层与非线性激活函数结合,并定义使用损失函数和优化器训练它们的标准。
激活函数 (ReLU, Sigmoid, Tanh)
收藏
神经网络的表示能力很大程度上得益于在层之间引入非线性。如果只是简单地堆叠线性变换(如nn.Linear层)而没有任何介入函数,整个网络将简化为一个单一的等效线性变换。无论网络有多少层,它都只能学习输入与输出之间的线性关系。
激活函数是引入这些重要非线性的组成部分。它们逐元素应用于层的输出(常被称为预激活值或logit),在将值传递给下一层之前对其进行转换。PyTorch 在torch.nn模块中提供了各种各样的激活函数,通常通过将它们实例化为层在模型定义中使用。我们来看看其中最常见的三种:ReLU、Sigmoid 和 Tanh。
ReLU (修正线性单元)
修正线性单元,简称ReLU,可以说是现代深度学习中最受欢迎的激活函数,尤其是在卷积神经网络中。它的定义非常简单:如果输入为正,它直接输出输入值,否则输出零。
其数学定义为:
ReLU(x)=max(0,x)ReLU(x)=max(0,x)
在 PyTorch 中,可以使用nn.ReLU:
importtorchimporttorch.nnas nn// 示例用法valrelu_activation=nn.ReLU()valinput_tensor=torch.randn(4)// 示例输入张量valoutput_tensor=relu_activation(input_tensor)println(f"输入: {input_tensor}")println(f"ReLU 输出: {output_tensor}")// 在简单模型中的示例classSimpleNetextendsnn.Module:def__init__(self):super().__init__()vallayer1=nn.Linear(10,20)valactivation=nn.ReLU()vallayer2=nn.Linear(20,5)defforward(x:torch.Tensor):x=layer1(x)x=activation(x)// 应用 ReLUx=layer2(x)returnx// 示例模型valmodel=SimpleNet()ReLU 函数对负输入为零,对正输入为线性。
优点:
- 计算效率高:计算非常简单(max(0,x)max(0,x))。
- 减少梯度消失:对于正输入,梯度为1,这有助于在训练期间梯度反向传播,相比于 Sigmoid 或 Tanh 等饱和函数。
- 引入稀疏性:由于负输入被映射到零,这可以导致网络中出现稀疏激活,有时可能是有益的。
缺点:
- ReLU 死亡问题:输入始终落在负区间的神经元将输出零。因此,流经它们的梯度也将为零,这意味着它们的权重在反向传播期间不会被更新。这些神经元实际上“死亡”了,不再对学习有贡献。Leaky ReLU 或 Parametric ReLU (PReLU) 等变体试图解决此问题。
- 非零中心:输出始终为非负值。
Sigmoid
Sigmoid 函数,有时也称为逻辑函数,将其输入压缩到 0 到 1 的范围内。它在历史上很受欢迎,尤其是在二元分类模型的输出层,其中输出代表一个概率。
其数学形式为:
σ(x)=11+e−xσ(x)=1+e−x1
在 PyTorch 中,可以使用nn.Sigmoid:
importtorchimporttorch.nnas nn// 示例用法valsigmoid_activation=nn.Sigmoid()valinput_tensor=torch.randn(4)// 示例输入张量valoutput_tensor=sigmoid_activation(input_tensor)println(f"输入: {input_tensor}")println(f"Sigmoid 输出: {output_tensor}")Sigmoid 函数将任意实数平滑地映射到 (0, 1) 的范围内。
优点:
- 输出易于理解:(0, 1) 的范围便于表示概率。
- 梯度平滑:函数处处可微,提供平滑的梯度。
缺点:
- 梯度消失:对于非常大或非常小的输入,函数会饱和(输出接近 1 或 0),梯度变得非常接近零。这会严重减缓或停止深度网络的学习,因为梯度难以通过多层反向传播。
- 非零中心:输出始终为正,这有时会减缓收敛速度,相比于零中心激活函数。
- 计算成本更高:指数函数比 ReLU 的简单比较成本更高。
由于梯度消失问题,Sigmoid 在今天的深度网络隐藏层中不如 ReLU 常用,但它在特定任务(例如二元分类或多标签分类)的输出层中仍然适用。
Tanh (双曲正切)
双曲正切函数,即 Tanh 函数,在数学上与 Sigmoid 相关,但将其输入压缩到 (-1, 1) 的范围内。
其定义为:
tanh(x)=ex−e−xex+e−x=2σ(2x)−1tanh(x)=e**x+e−xex−e−x=2σ(2x)−1
在 PyTorch 中,可以使用nn.Tanh:
importtorchimporttorch.nnas nn// 示例用法valtanh_activation=nn.Tanh()valinput_tensor=torch.randn(4)// 示例输入张量valoutput_tensor=tanh_activation(input_tensor)println(f"输入: {input_tensor}")println(f"Tanh 输出: {output_tensor}")Tanh 函数将任意实数平滑地映射到 (-1, 1) 的范围内。
优点:
- 零中心输出:与 Sigmoid 不同,Tanh 的输出以零为中心,这通常有助于模型在训练期间的收敛。零中心数据通常与基于梯度的优化方法配合得更好。
- 梯度平滑:与 Sigmoid 类似,它处处可微。
缺点:
- 梯度消失:与 Sigmoid 类似,Tanh 也会在很大正值或负值输入时出现饱和,导致深度网络中梯度消失。虽然由于其零中心性质,在隐藏层中它通常比 Sigmoid 更受青睐,但它仍然容易受到此问题的影响。
- 计算成本更高:涉及指数函数,使其比 ReLU 成本更高。
在 ReLU 兴起之前,Tanh 在隐藏层中通常比 Sigmoid 更受青睐,主要因为其零中心输出范围。它仍然常见于循环神经网络 (RNN) 和 LSTM 中。
选择激活函数
没有一个“最佳”激活函数适用于所有情况。然而,有一些通用指导原则:
- ReLU通常是前馈网络和 CNN 中隐藏层的默认选择,因为它高效且能有效缓解正输入时的梯度消失问题。从 ReLU 开始,如果遇到诸如死亡神经元之类的问题,再考虑其他替代方案。
- 如果怀疑存在“ReLU 死亡”问题,Leaky ReLU或Parametric ReLU (PReLU)是不错的替代方案。它们为负输入引入了一个小的非零斜率。
- Tanh在隐藏层中可能很有效,尤其是在 RNN 中,因为它有零中心输出。
- Sigmoid通常保留用于输出层,当你需要用于二元或多标签分类的概率时。因为梯度消失问题,避免在深度隐藏层中大量使用它。
通常需要进行实验,以找到适用于特定架构和数据集的最佳激活函数。在 PyTorch 中,更换激活函数很简单,通常只需更改一行代码,即激活模块实例化或在nn.Module的forward方法中被调用的位置。