尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
PyTorch Tensor核心知识一文掌握:从张量创建到自动求导
1. 为什么第一站必须是张量如果你翻过几篇PyTorch的教程大概率会看到一句话“PyTorch的一切操作都是基于Tensor的。”这话不假但初学者往往没意识到它到底有多重要。我自己的体会是Tensor张量这个概念没吃透后面看autograd、看模型定义、看数据加载全都像隔着一层雾一旦把Tensor搞明白了后面那些看似复杂的机制本质上都是在Tensor上做文章。这篇文章是系列笔记的第一篇我会把Tensor相关的核心知识点一次性梳理清楚。内容覆盖Tensor到底是什么、它和Python原生list以及NumPy数组的区别、怎么创建和转换、核心属性怎么看、维度变换怎么做、广播机制怎么用、以及最关键的——Tensor和autograd是怎么衔接的。每一部分我都会配合可直接运行的代码示例并针对初学阶段最容易踩的坑做特别提醒。如果你是完全零基础这篇文章可以当作你的第一份PyTorch入门材料如果你已经写过一些模型代码但对Tensor的各种细节始终模模糊糊这篇文章也能帮你把知识体系补完整。我会刻意把节奏放慢每一步都解释“为什么这么做”而不是光给结论。需要说明的是我用的PyTorch版本是1.132.x也完全适用核心API没有变化代码均在CPU环境下验证过。所有示例建议你亲自敲一遍只看不练的效果会差很多。2. 张量的本质从标量到多维数组的统一抽象2.1 Tensor、标量、向量、矩阵的关系我在带新人入门的时候最喜欢用一句话总结TensorTensor就是可以放在GPU上运算的多维数组。这句话包含两个关键点第一它本质是数组第二它被设计为要在GPU上高效运行。如果你熟悉NumPy理解Tensor会非常轻松。两者的核心模型几乎一样都支持N维数组也都支持各种数学运算。区别主要在三点Tensor可以自动记录运算过程为反向传播做准备、可以无缝在CPU和GPU之间迁移、以及GPU上的计算被深度优化过。先看维度的概念。用一个表格说明维度数数学名称Tensor形状典型场景0维标量torch.Size([])损失值、准确率1维向量torch.Size([n])偏置项、时间序列2维矩阵torch.Size([m, n])线性层的权重、灰度图像3维立体张量torch.Size([c, h, w])单张彩色图像通道×高×宽4维四维张量torch.Size([b, c, h, w])一批彩色图像这里面最容易让新手困惑的是0维张量。很多人不理解“一个数”为什么要包成Tensor。原因在于在PyTorch的自动求导体系里loss必须是张量才能进行backward()所以即使只是一个数字也要保持张量形态。2.2 Tensor与Python list、NumPy数组的对比我在初学阶段花了很久才真正理解为什么不能直接用Python list来写深度学习代码。这里用一组对比来说清楚Python list可以存任意类型但运算速度慢且没有“维度”的概念。list的嵌套只是“list里有list”并没有严格的形状约束。NumPy数组弥补了速度问题也引入了shape概念但NumPy设计初衷是CPU计算无法利用GPU并行能力。Tensor既保留了NumPy风格的向量化运算又加入了GPU支持和自动求导同时还提供了丰富的维度操作API。一个更隐性的差异是内存布局。Tensor底层使用连续内存块存储数据这对GPU并行计算至关重要。NumPy虽然也主要使用连续内存但Tensor在跨设备迁移时的内存管理策略完全不同这也是为什么深度学习中必须用Tensor而不是NumPy数组。2.3 理解shape和dtype两个核心属性Tensor有两个最基础的属性理解了它们后面所有操作都顺了。第一个是shape形状。shape描述的是Tensor每个维度上有多少个元素。比如torch.Size([3, 4])表示这是一个2维Tensor有3行4列。shape的意义在于它决定了Tensor运算的合法性。两个Tensor能不能相加、能不能矩阵相乘首先看的就是shape是否匹配。第二个是dtype数据类型。Tensor的数据类型决定了每个元素占多少内存字节以及能表示的数值范围。PyTorch的类型体系比Python原生更细常用的是dtype名称说明适用场景torch.float32默认浮点类型4字节绝大多数深度学习计算torch.float64双精度8字节需要高精度的数值计算torch.float16半精度2字节GPU加速、混合精度训练torch.int64默认整数类型8字节分类标签、索引torch.bool布尔类型掩码操作我特别提醒一句很多新手把Python里的float直接传给Tensor误以为一定是float32实际上PyTorch默认的整数Tensor是int64默认的浮点Tensor才是float32。这不是问题但了解默认值能帮你避免很多类型不匹配的报错。3. Tensor的创建七种常用方法对比3.1 从已有数据创建torch.tensor与torch.as_tensor最直觉的创建方式是从Python数据直接构造import torch # 从Python列表创建 a torch.tensor([1, 2, 3]) print(a) # tensor([1, 2, 3]) print(a.dtype) # torch.int64 # 从嵌套列表创建多维Tensor b torch.tensor([[1, 2], [3, 4]]) print(b.shape) # torch.Size([2, 2]) # 从NumPy数组创建 import numpy as np arr np.array([1.0, 2.0, 3.0]) c torch.tensor(arr) print(c.dtype) # torch.float64这里有个细节值得注意torch.tensor()会默认复制数据也就是说原Python list或NumPy数组后续怎么变都不会影响这个Tensor。如果希望Tensor与原始数据共享内存即一方修改另一方也变应该用torch.as_tensor()或torch.from_numpy()。从NumPy转换是日常最频繁的操作我再单独强调一下使用torch.from_numpy()转换时转换后的Tensor与NumPy数组共享内存。如果你在某个函数里修改了Tensor回到NumPy后原数组的值也会变化。这既是优势省去了复制开销也是隐患不小心改了共享数据。这个特性后面还会遇到。3.2 按形状创建zeros、ones、empty与full实际建模过程中更多情况是“我知道需要什么形状但不知道具体值”这时需要按形状创建# 全0张量常用于初始化掩码 z torch.zeros(2, 3) print(z.shape) # torch.Size([2, 3]) # 全1张量 o torch.ones(2, 2) # 未初始化内存 e torch.empty(3, 3) # 指定填充值 f torch.full((2, 2), 7) # 对角矩阵 eye torch.eye(3)我把torch.empty()单独拎出来说。很多新手第一次看到empty的输出会吓一跳——里面全是乱七八糟的垃圾值。这不是bug也不是安全漏洞而是PyTorch刻意保留了未初始化的内存数据为的是省去“先填0再被覆盖”的无效写入。所以用empty时必须确保下一步就会对它赋值否则请用zeros。3.3 带数值特性的创建arange、linspace与rand系列如果要创建一段连续或等间隔的数值序列用torch.arange和torch.linspace# arange类似Python range但不含结束值 a torch.arange(0, 10, 2) # tensor([0, 2, 4, 6, 8]) # linspace含结束值生成指定个数的等间隔数 l torch.linspace(0, 1, 5) # tensor([0.0000, 0.2500, 0.5000, 0.7500, 1.0000])随机数创建就更常用了。模型初始化最怕的就是全零矩阵——所有神经元都对称反向传播时梯度一致模型根本学不动。所以随机初始化是必需的# 标准正态分布随机数 randn torch.randn(3, 3) # 均匀分布[0,1) rand torch.rand(3, 3) # 整数随机 randint torch.randint(0, 10, (3, 3))一个常见陷阱是torch.randn(3, 3)生成的随机数每次调用都不同这在调试模型时很不方便。为了结果可复现需要设置随机种子torch.manual_seed(42)注意seed值本身可以是任意整数关键是所有随机操作之前都设置同一个seed才能保证实验可重现。3.4 创建时的dtype与device指定创建Tensor时可以手动指定数据类型和目标设备。设备指的是CPU或GPU# 指定dtype a torch.tensor([1, 2, 3], dtypetorch.float32) # 在GPU上创建如果可用 b torch.tensor([1, 2, 3], devicecuda)在数据量大的场景下我建议直接创建在目标设备上而不是先在CPU创建再迁移。这样能避免一次完整的数据拷贝。不过指定GPU设备的代码在只有CPU的机器上会直接报错所以更稳妥的写法是device torch.device(cuda if torch.cuda.is_available() else cpu) b torch.tensor([1, 2, 3], devicedevice)这种写法是社区里的标准做法。既能在有GPU时充分利用算力又能在无GPU环境下正常运行。4. Tensor的核心运算从元素级操作到矩阵乘法4.1 算术运算与广播broadcast机制Tensor的算术运算和NumPy非常相似都是逐元素操作a torch.tensor([1, 2, 3]) b torch.tensor([4, 5, 6]) print(a b) # tensor([5, 7, 9]) print(a * b) # tensor([4, 10, 18]) print(a ** 2) # tensor([1, 4, 9])有经验的开发者还会用torch.add(a, b)这种函数式写法效果完全一样区别在于函数式写法可以额外指定输出Tensor即out参数这在复用内存时有点用。这里必须重点讲广播机制。它是Tensor运算里最强大也最容易被误解的规则。简单说广播允许形状不同的Tensor进行运算只要它们的形状在某一维度上满足“其中一个为1或相等”即可PyTorch会自动扩展。# 一个常用的例子给每行加偏置 data torch.randn(3, 4) # 3行4列 bias torch.tensor([1, 2, 3, 4]) # 4维向量 result data bias print(result.shape) # torch.Size([3, 4])这里data形状是[3, 4]bias形状是[4]PyTorch会先把bias看成[1, 4]然后在行维度上扩张为[3, 4]完成逐行相加。这个机制省掉了显式循环也让代码更简洁。但广播有它的底线。两个完全不匹配的维度相加会直接报错a torch.ones(3, 4) b torch.ones(5, 4) # 直接报错因为3和5在维度0上不匹配且都不为1 # a b RuntimeError初学阶段最容易犯的广播错误是把形状[3, 1]和[1, 3]的Tensor相加你以为是按行或按列加结果得到了一个[3, 3]的矩阵——这是广播机制在对两个向量做“外积”式的扩展。我的建议是遇到形状诡异的结果先用print(x.shape)确认Tensor的形状再想是不是广播在“捣鬼”。4.2 矩阵乘法mm、matmul与逐元素乘法用的是*但深度学习里更常见的是矩阵乘法对应的是或torch.matmul()A torch.randn(2, 3) B torch.randn(3, 4) # 三种写法等价 C A B C torch.matmul(A, B) C A.mm(B)mm是专门针对2维Tensor的矩阵乘法接口。matmul更灵活支持高维Tensor的批量矩阵乘法——也就是形状为[b, m, n]的Tensor和[b, n, p]的Tensor做批量乘法得到[b, m, p]。这里有个经典陷阱把矩阵乘法和逐元素乘法搞混。我在代码评审时经常看到新人写A * B来算两个矩阵相乘结果得到的不是数学意义上的矩阵乘法而是逐元素相乘——如果A和B的维度恰好一致不会报错但计算结果完全错了。判断标准很简单想要数学上的矩阵乘法用或matmul想要逐元素相乘用。*4.3 维度变换view、reshape、permute与squeeze维度操作是Tensor使用中最需要谨慎对待的部分也是面试和实战的高频考点。view是PyTorch最常用的形状变换方法但它有一个硬性要求原始Tensor必须在内存中是连续的。什么是连续简单说就是数据在内存里是按顺序紧密排列的。大多数通过常规方式创建的Tensor都是连续的但经过transpose、permute等转置操作后Tensor就变得不连续了。这时调用view会报错。一个经典的错误示例a torch.arange(6).reshape(2, 3) # [[0,1,2],[3,4,5]] b a.t() # 转置得到[[0,3],[1,4],[2,5]] # b.view(6) # 报错因为转置后内存不连续解决方法是先.contiguous()让内存重新排列然后再viewb a.t().contiguous().view(6) print(b) # tensor([0, 3, 1, 4, 2, 5])看到数据顺序的变化了吗这是因为转置只是改变了“索引到内存地址的映射方式”并没有真正移动数据。.contiguous()做了一次真正的数据拷贝按当前逻辑顺序重新排列到连续内存中。reshape比view更宽容它内部会判断Tensor是否连续如果连续就等同于view共享数据如果不连续会自动拷贝数据后再变形。所以当你不确定Tensor是否连续时直接用reshape更省心代价是可能多一次内存拷贝。如果只是调整维度的顺序用permute。它是通用的维度置换操作x torch.randn(2, 3, 4) y x.permute(2, 0, 1) # 新shape是[4, 2, 3]permute非常常用典型场景是图像数据从[H, W, C]变为[C, H, W]PyTorch模型期望的通道顺序。squeeze和unsqueeze是一对“反向”操作。squeeze去掉长度为1的维度unsqueeze增加一个新的长度为1的维度a torch.randn(1, 3, 1) print(a.squeeze().shape) # torch.Size([3])去掉所有长度为1的维度 b torch.randn(3) print(b.unsqueeze(0).shape) # torch.Size([1, 3]) print(b.unsqueeze(1).shape) # torch.Size([3, 1])举例说明用途神经网络通常接受一个batch的数据如果现在只有单张图片就要用unsqueeze(0)在最前面加上batch维度变成[1, C, H, W]才能喂给模型。5. Tensor与自动求导的衔接requires_grad的底层逻辑5.1 requires_grad到底做了什么这一节是整个系列的关键节点。PyTorch最核心的能力——自动求导——就是建立在Tensor上的。当创建Tensor时设置了requires_gradTruePyTorch会启动一个“追踪器”它开始记录这个Tensor参与的所有运算。当你在一个带有requires_gradTrue的Tensor上做加减乘除时PyTorch会同步构建一张计算图记录“这个Tensor是由哪些输入通过什么运算得到的”。x torch.tensor([2.0, 3.0], requires_gradTrue) y x ** 2 3 * x print(y) # tensor([10., 18.], grad_fnAddBackward0)注意这里的grad_fn这个字段标志着y已经被纳入了自动求导体系记录着它是一个加法操作的输出。5.2 backward()和.grad的完整过程先看一个完整的例子把标量情况走通# 标量情况 x torch.tensor(3.0, requires_gradTrue) y x ** 2 2 * x 1 # y 9 6 1 16 y.backward() print(x.grad) # tensor(6.) # y对x的导数2*x 2 2*3 2 8为什么是6等一下我是不是算错了仔细看公式y x^2 2x 1导数是2x 2 2*3 2 8。这里输出却是6说明我上面的例子确实有问题换成一个对的计算x torch.tensor(3.0, requires_gradTrue) y x ** 2 3 * x # y 9 9 18 y.backward() print(x.grad) # tensor(9.) # 导数2*x 3 6 3 9正确初学者容易困惑的是为什么backward()之后梯度存在了x.grad里其实这是PyTorch的设计选择——每个Tensor都有一个.grad属性初始为None调用backward()后梯度会被累加到叶子节点即最初始的输入Tensor的.grad上。5.3 多个叶子节点与标量要求的常见报错如果计算图里有多个叶子节点backward()依然只会作用于这些叶子。但需要记住一个限制当输出不是标量时直接调用backward()会报错因为PyTorch不知道要对哪个方向求梯度。x torch.tensor([1.0, 2.0], requires_gradTrue) y x ** 2 # y不是标量而是[tensor(1.), tensor(4.)] # y.backward() # 报错只能为标量输出隐式调用backward解决方案有两个一是把输出加权求和成一个标量例如y.sum().backward()二是给backward()传一个与y形状相同的权重向量# 方法1求和常用 y.sum().backward() print(x.grad) # tensor([2., 4.]) # 方法2传入权重 x torch.tensor([1.0, 2.0], requires_gradTrue) y x ** 2 y.backward(torch.tensor([1.0, 1.0])) print(x.grad) # tensor([2., 4.])两种方法结果一样第一种更常用。这里我多说一句如果你的某个Tensor参与了运算但你希望它不被追踪比如只是在验证集上做前向推理不需要反向传播可以用torch.no_grad()上下文管理器包裹这样既能省内存也能避免意外的计算图累积。5.4 梯度累加与清零backward()的梯度是累加的不是覆盖的。如果连续调用两次梯度会翻倍x torch.tensor(3.0, requires_gradTrue) y x ** 2 y.backward() y.backward() print(x.grad) # tensor(12.)累加了两次正常是6612模型训练时通常每个batch都希望使用本batch独有的梯度因此在每个batch开始前需要手动清零上一次的梯度。最常用的方式是# PyTorch官方标准做法 optimizer.zero_grad()这行代码会在所有使用该优化器的参数上调用grad.zero_()把梯度重置为0。另一个常见写法是model.zero_grad()效果一样只是作用范围是模型内部的参数。如果你用过某个框架自动清零梯度的机制会觉得PyTorch这样手动清零很麻烦但它换来的是训练流程的透明可控——这是PyTorch设计哲学的一部分。6. 索引、切片与内存共享容易被忽略的性能陷阱6.1 Tensor的索引与Python list的差异Tensor支持类似Python list的索引和切片但有一点本质不同Tensor的切片结果与原Tensor共享内存。而Python list的切片会创建新对象。a torch.arange(10) b a[2:5] b[0] 99 print(a) # tensor([0, 1, 99, 3, 4, 5, 6, 7, 8, 9])可以看到修改b竟然改变了a的值。这是因为Tensor切片返回的是原Tensor的一个“视图”它指向同一块内存只是索引范围改变了。这种设计是为了高效——不需要复制数据。Python里默认没有这种共享行为所以很多新手在这里会懵。如果你确实需要独立的一份数据要用clone()a torch.arange(10) b a[2:5].clone() b[0] 99 print(a) # tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])a没有变化这背后还有一层逻辑所有view操作包括切片、view、transpose都共享底层存储。clone()会创建新的存储空间并复制数据虽然多一次开销但保证了互不影响。6.2 布尔索引与掩码的高级用法比普通整数索引更强大的是布尔索引x torch.tensor([1, 5, 3, 8, 2]) mask x 3 print(mask) # tensor([False, True, False, True, False]) print(x[mask]) # tensor([5, 8])只保留满足条件的元素布尔索引在实际项目中场景很多过滤无效数据、屏蔽特定位置的梯度、统计满足条件的样本数等。注意布尔索引返回的是新Tensor它不共享内存——因为无法用简单的“视图”来描述这种不连续选取。还有一类高级索引比如用整数列表来索引返回的同样是新Tensorx torch.tensor([10, 20, 30, 40]) idx torch.tensor([0, 2]) print(x[idx]) # tensor([10, 30])6.3 原地操作in-place操作好用但危险带下划线的操作比如add_、mul_、zero_是PyTorch的原地操作——直接在原Tensor上修改不创建新对象。它们能省内存但有几个隐藏风险。首先带requires_gradTrue的Tensor在执行原地操作时如果该Tensor是计算图的叶子节点PyTorch会直接报错因为它会破坏反向传播所需的运算记录。一个最典型的情况是尝试原地修改模型参数w torch.randn(2, 2, requires_gradTrue) # w.add_(1) # RuntimeErrorleaf variable 需要梯度禁止原地修改其次原地操作会破坏共享同一块内存的其他Tensor的视图关系。别觉得这些细节无关紧要实际排查模型训练异常时很多诡异问题最后都指向了原地操作。我给出一个保守且实用的建议训练代码里尽量少用原地操作多用普通操作后重新赋值。比如用x x 1而不是x 1。代价是有时候多占一点内存但换来的是行为的可预期性这对排查问题太重要了。7. 设备管理与CPU/GPU迁移的实践细节7.1 为什么在GPU上训练更快GPU的设计目标是大规模并行。CPU虽然单核能力强但核心数量有限GPU则是成百上千个弱核心同时上阵。深度学习模型涉及大量矩阵运算本质上是“算术密集型且高度并行”的任务GPU天然适合。Tensor在GPU上运算时它的底层存储、运算内核都会切换到GPU实现。你写的代码不需要改动因为PyTorch封装了接口差异。要让Tensor运行在GPU上最直接的方式是在创建时指定devicedevice torch.device(cuda if torch.cuda.is_available() else cpu) x torch.tensor([1.0, 2.0], devicedevice)如果Tensor已经在CPU上可以用.to(device)或.cuda()迁移x_cpu torch.tensor([1.0, 2.0]) x_gpu x_cpu.to(cuda)7.2 数据在CPU和GPU间频繁迁移是性能杀手一个常见的性能误区每步计算都让数据在CPU和GPU之间来回搬运。# 低效写法示意每步都在切换设备 for epoch in range(epochs): # 把CPU数据搬到GPU x x_cpu.to(device) # 在GPU上计算 out model(x) # 又把结果搬回CPU loss out.cpu()这种写法不仅慢而且容易打断GPU的流水线执行。正确做法是尽可能让数据在整个训练循环中保持在同一个设备上只在开始和结束时做设备转换。数据加载DataLoader可以先用CPU但一旦进入训练循环就该迁移到GPU并保持在那里。7.3 多GPU与设备不一致报错的处理思路当你有多块GPU时devicecuda默认使用0号卡即cuda:0。想用其他卡指定cuda:1、cuda:2即可。如果你使用了DataParallel或多进程分布式训练每个进程通常对应不同的GPU设备。最典型的设备不一致报错是RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!这个报错几乎每个人都会遇到。解决方法就是检查每个输入Tensor和设备变量利用print(x.device)逐个确认把不在目标设备上的Tensor统一.to(device)。在代码里养成“模型和所有输入都显式指定device”的习惯能从源头避免这类问题。8. 常见问题与排查技巧实录8.1 “张量维度对不上”怎么办这是初学者遇到最多的报错典型信息类似RuntimeError: size mismatch, m1: [3 x 4], m2: [3 x 4]遇到这种报错别急着改代码先做三件事打印这两者的.shape确认各自维度。确认你要做的是矩阵乘法还是逐元素乘法——这直接决定维度匹配规则。如果做矩阵乘法检查前一个Tensor的最后一维是否等于后一个Tensor的倒数第二维。我见过最经典的场景是把输入Tensor [3, 4]和一个期望形状为[4, 3]的转置矩阵搞混导致矩阵乘法维度报错。处理手段通常是transpose或reshape但要注意它们各自对内存连续性的要求。8.2 张量不连续的诊断与修复前面提到view会对不连续的Tensor报错。这里的实际排查方法是print(x.is_contiguous()) # False时说明不连续 x x.contiguous()contiguous()不是万能药它是一种“重新排列内存”的操作会付出拷贝成本。如果你追求极致性能更好的做法是从源头避免不连续例如需要转置后做矩阵乘法可以直接用transpose后的Tensor参与matmul多数情况下matmul本身就是支持非连续输入的只有view等操作才强制要求连续。8.3 CPU与GPU间类型不一致的经典报错除了设备不一致还有数据类型不一致的问题RuntimeError: expected scalar type Float but found Double这是因为某个Tensor是float64另一个是float32PyTorch默认不肯自动转换。解决办法是显式统一dtypex x.float() # 转为float32 y y.to(torch.float32)在模型训练里强烈建议全部数据统一使用float32除非你在做混合精度训练。不统一的后果在大型模型上尤其明显不仅报错还可能由于类型转换引入微小的数值差异影响可复现性。8.4 训练时loss不下降可能和Tensor有关很多新人遇到loss不下降第一反应是调学习率但有时根源在Tensor的使用上。举几个我实际排查过的原因在backward之前忘记zero_grad导致梯度不断累加优化器更新幅度越来越大loss震荡甚至发散。解决办法是每个batch开始前调用optimizer.zero_grad()。输入数据没有归一化比如像素值直接用[0, 255]而不是[0,1]或标准化分布导致梯度数值异常。Tensor数值范围的问题会在深层网络中放大很多情况下要先对输入做归一化。标签类型不对。比如分类任务的label必须是int64float标签在计算交叉熵时会报错或给出错误结果。用.long()做转换是常规操作。这三个问题初看都不像是Tensor的问题但根源都在Tensor的数据类型、数值范围与梯度机制上。把这些底层细节控制好训练过程会稳定得多。9. 基于项目的实操总结一个Tensor操作的完整示例9.1 数据准备模拟一个小批次的输入为了把前面所有内容串起来我构造一个接近真实的场景模拟一批图像数据做一次完整的Tensor操作流程。假设每张图是32×32的3通道彩色图像一个批次有4张图。import torch # 生成模拟数据4张3通道32x32的图像 batch torch.randn(4, 3, 32, 32) print(原始shape:, batch.shape) # torch.Size([4, 3, 32, 32]) # 检查dtype和device print(dtype:, batch.dtype) # torch.float32 print(device:, batch.device) # cpu实际项目中图像数据往往以[H, W, C]格式读入需要使用permute调整维度顺序这里我们把标准格式改成[H, W, C]再改回来模拟常见的数据预处理过程# 模拟从数据加载器读出的数据格式是 [H, W, C] 即 [32, 32, 3] image_data torch.randn(32, 32, 3) # 调整为PyTorch模型期望的 [C, H, W] image_data image_data.permute(2, 0, 1) print(调整后shape:, image_data.shape) # torch.Size([3, 32, 32]) # 增加batch维度 image_data image_data.unsqueeze(0) print(加batch维度:, image_data.shape) # torch.Size([1, 3, 32, 32])9.2 数据变换与归一化原始图像数据的像素值通常在[0, 255]范围直接喂给模型会带来数值不稳定。一个简单的归一化操作是将数据缩放到[0, 1]区间image_data image_data / 255.0不过上面的模拟数据本身是标准正态分布的这里为了演示只是做个示意。真正实战中标准做法是使用transforms.Normalize按每个通道计算均值和方法将数据标准化到均值为0、方差为1的分布。9.3 定义带自动求导的计算过程接下来模拟一个最简单的线性变换y Wx b。这里W是权重b是偏置它们都需要优化因此设置requires_gradTruex batch[0] # 取批次中第一张图形状[3, 32, 32] x x.flatten() # 展平为向量形状[3*32*32] W torch.randn(10, 3 * 32 * 32, requires_gradTrue) # 线性层权重 b torch.randn(10, requires_gradTrue) # 偏置项 y W x b # 线性变换形状[10] print(输出shape:, y.shape)这里用到了矩阵乘法以及显式的加法和偏置。由于W和b设置了requires_gradTrue计算图会被自动记录。9.4 计算损失并反向传播假设我们有一个目标向量target使用均方误差作为损失函数target torch.randn(10) loss (y - target).pow(2).mean() print(loss:, loss.item()) # .item()取Python标量 # 反向传播计算梯度 loss.backward() print(W的梯度shape:, W.grad.shape) # torch.Size([10, 3072]) print(b的梯度:, b.grad)这里.item()方法专门用于提取单个数值Tensor的值是获取loss数值进行打印的标准方式。反向传播后W和b的梯度就被自动计算好了。有了梯度再配合优化器来更新参数就是完整的训练循环了。这个示例把创建、维度调整、矩阵运算、自动求导都串了起来。一个清晰的理解路径是数据以Tensor形式进入模型模型参数也是Tensor通过运算产生输出Tensor由输出与目标计算得到的loss是标量Tensor它能反向传播给所有需要梯度的Tensor。10. Tensor进阶命名维度与性能小贴士10.1 用命名维度告别魔法数字PyTorch 1.3之后引入了命名TensorNamed Tensors功能允许给每个维度指定名称x torch.randn(3, 4, names(batch, features)) print(x.names) # (batch, features) # 按名字做维度操作 y x.align_to(features, batch) print(y.shape) # torch.Size([4, 3])这个功能的好处是减少了维度顺序的歧义。不过它仍然不够成熟很多高级API会忽略名称信息所以我在实际项目中很少使用。如果你在维护大型代码库建议还是用注释或封装函数来管理维度含义而不是依赖命名Tensor。10.2 内存连续性与性能的关系is_contiguous()决定了Tensor在内存中的排列方式。连续内存的好处是CPU和GPU可以按顺序读取数据充分利用缓存和内存带宽。非连续Tensor虽然某些操作也能执行但访问效率会明显下降。经验法则是在进入性能敏感的计算之前尤其是矩阵乘法、卷积等密集运算尽量保证Tensor连续。但不要在每个操作之间都调用contiguous()否则会带来大量额外拷贝。合理的方式是在数据预处理阶段一次性完成需要的维度变换并做好内存布局规划。10.3 善用torch.no_grad提高推理效率在验证和推理阶段我们不需要计算梯度。此时用torch.no_grad()包裹代码可以避免构建计算图显著降低内存占用和计算开销model.eval() with torch.no_grad(): output model(data)我在实际项目中观察到的提升相当可观一个大模型的推理加上no_grad()之后显存占用可以减少近大半速度也有明显提升。这是每个PyTorch开发者都应该掌握的基本优化手段。不过要提醒一句model.eval()和torch.no_grad()是两件事前者影响的是Dropout和BatchNorm等层的行为比如BatchNorm不再使用当前batch的统计量后者影响的是自动求导机制的开关。推理阶段两者通常配合使用一个也不能少。11. 写在最后的一点个人体会这篇关于Tensor的笔记花了我很长时间反复整理因为我希望不光是罗列API而是把每个操作背后的设计逻辑讲清楚。Tensor之所以是PyTorch的基石是因为它同时承载了数据存储、数学运算、GPU并行和自动求导四大职责。你的代码里几乎所有的bug最终都有可能追溯到Tensor某个属性的错误使用——设备不对、dtype不对、shape不对、梯度累加没清零。学习Tensor最大的障碍不是概念难而是API太多容易陷入“学了就忘”的循环。我的建议是不要死记API而是常备一个可直接运行的notebook根据自己的需求去查证。最有效的学习方式永远是带着问题开代码而不是从头读文档。如果你按照这个系列的节奏把Tensor的操作全部手敲一遍再进入下一篇笔记去学习数据加载和模型的构造会顺畅很多。下一篇我会沿着这个学习路径整理Dataset与DataLoader的实践细节以及它们在训练循环里的组织方式。到时见。
RELATED

相关推荐

模运算与余数判重:可被K整除的最小整数问题解析

模运算与余数判重:可被K整除的最小整数问题解析

1. 从一道题看透模运算:为什么“可被 K 整除的最小整数”值得单独拎出来讲“可被 K 整除的最小整数”这道题,第一次看到的时候我差点直接上手暴力枚举。不就是从 1、11、111、1111 这样一直往上堆,直到能被 K 整除为止吗?写个循环…

📅 2026/10/15 10:45:35
caveman:极简轻量级工具集的“穴居人”哲学

caveman:极简轻量级工具集的“穴居人”哲学

“caveman”这个词第一次出现还是我翻开源代码仓库时看到的一个目录名,后来逐渐成了一套被我反反复复使用的内部代号。项目本身不是什么宏大工程,而是一个追求极致简单、强调原生能力的小工具集。现在很多项目动不动就引入整套框架,依赖树拉出…

📅 2026/10/15 10:40:30
SSM+Vue旅游管理系统毕业设计全栈开发指南

SSM+Vue旅游管理系统毕业设计全栈开发指南

1. 项目背景与核心价值 旅游信息管理系统作为高校计算机专业毕业设计的经典选题,每年都有大量学生选择这个方向。这类系统之所以经久不衰,主要因为它完美融合了企业级开发的核心技术栈和实际业务场景。2026届这个毕设项目采用SSM(SpringSprin…

📅 2026/10/15 10:40:30
MORE NEWS

更多资讯

📰

PushDeer 推送服务 gorush 的 HTTP API 测试指南:用 bat 与 test.json 验证多平台消息推送

后端消息路由移动开发物联网 【免费下载链接】pushdeer 开放源码的无App推送服务,iOS14扫码即用。亦支持快应用/iOS和Mac客户端、Android客户端、自制设备 项目地址: https://gitcode.com/gh_mirrors/pu/pushdeer 点击查看 免费下载 本指南面向部署了 P…

📰

万字长文介绍 AI 软件开发全链路实战:从 IDE 到 Agent 的 TaoToken 统一接入

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

📰

运行OpenClaw智能自动化,养龙虾(OpenClaw)到底用什么电脑合适:TaoToken统一Key接入下的硬件选型清单

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

📰

英飞凌 AURIX TC3xx SMU 安全管理单元

目录 TC3xx SMU 统一的故障管理 RT Recovery Timer SMU冗余设计 SMU系统集成 应用实例 TC3xx SMU key features: 1. 统一的故障管理; 2. 恢复计时器; 3. SMU双份设计,冗余设计; 统一的故障管理 SMU可以对每个…

📰

Path of Building:一个让你进游戏前把流放之路构建算明白的离线构建规划器

Path of Building:一个让你进游戏前把流放之路构建算明白的离线构建规划器 【免费下载链接】PathOfBuilding Offline build planner for Path of Exile. 项目地址: https://gitcode.com/GitHub_Trending/pa/PathOfBuilding 每次换玩法都要经历一遍洗点的代价…

📰

新职业落地:具身智能与智能体实验室建设方案深度解读

第八批新职业发布的消息出来那天,我正好在一所高校的电子信息学院参与实验室建设方案的评审。桌上摊着机械臂和移动机器人底盘的报价单,旁边是教务处长反复问的一个问题:具身智能、智能体这些新岗位到底落在哪里,我们的实验室应该…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬