PyTorch Shape从零讲解: 深度学习最重要的数据契约
Shape 是深度学习最重要的数据契约
副标题:手把手带你把 batch、特征、类别维度刻进肌肉记忆——三条核心流水线 + 三个高频报错,让 80% 的 Shape 报错无处遁形
你有没有过这种崩溃瞬间:
RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x784 and 10x256)
你盯着这串数字看半天——784 和 10 到底哪个对、哪个错?心里一团乱麻。
或者更阴险的情况:代码不报错,loss 也在降,但准确率死活上不去。最后排查半天,发现是标签维度搞错了,模型学了个寂寞。
这不是你一个人的问题——Shape 不匹配是深度学习里最高频的报错来源。但我想告诉你一个关键认知:Shape 不是"凑对就行"的细节,它是模型每一层之间的"数据契约"。
打个比方:把模型想成一条工厂流水线,每一层是一个工位。前一个工位交出来的零件,尺寸(Shape)必须刚好能被下一个工位接住,否则整条线就卡死。你写下的每一行 Linear、Conv2d,其实都在签一份契约:“我承诺交出的张量长这样,你准备好接了吗?” Shape 错了,契约就作废,图就断了。
这篇我们就把这份"契约"的写法彻底讲清。我会给你一套维度符号表、四类本质操作、三条核心流水线。读完这一篇,你看到任何 Shape 都能一眼判断对不对。
一、先把五个维度符号刻进脑子
所有 PyTorch 代码里,第一维几乎永远是 batch(样本数)。后面所有维度,按任务不同有约定俗成的含义。这些符号我后面每篇文章都会反复用,你先把它们记牢:
| 符号 | 含义 | 典型场景 |
|---|---|---|
B | batch size,一批多少个样本 | 所有任务 |
F | feature,特征数量 | 表格数据 |
C | channel,通道数 | 图像(RGB=3) |
H / W | 高 / 宽 | 图像 |
T | sequence length,序列长度 | 文本、时间序列 |
D | embedding dimension,向量维度 | 词向量 |
V | vocabulary size,词表大小 | NLP |
num_classes | 类别数 | 分类任务 |
💡 直觉补充:为什么第一维一定是
B?因为训练是"一批一批"喂数据的,模型的设计天然假设最外层是"样本的集合"。一旦你把样本维和特征维弄反,后面每一层都会接错格式——这是绝大多数 Shape 报错的根源。
二、Shape 变化的四类本质操作
不管什么层、什么模型,张量的 Shape 变化逃不出这四类。记住它们,你看到任何 Shape 变换都能立刻归类:
| 类型 | 操作 | 例子 |
|---|---|---|
| 增加维度 | unsqueeze、stack、DataLoader 加 batch | [F] → [B, F] |
| 删除维度 | squeeze、flatten、pooling | [B,C,H,W] → [B,C*H*W] |
| 改变维度大小 | Linear、Conv2d、Embedding | [B,10] → [B,32] |
| 调换维度顺序 | transpose、permute | [B,H,W,C] → [B,C,H,W] |
提示:今天这篇先聚焦"契约"(每个维度应该是多少);下一篇我们专门拆工具箱——
reshape/view/transpose/permute/广播到底怎么用。这两篇是配套的。
三、三条核心流水线(全文重点,建议背下来)
这是全文最重要的部分。把这三条流水线记牢,你就能看懂 80% 的训练代码。
流水线 1:表格分类(最干净的全连接)
原始数据 [B, F]
↓ Linear(in=F, out=hidden)
[B, hidden]
↓ Linear(in=hidden, out=num_classes)
[B, num_classes]
↓ CrossEntropyLoss
target: [B]
每个样本有 F 个特征,经过两层全连接,最后输出 num_classes 个分数(logits)。背后的矩阵运算是:
Y = X W ⊤ + b Y = X W^\top + b Y=XW⊤+b
其中
X
X
X 是
[
B
,
i
n
]
[B, in]
[B,in],
W
W
W 是
[
o
u
t
,
i
n
]
[out, in]
[out,in],于是
X
W
⊤
XW^\top
XW⊤ 得到
[
B
,
o
u
t
]
[B, out]
[B,out]。这就是为什么 Linear 的 in_features 必须精确等于输入张量的最后一个维度——矩阵乘法要求内侧的两个维度相等。
import torch
import torch.nn as nn
class TableClassifier(nn.Module):
def __init__(self, in_features, hidden, num_classes):
super().__init__()
self.net = nn.Sequential(
nn.Linear(in_features, hidden), # [B, F] → [B, hidden]
nn.ReLU(),
nn.Linear(hidden, num_classes) # [B, hidden] → [B, num_classes]
)
def forward(self, x):
return self.net(x) # [B, F] → [B, num_classes]
model = TableClassifier(20, 64, 4)
criterion = nn.CrossEntropyLoss()
# 验证 Shape:32 个样本,每个 20 维特征
x = torch.randn(32, 20) # [B=32, F=20]
logits = model(x)
print(logits.shape) # torch.Size([32, 4])
y = torch.randint(0, 4, (32,)) # [B=32],注意是 1 维,不是 [32, 4]
loss = criterion(logits, y) # ✅ logits [32,4] 对 y [32]
最容易错的地方:CrossEntropyLoss 的标签是 [B](每个样本一个类别编号),不是 [B, num_classes](one-hot)。原因我们在上一篇讲过——CrossEntropyLoss 内部自带 Softmax,标签需要的是类别索引而非概率分布。
流水线 2:图像分类(CNN)
图片输入 [B, 3, 32, 32]
↓ Conv2d(3→16)
[B, 16, 32, 32]
↓ MaxPool2d(2)
[B, 16, 16, 16]
↓ Conv2d(16→32)
[B, 32, 16, 16]
↓ MaxPool2d(2)
[B, 32, 8, 8]
↓ Flatten
[B, 32*8*8] = [B, 2048]
↓ Linear(2048, num_classes)
[B, num_classes]

→ [B,16,32,32])。用不同颜色区分卷积层(蓝色)、池化层(绿色)、展平(橙色)、全连接(红色)。最右侧画出模型输出 [B, num_classes]和 target[B]` 的对应关系,用虚线连起来提示"标签是 1 维编号"。]
class SimpleCNN(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 16, 3, padding=1), # [B,3,32,32]→[B,16,32,32]
nn.ReLU(),
nn.MaxPool2d(2), # →[B,16,16,16]
nn.Conv2d(16, 32, 3, padding=1), # →[B,32,16,16]
nn.ReLU(),
nn.MaxPool2d(2), # →[B,32,8,8]
)
self.classifier = nn.Sequential(
nn.Flatten(), # →[B, 2048]
nn.Linear(32 * 8 * 8, num_classes) # →[B, 10]
)
def forward(self, x):
x = self.features(x)
x = self.classifier(x)
return x
关键检查点:Flatten 之后接 Linear,Linear 的 in_features 必须精确等于 C*H*W。算错一位,就会报我们开头看到的 mat1 and mat2 shapes cannot be multiplied。
流水线 3:二分类
输入 [B, F]
↓ Linear
[B, hidden]
↓ Linear(out=1)
[B, 1]
↓ BCEWithLogitsLoss
target: [B, 1] ← 注意是 2 维,标签是 float
model = nn.Sequential(
nn.Linear(20, 64),
nn.ReLU(),
nn.Linear(64, 1) # 输出 1 个 logit(单个样本"是该类"的得分)
)
criterion = nn.BCEWithLogitsLoss()
x = torch.randn(32, 20)
logits = model(x) # [32, 1]
y = torch.randint(0, 2, (32, 1)).float() # [32, 1],注意 .float()!
loss = criterion(logits, y) # ✅
二分类 vs 多分类的 Shape 差异——这张表请背下来,它是新手最容易踩的坑:
| 任务 | 模型输出 | 标签 Shape | 标签类型 | 损失函数 |
|---|---|---|---|---|
| 多分类 | [B, num_classes] | [B] | long | CrossEntropyLoss |
| 二分类 | [B, 1] | [B, 1] | float | BCEWithLogitsLoss |
⚠️ 为什么二分类标签是
[B,1]而多分类是[B]? 因为BCEWithLogitsLoss做的是逐元素的二分类(每个输出位独立地 sigmod + 二元交叉熵),它要求和模型输出逐位对齐的浮点概率标签;而CrossEntropyLoss是对每一行一次性做 softmax 的 softmax + 负对数似然,标签只需要"哪一行赢了"的整数编号。
四、DataLoader:最前面永远多一个 B
这是新手最常忽略的事:单个样本没有 batch 维度,经过 DataLoader 后自动在最前面加一个 B。
from torch.utils.data import TensorDataset, DataLoader
# 单个样本:只有特征维
x_single = torch.randn(10) # [10]
print(x_single.shape) # torch.Size([10])
# 经过 DataLoader:自动在最前面拼出 batch 维
dataset = TensorDataset(torch.randn(100, 10), torch.randint(0, 3, (100,)))
loader = DataLoader(dataset, batch_size=32)
for x, y in loader:
print(x.shape) # torch.Size([32, 10]) ← 多了个 32(batch 维)
print(y.shape) # torch.Size([32])
break
口诀:DataLoader 之后,最前面一定多一个 B。你手写的 forward 第一件事,就是把这多出来的 B 当成"样本的集合"去处理,而不是当成特征。
五、三个高频报错与排查(收藏备用)
报错 1:mat1 and mat2 shapes cannot be multiplied
RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x784 and 10x256)
原因:Linear 的 in_features 和输入张量的最后一维对不上。回到矩阵乘法
Y
=
X
W
⊤
Y = XW^\top
Y=XW⊤:mat1(输入)是
[
32
,
784
]
[32, 784]
[32,784],mat2(权重)是
[
256
,
10
]
[256, 10]
[256,10],PyTorch 实际要做的是
X
×
W
⊤
X \times W^\top
X×W⊤,要求
X
X
X 的列数 ==
W
⊤
W^\top
W⊤ 的行数,也就是
784
=
=
256
784 == 256
784==256——显然不成立。
x = torch.randn(32, 784)
layer = nn.Linear(10, 256) # in_features=10,但输入最后一维是 784
layer(x) # ❌ 报错
修复:把 in_features 改成 784,让内侧维度对齐。
报错 2:Expected target size
RuntimeError: Expected target size [32, 10], got [32]
原因:多分类标签写成了 one-hot [B, num_classes],但 CrossEntropyLoss 要的是 [B] 的类别索引。
# ❌ 错误:标签是 one-hot
y_onehot = torch.zeros(32, 10)
y_onehot[range(32), torch.randint(0, 10, (32,))] = 1
loss = criterion(logits, y_onehot)
# ✅ 正确:标签是类别索引
y = torch.randint(0, 10, (32,))
loss = criterion(logits, y)
报错 3:Expected object of scalar type Long
RuntimeError: Expected object of scalar type Long but got Float
原因:CrossEntropyLoss 的标签必须是 torch.long,但传了 float。
y = torch.randint(0, 10, (32,)).float() # ❌ float
loss = criterion(logits, y)
y = torch.randint(0, 10, (32,)).long() # ✅ long
loss = criterion(logits, y)
# 或者事后补救
y = y.long()
六、排查 Shape 的万能方法
在每个层后面 print(x.shape),是排查 Shape 最快、最朴素也最管用的办法。不要嫌它笨,顶会论文作者的 notebook 里也到处是这行。
def forward(self, x):
print("输入:", x.shape)
x = self.conv1(x)
print("conv1 后:", x.shape)
x = self.pool(x)
print("pool 后:", x.shape)
x = self.flatten(x)
print("flatten 后:", x.shape)
x = self.fc(x)
print("fc 后:", x.shape)
return x


,你接收的 in_features 必须 = 2048"。下方画一个红色叉号场景:契约写 2048,但 Linear 写 256,整条流水线断裂,弹出报错气泡。直观传达"Shape 是层与层之间的契约"。]
七、课后练习
练习 1:一个 CNN 输入是 [64, 3, 28, 28],经过 Conv2d(3, 32, 3, padding=1) 后 Shape 是多少?再经过 MaxPool2d(2) 呢?最后 Flatten 后接 Linear,in_features 应该是多少?
练习 2:下面代码为什么报错?怎么改?
model = nn.Linear(100, 5)
x = torch.randn(32, 10, 100)
y = model(x)
练习 3:多分类任务,num_classes=7,batch_size=16。写出模型输出、标签、loss 的 Shape 和类型。
练习 1:
Conv2d(3, 32, 3, padding=1):padding=1 + kernel=3 + stride=1 保持空间尺寸不变,Shape[64, 32, 28, 28]MaxPool2d(2):空间尺寸减半,Shape[64, 32, 14, 14]Flatten:[64, 32*14*14]=[64, 6272]Linear的in_features= 32 × 14 × 14 = 6272
练习 2:其实不报错。Linear 只看最后一维,[32, 10, 100] 经过 Linear(100, 5) 变成 [32, 10, 5]。这是 Linear 的广播特性——它只作用在最后一维,前面的维度原样保留。如果本意是让每个样本只输出一个 5 维向量,那输入应该是 [32, 100],需要先 reshape/flatten 掉中间维。
练习 3:
- 模型输出
logits:Shape[16, 7],类型 float - 标签
y:Shape[16],类型 long loss:标量,Shape[](0 维)
核心要点小结
- Shape 是每层之间的数据契约:维度含义约定俗成——第 0 维永远是
B(batch),最后一维通常是特征或类别。 - 矩阵乘法决定维度对齐:
Linear做的是 Y = X W ⊤ + b Y = XW^\top + b Y=XW⊤+b,要求输入最后一维 ==in_features,否则报mat1 and mat2 shapes cannot be multiplied。 - 三条核心流水线要背下来:表格分类、图像分类、二分类,看到模型结构就能脑补出每一层的 Shape。
- 多分类 vs 二分类 Shape 不同:多分类标签
[B]+long+CrossEntropyLoss;二分类标签[B,1]+float+BCEWithLogitsLoss。 - DataLoader 自动加 B:单个样本没有 batch 维,经过
DataLoader后最前面一定多一个B。 - 排查万能法:每层后面
print(x.shape)。
动手思考题
- 故意制造一次
mat1 and mat2 shapes cannot be multiplied:把TableClassifier(20, 64, 4)里的第一个Linear改成nn.Linear(21, 64),运行看报错里两个数字分别是什么含义? - 假设
Conv2d没有padding=1,而是默认padding=0、kernel=3、stride=1,那[B, 3, 32, 32]经过卷积后会变成多少?这会对后续Flatten+Linear的in_features产生什么连锁影响? - 二分类场景下,如果你"偷懒"把标签写成
[B](一维 long),BCEWithLogitsLoss会报错还是会静默出错?动手试试,结果说明了什么? - 在评论区贴出你见过最离谱的 Shape 报错,我们一起"会诊" 💬
下一篇我们解决实战中最头疼的问题:reshape、view、transpose、广播机制到底什么时候用哪个——把这些工具用对,Shape 转换再也不会靠瞎试。
📚 关于本系列
本文是 「AI 学习路线 · 阶段四:PyTorch 深度学习基础」 系列中的一篇。所有文章在我的个人博客上都有 可交互动画 + 完整学习路线 版本,建议配合食用 👇
🔗 在博客上阅读本文原版(含可交互组件、公式动画)
👉 Shape 是深度学习最重要的数据契约🗺️ 查看完整 AI 学习路线(从 0 到进阶,持续更新)
👉 bestsdz.xyz觉得有帮助的话,欢迎去博客点个收藏 ⭐,你的支持是我更新的最大动力!
更多推荐
所有评论(0)