news 2026/9/14 16:35:29

PyTorch Tensor 完全指南:从创建、广播到自动求导的踩坑总结

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyTorch Tensor 完全指南:从创建、广播到自动求导的踩坑总结

学深度学习绕不开的第一个基础概念,就是 Tensor。我当初翻《动手学深度学习》(D2L)的时候,前面数据操作和自动微分这几章看着简单,实际动手敲却踩了不少坑。后来把 Tensor 这部分彻底吃透,再回头看模型训练、反向传播这些内容,明显顺了很多。这篇笔记就把我在 Tensor 学习过程中整理出来的核心要点、常用代码和踩坑经验一次性写出来,给正在入门深度学习、尤其是刚开始用 PyTorch 的朋友做个参考。如果你是零基础也不用担心,我会尽量用生活化的方式把维度、形状、广播这些听起来高深的概念讲清楚;只要本地能跑起 Python 环境,照着代码敲一遍,Tensor 的基本功就算打下来了。

1. Tensor 是什么:先搞懂这个深度学习里的万能容器

1.1 从一维到N维:用三个例子理解 Tensor 的 shape

Tensor 最朴素的理解就一句话:它是一个可以装任意维度数据的容器。一维的 Tensor 就像一张成绩单,只能装一串数字,例如 40 个学生的语文分数;二维的 Tensor 就是一张表格,40 个学生各考 6 门课,于是有了 40 行乘以 6 列;三维的 Tensor 可以理解成一本表格合集,40 个学生、6 门课、4 个学期,三个维度就分别是学生、科目、学期。

在深度学习里,最常见的四维 Tensor 是图像数据,shape 通常是(batch_size, channel, height, width)。比如一批 32 张 224x224 的彩色图片,对应的 Tensor 就是(32, 3, 224, 224),中间的 3 是 RGB 三个通道。这个维度顺序初学者可能一时记不住,但多看几次就条件反射了。

我用代码随手创建一个三维 Tensor,方便你直接观察:

import torch x = torch.arange(24).reshape(2, 3, 4) print(x.shape) # torch.Size([2, 3, 4]) print(x)

输出结果是一组 2 个 3 行 4 列的数字块。理解 shape 的关键,是从“最外层到最内层”逐层数括号:最外层有 2 个块,每个块里有 3 行,每行有 4 个元素。以后你看到任何高维 Tensor,只要用这个“逐层数”的方法,就能快速搞清楚它的结构。

1.2 为什么深度学习框架偏爱 Tensor 而不是 NumPy

有 NumPy 基础的同学可能会问:NumPy 数组也能做多维运算,它和 Tensor 到底有什么区别?答案是:功能上两者确实高度重合,但 Tensor 是为深度学习量身定制的。

第一,Tensor 原生支持 GPU 加速。把数据放到 GPU 上之后,大规模矩阵运算可以并行执行,速度比 CPU 快几个数量级。NumPy 本身不支持这种运算,虽然可以通过第三方库间接调用 GPU,但体验远不如 Tensor 来得自然。第二,Tensor 有自动求导机制。训练神经网络的核心是反向传播求梯度,PyTorch 的 Tensor 会记录每一步运算,之后通过backward()自动算出梯度,这是 NumPy 做不到的。第三,Tensor 和 PyTorch 的神经网络模块深度集成,模型的输入输出、参数、损失函数,全部是用 Tensor 来流转的。

所以总结下来就是:NumPy 适合做通用科学计算,Tensor 是深度学习生态里的“通用货币”。也不是说学了 Tensor 就不用 NumPy,实际项目中两者经常混用,后面我会讲到它们之间怎么互转。

1.3 三大关键属性:shape、dtype、device

拿到一个 Tensor,第一反应应该看三样东西:形状(shape)、数据类型(dtype)、所在设备(device)。这三个属性决定了你能不能做某次运算,也决定了你能不能拿到想要的结果。

  • shape:形状,即每个维度的大小。它决定参与运算的矩阵能不能对齐。
  • dtype:数据类型。PyTorch 里常用的是torch.float32torch.float64torch.int64等。神经网络里的权重和激活值一般都用torch.float32,整数类型通常用来做索引或者标签。
  • device:数据在 CPU 还是 GPU 上。CPU 上的 Tensor 和 GPU 上的 Tensor 不能直接做运算,这是一个非常常见的报错来源。

来看一段实际代码:

x = torch.tensor([1, 2, 3], dtype=torch.float32) print(x.shape) # torch.Size([3]) print(x.dtype) # torch.float32 print(x.device) # cpu

值得注意的是,PyTorch 的默认浮点类型是torch.float32,默认整数类型是torch.int64。如果你用 Python 的float列表创建 Tensor,得到的就是torch.float32。后面训练模型时,如果模型参数是float32,而输入数据是float64,就可能触发类型不匹配的报错,这类问题我放到第 4 章踩坑部分细讲。

2. Tensor 创建与常用操作:照着抄就能跑通的代码清单

2.1 七种创建 Tensor 的方式与适用场景

我把自己平时用得最多的创建方式整理成了一张清单,初学阶段建议全部敲一遍,形成肌肉记忆。

import torch import numpy as np # 1. 从列表或数组创建 a = torch.tensor([[1, 2], [3, 4]]) b = torch.tensor(np.array([1.0, 2.0, 3.0])) # 2. 全0、全1、对角线为1的矩阵 zeros = torch.zeros((2, 3)) ones = torch.ones((3, 2)) eye = torch.eye(3) # 3. 随机数 rand = torch.randn(2, 3) # 标准正态分布 uniform = torch.rand(2, 3) # [0, 1) 均匀分布 randint = torch.randint(0, 10, (2, 3)) # 整数均匀分布 # 4. 等差数列 ar = torch.arange(0, 10, 2) # 从0到10,步长为2 lin = torch.linspace(0, 1, 5) # 从0到1等分5个点 # 5. 指定数值填充 full = torch.full((2, 3), 7.0) # 6. 形状复制 x = torch.randn(2, 3) like = torch.zeros_like(x) # 保持形状和dtype,填充0 # 7. 指定设备和是否需要梯度 gpu_tensor = torch.randn(2, 3, device='cuda' if torch.cuda.is_available() else 'cpu') param = torch.randn(2, 3, requires_grad=True)

每个都有它的使用场景。torch.randn常用于初始化模型权重,因为标准正态分布是神经网络参数初始化的常见选择;torch.arange适合生成序列数据或者位置编码;torch.zeros_liketorch.ones_like在实现自定义层时特别方便,不需要手动写死 shape。

2.2 reshape 与 view:这两个到底差在哪

reshapeview是改变 Tensor 形状最常用的两个方法,也是一对容易踩坑的兄弟。

view是视图操作,不复制底层数据,新旧 Tensor 共享同一块内存。但是view有个限制:要求原 Tensor 在内存中是连续的。如果对 Tensor 做了转置、切片等操作,内存可能就不再连续,此时调用view会直接报错。

reshape则更宽容:只要形状合法,它能重新排列数据,如果内存不连续,它会自动复制一份再调整形状。代价就是可能多一次内存拷贝,性能略低于view

来看一个真实的报错场景:

x = torch.arange(12).reshape(3, 4) y = x.t() # 转置,此时内存不连续 # y.view(12) # 报错 y.reshape(12) # 正常运行

初学阶段我的建议很简单:统一用reshape,先把需求搞定,等以后优化性能时再考虑view。另外还有一个操作contiguous(),可以把不连续的 Tensor 在内存中变成连续,之后就能继续用view了。它们之间的关系可以理解为:view是最省内存的视图操作,reshape是更安全的选择,contiguous()则是修复内存布局的工具。

2.3 索引、切片和算术运算的注意点

Tensor 的索引和切片跟 NumPy 几乎一模一样,上手很快,但要记住一个重要机制:切片返回的是视图,不是拷贝。这意味着你修改切片的结果,原 Tensor 也会跟着变。

x = torch.arange(12).reshape(3, 4) sub = x[1:3, :] sub[0, 0] = 999 print(x) # tensor([[ 0, 1, 2, 3], # [999, 5, 6, 7], # [ 8, 9, 10, 11]])

如果你的需求是“取出一部分数据独立修改,不影响原 Tensor”,记得先调用.clone(),比如x[1:3].clone()

算术运算里,初学阶段最容易忽略的是矩阵乘法和逐元素乘法的区别。PyTorch 里*表示逐元素相乘,形状必须能对齐或者广播;矩阵乘法用的是@torch.mmtorch.matmul。三个矩阵相乘的例子:

A = torch.randn(3, 4) B = torch.randn(4, 5) C = A @ B # 矩阵乘法,结果是 (3, 5) D = A * A # 逐元素平方,同形状

在《动手学深度学习》后续的层归一化、注意力机制里,到处都用到矩阵乘法,如果一开始就分清楚*@,后面看代码会轻松很多。

2.4 广播机制:不同形状的张量怎么相加

广播机制是 Tensor 操作里最“魔法”也最容易出 bug 的地方。简单说,当两个 Tensor 形状不完全一致时,PyTorch 会尝试把它们的维度自动扩展到一致,再进行运算。

具体规则是:从最后一个维度开始往前对齐,每个维度上的尺寸要么相等,要么其中一个为 1。比如(3, 1)(1, 4)相加时,第一对是 3 和 1,扩展成 3;第二对是 1 和 4,扩展成 4,最终得到(3, 4)

a = torch.tensor([[1.0], [2.0], [3.0]]) # shape (3, 1) b = torch.tensor([[10.0, 20.0, 30.0]]) # shape (1, 3) c = a + b print(c.shape) # torch.Size([3, 3])

我理解广播的方式是“复制后计算”:把(3, 1)的每一行复制 3 份,把(1, 3)的每一列复制 3 份,两个都变成了(3, 3),然后相加。不过真实实现并不会真的复制数据,只是逻辑上这么理解。

最容易出错的情况是维度不满足规则,比如(3, 2)(2, 3)相加,最后一位一个是 2 一个是 3,既不相等也没有 1,就会直接报错。解决广播报错的方法就是打印两个 Tensor 的shape,一个个维度去检查。

2.5 Tensor 与 NumPy 互转:共享内存的坑

Tensor 和 NumPy 的互转在数据处理阶段非常常见。torch.from_numpy()可以把 NumPy 数组变成 Tensor,.numpy()可以把 Tensor 变成 NumPy 数组。但有一个重要机制:这种转换是共享底层内存的,修改任意一方都会影响另一方。

arr = np.array([1.0, 2.0, 3.0]) t = torch.from_numpy(arr) t[0] = 100.0 print(arr) # [100. 2. 3.]

如果不想要这种耦合,就显式调用.clone()np.array(...)复制一份。另外,PyTorch 的to()方法可以实现 Tensor 在 dtype、device 之间的转换,比如t.float()t.to('cuda'),这些方法在训练循环里会频繁用到。

3. 自动求导机制:Tensor 凭什么是深度学习的基石

3.1 计算图与 requires_grad 的基础逻辑

深度学习训练的核心是梯度下降,而梯度来自反向传播。PyTorch 的自动求导机制,就是让所有可训练参数在计算过程中自动记录“历史”,然后在需要时一次性算出每个参数的梯度。

为了让 Tensor 参与自动求导,需要在创建时设置requires_grad=True。这个标志告诉 PyTorch:我需要在反向传播时计算关于这个张量的梯度。

x = torch.tensor([2.0], requires_grad=True) y = x ** 2

此时y就带有一个grad_fn属性,记录它是通过pow操作得到的。你可以把计算图理解成一条流水线:每个操作是一个节点,数据从最开始的输入流到最终的输出。反向传播时,误差从最后一个节点逆着流水线传回来,每个节点计算出局部梯度并传给前面一层。

我最初学这个机制时走了弯路,总想手动去推每个参数的梯度公式。后来发现,理解自动求导的关键是信任框架:你只要定义好前向计算和损失函数,调用backward(),梯度就会自动挂在每个requires_grad=True的张量的.grad属性上。

3.2 一次 backward 后到底发生了什么

来看一个最经典的最小例子,理解backward()的完整链路:

x = torch.tensor([2.0], requires_grad=True) y = x ** 2 y.backward() print(x.grad) # tensor([4.])

y = x^2x=2处的导数是4,所以x.grad的结果是4。这个例子简单,但已经说明了两件事:第一,backward()是从y这个标量开始的;第二,梯度存到了叶子节点x.grad里。

有几个细节需要特别说明:

  • 默认情况下backward()只能对标量生效。如果y是一个向量或者矩阵,你需要传入一个和y形状相同的gradient参数,表示每个输出位置接收到的梯度权重。初学训练模型时 loss 一般都是标量(比如取平均或求和),所以直接loss.backward()就够了。
  • 只有叶子节点(即由用户直接创建、不经过其它操作的requires_grad=True张量)会默认保存.grad。中间节点的梯度默认不保留,如果想保留,需要调用retain_grad()
  • 梯度是累加的。每次backward()之后,.grad里的值是加上这一次的梯度,而不是替换。这就是为什么训练循环里必须调用optimizer.zero_grad()或者w.grad.zero_()

第 3 点是新手踩坑重灾区。有一次我写训练循环忘了清零梯度,loss 在几个 epoch 后直接变成nan,排查半天才发现是梯度累加导致参数更新步长越来越大。

3.3 用 autograd 手写一个线性回归训练循环

为了真正理解自动求导在训练中是怎么工作的,我建议你实现一个最简单的线性回归,不用任何封装好的模型训练模块,只用 Tensor 和backward()。这也是《动手学深度学习》前几章结构最清晰的一个实验。

先生成模拟数据:

import torch # 真实参数 w_true = torch.tensor([2.0, -3.4]) b_true = torch.tensor([4.2]) # 1000个样本,2个特征 X = torch.randn(1000, 2) y = X @ w_true + b_true + 0.01 * torch.randn(1000)

然后初始化参数并写训练循环:

w = torch.zeros(2, requires_grad=True) b = torch.zeros(1, requires_grad=True) lr = 0.03 num_epochs = 100 for epoch in range(num_epochs): # 前向计算 y_hat = X @ w + b # shape (1000,) 广播后加b loss = ((y_hat - y) ** 2).mean() # 反向传播 loss.backward() # 手动更新参数 with torch.no_grad(): w -= lr * w.grad b -= lr * b.grad # 梯度清零,必须做 w.grad.zero_() b.grad.zero_() if epoch % 10 == 0: print(f'epoch {epoch}, loss {loss.item():.6f}') print(f'w = {w}, b = {b}')

这个例子虽然只有几十行,但几乎包含了之后所有深度学习训练循环的关键要素:前向计算、计算损失、反向传播、更新参数、清零梯度。我把每一步都拆开讲一下。

  • y_hat = X @ w + bX(1000, 2)w(2,),矩阵乘法结果是(1000,)b的形状是(1,),通过广播机制加到每个输出上。这里关注的正是形状是否对齐。
  • loss = ((y_hat - y) ** 2).mean():均方误差损失,必须是一个标量。如果这里用了.sum()而不是.mean(),loss 数值会大很多,学习率就需要相应调小;很多初学者在这里才意识到meansum对训练的影响。
  • 更新参数时为什么要加torch.no_grad()?因为如果不加,w -= lr * w.grad这个操作会被记录进计算图,导致内存和计算量翻倍,甚至造成梯度计算错误。更新参数这个动作不应该参与自动求导。
  • w.grad.zero_()必须放在参数更新之后、下一轮反向传播之前。漏掉这一步,梯度就会累积,loss 会异常波动甚至变成nan

训练结束后,得到的wb会非常接近真实的w_trueb_true。如果你跑出来的结果偏差比较大,可以把学习率调小一些,或者增加迭代轮数。

3.4 梯度累积、no_grad 与 detach:训练循环里的三个细节

除了上面提到的zero_(),学习自动求导时还会频繁遇到三个概念:梯度累积、no_graddetach

梯度累积指的是“不清零梯度,多累积几步再做一次更新”的技巧。因为显卡显存有限,有时一次装不了太大 batch,就用小 batch 跑几步,把梯度累加到一起,再统一更新参数。PyTorch 默认梯度累加的行为反而成了实现这个技巧的便利条件:只要不在每个 batch 后调zero_(),就能自然累积。

但更多时候梯度累加是 bug,不是功能。常规训练里,每个 batch 结束后一定要清零。

torch.no_grad()detach()都是“切断梯度传播”的手段。no_grad表示在其作用域内的所有运算都不构建计算图,常用于模型推理阶段,在验证集上计算精度时使用,可以显著降低内存占用。detach()则是返回一个不参与计算图的新 Tensor,但底层数据仍然共享。

两者的区别在于作用范围:no_grad是一个上下文环境,影响所有在它内部创建和运算的 Tensor;detach是单独作用在某个 Tensor 上。实际使用中,训练阶段需要“冻结”某一部分参数时,或者计算某个指标时不想让梯度影响主模型,都常用到detach()

4. Tensor 学习踩坑实录:这几个报错我赌你迟早会遇到

4.1 device 不匹配:CPU 和 GPU 张量不能直接运算

新手第一次用 GPU 训练时,最常见的报错是:

RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!

这个报错翻译成人话就是:你在一个算式里同时用了 GPU 上的数据和 CPU 上的数据,PyTorch 不知道按哪个设备计算。我自己的习惯是把所有 Tensor 和模型都统一放到同一个 device。有一行代码可以用来统一管理:

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

然后数据在进入模型前都调用.to(device),模型本身也调用model.to(device)。要注意的是,手动创建的常量 Tensor 也记得要to(device),比如torch.ones_like(x)这种利用已有 Tensor 创建的还好,但你要写torch.zeros(2)再和一个 GPU Tensor 相加,就会报错。

4.2 in-place 操作:改了个值,梯度算不出来了

in-place 操作指的是直接修改原 Tensor 内容的操作,比如x += 1x[0] = 3x.zero_()。这类操作本身没问题,问题出在自动求导机制上。

当 Tensor 参与过前向传播之后,计算图已经记录了它的旧值。如果你此时在原 Tensor 上做 in-place 修改,等反传时计算图发现“历史数据被改了”,就会直接报错:

RuntimeError: a leaf Variable that requires grad is being used in an in-place operation.

解决方法通常有两个:一是只对不参与求导的张量做 in-place 操作,比如训练循环里的w.grad.zero_(),这种是允许的;二是更新可训练参数时不要用 in-place,而是用w = w - lr * w.grad,或者把原来的 in-place 写法改成w -= lr * w.grad之前先确认w不是叶子节点且在no_grad作用域中。我在前面的线性回归例子里用的就是with torch.no_grad(): w -= lr * w.grad,在上下文内部操作,避开了这个坑。

4.3 shape 对不上:矩阵乘法到底怎么对齐

矩阵乘法报错也是高频问题,尤其是第一次接触卷积和全连接层时:

RuntimeError: mat1 and mat2 shapes cannot be multiplied (3x4 and 5x2)

这个报错很直白:(3, 4)(5, 2)无法相乘,因为左矩阵的列数 4 必须等于右矩阵的行数。排查时最有效的方法是打印每一步的shape,一行行追踪数据的维度变化。

有个细节要注意:一维张量和矩阵相乘时行为会跟直觉不一样。比如torch.randn(4) @ torch.randn(4, 5)得到(5,),而torch.randn(1, 4) @ torch.randn(4, 5)得到(1, 5)。前者一维向量参与了乘法,结果会被“降维”。如果代码里某个环节多了一个维度或者少了一个维度,结果就完全对不上。遇到这种情况,可以手动用unsqueeze(0)或者squeeze(0)调整维度,等形状稳定后再继续。

4.4 几个容易被忽视的小细节

还有几个我见过很多初学者反复踩的细节,放在最后提醒一下。

第一,打印 loss 时忘了.item()。如果直接print(loss),输出的是一个 Tensor,日志里会出现tensor(0.0123, grad_fn=...)。用loss.item()会变成纯 Python 数值,更干净,同时还能分离计算图。

第二,忘了zero_()清零梯度。症状是 loss 在训练初期下降正常,过几个 epoch 后突然剧烈波动甚至变成 nan。排查梯度累积最快的方法是检查训练循环里有没有zero_()

第三,对切片做原地修改时,误改了原 Tensor。前面 2.3 节说过,切片是视图不是拷贝,想要独立数据一定要clone()

第四,把 dropout 或 BatchNorm 的“训练模式/评估模式”概念跟 Tensor 操作混淆。虽然这不完全是 Tensor 的问题,但很多人在验证集上忘了关闭requires_grad,导致显存占用异常。建议验证阶段用with torch.no_grad():包住前向计算。

写到这里,Tensor 相关的核心知识基本覆盖了。我觉得 Tensor 这部分是《动手学深度学习》里最基础也最该反复练的一块,后续看卷积、循环神经网络、Transformer 时会发现,所有结构本质上都在做 Tensor 的搬运、变形和数学运算。我个人的体会是,练 Tensor 一定要动手敲,不要觉得“代码看着简单就跳过”,很多时候你以为自己会了,实际一跑代码,shape、device、梯度清零的问题全冒出来了。希望这份笔记能帮你少踩我踩过的那些坑,把基本功打得更扎实。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 16:33:25

用户画像与协同过滤双路音乐推荐系统实战

简介:本资源是一个面向人工智能初学者与项目实践者的音乐推荐系统完整工程,聚焦用户画像构建与协同过滤算法融合应用,解决个性化音乐推荐中的冷启动与精度提升问题。压缩包共74个文件,含30个Python核心模块(如recommen…

作者头像 李华
网站建设 2026/9/14 16:29:39

AI建站工具选型指南:We0.ai、ChatGPT Sites、Lovable与Bolt怎么选

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 16:29:15

用户画像驱动的协同过滤:冷启动友好型推荐系统实现

简介:本资源是一个面向人工智能初学者与项目实践者的音乐推荐系统完整工程,融合用户画像构建与基于用户的协同过滤算法,解决个性化音乐推荐中的冷启动与精度提升问题。项目基于KKBox公开竞赛数据集实现,采用Python3开发&#xff0…

作者头像 李华