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.shape
或tensor.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
总结一下,主要包括:
-
张量的创建:
- 从 Python 列表或 NumPy 数组创建 (
torch.tensor()
,torch.from_numpy()
)。 - 创建特定形状和值的张量 (
torch.zeros()
,torch.ones()
,torch.rand()
,torch.randn()
,torch.arange()
,torch.linspace()
)。 - 使用
_like
版本根据现有张量创建 (torch.zeros_like()
)。
- 从 Python 列表或 NumPy 数组创建 (
-
张量的属性:
- 形状 (
.shape
,.size()
)。 - 数据类型 (
.dtype
)。 - 所在设备 (
.device
)。 - 是否需要梯度 (
.requires_grad
)。 - 梯度值 (
.grad
)。 - 梯度函数 (
.grad_fn
),用于追踪操作历史以进行反向传播。
- 形状 (
-
张量的基本运算:
- 算术运算: 加、减、乘、除、矩阵乘法 (
@
或torch.matmul()
)。 - 元素级运算:
torch.abs()
,torch.sqrt()
,torch.exp()
,torch.log()
,torch.sin()
,torch.sigmoid()
,torch.relu()
等。 - 比较运算:
torch.eq()
,torch.gt()
,torch.lt()
等,返回布尔张量。
- 算术运算: 加、减、乘、除、矩阵乘法 (
-
索引、切片和连接:
- 索引和切片: 与 NumPy 非常相似,用于访问和修改张量的部分元素。
- 连接:
torch.cat()
(沿指定维度拼接张量),torch.stack()
(沿新维度堆叠张量)。 - 拆分:
torch.split()
,torch.chunk()
。
-
形状变换:
tensor.view()
: 改变张量形状(共享数据,要求内存连续)。tensor.reshape()
: 改变张量形状(不一定共享数据,更灵活)。tensor.T
或tensor.transpose()
: 转置。tensor.permute()
: 任意维度重排。tensor.unsqueeze()
: 增加维度。tensor.squeeze()
: 移除大小为1的维度。
-
聚合操作:
torch.sum()
,torch.mean()
,torch.prod()
,torch.std()
,torch.var()
。torch.min()
,torch.max()
(返回数值)。torch.argmin()
,torch.argmax()
(返回索引)。- 可以指定
dim
参数沿特定维度进行聚合。
-
GPU 加速:
- 检查 GPU 可用性 (
torch.cuda.is_available()
)。 - 将张量移至设备 (
tensor.to(device)
)。 - 直接在特定设备创建张量 (
torch.tensor(..., device=device)
)。
- 检查 GPU 可用性 (
-
自动求导 (
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
:
nn.Module
: 所有神经网络模块的基类。- 常见的层 (Layers): 如线性层、卷积层、循环层等。
- 激活函数 (Activation Functions): 如 ReLU, Sigmoid, Tanh 等。
- 损失函数 (Loss Functions): 如 MSELoss, CrossEntropyLoss 等。
- 容器 (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()
):这对于某些层(如Dropout
和BatchNorm
)在训练和评估时行为不同非常重要。
- 参数追踪 (
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()
: 交叉熵损失。非常重要,常用于多分类任务。- 它内部通常结合了
LogSoftmax
和NLLLoss
(负对数似然损失)。 - 期望的输入是模型的原始输出 (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.Module
和nn.Sequential
: 用于构建整个 Transformer block 和整个 LLM 模型。
torch.nn
模块是构建神经网络的瑞士军刀。理解这些核心组件如何工作以及如何将它们组合起来,是使用 PyTorch 进行深度学习的关键。
下一步的建议:
- 动手实践:尝试自己定义不同结构的小型网络,使用不同的层和激活函数。
- 理解参数: 仔细观察每一层(特别是
nn.Linear
和nn.Embedding
)的权重和偏置的形状是如何根据输入输出维度确定的。 - 连接
backward()
: 思考当loss.backward()
被调用时,梯度是如何流经这些层并更新它们的参数的。
DataLoader
是 PyTorch 中 torch.utils.data
模块的核心组件之一,它为我们提供了一种高效、灵活地加载数据的方式,特别是在训练神经网络时。
DataLoader
的主要作用和功能:
-
批量加载 (Batching):
- 神经网络通常以小批量 (mini-batches) 的方式处理数据,而不是一次处理一个样本或整个数据集。
DataLoader
会自动将数据集分割成指定大小的批次。 - 为什么需要批量加载?
- 内存效率: 一次性加载整个大型数据集到内存(尤其是 GPU 内存)是不可行的。
- 计算效率: 对小批量数据进行并行计算(尤其是在 GPU 上)比逐个样本处理更高效。
- 梯度稳定性: 使用小批量数据计算的梯度通常比单个样本的梯度更稳定,有助于模型收敛。
- 神经网络通常以小批量 (mini-batches) 的方式处理数据,而不是一次处理一个样本或整个数据集。
-
数据打乱 (Shuffling):
- 在每个训练轮次 (epoch) 开始时,
DataLoader
可以随机打乱数据的顺序。 - 为什么需要打乱?
- 防止模型学习到数据在原始顺序中可能存在的偶然模式。
- 确保每个批次的数据具有一定的随机性,有助于模型的泛化。
- 通常在训练时启用打乱 (
shuffle=True
),在验证和测试时禁用 (shuffle=False
) 以确保结果的可复现性。
- 在每个训练轮次 (epoch) 开始时,
-
并行加载 (Multiprocessing):
DataLoader
可以使用多个子进程 (num_workers > 0
) 来并行加载数据。- 为什么需要并行加载?
- 当数据预处理(如图像增强、文本分词等)比较耗时时,如果数据加载是瓶颈,GPU 可能会处于空闲等待状态。
- 通过并行加载,可以在 GPU 训练当前批次数据的同时,让 CPU 上的子进程预先加载和处理下一批次的数据,从而提高 GPU 的利用率和整体训练速度。
-
自定义数据处理 (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
。 在之前的例子中,我们使用了TensorDataset,它是一个简单的Dataset,可以将多个张量(如特征和标签)封装起来,并假设它们的第一维是样本索引。
- 必需参数,一个
-
batch_size
(int, optional, default=1):- 每个批次加载的样本数量。
-
shuffle
(bool, optional, default=False):- 是否在每个 epoch 开始时打乱数据顺序。通常在训练时设为
True
。
- 是否在每个 epoch 开始时打乱数据顺序。通常在训练时设为
-
num_workers
(int, optional, default=0):- 用于数据加载的子进程数量。
0
表示数据将在主进程中加载(没有并行)。- 大于
0
的值会启动相应数量的子进程进行并行加载。
- 设置合适的
num_workers
值可以显著加速数据加载,但过高的值也可能因为进程间通信开销而降低效率,或者消耗过多 CPU 资源。通常建议从一个较小的值(如2
或4
)开始尝试。
- 用于数据加载的子进程数量。
-
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
如何工作 (简化流程):
DataLoader
从dataset
中获取样本索引(根据shuffle
或sampler
的设置)。- 如果
num_workers > 0
,它会将这些索引分配给子进程。 - 每个子进程(或主进程,如果
num_workers == 0
)使用这些索引调用dataset.__getitem__(index)
来获取单个数据样本。 DataLoader
收集这些样本,形成一个列表(大小为batch_size
)。- 然后,这个样本列表会传递给
collate_fn
函数,该函数将它们合并成一个或多个批次张量。 - 最终,
DataLoader
的迭代器会yield
(产生) 这些处理好的批次张量,供训练循环使用。
总结:
DataLoader
是 PyTorch 数据加载流程的“引擎”,它负责将原始数据集有效地、批量地、可并行地转换成神经网络训练所需的格式。理解其工作原理和常用参数对于编写高效的 PyTorch 训练代码至关重要。