AI基石 | Python工具链(三):PyTorch入门 —— 炼丹炉已开,把 NumPy 扔进显卡里!

前言

在上一篇,我们手握 NumPy 和 Pandas,觉得自己已经能处理全世界的数据了。

但当你试图用 NumPy 手写一个神经网络时,你会撞上两堵墙:

  1. 算不动:矩阵一旦超过 10000 维,CPU 就开始发烫降频,NumPy 对此无能为力。
  2. 算不准:还记得微积分里的“链式法则”吗?如果神经网络有 100 层,手推导数公式会让你崩溃。只要写错一个符号,整个模型就废了。

今天,我们请出 AI 界的“屠龙刀” —— PyTorch。

它由 Meta (Facebook) 开源,它做对了最关键的两件事:

  1. 把张量搬到了 GPU 上(解决了“算不动”)。
  2. 实现了自动微分系统 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 这种我们自己创建的变量,才有梯度。像 yz 这种中间运算结果,梯度在反向传播后会被释放以节省内存。
  • 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()

五、 避坑与进阶建议

  1. Shape Mismatch (维度地狱)
    • 90% 的 PyTorch 报错都是因为矩阵维度对不上。
    • 干货技巧:在 forward 函数里,多插几句 print(x.shape),调试时极其有用。不要瞎猜,要看形状。
  2. Dataset 与 DataLoader
    • 实际项目中,数据不是一次性塞进内存的(显存会爆)。
    • 你需要学习 torch.utils.data.DataLoader,它能帮你把数据切成小块(Batch),并在 GPU 训练的同时,利用 CPU 多进程预读取下一批数据。这是工业级训练的标配。
  3. Debug 技巧
    • 如果你发现 Loss 不下降,或者变成 NaN,先检查学习率是不是太大了,再检查数据里有没有无穷大值,最后检查是不是忘了 optimizer.zero_grad()

六、 结语:这才是 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)。体会一下深度学习的威力。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐