📅 创建时间:2026-06-03 🏷️ 标签:#算子语义 #Broadcast #Reduce #Matmul #Shape推理 #语义陷阱 #NCHW #NHWC
📚 前置知识:[[03-graph-representation]](计算图表示) 📚 相关知识:[[06-frontend-formats]](前端格式)[[08-operator-fusion]](算子融合)
算子语义、广播、归约与形状推导 / Operator Semantics, Broadcasting, Reduction, and Shape Inference
┌──────────────────────────────────────────────────────────────────────────────┐
│ 🎯 场景:ONNX 导出时的 Shape 不兼容错误 │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ 你在导出 ONNX 模型时报错: │
│ │
│ RuntimeError: Got unexpected shape [batch, 3, 224, 224] for input 'x'. │
│ Expected shape compatible with [*, 3, 224, 224] │
│ │
│ 你检查了 PyTorch 模型,输入确实是 batch×3×224×224。 │
│ 但 ONNX exporter 报错说 shape 不兼容。 │
│ │
│ 你不知道问题出在 broadcast、slice 还是 exporter 的 bug 里。 │
│ │
└──────────────────────────────────────────────────────────────────────────────┘第1节:算子语义的重要性——同名算子,不同行为
1.1 语义差异的危害
算子语义不一致是深度学习框架互操作性的最大障碍之一。即使两个框架都声称支持某个操作,它们的实际行为可能完全不同。
python
"""
语义差异的危害示例:
场景:把 PyTorch 模型导出到 ONNX,然后部署到 TFLite
问题链:
1. PyTorch 的 conv2d 默认是 NCHW 格式
2. ONNX 规范支持 NCHW 和 NHWC(通过 operator 属性)
3. TFLite 的 conv2d 默认是 NHWC 格式
结果:
- 如果不显式指定格式
- PyTorch → ONNX 可能产生 NCHW
- ONNX → TFLite 可能转换失败或产生错误结果
这就是为什么理解算子语义如此重要。
"""1.2 语义差异的来源
| 来源 | 说明 | 示例 |
|---|---|---|
| 历史原因 | 不同框架独立发展,命名习惯不同 | PyTorch 的 view vs TF 的 reshape |
| 硬件优化 | 不同硬件偏好不同格式 | GPU 偏好 NCHW,CPU 偏好 NHWC |
| 默认值差异 | 未指定时使用框架默认值 | broadcast 方向、padding 算法 |
| 精度处理 | 浮点溢出、NaN 处理不同 | NaN * 0 在不同框架可能不同 |
| 索引语义 | 行优先 vs 列优先 | axis=0 在不同上下文含义不同 |
第2节:Broadcast 语义详解
2.1 NumPy/PyTorch Broadcast 规则
NumPy-style broadcast 从右对齐,自动补维。规则如下:
python
import torch
import numpy as np
print("=" * 60)
print("Broadcast 规则详解")
print("=" * 60)
# 规则1:维度从右向左对齐
# 如果维度不同,小的维度被扩展(或"广播")以匹配大的
# 示例1:简单情况
a = torch.randn(3, 4) # shape: (3, 4)
b = torch.randn(4) # shape: (4,)
# b 被广播到 (1, 4),然后和 a 的每一行相加
result = a + b
print(f"(3,4) + (4,) = (3,4) # b 变成 (1,4) 然后广播")
# 示例2:多维情况
a = torch.randn(5, 3, 4) # shape: (5, 3, 4)
b = torch.randn(3, 4) # shape: (3, 4)
# b 被广播到 (1, 3, 4) 然后和 a 相加
result = a + b
print(f"(5,3,4) + (3,4) = (5,3,4) # b 变成 (1,3,4) 然后广播")
# 示例3:维度不兼容情况
# a = torch.randn(5, 3, 4) # shape: (5, 3, 4)
# b = torch.randn(4, 5) # shape: (4, 5)
# 无法广播!因为:
# a: 5 3 4
# b: 4 5 ← 4 ≠ 5,无法匹配
print()
print("规则2:维度必须是 1 或相同")
print(" (3,) 和 (3,) → 广播")
print(" (1,) 和 (3,) → 广播(1 被扩展到 3)")
print(" (2,) 和 (3,) → 无法广播!")2.2 Broadcast 的具体示例
python
# ============================================================
# Broadcast 常见场景
# ============================================================
def broadcast_examples():
print("=" * 60)
print("Broadcast 场景示例")
print("=" * 60)
# 场景1:LayerNorm 中的 broadcast
# 输入: (batch, seq_len, hidden_dim) = (B, S, H)
# gamma, beta: (H,) → 被广播到 (1, 1, H)
# mean, var: scalar → 被广播到 (B, S, 1)
x = torch.randn(32, 128, 512) # B=32, S=128, H=512
mean = x.mean(dim=-1, keepdim=True) # shape: (32, 128, 1)
var = x.var(dim=-1, keepdim=True) # shape: (32, 128, 1)
# 归一化时,(x - mean) / sqrt(var + eps) 都是广播操作
print("LayerNorm Broadcast:")
print(f" 输入 x: {x.shape}")
print(f" mean: {mean.shape}")
print(f" var: {var.shape}")
print(f" x - mean: {(x - mean).shape}") # 广播
print()
# 场景2:Attention 中的 QK^T
# Q: (B, H, S, D) = (2, 8, 128, 64)
# K: (B, H, D, S) = (2, 8, 64, 128)
# QK^T: (B, H, S, S) = (2, 8, 128, 128)
# 但 attention score 需要 scale: score / sqrt(D)
# scale: scalar → 广播到 (B, H, S, S)
Q = torch.randn(2, 8, 128, 64)
K = torch.randn(2, 8, 64, 128)
scale = 1.0 / (64 ** 0.5) # scalar
print("Attention Scale:")
print(f" Q: {Q.shape}")
print(f" K: {K.shape}")
print(f" scale: {scale} (scalar)")
print(f" Q @ K.transpose(-2, -1): {(Q @ K.transpose(-2, -1)).shape}")
print()
# 场景3:残差连接
# input: (B, C, H, W)
# residual: (B, C, H, W) 或 (C,) 或 (1, C, 1, 1)
# broadcast 确保维度匹配
input_tensor = torch.randn(4, 64, 32, 32)
residual = torch.randn(64) # (C,) → 被广播到 (1, 64, 1, 1)
print("Residual Broadcast:")
print(f" input: {input_tensor.shape}")
print(f" residual: {residual.shape}")
print(f" input + residual: {(input_tensor + residual).shape}")
broadcast_examples()2.3 Broadcast 常见错误
python
# ============================================================
# Broadcast 常见错误
# ============================================================
def broadcast_errors():
print("=" * 60)
print("Broadcast 错误案例")
print("=" * 60)
print()
print("错误1:维度不匹配")
print("-" * 40)
# 错误情况
a = torch.randn(3, 4, 5) # (3, 4, 5)
b = torch.randn(4, 3) # (4, 3)
print(f" a.shape = {a.shape}")
print(f" b.shape = {b.shape}")
print(" 对齐方式:")
print(" a: 3 4 5")
print(" b: 4 3 ← 从右向左:5 vs 3(不兼容!)")
print()
print(" 尝试广播会报错:")
try:
result = a + b
except RuntimeError as e:
print(f" RuntimeError: {e}")
print()
print("错误2:隐式假设广播方向")
print("-" * 40)
# 很多人错误地认为 broadcast 总是从左向右
x = torch.randn(4, 1, 8) # (4, 1, 8)
y = torch.randn(1, 8, 4) # (1, 8, 4)
print(f" x.shape = {x.shape}")
print(f" y.shape = {y.shape}")
print(" NumPy/PyTorch 广播(从右向左):")
print(" x: 4 1 8")
print(" y: 1 8 4")
print(" 结果: 4 8 8 ← 不是 (4,1,4)!")
print(f" 实际结果: {(x + y).shape}")
print()
print("错误3:混淆 batch broadcast 和 channel broadcast")
print("-" * 40)
# 图像批次
images = torch.randn(8, 3, 224, 224) # (B, C, H, W)
mean = torch.tensor([0.485, 0.456, 0.406]) # (3,) RGB 均值
print(f" images: {images.shape}")
print(f" mean: {mean.shape}")
print()
print(" 如果直接减:")
print(" mean 被广播到 (1, 3, 1, 1)")
print(" 结果: (8, 3, 224, 224) ✓")
print(f" 实际: {(images - mean).shape}")
print()
print(" ⚠️ 注意:mean 是 RGB 顺序,需要确保和 C 维度匹配!")
print(" ImageNet: mean = [0.485, 0.456, 0.406] 对应 R,G,B")
broadcast_errors()2.4 TensorFlow/XLA Broadcast
python
"""
TensorFlow Broadcast 扩展支持
TensorFlow 相比 NumPy/PyTorch 有一些扩展:
1. tf.broadcast_to:显式广播
2. tf.shape + tf.where:复杂条件广播
3. 支持 masked 操作
示例:
import tensorflow as tf
# 显式广播到指定形状
x = tf.constant([1, 2, 3])
y = tf.broadcast_to(x, [3, 3])
# y = [[1, 2, 3],
# [1, 2, 3],
# [1, 2, 3]]
"""第3节:Reduce 操作详解
3.1 Reduce 的基本概念
Reduce 是将张量沿某个轴(或多个轴)聚合的操作。
python
import torch
print("=" * 60)
print("Reduce 操作详解")
print("=" * 60)
x = torch.arange(24).reshape(2, 3, 4).float() # (2, 3, 4)
print(f"输入张量 x:\n{x}")
print(f"x.shape = {x.shape}")
print()
print("-" * 40)
print("沿 axis=0 求和(batch 维度)")
print("-" * 40)
# 结果 shape: (3, 4)
print(f"x.sum(axis=0).shape = {x.sum(axis=0).shape}")
print(f"x.sum(axis=0):\n{x.sum(axis=0)}")
print()
print("-" * 40)
print("沿 axis=1 求和(sequence 维度)")
print("-" * 40)
# 结果 shape: (2, 4)
print(f"x.sum(axis=1).shape = {x.sum(axis=1).shape}")
print(f"x.sum(axis=1):\n{x.sum(axis=1)}")
print()
print("-" * 40)
print("沿 axis=2 求和(feature 维度)")
print("-" * 40)
# 结果 shape: (2, 3)
print(f"x.sum(axis=2).shape = {x.sum(axis=2).shape}")
print(f"x.sum(axis=2):\n{x.sum(axis=2)}")
print()
print("-" * 40)
print("keepdims=True(保持维度)")
print("-" * 40)
print(f"x.sum(axis=0, keepdims=True).shape = {x.sum(axis=0, keepdims=True).shape}")
print(f"x.sum(axis=1, keepdims=True).shape = {x.sum(axis=1, keepdims=True).shape}")3.2 Reduce 的语义差异
python
# ============================================================
# PyTorch vs NumPy vs TensorFlow Reduce 差异
# ============================================================
print("=" * 60)
print("Reduce 操作跨框架差异")
print("=" * 60)
# PyTorch API
pytorch_result = torch.sum(x, dim=0) # PyTorch 用 dim
# NumPy API
numpy_result = x.numpy().sum(axis=0) # NumPy 用 axis
# TensorFlow API
# tf.reduce_sum(x, axis=0) # TensorFlow 用 axis
print()
print("PyTorch: torch.sum(x, dim=0) ← 用 dim")
print("NumPy: x.sum(axis=0) ← 用 axis")
print("TensorFlow: tf.reduce_sum(x, axis=0) ← 用 axis")
print()
print("注意:PyTorch 用 'dim',NumPy/TF 用 'axis',这是命名差异!")
print()
print("-" * 40)
print("keepdims 差异")
print("-" * 40)
# PyTorch
pytorch_keepdims = torch.sum(x, dim=0, keepdim=True)
print(f"PyTorch keepdim=True: {pytorch_keepdims.shape}")
# NumPy
numpy_keepdims = x.numpy().sum(axis=0, keepdims=True)
print(f"NumPy keepdims=True: {numpy_keepdims.shape}")
print()
print("⚠️ 两者结果相同,但 API 命名不同!")3.3 Reduce 的常见用法
python
# ============================================================
# Reduce 常见场景
# ============================================================
def reduce_scenarios():
print("=" * 60)
print("Reduce 操作常见场景")
print("=" * 60)
print()
print("场景1:Cross Entropy 计算中的 logsumexp")
print("-" * 40)
logits = torch.randn(32, 1000) # (batch, classes)
max_logits = logits.max(dim=1, keepdim=True).values # (batch, 1)
# 数值稳定技巧:减去 max
stable_logits = logits - max_logits
log_sum_exp = stable_logits.exp().sum(dim=1, keepdim=True).log() # (batch, 1)
print(f" logits: {logits.shape}")
print(f" log_sum_exp: {log_sum_exp.shape}")
print(" 用于数值稳定的交叉熵计算")
print()
print("场景2:Global Average Pooling")
print("-" * 40)
feature_map = torch.randn(8, 256, 7, 7) # (B, C, H, W)
# 在 H 和 W 维度上做平均
pooled = feature_map.mean(dim=[2, 3]) # (B, C)
print(f" feature_map: {feature_map.shape}")
print(f" pooled: {pooled.shape}")
print(" 用于 CNN 的最后阶段,将空间维度池化掉")
print()
print("场景3:Sequence Masking 中的 sum")
print("-" * 40)
# 假设有 mask 表示有效位置
scores = torch.randn(4, 10) # (batch, seq_len)
mask = torch.tensor([[1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) # (batch, seq_len)
# masked sum
masked_scores = scores * mask.float()
summed = masked_scores.sum(dim=1) # (batch,)
print(f" scores: {scores.shape}")
print(f" mask: {mask.shape}")
print(f" masked_scores: {masked_scores.shape}")
print(f" summed: {summed.shape}")
reduce_scenarios()第4节:Matmul 和 BatchMatmul
4.1 2D 矩阵乘法
python
import torch
print("=" * 60)
print("2D 矩阵乘法")
print("=" * 60)
A = torch.randn(3, 4) # (M, K)
B = torch.randn(4, 5) # (K, N)
# torch.matmul:通用矩阵乘法
C = torch.matmul(A, B) # (3, 5)
# 或者使用 @ 运算符
C2 = A @ B
# torch.mm:2D 专用矩阵乘法
C3 = torch.mm(A, B)
print(f"A @ B = ({A.shape} @ {B.shape}) = {C.shape}")
print(f"torch.matmul(A, B) = {C.shape}")
print(f"torch.mm(A, B) = {C3.shape}")
print()
print("数学含义:C[i][j] = sum(A[i][k] * B[k][j]) for k in 0..K-1")4.2 Batch 矩阵乘法
python
print()
print("=" * 60)
print("Batch 矩阵乘法 (Batched Matmul)")
print("=" * 60)
# Batch Matmul:批量矩阵乘法
# A: (B, M, K) # B 个 MxK 矩阵
# B: (B, K, N) # B 个 KxN 矩阵
# C: (B, M, N) # B 个 MxN 矩阵
B = 4 # batch size
M = 3 # output rows
K = 5 # inner dimension
N = 2 # output cols
A = torch.randn(B, M, K) # (4, 3, 5)
B_mat = torch.randn(B, K, N) # (4, 5, 2)
C = torch.bmm(A, B_mat) # (4, 3, 2) bmm = batched mm
print(f"A @ B = ({A.shape} @ {B_mat.shape}) = {C.shape}")
print()
print("注意:batch 维度必须匹配,但 M, K, N 可以任意")
print()
# torch.baddbmm:batch + 对角线
print("-" * 40)
print("torch.baddbmm:带偏置的 batch matmul")
print("-" * 40)
bias = torch.randn(B, M, N) # (4, 3, 2)
result = torch.baddbmm(bias, A, B_mat) # bias + A @ B
print(f"baddbmm(bias, A, B) = {result.shape}")4.3 Broadcasting in Matmul
python
print()
print("=" * 60)
print("Matmul 中的 Broadcast")
print("=" * 60)
# torch.matmul 支持广播
# 情况1:常规 batch matmul
A = torch.randn(4, 3, 5) # (B, M, K)
B = torch.randn(4, 5, 2) # (B, K, N)
C = A @ B # (4, 3, 2)
print(f"常规 BMM: {A.shape} @ {B.shape} = {C.shape}")
# 情况2:广播 batch 维度
A = torch.randn(4, 3, 5) # (B, M, K)
B = torch.randn(5, 2) # (K, N) → 被广播到 (1, 5, 2) 然后 (4, 5, 2)
C = A @ B
print(f"Broadcast B: {A.shape} @ {B.shape} = {C.shape}")
print(" B 从 (5,2) 广播到 (4,5,2)")
# 情况3:广播两个 batch 维度
A = torch.randn(4, 1, 5) # (B, 1, K)
B = torch.randn(1, 3, 5, 2) # (1, A, K, N)
C = A @ B
print(f"Broadcast both: {A.shape} @ {B.shape} = {C.shape}")
print(" A 从 (4,1,5) 广播到 (4,3,5)")
print(" B 从 (1,3,5,2) 广播到 (4,3,5,2)")
print(" 结果: (4,3,5,2) → 只保留最后两个维度!")第5节:Convolution 变体
5.1 数据格式:NCHW vs NHWC
python
import torch
print("=" * 60)
print("卷积数据格式:NCHW vs NHWC")
print("=" * 60)
# PyTorch 默认使用 NCHW
# TensorFlow 默认使用 NHWC
# NCHW: (Batch, Channel, Height, Width)
# NHWC: (Batch, Height, Width, Channel)
input_nchw = torch.randn(1, 3, 224, 224) # PyTorch 格式
print(f"NCHW 格式: {input_nchw.shape}")
print(f" B=1, C=3, H=224, W=224")
print()
# 转换为 NHWC(在 Tensor 中表示)
input_nhwc = input_nchw.permute(0, 2, 3, 1) # (1, 224, 224, 3)
print(f"NHWC 格式: {input_nhwc.shape}")
print(f" B=1, H=224, W=224, C=3")
print()
print("格式选择的影响:")
print("-" * 40)
print()
print("NCHW(PyTorch 偏好):")
print(" ✅ GPU 内存访问效率高(同一通道数据连续)")
print(" ✅ 卷积核是 (C_out, C_in, K, K),硬件友好")
print(" ✅ PyTorch 默认")
print()
print("NHWC(TensorFlow/CPU 偏好):")
print(" ✅ RGB 图像自然排列(像素点是连续的)")
print(" ✅ 部分 CPU 实现更快(SIMD)")
print(" ✅ 某些硬件(NPU)原生支持")
print()
print("混合格式(NCHWc / HWNC):")
print(" - 把 channel 分成多个 group")
print(" - 结合 NCHW 和 NHWC 的优点")5.2 Conv2d 变体参数
python
# ============================================================
# Conv2d 参数详解
# ============================================================
def conv_variants():
print("=" * 60)
print("Conv2d 变体参数")
print("=" * 60)
x = torch.randn(1, 3, 32, 32) # (B, C, H, W)
print(f"输入: {x.shape}")
print()
# 标准卷积
conv = torch.nn.Conv2d(3, 64, kernel_size=3, padding=1)
print(f"标准 Conv(3,64,k=3,p=1): {conv(x).shape}")
# 空洞卷积 (Dilation)
conv_dilated = torch.nn.Conv2d(3, 64, kernel_size=3, padding=2, dilation=2)
print(f"空洞 Conv(dilation=2): {conv_dilated(x).shape}")
print(" ↑ 输出 H,W 不变,但感受野更大")
# 分组卷积 (Groups)
# 把输入通道分成 groups 组,每组独立卷积
conv_groups = torch.nn.Conv2d(4, 8, kernel_size=3, padding=1, groups=2)
# 输入必须是 4 的倍数,输出必须是 8 的倍数
x_groups = torch.randn(1, 4, 16, 16)
print(f"分组 Conv(4->8, groups=2): {conv_groups(x_groups).shape}")
print(" ↑ 参数量减少一半")
# Depthwise 卷积 (groups = in_channels)
conv_depthwise = torch.nn.Conv2d(3, 3, kernel_size=3, padding=1, groups=3)
print(f"Depthwise Conv(3,3,groups=3): {conv_depthwise(x).shape}")
print(" ↑ 每个通道单独卷积,用于轻量化网络 (MobileNet)")
# 转置卷积 (Deconv / ConvTranspose)
conv_transpose = torch.nn.ConvTranspose2d(3, 64, kernel_size=4, stride=2, padding=1)
print(f"Transposed Conv(3->64, s=2): {conv_transpose(x).shape}")
print(" ↑ 扩大特征图,用于上采样/生成网络")
# 1x1 卷积
conv_1x1 = torch.nn.Conv2d(3, 64, kernel_size=1)
print(f"1x1 Conv(3->64): {conv_1x1(x).shape}")
print(" ↑ 只改变通道数,不改变空间维度")
print(" ↑ 常用于通道间的信息融合 (Bottleneck)")
conv_variants()5.3 Convolution 输出尺寸公式
python
print()
print("=" * 60)
print("卷积输出尺寸公式")
print("=" * 60)
print("""
标准卷积输出尺寸:
┌────────────────────────────────────────────────────────┐
│ output_size = floor((input_size + 2*padding - │
│ dilation*(kernel_size-1) - 1) │
│ / stride) + 1 │
└────────────────────────────────────────────────────────┘
转置卷积输出尺寸:
┌────────────────────────────────────────────────────────┐
│ output_size = (input_size - 1) * stride │
│ - 2 * padding │
│ + dilation * (kernel_size - 1) │
│ + output_padding + 1 │
└────────────────────────────────────────────────────────┘
""")
# 验证
x = torch.randn(1, 1, 10, 10)
conv = torch.nn.Conv2d(1, 1, kernel_size=3, stride=1, padding=0)
y = conv(x)
print(f"输入: (1, 1, 10, 10)")
print(f"Conv(k=3, s=1, p=0): 输出 {y.shape}")
print(f"验证: (10 - 3 + 0) / 1 + 1 = {int((10 - 3) / 1 + 1)}")
conv = torch.nn.Conv2d(1, 1, kernel_size=3, stride=2, padding=1)
y = conv(x)
print(f"\n输入: (1, 1, 10, 10)")
print(f"Conv(k=3, s=2, p=1): 输出 {y.shape}")
print(f"验证: (10 + 2 - 2) / 2 + 1 = {int((10 + 2*1 - 2) / 2 + 1)}")第6节:Shape 推导与类型检查
6.1 静态 Shape 推导
python
print()
print("=" * 60)
print("Shape 推导(静态分析)")
print("=" * 60)
print("""
静态 Shape 推导的意义:
1. 在运行时之前就知道输出形状
2. 提前分配内存
3. 检测形状不匹配错误
4. 优化内存布局
Shape 推导规则示例:
op: matmul(A: (B, M, K), B: (B, K, N)) → (B, M, N)
op: reshape(x: (*), shape: (B, M, N)) → (B, M, N) if B*M*N = prod(*)
op: conv2d(x: (N, C_in, H, W), w: (C_out, C_in, K, K),
stride: s, padding: p) → (N, C_out,
(H + 2p - K)/s + 1,
(W + 2p - K)/s + 1)
""")
# 实际代码示例
def shape_inference_examples():
"""展示如何推导 Shape"""
print("Shape 推导实例:")
print("-" * 40)
# 输入
x = torch.randn(8, 3, 224, 224) # NCHW
# Conv + BN + ReLU
conv1 = torch.nn.Conv2d(3, 64, 7, stride=2, padding=3)
# Shape: (N, 64, (224+6-7)/2+1, (224+6-7)/2+1) = (8, 64, 112, 112)
bn1 = torch.nn.BatchNorm2d(64)
# Shape: (8, 64, 112, 112) - BN 不改变 Shape
relu = torch.nn.ReLU()
# Shape: (8, 64, 112, 112) - ReLU 不改变 Shape
print(f"输入: {x.shape}")
x = conv1(x)
print(f"Conv: {x.shape}")
x = bn1(x)
print(f"BN: {x.shape}")
x = relu(x)
print(f"ReLU: {x.shape}")
# 全局池化
gap = torch.nn.AdaptiveAvgPool2d((1, 1))
x = gap(x)
print(f"GAP: {x.shape}") # (8, 64, 1, 1)
# Flatten
x = x.view(x.size(0), -1)
print(f"View: {x.shape}") # (8, 64)
shape_inference_examples()6.2 静态分析能发现什么
python
print()
print("=" * 60)
print("静态 Shape 分析的能力边界")
print("=" * 60)
print("""
静态 Shape 分析 **可以** 发现的问题:
✅ Shape 不匹配:
matmul(A: (3, 4), B: (5, 4)) → 无法相乘!3 ≠ 5 错误
✅ Broadcast 不兼容:
(3, 4) + (5, 4) → 广播不兼容
✅ 维度约束:
reshape(x: (24,), shape: (4, 6)) → OK
reshape(x: (24,), shape: (4, 5)) → 4*5 ≠ 24,错误
✅ Conv 参数验证:
groups=2 要求 in_channels % groups == 0
静态 Shape 分析 **无法** 发现的问题:
❌ 数值溢出:
float16 的乘法可能导致溢出
❌ 逻辑错误:
你想计算 cos(x) 但写成了 sin(x)
❌ 运行时动态形状:
if batch_size > 32 then reshape to (64, ...) else reshape to (32, ...)
❌ 语义错误:
axis=-1 和 axis=2 可能形状相同但语义不同
""")6.3 ONNX Shape 推理
python
# ============================================================
# ONNX Shape 推理
# ============================================================
"""
ONNX 提供静态 Shape 推理工具:
import onnx
from onnx import shape_inference
model = onnx.load("model.onnx")
# 推理 Shape
inferred_model = shape_inference.infer_shapes(model)
# 获取输入输出 Shape
for input_tensor in inferred_model.graph.input:
shape = [dim.dim_value for dim in input_tensor.type.tensor_type.shape.dim]
print(f"Input: {input_tensor.name}, shape: {shape}")
"""
print()
print("ONNX Shape 推理示例:")
print("-" * 40)
print("""
当导出 ONNX 时:
torch.onnx.export(
model,
x,
"model.onnx",
input_names=["input"],
output_names=["output"],
dynamic_axes={
"input": {0: "batch_size"}, # 动态 batch 维度
"output": {0: "batch_size"}
},
opset_version=13 # 指定 opset 版本
)
结果:
- 如果不指定 dynamic_axes:所有维度都是静态的
- 指定后:batch_size 维度变为动态
运行时:
- 输入 shape: (?, 3, 224, 224) → batch 可以是任意值
""")第7节:跨框架算子对比表
7.1 常见操作的框架对比
| 操作 | PyTorch | TensorFlow | ONNX | NumPy |
|---|---|---|---|---|
| 矩阵乘法 | A @ B / torch.mm | tf.matmul | MatMul | A @ B |
| 逐元素乘法 | A * B | A * B | Mul | A * B |
| 广播加法 | A + b | A + b | Add | A + b |
| Reshape | x.view(...) | tf.reshape | Reshape | x.reshape |
| 转置 | x.transpose(...) | tf.transpose | Transpose | x.T |
| 索引切片 | x[..., 0] | x[..., 0] | Slice | x[..., 0] |
| Concatenate | torch.cat | tf.concat | Concat | np.concatenate |
| 堆叠 | torch.stack | tf.stack | Unsqueeze+Concat | np.stack |
| Reduce Sum | x.sum(dim=0) | tf.reduce_sum | ReduceSum | x.sum(axis=0) |
| Reduce Mean | x.mean(dim=0) | tf.reduce_mean | ReduceMean | x.mean(axis=0) |
| Softmax | F.softmax | tf.nn.softmax | Softmax | - |
| Conv2d | F.conv2d | tf.nn.conv2d | Conv | - |
| MaxPool | F.max_pool2d | tf.nn.max_pool | MaxPool | - |
| BatchNorm | F.batch_norm | tf.nn.batch_normalization | BatchNormalization | - |
| Dropout | F.dropout | tf.nn.dropout | Dropout | - |
7.2 常见语义陷阱
python
print()
print("=" * 60)
print("跨框架语义陷阱汇总")
print("=" * 60)
print("""
陷阱1:Conv padding 模式
┌─────────────────────────────────────────────────────────────────┐
│ PyTorch: padding='valid' → 无 padding │
│ TensorFlow: padding='VALID' → 无 padding │
│ │
│ PyTorch: padding='same' → 输出尺寸 = ceil(input / stride) │
│ TensorFlow: padding='SAME' → 输出尺寸 = ceil(input / stride) │
│ │
│ ⚠️ 注意:TF 'same' 会自动计算 padding,PyTorch 需要手动 │
└─────────────────────────────────────────────────────────────────┘
陷阱2:Axis/Dim 的含义
┌─────────────────────────────────────────────────────────────────┐
│ PyTorch: dim=-1 表示最后一个维度 │
│ NumPy: axis=-1 表示最后一个维度 │
│ ✓ 两者一致 │
│ │
│ 但 PyTorch 的 reduce 用 dim,NumPy 用 axis │
│ torch.sum(x, dim=0) vs x.sum(axis=0) │
└─────────────────────────────────────────────────────────────────┘
陷阱3:Broadcast 行为完全一致
┌─────────────────────────────────────────────────────────────────┐
│ PyTorch/NumPy: 从右向左广播 │
│ TensorFlow: 从右向左广播(相同) │
│ │
│ ✅ 主流框架 broadcast 行为一致 │
│ │
│ 但 PaddlePaddle 某些旧版本可能有差异 │
└─────────────────────────────────────────────────────────────────┘
陷阱4:数据类型转换
┌─────────────────────────────────────────────────────────────────┐
│ PyTorch: tensor.float() → 转换为 float32 │
│ TensorFlow: tf.cast(tensor, tf.float32) → 转换为 float32 │
│ ✓ API 不同但结果相同 │
│ │
│ ⚠️ 但精度可能不同:不同框架的浮点舍入可能略有差异 │
└─────────────────────────────────────────────────────────────────┘
""")升华
┌──────────────────────────────────────────────────────────────────────────────┐
│ 📚 算子语义:核心原则 │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. **同名算子可能不同义**:CrossEntropy 在不同框架有细微差异 │
│ │
│ 2. **默认值是陷阱来源**:未指定参数时框架有各自的默认值 │
│ │
│ 3. **Broadcast 从右向左**:NumPy/PyTorch/TF 一致,但小心维度对齐 │
│ │
│ 4. **Shape 推理有边界**:静态分析能发现形状问题,无法发现数值问题 │
│ │
└──────────────────────────────────────────────────────────────────────────────┘"AI 可查 vs 必须理解"清单
必须理解(不理解就等于不会):
- 🔴 Broadcast 规则:从右对齐,自动补维,维度必须是 1 或相同
- 🔴 Reduce 的 axis 语义:哪个维度被聚合,结果维度是多少
- 🔴 Matmul vs 逐元素乘法:@ vs * 的区别和 broadcast 行为
- 🔴 Conv2d 的 padding/stride/dilation:输出尺寸公式
- 🔴 NCHW vs NHWC:不同框架的默认值和转换方法
AI 可查(知道去哪查就行):
- ✅ 具体框架的 API 签名:文档中搜索具体函数
- ✅ 特定算子的精确语义:ONNX 规范文档
- ✅ 特定硬件的优化建议:NVIDIA/AMD 官方文档
- ✅ 数据类型转换细节:具体函数的精度行为
- ✅ 框架特定的操作:如
tf.function、torch.jit.script
学习状态:🟡 开始学习