☰
PyTorch学习笔记:Tensor张量本质、核心操作与实战避坑指南
2026/10/15 10:44:16 网站建设 项目流程

这篇PyTorch学习笔记系列的第一篇,聊聊Tensor(张量)。我在最开始接触PyTorch的时候,被各种概念绕得晕头转向,后来发现只要把Tensor这个“地基”彻底搞懂,后面学自动求导、搭建神经网络都会顺畅很多。这篇笔记是给刚入门深度学习、或者从其他框架转过来的朋友准备的,我会从张量的本质讲起,结合我实际跑代码时踩过的坑,把创建、属性、操作、设备迁移这些核心知识点一次说透。

1. Tensor基础认知:从数据结构角度理解张量

1.1 为什么PyTorch的一切都是张量

在深度学习里,图像、文本、音频这些数据最终都要变成数字才能被模型处理。一张 ( 28 \times 28 ) 的灰度图片,在 PyTorch 里就是一个形状为[1, 28, 28]的张量;一句经过编码的文本,可能变成[batch_size, seq_len, hidden_dim]的三维张量。可以说,张量就是深度学习的“通用货币”。

张量和我们熟悉的矩阵、数组有什么本质区别?简单说,矩阵是二维的,数组是一维的,而张量是n维的。标量是零维张量,向量是一维张量,矩阵是二维张量,三维及以上的统称为高维张量。这种统一的数据结构让不同形态的数据可以走同一套计算逻辑,这也是 PyTorch 把 Tensor 作为核心抽象的原因。

1.2 Tensor与NumPy数组的核心差异

用NumPy的朋友不陌生ndarray,Tensor 和它有相似之处,但有几个关键不同点必须刻在脑子里:

  1. 设备支持:Tensor 可以在 GPU 上运行,ndarray只能待在 CPU 上。深度学习训练动辄上亿次矩阵运算,没有 GPU 加速基本跑不动。
  2. 自动求导:Tensor 自带requires_grad属性,可以记录运算过程并自动计算梯度,NumPy 做这个需要手写反向传播。
  3. 梯度信息:Tensor 可以携带grad_fn和.grad,记录它是由什么操作产生的,方向传播时顺着这个链条反向求导即可。

注意:我见过很多初学者图省事,在训练循环里把 Tensor 转成 NumPy 再操作,这个习惯非常不好。除了打断计算图,CPU 和 GPU 之间的数据拷贝开销也比你想象的大得多。

2. 创建Tensor的七种武器与适用场景

2.1 直接创建:torch.tensor与torch.Tensor的区别

创建 Tensor 的方法看着多,其实核心就是几类。先说最常用的torch.tensor:

import torch # 从列表创建 a = torch.tensor([1, 2, 3]) print(a) # tensor([1, 2, 3]) # 指定数据类型 b = torch.tensor([1.5, 2.5], dtype=torch.float32) print(b.dtype) # torch.float32 # 从numpy创建 import numpy as np arr = np.array([4, 5, 6]) c = torch.tensor(arr)

这里有个新手容易踩的坑:torch.Tensor(注意大写)和torch.tensor(小写)看起来差不多,实际上行为完全不同。torch.Tensor是默认张量类型torch.FloatTensor的别名,它接收的是形状参数;而torch.tensor接收的是具体数据:

# torch.Tensor(3) 创建的是长度为3的未初始化Tensor d = torch.Tensor(3) print(d) # 输出的是未初始化的随机值,看上去像垃圾数据 # torch.tensor(3) 创建的是标量3 e = torch.tensor(3) print(e) # tensor(3)

未初始化的torch.Tensor(3)输出值完全取决于当时内存里的残留数据,属于不确定值。如果你要创建指定形状的矩阵,请用torch.zeros、torch.ones这类明确初始化函数,不要用torch.Tensor(shape)这种写法,因为它不保证内容清零,稍不注意就会引入脏数据。

2.2 规则生成:zeros、ones、eye与rand系列

深度学习中大量场景需要特定形状的初始化张量,我总结了一张常用对照表:

函数作用典型使用场景
torch.zeros(shape)全0张量初始化偏置项、mask矩阵
torch.ones(shape)全1张量初始化某些缩放参数
torch.eye(n)单位矩阵计算某些距离度量
torch.rand(shape)[0,1)均匀分布随机初始化
torch.randn(shape)标准正态分布权重初始化(配合Kaiming初始化等)
torch.arange(start, end, step)等差序列生成位置编码
torch.linspace(start, end, steps)等间隔序列可视化坐标轴取值

特别注意rand和randn的区别。torch.rand生成的是[0, 1)区间的均匀分布随机数,适合概率相关的操作;而torch.randn生成的是均值为0、方差为1的标准正态分布,神经网络的权重初始化基本都用randn,因为它更利于梯度传播的稳定性。

# 一个典型的权重初始化写法 weight = torch.randn(256, 128) * 0.01 # 乘0.01是让初始值足够小,避免刚开始训练时激活值过大

2.3 形状变换:view、reshape与permute

创建完张量,下一步经常是调整形状。view和reshape都能改变张量形状,但有个细微差别:view要求张量在内存中连续(contiguous),reshape则更灵活,如果输入不连续它会自动复制一份再变换形状。

x = torch.randn(2, 3, 4) print(x.shape) # torch.Size([2, 3, 4]) # 展平为 (2, 12) flat = x.view(2, -1) # -1表示自动推导该维度 print(flat.shape) # torch.Size([2, 12]) # 转置维度 (3, 2, 4) transposed = x.permute(1, 0, 2) print(transposed.shape) # torch.Size([3, 2, 4])

这里必须提一个我实际工作中反复踩的坑:做了transpose或permute之后,张量在内存中不再连续,如果这时候直接调用view会报错。比如把[2, 3, 4]转置成[3, 2, 4]后调用view(-1),你会得到一个 runtime error。解决方法是先调用.contiguous()让内存重新连续,再调view。虽然reshape会自动处理,但多一次contiguous()拷贝比不明不白地报错要好,至少你知道发生了什么。

3. 深入理解Tensor的核心属性:shape、dtype与device

3.1 这三者决定了一个Tensor的基本行为

一个 Tensor 的几乎所有操作都绕不开这三个基本属性。打个比方:shape决定了这个张量的“骨架”,dtype决定了数据的“精度规格”,device决定了数据“住在哪里”。三者缺一不可。

  • shape:返回一个torch.Size对象,本质上是整数元组
  • dtype:数据精度,常见的有torch.float32、torch.int64、torch.bool等
  • device:数据存储位置,CPU 或 CUDA 设备
x = torch.randn(64, 3, 32, 32) print(x.shape) # torch.Size([64, 3, 32, 32]) print(x.dtype) # torch.float32 print(x.device) # device(type='cpu')

注意shape返回的torch.Size和普通元组兼容,可以直接索引、相加,比如x.shape[0]或者tuple(x.shape)。

3.2 dtype选择背后的计算逻辑

为什么要费劲区分 dtype?因为不同的精度直接影响内存占用和计算速度。一张[1, 3, 224, 224]的 RGB 图片:

  • float32:每个像素占4字节,总占用 ( 1 \times 3 \times 224 \times 224 \times 4 = 602112 ) 字节,约0.6MB
  • float64:每个像素占8字节,内存直接翻倍

在训练大模型时,模型参数动辄几亿甚至几十亿,float32的权重如果换成float64,显存占用直接翻倍,很多显卡直接爆掉。所以默认情况下 PyTorch 的张量都是float32,这也是计算精度和资源消耗的平衡点。

实际使用中的规律:

  1. 默认torch.tensor([1, 2, 3])得到int64
  2. 默认torch.tensor([1.0, 2.0])得到float32
  3. 深度学习模型的输入和权重默认float32
  4. 标签(分类任务的类别编号)通常用int64

3.3 设备管理:CPU与GPU之间优雅地搬迁

训练深度学习模型,GPU基本是标配。数据在 CPU 和 GPU 之间迁移用.to()方法:

# 判断是否有可用GPU device = torch.device("cuda" if torch.cuda.is_available() else "cpu") x = torch.randn(3, 3) x = x.to(device) print(x.device) # device(type='cuda', index=0)

新手最常犯的错误:模型和数据不在同一个设备上。比如模型在 GPU,数据还在 CPU,一运行就报Expected all tensors to be on the same device。我的习惯是每次操作用.to(device)前都先确认 device 变量定义好了,所有张量和模型统一调用.to(device),不要一会儿.cuda()一会儿.to(device),混用容易乱。

还有一个性能细节:GPU 和 CPU 切换开销不小。频繁的.cpu()再.to("cuda")会带来大量的 PCIe 数据传输,影响训练速度。在一个训练循环里,尽量减少这种来回切换。一般只在需要保存模型或者做可视化的时候才把数据搬回 CPU。

4. 张量的索引、切片与条件筛选

4.1 像切西瓜一样操作Tensor:基本索引

索引和切片是复制 NumPy 的操作习惯,这里有几个高阶技巧:

x = torch.arange(24).reshape(2, 3, 4) # 取第一维的第一个,第二维的第二个元素 print(x[0, 1]) # tensor([4, 5, 6, 7]) # 切片:取所有通道,高度维取一半,宽度维全部 img = torch.randn(3, 224, 224) patch = img[:, 0:112, :] print(patch.shape) # torch.Size([3, 112, 224])

技巧1:使用...省略多余的冒号。对于三维以上张量,x[..., 0]表示取最后一个维度的第0个元素,比写x[:, :, 0]简洁。

技巧2:使用bool掩码做筛选:

scores = torch.randn(5) mask = scores > 0 print(mask) # tensor([True, False, True, ...]) print(scores[mask]) # 只保留大于0的分数

这种布尔索引在模型输出筛选、目标检测的置信度过滤中非常实用。2020年那会儿我做目标检测的置信度过滤就用这个,一句preds[conf_mask]就能把所有低置信度的框全部过滤掉,不用写循环。

4.2 高阶索引:gather与scatter

gather和scatter属于知道能救命、不知道也能凑合过的操作。以gather为例,它的场景是你有一个二维张量,你需要按照索引矩阵沿某个维度取值:

# 一个实际的例子:按索引挑出某个维度上的值 scores = torch.randn(3, 4) indices = torch.tensor([[0, 2, 3, 1], [1, 0, 2, 3]]) # 沿行方向(dim=1)逐个取值 selected = torch.gather(scores, 1, indices) print(selected.shape) # torch.Size([2, 4])

在实现某些损失函数时(如带类别权重的交叉熵、采样式损失),gather几乎是唯一优雅的方案。我见过不少同学写两层循环慢慢取值,那种写法在 batch size 大时性能血崩,gather一行搞定,速度提升几十倍。

4.3 切片后修改的陷阱:in-place操作与计算图

在 PyTorch 里,普通索引返回的是原张量的“视图”(view),修改视图会改变原数据:

x = torch.arange(9).reshape(3, 3) view = x[0, :] view[0] = 999 print(x[0, 0]) # tensor(999) 原数据也被改了

这既是好事(省内存)也是隐患(容易误改数据)。如果你拷贝一份独立数据,使用.clone():

view = x[0, :].clone() view[0] = -1 print(x[0, 0]) # tensor(999) 原数据不受影响

这个坑在模型调试时非常隐蔽。我见过有人调试 LOSS 异常,查了半天发现是某处索引返回视图后不小心改了输入数据,导致后续 batch 全部用了脏数据。如果你的张量requires_grad=True,视图的修改还会影响计算图,报出“leaf variable has been modified”之类的错误。

5. 张量的核心运算:逐元素、矩阵乘法与广播机制

5.1 逐元素运算:加减乘除与激活函数

逐元素运算就是两个张量对应位置直接算。这点和 NumPy 完全类似:

a = torch.tensor([1.0, 2.0, 3.0]) b = torch.tensor([4.0, 5.0, 6.0]) print(a + b) # tensor([5., 7., 9.]) print(a * b) # tensor([ 4., 10., 18.]) 逐元素乘法 print(a ** 2) # tensor([1., 4., 9.])

注意*是逐元素乘法,@(或者torch.matmul)才是矩阵乘法,两者绝不能混用。

激活函数也是一类逐元素运算。PyTorch 把relu、sigmoid、tanh等都封装好了:

x = torch.randn(3, 3) y = torch.relu(x) # 等价于 max(0, x) z = torch.sigmoid(x) # 将值压缩到 (0, 1)

5.2 矩阵乘法:torch.mm与torch.matmul的适用场景

矩阵乘法在深度学习中无处不在,全连接层、注意力机制、卷积实现,核心都是矩阵乘法。常用的有三个函数:

  • torch.mm(a, b):只能处理二维矩阵乘法,要求维度严格匹配
  • torch.matmul(a, b):支持广播和更高维度的 batch 矩阵乘法
  • a @ b:matmul的运算符写法
a = torch.randn(3, 4) b = torch.randn(4, 5) c = a @ b print(c.shape) # torch.Size([3, 5]) # 批量矩阵乘法:batch维自动匹配 batched_a = torch.randn(10, 3, 4) batched_b = torch.randn(10, 4, 5) batched_c = batched_a @ batched_b print(batched_c.shape) # torch.Size([10, 3, 5])

在自注意力机制里,Q @ K.T得到的形状是[batch, seq_len, seq_len],再乘V变成[batch, seq_len, dim],这些都是matmul的标准用法。新手容易混淆的一点是矩阵乘法的维度匹配规则:(..., M, K) @ (..., K, N) -> (..., M, N),中间那个K必须相等,这个规则建议用笔写在笔记本第一页。

5.3 广播机制:让不同形状的张量做运算

广播机制是 PyTorch 最强大也最容易出 bug 的特性。什么叫广播?它允许形状不完全匹配的两个张量进行运算,PyTorch 会自动扩展维度。最常见的例子:

a = torch.randn(3, 4) b = torch.randn(4) # b可以被广播成(3, 4) c = a + b print(c.shape) # torch.Size([3, 4])

广播规则的核心逻辑是:从最后一个维度开始对齐,如果两个维度相等,继续对齐前一个维度;如果某个维度为1,则该维度可以拉伸;如果某个维度一个存在一个不存在,存在的那个维度需要是1或者另一个可以扩展。无法匹配时直接报错。

我踩过最隐蔽的坑是形状为[3, 1]和[4]的运算,因为维度为1的存在,两者可以广播成[3, 4],但有时候这不是你想要的。比如目标检测中做 anchor 和 gt 的 IoU 计算,[num_anchor, 4]和[num_gt, 4]之间如果中间维度配错,广播出来的结果会非常离谱,而且完全不报错。建议在关键运算前后都打印一下.shape,确认输出的形状符合预期,别省这一步。

5.4 归并运算:sum、mean、max与dim参数

归并运算把张量某个维度的信息压缩成更小的张量。dim参数是这里的关键,它指定要压缩哪个维度:

x = torch.randn(4, 3) print(x.shape) # torch.Size([4, 3]) # 按列求均值,压缩第0维,结果是每列的平均值 col_mean = x.mean(dim=0) print(col_mean.shape) # torch.Size([3]) # 按行求均值,压缩第1维 row_mean = x.mean(dim=1) print(row_mean.shape) # torch.Size([4])

dim=0和dim=1的区别搞混是所有新手的必经之痛。我自己的记忆技巧:dim=0表示从行这个维度上压缩,即把每一列的多行数据合并成一个数;dim=1表示从列这个维度上压缩。当你把张量想象成一个 Excel 表格时,dim=0的操作相当于“按列汇总”,dim=1相当于“按行汇总”。

还有一个keepdim=True参数经常被忽略。如果设置x.mean(dim=0, keepdim=True),结果形状是[1, 3]而不是[3],这在后续做广播运算时会省掉很多维数匹配麻烦。

6. 张量与NumPy的双向转换

6.1 转换方法:numpy()与from_numpy()

深度学习的预处理通常在 NumPy 里做,数据到了模型输入阶段要转成 Tensor;而结果输出后为了可视化或保存,又往往要转回 NumPy。双向转换代码如下:

import numpy as np import torch # NumPy -> Tensor arr = np.array([[1, 2], [3, 4]]) tensor = torch.from_numpy(arr) print(tensor) # tensor([[1, 2], [3, 4]]) # Tensor -> NumPy back = tensor.numpy() print(type(back)) # <class 'numpy.ndarray'>

torch.tensor(arr)和torch.from_numpy(arr)都能实现转换,但有一个重要区别:torch.from_numpy生成的新 Tensor 与原始 NumPy 数组共享内存。修改其中一个,另一个也会变,好处是零拷贝、速度快。

6.2 共享内存带来的连锁风险

共享内存有两面性。在做数据增强时,如果你用from_numpy把图像数组转成 Tensor,然后就地修改这个 Tensor,原始 NumPy 里的图像数据也会被悄悄改掉,这可能导致你的数据集被污染。下面的代码展示这个现象:

arr = np.array([1.0, 2.0, 3.0]) tensor = torch.from_numpy(arr) tensor[0] = 999.0 print(arr) # [999. 2. 3.] 原数组被修改了!

如果不想共享内存,老老实实用torch.tensor(arr)复制一份数据。这里复制开销虽然比from_numpy大,但换来了数据安全,尤其是在数据加载流水线里,避免无意中改了原始数据。

还有一个细节:在 GPU 上运行的 Tensor 不能直接.numpy(),因为 NumPy 只支持 CPU 数据。必须先.cpu()再.numpy():

tensor_gpu = torch.randn(3).to("cuda") arr = tensor_gpu.cpu().numpy() # 先搬回CPU再转

6.3 数据类型一致性的坑

NumPy 转 Tensor 时,数据格式必须提前对齐。比如你的标签数组是np.int64,转成 Tensor 后就是torch.int64;如果你的模型设定标签是int64,交叉熵损失函数正好配套。但如果 NumPy 传进来的是int32,而模型用int64,接口就会报错。所以我习惯在预处理阶段显式指定 dtype:

arr = arr.astype(np.float32) # 转成float32 tensor = torch.from_numpy(arr) # 得到torch.float32的Tensor

另外注意:PyTorch 的float64(即double)在很多 GPU 算子上支持有限,运算效率也低。如果发现你的 Tensor 总是torch.float64类型,很可能是 NumPy 里的float64被带过来了,在转 Tensor 前一定先转成float32。

7. 张量的自动求导基础:requires_grad与计算图

7.1 叶子张量与requires_grad标记

Tensor 和 NumPy 的最大差别就是自动求导。当你设置requires_grad=True,PyTorch 就会开始记录这个张量上的所有操作,构建一张计算图,之后就可以自动反向传播计算梯度:

x = torch.tensor([2.0], requires_grad=True) y = x ** 2 + 3 * x y.backward() print(x.grad) # tensor([7.]) 即 2*x + 3 在 x=2 时的导数

这里x是叶子张量(leaf tensor),它没有依赖任何其他需要梯度的张量,梯度最终就累积在这个叶子张量上。参数requires_grad的传播规则是:如果一个 Tensor 需要梯度,那么所有由它运算得到的 Tensor 也都需要梯度。

7.2 计算图:前向与反向的完整链路

计算图是个有向无环图。在前向传播时,数据从x经过x**2、3*x等操作流到y,每个操作都会创建一个节点并记录实际数值;调用backward()时,PyTorch 从y出发,利用链式法则把梯度逐层传回。

举一个实际模型训练的例子:

import torch.nn as nn model = nn.Linear(4, 1) optimizer = torch.optim.SGD(model.parameters(), lr=0.01) inputs = torch.randn(8, 4) labels = torch.randn(8, 1) outputs = model(inputs) loss = ((outputs - labels) ** 2).mean() optimizer.zero_grad() loss.backward() optimizer.step()

这里的关键链条:inputs -> model -> outputs -> loss。loss.backward()执行完成后,model里的参数(weight和bias)都累积了.grad梯度,然后optimizer.step()根据这些梯度更新参数值。

7.3 梯度累积与zero_grad的必要性

上面代码中的optimizer.zero_grad()很多人不理解为什么每次训练都要调用。原因很简单:PyTorch 的梯度是累积的,你不清零,下一次backward()得到的梯度会和上一次的叠加。如果不zero_grad()做一次彻底的清零,模型参数更新会越走越偏,loss 表现会异常发散。

我在 Debug 时见过太多新手出现这种状况:loss 一开始下降,然后突然变大然后完全发散。检查半天发现是忘记zero_grad(),梯度翻来覆去累加,参数更新幅度爆炸。这个动作虽然不起眼,却是训练稳定的基本保障。

对于不需要计算梯度的场景,用torch.no_grad()包住代码块则能显著提升推理速度,并省去记录计算图的内存开销:

with torch.no_grad(): outputs = model(inputs) # 这里不会构建计算图

在验证集上评估模型、推理部署时,这个上下文管理器几乎必用。

8. 常见问题与排查技巧实录

8.1 设备不一致:Expected all tensors to be on the same device

这个报错是新手出现频率最高的。大意是模型中某些参数在 GPU 上,但你给的数据还在 CPU。解决思路很简单,统一设备和风格:

device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) inputs = inputs.to(device) labels = labels.to(device)

训练循环里每次拿到一个 batch,第一行就写.to(device),形成肌肉记忆。千万不要混用.cuda()和.to(device),两种风格并存极易导致某些张量漏迁移。

8.2 数据形状不匹配:size mismatch报错

矩阵乘法中维度不匹配会报size mismatch。排查思路很直接:打印相关张量的shape,检查维度口诀(..., M, K) @ (..., K, N)。特别注意转置后的形状变化,x.T会交换最后两个维度,这在矩阵乘法里很常见:

a = torch.randn(3, 4) b = torch.randn(3, 5) # 错误:a @ b 不行,因为 4 != 3 # 正确:a.T @ b,形状变成 (4, 3) @ (3, 5) -> (4, 5)

8.3 dtype不匹配与自动类型提升的坑

PyTorch 的默认规则是浮点数和整数运算会报错,两个不同类型的浮点数运算可能触发自动提升类型。但神经网络大多数算子要求所有输入都是同一 dtype,尤其是float32。如果报expected scalar type Float but found Long,说明你传入了一个int64的张量给一个要求浮点数的算子。解决办法是在创建时指定dtype=torch.float32或者用.float()转换。

8.4 梯度爆炸与梯度消失的早期信号

训练时如果 loss 变成 NaN 或 inf,大概率是梯度爆炸。先用下面几步定位:

  1. 打印每一层的权重梯度范数,定位爆炸层
  2. 检查学习率是否设置过大
  3. 对输入做归一化(数据标准化)
  4. 使用梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

梯度消失则表现为训练很久 loss 不降,这时检查是否用了不合适的激活函数,或者网络初始化方式不佳。relu比sigmoid能更好缓解梯度消失,权重初始化用 Kaiming 初始化而非默认的全 0 初始化。

8.5 显存不足:CUDA out of memory

显存爆掉处理要讲究方法:

  • 调小 batch_size:第一步,直接立竿见影
  • 确认没有把整个数据集一次性搬到 GPU
  • 训练结束时及时del大变量,并调用torch.cuda.empty_cache()
  • 使用torch.no_grad()包裹验证代码
  • 如果验证集仍然爆显存,可以把验证逻辑拆成多个小 batch

这个问题的根源很多人都没意识到:显存里存的不仅仅是你当前的 batch,还有整个计算图的信息,因为反向传播需要每个节点的中间激活值。模型越深、batch 越大,缓存激活值占用显存就越多。

9. 效率优化与实操心得

9.1 多写向量化代码,少写显式循环

用循环逐步操作张量,性能和代码优雅度都会输得很惨。以计算一组向量两两之间的欧氏距离为例:

# 不推荐:双重循环 def pairwise_distance_loop(x): n = x.shape[0] dist = torch.zeros(n, n) for i in range(n): for j in range(n): dist[i, j] = ((x[i] - x[j]) ** 2).sum() ** 0.5 return dist # 推荐:利用广播和矩阵运算 def pairwise_distance_vec(x): # (n, d) -> (n, 1, d) 广播到 (n, n, d) diff = x[:, None, :] - x[None, :, :] dist = (diff ** 2).sum(dim=-1) ** 0.5 return dist

显式循环在 CPU 上慢得让人无法忍受,向量化之后可以利用底层的高效矩阵运算库并行计算,快几个数量级。

9.2 善用TorchScript和JIT提升推理性能

如果你把模型的推理阶段用于生产,可以考虑用torch.jit.script或torch.jit.trace把模型转为 TorchScript。Script 后的模型不依赖 Python 解释器,可以序列化保存加载,在 C++ 环境部署时非常有用。不过要注意:脚本化之后部分动态控制流可能需要额外处理,建议先用trace试试,不行再上script。

9.3 把关键检查写成断言

在训练脚本开头和关键节点加上断言,可以快速定位问题:

assert inputs.shape[1] == 3, f"期望3通道输入,实际 {inputs.shape[1]} 通道" assert inputs.dtype == torch.float32, f"输入dtype为 {inputs.dtype},应为float32" assert labels.max() < num_classes, "标签值超出类别数"

这些断言在初期调试期会帮你挡住大量隐蔽问题。等确定了流水线稳定,可以在训练脚本里加一个--check参数,只有开启时才断言,避免生产推理时的额外开销。

9.4 一个完整的Tensor练习Demo

把所有知识点串起来,这里给一个可以直接跑的练习脚本:

import torch import torch.nn as nn def demo(): # 1. 创建设备 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 2. 创建数据:模拟一个batch的32张28x28灰度图 images = torch.randn(32, 1, 28, 28).to(device) labels = torch.randint(0, 10, (32,)).to(device) # 3. 标签做one-hot编码示例 one_hot = torch.zeros(32, 10).scatter_(1, labels.unsqueeze(1), 1) # 4. 展平特征做线性分类 flat_images = images.view(32, -1) linear = nn.Linear(28 * 28, 10).to(device) outputs = linear(flat_images) print("输出形状:", outputs.shape) # 5. 计算损失并反向传播 loss = ((outputs - one_hot) ** 2).mean() loss.backward() print("梯度形状:", linear.weight.grad.shape) if __name__ == "__main__": demo()

这个 demo 涵盖了设备迁移、形状变换、随机种子创建、one-hot 编码、线性层、损失计算和反向传播,基本把 Tensor 的核心知识点全串起来了。

10. 后续学习路线建议

Tensor 的笔记到这里就告一段落。我个人觉得,掌握 Tensor 有三个阶段可以自我对照:

第一阶段是会用:能创建张量、改 shape、做运算,遇到报错知道怎么回事。这个阶段基本熟悉了操作 API。

第二阶段是懂内存:知道哪些操作是视图、哪些是拷贝、怎么避免不必要的内存复制、广播操作的真正开销在哪里。这个阶段写出来的代码性能会有明显提升。

第三阶段是懂计算图:理解requires_grad传播、梯度累积、反向传播的链路。这个阶段就可以开始探索自定义autograd.Function、自定义复杂的训练逻辑。

从 Tensor 到完整训练一个模型,下一站通常是torch.nn.Module、数据集加载DataLoader、优化器选择、训练循环编写。建议学习路径是:Tensor -> 自动求导(autograd)->nn.Module搭建网络 -> DataLoader 加载数据 -> 训练循环 -> 模型保存和加载。每一步都要亲手敲代码,光看笔记永远学不会。我自己带过几个实习的同学,他们进步最快的时候往往不是看资料的时候,而是被 bug 卡住后自己翻文档排查的时候。

最后分享一个实战中的小经验:把常用的 Tensor 操作整理成自己的速查表,写在一个笔记文件里,遇到不确定的 API 先查自己的速查表,比翻官方文档快很多。等用熟了,你自然会形成一套自己的常用操作库。希望这篇 Tensor 学习笔记对你有帮助,下一篇笔记我们聊自动求导,把梯度传播的机制彻底讲透。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询