在人工智能领域,PyTorch 凭借其灵活的动态计算图和直观的 Python 风格,已成为学术界与工业界进行 深度学习 研究的主流框架。无论你是在探索 自然语言处理 还是 计算机视觉,掌握其核心——张量与自动微分——都是构建 神经网络 的基石。本文将带你从零开始,系统梳理 PyTorch 的关键概念,并最终通过一个 机器学习 经典案例串联全部知识点。
一、张量:AI 数据流的通用语言
在 PyTorch 的世界里,一切数据(从单个数值到图像、文本序列)都以 张量(Tensor) 的形式存在。你可以将其视为一个多维数组,它的维度结构直接映射了现实数据的复杂度:
- 0 维张量(标量):代表单一数值,如损失值。
- 1 维张量(向量):代表特征序列,如房价预测中的多个特征。
- 2 维张量(矩阵):常用于存储表格数据或权重矩阵。
- 3 维及以上:适合表示图像(高、宽、通道)或时间序列数据。
理解张量的创建与变换,是使用 PyTorch 构建高效数据管道的第一步。下面我们深入实操环节。
二、张量创建:从基础到进阶
PyTorch 提供了极其丰富的张量初始化手段,以适应不同的算法需求。
1. 直接构造与形状定义
最直接的方式是利用 torch.tensor() 从 Python 列表或 NumPy 数组转化。而当我们需要预先分配内存空间时,可以使用 torch.Tensor() 快速生成指定形状的张量(注意此时数值是未初始化的随机内存值)。
import torch
import numpy as np
# 从一个数字创建标量张量
tensor_scalar = torch.tensor(10)
print(tensor_scalar) # tensor(10)
# 从 Python 列表创建一维张量
tensor_1d = torch.tensor([1, 2, 3])
print(tensor_1d) # tensor([1, 2, 3])
# 从 NumPy 数组创建二维张量
numpy_arr = np.array([[1, 2, 3], [4, 5, 6]])
tensor_2d = torch.tensor(numpy_arr)
print(tensor_2d)
# 输出:
# tensor([[1, 2, 3],
# [4, 5, 6]])%%PROTROTECTED_CODE_2%%
2. 指定数据类型与序列生成
在 深度学习 中,精度控制至关重要。通过 dtype 参数可指定 int32、float32 等类型,这有助于在训练大模型时优化显存占用。
# 方式一:使用专用构造函数
tensor_int32 = torch.IntTensor(2, 3) # int32 类型
tensor_int64 = torch.LongTensor([1, 2, 3]) # int64 类型
tensor_f32 = torch.FloatTensor([9, 8, 7]) # float32 类型
tensor_f64 = torch.DoubleTensor(2, 3, 1) # float64 类型
# 方式二:使用 dtype 参数(推荐,更清晰)
tensor_a = torch.tensor([1, 2, 3], dtype=torch.int32)
tensor_b = torch.tensor([1, 2, 3], dtype=torch.float32)
# 注意:如果数据类型不匹配,小数会被直接截断
tensor_trunc = torch.IntTensor([[1.1, 2.2, 3.6]])
print(tensor_trunc) # tensor([[1, 2, 3]], dtype=torch.int32)同时,生成等差数列或等比数列是构造输入数据的常用手段:
# torch.arange(start, end, step) — 按步长生成 [start, end) 区间内的数
tensor_arange = torch.arange(10, 30, 2)
print(tensor_arange) # tensor([10, 12, 14, 16, 18, 20, 22, 24, 26, 28])
# 只给 end,默认从 0 开始,步长为 1
tensor_arange_simple = torch.arange(6)
print(tensor_arange_simple) # tensor([0, 1, 2, 3, 4, 5])
# torch.linspace(start, end, steps) — 在 [start, end] 区间内均匀生成指定数量的数
tensor_linspace = torch.linspace(10, 30, 5)
print(tensor_linspace) # tensor([10., 15., 20., 25., 30.])
# torch.logspace(start, end, steps, base) — 生成等比数列
# 底数为 2,指数从 1 到 3,共 3 个数 → 2^1=2, 2^2=4, 2^3=8
tensor_logspace = torch.logspace(1, 3, 3, 2)
print(tensor_logspace) # tensor([2., 4., 8.])3. 初始化的艺术:固定值与随机分布
权重初始化直接影响 神经网络 的收敛速度。全零、全一用于特定场景,而正态分布、均匀分布等随机初始化则是打破对称性的关键。
# 全 0 张量
zeros = torch.zeros(2, 3)
print(zeros)
# tensor([[0., 0., 0.],
# [0., 0., 0.]])
# 全 1 张量,形状与已有张量相同
ones_like = torch.ones_like(zeros)
print(ones_like)
# 所有元素都填成同一个值
full = torch.full((2, 3), 6)
print(full)
# 未初始化的空张量(内容随机,但分配速度很快)
empty = torch.empty(2, 3)
print(empty)
# 单位矩阵(对角线为 1)
eye = torch.eye(3)
print(eye)
# tensor([[1., 0., 0.],
# [0., 1., 0.],
# [0., 0., 1.]])# 均匀分布 [0, 1) 上的随机数
rand_uniform = torch.rand(2, 3)
print(rand_uniform)
# 整数均匀分布 [low, high) 上的随机整数
rand_int = torch.randint(1, 10, (2, 3))
print(rand_int)
# 标准正态分布 N(0, 1) 上的随机数
rand_normal = torch.randn(4, 2)
print(rand_normal)
# 自定义正态分布 N(mean, std),形状可以指定
rand_custom = torch.normal(5, 1, (2, 3))
print(rand_custom)
# 随机排列(洗牌):生成 0 到 n-1 的随机顺序
rand_perm = torch.randperm(10)
print(rand_perm) # 每次运行结果不同,例如 tensor([4, 8, 0, 2, 3, 7, 1, 5, 9, 6])
# 固定随机种子,让结果可以复现
print(torch.random.initial_seed()) # 查看当前种子
torch.manual_seed(42) # 设置种子为 42
print(torch.random.initial_seed())三、数据类型转换与内存共享机制
在训练循环中,频繁的数据类型转换(如 float64 转 float32)是优化性能的常见手段。PyTorch 提供了简洁的 API 实现这一点。
tensor = torch.tensor([1, 2, 3])
print(tensor, tensor.dtype) # tensor([1, 2, 3]) torch.int64
# 方法一:使用 type() 方法
tensor = tensor.type(torch.float32)
print(tensor, tensor.dtype) # tensor([1., 2., 3.]) torch.float32
# 方法二:使用专用转换方法(更简洁)
tensor = tensor.double() # 转换为 float64
tensor = tensor.long() # 转换为 int64⚠️ 特别注意:Tensor 与 NumPy 数组互转时,默认存在 内存共享 机制。修改其中一个,另一个也会随之改变,这既是优势(高效),也是陷阱(易产生隐晦 bug)。
# Tensor → NumPy(共享内存)
tensor = torch.rand(3, 2)
numpy_arr = tensor.numpy()
print(type(tensor), type(numpy_arr)) #
# 修改 Tensor,NumPy 数组也会跟着变
tensor[:, 0] = 4
print("修改后的 Tensor:\n", tensor)
print("同步变化的 NumPy 数组:\n", numpy_arr)
# 如果不希望共享内存,可以复制一份
numpy_arr_copy = tensor.numpy().copy()
tensor[:, 0] = -1
print("Tensor 再次修改后:\n", tensor)
print("NumPy 副本没有变化:\n", numpy_arr_copy)
# NumPy → Tensor(同样共享内存)
numpy_arr = np.random.randn(3)
tensor_from_np = torch.from_numpy(numpy_arr)
print("原始 NumPy:", numpy_arr)
print("转换得到的 Tensor:", tensor_from_np)
# 修改 NumPy,Tensor 也会变
numpy_arr[0] = 100
print("修改 NumPy 后:\n", numpy_arr)
print("Tensor 同步变化:\n", tensor_from_np)
# 如果想彻底独立,用 copy()
tensor_safe = torch.from_numpy(numpy_arr.copy())
# 使用 torch.tensor() 也会创建独立副本(深拷贝)
tensor_independent = torch.tensor(numpy_arr) 若需提取单个标量值(如损失值)用于绘图或打印,使用 .item() 是最优雅的方式。
scalar_tensor = torch.tensor(1)
print(scalar_tensor) # tensor(1)
print(scalar_tensor.item()) # 1 (Python int)四、张量运算:算术、乘法与内存优化
张量运算是 机器学习 模型的数学基础。
1. 元素级运算与就地操作
加减乘除(add, sub)提供了非破坏性版本,而带下划线的版本(如 add_())则直接修改原张量,节省内存。
tensor = torch.randint(1, 9, (2, 3))
print("原始张量:\n", tensor)
# 加法(不修改原数据)
result = tensor.add(10)
print("加法结果:\n", result)
print("原张量不变:\n", tensor)
# 加法(就地修改)
tensor.add_(10)
print("就地加法后:\n", tensor)
# 类似地,减法 sub()/sub_(),乘法 mul()/mul_(),除法 div()/div_()
# 取负
print("取负:\n", tensor.neg())
# 幂运算
pow_tensor = torch.tensor([1, 2, 3])
pow_tensor.pow_(2) # 原地求平方
print("平方后:", pow_tensor) # tensor([1, 4, 9])
# 平方根
sqrt_tensor = torch.tensor([1.0, 2.0, 3.0])
sqrt_tensor.sqrt_()
print("平方根后:", sqrt_tensor)
# 指数运算 e^x
exp_tensor = torch.tensor([1.0, 2.0, 3.0])
print("e^tensor:", exp_tensor.exp())
# 自然对数 ln(x)
print("ln(tensor):", exp_tensor.log())2. 分清哈达玛积与矩阵乘法
这是初学者的高频误区:
- 哈达玛积(*):对应位置元素相乘,要求形状一致。
- 矩阵乘法(@):遵循线性代数规则,执行行与列的运算。
%%PROTROTECTED_CODE_11%%
性能优化技巧:在循环中避免 X = X @ Y 这种写法,因为它会频繁释放旧内存并分配新内存。推荐使用切片赋值法将结果写回原内存空间。
X = torch.randint(1, 9, (3, 2, 4))
Y = torch.randint(1, 9, (3, 4, 1))
print("原始 id:", id(X))
X[:] = X @ Y # 原地更新,id 不会变
print("原地更新后 id:", id(X))3. 聚合统计函数
PyTorch 内置了大量统计函数(如 sum, mean),通过 dim 参数控制压缩维度。例如,对形状为 (3, 2, 4) 的张量执行 sum(dim=0),结果形状将变为 (2, 4)。
# 创建一个 3×2×4 的随机整数张量,并转为 float 类型以便求均值
tensor = torch.randint(1, 9, (3, 2, 4)).float()
print("原始形状:", tensor.shape)
# 求和
print("所有元素之和:", tensor.sum())
print("按第 0 维求和后的形状:", tensor.sum(dim=0).shape) # (2, 4)
# 求均值
print("所有元素的均值:", tensor.mean())
print("按第 1 维求均值后的形状:", tensor.mean(dim=1).shape) # (3, 4)
# 最大值及其索引
max_val = tensor.max()
print("全局最大值:", max_val)
# 按维度求最大值,返回 (最大值, 索引)
max_vals, max_idxs = tensor.max(dim=2)
print("沿第 2 维的最大值:", max_vals)
print("对应的索引:", max_idxs)
# 最小值索引(展平后的位置)
print("最小值索引(一维位置):", tensor.argmin())
# 标准差
print("标准差:", tensor.std())
# 去重
print("去重后的元素:", tensor.unique())
# 排序(返回排序后的值和原始索引)
sorted_vals, sorted_idxs = tensor.sort()
print("排序后的值:", sorted_vals)五、索引、切片与形状变换
高效的数据操作能力决定了数据预处理的效率。
1. 灵活的数据选取
PyTorch 的索引语法与 NumPy 几乎无缝衔接,支持范围索引、列表索引(选取不连续位置)以及强大的布尔索引(按条件筛选)。
tensor = torch.randint(1, 9, (3, 5, 4))
print("原始形状:", tensor.shape) # (3, 5, 4)
# 取第 0 维的第 0 个元素(得到 5×4 的矩阵)
print(tensor[0])
# 取所有第 0 维,第 1 维的第 1 个元素(得到 3×4 的矩阵)
print(tensor[:, 1])
# 精确取一个标量:第 2 维的第 1 个,第 1 维的第 3 个,第 2 维的第 4 个(索引从 0 开始)
print(tensor[2, 1, 3])
# 范围切片
print(tensor[-1, 1:4, 0:3]) # 最后一批,第 1~3 行,前 3 列# 列表索引:取 (0,1) 和 (1,2) 位置的值
print(tensor[[0, 1], [1, 2]])
# 布尔索引:找出满足条件的元素
# 条件:第 2 维的第 0 个通道的值大于 5
mask = tensor[:, :, 0] > 5
print("布尔掩码形状:", mask.shape) # (3, 5)
print("满足条件的元素:", tensor[mask])
# 更精细的筛选:特定位置大于 5
mask2 = tensor[:, 1, 2] > 5
print("满足条件的整批数据:", tensor[mask2])2. 维度重排的底层逻辑
理解 transpose/permute 与 reshape/view 的区别至关重要。
- transpose 与 permute:仅交换维度映射关系,不实际移动内存,因此操作快。
- view:要求张量在物理内存中连续,否则会报错。它共享底层数据。
- reshape:更通用,若内存不连续会自动拷贝数据以保证成功。
tensor = torch.randint(1, 9, (2, 3, 6))
print("原始形状:", tensor.shape) # (2, 3, 6)
# 交换第 1 维和第 2 维
transposed = tensor.transpose(1, 2)
print("transpose(1,2) 后形状:", transposed.shape) # (2, 6, 3)
# 重排所有维度:原始 (2,3,6) → (6,2,3)
permuted = tensor.permute(2, 0, 1)
print("permute(2,0,1) 后形状:", permuted.shape) # (6, 2, 3)tensor = torch.randint(1, 9, (3, 5, 4))
# reshape 可以自动推断维度(-1 表示自动计算)
reshaped = tensor.reshape(6, 10)
print("reshape(6,10) 形状:", reshaped.shape)
reshaped_auto = tensor.reshape(3, -1)
print("reshape(3, -1) 形状:", reshaped_auto.shape) # (3, 20)
# view 要求内存连续
print("是否连续:", tensor.is_contiguous()) # True
viewed = tensor.view(-1, 10)
print("view(-1,10) 形状:", viewed.shape)
# 经典坑:transpose 之后内存不再连续
tensor_t = tensor.T
print("转置后是否连续:", tensor_t.is_contiguous()) # False
# 下面这行会报错:view 要求连续内存
# tensor_t.view(-1)
# 解决方法:先调用 contiguous()
contiguous_t = tensor_t.contiguous()
print("contiguous 后可以 view:", contiguous_t.view(-1).shape) 当遇到 view 报错时,调用 .contiguous() 强制整理内存即可解决。此外,unsqueeze(增加维度)和 squeeze(压缩维度)常用于调整 batch 或通道维度。
tensor = torch.tensor([1, 2, 3, 4, 5])
print("原始形状:", tensor.shape) # (5,)
# 在位置 0 插入新维度
unsqueezed_0 = tensor.unsqueeze(dim=0)
print("unsqueeze(0) 形状:", unsqueezed_0.shape) # (1, 5)
# 在位置 1 插入新维度
unsqueezed_1 = tensor.unsqueeze(dim=1)
print("unsqueeze(1) 形状:", unsqueezed_1.shape) # (5, 1)
# 在最后插入(dim=-1)
unsqueezed_last = tensor.unsqueeze(dim=-1)
print("unsqueeze(-1) 形状:", unsqueezed_last.shape) # (5, 1)
# 删除所有大小为 1 的维度
squeezed = unsqueezed_0.squeeze()
print("squeeze 后形状:", squeezed.shape) # (5,)3. 拼接与堆叠
cat 沿已有维度拼接(总维度不变),而 stack 则创建新维度堆叠(维度增加),后者常用于构建 batch 数据。
# cat 示例
a = torch.randint(1, 9, (2, 2, 5))
b = torch.randint(1, 9, (2, 1, 5))
catted = torch.cat([a, b], dim=1)
print("cat 后形状:", catted.shape) # (2, 3, 5)
# stack 示例
x = torch.randint(1, 9, (3, 1, 5))
y = torch.randint(1, 9, (3, 1, 5))
stacked = torch.stack([x, y], dim=2)
print("stack 后形状:", stacked.shape) # (3, 1, 2, 5)六、自动微分:神经网络的引擎
这是 PyTorch 最核心的亮点。只需设置 requires_grad=True,PyTorch 便会自动追踪所有张量操作并构建 动态计算图。调用 backward() 后,梯度将自动计算并存储至 .grad 属性中。
import torch
# 输入 x 和目标值 y
x = torch.tensor(10.0)
y = torch.tensor(3.0)
# 初始化权重 w 和偏置 b,并告诉 PyTorch 需要追踪它们的梯度
w = torch.rand(1, 1, requires_grad=True)
b = torch.rand(1, 1, requires_grad=True)
# 前向传播:计算预测值
z = w * x + b
# 定义损失函数(均方误差)
loss_fn = torch.nn.MSELoss()
loss = loss_fn(z, y)
# 反向传播:自动计算 w 和 b 的梯度
loss.backward()
# 输出梯度
print("w 的梯度:\n", w.grad)
print("b 的梯度:\n", b.grad)
# 叶子节点:由用户直接创建,不是计算得到的
print("x 是叶子节点:", x.is_leaf) # True
print("w 是叶子节点:", w.is_leaf) # True
print("z 是叶子节点:", z.is_leaf) # False(由计算得到)
print("loss 是叶子节点:", loss.is_leaf) # False✅ 动态图的优势:计算图是实时构建的,这意味着你可以在 if 或 for 循环中灵活改变网络结构,这对科研实验极其友好。
⚠️ 若需将某些张量从计算图中剥离(如对抗训练中冻结生成器),可使用 detach() 方法。
x = torch.ones(2, 2, requires_grad=True)
y = x * x
# 分离 y,新变量 u 不再记录计算历史
u = y.detach()
z = u * x
# 反向传播时,梯度不会经过 u 传播到 x
z.sum().backward()
# 验证:x.grad 应该等于 u(被当作常数),而不是 3*x^2
print(x.grad == u) # 所有元素为 True[AFFILIATE_SLOT_1]
七、实战:从零构建线性回归模型
理论知识已备齐,现在我们将所有要点串联,完成一个完整的 机器学习 任务:拟合 y = 2.5 * x + 5.2 + 噪声。标准流程包含四步:数据准备、模型构建、损失函数与优化器定义、训练循环。
import torch
import matplotlib.pyplot as plt
from torch import nn, optim
from torch.utils.data import TensorDataset, DataLoader
# ---------- 1. 准备数据 ----------
# 生成 100 个样本,每个样本 1 个特征
X = torch.randn(100, 1) # 输入
w_true = torch.tensor([2.5]) # 真实权重
b_true = torch.tensor([5.2]) # 真实偏置
noise = torch.randn(100, 1) * 0.1 # 添加噪声,模拟真实数据
y = w_true * X + b_true + noise # 目标值
# 将数据包装成 Dataset 和 DataLoader
dataset = TensorDataset(X, y)
dataloader = DataLoader(
dataset,
batch_size=10, # 每次取 10 个样本训练
shuffle=True # 每个 epoch 打乱顺序
)
# ---------- 2. 构建模型 ----------
# nn.Linear 就是一个线性层:y = weight * x + bias
model = nn.Linear(in_features=1, out_features=1)
# ---------- 3. 定义损失函数和优化器 ----------
loss_fn = nn.MSELoss() # 均方误差损失,适合回归
optimizer = optim.SGD(model.parameters(), lr=1e-3) # 随机梯度下降,学习率 0.001
# ---------- 4. 训练循环 ----------
loss_list = [] # 记录每个 epoch 的平均损失,用于绘图
num_epochs = 1000
for epoch in range(num_epochs):
total_loss = 0
num_samples = 0
for x_batch, y_batch in dataloader:
# 前向传播:计算预测值
y_pred = model(x_batch)
# 计算损失
loss = loss_fn(y_pred, y_batch)
total_loss += loss.item()
num_samples += len(y_batch)
# 反向传播前必须清零梯度(否则梯度会累积)
optimizer.zero_grad()
# 反向传播:计算梯度
loss.backward()
# 更新参数
optimizer.step()
avg_loss = total_loss / num_samples
loss_list.append(avg_loss)
# 打印结果
print(f"训练得到的权重: {model.weight.item():.4f} (真实值: 2.5)")
print(f"训练得到的偏置: {model.bias.item():.4f} (真实值: 5.2)")
# 绘制损失曲线
plt.plot(loss_list)
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title("线性回归训练损失曲线")
plt.show()运行上述代码,你会发现损失稳步下降,最终学到的参数逼近真实值。这一套代码模板正是 PyTorch 处理各类 AI 问题的通用范式。
本文所有代码都可以直接运行,建议打开 Jupyter Notebook 边看边敲。
[AFFILIATE_SLOT_2]
总结与行动建议
本文从 张量 这一核心数据结构出发,系统梳理了创建、运算、变换与索引的常用技巧,并深入剖析了 自动微分 的原理与优势。最后通过线性回归实战,展示了从数据加载到参数更新的完整训练闭环。 现在,请动手运行文中的示例代码,尝试调整学习率、batch size 甚至将 nn.Linear 替换为卷积层或循环层,开启你的 PyTorch 深度探索之旅吧!
浙公网安备 33010602011771号