
# 章节 4: 使用 `torch.nn` 搭建模型
在熟悉了PyTorch张量和用于梯度计算的Autograd系统后,我们现在开始构建神经网络本身。本章主要介绍`torch.nn`包,它是PyTorch用于高效构建网络结构的专用库。
你将学习如何使用核心`nn.Module`类作为模型的设计蓝图。我们将使用PyTorch提供的常用构建块来组装网络,包括线性层(`nn.Linear`)、卷积层(`nn.Conv2d`)和循环层(`nn.RNN`)。我们还将整合像激活函数(例如ReLU、Sigmoid)这样的重要组成部分,以引入非线性处理能力。此外,你将学习如何使用`torch.nn`中的损失函数(如MSELoss*MSE**L**oss*或CrossEntropyLoss*C**ross**E**n**t**ro**p**y**L**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`需要在你的自定义类中实现两个主要方法:
1. **`__init__(self)`:** 构造函数。你在这里定义和初始化网络的组件,例如层(卷积层、线性层等)、激活函数,甚至其他`nn.Module`实例(子模块)。这些组件通常被作为类实例(`self`)的属性进行赋值。
2. **`forward(self, input_data)`:** 此方法定义了网络的*前向传播*。它规定了输入数据(`input_data`)如何流经在`__init__`中定义的层和组件。`forward`方法接收一个或多个输入张量,并返回一个或多个输出张量。PyTorch的Autograd系统会根据此`forward`方法中执行的操作自动构建计算图,从而实现自动微分。
以下是一个自定义模块的骨架:
```scala 3
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.nn
class MySimpleNetwork[D <: ] extends nn.Module:
def __init__(self):
super(MySimpleNetwork, self).__init__()
# 在 __init__ 中定义层或组件
# 示例:一个线性层
self.layer1 = nn.Linear(in_features=10, out_features=5)
# 示例:一个激活函数实例
self.activation = nn.ReLU()
def forward(x: Tensor[D]):
# 定义数据流经组件的方式
x = self.layer1(x)
x = self.activation(x)
return x
// 实例化网络
val model = MySimpleNetwork()
println(model)
```
```java
import org.bytedeco.javacpp.LongPointer;
import org.bytedeco.pytorch.*;
import org.bytedeco.pytorch.Module;
import org.bytedeco.pytorch.global.torch;
import static org.bytedeco.pytorch.global.torch.*;
class MySimpleNetwork extends Module {
MySimpleNetwork() {
layer1 = register_module("layer1", new LinearImpl(10, 5));
activation = register_module("activation", new ReLUImpl());
}
Tensor forward(Tensor input) {
Tensor x = layer1.forward(input);
x = activation.forward(x);
return x;
}
private final LinearImpl layer1;
private final ReLUImpl activation;
}
System.out.println("\n=== MySimpleNetwork 示例 ===");
MySimpleNetwork model2 = new MySimpleNetwork();
System.out.println(model2);
```
执行此代码将打印出网络结构的表示,展示`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`定义你自己的自定义可学习参数:
```scala 3
class CustomModuleWithParameter extends nn.Module:
def __init__(self):
super().__init__()
# 一个可学习的参数张量
val my_weight = nn.Parameter(torch.randn(5, 2))
// 一个普通的张量属性(不会自动跟踪用于优化)
val my_info = torch.tensor([1.0, 2.0])
def forward(x: Tensor[D]):
// 示例用法
return torch.matmul(x, my_weight)
val module = CustomModuleWithParameter()
# 访问模块跟踪的参数
for name, param <- module.named_parameters():
println(f"Parameter name: {name}, Shape: {param.shape}, Requires grad: {param.requires_grad}")
```
```java
// 02 - CustomModuleWithParameter
class CustomModuleWithParameter extends Module {
CustomModuleWithParameter() {
my_weight = register_parameter("my_weight", randn(5, 2));
var t = new float[]{1.0f, 2.0f};
my_info = torch.tensor(t);
}
Tensor forward(Tensor x) {
Tensor result = matmul(x, my_weight);
return result;
}
private final Tensor my_weight;
private final Tensor my_info;
}
System.out.println("\n=== CustomModuleWithParameter 示例 ===");
CustomModuleWithParameter moduleWithParameter = new CustomModuleWithParameter();
var paramBegin = moduleWithParameter.named_parameters().begin();
var paramEnd = moduleWithParameter.named_parameters().end();
while(!paramBegin.equals(paramEnd)) {
var item = paramBegin.get();
System.out.printf("参数名称: %s, 形状: %s%n", item.key().getString(), shapeToString(item.value().sizes()));
paramBegin.increment();
}
private static String shapeToString(LongArrayRef sizes) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < sizes.size(); i++) {
if (i > 0) sb.append(", ");
sb.append(sizes.get(i));
}
sb.append("]");
return sb.toString();
}
```
注意`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` 来定义你自己的自定义网络架构就变得必不可少。这种方法在指定数据如何通过模型流动方面提供了最大的灵活性。
这个基本过程包含两个主要步骤:
1. **在构造函数 (`__init__`) 中定义层**:创建一个继承自 `torch.nn.Module` 的 Python 类。在其 `__init__` 方法中,你必须首先调用父类的构造函数 (`super().__init__()`)。然后,实例化网络所需的所有层(例如 `nn.Linear`、`nn.Conv2d`、`nn.ReLU` 等),并将它们作为类实例的属性(使用 `self`)进行分配。这些层就成为你的自定义模块的子模块。
2. **在 `forward` 方法中定义数据流**:为你的类实现 `forward` 方法。此方法将输入张量作为参数,并定义输入数据如何通过你在 `__init__` 中定义的层进行传播。此方法的输出是你的网络对于给定输入的最终输出。PyTorch 的 Autograd 系统会根据此 `forward` 方法中执行的操作自动构建计算图。
我们从一个基本例子开始:一个实现为自定义模块的简单线性回归模型。
```scala 3
import torch
import torch.nn as nn
class SimpleLinearModel extends nn.Module:
def __init__(self, input_features: Int, output_features: Int):
// 调用父类构造函数
super().__init__()
// 定义单个线性层
val linear_layer = nn.Linear(input_features, output_features)
// 打印初始化信息
println(f"已初始化 SimpleLinearModel,输入特征数={input_features},输出特征数={output_features}")
println(f"已定义层: {linear_layer}")
def forward(x: Tensor[D]):
// 定义前向传播:将输入通过线性层
println(f"前向传播输入形状: {x.shape}")
val output = linear_layer(x)
println(f"前向传播输出形状: {output.shape}")
return output
// --- 使用示例 ---
// 定义输入和输出维度
val in_dim = 10
val out_dim = 1
// 实例化自定义模型
val model = SimpleLinearModel(in_dim, out_dim)
// 创建一些模拟输入数据(batch_size=5,特征数=10)
val dummy_input = torch.randn(5, in_dim)
println(f"\n模拟输入张量形状: {dummy_input.shape}")
// 将数据通过模型
val output = model(dummy_input)
println(f"模型输出张量形状: {output.shape}")
// 检查参数(自动注册)
println("\n模型参数:")
for (name, param) <- model.named_parameters():
if param.requires_grad then
println(s" 名称: {name}, 形状: {param.shape}")
```
```java
// 03 - SimpleLinearModel
static class SimpleLinearModel extends Module {
SimpleLinearModel(int input_features, int output_features) {
linear_layer = register_module("linear_layer", new LinearImpl(input_features, output_features));
System.out.printf("已初始化 SimpleLinearModel,输入特征数= %d,输出特征数= %d%n", input_features, output_features);
System.out.printf("已定义层: %s%n", linear_layer);
}
Tensor forward(Tensor x) {
System.out.printf("前向传播输入形状: %s%n", shapeToString(x.sizes()));
Tensor output = linear_layer.forward(x);
System.out.printf("前向传播输出形状: %s%n", shapeToString(output.sizes()));
return output;
}
private final LinearImpl linear_layer;
}
System.out.println("\n=== SimpleLinearModel 示例 ===");
int in_dim = 10;
int out_dim = 1;
SimpleLinearModel model3 = new SimpleLinearModel(in_dim, out_dim);
Tensor dummy_input = torch.randn(5, in_dim).to(ScalarType.Float);
System.out.printf("\n模拟输入张量形状: %s%n", shapeToString(dummy_input.sizes()));
Tensor output = model3.forward(dummy_input);
System.out.printf("模型输出张量形状: %s%n", shapeToString(output.sizes()));
var paramsBegin = model3.named_parameters().begin();
var paramsEnd = model3.named_parameters().end();
while(!paramsBegin.equals(paramsEnd)) {
var item = paramsBegin.get();
System.out.printf("参数名称: %s, 形状: %s%n", item.key().getString(), shapeToString(item.value().sizes()));
paramsBegin.increment();
}
```
在此示例中:
- `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 激活函数。
```scala 3
import torch
import torch.nn as nn
import torch.nn.functional as F
class SimpleMLP extends nn.Module:
def __init__(input_size: Int, hidden_size: Int, output_size: Int):
super().__init__()
# 定义层
val layer1 = nn.Linear(input_size, hidden_size)
val activation = nn.ReLU() // 将激活函数定义为层
val layer2 = 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}")
def forward(x: Tensor[D]):
// 定义前向传播序列
println(f"前向传播输入形状: {x.shape}")
val x = layer1(x)
println(f"经过层 1 后的形状: {x.shape}")
val x = activation(x) // 应用 ReLU 激活
println(f"经过激活函数后的形状: {x.shape}")
val x = layer2(x)
println(f"经过层 2(输出)后的形状: {x.shape}")
return x
// --- 使用示例 ---
// 定义维度
val in_size = 784 // 示例:展平的 28x28 图像
val hidden_units = 128
val out_size = 10 // 示例:用于分类的 10 个类别
// 实例化 MLP
val mlp_model = SimpleMLP(input_size=in_size, hidden_size=hidden_units, output_size=out_size)
// 创建模拟输入(批大小=32)
val dummy_mlp_input = torch.randn(32, in_size)
println(f"\n模拟 MLP 输入形状: {dummy_mlp_input.shape}")
// 前向传播
val mlp_output = mlp_model(dummy_mlp_input)
println(f"MLP 输出形状: {mlp_output.shape}")
// 检查参数
println("\nMLP 模型参数:")
for (name, param) <- mlp_model.named_parameters():
if param.requires_grad then
println(f" 名称: {name}, 形状: {param.shape}")
```
```java
// 04 - SimpleMLP
static class SimpleMLP extends Module {
SimpleMLP(int input_size, int hidden_size, int output_size) {
layer1 = register_module("layer1", new LinearImpl(input_size, hidden_size));
activation = register_module("activation", new ReLUImpl());
layer2 = register_module("layer2", new LinearImpl(hidden_size, output_size));
System.out.printf("已初始化 SimpleMLP: 输入=%d, 隐藏层=%d, 输出=%d%n", input_size, hidden_size, output_size);
System.out.printf("层 1: %s%n", layer1);
System.out.printf("激活函数: %s%n", activation);
System.out.printf("层 2: %s%n", layer2);
}
Tensor forward(Tensor input) {
System.out.printf("前向传播输入形状: %s%n", shapeToString(input.sizes()));
Tensor x = layer1.forward(input);
System.out.printf("经过层 1 后的形状: %s%n", shapeToString(x.sizes()));
x = activation.forward(x);
System.out.printf("经过激活函数后的形状: %s%n", shapeToString(x.sizes()));
x = layer2.forward(x);
System.out.printf("经过层 2(输出)后的形状: %s%n", shapeToString(x.sizes()));
return x;
}
private final LinearImpl layer1;
private final ReLUImpl activation;
private final LinearImpl layer2;
}
System.out.println("\n=== SimpleMLP 示例 ===");
int in_size = 784;
int hidden_units = 128;
int out_size = 10;
SimpleMLP mlp_model = new SimpleMLP(in_size, hidden_units, out_size);
Tensor dummy_mlp_input = torch.randn(32, in_size).to(ScalarType.Float);
System.out.printf("\n模拟 MLP 输入形状: %s%n", shapeToString(dummy_mlp_input.sizes()));
Tensor mlp_output = mlp_model.forward(dummy_mlp_input);
System.out.printf("MLP 输出形状: %s%n", shapeToString(mlp_output.sizes()));
var mlpParamsBegin = mlp_model.named_parameters().begin();
var mlpParamsEnd = mlp_model.named_parameters().end();
while(!mlpParamsBegin.equals(mlpParamsEnd)) {
var item = mlpParamsBegin.get();
System.out.printf("参数名称: %s, 形状: %s%n", item.key().getString(), shapeToString(item.value().sizes()));
mlpParamsBegin.increment();
}
```
这里,`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*),其中 ∗∗ 代表任意数量的前导维度(如批大小),Hin*H**in* 是输入特征的数量,那么 `nn.Linear` 层会将其转换为形状为 (∗,Hout)(∗,*H**o**u**t*) 的输出张量,其中 Hout*H**o**u**t* 是为该层指定的输出特征数量。
在数学上,此操作表示为 y=xWT+b*y*=*x**W**T*+*b*,其中:
- x*x* 是输入
- W*W* 是权重矩阵
- b*b* 是偏置向量
- y*y* 是输出
您可以通过指定输入特征和输出特征的数量来创建线性层。
```scala 3
import torch
import torch.nn as nn
val linear_layer = nn.Linear(in_features=20, out_features=30)
val input_tensor = torch.randn(64, 20)
val output_tensor = linear_layer(input_tensor)
println(f"Input shape: {input_tensor.shape}")
println(f"Output shape: {output_tensor.shape}")
println(f"\nWeight shape: {linear_layer.weight.shape}")
println(f"Bias shape: {linear_layer.bias.shape}")
```
```java
System.out.println("\n=== Linear 线性层示例 ===");
LinearImpl linear_layer = new LinearImpl(20, 30);
Tensor input_tensor2 = torch.randn(64, 20).to(ScalarType.Float);
Tensor output_tensor2 = linear_layer.forward(input_tensor2);
System.out.printf("Input shape: %s%n", shapeToString(input_tensor2.sizes()));
System.out.printf("Output shape: %s%n", shapeToString(output_tensor2.sizes()));
System.out.printf("\nWeight shape: %s%n", shapeToString(linear_layer.weight().sizes()));
System.out.printf("Bias shape: %s%n", shapeToString(linear_layer.bias().sizes()));
```
线性层是许多架构中的基本组成部分,包括简单的多层感知机(MLP),并且在像 CNN 和 RNN 这样更复杂的模型中,它们通常作为最终的分类或回归头部。
### 卷积层 (`nn.Conv2d`)
卷积层是现代计算机视觉模型的核心。与对扁平特征向量进行操作的线性层不同,卷积层设计用于处理网格状数据(如图像),并保留空间关系。用于二维数据(如图像)的主要层是 `nn.Conv2d`。
它的工作原理是通过在输入空间维度(高和宽)上滑动小型滤波器(卷积核)。对于滤波器的每个位置,它计算滤波器权重与滤波器下输入图像块的点积,从而在输出特征图中生成一个元素。这个过程有助于检测边缘、角点和纹理等空间模式。
`nn.Conv2d` 的主要参数包括:
- `in_channels`:输入图像中的通道数量(例如,RGB 图像为 3)。
- `out_channels`:要应用的滤波器数量。每个滤波器生成一个输出通道(特征图)。
- `kernel_size`:滤波器的大小(高,宽)。可以是单个整数用于方形卷积核,或一个元组 `(H, W)`。
- `stride` (可选,默认 1):滤波器在每一步移动的像素数。
- `padding` (可选,默认 0):添加到输入边界的零填充量。
```scala 3
val conv_layer = nn.Conv2d(in_channels=3, out_channels=6, kernel_size=5)
val input_image_batch = torch.randn(16, 3, 32, 32)
val output_feature_maps = conv_layer(input_image_batch)
println(f"Input shape: {input_image_batch.shape}")
println(f"Output shape: {output_feature_maps.shape}")
println(f"\nWeight (filter) shape: {conv_layer.weight.shape}")
println(f"Bias shape: {conv_layer.bias.shape}")
```
```java
System.out.println("\n=== Conv2d 卷积层示例 ===");
Conv2dImpl conv_layer = new Conv2dImpl(new Conv2dOptions(3, 6, new LongPointer(5)));
Tensor input_image_batch = torch.randn(16, 3, 32, 32).to(ScalarType.Float);
Tensor output_feature_maps = conv_layer.forward(input_image_batch);
System.out.printf("Input shape: %s%n", shapeToString(input_image_batch.sizes()));
System.out.printf("Output shape: %s%n", shapeToString(output_feature_maps.sizes()));
System.out.printf("\nWeight (filter) shape: %s%n", shapeToString(conv_layer.weight().sizes()));
System.out.printf("Bias shape: %s%n", shapeToString(conv_layer.bias().sizes()));
```
`nn.Conv2d` 及其变体(`nn.Conv1d`、`nn.Conv3d`)对涉及空间层次的任务是不可或缺的,主要用于图像和视频分析,但有时也应用于序列数据。我们将在第 7 章更详细地了解如何构建 CNN。
### 循环层 (`nn.RNN`)
循环神经网络(RNN)设计用于处理序列数据,其中元素的顺序很重要。示例包括文本、时间序列数据或音频信号。RNN 层的核心思想是维护一个隐藏状态,该状态捕捉序列中先前元素的信息,并影响当前元素的处理。
PyTorch 中基本的 `nn.RNN` 层逐步处理输入序列。在每一步 t*t*,它接收输入 xt*x**t* 和前一个隐藏状态 ht−1*h**t*−1,以计算输出 ot*o**t*(可选,通常只使用最终隐藏状态)和新的隐藏状态 ht*h**t*。
`nn.RNN` 的主要参数:
- `input_size`:每个时间步输入中的特征数量。
- `hidden_size`:隐藏状态中的特征数量。
- `num_layers` (可选,默认 1):堆叠 RNN 层的数量。
- `batch_first` (可选,默认 False):如果为 True,输入和输出张量将以 `(batch, seq_len, features)` 形式提供,而不是默认的 `(seq_len, batch, features)`。
```scala 3
// 示例:处理一批 10 个序列,每个序列长 20 步,每步有 5 个特征。
// 使用大小为 30 的隐藏状态。
// 设置 batch_first=True 以便更方便地处理数据。
val rnn_layer = nn.RNN(input_size=5, hidden_size=30, batch_first=true)
// 创建一个示例输入张量(批,序列长度,输入特征)
val input_sequence_batch = torch.randn(10, 20, 5)
// 初始化隐藏状态(层数,批,隐藏状态大小)
// 如果未提供,默认为零。
val initial_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}")
println(f"Output sequence shape: {output_sequence.shape}")
println(f"Final hidden state shape: {final_hidden_state.shape}")
```
```java
```
虽然 `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`:
```scala 3
import torch
import torch.nn as nn
val relu_activation = nn.ReLU()
val input_tensor = torch.randn(4)
val output_tensor = relu_activation(input_tensor)
println(f"输入: {input_tensor}")
println(f"ReLU 输出: {output_tensor}")
class SimpleNet extends nn.Module:
def __init__(self):
super().__init__()
val layer1 = nn.Linear(10, 20)
val activation = nn.ReLU()
val layer2 = nn.Linear(20, 5)
def forward(x: torch.Tensor):
x = layer1(x)
x = activation(x) // 应用 ReLU
x = layer2(x)
return x
// 示例模型
val model = SimpleNet()
```
```java
// 08 - SimpleNet
static class SimpleNet extends Module {
SimpleNet() {
layer1 = register_module("layer1", new LinearImpl(10, 20));
activation = register_module("activation", new ReLUImpl());
layer2 = register_module("layer2", new LinearImpl(20, 5));
}
Tensor forward(Tensor input) {
Tensor x = layer1.forward(input);
x = activation.forward(x);
x = layer2.forward(x);
return x;
}
private final LinearImpl layer1;
private final ReLUImpl activation;
private final LinearImpl layer2;
}
SimpleNet model8 = new SimpleNet();
System.out.println("\nSimpleNet 实例: " + model8);
```
> 
>
> ReLU 函数对负输入为零,对正输入为线性。
**优点:**
- **计算效率高:** 计算非常简单(max(0,x)max(0,*x*))。
- **减少梯度消失:** 对于正输入,梯度为1,这有助于在训练期间梯度反向传播,相比于 Sigmoid 或 Tanh 等饱和函数。
- **引入稀疏性:** 由于负输入被映射到零,这可以导致网络中出现稀疏激活,有时可能是有益的。
**缺点:**
- **ReLU 死亡问题:** 输入始终落在负区间的神经元将输出零。因此,流经它们的梯度也将为零,这意味着它们的权重在反向传播期间不会被更新。这些神经元实际上“死亡”了,不再对学习有贡献。Leaky ReLU 或 Parametric ReLU (PReLU) 等变体试图解决此问题。
- **非零中心:** 输出始终为非负值。
```java
```
```java
System.out.println("\n=== ReLU 激活函数示例 ===");
ReLUImpl relu_activation = new ReLUImpl();
Tensor input_tensor = randn(4).to(ScalarType.Float);
Tensor output_tensor = relu_activation.forward(input_tensor);
System.out.printf("输入: %s%n", input_tensor);
System.out.printf("ReLU 输出: %s%n", output_tensor);
```
## Sigmoid
Sigmoid 函数,有时也称为逻辑函数,将其输入压缩到 0 到 1 的范围内。它在历史上很受欢迎,尤其是在二元分类模型的输出层,其中输出代表一个概率。
其数学形式为:
σ(x)=11+e−x*σ*(*x*)=1+*e*−*x*1
在 PyTorch 中,可以使用 `nn.Sigmoid`:
```scala 3
import torch
import torch.nn as nn
val sigmoid_activation = nn.Sigmoid()
val input_tensor = torch.randn(4)
val output_tensor = sigmoid_activation(input_tensor)
println(f"输入: {input_tensor}")
println(f"Sigmoid 输出: {output_tensor}")
```
```java
System.out.println("\n=== Sigmoid 激活函数示例 ===");
SigmoidImpl sigmoid_activation = new SigmoidImpl();
Tensor input_tensor9 = randn(4).to(ScalarType.Float);
Tensor output_tensor9 = sigmoid_activation.forward(input_tensor9);
System.out.printf("输入: %s%n", input_tensor9);
System.out.printf("Sigmoid 输出: %s%n", output_tensor9);
```
>
>
> 
>
> 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*−*x**e**x*−*e*−*x*=2*σ*(2*x*)−1
在 PyTorch 中,可以使用 `nn.Tanh`:
```scala 3
import torch
import torch.nn as nn
val tanh_activation = nn.Tanh()
val input_tensor = torch.randn(4)
val output_tensor = tanh_activation(input_tensor)
println(f"输入: {input_tensor}")
println(f"Tanh 输出: {output_tensor}")
```
```java
System.out.println("=== Tanh 激活函数示例 ===");
TanhImpl tanh_activation = new TanhImpl();
Tensor input_tensor10 = randn(4).to(ScalarType.Float);
Tensor output_tensor10 = tanh_activation.forward(input_tensor10);
System.out.printf("输入: %s%n", input_tensor10);
System.out.printf("Tanh 输出: %s%n", output_tensor10);
```
> 
>
> 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` 方法中被调用的位置。