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