AI基石 | Python工具链(三):PyTorch入门 —— 炼丹炉已开,把 NumPy 扔进显卡里!
PyTorch利用GPU加速计算和自动微分,实现高效神经网络训练,是AI开发的核心框架。
AI基石 | Python工具链(三):PyTorch入门 —— 炼丹炉已开,把 NumPy 扔进显卡里!
前言
在上一篇,我们手握 NumPy 和 Pandas,觉得自己已经能处理全世界的数据了。
但当你试图用 NumPy 手写一个神经网络时,你会撞上两堵墙:
- 算不动:矩阵一旦超过 10000 维,CPU 就开始发烫降频,NumPy 对此无能为力。
- 算不准:还记得微积分里的“链式法则”吗?如果神经网络有 100 层,手推导数公式会让你崩溃。只要写错一个符号,整个模型就废了。
今天,我们请出 AI 界的“屠龙刀” —— PyTorch。
它由 Meta (Facebook) 开源,它做对了最关键的两件事:
- 把张量搬到了 GPU 上(解决了“算不动”)。
- 实现了自动微分系统 Autograd(解决了“算不准”)。
系好安全带,我们要开始真正的“硬核”编程了。
一、 Tensor (张量):觉醒了“原力”的 NumPy
PyTorch 的 Tensor 在语法上几乎和 NumPy 的 ndarray 一模一样。这不是巧合,而是为了降低迁移成本。但 Tensor 多了两个核心属性:device(设备)和 grad(梯度)。
1. 设备感知:CPU vs GPU
NumPy 只能在 CPU 上跑,而 Tensor 可以随意切换领地。
import torch
import numpy as np
print(f"PyTorch版本: {torch.__version__}")
print(f"CUDA可用: {torch.cuda.is_available()}")
print(f"CUDA版本: {torch.version.cuda}")
print(f"可用设备数: {torch.cuda.device_count()}")
print(f"当前设备: {torch.cuda.current_device()}")
print(f"设备名称: {torch.cuda.get_device_name(0) if torch.cuda.device_count() > 0 else '无GPU'}")
# 1. 创建 Tensor (和 NumPy 极其相似)
x_np = np.array([1, 2, 3])
x_pt = torch.tensor([1, 2, 3]) # 默认在 CPU
# 2. 桥接 NumPy
# 两者共享内存!修改一个,另一个也会变(如果在CPU上)
y_pt = torch.from_numpy(x_np)
# 3. --- 硬核时刻:搬运到 GPU ---
if torch.cuda.is_available():
# 这行代码是深度学习加速 100 倍的关键
device = torch.device("cuda")
x_cuda = x_pt.to(device)
print(f"数据在: {x_cuda.device}")
# 注意:GPU 上的张量不能直接和 NumPy 交互,必须先 .cpu() 搬回来
result = x_cuda.cpu().numpy()
print(result)
else:
print("没有显卡,只能用 CPU 跑代码,惨。")
# PyTorch版本: 2.9.1+cu126
# CUDA可用: True
# CUDA版本: 12.6
# 可用设备数: 1
# 当前设备: 0
# 设备名称: NVIDIA GeForce RTX 3060
# 数据在: cuda:0
干货 Tip:在写代码时,养成定义 device 的习惯,这是从脚本小子进阶到工程化的第一步。
2. 动态图机制 (Dynamic Computational Graph)
这是 PyTorch 战胜 TensorFlow 1.x 的杀手锏。
- TensorFlow (旧版):先定义图(像画好电路板),再灌水(数据)。一旦出错,不知道是哪条线烧了。
- PyTorch:边执行边建图。代码运行到哪,图就建到哪。你可以用 Python 的
if/else随意控制网络结构。
二、 Autograd:自动微分的“黑魔法”
这是本篇最核心的干货。PyTorch 是如何知道怎么求导的?
当你创建一个 Tensor 并设置 requires_grad=True 时,PyTorch 就不再只把它看作一个数据,而是一个计算图的节点。
1. 追踪历史 (Tracking History)
PyTorch 会在后台启动一个“书记员”,记录你对这个张量做的每一次运算。
# 创建一个需要求导的张量 w (比如权重)
w = torch.tensor(1.0, requires_grad=True)
x = torch.tensor(2.0)
# 定义运算
y = w * x # 第一步:乘法
z = y ** 2 # 第二步:平方
# 此时,PyTorch 悄悄建立了一张图:
# w -> [Mul] -> y -> [Pow] -> z
2. 反向传播 (Backward)
当你调用 z.backward() 时,PyTorch 会沿着刚才那张图反向跑一遍,利用链式法则自动计算出 z 对 w 的导数,并存入 w.grad 中。
z.backward()
# 手推验证:
# z = (wx)^2
# dz/dw = 2(wx) * x = 2 * (1*2) * 2 = 8
print(f"PyTorch 算的导数: {w.grad}") # 输出 8.0
干货 Tip:
- Leaf Node (叶子节点):像
w这种我们自己创建的变量,才有梯度。像y和z这种中间运算结果,梯度在反向传播后会被释放以节省内存。 - grad_fn:你可以打印
z.grad_fn,会发现它叫<PowBackward0>,这就是 PyTorch 记录的“运算历史”。
三、 实战:手写线性回归(从原理到框架)
为了让你彻底理解框架的封装艺术,我们用两种方式实现同一个线性回归任务:拟合 y=3x+0.8y = 3x + 0.8y=3x+0.8。
方式一:低阶实现(手动挡)
这种写法能让你看清 PyTorch 到底在干什么。
import torch
# 1. 准备数据
X = torch.rand(100, 1)
y = 3 * X + 0.8 + 0.05 * torch.randn(100, 1) # 加点噪音
# 2. 初始化参数 (随机猜)
w = torch.randn(1, 1, requires_grad=True)
b = torch.randn(1, 1, requires_grad=True)
learning_rate = 0.1
for epoch in range(1000):
# --- Forward (前向传播) ---
y_pred = X @ w + b # 矩阵乘法
# --- Loss (计算误差) ---
loss = (y_pred - y).pow(2).mean() # MSE Loss
# --- Backward (反向传播) ---
# 这一步会自动把 dLoss/dw 和 dLoss/db 算出来,存进 w.grad 和 b.grad
loss.backward()
# --- Update (更新参数) ---
# 核心干货:为什么要用 no_grad?
# 因为更新参数 w = w - lr * grad 是一个计算操作。
# 如果不关掉梯度追踪,PyTorch 会以为这步操作也要算进计算图里,导致无限套娃和内存爆炸。
with torch.no_grad():
w -= learning_rate * w.grad
b -= learning_rate * b.grad
# 核心干货:为什么要 zero_grad?
# PyTorch 的梯度默认是“累加”的(为了支持 RNN 等复杂网络)。
# 在下一轮迭代前,必须把上一轮的梯度清零!
w.grad.zero_()
b.grad.zero_()
if epoch % 100 == 0:
print(f"Epoch {epoch}: w={w.item():.2f}, b={b.item():.2f}")
# Epoch 0: w=1.51, b=-1.53
# Epoch 100: w=2.95, b=0.82
# Epoch 200: w=2.99, b=0.80
# Epoch 300: w=3.00, b=0.80
# Epoch 400: w=3.00, b=0.80
# Epoch 500: w=3.00, b=0.80
# Epoch 600: w=3.00, b=0.80
# Epoch 700: w=3.00, b=0.80
# Epoch 800: w=3.00, b=0.80
# Epoch 900: w=3.00, b=0.80
方式二:高阶实现(自动挡)
这才是 AI 工程师每天写的代码。 我们使用 torch.nn (Neural Network) 和 torch.optim (Optimizer)。
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
# ==================== 设置随机种子 ====================
torch.manual_seed(42)
# ==================== 创建训练数据 y = 3x + 0.8 ====================
# 生成更多数据点
n_samples = 100
X = torch.randn(n_samples, 1) * 2 # 生成-2到2之间的随机数,增加数据范围
true_w = 3.0
true_b = 0.8
noise = torch.randn(n_samples, 1) * 0.2 # 添加少量噪声
y = true_w * X + true_b + noise
print("=" * 60)
print("数据生成信息:")
print("=" * 60)
print(f"样本数量: {n_samples}")
print(f"真实关系: y = {true_w:.1f}x + {true_b:.1f}")
print(f"X范围: [{X.min():.2f}, {X.max():.2f}]")
print(f"y范围: [{y.min():.2f}, {y.max():.2f}]")
print(f"添加噪声: 标准差 {noise.std():.3f}")
# ==================== 检查 GPU 可用性 ====================
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"\n使用设备: {device}")
# ==================== 可视化原始数据 ====================
plt.figure(figsize=(12, 4))
plt.subplot(1, 3, 1)
plt.scatter(X.cpu().numpy(), y.cpu().numpy(), alpha=0.5, label='数据点')
plt.plot(X.cpu().numpy(), (true_w * X + true_b).cpu().numpy(),
'r-', linewidth=3, label=f'真实: y={true_w}x+{true_b}')
plt.xlabel('X')
plt.ylabel('y')
plt.title('原始数据 (y=3x+0.8)')
plt.legend()
plt.grid(True, alpha=0.3)
# ==================== 1. 定义模型 ====================
class LinearRegressionModel(nn.Module):
def __init__(self):
super().__init__()
self.linear = nn.Linear(1, 1)
def forward(self, x):
return self.linear(x)
# 创建模型并移到设备
model = LinearRegressionModel().to(device)
print(f"\n模型初始参数:")
print(f"初始权重: {model.linear.weight.item():.4f}")
print(f"初始偏置: {model.linear.bias.item():.4f}")
# ==================== 移动数据到设备 ====================
X_gpu = X.to(device)
y_gpu = y.to(device)
# ==================== 2. 定义损失函数和优化器 ====================
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 添加学习率调度器(可选,让训练更稳定)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=200, gamma=0.9)
# ==================== 3. 训练循环 ====================
print("\n" + "=" * 60)
print("开始训练...")
print("=" * 60)
losses = [] # 记录损失
weights = [] # 记录权重变化
biases = [] # 记录偏置变化
for epoch in range(2000):
# 训练步骤
optimizer.zero_grad()
outputs = model(X_gpu)
loss = criterion(outputs, y_gpu)
loss.backward()
optimizer.step()
scheduler.step() # 更新学习率
# 记录
losses.append(loss.item())
weights.append(model.linear.weight.item())
biases.append(model.linear.bias.item())
# 打印进度
if (epoch + 1) % 200 == 0:
current_lr = scheduler.get_last_lr()[0]
print(f'Epoch [{epoch + 1:4d}/2000], Loss: {loss.item():.6f}, '
f'LR: {current_lr:.5f}, '
f'w: {model.linear.weight.item():.4f}, '
f'b: {model.linear.bias.item():.4f}')
# ==================== 4. 查看训练结果 ====================
print("\n" + "=" * 60)
print("训练完成!")
print("=" * 60)
final_w = model.linear.weight.item()
final_b = model.linear.bias.item()
print(f"\n真实关系: y = {true_w:.4f}x + {true_b:.4f}")
print(f"学习到的: y = {final_w:.4f}x + {final_b:.4f}")
print(f"权重误差: {abs(final_w - true_w):.4f}")
print(f"偏置误差: {abs(final_b - true_b):.4f}")
# ==================== 5. 可视化训练过程 ====================
plt.subplot(1, 3, 2)
plt.plot(losses)
plt.xlabel('Epoch')
plt.ylabel('Loss (MSE)')
plt.title('训练损失曲线')
plt.grid(True, alpha=0.3)
plt.yscale('log') # 对数坐标更清晰
plt.subplot(1, 3, 3)
plt.plot(weights, label='权重 w')
plt.plot(biases, label='偏置 b')
plt.axhline(y=true_w, color='r', linestyle='--', alpha=0.5, label='真实 w')
plt.axhline(y=true_b, color='g', linestyle='--', alpha=0.5, label='真实 b')
plt.xlabel('Epoch')
plt.ylabel('参数值')
plt.title('参数收敛过程')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# ==================== 6. 最终拟合效果可视化 ====================
plt.figure(figsize=(10, 6))
plt.scatter(X.cpu().numpy(), y.cpu().numpy(), alpha=0.5, label='数据点')
# 真实直线
x_range = torch.linspace(X.min(), X.max(), 100).reshape(-1, 1)
y_true = true_w * x_range + true_b
plt.plot(x_range.numpy(), y_true.numpy(), 'r-', linewidth=3,
label=f'真实: y={true_w:.2f}x+{true_b:.2f}')
# 预测直线
model.eval()
with torch.no_grad():
x_range_gpu = x_range.to(device)
y_pred = model(x_range_gpu).cpu()
plt.plot(x_range.numpy(), y_pred.numpy(), 'b--', linewidth=3,
label=f'拟合: y={final_w:.2f}x+{final_b:.2f}')
plt.xlabel('X')
plt.ylabel('y')
plt.title(f'线性回归拟合结果 (y={true_w}x+{true_b})')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
# ==================== 7. 详细参数分析 ====================
print("\n" + "=" * 60)
print("详细参数分析")
print("=" * 60)
# 查看模型的所有参数和梯度
model.train() # 切回训练模式以查看梯度
dummy_loss = criterion(model(X_gpu), y_gpu)
dummy_loss.backward()
for name, param in model.named_parameters():
print(f"\n{name}:")
print(f" 数值: {param.data.cpu().numpy().flatten()}")
print(f" 梯度: {param.grad.cpu().numpy().flatten() if param.grad is not None else 'None'}")
print(f" 是否需要梯度: {param.requires_grad}")
# ==================== 8. 测试预测 ====================
print("\n" + "=" * 60)
print("测试预测")
print("=" * 60)
test_X = torch.tensor([[0.5], [1.0], [1.5], [2.0], [2.5]]).to(device)
model.eval()
with torch.no_grad():
predictions = model(test_X)
print(f"{'输入 x':>10} | {'预测 y':>10} | {'真实 y':>10} | {'误差':>10}")
print("-" * 50)
for i, x in enumerate(test_X.cpu()):
pred = predictions[i].cpu().item()
truth = true_w * x.item() + true_b
error = abs(pred - truth)
print(f"{x.item():10.2f} | {pred:10.4f} | {truth:10.4f} | {error:10.4f}")
print("\n" + "=" * 60)
print("模型训练完成!")
print(f"最终损失: {losses[-1]:.6f}")
print("=" * 60)
============================================================
# 数据生成信息:
# ============================================================
# 样本数量: 100
# 真实关系: y = 3.0x + 0.8
# X范围: [-4.41, 4.44]
# y范围: [-12.35, 13.88]
#
# 添加噪声: 标准差 0.178
#
# 使用设备: cuda
#
# 模型初始参数:
# 初始权重: 0.4801
# 初始偏置: 0.8415
#
# ============================================================
# 开始训练...
# ============================================================
# Epoch [ 200/2000], Loss: 0.031221, LR: 0.00900, w: 3.0011, b: 0.8096
# Epoch [ 400/2000], Loss: 0.031214, LR: 0.00810, w: 3.0012, b: 0.8072
# Epoch [ 600/2000], Loss: 0.031214, LR: 0.00729, w: 3.0012, b: 0.8071
# Epoch [ 800/2000], Loss: 0.031214, LR: 0.00656, w: 3.0012, b: 0.8071
# Epoch [1000/2000], Loss: 0.031214, LR: 0.00590, w: 3.0012, b: 0.8071
# Epoch [1200/2000], Loss: 0.031214, LR: 0.00531, w: 3.0012, b: 0.8071
# Epoch [1400/2000], Loss: 0.031214, LR: 0.00478, w: 3.0012, b: 0.8071
# Epoch [1600/2000], Loss: 0.031214, LR: 0.00430, w: 3.0012, b: 0.8071
# Epoch [1800/2000], Loss: 0.031214, LR: 0.00387, w: 3.0012, b: 0.8071
# Epoch [2000/2000], Loss: 0.031214, LR: 0.00349, w: 3.0012, b: 0.8071
#
# ============================================================
# 训练完成!
# ============================================================
#
# 真实关系: y = 3.0000x + 0.8000
# 学习到的: y = 3.0012x + 0.8071
# 权重误差: 0.0012
# 偏置误差: 0.0071
# ============================================================
# 详细参数分析
# ============================================================
#
# linear.weight:
# 数值: [3.0011792]
# 梯度: [-2.7798116e-05]
# 是否需要梯度: True
#
# linear.bias:
# 数值: [0.8071371]
# 梯度: [8.128583e-06]
# 是否需要梯度: True
#
# ============================================================
# 测试预测
# ============================================================
# 输入 x | 预测 y | 真实 y | 误差
# --------------------------------------------------
# 0.50 | 2.3077 | 2.3000 | 0.0077
# 1.00 | 3.8083 | 3.8000 | 0.0083
# 1.50 | 5.3089 | 5.3000 | 0.0089
# 2.00 | 6.8095 | 6.8000 | 0.0095
# 2.50 | 8.3101 | 8.3000 | 0.0101
#
# ============================================================
# 模型训练完成!
# 最终损失: 0.031214
# ============================================================


四、 核心总结:PyTorch 的“五步心法”
不管你将来写的是简单的线性回归,还是最新的 ChatGPT (Transformer) 架构,其训练核心代码永远只有这五步。请把这段代码刻在 DNA 里:
# 1. 梯度清零:打扫战场,准备下一轮
optimizer.zero_grad()
# 2. 前向传播:模型根据输入做出预测
output = model(input)
# 3. 计算损失:看看预测错得有多离谱
loss = loss_fn(output, target)
# 4. 反向传播:把责任(梯度)分摊给每个参数
loss.backward()
# 5. 优化更新:参数根据梯度修正自己
optimizer.step()
五、 避坑与进阶建议
- Shape Mismatch (维度地狱):
- 90% 的 PyTorch 报错都是因为矩阵维度对不上。
- 干货技巧:在
forward函数里,多插几句print(x.shape),调试时极其有用。不要瞎猜,要看形状。
- Dataset 与 DataLoader:
- 实际项目中,数据不是一次性塞进内存的(显存会爆)。
- 你需要学习
torch.utils.data.DataLoader,它能帮你把数据切成小块(Batch),并在 GPU 训练的同时,利用 CPU 多进程预读取下一批数据。这是工业级训练的标配。
- Debug 技巧:
- 如果你发现 Loss 不下降,或者变成
NaN,先检查学习率是不是太大了,再检查数据里有没有无穷大值,最后检查是不是忘了optimizer.zero_grad()。
- 如果你发现 Loss 不下降,或者变成
六、 结语:这才是 AI 的起点
恭喜你!学完这一篇,你才算真正拿到了进入 AI 殿堂的钥匙。
NumPy 让你理解了矩阵,Autograd 让你理解了学习,PyTorch 让你拥有了算力。
现在的你,已经具备了复现经典算法的能力。
下一阶段预告
我们将带着 PyTorch 这把屠龙刀,进入 【机器学习基础】。
我们不再手写 y=wx+by=wx+by=wx+b,而是要去挑战经典的分类问题:逻辑回归、决策树、SVM。我们将看看 AI 是如何把花哨的数据分类得井井有条的。
作业:
尝试修改上面的“高阶实现”代码,把 nn.Linear(1, 1) 改成一个包含隐藏层的神经网络(比如 nn.Sequential(nn.Linear(1, 10), nn.ReLU(), nn.Linear(10, 1))),去拟合一个非线性函数(比如 y=x2y=x^2y=x2)。体会一下深度学习的威力。
更多推荐

所有评论(0)