一、开篇:为什么是 PyTorch

一句话点睛:PyTorch = NumPy 一样好写 + 自动求导 + GPU 加速。

PyTorch 的三大优势:

  • 动态图 —— 代码怎么写,图就怎么建,支持原生 if/elsefor,调试像普通 Python 一样
  • 学术主流 —— 顶会论文开源代码 90% 用 PyTorch,复现门槛低
  • Python 生态无缝 —— 和 NumPy、Matplotlib、Scikit-learn 无缝集成

本文目标:用 PyTorch 写一个神经网络,拟合 y = x 2 y = x^2 y=x2,把整个流程跑通。

读完本文你能掌握:

  • uv 5 分钟装好 PyTorch
  • Tensor 的核心操作
  • 4 个最常用的激活函数
  • nn.Module 搭建神经网络的标配套路
  • 标准训练循环模板(Autograd 部分下一篇再讲)

📝 阅读路线
本文是 PyTorch 系列第 1 篇,注重「能跑通」。下一篇会专门讲自动微分(Autograd)和反向传播——也就是 loss.backward() 这一行的魔法来自哪里。


二、环境安装:用 uv 5 分钟搞定

2.1 为什么用 uv 而不是 conda?

如果你还在用 conda 装 PyTorch,强烈建议你试试 uv。它是一个用 Rust 写的极速 Python 包管理器,相比 pip/conda:

维度 pip conda uv
安装速度 很慢 极快(10~100x)
磁盘占用 极小
锁文件 需 pipenv/poetry 原生支持 uv.lock
Python 版本管理 内置 内置

装一次就回不去。

2.2 安装 uv

可以参考我的笔记:uv学习笔记

# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows (PowerShell)
irm https://astral.sh/uv/install.ps1 | iex

# 使用 pip 安装
pip install uv

# 验证安装成功
uv --version

2.3 创建项目 + 虚拟环境

uv init pytorch-quickstart
cd pytorch-quickstart
uv venv

# 激活虚拟环境
# macOS / Linux:
source .venv/bin/activate
# Windows:
.venv\Scripts\activate

💡 为什么要虚拟环境?
不同项目用不同版本的 PyTorch(甚至 Python),不隔离就会冲突。uv venv 创建的 .venv 目录是项目本地的,删了就能干净地重装。

2.4 安装 PyTorch

先去 PyTorch 官网 选你的配置,下面对应最常见的两种情况。

CPU 版(新手推荐,先跑通再说):

uv pip install torch torchvision torchaudio

GPU 版(有 NVIDIA 显卡、追求速度):

先看你的 CUDA 驱动版本:

nvidia-smi    # 右上角 "CUDA Version: 12.x" 就是支持的最高版本

比如你的驱动支持 CUDA 12.1:

uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121

如果想用 uv add 命令去安装的话,你需要把你的 pyproject.toml 文件改为下面的样子,然后直接去 uv add torch torchvision torchaudio。注意,cu126 按照你的gpu可以换成合适的版本,比如我的显卡是 5080,那么 cu126 就会报错,无法使用 gpu 来进行加速,但我换成了 cu130 就不会报错了。

[project]
name = "nlp"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "torch",
    "torchvision",
    "torchaudio",
]

[[tool.uv.index]]
name = "pytorch-gpu"
url = "https://download.pytorch.org/whl/cu126"
explicit = true  # 关键:这确保只有明确指定该索引的包才去这里找

# 强制指定这三个包去 pytorch-gpu 索引找(这步最关键)
[tool.uv.sources]
torch = { index = "pytorch-gpu" }
torchvision = { index = "pytorch-gpu" }
torchaudio = { index = "pytorch-gpu" }

⚠️ CUDA 版本对应关系
装的 PyTorch 自带的 CUDA 不能超过 nvidia-smi 显示的版本。比如显示 12.4,可以装 cu121、cu124,但不能装 cu118(11.8 低于 12.4 反而没事,但 cu118 时代太久了没必要)。装错版本会报 libcudart.so not found

2.5 验证安装

import torch

print("PyTorch 版本:", torch.__version__)
print("CUDA 是否可用:", torch.cuda.is_available())

if torch.cuda.is_available():
    print("当前 GPU:", torch.cuda.get_device_name(0))

💡 国内下载慢?
临时换清华源:

uv pip install torch --index-url https://pypi.tuna.tsinghua.edu.cn/simple

永久配置可在 ~/.config/pip/pip.conf(Linux/Mac)或 %APPDATA%\pip\pip.ini(Windows)里加。


三、Tensor:深度学习的「乐高积木」

如果把神经网络比作一栋房子,Tensor 就是砖块。PyTorch 的一切运算,本质上都是 Tensor 之间的操作。

3.1 什么是 Tensor

一句话:Tensor = NumPy ndarray + GPU 支持 + 自动求导支持

import torch
import numpy as np

# NumPy 数组
a = np.array([1, 2, 3])

# PyTorch 张量
b = torch.tensor([1, 2, 3])

print(type(a), type(b))   # <class 'numpy.ndarray'> <class 'torch.Tensor'>

Tensor 的「维度」从 0 开始数:

维度 名称 例子 常见用途
0 维 标量(scalar) tensor(3.14) 损失值
1 维 向量(vector) tensor([1, 2, 3]) 偏置、一条样本的特征
2 维 矩阵(matrix) tensor([[1, 2], [3, 4]]) 一批样本的特征
3 维 张量 torch.randn(2, 3, 4) 序列(batch × seq_len × hidden)
4 维 张量 torch.randn(2, 3, 28, 28) 图像(batch × channel × H × W)

3.2 创建 Tensor 的 5 种姿势

import torch

# 姿势 1: 从 Python 列表创建
t1 = torch.tensor([1, 2, 3])

# 姿势 2: 创建全 0 / 全 1 / 未初始化
t2 = torch.zeros(2, 3)              # 2x3 全 0
t3 = torch.ones(2, 3)               # 2x3 全 1
t4 = torch.empty(2, 3)              # 2x3 未初始化(值是内存里残留的垃圾,速度快)

# 姿势 3: 创建等差 / 等距序列
t5 = torch.arange(0, 10, 2)         # tensor([0, 2, 4, 6, 8])
t6 = torch.linspace(0, 1, 5)        # tensor([0.00, 0.25, 0.50, 0.75, 1.00])

# 姿势 4: 随机初始化
t7 = torch.rand(2, 3)               # 均匀分布 [0, 1)
t8 = torch.randn(2, 3)              # 标准正态分布 N(0, 1)

# 姿势 5: 从 NumPy 转过来
import numpy as np
arr = np.array([1, 2, 3])
t9 = torch.from_numpy(arr)          # 共享内存!改一个另一个也变

⚠️ torch.empty 不是 torch.zeros

empty 不初始化数据,速度比 zeros 快。如果你打算立刻覆盖它的值,用 empty 性能更好;否则用 zeros 避免读到垃圾值。

3.3 Tensor 的常用属性

import torch

t = torch.randn(2, 3, 4)            # shape: (2, 3, 4)
print(t.shape)            # torch.Size([2, 3, 4])  ← 形状
print(t.ndim)             # 3                       ← 维度数
print(t.numel())          # 24                      ← 元素总数
print(t.dtype)            # torch.float32           ← 数据类型
print(t.device)           # cpu                     ← 所在设备
print(t.element_size())   # 4                       ← 每个元素占多少字节
print(t.itemsize)         # 4                       ← 等价于 element_size()
print(t.nbytes)           # 96                      ← 总字节数 = numel * element_size
print(t.requires_grad)    # False                   ← 是否需要梯度

💡 常用属性速查

属性 含义 典型用途
shape / size() 形状(每维的大小) 维度检查、reshape 前
ndim / dim() 维度数 快速判断几维 tensor
numel() / nelement() 元素总数 统计参数量、算 FLOPs
dtype 数据类型 精度控制、类型转换
device 所在设备 多卡训练、设备迁移
element_size() 单元素字节数 估算显存占用
nbytes 总字节数 = numel() * element_size()
requires_grad 是否开启梯度追踪 冻结参数、推理
is_leaf 是否为叶子节点 下一篇 Autograd 会细讲

3.4 索引与切片

下面的所有例子都用这个 3×3 矩阵:

import torch

t1 = torch.tensor([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
])

print(t1.shape, t1.ndim)    # torch.Size([3, 3]) 3
普通索引

tensor[行, 列] 取单个元素:

print(t1[0, 0])    # tensor(1)        # 第 0 行第 0 列
print(t1[1, 2])    # tensor(6)        # 第 1 行第 2 列
print(t1[-1, -1])  # tensor(9)        # 负索引从后往前数
范围索引(切片)

start:stop:step 切出子矩阵:

print(t1[:2, :2])
# tensor([[1, 2],
#         [4, 5]])

print(t1[1:, 1:])                      # 右下角 2×2
# tensor([[5, 6],
#         [8, 9]])

print(t1[::2, ::2])                    # 步长为 2
# tensor([[1, 3],
#         [7, 9]])
列表索引(花式索引)

用整数列表同时取多个不相邻的元素——这是和列表最大的区别:

# 配对索引:取 (0,0)、(1,1)、(2,2) 三个对角线元素
print(t1[[0, 1, 2], [0, 1, 2]])
# tensor([1, 5, 9])

💡 花式索引的「配对」规则
上面 [[0,1,2], [0,1,2]] 不是「先取行再取列」,而是把两个列表逐元素配对成坐标:(0,0)(1,1)(2,2),结果是一个 1D 张量。

列表与普通索引混用

列表索引可以和切片/标量混用,维度不同,会触发广播机制

# 取第 0、1、2 行的「第 0 列」
print(t1[[0, 1, 2], 0]) 
# tensor([1, 4, 7])
布尔索引

用比较运算生成布尔 mask,再筛选元素:

print(t1 > 5)
# tensor([[False, False, False],
#         [False, False,  True],
#         [ True,  True,  True]])

print(t1[t1 > 5])
# tensor([6, 7, 8, 9])

布尔索引的杀手级用法:一行就能完成「按条件赋值」:

# 把所有 > 5 的元素都改成 0
t1[t1 > 5] = 0
print(t1)
# tensor([[1, 2, 3],
#         [4, 5, 0],
#         [0, 0, 0]])

3.6 广播机制

两个形状不同的 tensor 也能做运算——小的那个会被「虚拟」地扩展到匹配大的那个:

a = torch.ones(3, 4)        # shape: (3, 4)
b = torch.tensor([10, 20, 30, 40])  # shape: (4,)

c = a + b                   # 广播后 b 变成 (3, 4)
print(c)
# tensor([[11., 21., 31., 41.],
#         [11., 21., 31., 41.],
#         [11., 21., 31., 41.]])

广播规则:从最后一个维度往前比对,每个维度要么相等、要么其中一个是 1、要么其中一个不存在。满足就广播,不满足就报错。

3.7 常用数学运算

a = torch.tensor([1.0, 2.0, 3.0])
b = torch.tensor([4.0, 5.0, 6.0])

# 逐元素运算
a + b            # 加
a * b            # 逐元素乘(注意:不是矩阵乘!)
a ** 2           # 平方

# 矩阵乘
A = torch.randn(2, 3)
B = torch.randn(3, 4)
C = A @ B                    # shape: (2, 4)
C2 = A.matmul(B)             # 等价写法
C3 = torch.mm(A, B)          # 等价写法(只支持 2D)

# 聚合运算
t = torch.tensor([[1.0, 2.0], [3.0, 4.0]])

t.sum()                       # 全部求和: tensor(10.)
t.sum(dim=0)                  # 沿第 0 维求和: tensor([4., 6.])
t.sum(dim=1)                  # 沿第 1 维求和: tensor([3., 7.])
t.mean()                      # 平均: tensor(2.5000)
t.mean(dim=0)                 # 沿第 0 维平均: tensor([2., 3.])
t.max()                       # 最大值
t.argmax()                    # 最大值的索引

💡 dim 怎么记
dim=0 沿着「第 0 维的方向」——把行压掉,留下列的统计。dim=1 同理压掉列。dim 永远是「被消除的维度」。

3.8 与 NumPy 互转

# Tensor → NumPy
t = torch.ones(3)
arr = t.numpy()
print(type(arr))   # <class 'numpy.ndarray'>

# NumPy → Tensor
arr2 = np.array([1, 2, 3])
t2 = torch.from_numpy(arr2)

⚠️ 共享内存陷阱
torch.from_numpy()tensor.numpy() 转换出来的对象共享底层内存。修改一个,另一个也跟着变!如果要断开共享,用 .clone()

t2 = torch.from_numpy(arr2).clone()    # 独立副本

3.9 搬上 GPU

一行代码搞定:

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

x = torch.rand(2, 3)
x = x.to(device)                 # ← CPU → GPU
y = torch.rand(2, 3, device=device)   # 创建时直接指定

⚠️ GPU 上的 Tensor 不能直接转 NumPy
报错:TypeError: can't convert cuda:0 device type tensor to numpy

对策:先 .cpu().numpy(),或先 .detach() 再转。

x.cpu().numpy()                # 先搬回 CPU

四、激活函数:给网络注入「非线性」

4.1 为什么需要激活函数?

先想一个问题:没有激活函数,多层神经网络和一层等价吗?

答案是等价。假设每层都是 y = W x + b y = Wx + b y=Wx+b,三层堆起来就是 y = W 3 W 2 W 1 x + … y = W_3 W_2 W_1 x + \dots y=W3W2W1x+——本质上还是线性变换,表达能力极差。

激活函数就是给每一层「加点弯弯绕绕」,让网络能拟合任意复杂的函数(万能逼近定理)。

4.2 4 个最常用的激活函数

4.2.1 ReLU(隐藏层默认首选)

ReLU ( x ) = max ⁡ ( 0 , x ) \text{ReLU}(x) = \max(0, x) ReLU(x)=max(0,x)

relu = torch.nn.ReLU()
x = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])
print(relu(x))     # tensor([0., 0., 0., 1., 2.])

特点

  • 计算极快(就是比大小)
  • 梯度不会饱和(正区间梯度恒为 1,缓解梯度消失)
  • 死亡 ReLU 问题:负区间梯度恒为 0,对应神经元「死掉」再也不更新

💡 ReLU 的小变种

  • nn.LeakyReLU(0.01):负区间给个小的正斜率
  • nn.PReLU():斜率也是可学习参数
  • nn.GELU():Transformer 标配,曲线更平滑
4.2.2 Sigmoid(二分类输出层)

σ ( x ) = 1 1 + e − x \sigma(x) = \frac{1}{1 + e^{-x}} σ(x)=1+ex1

sigmoid = torch.nn.Sigmoid()
print(sigmoid(torch.tensor([0.0])))   # tensor([0.5000])

特点

  • 输出范围 ( 0 , 1 ) (0, 1) (0,1),天然适合表示「概率」
  • 严重缺点:两端梯度接近 0,导致梯度消失——深层网络基本不用它做隐藏层
  • 隐藏层几乎不用,只在二分类输出层偶尔使用
4.2.3 Tanh(零中心化的 Sigmoid)

tanh ⁡ ( x ) = e x − e − x e x + e − x \tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} tanh(x)=ex+exexex

tanh = torch.nn.Tanh()
print(tanh(torch.tensor([0.0])))    # tensor([0.])

特点

  • 输出范围 ( − 1 , 1 ) (-1, 1) (1,1)零中心(比 Sigmoid 训练更稳定)
  • 同样存在梯度消失
  • RNN 类模型偶尔用,主战场已让给 ReLU
4.2.4 Softmax(多分类输出层)

Softmax ( x i ) = e x i ∑ j e x j \text{Softmax}(x_i) = \frac{e^{x_i}}{\sum_{j} e^{x_j}} Softmax(xi)=jexjexi

softmax = torch.nn.Softmax(dim=1)
logits = torch.tensor([[1.0, 2.0, 3.0]])
print(softmax(logits))
# tensor([[0.0900, 0.2447, 0.6652]])

特点

  • 把一组数归一化成概率分布(和为 1)
  • nn.CrossEntropyLoss 使用时,模型里不要再加 Softmax(loss 内部自带)

4.3 选型速查表

函数 公式 输出范围 适用位置 备注
ReLU max ⁡ ( 0 , x ) \max(0, x) max(0,x) [ 0 , + ∞ ) [0, +\infty) [0,+) 隐藏层默认 简单、高效
LeakyReLU max ⁡ ( 0.01 x , x ) \max(0.01x, x) max(0.01x,x) ( − ∞ , + ∞ ) (-\infty, +\infty) (,+) 隐藏层 解决死亡 ReLU
GELU x ⋅ Φ ( x ) x \cdot \Phi(x) xΦ(x) ( − ∞ , + ∞ ) (-\infty, +\infty) (,+) Transformer 隐藏层 平滑、效果好
Sigmoid 1 1 + e − x \frac{1}{1+e^{-x}} 1+ex1 ( 0 , 1 ) (0, 1) (0,1) 二分类输出层 隐藏层禁用
Tanh e x − e − x e x + e − x \frac{e^x-e^{-x}}{e^x+e^{-x}} ex+exexex ( − 1 , 1 ) (-1, 1) (1,1) RNN 隐藏层 零中心
Softmax e x i ∑ e x j \frac{e^{x_i}}{\sum e^{x_j}} exjexi ( 0 , 1 ) (0, 1) (0,1) 和为 1 多分类输出层 配 CrossEntropyLoss

五、搭建神经网络:torch.nn 模块

5.1 nn.Module:所有模型的「祖宗」

PyTorch 里所有神经网络都继承自 nn.Module。它要做的两件事:

  1. __init__声明用哪些层
  2. forward定义数据怎么流过这些层
import torch
import torch.nn as nn

class MyNet(nn.Module):
    def __init__(self):
        super().__init__()                  # ← 必须!初始化父类
        self.fc1 = nn.Linear(784, 128)      # 输入层 → 隐藏层
        self.fc2 = nn.Linear(128, 64)       # 隐藏层 → 隐藏层
        self.fc3 = nn.Linear(64, 10)        # 隐藏层 → 输出层
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.relu(self.fc1(x))
        x = self.relu(self.fc2(x))
        x = self.fc3(x)                     # 输出层一般不加激活(交给 loss 处理)
        return x

model = MyNet()
print(model)
# MyNet(
#   (fc1): Linear(in_features=784, out_features=128, bias=True)
#   (fc2): Linear(in_features=128, out_features=64, bias=True)
#   (fc3): Linear(in_features=64, out_features=10, bias=True)
#   (relu): ReLU()
# )

5.2 简单模型用 nn.Sequential

如果你的网络是层与层顺序串接(没有分支、跳跃连接),可以用 nn.Sequential 一行搞定:

model = nn.Sequential(
    nn.Linear(784, 128),
    nn.ReLU(),
    nn.Linear(128, 64),
    nn.ReLU(),
    nn.Linear(64, 10),
)

5.3 常用层速查

# 全连接层
nn.Linear(in_features, out_features)

# 2D 卷积(图像)
nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0)

# 池化(下采样)
nn.MaxPool2d(kernel_size=2)
nn.AvgPool2d(kernel_size=2)

# 循环神经网络(序列)
nn.RNN(input_size, hidden_size, num_layers=1)
nn.LSTM(input_size, hidden_size, num_layers=1)
nn.GRU(input_size, hidden_size, num_layers=1)

# 正则化
nn.Dropout(p=0.5)                # 训练时随机失活,推理时自动关闭
nn.BatchNorm1d(num_features)     # 1D 批归一化(全连接后)
nn.BatchNorm2d(num_features)     # 2D 批归一化(卷积后)

# 展平
nn.Flatten()                     # 把 (B, C, H, W) → (B, C*H*W)

5.4 损失函数速查

损失函数衡量「模型预测值」和「真实值」的差距——训练的目标就是让这个差距越来越小。

# 回归任务:预测连续值
nn.MSELoss()                    # 均方误差,最常用

# 多分类任务:类别互斥(如手写数字 0~9)
nn.CrossEntropyLoss()           # 内部 = LogSoftmax + NLLLoss,**输入是 logits(不加 Softmax)**

# 二分类任务:是 / 不是
nn.BCEWithLogitsLoss()          # 内部 = Sigmoid + BCELoss,**输入是 logits**,比单独组合更稳定

# 兼容性更好的版本(输出已经是概率时)
nn.BCELoss()                    # 二分类交叉熵,输入需先过 Sigmoid
nn.NLLLoss()                    # 负对数似然,输入需先过 LogSoftmax

⚠️ 配对关系

  • nn.CrossEntropyLoss原始 logits(网络最后输出层不要加 Softmax)
  • nn.BCEWithLogitsLoss原始 logits(网络最后输出层不要加 Sigmoid)
  • 这两个「WithLogits」版本数值更稳定,是 PyTorch 官方推荐写法

5.5 优化器速查

优化器决定「拿到梯度后,参数怎么更新」

import torch.optim as optim

# 最经典:随机梯度下降
optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

# 默认首选:Adam(自适应学习率,大多数场景直接用就行)
optim.Adam(model.parameters(), lr=1e-3)

# Transformer 时代标配:AdamW(Adam + 正确的权重衰减)
optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-2)

💡 三个关键超参

  • lr(学习率):最敏感的超参。太大学不到东西(loss 震荡),太小学得太慢。一般从 1e-3 试起。
  • momentum(动量):SGD 专属,常见 0.9。Adam 不需要(内部已经处理了)。
  • weight_decay(权重衰减):L2 正则的强度,防过拟合。常用 1e-4 ~ 1e-2

5.6 设备迁移:把模型也搬上 GPU

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 模型搬上 GPU
model = MyNet().to(device)

# 数据搬上 GPU(必须在同一个设备上才能运算)
x = x.to(device)
y = y.to(device)

# 现在可以正常用了
pred = model(x)
loss = loss_fn(pred, y)

⚠️ 设备必须对齐
模型在 GPU、数据在 CPU → 报错。模型在 CPU、数据在 GPU → 报错。整个训练 pipeline 上的所有 tensor 都必须在同一个 device 上


六、跑通第一个神经网络:拟合 y = x 2 y = x^2 y=x2

理论说了一堆,是时候写个完整可运行的例子了。

6.1 目标

用 PyTorch 搭一个 3 层 MLP(多层感知机),学习函数:

y = x 2 , x ∈ [ − 1 , 1 ] y = x^2, \quad x \in [-1, 1] y=x2,x[1,1]

不依赖任何数据集——我们自己造数据,零干扰,专注模型搭建本身。

6.2 完整代码

新建 fit_x2.py

import torch
import torch.nn as nn
import matplotlib.pyplot as plt

# 解决中文乱码问题
matplotlib.rcParams['font.sans-serif'] = ['SimHei']  # 设置中文字体(黑体)
matplotlib.rcParams['axes.unicode_minus'] = False     # 解决负号显示为方块的问题

# ============== 1. 准备数据 ==============
# 真实的函数关系: y = x^2
x_train = torch.linspace(-1, 1, 200).reshape(-1, 1)  # shape: (200, 1)
y_train = x_train ** 2                              # shape: (200, 1)

# ============== 2. 定义模型 ==============
class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(1, 64),
            nn.Tanh(),                              # 隐藏层用 Tanh 拟合平滑曲线
            nn.Linear(64, 64),
            nn.Tanh(),
            nn.Linear(64, 1),
        )

    def forward(self, x):
        return self.net(x)

model = MLP()
print("模型结构:")
print(model)

# ============== 3. 损失函数 + 优化器 ==============
loss_fn = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

# ============== 4. 训练循环 ==============
losses = []
epochs = 3000
for epoch in range(epochs):
    # 前向传播
    pred = model(x_train)
    loss = loss_fn(pred, y_train)

    # 反向传播 + 参数更新(下一篇详解)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    losses.append(loss.item())

    if (epoch + 1) % 500 == 0:
        print(f"Epoch {epoch+1:4d} | loss = {loss.item():.6f}")

# ============== 5. 可视化结果 ==============
with torch.no_grad():
    y_pred = model(x_train)

plt.figure(figsize=(10, 4))

# 左图:拟合效果
plt.subplot(1, 2, 1)
plt.scatter(x_train.numpy(), y_train.numpy(), s=10, alpha=0.5, label="True (y = x²)")
plt.plot(x_train.numpy(), y_pred.numpy(), 'r-', lw=2, label="MLP prediction")
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.title("MLP 拟合 y = x²")
plt.grid(True, alpha=0.3)

# 右图:loss 曲线
plt.subplot(1, 2, 2)
plt.plot(losses)
plt.xlabel("Epoch")
plt.ylabel("MSE Loss")
plt.title("Training Loss")
plt.yscale("log")
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig("fit_x2.png", dpi=120, bbox_inches="tight")
print("已保存 fit_x2.png")

6.3 跑起来

uv run python fit_x2.py

预期输出:

模型结构:
MLP(
  (net): Sequential(
    (0): Linear(in_features=1, out_features=64, bias=True)
    (1): Tanh()
    (2): Linear(in_features=64, out_features=64, bias=True)
    (3): Tanh()
    (4): Linear(in_features=64, out_features=1, bias=True)
  )
)
Epoch  500 | loss = 0.001234
Epoch 1000 | loss = 0.000123
Epoch 1500 | loss = 0.000045
Epoch 2000 | loss = 0.000023
Epoch 2500 | loss = 0.000014
Epoch 3000 | loss = 0.000009
已保存 fit_x2.png

最终结果如图,左图为拟合效果,右图为 loss 曲线。
在这里插入图片描述

七、小结 & 下一步

一张图总结

[uv 安装 PyTorch]
       ↓
[Tensor 基础操作] ─── 乐高积木
       ↓
[激活函数] ─── 引入非线性
       ↓
[nn.Module 搭网络] ─── 把积木拼成房子
       ↓
[训练循环 5 行] ─── 房子不断修缮
       ↓
[第一个 MLP 跑通!]

本文关键 API 速记

类别 API 一句话
创建 torch.tensor() / zeros() / randn() 造数据
形状 tensor.shape / reshape() / view() 维度变换
设备 tensor.to(device) CPU ↔ GPU
模型 class Net(nn.Module): def forward(self, x) 搭网络
损失 nn.MSELoss() / CrossEntropyLoss() 衡量差距
优化 torch.optim.Adam(model.parameters(), lr=1e-3) 更新参数

推荐下一步阅读

如果本文对你有帮助,欢迎点赞、收藏、转发 👍

有任何问题,评论区见!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐