虽然通过子类化torch.nn.Module定义自定义网络架构提供了最大程度的灵活性,但许多常见模型都涉及层的一个直观序列,其中一层的输出直接作为下一层的输入。对于这些线性堆叠,PyTorch提供了一个方便的容器:torch.nn.Sequential。
nn.Sequential作为一个包装器,接收一个有序的模块序列(如层和激活函数),并在输入数据通过时,以该特定顺序执行它们。可以将其视为为您的数据转换构建一个流程。当您不需要复杂的数据流逻辑、跳跃连接或多输入/多输出路径时,这种方法简化了模型定义。
使用nn.Sequential定义模型
您可以通过将您想要包含的模块作为参数传递给其构造函数来创建Sequential模型。顺序很重要,因为它决定了数据流。
让我们构建一个简单的两层前馈网络,它接收一个784维的输入(例如扁平化的MNIST图像),通过一个包含128个单元和ReLU激活的隐藏层,最后生成一个10维输出(用于10个类别)。
import torch
import torch.nn as nn
import OrderedDict
//定义输入、隐藏层和输出维度
val input_size = 784
val hidden_size = 128
val output_size = 10
// 方法1:直接将模块作为参数传递
val model_v1 = nn.Sequential(
nn.Linear(input_size, hidden_size), // 第1层:线性变换
nn.ReLU(), // 激活函数1:非线性
nn.Linear(hidden_size, output_size) // 第2层:线性变换
)
// 打印模型结构
println("Model V1 (Unnamed Layers):")
println(model_v1)
// 示例用法:创建一个虚拟输入张量
// 假设批量大小为64
val dummy_input = torch.randn(64, input_size)
val output = model_v1(dummy_input)
println("\nOutput shape:", output.shape) // 预期:torch.Size([64, 10])
// 01 - Sequential模型示例(方法1:直接传递模块)
System.out.println("\n=== 01 - Sequential模型示例(方法1)===");
int input_size = 784;
int hidden_size = 128;
int output_size = 10;
// 创建Sequential模型
SequentialImpl model_v1 = new SequentialImpl();
model_v1.push_back(new LinearImpl(input_size, hidden_size));
model_v1.push_back(new ReLUImpl());
model_v1.push_back(new LinearImpl(hidden_size, output_size));
System.out.println("Model V1 (Unnamed Layers):");
System.out.println(model_v1);
// 测试模型
Tensor dummy_input_02 = randn(64, input_size).to(ScalarType.Float);
Tensor output_02 = model_v1.forward(dummy_input_02);
System.out.println("\nOutput shape: " + shapeToString(output_02.sizes()));
// 1. 初始化模型、损失函数和优化器
// 假设 YourModel 是一个继承自 Module 的类
SequentialImpl model = new SequentialImpl();
model.push_back(new LinearImpl(10, 5));
model.push_back( new ReLUImpl());
model.push_back( new LinearImpl(5, 1));
这创建了一个模型,其中输入数据首先经过nn.Linear(784, 128),然后应用nn.ReLU()激活,最后结果通过nn.Linear(128, 10)。请注意,定义是多么紧凑。Sequential容器自动处理将一个模块的输出作为下一个模块的输入传递。
在nn.Sequential中命名层
尽管前一种方法可行,但层只被分配了默认的数字索引(0、1、2等)。这可能会使得后续调试或访问特定层变得更难。为了清晰度和可访问性,更好的做法是使用Python collections模块中的OrderedDict来为您的层提供名称。
// 方法2:使用OrderedDict进行命名层
val model_v2 = nn.Sequential(OrderedDict(
"fc1" -> nn.Linear(input_size, hidden_size), // 全连接层1
"relu1" -> nn.ReLU(), // ReLU激活
"fc2" -> nn.Linear(hidden_size, output_size) // 全连接层2
))
// 打印模型结构
println("\nModel V2 (Named Layers):")
println(model_v2)
// 现在可以通过名称访问特定层
println("\nAccessing fc1 weights shape:", model_v2.fc1.weight.shape)
// 如果需要,也可以使用整数索引访问
println("Accessing layer at index 0:", model_v2[0])
// 或者如果使用OrderedDict,直接通过字符串名称访问
println("Accessing layer by name 'relu1':", model_v2.relu1)
// ========== 1. 定义模型超参数 ==========
long inputSize = 10; // 输入维度
long hiddenSize = 20; // 隐藏层维度
long outputSize = 5; // 输出维度
// ========== 2. 构建OrderedDict(StringAnyModuleDict) ==========
// StringAnyModuleDict 对应 Python 的 OrderedDict,存储<层名称, 层实例>
StringAnyModuleDict orderedDict = new StringAnyModuleDict();
// 2.1 添加全连接层1(fc1)
LinearImpl fc1 = new LinearImpl(inputSize, hiddenSize);
orderedDict.insert("fc1", new AnyModule(fc1));
// 2.2 添加ReLU激活层(relu1)
ReLUImpl relu1 = new ReLUImpl();
orderedDict.insert("relu1", new AnyModule(relu1));
// 2.3 添加全连接层2(fc2)
LinearImpl fc2 = new LinearImpl(hiddenSize, outputSize);
orderedDict.insert("fc2", new AnyModule(fc2));
// ========== 3. 创建命名层Sequential模型(对应model_v2) ==========
// 使用指定的 SequentialImpl 构造方法:接收 StringAnyModuleDict
SequentialImpl modelV2 = new SequentialImpl(orderedDict);
System.out.println("\nAccessing fc1 weights shape:"+modelV2.get(0).asLinear().weight().sizes().vec().toString())
// 如果需要,也可以使用整数索引访问
System.out.println("Accessing layer at index 0:"+modelV2.get(0))
// 或者如果使用OrderedDict,直接通过字符串名称访问
System.out.println("Accessing layer by name 'relu1':" +modelV2.get(1).asReLU())
使用OrderedDict保留了插入顺序(这对nn.Sequential非常重要),同时允许您引用诸如model_v2.fc1或model_v2.relu1之类的层。这显著提高了代码的可读性和可维护性,特别是对于稍长的序列,使得检查模型的特定部分变得更容易。
nn.Sequential 容器输入(批量, 784)fc1: nn.Linear(784, 128)relu1: nn.ReLU()fc2: nn.Linear(128, 10)输出(批量, 10)
数据流经使用命名层通过
nn.Sequential定义的model_v2。输入按线性顺序通过fc1、relu1和fc2。
何时使用nn.Sequential
nn.Sequential特别适合于:
- 简单的全连接网络: 模型中的层线性堆叠,没有分支或跳过,例如基本的多层感知机(MLP)或某些CNN的初始特征提取阶段。
- 定义可复用模块: 创建自包含的层模块(例如,一个包含
Conv2d、BatchNorm2d和ReLU的卷积模块),然后可以将其作为单个模块整合到更大的自定义nn.Module结构中。 - 快速原型开发: 快速组装标准架构以测试想法或建立基线。
局限性
nn.Sequential的主要局限在于其严格的线性性质。它假定一个单一输入和一个单一输出,数据顺序流经所有包含的模块。您不能直接使用它来定义具有更复杂拓扑的模型,例如:
- 跳跃连接: 像ResNet这样的架构,其中较早层的输出被添加到较晚层的输出中,这需要在自定义的
forward方法中显式实现。 - 多输入或多输出: 处理几个不同输入流或生成多个输出张量的模型不能仅凭
nn.Sequential来表示。 - 共享层: 架构中完全相同的层实例在网络拓扑的不同点被应用。
- 条件逻辑: 任何数据流依赖于运行时条件或需要对从一层的输出到下一层输入的数据进行操作的场景。
对于任何表现出这些特点的架构,您必须通过子类化torch.nn.Module并自行实现forward方法来定义一个自定义模型,这将给予您对数据流的完全控制,如前所述在“定义自定义网络架构”一节中讨论的。
总之,nn.Sequential提供了一种清晰高效的方法来定义线性堆叠神经网络层的常见模式。它是一个有价值且方便的工具,适用于更简单的架构和组件模块,补充了自定义nn.Module类这种更灵活的方法。现在您可以使用nn.Module或nn.Sequential定义模型结构了,下一步是定义模型将优化的目标函数,这将引出损失函数。
torch.nn 损失)收藏
神经网络,由堆叠的层和激活函数构建而成,进行预测。为了使这些网络有效学习,必须有一种方法来量化其预测值与真实目标值之间的偏差有多大。这种量化是损失函数的主要作用,损失函数也称为准则函数或目标函数。
torch.nn 包提供了深度学习中常用的一系列标准损失函数。基本思路很简单:损失函数接收模型的输出(预测值)和真实值(目标值)作为输入,并计算出一个标量值,表示“误差”或“损失”。然后,PyTorch 的 Autograd 系统在反向传播过程中使用这个标量损失值来计算梯度,这些梯度进而指导优化器(如 torch.optim 中的 SGD 或 Adam)如何调整模型的参数(权重和偏置)以最小化此损失。
选择合适的损失函数非常重要,因为它直接定义了模型试图达成的目标。让我们看看 torch.nn 中一些最常用的损失函数。
输入数据 (X)模型 (nn.Module)预测值 (ŷ)损失函数 (例如,nn.MSELoss)目标数据 (y)标量损失值用于反向传播
损失函数比较模型预测值与目标数据,以生成一个标量损失值,该值通过反向传播指导参数更新。
常用损失函数
PyTorch 将损失函数实现为继承自 nn.Module 的类。你首先实例化损失函数类,然后使用模型的预测值和目标值调用该实例。
回归损失
这些通常用于目标是预测连续值的情况。
-
均方误差 (MSELoss): 可能是回归任务中最常用的损失函数。它衡量预测值和实际值之间差异的平方的平均值。
公式如下:
损失(y,y^)=1N∑i=1N(yi−y^i)2损失(y,y^)=N1i=1∑N(y**i−y^i)2
其中 NN 是批次中的样本数量,yiy**i 是真实值,y^iy^i 是预测值。对差异进行平方会更严厉地惩罚较大的误差。
使用
torch.nn.MSELoss:import torch import torch.nn as nn // 实例化均方误差损失函数 val loss_fn = nn.MSELoss() // 示例预测值和目标值(批大小为 3,1 个输出特征) val predictions = torch.randn(3, 1, requires_grad=true) val targets = torch.randn(3, 1) // 计算损失 val loss = loss_fn(predictions, targets) println(f"MSE Loss: {loss.item()}") // 现在可以通过 loss.backward() 计算梯度 loss.backward() println(predictions.grad)// 03 - MSELoss示例 System.out.println("\n=== 03 - MSELoss示例 ==="); MSELossImpl loss_fn_03 = new MSELossImpl(); Tensor predictions_03 = torch.randn(new LongArrayRef(new LongPointer(3,1))).requires_grad_(true); Tensor targets_03 = torch.randn(new LongArrayRef(new LongPointer(3,1))); Tensor loss_03 = loss_fn_03.forward(predictions_03, targets_03); System.out.println("MSE Loss: " + loss_03.item_float()); loss_03.backward(); System.out.println("预测梯度: " + predictions_03.grad()); -
平均绝对误差 (L1Loss): 另一种常用的回归损失。它衡量预测值和实际值之间绝对差异的平均值。
公式如下:
损失(y,y^)=1N∑i=1N∣yi−y^i∣损失(y,y^)=N1i=1∑N∣y**i−y^i∣
与 MSE 相比,L1 损失通常被认为对异常值不那么敏感,因为它不对误差进行平方。
使用
torch.nn.L1Loss:import torch import torch.nn as nn val loss_fn_l1 = nn.L1Loss() // 示例预测值和目标值(批大小为 3,1 个输出特征) val predictions = torch.tensor([[1.0], [2.5], [0.0]], requires_grad=true) val targets = torch.tensor([[1.2], [2.2], [0.5]]) // 计算损失 val loss_l1 = loss_fn_l1(predictions, targets) println(f"L1 Loss: {loss_l1.item()}") // |1-1.2|, |2.5-2.2|, |0-0.5| 的平均值 // (0.2 + 0.3 + 0.5) / 3 = 1.0 / 3 = 0.333...// 04 - L1Loss示例 System.out.println("\n=== 04 - L1Loss示例 ==="); L1LossImpl loss_fn_l1 = new L1LossImpl(); var dou = new Double[][]{{1.0}, {2.5}, {0.0}}; var flatArr = TensorToolkit.flatten(dou); var shapeArr = TensorToolkit.getShape(dou); // var reff = new LongArrayRef(new DoublePointer((double[])flatArr),new LongPointer(3,1)) Tensor predictions_04 = torch.tensor((float[])flatArr).reshape(shapeArr).requires_grad_(true); var dou2 = new Double[][]{ {1.2}, {2.2}, {0.5} }; var flatArr2 = (float[])TensorToolkit.flatten(dou2); var shapeArr2 = TensorToolkit.getShape(dou2); Tensor targets_04 = torch.tensor(flatArr2).reshape(shapeArr2); Tensor loss_l1 = loss_fn_l1.forward(predictions_04, targets_04); System.out.println("L1 Loss: " + loss_l1.item_float());
分类损失
这些用于目标是预测离散类别标签的情况。
-
交叉熵损失 (CrossEntropyLoss): 这是多类别分类问题的标准损失函数。当你的模型为每个类别输出原始分数(logits)时,它特别有效。
torch.nn.CrossEntropyLoss方便地将两个步骤合二为一:- 对模型的原始输出分数(logits)应用
LogSoftmax函数。Softmax 将 logits 转换为和为 1 的概率,而 LogSoftmax 则取这些概率的对数。 - 计算 LogSoftmax 输出和目标类别索引之间的负对数似然损失 (
NLLLoss)。
它需要:
- 输入(预测值): 每个类别的原始、未归一化分数(logits)。形状通常为
(N, C),其中N是批大小,C是类别数量。 - 目标: 类别索引(从 0 到 C-1 的整数)。形状通常为
(N)。
import torch import torch.nn as nn // 实例化交叉熵损失函数 val loss_fn_ce = nn.CrossEntropyLoss() // 示例:包含 3 个样本的批次,5 个类别 // 来自模型的原始分数(logits) val predictions_logits = torch.randn(3, 5, requires_grad=true) // 真实类别索引(必须是 LongTensor) val targets_classes = torch.tensor(Seq(1, 0, 4)) // 3 个样本的类别索引 // 计算损失 val loss_ce = loss_fn_ce(predictions_logits, targets_classes) println(f"Cross-Entropy Loss: {loss_ce.item()}") // 现在可以通过 loss_ce.backward() 计算梯度 loss_ce.backward() println(predictions_logits.grad)// 05 - CrossEntropyLoss示例 System.out.println("\n=== 05 - CrossEntropyLoss示例 ==="); CrossEntropyLossImpl loss_fn_ce = new CrossEntropyLossImpl(); Tensor predictions_logits = torch.randn(new LongArrayRef(new LongPointer(3,5))).requires_grad_(true); Tensor targets_classes = torch.tensor(new LongArrayRef(new LongPointer(new long[]{1, 0, 4}),3)).to(ScalarType.Long); Tensor loss_ce = loss_fn_ce.forward(predictions_logits, targets_classes); System.out.println("Cross-Entropy Loss: " + loss_ce.item_float()); loss_ce.backward(); System.out.println("预测梯度: " + predictions_logits.grad());通常推荐使用
nn.CrossEntropyLoss,而不是手动应用LogSoftmax和NLLLoss,因为前者具有更好的数值稳定性。 - 对模型的原始输出分数(logits)应用
-
二元交叉熵损失 (BCELoss 和 BCEWithLogitsLoss): 用于二元(两类别)分类问题或多标签分类(每个样本可以属于多个类别)。
torch.nn.BCELoss: 计算目标与输出之间的二元交叉熵。它期望模型的输出已经是概率(例如,在应用 Sigmoid 激活函数之后),通常在 [0, 1] 范围内。- 输入(预测值):概率,形状
(N, *)。 - 目标:概率(通常为 0.0 或 1.0),与输入形状相同。
- 输入(预测值):概率,形状
torch.nn.BCEWithLogitsLoss: 这个版本在数值上比先使用 Sigmoid 层再使用BCELoss更稳定和方便。它将 Sigmoid 激活和 BCE 计算合为一步。它期望原始 logits 作为输入。- 输入(预测值):原始 logits(Sigmoid 之前),形状
(N, *)。 - 目标:概率(通常为 0.0 或 1.0),与输入形状相同。
- 输入(预测值):原始 logits(Sigmoid 之前),形状
对于大多数二元分类任务,
BCEWithLogitsLoss是更优选择:import torch import torch.nn as nn // 实例化二元交叉熵损失函数 val loss_fn_bce_logits = nn.BCEWithLogitsLoss() // 示例:包含 4 个样本的批次,1 个输出节点(二元分类) val predictions_logits_bin = torch.randn(4, 1, requires_grad=true) // 原始 logits // 目标应为浮点数(0.0 或 1.0) val targets_bin = torch.tensor(Seq(1.0, 0.0, 0.0, 1.0)) // 4 个样本的目标 // 计算损失 val loss_bce = loss_fn_bce_logits(predictions_logits_bin, targets_bin) println(f"BCE With Logits Loss: {loss_bce.item()}")// 06 - BCEWithLogitsLoss示例 System.out.println("\n=== 06 - BCEWithLogitsLoss示例 ==="); BCEWithLogitsLossImpl loss_fn_bce_logits = new BCEWithLogitsLossImpl(); Tensor predictions_logits_bin = torch.randn(new LongArrayRef(new LongPointer(4,1))).requires_grad_(true); // var doubles = {1.0, 0.0, 0.0, 1.0}; // var size = 4L; // var ref = new DoubleArrayRef( new DoublePointer(doubles), size); // 1. 准备数据 double[] data = {1.0, 0.0, 0.0, 1.0}; // 2. 创建持久化的指针对象,防止被 GC 回收 DoublePointer ptr = new DoublePointer(data); // 3. 构造 ArrayRef。注意:有些版本需要传入 ptr 本身,有些需要 ptr.get() // 如果 DoubleArrayRef 构造函数定义正确,这样写通常没问题 var ref = new DoubleArrayRef(ptr, data.length); Tensor targets_bin = torch.tensor(ref).view(4, -1); Tensor loss_bce = loss_fn_bce_logits.forward(predictions_logits_bin, targets_bin); System.out.println("BCE With Logits Loss: " + loss_bce.item_float());
选择合适的损失函数
选择很大程度上取决于你的具体任务:
- 回归(预测连续值): 从
nn.MSELoss开始。如果你怀疑异常值严重影响训练,可以考虑nn.L1Loss。 - 二元分类(两个类别): 使用
nn.BCEWithLogitsLoss。确保你的模型有一个输出节点产生 logits。 - 多类别分类(每个样本只有一个正确类别): 使用
nn.CrossEntropyLoss。确保你的模型有C个输出节点产生 logits,其中C是类别数量。 - 多标签分类(每个样本可以有多个正确类别): 使用
nn.BCEWithLogitsLoss。确保你的模型有C个输出节点产生 logits,并且你的目标是多热编码(例如,如果存在类别 0 和 2,则为[1.0, 0.0, 1.0, 0.0])。
在训练中使用损失函数
在典型的训练循环中,你会在循环外部实例化一次所选择的损失函数。在循环内部,获取模型对一批数据的预测后,将预测值和相应的目标标签传递给损失函数实例,以计算该批次的损失。
// 假设模型、优化器、数据加载器已定义
// --- 训练循环外部 ---
// 示例:多类别分类
val num_classes = 10
val model = nn.Linear(784, num_classes) // 简单的线性模型示例
val optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
val loss_fn = nn.CrossEntropyLoss()
// 模拟数据加载器(替换为实际的 DataLoader)
val dummy_dataloader = Seq.fill(5)(torch.randn(64, 784), torch.randint(0, num_classes, (64,)))
// --- 训练循环内部 ---
model.train() // 将模型设置为训练模式
for (batch_idx, (data, target)) <- dummy_dataloader.zipWithIndex:
// 1. 清零梯度
optimizer.zero_grad()
// 2. 前向传播:获取预测值(logits)
val predictions = model(data)
// 3. 计算损失
val loss = loss_fn(predictions, target)
// 4. 反向传播:计算梯度
loss.backward()
// 5. 优化器步进:更新权重
optimizer.step()
if batch_idx % 2 == 0 then // 定期打印损失
println(f"Batch {batch_idx}, Loss: {loss.item():.4f}")
import org.bytedeco.pytorch.*;
import static org.bytedeco.pytorch.global.torch.*;
import java.util.List;
import java.util.stream.IntStream;
// 定义一个 Record 用于在循环中进行模式匹配解构
record Batch(int index, Tensor data, Tensor target) {}
System.out.println("\n=== 07 - 训练循环示例 ===");
int num_classes = 10;
LinearImpl model = new LinearImpl(784, num_classes);
SGD optimizer = new SGD(model.parameters(), new SGDOptions(0.01));
CrossEntropyLossImpl loss_fn = new CrossEntropyLossImpl();
model.train(true);
// 1. 创建模拟 DataLoader (使用 List 替换 Seq)
var dummyDataloader = IntStream.range(0, 5)
.mapToObj(i -> new Batch(
i,
randn(new long[]{64, 784}, float32()),
randint(0, numClasses, new long[]{64}, int64())
))
.toList();
// 使用 Java 21+ 的增强型 for 循环与 Record 模式匹配进行解构
for (Batch(var batchIdx, var data, var target) : dummyDataloader) {
System.out.println("Batch " + batchIdx + ", data shape: " + shapeToString(data.sizes()));
System.out.println("Batch " + batchIdx + ", target shape: " + shapeToString(target.sizes()));
// 训练步骤
optimizer.zero_grad();
Tensor predictions = model.forward(data.to(ScalarType.Float));
Tensor batch_loss = loss_fn.forward(predictions, target.to(ScalarType.Long));
batch_loss.backward();
optimizer.step();
if (batch_idx % 2 == 0) {
System.out.println("Batch " + batchIdx + ", Loss: " + batch_loss.item_float());
}
}
通过从 torch.nn 中选择合适的损失函数并将其正确集成到你的训练过程中,你为模型提供了一个明确的学习目标,与优化器和反向传播一起构成了模型训练机制的根本部分。
torch.optim)收藏
优化器在神经网络训练中扮演着重要角色。在定义神经网络架构(例如使用 torch.nn.Module)并选定一个合适的损失函数来衡量模型预测与实际目标之间的差异后,优化器的作用是更新模型的参数(如权重和偏置),以最小化计算出的损失。torch.optim 包为深度学习中常用的多种优化算法提供了实现。
回顾 Autograd 一章,调用 loss.backward() 会计算损失相对于所有 requires_grad=True 的模型参数的梯度。这些梯度指示了每个参数为减少损失所需的改变方向和大小。然而,仅仅计算梯度是不够的;我们需要一种机制来应用这些更新。优化器提供了这种机制。
优化过程
核心来说,训练神经网络是一个优化问题。我们希望找到一组参数(权重 ww 和偏置 bb)来最小化损失函数 LL。梯度下降是实现这一目的的基础算法。基本思路是迭代地沿着梯度的反方向调整参数:
θnew=θold−η∇θLθne**w=θol**d−η∇θ**L
这里,θθ 代表一个参数(如权重或偏置),∇θL∇θ**L 是损失 LL 对 θθ 的梯度,而 ηη (eta) 是学习率,一个控制步长的超参数。
PyTorch 的 torch.optim 包实现了这一核心思想,以及一些旨在提高收敛速度和稳定性的更精巧变体。
使用 torch.optim
要在 PyTorch 中使用优化器,首先需要导入该包:
import torch.optim as optim
接下来,实例化一个优化器对象。创建时,你必须告诉优化器它应该管理哪些参数。通常,你会使用 model.parameters() 方法传入模型的参数。你还需要指定学习率 (lr) 以及其他可能与算法相关的超参数。
// 假设 'model' 是你的 nn.Module 子类的一个实例
// 示例:使用随机梯度下降 (SGD)
val optimizer = optim.SGD(model.parameters(), lr=0.01)
// 示例:使用 Adam 优化器
val optimizer = optim.Adam(model.parameters(), lr=0.001)
// ========== 方法1:创建SGD优化器(对应 lr=0.01) ==========
// 步骤1:构造SGD优化器配置(设置学习率)
SGDOptions sgdOptions = new SGDOptions(0.01);
sgdOptions.lr().put(0.01); // 设置学习率 lr=0.01
// 步骤2:创建SGD优化器(传入模型参数 + 配置)
SGD optimizerSGD =new SGD(model.parameters(), sgdOptions);
System.out.println("SGD优化器创建完成,学习率:" + sgdOptions.get_lr());
// ========== 方法2:创建Adam优化器(对应 lr=0.001) ==========
// 步骤1:构造Adam优化器配置(设置学习率)
AdamOptions adamOptions = new AdamOptions();
adamOptions.lr().put(0.001); // 设置学习率 lr=0.001
// 步骤2:创建Adam优化器(传入模型参数 + 配置)
Adam optimizerAdam = new Adam(model.parameters(), adamOptions);
System.out.println("Adam优化器创建完成,学习率:" + adamOptions.get_lr());
model.parameters() 调用会返回一个迭代器,遍历你的模型中所有可学习的参数。优化器持有对这些张量的引用,并知道如何根据它们的 .grad 属性(该属性在 loss.backward() 调用期间填充)来更新它们。
常用优化器
尽管 torch.optim 提供了许多算法,但随机梯度下降 (SGD) 和 Adam 是两个最常用的起始点。
随机梯度下降 (SGD)
SGD 是一种经典的优化算法。在其 PyTorch 实现中,它可以在小批量数据(这是标准做法)而非单个样本上运行。它根据当前小批量数据计算出的梯度更新参数。
optim.SGD 优化器有几个重要参数:
params: 要优化的参数的可迭代对象(例如,model.parameters())。lr: 学习率 (ηη)。这是一个关键的超参数。选择过小的值可能导致收敛缓慢,而过大的值可能导致不稳定或发散。momentum: 一种有助于加速 SGD 朝相关方向前进并抑制振荡的方法。它将之前更新向量的一部分添加到当前更新向量中。典型值为 0.9。weight_decay: 在更新步骤中隐式地向损失函数添加 L2 正则化(对大权重的惩罚)。这有助于防止过拟合。
// 带有动量和权重衰减的 SGD
val optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-4)
Adam (自适应矩估计)
Adam 是一种自适应学习率优化算法,这意味着它为不同参数计算各自的学习率。它结合了 RMSprop(根据最近梯度平方的平均值调整学习率)和动量的思想。Adam 通常比 SGD 收敛更快,且相对有效,常常在默认设置下表现良好。
optim.Adam 的重要参数:
params: 要优化的参数。lr: 初始学习率(Adam 在内部进行调整)。常见的起始值是1e-3或0.001。betas: 一个元组(beta1, beta2),控制动量估计的指数衰减率(通常是(0.9, 0.999))。eps: 添加到分母中的一个小项,用于数值稳定性(通常是1e-8)。weight_decay: 添加 L2 正则化。
// 带有默认 betas 和指定学习率的 Adam
val optimizer = optim.Adam(model.parameters(), lr=0.001)
// 带有自定义 betas 和权重衰减的 Adam
val optimizer = optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.999), weight_decay=1e-5)
其他常用优化器,如 RMSprop、Adagrad 和 AdamW(改进了权重衰减处理的 Adam)也在 torch.optim 中提供。选择通常取决于具体问题和实际表现。
将优化器整合到训练循环中
在训练循环中使用优化器涉及每次迭代的两个主要步骤,通常在计算损失和梯度之后执行:
optimizer.zero_grad(): 在计算当前小批量数据的梯度(通过loss.backward())之前,必须清除之前迭代累积的梯度。PyTorch 默认在每次调用backward()时累积梯度。如果你忘记将其归零,来自多个批次的梯度将会混合,导致不正确的更新。这通常在循环开始时或在调用backward()之前执行。optimizer.step(): 在用loss.backward()计算梯度后,调用optimizer.step()会更新所有已在优化器中注册的参数。它会使用计算出的梯度(存储在parameter.grad中)和学习率来应用特定的优化算法(如 SGD 或 Adam)。
以下是整合了优化器的训练迭代的简化结构:
// 假设模型、准则(损失函数)和优化器已定义
// 假设 data_loader 提供批量的输入和目标
// 将模型设置为训练模式
model.train() // 将模型设置为训练模式
for inputs, targets <- data_loader:
// 1. 梯度归零
optimizer.zero_grad()
// 2. 前向传播:计算模型预测
val outputs = model(inputs)
// 3. 计算损失
val loss = criterion(outputs, targets)
// 4. 反向传播:计算梯度
loss.backward()
// 5. 更新权重
optimizer.step()
// (可选:日志记录、指标计算等)
下图展示了优化器在标准训练周期中的作用:
训练迭代optimizer.zero_grad()前向传播(模型输出)下一次迭代计算损失(准则)下一次迭代模型参数(权重, 偏置)loss.backward()(计算梯度)下一次迭代optimizer.step()(更新权重)下一次迭代参数梯度(.grad 属性)填充下一次迭代修改优化器(例如, Adam, SGD)初始化时传入读取清除 .grad
优化器使用
loss.backward()计算出的梯度,通过optimizer.step()更新模型参数,在此之前确保使用optimizer.zero_grad()清除了之前的梯度。
调整学习率
有时,在训练期间调整学习率是有益的。例如,你可能希望以较大的学习率开始以加快初始进展,之后再降低学习率以更精细地调整参数。PyTorch 在 torch.optim.lr_scheduler 中为此提供了学习率调度器。这些调度器根据预设规则(例如,每隔几个 epoch 减少一次,或者当验证性能稳定时)调整与优化器相关的学习率。尽管它们功能强大,但调度器的详细用法通常在更高级的背景下介绍。
总结来说,torch.optim 是 PyTorch 中训练神经网络不可或缺的工具。通过选择合适的优化器、配置其学习率等超参数,并将 optimizer.zero_grad() 和 optimizer.step() 正确整合到训练循环中,你便为模型提供了从数据中学习并最小化损失的机制。尝试不同的优化器和学习率是开发有效深度学习模型的常规部分。
收藏
让我们将本章的理念付诸实践,通过搭建一个简单的神经网络。我们将构建一个小型的前馈网络,专为二分类任务而设计。假设我们有包含两个特征的输入数据,并想将每个数据点归入两个类别(0或1)中的一个。
任何PyTorch模型的基础都是torch.nn.Module类。通过继承nn.Module可以创建自定义网络,并在__init__方法中定义层,在forward方法中定义数据流。
定义网络架构
我们将创建一个具有以下特点的网络:
- 一个接受2个特征的输入层。
- 一个包含10个神经元和ReLU激活函数的隐藏层。
- 一个包含1个神经元的输出层,生成适合二分类的单个logit值。
以下是定义此架构的Python代码:
import torch
import torch.nn as nn
import torch.optim as optim
# 定义网络结构
class SimpleNet extends nn.Module:
def __init__(input_size: Int, hidden_size: Int, output_size: Int):
super().__init__() # 初始化父类
val layer_1 = nn.Linear(input_size, hidden_size)
val relu = nn.ReLU()
val layer_2 = nn.Linear(hidden_size, output_size)
def forward(self, x: Tensor[D]):
// 定义前向传播
val x = layer_1(x)
val x = relu(x)
val x = layer_2(x)
// 注意:如果后续使用BCEWithLogitsLoss,这里不应用Sigmoid
return x
// 定义网络参数
val input_features = 2
val hidden_units = 10
val output_classes = 1 // 二分类logit的单个输出
// 实例化网络
val model = SimpleNet(input_features, hidden_units, output_classes)
// 打印模型结构
println(model)
static class SimpleNet extends Module {
SimpleNet(int input_size, int hidden_size, int output_size) {
// 定义网络层
layer_1 = register_module("layer_1", new LinearImpl(input_size, hidden_size));
relu = register_module("relu", new ReLUImpl());
layer_2 = register_module("layer_2", new LinearImpl(hidden_size, output_size));
}
Tensor forward(Tensor input) {
// 定义前向传播
Tensor x = layer_1.forward(input);
x = relu.forward(x);
x = layer_2.forward(x);
// 注意:如果后续使用BCEWithLogitsLoss,这里不应用Sigmoid
return x;
}
private final LinearImpl layer_1;
private final ReLUImpl relu;
private final LinearImpl layer_2;
}
运行此代码将打印我们新定义网络的结构,显示层及其顺序:
SimpleNet(
(layer_1): Linear(in_features=2, out_features=10, bias=True)
(relu): ReLU()
(layer_2): Linear(in_features=10, out_features=1, bias=True)
)
此输出证实我们有一个线性层,将2个输入特征映射到10个隐藏单元,接着是ReLU激活,最后是另一个线性层,将10个隐藏单元映射到单个输出值。
准备训练组件
在我们开始训练之前(训练的详细内容将在后面介绍),我们需要实例化模型,定义损失函数,并选择一个优化器。让我们来设置这些。
// --- 数据准备(示例占位符)---
// 假设我们有一些输入数据(X)和目标标签(y)
// 对于此示例,我们创建一些虚拟张量
// 包含5个样本的迷你批次,每个样本有2个特征
val dummy_input = torch.randn(5, input_features)
// 对应的虚拟标签(0或1)- BCEWithLogitsLoss需要浮点类型
val dummy_labels = torch.randint(0, 2, (5, 1)).float()
// --- 实例化模型、损失和优化器 ---
// 模型已在上面实例化:model = SimpleNet(...)
// 损失函数:带Logits的二元交叉熵
// 此损失函数适用于二分类,并期望接收原始logits作为输入
val criterion = nn.BCEWithLogitsLoss()
// 优化器:Adam是一个常用的选择
// 我们将模型的参数传递给优化器
val learning_rate = 0.01
val optimizer = optim.Adam(model.parameters(), lr=learning_rate)
println(s"\n使用的损失函数: $criterion")
println(s"使用的优化器: $optimizer")
System.out.println("=== 08 - 网络定义和实例化 ===");
int input_features = 2;
int hidden_units = 10;
int output_classes = 1; // 二分类logit的单个输出
// 实例化网络
SimpleNet model_08 = new SimpleNet(input_features, hidden_units, output_classes);
// 打印模型结构
System.out.println("模型结构: " + model_08);
// 09 - 数据准备、损失和优化器
System.out.println("\n=== 09 - 数据准备、损失和优化器 ===");
// 创建虚拟输入数据和标签
Tensor dummy_input = torch.randn(5, input_features).to(ScalarType.Float);
Tensor dummy_labels = torch.randint(0, 2, new LongArrayRef(new LongPointer(5, 1))).to(ScalarType.Float);
// 损失函数:带Logits的二元交叉熵
BCEWithLogitsLossImpl criterion = new BCEWithLogitsLossImpl();
// 优化器:Adam
double learning_rate = 0.01;
Adam optimizer_09 = new Adam(model_08.parameters(), new AdamOptions(learning_rate));
System.out.println("使用的损失函数: " + criterion);
System.out.println("使用的优化器: " + optimizer_09);
模拟前向和反向传播
现在我们已经准备好模型、损失函数和优化器,让我们模拟训练过程中的单个步骤,来查看这些组件如何协同工作。这包括:
- 将输入数据通过模型(前向传播)。
- 计算模型输出与真实标签之间的损失。
- 使用反向传播计算梯度。
- 使用优化器更新模型的权重。
// --- 模拟单个训练步骤 ---
// 1. 前向传播:获取模型预测(logits)
val outputs = model(dummy_input)
println(s"\n模型输出(logits)形状:${outputs.shape}")
// println(f"Sample outputs: {outputs.detach().numpy().flatten()}") // 可选:查看输出
// 2. 计算损失
val loss = criterion(outputs, dummy_labels)
println(s"计算的损失:${loss.item():.4f}") // .item()获取标量值
// 3. 反向传播:计算梯度
// 首先,确保梯度已从上一步归零(在实际循环中很重要)
optimizer.zero_grad()
loss.backward() // 计算损失相对于模型参数的梯度
// 4. 优化器步骤:更新模型权重
optimizer.step() // 根据计算出的梯度更新参数
// --- 检查参数(可选)---
// 您可以在反向传播后(在optimizer.step()之前)检查梯度
println("\nlayer_1权重的梯度(示例):")
println(model.layer_1.weight.grad[0, :]) // 访问特定参数的梯度
// 或者在步骤后检查参数值
println("\n更新后的layer_1权重(示例):")
println(model.layer_1.weight[0, :])
// 10 - 模拟单个训练步骤
System.out.println("\n=== 10 - 模拟单个训练步骤 ===");
// 1. 前向传播
Tensor outputs = model_08.forward(dummy_input);
System.out.println("模型输出(logits)形状:" + shapeToString(outputs.sizes()));
// 2. 计算损失
Tensor loss = criterion.forward(outputs, dummy_labels);
System.out.println("计算的损失:" + loss.item_float());
// 3. 反向传播
optimizer_09.zero_grad();
loss.backward();
// 4. 优化器步骤
optimizer_09.step();
// --- 检查参数(示例)---
System.out.println("%nlayer_1权重的梯度(示例):");
// Java 中访问 Tensor 索引建议使用 .index() 或 .narrow()
// 这里使用切片访问:第0行,所有列
try (Tensor grad = model_08.layer_1.weight().grad().index(new TensorIndexVector(0))) {
System.out.println(grad);
}
System.out.println("%n更新后的layer_1权重(示例):");
try (Tensor weight = model_08.layer_1.weight().index(new TensorIndexVector(0))) {
System.out.println(weight);
}
在此步骤中,我们执行了前向传播,从我们的SimpleNet获取原始输出(logits)。然后我们使用BCEWithLogitsLoss计算这些输出和我们的dummy_labels之间的差异。调用loss.backward()触发了Autograd计算所有requires_grad=True参数的梯度(默认包括我们nn.Linear层的权重和偏置)。最后,optimizer.step()使用计算出的梯度和Adam优化算法更新了模型的参数。请记住在实际训练循环中的下一次反向传播之前调用optimizer.zero_grad(),以防止梯度累积。
您现在已成功使用torch.nn构建了一个简单的神经网络,定义了其层和前向传播,实例化了它,并将其与损失函数和优化器连接起来,为下一阶段:训练做好了准备。
