做深度学习的人每天都要跟 numpy 和 pytorch 这两个库打交道而它们之间的数据类型转换看起来就是个“张量转来转去”的小事实际上却是无数隐蔽 bug 的源头。我见过不少同学卡在“为什么我的损失突然变成 NaN 了”“为什么 GPU 上内存爆了”“为什么转出来的数组改了原来的 tensor 也跟着变了”这类问题里最后定位下来全是 dtype 或转换方式没搞对。这篇内容我就把这几年实打实踩过的坑、验证过的做法从底层原理到实战代码一次性梳理清楚保证你看完能避开 90% 的转换陷阱。1. 先搞清楚底层存储numpy 和 pytorch 都离不开 dtype1.1 dtype 到底是什么为什么它决定内存和计算速度dtype 是 data type 的缩写用来描述数组里每个元素的数据类型。numpy 里的np.float32、np.int64、np.uint8pytorch 里的torch.float32、torch.int64、torch.uint8说的都是同一件事这个数组或张量里的元素是多少位的、有符号还是无符号、是浮点还是整型。你可以把 dtype 想成是装货物的“集装箱规格”。同样是装一万个数字用int8装每个箱子只要 1 个字节用int64装每个箱子要 8 个字节。内存占用差了 8 倍读写带宽、运算速度自然也差出几个量级。尤其是在 GPU 上显存是稀缺资源你把一个本来该用float32的模型参数误转成float64显存可能直接翻倍训练速度立刻变慢这往往不是代码逻辑问题就是 dtype 选错了。dtype 还决定了运算的语义。比如两个uint8数组相加结果可能因为溢出而变成一个错误的小数一个float64的数组和一个float32的数组做运算numpy 和 pytorch 的自动提升规则也不完全一样。所以先记住这句话任何时候看到数据异常、内存异常、计算结果异常第一件事就是检查 dtype 和设备。1.2 numpy 和 pytorch 的 dtype 映射关系numpy 和 pytorch 虽然各自定义了一套数据类型名称但绝大多数是直接对应的不过有几个容易混淆的点。下面这张表是老程序员手边常备的numpy dtypepytorch dtype位宽说明np.float32torch.float32/torch.float32位深度学习默认精度最常用np.float64torch.float64/torch.double64位科学计算常用显存/内存占用高np.int64torch.int64/torch.long64位pytorch 中标签、索引默认类型np.int32torch.int32/torch.int32位某些操作不支持容易踩坑np.int16torch.int1616位不常用np.int8torch.int88位量化模型常用np.uint8torch.uint88位图像数据极常用0-255np.bool_torch.bool8位布尔类型np.complex64/128torch.complex64/128对应复数类型注意 pytorch 版本支持这里要特别提醒两个容易忽略的点。第一pytorch 的默认整型标签是torch.int64也就是torch.long如果你从 numpy 转过来时数组是np.int32直接from_numpy得到的 tensor 就是int32喂给nn.CrossEntropyLoss大概率会报错或者精度不对因为交叉熵损失期望目标索引是int64。第二numpy 支持字符串类型的数组比如dtypeU12但 pytorch 的 tensor 完全不支持字符串如果你从 CSV 读进来一个object或str类型的数组直接转 tensor 必然报错。这种必须先做编码映射比如把类别字符串映射成整数。2. 最常用的转换场景numpy 到 tensortensor 到 numpy2.1 numpy 转 tensortorch.from_numpy和torch.tensor到底选谁先看最常用的两种写法import numpy as np import torch arr np.arange(6, dtypenp.float32).reshape(2, 3) # 写法一共享内存 t1 torch.from_numpy(arr) # 写法二复制数据 t2 torch.tensor(arr)torch.from_numpy(arr)返回的 tensor 和原来的 numpy 数组共用同一块底层内存。这意味着你对t1做任何修改都会直接影响arr反过来也是。这种零拷贝的方式在数据预处理、数据增强时非常香不需要额外的内存和时间开销。torch.tensor(arr)则一定会把数据拷贝一份生成的新 tensor 和原 numpy 数组没有任何内存关系。此外torch.tensor对 dtype 的推断也比较“聪明”它总是尝试把 numpy 的 dtype 映射到 pytorch 的对应类型但有时你传入一个np.float64它会保留成float64这不一定是你想要的深度学习默认精度。在实际项目里我的习惯是如果是临时转换不担心内存就用torch.tensor如果是要在训练循环里频繁使用同一份数据或者希望共享内存减少拷贝就用torch.from_numpy。但共享内存有副作用后面专门讲。注意torch.from_numpy只支持数值型数组不支持np.str_、np.object_也不支持np.ndarray的子类像np.ma.MaskedArray。遇到底层数据不是标准连续内存的数组最好先np.ascontiguousarray再转。2.2 tensor 转 numpy.numpy()与.cpu().numpy()从 tensor 转回 numpy 有两种常见情况# CPU tensor 直接转 t torch.arange(6, dtypetorch.float32).reshape(2, 3) arr t.numpy() # 梯度张量需要先 detach t torch.randn(2, 3, requires_gradTrue) arr t.detach().numpy()对 CPU 上普通的 tensor直接.numpy()得到的 numpy 数组和 tensor共享内存修改任意一方都会影响另一方。如果你希望得到独立副本可以先.clone().numpy()或.cpu().detach().numpy().copy()。一个非常常见的报错是RuntimeError: Cant call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.因为带有requires_gradTrue的张量参与反向传播计算图numpy 不支持自动求导所以框架强制你调用.detach()把它从计算图上摘下来。如果你只是想把梯度打印出来看一看可以weight.grad.detach().numpy()千万别直接.numpy()那必然报错。2.3 共享内存带来的副作用修改一方另一方会跟着变这一节要重点讲因为它造成的 bug 非常隐蔽。看例子arr np.ones((2, 2), dtypenp.float32) t torch.from_numpy(arr) t[0, 0] 99 print(arr) # [[99. 1.] # [ 1. 1.]]看到了吗改了 tensor原 numpy 数组也跟着变成了 99。反过来也一样t torch.arange(4, dtypetorch.float32).reshape(2, 2) arr t.numpy() arr[1, 1] 123 print(t) # tensor([[ 0., 1.], # [ 2., 123.]])这在什么场景下会坑到你比如你对 numpy 数组做了归一化然后torch.from_numpy转了 tensor后来又因为别的逻辑改了 tensor 里的值那你后面再用原 numpy 数组时就可能拿到被污染的数据。有时这种变化是有意的但如果你不清楚底层共享内存调试起来会非常让人抓狂。如果需要彻底断开共享做法是arr np.ones((2, 2), dtypenp.float32) t torch.tensor(arr) # 已经是拷贝 # 或者由共享 tensor 再 clone 一份 t2 torch.from_numpy(arr).clone()记住一条经验法则用torch.tensor或者clone()换拷贝用torch.from_numpy和.numpy()换共享。明确你此刻到底需不需要共享就不会出诡异问题。3. 深度转换强制改 dtype 与精度管理3.1 numpy 的.astype()和 pytorch 的.to(dtype)上面讲的都是保持 dtype 不变地互转但实际项目里更多时候还需要改变 dtype 本身。numpy 里最常用的是astypearr np.array([1.2, 2.7, 3.5], dtypenp.float32) arr_int arr.astype(np.int64) print(arr_int) # [1 2 3]注意astype默认返回一个新的数组即使目标和原 dtype 一样一般也会返回拷贝当然不能依赖这一点。转换时小数部分会被直接截断不会四舍五入这在很多数据处理场景是个隐形坑。pytorch 里对应的方法最常见的是.to(dtype)或者快捷方法.float()、.double()、.half()、.long()、.int()等t torch.tensor([1.2, 2.7, 3.5]) t_int t.to(torch.int64) t_float t.float().to()的语义更丰富它可以同时指定设备、dtype比如tensor.to(devicecuda, dtypetorch.float16)是统一控制的好工具。但要注意tensor.to(dtype...)和tensor.float()都是返回值不会原地修改原 tensor。很多人习惯写t t.float()赋值如果忘写赋值原 tensor 的 dtype 一点没变后续代码就会用错。.to()底层其实有拷贝是从原 tensor 生成新 dtype 的数据但它的内存分配不一定和原 tensor 共享。.float()等快捷方法本质上也是调用了.to(torch.float32)。3.2 float16、float32、float64精度选择和转换代价深度学习里最常用的三个浮点精度是float32大部分模型的默认参数类型也是 PyTorch 训练时的默认精度。float16混合精度训练AMP中的主力显存占用量减半但数值范围比 float32 小很多容易上溢/下溢。float64通常用于科学计算、数值稳定性要求高的算法numpy 里的默认浮点类型。从高精度转低精度最直接的影响是精度丢失。比如arr64 np.array([0.123456789, 1.0000001], dtypenp.float64) arr32 arr64.astype(np.float32) print(arr32) # [0.12345679 1.0000001] —— 后面的有效位被舍掉了如果数值超出目标精度的表示范围甚至会变成 infbig np.float64(1e300) print(big.astype(np.float32)) # inf同时触发 RuntimeWarning反过来从 float16 升到 float32 一般不会出问题只是白白多占了内存。所以转换的原则是只在需要的时候升精度不要为了“保险”把所有数据都转成 float64否则显存和速度都会很受伤。在混合精度训练中一个常见操作是把模型参数在 forward 前临时转成半精度但 loss 和 optimizer 步骤通常还是保持 float32如果这里转换时机不对或者把 losshalf()成 float16很容易导致数值不稳定。我的建议是混合精度训练直接用 PyTorch 官方的torch.amp.autocast上下文管理器让框架自动处理精度不要手动到处half()。3.3 转换中的坑溢出、截断、布尔与整数互转这是一个特别容易被坑的领域我列几个真实踩过的例子。第一浮点转整型是截断不是四舍五入arr np.array([3.7, -3.2]) print(arr.astype(np.int32)) # [3 -3]向零方向截断正数和负数的行为都是舍去小数部分。如果需要四舍五入先用np.round()再用astype。第二有符号转无符号会“爆掉”x np.array([-1, 127, 128], dtypenp.int8) print(x.astype(np.uint8)) # [255 127 128]原因很简单-1在内存中的二进制存储是11111111按无符号解读就是 255。这在图像处理里特别常见如果你图像数据里有负数或者超过 255 的值直接转 uint8 会得到一团完全错乱的数据。正确的做法是先clip到目标范围再转。第三整型转布尔0 是 False非 0 都是 True。这个大家基本知道但反过来布尔转数字时True 变成 1False 变成 0有时会影响后续矩阵运算的语义比如把 masked 矩阵直接astype(np.float32)会得到一堆 1 和 0如果你本意是用布尔掩码做索引那就没问题但如果要保留原始值就不能这么转。第四PyTorch 里float64和float32的传递。nn.Linear的权重默认是float32如果你把输入喂成float64许多层会直接报类型不匹配错误。所以在模型推理时输入 tensor 的 dtype 必须和模型参数一致一般统一转成float32后再进入模型。4. 复杂场景实战GPU、非连续内存、形状陷阱4.1 GPU tensor 转 numpy 必须经过 CPU很多新手第一次在 GPU 上跑模型想把中间结果打印成 numpy直接写t torch.randn(3, 3, devicecuda) arr t.numpy() # 报错这必然报错。因为.numpy()支持的底层内存是 CPU 内存GPU 显存里的数据需要先拷回 CPU也就是.cpu()arr t.cpu().numpy()这里有个细节.cpu()返回的 tensor 是一个新的 CPU tensor它与原来的 GPU tensor 不再共享显存但之后对它.numpy()会与这个新 CPU tensor 共享内存。也就是说你修改arr只会影响.cpu()生成的那个临时 tensor不会影响 GPU 上的原始数据。如果你想让 GPU 数据共享内存到 numpy那是不可能的除非先把它放到 CPU。从 numpy 转到 GPU tensor 也一样arr np.arange(6, dtypenp.float32) t_gpu torch.from_numpy(arr).cuda()注意from_numpy先创建一个共享内存的 CPU tensor然后.cuda()会再拷贝一份到 GPU。所以如果你不需要 CPU tensor这个过程会有一次不必要的数据传输和临时内存开销。更直接的方式是torch.tensor(arr, devicecuda)但它默认是拷贝不会共享。在训练循环里频繁tensor.cpu().numpy()是一个很大的性能杀手。因为它会让 GPU 同步等待 CPU 拷贝打断异步流水线。正确做法是只在每个 epoch 验证、或者最后可视化时做这种转换不要在每个 batch 里都打印中间状态。4.2 非连续内存.contiguous()为什么不能随便省先看一个经典报错t torch.arange(6).reshape(2, 3).t() # 转置后 t2 t.view(6) # RuntimeError: view size is not compatible with input tensors size and stride原因不是 view 大小不对而是t在底层内存里不连续。PyTorch 的 tensor 除了数据还保存 stride步长用来描述每个维度在内存里跳多少字节。转置、permute、某些切片操作会生成新的 stride但底层数据顺序还是原来的这时候张量变成了“视图”而不是“翻新拷贝”。如果把这个非连续的 tensor 转 numpy 或强制转换 dtype一些旧版 PyTorch 会直接报错ValueError: NumPy conversion is currently only supported for contiguous tensors.新版本可能做了自动处理但会悄悄多复制一份数据而且可能导致共享内存失效行为不够直观。所以我的习惯是遇到非连续 tensor要么直接.contiguous()要么用.reshape()代替.view()。.contiguous()会重新分配一块连续内存把数据按逻辑顺序搬过去返回一个新的连续 tensor。它不是“原地修改”需要赋值给新变量。在 numpy 转 pytorch 时也可能遇到类似问题。比如你有一个经过切片得到的 numpy 子数组它的数据可能不是 C 风格连续特别是Fortranorder 或转置视图。torch.from_numpy对这种数组会给出警告或者直接报错。稳妥做法是先np.ascontiguousarray(arr)把内存排布整理成连续再转。arr np.random.rand(4, 5).T # 非连续 arr_c np.ascontiguousarray(arr) t torch.from_numpy(arr_c)4.3 形状与维度陷阱0 维数组、批量维、顺序问题转换时形状不会自动变化但有一个经常被误解的情况标量 tensor 转 numpy 得到的是 0 维数组而不是普通的 Python 标量。t torch.tensor(3.14) arr t.numpy() print(arr.shape) # () print(float(arr)) # 3.14如果你只是要数值最好用t.item()既快又能拿到 Python 原生float或int。numpy的标量展示可能很长而且有些库对 0 维数组的处理不友好。另一个坑是维度顺序。numpy 通常使用(H, W, C)表示图像而 PyTorch 里的图像张量习惯是(C, H, W)。如果你直接from_numpy把一张(H, W, C)的图喂给卷积层会得到一堆维度错误。转换完记得permute(2, 0, 1)调整通道顺序同时注意 permute 之后张量不连续再接.contiguous()。还有批量维。模型 inference 时输入要求四维(N, C, H, W)单张图片转过来是三维(C, H, W)需要用.unsqueeze(0)加批维等输出回来要可视化时又抱成三维用.squeeze(0)。这种操作和 dtype 转换叠加在一起时最好写一个统一的数据预处理函数保证输入输出的一致性。5. 常见问题与排查技巧实录5.1 常见报错速查表我把这几年在 numpy/tensor 转换中遇到的典型报错整理成一张表直接照着查报错信息原因解决方案Cant call numpy() on Tensor that requires grad. Use tensor.detach().numpy()张量带梯度信息处于计算图中先.detach().numpy()RuntimeError: Expected object of device type cuda but got device type cputensor 与操作对象的设备不一致确认两边都在 CPU 或都.cuda()GPU tensor 转 numpy 前先.cpu()ValueError: NumPy conversion is currently only supported for contiguous tensors张量底层内存不连续先.contiguous()再转TypeError: cant convert np.ndarray of type numpy.str_numpy 数组是字符串类型映射为整数/数字后转换RuntimeError: expected scalar type Float but found Double输入 dtype 与模型参数 dtype 不一致统一转成torch.float32RuntimeWarning: overflow encountered in cast数值超出了目标 dtype 表示范围先 clip 或换更高精度转换后的数组修改影响了原数组使用了共享内存的转换方式确认语义用.copy()/torch.tensor断开5.2 训练循环里的性能警告不要小看“打印一个 tensor”这件事。如果你的代码里每个 step 都做tensor.detach().cpu().numpy()这一瞬间 GPU 必须把数据拷到 CPU而且可能阻塞后续的异步计算。实测同一个训练循环在验证阶段做numpy()和只在最后做整体 epoch 时间能差 20% 以上。所以我一般会把需要转换的中间量攒下来一个 epoch 结束后统一处理或者在 DataLoader 的 collate 阶段就把数据转好运行过程中尽量不碰 numpy。5.3 我常用的几个小技巧第一写一个to_numpy的包装函数统一处理梯度、设备、连续性问题def to_numpy(tensor): if tensor.requires_grad: tensor tensor.detach() if tensor.is_cuda: tensor tensor.cpu() if not tensor.is_contiguous(): tensor tensor.contiguous() return tensor.numpy()这样每次一行就能拿到稳定的 numpy 数组再也不会因为漏了detach或cpu而卡半天。第二在 debug 时打印张量信息不要只打印值。一个函数可以同时打印 shape、dtype、device、是否连续def debug_tensor(t, nametensor): print(f{name}: shape{t.shape}, dtype{t.dtype}, device{t.device}, contiguous{t.is_contiguous()})这比单纯print(t)有用得多尤其是排查形状和精度问题时。第三检查两个数组是否共享内存可以用 numpy 的np.shares_memory(a, b)。比如不确定from_numpy是否真的共享直接判断一下心里有数。第四在模型保存和加载时注意参数 dtype。比如有些 checkpoint 保存的是float16加载到 CPU 之后模型也是half()这时喂float32的输入就可能报错。加载后统一model.float()并在输入侧也统一 dtype是省心做法。这些技巧单看都不难难的是把它们变成习惯。我个人在实际操作中的体会是numpy 和 pytorch 的转换翻车大多不是因为 API 记不住而是因为没搞清“共享”和“拷贝”、“CPU”和“GPU”、“连续”和“非连续”这三对概念。你把这三对概念刻进脑子再配合一个万能的to_numpy包装函数大部分转换问题基本就不会再来找你。