Skip to main content

Pytorch

PyTorch 张量 (Tensors)

如果你已经熟悉 NumPy 的 ndarray,那么理解 PyTorch 的张量会非常容易。PyTorch 张量与 NumPy 数组非常相似,但有一个关键的区别:张量可以在 GPU 上进行运算,从而极大地加速计算,这对于训练大模型至关重要。

  • 什么是张量?

    • 多维数组。
    • 0维张量:标量 (一个数字)
    • 1维张量:向量 (一列数字)
    • 2维张量:矩阵
    • 更高维度的张量... (例如,在 LLM 中,一个批次的词嵌入数据通常是 3D 张量:[batch_size, sequence_length, embedding_dimension])
  • 创建张量:

    import torch
    import numpy as np
    
    # 从 Python 列表创建
    data_list = [[1, 2], [3, 4]]
    tensor_from_list = torch.tensor(data_list)
    print("从列表创建的张量:\n", tensor_from_list)
    print("张量的数据类型:", tensor_from_list.dtype) # 默认为 torch.int64
    
    # 从 NumPy 数组创建 (共享内存,除非显式拷贝)
    numpy_array = np.array([[5, 6], [7, 8]])
    tensor_from_numpy = torch.from_numpy(numpy_array) # 或者 torch.as_tensor(numpy_array)
    print("\n从NumPy数组创建的张量:\n", tensor_from_numpy)
    
    # NumPy 数组和 PyTorch 张量之间的转换
    numpy_back = tensor_from_numpy.numpy()
    print("\n转换回NumPy数组:\n", numpy_back)
    
    # 创建特定形状和类型的张量
    zeros_tensor = torch.zeros(2, 3) # 2x3 的全零张量
    ones_tensor = torch.ones(2, 3, dtype=torch.float32) # 指定数据类型
    rand_tensor = torch.rand(2, 3)  # [0, 1) 均匀分布
    randn_tensor = torch.randn(2, 3) # 标准正态分布
    
    print("\n全零张量:\n", zeros_tensor)
    print("全一张量 (float32):\n", ones_tensor)
    print("随机张量 (均匀分布):\n", rand_tensor)
    print("随机张量 (正态分布):\n", randn_tensor)
    
    # 指定数据创建张量,并让PyTorch推断大小
    x_data = torch.tensor([[1., -1.], [1., -1.]]) # 注意使用浮点数
    x_ones = torch.ones_like(x_data) # 创建形状和类型与x_data相同的全一张量
    x_rand = torch.rand_like(x_data, dtype=torch.float) # 覆盖数据类型
    print("\nx_rand:\n", x_rand)
    
  • 张量属性: 与 NumPy 类似

    • tensor.shapetensor.size(): 张量的形状。
    • tensor.dtype: 张量的数据类型 (如 torch.float32, torch.int64)。
    • tensor.device: 张量所在的设备 (CPU 或 GPU)。
    print(f"\n形状: {rand_tensor.shape}")
    print(f"数据类型: {rand_tensor.dtype}")
    print(f"设备: {rand_tensor.device}") # 默认在 CPU 上
    
  • 张量操作: 许多操作与 NumPy 类似。

    • 算术运算: +, -, *, /, @ (矩阵乘法) 或 torch.matmul()
    • 索引和切片: 与 NumPy 类似。
    • 变形: tensor.view(), tensor.reshape(), tensor.transpose(), tensor.permute()
      • view(): 返回一个新的张量,与原始张量共享数据。只能用于连续内存的张量。
      • reshape(): 功能更强大,不总是共享数据,可以在需要时创建副本。
    • 聚合操作: torch.sum(), torch.mean(), torch.max(), torch.argmax()
    a = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32)
    b = torch.tensor([[5, 6], [7, 8]], dtype=torch.float32)
    
    # 加法
    print("\na + b:\n", a + b)
    print("torch.add(a, b):\n", torch.add(a, b))
    
    # 矩阵乘法
    print("\na @ b (矩阵乘法):\n", a @ b)
    print("torch.matmul(a, b):\n", torch.matmul(a, b))
    
    # 索引
    print("\na的第一行:", a[0])
    print("a的[0,1]元素:", a[0, 1])
    
    # view (类似 reshape, 但共享数据)
    x = torch.randn(4, 4)
    y = x.view(16)
    z = x.view(-1, 8)  # -1 表示由其他维度推断
    print("\nx的形状:", x.shape)
    print("y的形状 (view成16个元素):", y.shape)
    print("z的形状 (view成nx8):", z.shape)
    

GPU 加速

这是 PyTorch 相对于 NumPy 的一个核心优势,对于大模型训练至关重要。

  • 检查 GPU 是否可用:

    if torch.cuda.is_available():
        device = torch.device("cuda")          # 使用第一个可用的 CUDA GPU
        print(f"\nGPU '{torch.cuda.get_device_name(0)}' 可用,将使用GPU。")
    else:
        device = torch.device("cpu")
        print("\nGPU 不可用,将使用CPU。")
    
  • 将张量移动到 GPU:

    # 创建一个张量 (默认在CPU)
    cpu_tensor = torch.randn(3, 3)
    print("CPU 张量设备:", cpu_tensor.device)
    
    # 将张量移动到之前确定的设备 (GPU或CPU)
    if torch.cuda.is_available():
        gpu_tensor = cpu_tensor.to(device)
        print("GPU 张量设备:", gpu_tensor.device)
    
        # 也可以直接在GPU上创建张量
        gpu_direct_tensor = torch.randn(2, 2, device=device)
        print("直接在GPU上创建的张量设备:", gpu_direct_tensor.device)
    
        # 注意:不同设备上的张量不能直接运算,需要先移动到同一设备
        # cpu_tensor + gpu_tensor # 这会报错
        result_on_gpu = gpu_tensor + gpu_direct_tensor
        print("GPU上运算结果设备:", result_on_gpu.device)
    
        # 将结果移回CPU (例如用于打印或与NumPy交互)
        result_on_cpu = result_on_gpu.cpu()
        print("移回CPU的结果设备:", result_on_cpu.device)
    
  • .to(device) 方法 是将张量或模型移动到特定设备(CPU 或 GPU)的通用方法。

自动求导 (torch.autograd)

这是 PyTorch 另一个核心特性,也是训练神经网络(包括 LLM)的基石。它能够自动计算梯度(导数)。

  • requires_grad=True: 当创建一个张量时,如果设置 requires_grad=True,PyTorch 会开始追踪在该张量上的所有操作,以便进行梯度计算。
  • 计算图 (Computational Graph): PyTorch 会构建一个动态计算图,记录数据如何通过操作组合得到最终结果。
  • .backward(): 当你在一个标量输出上调用 .backward() 时,PyTorch 会自动计算计算图中所有 requires_grad=True 的张量相对于该标量输出的梯度。
  • .grad: 梯度会累积到张量的 .grad 属性中。
# 创建需要梯度的张量
x = torch.ones(2, 2, requires_grad=True)
print("\nx:\n", x)

# 对 x 进行一些操作
y = x + 2
print("y:\n", y) # y 也会自动具有 grad_fn,因为它是由 x 计算得来的

z = y * y * 3
out = z.mean() # out 是一个标量
print("z:\n", z)
print("out (标量):\n", out)

# 计算梯度
out.backward() # 等价于 out.backward(torch.tensor(1.))

# 打印 x 的梯度 d(out)/dx
print("\nx的梯度 (d(out)/dx):\n", x.grad)

解释 x.grad 的计算过程 (链式法则): out = (1/4) * Σ z_i z_i = 3 * y_i^2 y_i = x_i + 2

∂out/∂z_i = 1/4 ∂z_i/∂y_i = 6 * y_i ∂y_i/∂x_i = 1

所以,∂out/∂x_i = (∂out/∂z_i) * (∂z_i/∂y_i) * (∂y_i/∂x_i) = (1/4) * (6 * y_i) * 1 = (3/2) * y_i 因为 x = [[1,1],[1,1]], 所以 y = [[3,3],[3,3]] 因此,x.grad 应该是 (3/2) * [[3,3],[3,3]] = [[4.5, 4.5],[4.5, 4.5]],这与程序输出一致。

LLM 相关性:

  • 大模型的参数(权重和偏置)都是 requires_grad=True 的张量。
  • 损失函数 (Loss Function) 的输出是一个标量。
  • 调用 loss.backward() 会计算损失相对于模型所有参数的梯度。
  • 优化器 (Optimizer) 使用这些梯度来更新模型参数,从而使损失减小。

重要注意事项:

  • 只有浮点类型的张量 (如 torch.float32, torch.float64) 才能计算梯度。
  • 默认情况下,新创建的张量 requires_grad=False
  • 梯度是累积的!在每次迭代(例如,在训练循环中)计算梯度之前,通常需要使用 optimizer.zero_grad() 或手动将 .grad 设置为 None 来清除旧的梯度。
    # 梯度累积示例
    # 第一次 backward
    # out.backward()
    # print(x.grad)
    
    # 如果再次 backward 而不清除梯度
    # out.backward() # 这会报错,因为图已经被释放了,除非指定 retain_graph=True
                     # 或者梯度会累加
    
    # 正确的做法是在新一轮计算前清除梯度
    # x.grad.data.zero_() # 一种清除梯度的方式
    
  • 可以用 with torch.no_grad(): 上下文管理器来临时禁用梯度计算,这在模型评估(推理)阶段非常有用,可以减少内存消耗并加速计算。
    print("\n原始x的requires_grad:", x.requires_grad)
    print("在torch.no_grad()上下文中:")
    with torch.no_grad():
        y_no_grad = x + 2
        print("y_no_grad的requires_grad:", y_no_grad.requires_grad) # False
    

总结一下,主要包括:

  1. 张量的创建:

    • 从 Python 列表或 NumPy 数组创建 (torch.tensor(), torch.from_numpy())。
    • 创建特定形状和值的张量 (torch.zeros(), torch.ones(), torch.rand(), torch.randn(), torch.arange(), torch.linspace())。
    • 使用 _like 版本根据现有张量创建 (torch.zeros_like())。
  2. 张量的属性:

    • 形状 (.shape, .size())。
    • 数据类型 (.dtype)。
    • 所在设备 (.device)。
    • 是否需要梯度 (.requires_grad)。
    • 梯度值 (.grad)。
    • 梯度函数 (.grad_fn),用于追踪操作历史以进行反向传播。
  3. 张量的基本运算:

    • 算术运算: 加、减、乘、除、矩阵乘法 (@torch.matmul())。
    • 元素级运算: torch.abs(), torch.sqrt(), torch.exp(), torch.log(), torch.sin(), torch.sigmoid(), torch.relu() 等。
    • 比较运算: torch.eq(), torch.gt(), torch.lt() 等,返回布尔张量。
  4. 索引、切片和连接:

    • 索引和切片: 与 NumPy 非常相似,用于访问和修改张量的部分元素。
    • 连接: torch.cat() (沿指定维度拼接张量), torch.stack() (沿新维度堆叠张量)。
    • 拆分: torch.split(), torch.chunk()
  5. 形状变换:

    • tensor.view(): 改变张量形状(共享数据,要求内存连续)。
    • tensor.reshape(): 改变张量形状(不一定共享数据,更灵活)。
    • tensor.Ttensor.transpose(): 转置。
    • tensor.permute(): 任意维度重排。
    • tensor.unsqueeze(): 增加维度。
    • tensor.squeeze(): 移除大小为1的维度。
  6. 聚合操作:

    • torch.sum(), torch.mean(), torch.prod(), torch.std(), torch.var()
    • torch.min(), torch.max() (返回数值)。
    • torch.argmin(), torch.argmax() (返回索引)。
    • 可以指定 dim 参数沿特定维度进行聚合。
  7. GPU 加速:

    • 检查 GPU 可用性 (torch.cuda.is_available())。
    • 将张量移至设备 (tensor.to(device))。
    • 直接在特定设备创建张量 (torch.tensor(..., device=device))。
  8. 自动求导 (torch.autograd):

    • 设置 requires_grad=True 来追踪操作。
    • 调用 .backward() 计算梯度。
    • 通过 .grad 属性访问梯度。
    • 使用 with torch.no_grad(): 禁用梯度计算。
    • 梯度累积和清除 (optimizer.zero_grad()tensor.grad.zero_())。

这些操作构成了 PyTorch 张量操作的基石。当你开始构建神经网络层、定义损失函数和编写训练循环时,你会不断地用到这些基础操作。

当然,PyTorch 作为一个庞大的库,还有更多高级或特定场景下的张量操作,但对于入门和理解核心原理,以上这些是最重要的。 随着你学习的深入,比如接触到更复杂的网络结构(如 RNN、Transformer 中的特定操作)或者分布式训练,你可能会遇到一些新的、更专门的张量函数。但万变不离其宗,都是基于这些基础概念的扩展。

对于初步学习,掌握这些已经非常好了。下一步的关键是将这些孤立的张量操作与神经网络的概念(如层、激活函数、损失函数、优化器)联系起来。

torch.nn

好的,torch.nn 模块是 PyTorch 中用于构建神经网络的核心。它提供了预定义的层 (layers)、损失函数 (loss functions) 以及其他构建神经网络所需的工具。这些构建块使得创建复杂的模型变得更加模块化和方便。

我们将从以下几个关键方面来学习 torch.nn

  1. nn.Module: 所有神经网络模块的基类。
  2. 常见的层 (Layers): 如线性层、卷积层、循环层等。
  3. 激活函数 (Activation Functions): 如 ReLU, Sigmoid, Tanh 等。
  4. 损失函数 (Loss Functions): 如 MSELoss, CrossEntropyLoss 等。
  5. 容器 (Containers): 如 nn.Sequential,用于串联多个层。

1. nn.Module:神经网络模块的基石

  • 是什么torch.nn.Module 是 PyTorch 中所有神经网络模块(包括层、整个模型)的基类。
  • 如何使用:当你定义自己的神经网络时,你需要创建一个继承自 nn.Module 的类。
  • 核心方法
    • __init__(self, ...): 在这里定义你的网络层。这些层本身也是 nn.Module 的子类实例,它们会被自动注册为模型的参数。
    • forward(self, input_data): 在这里定义数据如何通过你定义的层进行前向传播。这是必须实现的方法。
  • 自动功能:继承 nn.Module 后,你的类会自动获得一些有用的功能,例如:
    • 参数追踪 (model.parameters(), model.named_parameters()): 可以轻松访问模型中所有可学习的参数 (权重和偏置)。
    • 模型移动 (model.to(device)): 可以将整个模型及其参数移动到 CPU 或 GPU。
    • 模型状态保存与加载 (torch.save(model.state_dict(), PATH), model.load_state_dict(torch.load(PATH)))。
    • 切换训练/评估模式 (model.train(), model.eval()):这对于某些层(如 DropoutBatchNorm)在训练和评估时行为不同非常重要。
import torch
import torch.nn as nn
import torch.nn.functional as F # 通常包含无状态的函数,如激活函数

# 定义一个简单的线性回归模型
class SimpleLinearRegression(nn.Module):
    def __init__(self, input_dim, output_dim):
        super(SimpleLinearRegression, self).__init__() # 必须调用父类的 __init__
        # 定义网络层
        self.linear = nn.Linear(input_dim, output_dim) # 一个线性层

    def forward(self, x):
        # 定义前向传播逻辑
        out = self.linear(x)
        return out

# 实例化模型
input_size = 5  # 假设输入特征维度为5
output_size = 1 # 假设输出维度为1 (例如预测一个值)
model = SimpleLinearRegression(input_size, output_size)
print("模型结构:\n", model)

# 查看模型参数
print("\n模型参数:")
for name, param in model.named_parameters():
    if param.requires_grad:
        print(name, param.shape, param.dtype)

# 创建一个随机输入数据
dummy_input = torch.randn(10, input_size) # 10个样本,每个样本5个特征

# 前向传播
output = model(dummy_input) # 等价于 model.forward(dummy_input)
print("\n模型输出形状:", output.shape)

2. 常见的层 (Layers)

torch.nn 提供了大量预定义的层,用于构建不同类型的神经网络。

  • nn.Linear(in_features, out_features, bias=True): 全连接层(或密集层)。

    • 对输入数据进行线性变换:output = input @ W^T + b
    • in_features: 输入特征的数量。
    • out_features: 输出特征的数量。
    • bias: 是否使用偏置项,默认为 True
    • 权重 W 的形状是 (out_features, in_features),偏置 b 的形状是 (out_features)
  • 卷积层 (Convolutional Layers) (常用于图像处理,也在 LLM 的某些变体中使用):

    • nn.Conv1d(in_channels, out_channels, kernel_size, stride=1, padding=0, ...): 一维卷积。
    • nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, ...): 二维卷积。
    • in_channels: 输入通道数 (例如,RGB 图像为 3)。
    • out_channels: 输出通道数 (卷积核的数量)。
    • kernel_size: 卷积核的大小。
    • stride: 步长。
    • padding: 填充。
  • 循环层 (Recurrent Layers) (常用于序列数据,是早期 NLP 模型的基础,LLM 中的 Transformer 是一种替代方案):

    • nn.RNN(input_size, hidden_size, num_layers, batch_first=False, ...)
    • nn.LSTM(input_size, hidden_size, num_layers, batch_first=False, ...)
    • nn.GRU(input_size, hidden_size, num_layers, batch_first=False, ...)
    • input_size: 输入特征的维度。
    • hidden_size: 隐藏状态的维度。
    • num_layers: 堆叠的循环层数量。
    • batch_first=True: 如果输入数据的第一个维度是批大小 (batch size),则设为 True (通常推荐这样做)。
  • 池化层 (Pooling Layers) (常与卷积层配合使用):

    • nn.MaxPool2d(kernel_size, stride=None, ...)
    • nn.AvgPool2d(kernel_size, stride=None, ...)
  • Dropout 层:

    • nn.Dropout(p=0.5): 在训练期间以概率 p 随机将输入张量中的部分元素置为零,用于防止过拟合。在评估模式 (model.eval()) 下,Dropout 不起作用。
  • Embedding 层 (nn.Embedding(num_embeddings, embedding_dim)): 这个对 LLM 非常重要!

    • 用于将离散的类别索引(通常是词汇表中的单词 ID)映射为密集向量(词嵌入)。
    • num_embeddings: 词汇表的大小 (不同词语的数量)。
    • embedding_dim: 每个词嵌入向量的维度。
    • 它本质上是一个查找表,其权重是一个 (num_embeddings, embedding_dim) 的矩阵。
    vocab_size = 1000  # 假设词汇表有1000个词
    embedding_dim = 50 # 每个词用50维向量表示
    embedding_layer = nn.Embedding(vocab_size, embedding_dim)
    
    # 假设输入是一批词语的ID (batch_size=2, sequence_length=5)
    word_ids = torch.randint(0, vocab_size, (2, 5)) # 长整型张量
    print("\n输入词语ID:\n", word_ids)
    
    word_embeddings = embedding_layer(word_ids)
    print("词嵌入形状:", word_embeddings.shape) # (2, 5, 50)
    

3. 激活函数 (Activation Functions)

激活函数为神经网络引入非线性,使其能够学习更复杂的模式。它们通常作为独立的层或在 torch.nn.functional (简写为 F) 中以函数形式提供。

  • nn.ReLU()F.relu(x): max(0, x)。最常用的激活函数之一。
  • nn.Sigmoid()torch.sigmoid(x)F.sigmoid(x): 1 / (1 + exp(-x))。输出范围 (0, 1),常用于二分类的输出层或门控机制。
  • nn.Tanh()torch.tanh(x)F.tanh(x): (exp(x) - exp(-x)) / (exp(x) + exp(-x))。输出范围 (-1, 1)。
  • nn.Softmax(dim=None)F.softmax(x, dim=None): 将输入转换为概率分布。常用于多分类任务的输出层。dim 参数指定在哪个维度上进行 softmax 计算(通常是类别维度)。
  • nn.GELU()F.gelu(x): 高斯误差线性单元,Transformer 模型(如 BERT, GPT)中常用的激活函数。
  • nn.SiLU() (也被称为 Swish) 或 F.silu(x): x * sigmoid(x)
# 示例:一个包含激活函数的简单网络
class NetworkWithActivation(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        super().__init__()
        self.fc1 = nn.Linear(input_dim, hidden_dim)
        self.relu = nn.ReLU() # 可以作为一层
        self.fc2 = nn.Linear(hidden_dim, output_dim)

    def forward(self, x):
        x = self.fc1(x)
        x = self.relu(x) # 或者 x = F.relu(x)
        x = self.fc2(x)
        # 如果是多分类,最后可能还会有一个 softmax
        # x = F.softmax(x, dim=1) # 假设输出是 logits
        return x

model_act = NetworkWithActivation(10, 20, 3) # 输入10, 隐藏20, 输出3 (例如3个类别)
print("\n带激活函数的模型:\n", model_act)
dummy_input_act = torch.randn(4, 10) # 4个样本
output_act = model_act(dummy_input_act)
print("输出形状:", output_act.shape)

4. 损失函数 (Loss Functions)

损失函数衡量模型预测值与真实目标值之间的差异。训练的目标是最小化这个损失。

  • nn.MSELoss(): 均方误差损失。常用于回归任务。
    • loss(input, target) = mean((input - target)^2)
  • nn.CrossEntropyLoss(): 交叉熵损失。非常重要,常用于多分类任务。
    • 它内部通常结合了 LogSoftmaxNLLLoss (负对数似然损失)。
    • 期望的输入是模型的原始输出 (logits),而不是经过 softmax 的概率。
    • 期望的目标是类别索引 (长整型)。
  • nn.BCELoss(): 二元交叉熵损失。常用于二分类任务(通常模型输出需要先经过 Sigmoid)。
  • nn.BCEWithLogitsLoss(): 结合了 Sigmoid 层和 BCELoss,数值上更稳定,推荐用于二分类。输入是模型的原始 logits。
  • nn.NLLLoss(): 负对数似然损失。通常与 LogSoftmax 一起使用。
# 示例:计算损失
criterion_mse = nn.MSELoss()
criterion_ce = nn.CrossEntropyLoss()

# 回归任务示例
predictions_reg = torch.randn(5, 1) # 5个样本,每个预测1个值
targets_reg = torch.randn(5, 1)
loss_mse = criterion_mse(predictions_reg, targets_reg)
print(f"\nMSE Loss: {loss_mse.item()}") # .item() 获取标量张量的值

# 多分类任务示例 (3个类别)
predictions_cls = torch.randn(5, 3) # 5个样本,每个样本对3个类别的 logits
targets_cls = torch.randint(0, 3, (5,)) # 5个样本的真实类别索引 (0, 1, 或 2)
loss_ce = criterion_ce(predictions_cls, targets_cls)
print(f"Cross Entropy Loss: {loss_ce.item()}")

5. 容器 (Containers)

容器可以将多个层组合起来,形成更大的网络结构。

  • nn.Sequential(*args): 一个有序的模块容器。数据会按照模块在构造函数中定义的顺序依次通过它们。

    model_seq = nn.Sequential(
        nn.Linear(input_size, 128),
        nn.ReLU(),
        nn.Linear(128, 64),
        nn.ReLU(),
        nn.Linear(64, output_size)
        # 如果是分类,这里可能还会有一个 nn.LogSoftmax(dim=1) 或在CrossEntropyLoss中处理
    )
    print("\nSequential 模型:\n", model_seq)
    output_seq = model_seq(dummy_input)
    print("Sequential 模型输出形状:", output_seq.shape)
    
  • nn.ModuleList([module1, module2, ...]): 将子模块保存在一个列表中。ModuleList 本身不实现 forward 方法,你需要自己在模型的 forward 方法中显式地迭代调用这些模块。它能正确地注册模块的参数。

  • nn.ModuleDict({name1: module1, name2: module2, ...}): 将子模块保存在一个字典中。同样需要自己实现 forward

LLM 相关性:

  • nn.Linear: Transformer 模型中的前馈网络 (Feed-Forward Networks) 和计算 Q, K, V 矩阵时都会用到。
  • nn.Embedding: 将输入的 token ID 转换为词嵌入,是 LLM 的第一层。
  • nn.LayerNorm: 层归一化,Transformer 中广泛使用,用于稳定训练。
  • nn.Dropout: 用于防止过拟合。
  • 激活函数: GELU 是 Transformer 中常用的。
  • 损失函数: CrossEntropyLoss 用于语言模型的下一个词预测任务。
  • nn.Modulenn.Sequential: 用于构建整个 Transformer block 和整个 LLM 模型。

torch.nn 模块是构建神经网络的瑞士军刀。理解这些核心组件如何工作以及如何将它们组合起来,是使用 PyTorch 进行深度学习的关键。

下一步的建议:

  1. 动手实践:尝试自己定义不同结构的小型网络,使用不同的层和激活函数。
  2. 理解参数: 仔细观察每一层(特别是 nn.Linearnn.Embedding)的权重和偏置的形状是如何根据输入输出维度确定的。
  3. 连接 backward(): 思考当 loss.backward() 被调用时,梯度是如何流经这些层并更新它们的参数的。

DataLoader 是 PyTorch 中 torch.utils.data 模块的核心组件之一,它为我们提供了一种高效、灵活地加载数据的方式,特别是在训练神经网络时。

DataLoader 的主要作用和功能:

  1. 批量加载 (Batching):

    • 神经网络通常以小批量 (mini-batches) 的方式处理数据,而不是一次处理一个样本或整个数据集。DataLoader 会自动将数据集分割成指定大小的批次。
    • 为什么需要批量加载?
      • 内存效率: 一次性加载整个大型数据集到内存(尤其是 GPU 内存)是不可行的。
      • 计算效率: 对小批量数据进行并行计算(尤其是在 GPU 上)比逐个样本处理更高效。
      • 梯度稳定性: 使用小批量数据计算的梯度通常比单个样本的梯度更稳定,有助于模型收敛。
  2. 数据打乱 (Shuffling):

    • 在每个训练轮次 (epoch) 开始时,DataLoader 可以随机打乱数据的顺序。
    • 为什么需要打乱?
      • 防止模型学习到数据在原始顺序中可能存在的偶然模式。
      • 确保每个批次的数据具有一定的随机性,有助于模型的泛化。
      • 通常在训练时启用打乱 (shuffle=True),在验证和测试时禁用 (shuffle=False) 以确保结果的可复现性。
  3. 并行加载 (Multiprocessing):

    • DataLoader 可以使用多个子进程 (num_workers > 0) 来并行加载数据。
    • 为什么需要并行加载?
      • 当数据预处理(如图像增强、文本分词等)比较耗时时,如果数据加载是瓶颈,GPU 可能会处于空闲等待状态。
      • 通过并行加载,可以在 GPU 训练当前批次数据的同时,让 CPU 上的子进程预先加载和处理下一批次的数据,从而提高 GPU 的利用率和整体训练速度。
  4. 自定义数据处理 (Customizable Data Loading Logic):

    • DataLoader 配合 Dataset 对象一起工作。Dataset 是一个抽象类,你需要继承它并实现 __len__ (返回数据集大小) 和 __getitem__ (根据索引返回一个数据样本) 方法。
    • 这使得你可以定义非常灵活的数据加载和预处理逻辑。

DataLoader 的核心参数:

当你创建一个 DataLoader 实例时,通常会用到以下关键参数:

  • dataset:

    • 必需参数,一个 torch.utils.data.Dataset 对象。它定义了如何访问数据集中的单个样本。
    • PyTorch 提供了很多内置的 Dataset(例如,torchvision.datasets.ImageFolder, torchvision.datasets.CIFAR10),你也可以创建自定义的 Dataset
  • batch_size (int, optional, default=1):

    • 每个批次加载的样本数量。
  • shuffle (bool, optional, default=False):

    • 是否在每个 epoch 开始时打乱数据顺序。通常在训练时设为 True
  • num_workers (int, optional, default=0):

    • 用于数据加载的子进程数量。
      • 0 表示数据将在主进程中加载(没有并行)。
      • 大于 0 的值会启动相应数量的子进程进行并行加载。
    • 设置合适的 num_workers 值可以显著加速数据加载,但过高的值也可能因为进程间通信开销而降低效率,或者消耗过多 CPU 资源。通常建议从一个较小的值(如 24)开始尝试。
  • pin_memory (bool, optional, default=False):

    • 如果为 True,并且数据是从 CPU 加载到 GPU,DataLoader 会将张量复制到 CUDA 的固定内存 (pinned memory) 中。
    • 作用: 当数据从 CPU 传输到 GPU 时,如果数据在固定内存中,传输速度会更快。
    • 通常在与 GPU 一起使用时,并且 num_workers > 0 时,设置为 True 会有性能提升。
  • drop_last (bool, optional, default=False):

    • 如果数据集的总样本数不能被 batch_size 整除,最后一个批次的样本数量会小于 batch_size
    • 如果 drop_last=True,则会丢弃这个不完整的最后一个批次。
    • 如果 drop_last=False(默认),则最后一个批次会包含剩余的样本。
    • 在某些模型(特别是 RNN,如果批次内所有序列长度不一致且依赖于批次大小进行某些计算)或特定情况下,保持批次大小一致可能更方便,此时可以设为 True
  • sampler (Sampler or Iterable, optional):

    • 定义从数据集中提取样本的策略。如果指定了 sampler,则 shuffle 参数必须为 False
    • 你可以自定义 Sampler 来实现更复杂的采样逻辑,例如加权采样 (WeightedRandomSampler) 来处理类别不平衡问题。
  • collate_fn (callable, optional):

    • 一个函数,用于将从 Dataset__getitem__ 中获取的样本列表(即一个批次的样本)合并成一个批次张量。
    • 默认的 collate_fn 通常能很好地处理由 NumPy 数组或 PyTorch 张量组成的样本。
    • 何时需要自定义 collate_fn
      • 当你的 Dataset 返回的样本包含不同长度的序列(如文本数据)时,你需要在 collate_fn 中进行填充 (padding) 操作,使批次内所有序列长度一致。
      • 当样本结构比较复杂(例如,包含字典、非张量类型的数据)时。
      • 在之前的 ComplexCNN 例子中,我们没有显式指定 collate_fn,因为 TensorDataset 返回的已经是张量,默认的 collate_fn 可以直接将它们堆叠起来形成批次。
      • Seq2Seq 的例子中,如果我们要自己处理原始文本并进行填充,那么自定义 collate_fn 将非常有用。在之前的简化版中,我们假设了 TensorDataset 已经包含了填充好的序列。

DataLoader 如何工作 (简化流程):

  1. DataLoaderdataset 中获取样本索引(根据 shufflesampler 的设置)。
  2. 如果 num_workers > 0,它会将这些索引分配给子进程。
  3. 每个子进程(或主进程,如果 num_workers == 0)使用这些索引调用 dataset.__getitem__(index) 来获取单个数据样本。
  4. DataLoader 收集这些样本,形成一个列表(大小为 batch_size)。
  5. 然后,这个样本列表会传递给 collate_fn 函数,该函数将它们合并成一个或多个批次张量。
  6. 最终,DataLoader 的迭代器会 yield (产生) 这些处理好的批次张量,供训练循环使用。

总结:

DataLoader 是 PyTorch 数据加载流程的“引擎”,它负责将原始数据集有效地、批量地、可并行地转换成神经网络训练所需的格式。理解其工作原理和常用参数对于编写高效的 PyTorch 训练代码至关重要。