🏠 总目录📚 本教程 11 · 从 NumPy 到 PyTorch ← →
📑 本页目录(点开跳转)

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])

关键信息

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]
t.repeat_interleave(3) -> [1, 1, 1, 2, 2, 2, 3, 3, 3]
np.tile(a, 3) -> [1 2 3 1 2 3 1 2 3]
np.split(b, 3) -> [[0, 1], [2, 3], [4, 5]]
torch.split(s, 3) -> [[0, 1, 2], [3, 4, 5]]
torch.chunk(s, 3) -> [[0, 1], [2, 3], [4, 5]]
np.transpose(m,(2,0,1))-> (4, 2, 3)
torch.transpose(u,0,1) -> (3, 2, 4)
u.permute(2,0,1) -> (4, 2, 3)
np np.zeros((2,3)).size -> 6
torch torch.zeros(2,3).size()-> (2, 3)
torch torch.zeros(2,3).numel()-> 6
torch .size 忘了加括号 -> <built-in method size of Tensor object at 0x

⚠️ 最后那行的地址每次跑都不一样,看的是它根本没报错这件事。

写法 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())

关键信息

numpy a.clip(0, 2) 之后 a -> [1. 2. 3.]
numpy np.clip(out=a) 之后 -> [1. 2. 2.]
numpy b += 1 -> [2. 3. 4.]
torch t.clamp(0, 2) -> [1.0, 2.0, 3.0]
torch t.clamp_(0,2) -> [1.0, 2.0, 2.0]
x.add(1) 是同一块内存吗: False
x.add_(1) 是同一块内存吗: True | x = [2.0, 3.0, 4.0]

⭐ 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)

关键信息

from_numpy 之后改 tensor,arr -> [99 2 3]
torch.tensor 之后改 tensor,arr2 -> [1 2 3]
改完 tensor,之前 .numpy() 拿到的 -> [42. 1. 1.]
arr3 *= 10 之后 tensor -> [10.0, 20.0, 30.0]
arr4 = arr4*10 之后 tensor -> [1.0, 2.0, 3.0]
requires_grad 的 .numpy() -> Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.
写法 内存 什么时候用
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)

关键信息

np.array([1.0, 2.0]) float64
torch.tensor([1.0, 2.0]) torch.float32
torch.from_numpy(np 的 f64) torch.float64
torch.tensor(np 的 f64) torch.float64
np.zeros(3) 占字节 24
torch.zeros(3) 占字节 12
float64 求和 4999281.562134
float32 求和 4999281.500000
np 整数数组 .mean() -> 2.0
torch 整数张量 .mean() -> mean(): could not infer output dtype. Input dtype must be either a
np int/int 的 dtype -> float64
torch int/int 的 dtype -> torch.float32

⭐ 一句话记住: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 里一律写 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 在那里。⭐ 那是「怎么用」,本章是「为什么你写出来的和你想的不一样」

✅ 检查点

  1. np.repeat(a, 3) 和 t.repeat(3) 各给出什么?NumPy 的 repeat 在 torch 里对应哪个函数?
  2. np.split(b, 3) 和 torch.split(s, 3)(b、s 都是 6 个元素)各切出几份、每份几个?「切成 3 份」在 torch 里该写什么?
  3. np.transpose(m, (2,0,1)) 在 torch 里对应哪个函数?torch.transpose 干的是什么?
  4. a.size 和 t.size 分别是什么?写 f"{t.size}" 会发生什么?
  5. t.clamp(0, 2) 和 t.clamp_(0, 2) 有什么区别?怎么用一行代码验证?
  6. torch.from_numpy(arr) 和 torch.tensor(arr) 的区别是什么?实测里改完 tensor,两个 arr 分别变成了什么?
  7. arr *= 10 和 arr = arr * 10 对由它 from_numpy 出来的 tensor 有什么不同影响?
  8. NumPy 和 torch 的浮点默认 dtype 各是什么?torch.from_numpy(np.array([1.0, 2.0])) 的 dtype 是哪个,为什么?
  9. np.array([1,2,3]).mean() 和 torch.tensor([1,2,3]).mean() 各是什么结果?
  10. torch.manual_seed(42) 之后再抽 np.random.rand(3),两次会一样吗?为什么?
  11. torch 到底收不收 axis=?该怎么写?
👀 答案
  1. 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。
  2. 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]]。
  3. 对应 t.permute(2,0,1)(实测形状都是 (4,2,3))。⚠️ torch.transpose(u, 0, 1) 只交换两根轴,给的是 (3,2,4)。「交换两根轴」在 NumPy 那侧叫 np.swapaxes。
  4. 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: 更糟,永远为真。
  5. 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。
  6. from_numpy 借同一块内存,torch.tensor 拷贝。实测:from_numpy 那条 arr 变成 [99 2 3],torch.tensor 那条 arr2 还是 [1 2 3]。反方向的 .numpy() 也是借(实测 [42. 1. 1.])。
  7. arr3 *= 10 是原地写,tensor 跟着变成 [10.0, 20.0, 30.0];arr4 = arr4 * 10 是新建数组再挪名字,原内存没动,tensor 还是 [1.0, 2.0, 3.0]。💀 两行只差一个空格,都不报错。
  8. 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()。
  9. NumPy 给 2.0(自动升成浮点);torch 直接报错 mean(): could not infer output dtype. Input dtype must be either a floating point or complex dtype.。要算就写 t.float().mean()。
  10. 不一样(实测 一致吗: 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() 要一次播三个的原因。
  11. 收一部分:sum / max / argsort / cumsum / cat / stack / chunk / unsqueeze 收 axis 和 keepdims,⚠️ 而 split / roll / flip / nn.Softmax 不收(实测四个全是 TypeError),哪些收没有规律。规矩:在 torch 里一律写 dim / keepdim —— 「大部分时候管用」的写法比彻底不管用更难排查。反方向 NumPy 一个都不收。

🛑 可以停在这里

⚡ 走神救援

⭐ 本章判据:只收「你照 NumPy 的样子写出来、而且它不报错」的东西。 写错就报错的不进来——报错自己会说。

💀💀 陷阱一:四个同名不同物,全都不报错。

⭐ 陷阱二:下划线才是原地。 不带下划线的版本结果被丢掉、原张量一点没变。判据用 data_ptr()。⚠️ NumPy 那半是对称的(a.clip(...) 同样白写),只是原地写法不同。

⭐ 陷阱三:from_numpy 借的是同一块内存,改一边另一边跟着变;torch.tensor(arr) 才是拷贝。⭐ 这就是第 04 章「视图还是拷贝」的跨库版本,判据一模一样。 💀 最阴的一组:arr *= 10 是原地写、tensor 跟着变;arr = arr * 10 是新建再挪名字、tensor 不变——两行只差一个空格,都不报错。

⭐ 陷阱四:dtype 默认值不同——NumPy 浮点默认双精度,torch 默认单精度。

下一节 👉 附录A-速查.md

打卡记录保存在你的浏览器里,首页能看到总进度