📑 本页目录(点开跳转)
11 · 从 NumPy 到 PyTorch:四个语义陷阱
⏱ 96 分钟 | ⭐ np.split(a, 3) 切成三份、torch.split(t, 3) 切成每份三个 —— 两边都不报错
🎯 一句话
从 NumPy 转 PyTorch,真正会咬你的不是那些改了名字的函数 —— 那些写错了会报错。
会咬你的是名字一模一样、行为却不一样的那几个:repeat、split、transpose、size。它们照常运行,只是算的不是你想算的东西。
🧩 一、先划清这一章的边界
这一章不教 PyTorch。它只回答一个很窄的问题:你带着 NumPy 的肌肉记忆去写 torch,会写出什么东西来。
| 想知道 | 去哪 |
|---|---|
| 张量自己是什么(storage + shape/stride/offset 那份说明书) | PyTorch 这个框架本身 · 01 · 张量到底是什么 |
就地操作为什么会和 autograd 打架、RuntimeError: a leaf Variable that requires grad... |
PyTorch · 03 · 三种就地操作报错 |
detach / no_grad / requires_grad 三个断的不是一个东西 |
PyTorch · 04 |
stride 到底怎么算出来的、C 序 F 序 |
⭐ 本板块 05 · 内存布局与 stride —— 本章一句都不重讲 |
| 「视图还是拷贝」的完整判据表 | 本板块 04 · 视图还是拷贝,torch 的规则和那边一致 |
| ⭐ numpy ↔ torch 的完整术语对照 | 本章第七节。站内只有这一份,PyTorch 板块的张量章是链过来的 |
⭐ 本章的判据:只收「你会照 NumPy 的样子写出来,而且它不报错」的东西。 写错就报错的(
torch.dot只吃一维、view撞 stride)不进这一章 —— 报错本身会告诉你。
⚠️ 下面所有输出都是在 PyTorch 2.13.0+cpu / NumPy 2.4 / Python 3.13 / Windows 上实跑的。
🧩 二、💀💀 陷阱一:四个同名不同物
这一节是本章唯一必读的。 四个函数在两边都存在、都能跑、都不报错,语义却是错开的。
import numpy as np
import torch
a = np.array([1, 2, 3]); t = torch.tensor([1, 2, 3])
print("np.repeat(a, 3) ->", np.repeat(a, 3))
print("t.repeat(3) ->", t.repeat(3).tolist())
print("t.repeat_interleave(3) ->", t.repeat_interleave(3).tolist())
print("np.tile(a, 3) ->", np.tile(a, 3))
b = np.arange(6); s = torch.arange(6)
print("np.split(b, 3) ->", [x.tolist() for x in np.split(b, 3)])
print("torch.split(s, 3) ->", [x.tolist() for x in torch.split(s, 3)])
print("torch.chunk(s, 3) ->", [x.tolist() for x in torch.chunk(s, 3)])
m = np.arange(24).reshape(2, 3, 4); u = torch.arange(24).reshape(2, 3, 4)
print("np.transpose(m,(2,0,1))->", np.transpose(m, (2, 0, 1)).shape)
print("torch.transpose(u,0,1) ->", tuple(torch.transpose(u, 0, 1).shape))
print("u.permute(2,0,1) ->", tuple(u.permute(2, 0, 1).shape))
print("np np.zeros((2,3)).size ->", np.zeros((2, 3)).size)
print("torch torch.zeros(2,3).size()->", tuple(torch.zeros(2, 3).size()))
print("torch torch.zeros(2,3).numel()->", torch.zeros(2, 3).numel())
print("torch .size 忘了加括号 ->", f"{torch.zeros(2, 3).size}"[:44])
关键信息
⚠️ 最后那行的地址每次跑都不一样,看的是它根本没报错这件事。
| 写法 | NumPy 的意思 | torch 的意思 | ⭐ 对应关系 |
|---|---|---|---|
repeat |
逐元素重复:[1 1 1 2 2 2 3 3 3] |
整体平铺:[1 2 3 1 2 3 1 2 3] |
💀 正好错开:np.repeat ↔ t.repeat_interleave,np.tile ↔ t.repeat |
split(x, 3) |
切成 3 份(每份 2 个) | 每份 3 个(切出 2 份) | ⭐ 「切成 n 份」在 torch 是 chunk |
transpose |
收一个完整排列:(2,0,1) → (4,2,3) |
⚠️ 只交换两根轴:(0,1) → (3,2,4) |
「完整排列」在 torch 是 permute |
size |
属性,元素总个数 = 6 |
⚠️ 方法,返回形状 (2, 3) |
「元素总个数」在 torch 是 numel() |
💀 repeat 那一条最贵:把 [1,2,3] 复制 3 遍这件事,两边都做了,只是一个交错、一个连排。你拿它去构造「每个样本重复 K 次」的批次,batch 的形状完全正确、总数完全正确、内容全是错的。下游算 loss 照样收敛,只是收敛到别的东西上。
💀 size 那一条以最蠢的方式咬人:print(f"batch 大小 {t.size}") 不报错,只是日志里出现一串 <built-in method size of Tensor object at 0x...>。更糟的是 if t.size > 100: —— 方法对象和整数比较才会报错,但如果你写的是 if t.size:,它永远为真。
⭐ 记法:
repeat/split/transpose/size这四个词,在 torch 里一律先停一秒。 其余绝大多数同名函数(sum、mean、where、stack、argsort、einsum、maximum)语义是一致的。
🧩 三、陷阱二:下划线才是原地,不加下划线等于没写
NumPy 表达「原地」有两种写法:+= 和 out=。torch 两种都不用,它用函数名末尾的下划线。
import numpy as np
import torch
a = np.array([1., 2., 3.])
a.clip(0, 2) # 返回新的,结果被丢掉,a 不变
print("numpy a.clip(0, 2) 之后 a ->", a)
np.clip(a, 0, 2, out=a) # numpy 的原地写法:out=
print("numpy np.clip(out=a) 之后 ->", a)
b = np.array([1., 2., 3.])
b += 1 # numpy 的另一种原地:+=
print("numpy b += 1 ->", b)
t = torch.tensor([1., 2., 3.])
t.clamp(0, 2) # 结果被丢掉了,t 一点没变
print("torch t.clamp(0, 2) ->", t.tolist())
t.clamp_(0, 2) # 下划线才是原地
print("torch t.clamp_(0,2) ->", t.tolist())
x = torch.tensor([1., 2., 3.])
print("x.add(1) 是同一块内存吗:", x.add(1).data_ptr() == x.data_ptr())
print("x.add_(1) 是同一块内存吗:", x.add_(1).data_ptr() == x.data_ptr(), "| x =", x.tolist())
关键信息
⭐ data_ptr() 就是判据:x.add(1) 给了一块新内存(False),x.add_(1) 还在原来那块(True)。
⚠️ 这个坑在两边是对称的,NumPy 用户经常忘的是自己那一半:a.clip(0, 2) 同样返回新数组、同样把结果丢掉,a 还是 [1. 2. 3.]。⭐ 两边的规矩其实是同一条:不接返回值 = 白写。 区别只在「怎么表达原地」:
| 要原地 | NumPy | torch |
|---|---|---|
| 加法 | a += 1 |
t.add_(1)(t += 1 也可以) |
| 裁剪 | np.clip(a, 0, 2, out=a) |
t.clamp_(0, 2) |
| 一般函数 | np.f(a, out=a),只有部分函数支持 out= |
⭐ 规律得多:几乎每个都有下划线版 |
| 填值 | a[:] = 0 |
t.zero_() 或 t.fill_(0) |
⚠️⚠️ 别把「torch 的下划线更好用」理解成「多用下划线」。 在 torch 里原地操作会和自动求导打架:RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation。⭐ 那是 PyTorch · 03 · 三种就地操作报错 的正题,那一章还讲了最贵的那种根本不报错。本章只负责告诉你「下划线是什么意思」。
🧩 四、陷阱三:from_numpy 借的是同一块内存
import numpy as np
import torch
arr = np.array([1, 2, 3])
t = torch.from_numpy(arr)
t[0] = 99
print("from_numpy 之后改 tensor,arr ->", arr)
arr2 = np.array([1, 2, 3])
t2 = torch.tensor(arr2)
t2[0] = 99
print("torch.tensor 之后改 tensor,arr2 ->", arr2)
t3 = torch.tensor([1., 1., 1.])
d = t3.numpy()
t3[0] = 42
print("改完 tensor,之前 .numpy() 拿到的 ->", d)
arr3 = np.array([1., 2., 3.])
t4 = torch.from_numpy(arr3)
arr3 *= 10 # 原地,tensor 跟着变
print("arr3 *= 10 之后 tensor ->", t4.tolist())
arr4 = np.array([1., 2., 3.])
t5 = torch.from_numpy(arr4)
arr4 = arr4 * 10 # 重新绑定,不是原地
print("arr4 = arr4*10 之后 tensor ->", t5.tolist())
g = torch.tensor([1., 2.], requires_grad=True)
try:
g.numpy()
except RuntimeError as e:
print("requires_grad 的 .numpy() ->", e)
关键信息
| 写法 | 内存 | 什么时候用 |
|---|---|---|
torch.from_numpy(arr) |
⭐ 共享 | 数组很大、不想多占一份 |
torch.tensor(arr) |
拷贝 | ⭐ 默认就用这个,除非你确实想省那份内存 |
t.numpy() |
⭐ 共享 | 拿去画图 / 算指标;⚠️ 别在这个数组上就地改 |
⭐ 这就是第 04 章那套「视图还是拷贝」的跨库版本 —— 判据完全一样:共享内存的东西,改哪一边另一边都变,而且不报错。 唯一的新东西是「另一边」现在换了个库。
⭐ 倒数第二组输出是这一节的关键,它把「共享」和「重新绑定」的区别摆出来了:
arr3 *= 10 是原地写,tensor 跟着变成 [10.0, 20.0, 30.0];
arr4 = arr4 * 10 是新建一个数组再把名字挪过去,原来那块内存没动,tensor 还是 [1.0, 2.0, 3.0]。
💀 两行代码看起来只差一个空格,行为完全不同,而且都不报错。
⚠️ .numpy() 有两个附加条件:张量要在 CPU 上、requires_grad 要是 False。上面那句报错原文已经把修法写在里面了 —— Use tensor.detach().numpy() instead。在 GPU 上还要先 .cpu(),完整写法是 t.detach().cpu().numpy()。
🗓️ 未实跑 —— 需要 GPU:.cpu() 那一段本机验证不了,只有 requires_grad 这一条是实跑的。
🛑 读到这里可以停 —— 前半章讲完了(约 34 分钟)。 后半章还有:陷阱四:dtype 默认值,两边不一样 · 第五个隐性差别:两套随机数状态 ·
axis还是dim:别名是单向的 回来的时候不用重读,直接从下一节接着看就行。
🧩 五、陷阱四:dtype 默认值,两边不一样
import numpy as np
import torch
print("np.array([1.0, 2.0]) ", np.array([1.0, 2.0]).dtype)
print("torch.tensor([1.0, 2.0]) ", torch.tensor([1.0, 2.0]).dtype)
print("torch.from_numpy(np 的 f64) ", torch.from_numpy(np.array([1.0, 2.0])).dtype)
print("torch.tensor(np 的 f64) ", torch.tensor(np.array([1.0, 2.0])).dtype)
print("np.zeros(3) 占字节 ", np.zeros(3).nbytes)
print("torch.zeros(3) 占字节 ", torch.zeros(3).element_size() * torch.zeros(3).numel())
x = np.random.default_rng(0).random(10 ** 7)
print("float64 求和 %.6f" % float(torch.from_numpy(x).sum()))
print("float32 求和 %.6f" % float(torch.from_numpy(x).float().sum()))
print("np 整数数组 .mean() ->", np.array([1, 2, 3]).mean())
try:
torch.tensor([1, 2, 3]).mean()
except RuntimeError as e:
print("torch 整数张量 .mean() ->", str(e)[:66])
print("np int/int 的 dtype ->", (np.array([1, 2]) / np.array([2, 4])).dtype)
print("torch int/int 的 dtype ->", (torch.tensor([1, 2]) / torch.tensor([2, 4])).dtype)
关键信息
⭐ 一句话记住:NumPy 的浮点默认 float64,torch 的默认 float32。 同样三个 0,NumPy 占 24 字节,torch 占 12 字节。
⚠️⚠️ 但是「转过来」的时候默认值不生效:torch.from_numpy 和 torch.tensor(np_array) 都原样带着 float64 过来。只有从 Python 列表建的才是 float32。
💀 于是最经典的那个报错就来了:数据从 NumPy 来(float64),模型权重是 float32,矩阵乘法当场 mat1 and mat2 must have the same dtype, but got Double and Float。
⭐ 修法是把数据 .float(),不要把模型 .double() —— 那一整套在 PyTorch · 01 · 张量到底是什么 讲透了,本章不重复。
⭐ 反过来那一半才是静默的:.float() 之后精度真的掉了。实测 1000 万个 [0,1) 随机数求和,float64 给 4999281.562134,float32 给 4999281.500000,差 0.062134。⚠️ 这不是 bug,是 float32 的有效位数只有约 7 位十进制 —— 结果都到七位数了,小数点后自然存不住。💀 它不报警、不报错,你只会看到「同一份数据,NumPy 里算出来的指标和 torch 里算出来的对不上第三位小数」。
⚠️ 两条附带的差别:
| NumPy | torch | |
|---|---|---|
| 整数数组求均值 | 2.0,自动升成浮点 |
💀 直接报错:mean(): could not infer output dtype |
| 整数 ÷ 整数 | float64 |
float32 |
⭐ 整数求均值那一条是好事:torch 宁可报错也不猜。真要算就自己写 t.float().mean(),把「我接受精度变化」这件事写出来。
⭐ 浮点低精度(
float16/bfloat16/ 混合精度训练)整块归 AI 基础设施 · 06 · 混合精度,本章只到「默认值不一样」为止。 整数那一侧的回绕与截断在 本板块 08 章。
🧩 六、第五个隐性差别:两套随机数状态
第 10 章结尾预告过这一条,现在兑现。
import numpy as np
import torch
torch.manual_seed(42); n1 = np.random.rand(3)
torch.manual_seed(42); n2 = np.random.rand(3)
print("两次 manual_seed(42) 之间,np.random.rand 一致吗:", np.array_equal(n1, n2))
np.random.seed(7); t1 = torch.rand(3)
np.random.seed(7); t2 = torch.rand(3)
print("两次 np.random.seed(7) 之间,torch.rand 一致吗:", torch.equal(t1, t2))
torch.manual_seed(42); a1 = torch.rand(3)
torch.manual_seed(42); a2 = torch.rand(3)
print("两次 manual_seed(42) 之间,torch.rand 一致吗:", torch.equal(a1, a2),
np.round(a1.numpy(), 4))
对照
两次 manual_seed(42) 之间,np.random.rand 一致吗: False
两次 np.random.seed(7) 之间,torch.rand 一致吗: False
两次 manual_seed(42) 之间,torch.rand 一致吗: True [0.8823 0.915 0.3829]
⭐ 三行输出说的是同一件事:torch.manual_seed 只管 torch 的流,np.random.seed 只管 NumPy 的流,互相完全不认。只有第三行(自己播自己)才是 True。
💀 它为什么难查:你的数据增强用 NumPy 写、模型初始化用 torch —— 只播一个,另一半永远在漂。而且它不报错,两次跑出来的 loss 曲线只是「差不多」,你会以为那是正常的训练波动。
⭐ 所以站内那份 set_seed() 清单要一次播三个(random / numpy / torch,用 GPU 的还要 torch.cuda),完整写法在 ML 基础 · 11 · 训练调试手册。本章只解释它为什么要写这么多行:因为那是三套互不相通的状态。
⚠️ 多进程那一层还有第三套规矩:NumPy 这边是 SeedSequence(seed).spawn(n)(第 10 章第五节),torch 的 DataLoader 有它自己的 worker 种子机制 —— 那是 PyTorch · 08 · DataLoader 与多进程 的题。⭐ 两套解决的是同一个问题:四个 worker 别抽出一样的「随机」数。
🛑 第二个休息点 —— 中段讲完了(约 19 分钟)。 最后一段还有:
axis还是dim:别名是单向的 这一章确实长,分三次读完全没问题 —— 回来直接从下一节接着看。
🛑 读到这里可以停 —— 已经读了约 53 分钟。 最后一段还有(约 42 分钟):
axis还是dim:别名是单向的 · 检查点与走神救援 回来的时候不用重读,直接从下一节接着看就行。
📋 七、axis 还是 dim:别名是单向的
这一条写错就报错,所以它不算陷阱 —— 但它是全站被问得最多的一句,放这儿一次说清。
import numpy as np
import torch
t = torch.arange(6.).reshape(2, 3)
print("t.sum(dim=0) ->", t.sum(dim=0).tolist())
print("t.sum(axis=0) torch 也收 ->", t.sum(axis=0).tolist())
print("t.sum(axis=0, keepdims=True) ->", tuple(t.sum(axis=0, keepdims=True).shape))
for name, fn in [("torch.split(t, 1, axis=0)", lambda: torch.split(t, 1, axis=0)),
("torch.roll(t, 1, axis=0)", lambda: torch.roll(t, 1, axis=0)),
("torch.flip(t, axis=[0])", lambda: torch.flip(t, axis=[0])),
("torch.nn.Softmax(axis=1)", lambda: torch.nn.Softmax(axis=1))]:
try:
fn(); print(f"{name:26s} -> OK")
except TypeError as e:
print(f"{name:26s} -> TypeError: {str(e)[:44]}")
a = np.arange(6.).reshape(2, 3)
try:
a.sum(dim=0)
except TypeError as e:
print("numpy a.sum(dim=0) -> TypeError:", e)
try:
a.sum(axis=0, keepdim=True)
except TypeError as e:
print("numpy a.sum(keepdim=True) -> TypeError:", e)
算一算
t.sum(dim=0) -> [3.0, 5.0, 7.0]
t.sum(axis=0) torch 也收 -> [3.0, 5.0, 7.0]
t.sum(axis=0, keepdims=True) -> (1, 3)
torch.split(t, 1, axis=0) -> TypeError: split() got an unexpected keyword argument '
torch.roll(t, 1, axis=0) -> TypeError: roll() got an unexpected keyword argument 'a
torch.flip(t, axis=[0]) -> TypeError: flip() missing 1 required positional argumen
torch.nn.Softmax(axis=1) -> TypeError: Softmax.__init__() got an unexpected keyword
numpy a.sum(keepdim=True) -> TypeError: _sum() got an unexpected keyword argument 'keepdim'. Did you mean 'keepdims'?
⭐ 结论只有一句:别名是单向的。
- torch 收
axis和keepdims—— 但只收一部分。sum/max/argsort/cumsum/cat/stack/chunk/unsqueeze都收,⚠️ 而split/roll/flip/nn.Softmax不收,实测四个全是TypeError。哪些收哪些不收没有规律,别去记。 - NumPy 一个都不收:
a.sum(dim=0)是TypeError。(keepdim那条 NumPy 还好心提示了Did you mean 'keepdims'?。)
⭐ 所以规矩是:在 torch 里一律写 dim / keepdim。 写 axis 大多数时候能过,然后在某个你没想到的函数上突然炸 —— 这种「大部分时候管用」的写法比彻底不管用更难排查。
完整对照表(站内只有这一份)
⭐ 形状与轴(axis 怎么数、keepdims 为什么存在,在第 02 章):
| NumPy | torch | ⚠️ 备注 |
|---|---|---|
a.shape |
t.shape / t.size() |
💀 t.size 是方法,别忘括号 |
a.size(元素个数) |
t.numel() |
💀 同名不同物,见第二节 |
a.ndim |
t.dim() / t.ndim |
两个都能用 |
axis= / keepdims= |
dim= / keepdim= |
⭐ 少一个 s,一律写 torch 那套 |
a.reshape(2,3) |
t.reshape(2,3) / t.view(2,3) |
⚠️ view 有 stride 限制,见第 05 章 |
a.ravel() / a.flatten() |
t.flatten() / t.reshape(-1) |
—— |
np.expand_dims(a, 0) / a[None] |
t.unsqueeze(0) / t[None] |
⭐ None 两边都认 |
a.squeeze() |
t.squeeze() |
—— |
np.transpose(a, (2,0,1)) |
t.permute(2,0,1) |
💀 同名不同物,见第二节 |
np.swapaxes(a, 0, 1) |
torch.transpose(t, 0, 1) |
⭐ 这两个才是一对 |
np.broadcast_to(a, s) |
t.expand(s) |
⚠️ numpy 侧只读会报错;torch 侧能写且不报错,写一格整列跟着变 —— 要写先 .clone() |
⭐ 拼接与切分:
| NumPy | torch | ⚠️ 备注 |
|---|---|---|
np.concatenate([a,b], axis=0) |
torch.cat([a,b], dim=0) |
⭐ 名字变了,写错会报错 |
np.stack([a,b]) |
torch.stack([a,b]) |
同名同义 |
np.split(a, 3)(切成 3 份) |
torch.chunk(t, 3) |
💀 同名不同物,见第二节 |
np.array_split(a, 3) |
torch.chunk(t, 3) |
⚠️ 不能整除时分法不同:7 个元素前者 [3,2,2]、后者 [3,3,1] |
np.repeat(a, 3) |
t.repeat_interleave(3) |
💀 同名不同物,见第二节 |
np.tile(a, 3) |
t.repeat(3) |
💀 正好和上一行错开 |
⭐ 数值与运算:
| NumPy | torch | ⚠️ 备注 |
|---|---|---|
a @ b |
t @ u |
同 |
np.einsum("ij,j->i", ...) |
torch.einsum("ij,j->i", ...) |
同名同义,实测一致 |
np.clip(a, 0, 2) |
torch.clamp(t, 0, 2) |
⭐ 名字变了 |
np.where(c, a, b) |
torch.where(c, t, u) |
同 |
np.maximum(a, b) |
torch.maximum(t, u) |
同 |
np.take_along_axis(a, i, axis=1) |
torch.gather(t, 1, i) |
⭐ 实测两边给同样的结果 |
np.argsort / np.argmax |
torch.argsort / torch.argmax |
同 |
np.argpartition(a, -k) |
torch.topk(t, k) |
⚠️ 语义不同:topk 是排好序的 |
a.mean()(整数也行) |
💀 t.mean() 整数报错 |
先 t.float() |
np.linalg.norm |
torch.linalg.norm |
同 |
⭐ 类型、拷贝与设备:
| NumPy | torch | ⚠️ 备注 |
|---|---|---|
浮点默认 float64 |
浮点默认 float32 |
⭐ 见第五节 |
a.astype(np.float32) |
t.to(torch.float32) / t.float() |
—— |
a.copy() |
t.clone() |
⭐ torch 还多一个 .detach(),见 PyTorch · 04 |
np.zeros_like(a) |
torch.zeros_like(t) |
同 |
a += 1 / np.f(a, out=a) |
t.add_(1),下划线 |
⭐ 见第三节 |
float(a[0]) |
t[0].item() |
两边 .item() 都能用 |
| —— | t.to("cuda") / t.cpu() |
NumPy 没有这一格 |
np.random.default_rng(0) |
torch.manual_seed(0) |
💀 两套独立状态,见第六节 |
🔗 这一章连到哪里
| 相关的地方 | 为什么 |
|---|---|
| PyTorch 这个框架本身 · 01 · 张量到底是什么 | ⭐ 张量的自足解释:一块 storage + shape/stride/offset/dtype/device/requires_grad 那份说明书。本章是「从 NumPy 走过去」,那一章是「站在 torch 里往下看」 —— 先看哪个都行 |
| PyTorch · 03 · 三种就地操作报错 | 第三节只讲了下划线是什么意思。⭐ 用了之后会撞上什么(包括最贵的那种根本不报错)在那一章 |
PyTorch · 04 · detach / no_grad / requires_grad |
第四节那句 Use tensor.detach().numpy() instead 里的 detach 到底断了什么 |
| PyTorch · 08 · DataLoader 与多进程 | 第六节末尾那半句:torch 侧的 worker 种子机制。⭐ 它和 NumPy 的 SeedSequence.spawn 解决的是同一个问题 |
| 04 · 视图还是拷贝 | ⭐ 第四节的 from_numpy 共享内存,就是那一章「视图还是拷贝」判据的跨库版本 —— 判据完全一样,只是「另一边」换成了另一个库 |
| 05 · 内存布局与 stride | view 为什么有时候报 view size is not compatible ...。⭐ 机制在那一章,本章一句都不重讲 |
| 02 · shape 和 axis 到底怎么数 | 第七节把 axis 换成了 dim,但怎么数轴这件事没变。⚠️ 不会数轴的话,改名字也救不了 |
| 10 · 随机数、种子与可复现 | 第六节兑现的正是那一章结尾的预告。⭐ 那边讲的「一个项目只用一套 API」,在这里升级成「记得你现在有两套库、三套状态」 |
| 08 · 整数 dtype 的真相 | 第五节只说了「整数除法两边给的浮点位宽不同」。⭐ 整数自己会怎么回绕、astype 为什么是截断,在那一章 |
| AI 基础设施 · 06 · 混合精度 | 第五节到「float32 求和差 0.062134」为止。⭐ float16 / bfloat16 / GradScaler 那一整套是那一章的正题 |
| ML 基础 · 11 · 训练调试手册 | 那份 set_seed() 要一次播 random / numpy / torch / torch.cuda。⭐ 第六节解释的正是它为什么要写这么多行 |
| ML 基础 · 15 · PyTorch 实战手册 | 「十分钟跑通一个训练循环」和六个高频 Bug 在那里。⭐ 那是「怎么用」,本章是「为什么你写出来的和你想的不一样」 |
✅ 检查点
np.repeat(a, 3)和t.repeat(3)各给出什么?NumPy 的repeat在 torch 里对应哪个函数?np.split(b, 3)和torch.split(s, 3)(b、s都是 6 个元素)各切出几份、每份几个?「切成 3 份」在 torch 里该写什么?np.transpose(m, (2,0,1))在 torch 里对应哪个函数?torch.transpose干的是什么?a.size和t.size分别是什么?写f"{t.size}"会发生什么?t.clamp(0, 2)和t.clamp_(0, 2)有什么区别?怎么用一行代码验证?torch.from_numpy(arr)和torch.tensor(arr)的区别是什么?实测里改完 tensor,两个arr分别变成了什么?arr *= 10和arr = arr * 10对由它from_numpy出来的 tensor 有什么不同影响?- NumPy 和 torch 的浮点默认 dtype 各是什么?
torch.from_numpy(np.array([1.0, 2.0]))的 dtype 是哪个,为什么? np.array([1,2,3]).mean()和torch.tensor([1,2,3]).mean()各是什么结果?torch.manual_seed(42)之后再抽np.random.rand(3),两次会一样吗?为什么?- torch 到底收不收
axis=?该怎么写?
👀 答案
np.repeat(a, 3)→[1 1 1 2 2 2 3 3 3](逐元素重复);t.repeat(3)→[1, 2, 3, 1, 2, 3, 1, 2, 3](整体平铺)。💀 两边正好错开:np.repeat↔t.repeat_interleave(实测[1,1,1,2,2,2,3,3,3]),np.tile↔t.repeat。np.split(b, 3)→ 3 份,每份 2 个([[0,1],[2,3],[4,5]]);torch.split(s, 3)→ 每份 3 个,切出 2 份([[0,1,2],[3,4,5]])。「切成 3 份」在 torch 是torch.chunk(s, 3),实测给[[0,1],[2,3],[4,5]]。- 对应
t.permute(2,0,1)(实测形状都是(4,2,3))。⚠️torch.transpose(u, 0, 1)只交换两根轴,给的是(3,2,4)。「交换两根轴」在 NumPy 那侧叫np.swapaxes。 a.size是属性,元素总个数 = 6;t.size是方法,t.size()返回形状(2, 3),元素个数要写t.numel()。💀f"{t.size}"不报错,打出一串<built-in method size of Tensor object at 0x...>;if t.size:更糟,永远为真。t.clamp(0, 2)返回新的、t一点没变(实测还是[1.0, 2.0, 3.0]);t.clamp_(0, 2)是原地([1.0, 2.0, 2.0])。验证用data_ptr():x.add(1).data_ptr() == x.data_ptr()是False,x.add_(1)那句是True。from_numpy借同一块内存,torch.tensor拷贝。实测:from_numpy那条arr变成[99 2 3],torch.tensor那条arr2还是[1 2 3]。反方向的.numpy()也是借(实测[42. 1. 1.])。arr3 *= 10是原地写,tensor 跟着变成[10.0, 20.0, 30.0];arr4 = arr4 * 10是新建数组再挪名字,原内存没动,tensor 还是[1.0, 2.0, 3.0]。💀 两行只差一个空格,都不报错。- NumPy 默认
float64,torch 默认float32(np.zeros(3)占 24 字节、torch.zeros(3)占 12 字节)。⚠️ 但torch.from_numpy和torch.tensor(np_array)都原样带着float64过来 —— 默认值只对从 Python 列表建的生效。💀 这就是mat1 and mat2 must have the same dtype, but got Double and Float的来源,修法是把数据.float(),别把模型.double()。 - NumPy 给
2.0(自动升成浮点);torch 直接报错mean(): could not infer output dtype. Input dtype must be either a floating point or complex dtype.。要算就写t.float().mean()。 - 不一样(实测
一致吗: False)。torch.manual_seed只管 torch 的流,完全没碰 NumPy 的全局状态;反过来np.random.seed(7)也不影响torch.rand(同样False)。只有自己播自己才是True(实测torch.manual_seed(42)两次都给[0.8823 0.915 0.3829])。这就是set_seed()要一次播三个的原因。 - 收一部分:
sum/max/argsort/cumsum/cat/stack/chunk/unsqueeze收axis和keepdims,⚠️ 而split/roll/flip/nn.Softmax不收(实测四个全是TypeError),哪些收没有规律。规矩:在 torch 里一律写dim/keepdim—— 「大部分时候管用」的写法比彻底不管用更难排查。反方向 NumPy 一个都不收。
🛑 可以停在这里
⚡ 走神救援
⭐ 本章判据:只收「你照 NumPy 的样子写出来、而且它不报错」的东西。 写错就报错的不进来——报错自己会说。
💀💀 陷阱一:四个同名不同物,全都不报错。
repeat正好错开:NumPy 的是逐元素重复,torch 的是整体平铺。⭐ 对应关系是np.repeat↔repeat_interleave、np.tile↔t.repeat。你拿它构造「每个样本重复 K 次」的批次,形状和总数全对、内容全错,loss 照样收敛。split:NumPy 的参数是「切成几份」,torch 的是「每份几个」——「切成 n 份」在 torch 是chunk。transpose:NumPy 收完整排列,torch 只换两根轴;完整排列是permute。size:NumPy 是属性(元素个数),torch 是方法(返回形状)——💀f"{t.size}"不报错,打出一个<built-in method ...>。⭐ 陷阱二:下划线才是原地。 不带下划线的版本结果被丢掉、原张量一点没变。判据用
data_ptr()。⚠️ NumPy 那半是对称的(a.clip(...)同样白写),只是原地写法不同。⭐ 陷阱三:
from_numpy借的是同一块内存,改一边另一边跟着变;torch.tensor(arr)才是拷贝。⭐ 这就是第 04 章「视图还是拷贝」的跨库版本,判据一模一样。 💀 最阴的一组:arr *= 10是原地写、tensor 跟着变;arr = arr * 10是新建再挪名字、tensor 不变——两行只差一个空格,都不报错。⭐ 陷阱四:dtype 默认值不同——NumPy 浮点默认双精度,torch 默认单精度。
下一节 👉 附录A-速查.md