跳转到内容

Tensor、形状与 NumPy 对照

计算机基础 · PyTorch

前置知识:ndarray、shape 与 dtype · 广播、axis 与向量化

本章目录

本章学习目标

  • 解释 Tensor 的 shape、dtype、device 与存储关系
  • 区分复制构造、共享内存和脱离梯度
  • 用 CPU 核对广播、矩阵乘法和张量转换

先把数组知识迁移到张量

Tensor 是带有形状、数据类型和设备信息的多维数值对象。它与 ndarray 一样可以表达一列数据、一张特征表或更高维批次;不同之处在于 PyTorch 为张量计算提供自动求导和设备迁移机制。课程统一使用 CPU,不需要安装 CUDA,也不把显卡当作学习前提。先掌握二维表格输入比直接拿复杂网络更容易看清每个操作的含义。

约定输入 X 的形状为样本数乘特征数,权重 W 为特征数乘输出数,偏置 b 为输出数。三行两列的 X 与两行一列的 W 相乘,得到三行一列预测。三行一列与三元素的一维数组不是同一形状;后面计算损失时,这种差别可能使广播悄悄生成三行三列的数据。输出看起来都是三个数字,并不能说明形状可以混用。

检查张量时先打印 shape、dtype、device,再问它是否需要梯度。整型适合索引和类别标签,浮点型适合连续特征和可微参数。浮点计算也有精度误差,不能把所有数值比较都写成完全相等。示例使用 float32,矩阵运算与 NumPy 对照时使用容差判断。

创建方式决定存储是否共享

torch.tensor 通常复制输入数据,torch.from_numpy 则与兼容的 NumPy 数组共享 CPU 内存。因此在数组上改一个元素,from_numpy 得到的张量也会改变。共享内存适合减少无意义复制,却要求你明确数据的所有权。它不能凭空把 CPU 数组变成 GPU 数组;迁移设备需要另一个显式操作。

从一个已有张量获得独立存储可用 clone;detach 只切断自动求导关系,不保证复制存储。如果需要既切断梯度又独立保存数值,使用 detach().clone()。这些操作回答两个不同问题:有没有求导关系,以及是否共享内存。把它们混成一句“复制一下”很容易在训练或缓存数据时留下隐藏修改。

本章小实验先创建数组 [1,2,3],分别用 tensor 和 from_numpy 构造张量,再修改数组第一项。共享张量出现变化,复制张量保持原值。接着用 clone 保存独立版本,再修改共享张量,独立版本不变。输出用于解释实际机制,而不是靠变量名猜测对象是否独立。

算清广播和矩阵乘法

逐元素乘法使用星号,矩阵乘法使用 @ 或 matmul。对于 X 三行两列,乘一个两元素向量会按最后一维广播,使每列按对应系数缩放;X @ W 则把每行两项加权合成为输出。两者的结果形状和表达的任务都不同。张量广播从末尾维度比较,相同或其中一个为一才允许扩展。

取 X=[[1,2],[3,4],[5,6]]、W=[[2],[1]],逐行相乘相加得到 [4,10,16],保留二维输出得到三行一列。随后加偏置 [0.5],每个样本都加同一个数。先手算这一组结果,再核对代码,能够判断代码在做线性预测还是仅仅缩放两列。

reshape 重新解释形状,转置交换轴;它们不是随意修正报错的办法。应该先写每个轴的业务意义,再选择操作。非连续张量使用 view 可能报错,reshape 在必要时会复制,因此不要把 reshape 等同于零成本。数据规模较大时还要考虑内存布局和复制成本。

从 Tensor 回到分析表

需要导出预测到 pandas 或 NumPy 时,通常先从计算图脱离,再移到 CPU,最后调用 numpy。课程 CPU 示例使用 prediction.detach().cpu().numpy(),这样在迁移到其他设备时也有明确路径。不能对仍需要梯度的张量直接调用 numpy;这个限制避免用户通过另一个数组接口进行无法追踪的修改。

张量中通常没有 DataFrame 的行索引和列名,所以转换前要单独保存样本键与特征顺序。同样的数据如果列顺序变了,模型收到的数值虽然形状正确,含义却完全错误。模型接入的检查至少包括特征列、数据类型、缺失值规则、样本数量和键的一致性。

不要依赖 set_default_dtype 或默认设备来隐式规定所有脚本行为。示例明确指定 dtype 和 device,并在关键步骤加 assert。设备不一致、整型输入、错误维度是比网络层数更常见的入门问题。章节脚本用小数据验证转换、广播和共享存储;大批量任务应在此基础上逐步扩展。

完整示例与实际输出

import numpy as np
import torch
a=np.array([1.,2.,3.],dtype=np.float32)
shared=torch.from_numpy(a)
copied=torch.tensor(a)
a[0]=9
assert shared[0].item()==9 and copied[0].item()==1
independent=shared.clone()
shared[1]=7
assert independent[1].item()==2
X=torch.tensor([[1.,2.],[3.,4.],[5.,6.]])
W=torch.tensor([[2.],[1.]])
y=X@W+torch.tensor([.5])
assert y.shape==(3,1)
np.testing.assert_allclose(y.numpy().ravel(),[4.5,10.5,16.5])
assert (X*torch.tensor([2.,1.])).shape==(3,2)
print('device=',X.device,'dtype=',X.dtype,'shape=',tuple(y.shape))
print('shared=',shared.tolist(),'copied=',copied.tolist(),'prediction=',y.tolist())

实际运行通过CPU Python execution.

device= cpu dtype= torch.float32 shape= (3, 1)
shared= [9.0, 7.0, 3.0] copied= [1.0, 2.0, 3.0] prediction= [[4.5], [10.5], [16.5]]

五道自测:先回答,再核对

01 · X 是 (8,3),W 是 (3,2),X@W 的形状是什么?

查看提示

对齐矩阵乘法的内侧维度。

查看参考答案

(8,2):每个样本输出两个数。

02 · detach 是否保证不与原张量共享内存?

查看提示

分别思考梯度关系和内存关系。

查看参考答案

不保证。需要独立存储时使用 detach().clone()。

03 · from_numpy 创建的张量改动后会影响原数组吗?

查看提示

观察示例的两个构造函数。

查看参考答案

兼容的 CPU 数组与张量共享内存,改动可相互影响。

04 · 三行一列减去三个元素的一维目标有什么风险?

查看提示

从末尾维度逐一比较。

查看参考答案

广播可能得到 (3,3),而不是期望的 (3,1) 残差。

05 · 为什么导出预测前要保留样本键?

查看提示

区分数值容器和带标签的表。

查看参考答案

Tensor 不保存 DataFrame 的行索引;需要键恢复预测与原记录的对应关系。

两项动手任务

任务 1 · 构造四行两列输入,计算单输出预测并与 NumPy 核对。

提示

先写出输入、输出、形状和判断条件,再运行小数据。

完整参考解答与检查方式
import numpy as np, torch
a=np.arange(8,dtype=np.float32).reshape(4,2)
w=np.array([[2.],[-1.]],dtype=np.float32)
out=torch.from_numpy(a)@torch.from_numpy(w)
assert out.shape==(4,1)
np.testing.assert_allclose(out.numpy(),a@w)
print(out.tolist())

任务 2 · 生成一个需要梯度的张量,保存独立 NumPy 副本并证明后续修改不影响它。

提示

先写出输入、输出、形状和判断条件,再运行小数据。

完整参考解答与检查方式
import numpy as np, torch
t=torch.tensor([1.,2.],requires_grad=True)
snapshot=t.detach().cpu().numpy().copy()
with torch.no_grad(): t.add_(10)
np.testing.assert_allclose(snapshot,[1.,2.])
print(snapshot)

detach 没有复制存储,因此这里 numpy 后仍需要 copy。

官方资料与掌握状态

完成两项任务后,按独立完成程度记录。状态会尽量保存在当前浏览器,可导出备份。

动手任务记录

掌握状态与复习

关掉资料独立完成后记 A;提示后完成记 B;需要重学记 C。

记录尽量保存在当前浏览器,建议定期导出备份。