你打开这个系列,正兴致勃勃读着概念,代码里突然蹦出这么一行:
Q = self.W_q(x).view(batch, seq_len, self.num_heads, self.d_k).transpose(1, 2)每个字母都认识,连起来完全不知道在干嘛。如果你和我一样对 PyTorch 不熟——这行就是拦路虎,而且它还会在后面几十篇里反复出现。
一句话定位:PyTorch 是一个深度学习框架——你只管把网络像搭积木一样拼出来,它帮你自动算"每个权重该怎么调"的梯度。
这篇是系列的第零站。它不教 AI 概念(那是后面 35 篇的活),只教你读懂那些代码需要的 PyTorch 最小语法集。读完你会知道:数据怎么装进张量、形状怎么变、网络怎么搭成一个类、训练循环怎么转。其中重点是「张量形状操作」——它是全系列代码里被用得最多、却从没人解释、最容易卡死人的部分。
怎么用这篇:读系列前花一刻钟过一遍建立全局印象;后面任何一篇代码看不懂时,回来查对应那块就行。
1. 张量 Tensor —— 数字容器
PyTorch 里几乎一切数据都是「张量」(Tensor)。张量就是多维数组——一维的叫向量、二维的叫矩阵、三维及以上统称张量。你可以把它想成一个装数字的、形状可变的盒子。
1.1 创建张量
import torch # 导入 PyTorch 库,后续所有 torch.xxx 都来自它
# torch.tensor:把普通 Python 列表变成张量
x = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) # 二维 → 一个 2×2 矩阵
# 下面几个是系列代码里几乎每篇都用的"造数据"函数
a = torch.randn(2, 3) # 标准(正态)分布的随机数,形状 2×3——造假数据最常用
z = torch.zeros(3) # 全 0,形状 3
r = torch.arange(0, 6) # 0,1,2,3,4,5关键:维度是层层嵌套的。看这个阶梯,每一级就是「多套」上一级结构:
torch.tensor(5) # 0 维:单个数(标量) shape []
torch.tensor([1, 2, 3]) # 1 维:一排数(向量) shape [3]
torch.tensor([[1, 2], # 2 维:一张表(矩阵) shape [2, 2]
[3, 4]])
torch.tensor([[[1, 2], # 3 维:两张 2×2 表摞起来 shape [2, 2, 2]
[3, 4]],
[[5, 6],
[7, 8]]])心智模型:每多一个维度,就是「多套」上一级结构——2 维 = 向量排成表;3 维 = 表摞成一摞;4 维 = 几摞表叠一起。一条不易记错的规则:最里面两维是一张「表」(矩阵),前面几个维度乘起来,决定有几张表。所以 3 维 [2, 3, 4] = 最内 3×4 一张表、前面 2 → 共 2 张 3×4 表;4 维 (1, 2, 4, 4) = 最内 4×4 一张表、前面 1×2 → 共 2 张 4×4 表(验算:一张 4×4 是 16 个数,2 张 = 32,正好等于 1×2×4×4;对应拆头里 2 个头,每个头一张表)。别被多层方括号吓到,它只是"多套了几层"——这个模型是后面读懂拆头(4 维)的钥匙。
新手第一个坑:神经网络只认小数(浮点数)。写
torch.tensor([1, 2])默认是整数类型,喂给网络常常报错;要写成[1.0, 2.0]带小数点。
1.2 看张量:shape 和 dtype
先区分两件最容易混的事——张量的「内容」和「形状」:
torch.manual_seed(42) # 固定随机种子,让下面这堆随机数每次都一样(方便你对照)
x = torch.randn(3, 4) # 造一个 3×4 的随机张量
print(x)
# tensor([[ 0.3367, 0.1288, 0.2345, 0.2303],
# [-1.1229, -0.1863, 2.2082, -0.6380],
# [ 0.4617, 0.2674, 0.5349, 0.8094]])
print(x.shape)
# torch.Size([3, 4])x的内容是 12 个随机数(上面那堆)。x.shape返回的[3, 4]只是在告诉你这 12 个数怎么排列——摆成 3 行 4 列。
新手最容易栽的坑:把
[3, 4]当成「张量里装了 3 和 4 两个数」。错——它是「第 0 维 = 3、第 1 维 = 4」的尺码标签,不是数据。就像快递箱上贴的「30×20×15 cm」:那三个数是箱子尺寸,不是箱子里的东西。对 2 维张量,[3, 4]就是 3 行 4 列的矩阵;方括号只是 PyTorch 显示形状时的写法,别被它误导成「数组里有两个元素」。
print(x.dtype) # torch.float32 —— 数据类型(小数类型)shape 是你调试时最常打的东西——「这个张量长什么样」。读代码遇到看不懂的运算,第一步永远是 print 出各个张量的 shape,对上了就懂了一大半。
1.3 负维度索引(系列高频盲区)
x = torch.arange(24, dtype=torch.float).view(2, 3, 4) # 0~23 摆成 (2,3,4),shape [2, 3, 4]
# 维度编号:第 0 维=2,第 1 维=3,第 2 维=4
# 负号 = 从后往前数:-1 是最后一维(4),-2 是倒数第二维(3)
x.sum(dim=-1) # 沿最后一维求和——把每组 4 个数加起来
# tensor([[ 6., 22., 38.],
# [54., 70., 86.]]) # 结果 shape [2, 3](最后一维 4 被"求和消掉了")为什么要负维度?因为代码常想表达「沿最后一个维度操作」,用 dim=-1 比数清楚是第几维省心得多。系列里 F.softmax(x, dim=-1)、x.transpose(-2, -1) 都是这意思——「最后一维」或「倒数两维」。记住:-1 永远指最后一维。
2. 张量形状操作 ★重点
这是全系列代码里最高频、却最没人讲、最容易卡死人的一块。前面说了张量有 shape,而很多运算的本质就是「改变或对齐 shape」。下面 5 个操作是基础,最后用一个真实的拆头案例把它们串起来。
2.1 改形状不挪数据:view / reshape / contiguous
x = torch.arange(12) # 一维:[0,1,2,...,11],shape [12]
y = x.view(3, 4) # view 改"看法",摆成 3 行 4 列矩阵:
# [[ 0, 1, 2, 3],
# [ 4, 5, 6, 7],
# [ 8, 9, 10, 11]]
z = x.reshape(2, 6) # reshape 更通用(连"内存不连续"也能改),摆成 2 行 6 列:
# [[ 0, 1, 2, 3, 4, 5],
# [ 6, 7, 8, 9, 10, 11]]
# view 要求张量在内存里连续;不连续时会报错,这时先 .contiguous() 再 view
out = x.contiguous().view(3, 4)
# 多维也一样:把一维数据摆成更高维——这正是后面拆头 view(batch, 词, 头, 维) 的简化版
m = torch.arange(24) # shape [24]
n = m.view(2, 3, 4) # 摆成 (2, 3, 4) = 2 张 3×4 矩阵(用上面的"摞矩阵"模型去想):
# n[0] = [[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]]
# n[1] = [[12, 13, 14, 15], [16, 17, 18, 19], [20, 21, 22, 23]]关键认知:view / reshape 改的是「形状」(怎么看这些数),不改变数据本身、也不改变元素总数(12 个数怎么摆都是 12 个)。括号里的数字之积必须等于元素总数;可以写一个 -1 让 PyTorch 自己算那一位:x.view(3, -1) 会自动得出第二位是 4。
2.2 转置与换轴:transpose
x = torch.tensor([[1., 2., 3.], # 2 行 3 列:
[4., 5., 6.]]) # [[1., 2., 3.],
# [4., 5., 6.]]
y = x.transpose(0, 1) # 交换第 0、1 维 → 3 行 2 列:
# [[1., 4.],
# [2., 5.],
# [3., 6.]]
# 多头注意力里到处是 .transpose(1, 2)、.transpose(-2, -1),就是"把某两个维度互换"多维 transpose 同理——只换两个轴的顺序,每维大小不变:
p = torch.randn(2, 3, 4) # shape (2, 3, 4) = (句子, 词, 维)
q = p.transpose(1, 2) # 交换第 1、2 维 → (2, 4, 3) = (句子, 维, 词)
# 拆头里的 .transpose(1, 2) 就是这种"把'头'维度换到前面"transpose(a, b) 把第 a 维和第 b 维互换。它也不挪数据,只改「维度的排列顺序」。
2.3 增减维度:unsqueeze / squeeze
x = torch.tensor([[1., 2., 3.], # shape [2, 3]
[4., 5., 6.]])
y = x.unsqueeze(0) # 在第 0 位插一个大小为 1 的新维度 → shape [1, 2, 3]
# y = [[[1., 2., 3.], [4., 5., 6.]]](外层多包了一对括号)
z = y.squeeze(0) # 把大小为 1 的维度去掉 → 回到 shape [2, 3],值不变什么时候用?有些运算要求维度数对齐(比如「1 句话」要表示成 [1, 词数, 维度]),就用 unsqueeze 补一个维度。系列里 batch(批次)维度经常这么补。
2.4 广播(broadcasting):不同 shape 怎么自动对齐
a = torch.zeros(2, 3) # shape [2, 3],全是 0
b = torch.tensor([10., 20., 30.]) # shape [3]
c = a + b # 居然能加!b 被自动"广播"——复制到 a 的每一行再相加
# tensor([[10., 20., 30.],
# [10., 20., 30.]]) shape [2, 3]规则(不用背):从最后一维往前对齐,某维是 1 或缺失就自动复制扩展。效果上就是「一个向量加到一个矩阵的每一行」。系列里 MOE 的门控权重相加就靠广播。遇到 shape 明明对不上却居然能运算,多半就是广播在帮忙。
2.5 矩阵乘:@ 运算符
A = torch.tensor([[1., 2., 3.], # shape [2, 3]
[4., 5., 6.]])
B = torch.tensor([[1., 0.], # shape [3, 2]
[0., 1.],
[1., 1.]])
C = A @ B # 矩阵乘法(等价于 torch.matmul(A, B))
# tensor([[ 4., 5.],
# [10., 11.]]) shape [2, 2]
# A @ B 要求 A 的最后一维 == B 的倒数第二维(这里 3==3)@ 就是矩阵乘法,系列里 Q @ K、weights @ V 满眼都是它。注意它和逐元素乘 *(a * b 是每个对应位置相乘)完全不同——这是新手第二大坑。
2.6 ★贯穿案例:多头注意力那行拆头代码到底在干嘛
把上面 5 个操作串起来。下面是 多头注意力 那篇里真实的一行(系列第二站的核心):
# x 形状: (batch, seq_len, d_model),假设 (1, 4, 8)——1句话、4个词、每词8维
# 假设 num_heads=2,所以 d_k = d_model / num_heads = 4
Q = self.W_q(x).view(batch, seq_len, self.num_heads, self.d_k).transpose(1, 2)拆开这一长串,看每步 shape 怎么变:
self.W_q(x)—— 一个线性变换,形状不变,还是(1, 4, 8).view(1, 4, 2, 4)—— 把每个词的 8 维拆成「2 个头 × 每头 4 维」,形状变(1, 4, 2, 4)。用 view 是因为 8 = 2×4,只是把数据重新分组。.transpose(1, 2)—— 把「头」维度(第 2 维)换到「词」维度(第 1 维)前面,形状变(1, 2, 4, 4)=(句子, 头, 词, 每头维度)。
为什么要折腾?因为后面要对每个头独立算注意力。把「头」维度提到前面后,剩下的 (4个词, 4维) 就能当成普通小矩阵,一次性批量处理 2 个头。
一句话总结:这行就是「把一摞词向量,切成几个头,再把『头』这个维度拎到前面,方便批量算」。
图 1:拆头的形状演变——(1,4,8) 的词向量先 view 成 (1,4,2,4) 切出 2 个头,再 transpose 把头维度提到前面变成 (1,2,4,4),方便每个头独立做矩阵乘。
紧跟着还有配套的两行(合头):
scores = Q @ K.transpose(-2, -1) # Q 乘 K 的转置:每个头里,4个词两两算相关度 → (1, 2, 4, 4)
weights = F.softmax(scores, dim=-1) # 沿最后一维把每个词对所有人的得分归一成权重
out = weights @ V # 权重加权求和 → (1, 2, 4, 4)
out = out.transpose(1, 2).contiguous().view(batch, seq_len, d_model) # 把头拼回去 → (1, 4, 8)最后一行是「合头」:先 transpose 把头维度换回原位,再 contiguous()(因为 transpose 后内存不连续,view 前要重新排成连续),最后 view 把 2头×4维 合回 8 维。
这 4 行集齐了 view / transpose / 负维度 / @ / contiguous 全家桶。看懂它们,系列第二站往后的代码就不再有「黑魔法」。
2.7 选讲:einsum(只在 MLA 那篇出现)
torch.einsum('bhid,bhjd->bhij', Q, K) # 用"字母下标"一次性写完转置 + 矩阵乘einsum 是一种用字母下标描述运算的紧凑写法,上面这行等价于 Q @ K.transpose(-2, -1)。它只在系列最后的 MLA 用过一次,其它地方用不上,现在跳过、到那篇再回来看即可。
3. 搭网络 nn.Module
系列里几乎每个模型都长这样:
class XxxNet(nn.Module):
def __init__(self):
super().__init__()
self.layer = nn.Linear(4, 2)
def forward(self, x):
return self.layer(x)这套 class 写法,对没怎么碰过 Python 面向对象的人是个坎。下面逐个拆。
3.1 四件套
class XxxNet(nn.Module):—— 定义一个叫XxxNet的类,括号里的nn.Module表示「照 PyTorch 的网络模板来造」。继承它,你的类就自动有了「记录参数、自动算梯度」等能力。def __init__(self):—— 构造函数,「这个网络由哪些零件组成」写在这里。super().__init__()—— 固定写法,照抄即可。它让 PyTorch 先把nn.Module模板该初始化的内部状态都准备好。不写这行,模型会报错或行为异常。def forward(self, x):—— 「数据怎么从输入流到输出」写在这里。
3.2 init 放零件,forward 放数据流
class TinyNet(nn.Module):
def __init__(self):
super().__init__()
self.layer1 = nn.Linear(4, 8) # 零件1:线性层,4 维进 8 维出
self.layer2 = nn.Linear(8, 1) # 零件2:8 维进 1 维出
def forward(self, x):
x = self.layer1(x) # 数据先过 layer1
x = self.layer2(x) # 再过 layer2
return x3.3 self 是什么 / 为何 super 不能漏
self指向「当前这个网络实例自己」。self.layer1 = ...的意思是「给这个网络装上一个叫layer1的零件,以后用self.layer1就能找到它」。没有self,零件就只是个局部变量,forward里根本用不到。super().__init__():nn.Module在背后维护着「参数清单」「梯度状态」等。不调super,这些没初始化,后面net.parameters()会取不到东西、训练直接出错。所以这行是定式,照抄。
3.4 model(x) 自动调 forward
net = TinyNet() # 造出一个网络实例(此刻权重是随机的)
out = net(x) # 喂数据——注意:外面写 net(x),不是 net.forward(x)PyTorch 的规矩:写 net(x),它会自动去调你的 forward。这是新手最常困惑的点——明明定义的是 forward,为什么外面调的是 net(x)?记住这个对应关系就行。
4. 现成零件(精简 + 指路系列篇)
nn.Module 是模板,torch.nn 里还有一堆现成零件。下面只列系列里高频出现的,每个一句话带过——想懂原理点链接跳到对应单篇,这里只讲怎么用。
4.1 nn.Linear —— 全连接层,算 y = Wx + b
self.fc = nn.Linear(4, 2) # 4 维输入 → 2 维输出,内部自带权重 W 和偏置 b系列几乎每篇都用。它把「加权求和 + 偏置」打包成一层。原理见 神经网络。
4.2 nn.Embedding —— 查表,把 token 编号变成向量
self.embed = nn.Embedding(vocab_size, d_model) # 词表大小 → 每词 d_model 维向量输入是整数编号(token id),输出是该编号对应的向量。原理见 词向量。
4.3 nn.ModuleList —— 叠多个层
self.layers = nn.ModuleList([nn.Linear(8, 8) for _ in range(3)]) # 3 个层叠起来坑:叠层一定要用
nn.ModuleList而不是普通 Pythonlist——只有前者里面的层才会被注册成「模型参数」。普通list装的层,参数不会被训练到。MOE 里多个专家网络就用它。
4.4 损失函数 —— 量「预测离真相多远」
loss_fn = nn.MSELoss() # 回归任务:预测值和真值的均方误差
loss_fn = nn.CrossEntropyLoss() # 分类任务:交叉熵(注意它内部自带 softmax)5. 训练循环四件套 + optimizer
网络定义好还什么都不会(权重是随机的),让它变聪明靠「训练循环」。全系列的训练循环骨架都一样,记住下面这五步。
5.1 先备好优化器
opt = torch.optim.Adam(net.parameters(), lr=0.01)
# net.parameters() —— 把网络里所有"要学的权重"(W、b 等)交给优化器
# lr —— 学习率,每步走多大(太大不稳、太小太慢)Adam 是最常用的优化器,还有 SGD。原理见 优化器 和 学习率。
5.2 四件套(系列每篇训练循环的核心)
for epoch in range(100): # 重复 100 轮
pred = net(X) # ① 前向:算预测
loss = loss_fn(pred, y) # ② 算误差
opt.zero_grad() # ③ 清空上一步的旧梯度(不清会累加出错)
loss.backward() # ④ 反向:自动算每个权重的梯度
opt.step() # ⑤ 用梯度更新权重——这一步才是真正"学习"对应关系:backward() 干的是 反向传播,step() 干的是 梯度下降。zero_grad() 必须在 backward() 前,否则梯度会在多个 batch 间累加。
5.3 完整可跑迷你训练
把上面零件拼起来,教一个最小的网络学会 y ≈ 2x + 1 这条规律。重点对照训练循环和 5.2 的五步。
import torch
import torch.nn as nn
# 假数据:5 个点,大致满足 y ≈ 2x + 1
X = torch.tensor([[0.], [1.], [2.], [3.], [4.]]) # 输入 x
y = torch.tensor([[1.], [3.], [5.], [7.], [9.]]) # 对应的 y(正好 2x+1)
class TinyNet(nn.Module):
def __init__(self):
super().__init__()
self.fc = nn.Linear(1, 1) # 1 维进 1 维出:就是要它学会 y = wx + b
def forward(self, x):
return self.fc(x)
torch.manual_seed(0) # 固定随机种子,让结果可复现
net = TinyNet() # 造出网络(此刻 w、b 随机)
opt = torch.optim.SGD(net.parameters(), lr=0.05) # 优化器:SGD,学习率 0.05
loss_fn = nn.MSELoss() # 损失:均方误差
for epoch in range(300): # 训练 300 轮
pred = net(X) # ① 前向
loss = loss_fn(pred, y) # ② 算误差
opt.zero_grad() # ③ 清旧梯度
loss.backward() # ④ 反向算梯度
opt.step() # ⑤ 更新权重
# 看它学到的 w 和 b(理想值 w≈2.0, b≈1.0)
print(f"学到的权重 w ≈ {net.fc.weight.item():.3f}")
print(f"学到的偏置 b ≈ {net.fc.bias.item():.3f}")
print(f"最终 loss = {loss.item():.4f}")运行输出:
学到的权重 w ≈ 2.000
学到的偏置 b ≈ 1.000
最终 loss = 0.0000没有人把 y = 2x + 1 写进代码——网络是从 5 个数据点里自己把权重练出来的,w 和 b 都精准命中了 2 和 1。这里的 loss 几乎为 0,是因为 y = 2x + 1 是一条精确的直线,而 nn.Linear 本身也是线性的,所以能完美拟合;真实任务的数据不会有这么干净,loss 会停在一个非零的小值——但训练骨架完全一样。把它换成更复杂的网络、更复杂的数据,骨架完全一样:造网络 → 备优化器和损失 → 循环(前向→误差→清梯度→反向→更新)。
6. 杂项速查表
下面这些 API 在系列里零散出现,不需要现在精通,遇到时回来查即可。
| API | 一句话 | 系列哪篇用到 |
|---|---|---|
F.softmax(x, dim=-1) | 把一组得分归一成「加和为 1」的概率 | Softmax |
register_buffer(...) | 注册一个「不参与训练但要跟着模型存」的张量(如位置编码表) | 位置编码 |
torch.triu / masked_fill | 生成上三角 / 把某些位置填成特定值(因果掩码用) | 因果掩码 |
with torch.no_grad(): | 临时关掉梯度计算(测试、推理时省内存) | 多篇的测试代码 |
.detach() | 把张量从「计算图」摘出来(不再追踪梯度) | 词向量 |
.item() | 把单元素张量取出成普通 Python 数字(打印用) | 多篇 |
requires_grad=True | 声明这个张量需要追踪梯度 | 链式法则 |
小结
PyTorch 就两件事——把数据装进张量、把网络拼成 nn.Module,剩下的梯度计算它自动帮你做。这篇给你的是读懂本系列代码的最小集合:
- 张量怎么建(
tensor / randn / zeros / arange) - 形状怎么变(
view / transpose / unsqueeze / 广播 / @)——这是重点,尤其多头注意力那行.view().transpose()拆头,看懂它系列代码就不再有黑魔法 - 网络怎么搭(
class + __init__ + forward + super) - 训练怎么转(
zero_grad → backward → step)
接下来回到 学透 Transformer & MOE 学习地图,从第一站神经网络开始——你会发现每个概念都在反复用这些 PyTorch 零件。
参考资料
- Learn the Basics - PyTorch 官方教程 https://pytorch.org/tutorials/beginner/basics/intro.html
- 数据操作 - 《动手学深度学习》 https://zh.d2l.ai/chapter_preliminaries/ndarray.html
- Tensors - PyTorch 官方文档 https://pytorch.org/docs/stable/tensors.html