【Java深度学习】PyTorch On Java 系列课程 第四章 08 :神经网络【AI Infra 3.0】[PyTorch Java 硕士研一课程]

0 阅读26分钟

SCR-20260220-ufky.png

# 章节 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.*;

    // 01 - MySimpleNetwork
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;
    }
        // 01 - MySimpleNetwork 示例
        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;
    }
    
            // 02 - CustomModuleWithParameter 示例
        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;
    }
    
          // 03 - SimpleLinearModel 示例
        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 //通常用于函数式 API,如激活函数

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 (线性)输出

![image-20251014145824566](C:\Users\hai71\AppData\Roaming\Typora\typora-user-images\image-20251014145824566.png)

> 数据流经 `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

// 示例:创建一个线性层,输入特征大小为 20,输出特征大小为 30
val linear_layer = nn.Linear(in_features=20, out_features=30)

// 创建一个示例输入张量(批大小 64,20 个特征)
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}")
// 预期输出:
// 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])
```

```java
        // 05 - Linear 线性层示例
        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
// 示例:处理一批 16 张图像,3 通道(RGB),32x32 像素
// 应用 6 个滤波器(输出通道),每个大小为 5x5
val conv_layer = nn.Conv2d(in_channels=3, out_channels=6, kernel_size=5)  

// 创建一个示例输入张量(批大小,通道,高,宽)
// PyTorch 通常期望通道优先的格式 (N, C, H, W)
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}")
// 没有填充/步幅时,输出大小会减小: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])
```

```java
        // 06 - Conv2d 卷积层示例
        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}") // (层数,批,隐藏状态大小)
// 预期输出:
// 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])
```

```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 实例
        SimpleNet model8 = new SimpleNet();
        System.out.println("\nSimpleNet 实例: " + model8);



```
> ![image-20251014150031495](C:\Users\hai71\AppData\Roaming\Typora\typora-user-images\image-20251014150031495.png)
>
> ReLU 函数对负输入为零,对正输入为线性。

**优点:**

- **计算效率高:** 计算非常简单(max⁡(0,x)max(0,*x*))。
- **减少梯度消失:** 对于正输入,梯度为1,这有助于在训练期间梯度反向传播,相比于 Sigmoid 或 Tanh 等饱和函数。
- **引入稀疏性:** 由于负输入被映射到零,这可以导致网络中出现稀疏激活,有时可能是有益的。

**缺点:**

- **ReLU 死亡问题:** 输入始终落在负区间的神经元将输出零。因此,流经它们的梯度也将为零,这意味着它们的权重在反向传播期间不会被更新。这些神经元实际上“死亡”了,不再对学习有贡献。Leaky ReLU 或 Parametric ReLU (PReLU) 等变体试图解决此问题。
- **非零中心:** 输出始终为非负值。

```java


```

```java
        // 08 - ReLU 激活函数示例
        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 函数,有时也称为逻辑函数,将其输入压缩到 01 的范围内。它在历史上很受欢迎,尤其是在二元分类模型的输出层,其中输出代表一个概率。

其数学形式为:

σ(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
  // 09 - Sigmoid 激活函数示例
        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);

```

> 
>
> ![image-20251014150104727](C:\Users\hai71\AppData\Roaming\Typora\typora-user-images\image-20251014150104727.png)
>
> Sigmoid 函数将任意实数平滑地映射到 (0, 1) 的范围内。

**优点:**

- **输出易于理解:** (0, 1) 的范围便于表示概率。
- **梯度平滑:** 函数处处可微,提供平滑的梯度。

**缺点:**

- **梯度消失:** 对于非常大或非常小的输入,函数会饱和(输出接近 10),梯度变得非常接近零。这会严重减缓或停止深度网络的学习,因为梯度难以通过多层反向传播。
- **非零中心:** 输出始终为正,这有时会减缓收敛速度,相比于零中心激活函数。
- **计算成本更高:** 指数函数比 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
       // 10 - Tanh 激活函数示例
        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);


```

> ![image-20251014150132782](C:\Users\hai71\AppData\Roaming\Typora\typora-user-images\image-20251014150132782.png)
>
> 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` 方法中被调用的位置。