深度学习框架这几年更迭得很快,但不管怎么变,PyTorch的张量(Tensor)永远是最核心、最值得花时间啃明白的东西。很多刚入门深度学习的朋友,一上来就去看模型结构、跑训练代码,遇到shape对不上、报错看不懂、GPU利用率上不去这类问题就卡住了——根子往往不在模型,而在张量基础没打牢。
这篇内容就是围绕PyTorch张量来写的。我会从张量的本质讲起,把创建、索引、形状变换、运算、设备迁移、自动求导这些核心操作一个个过一遍,穿插我实际踩过的坑和排查思路。不管你是刚装好PyTorch还没写过几行代码的萌新,还是已经能跑通简单模型但总觉得基础不扎实的同学,这篇内容都能帮你在张量这件事上彻底搞透。
1. 张量的本质:为什么它是深度学习的基石
1.1 张量到底是什么:从向量、矩阵说起
你可能听过“张量就是多维数组”这个说法,这个说法方向是对的,但没有把最关键的差异讲透。
中学数学里我们处理的是标量,一个数。进入线性代数之后接触向量,一维数组,很多个数字排成一排。再进一步是矩阵,二维的表格结构。张量就是把维度继续往上推:三维、四维、甚至更高维度,在计算机里就是带shape的数值存储结构。
但是PyTorch的张量和普通的NumPy数组、Python列表相比,多了一个截然不同的能力:它能自动记录计算路径,也就是自动求导机制。这一点在后面会详细展开,这里先记住一句话:张量是PyTorch用来存储数据、参与计算、反向传播的核心载体。
再说得直白一点。你训练一个神经网络,输入图片是张量,权重参数是张量,卷积后的特征图是张量,损失值算出来是零维张量。整个深度学习训练过程,就是张量在计算图里不断流转、变换、求导的过程。理解了张量,PyTorch就理解了七八成。
1.2 张量 vs NumPy数组 vs Python列表
我刚学过NumPy再学PyTorch的时候,觉得这两者简直一模一样:都能做索引、切片、矩阵运算。但用多了之后发现差异非常明显。
看个最直观的例子:
python复制import torch
import numpy as np
# Python列表
list_data = [[1, 2], [3, 4]]
# NumPy数组
np_data = np.array(list_data)
# PyTorch张量
tensor_data = torch.tensor(list_data)
print(np_data.shape) # (2, 2)
print(tensor_data.shape) # torch.Size([2, 2])
这时候看起来只是打印方式不同,但有几个关键点拉开差距:
- NumPy默认用CPU计算,PyTorch张量可以无缝切换到GPU,通过
.cuda()或者.to(device)一句代码搞定。 - 张量自带
requires_grad属性,打开之后参与运算的整个过程会被计算图记录,这是神经网络训练的地基。 - PyTorch张量和NumPy数组之间转换很简单,但要注意共享内存的坑(后面详细说)。
- PyTorch的自动微分体系只认自己的张量类型,你用NumPy数组做反向传播是走不通的。
TensorFlow里也有张量,概念上类似,但PyTorch的GIL锁处理、动态计算图和命令式风格,让它在调试体验上舒服很多。这也是为什么学术界和工业界越来越多项目转向PyTorch。
1.3 张量独有的杀手锏:自动求导与设备无关计算
自动求导和自动驾驶不是一回事,它的意思是:你定义一个张量,打开requires_grad=True,那么所有基于它进行的运算,都会被框架自动记录。等前向传播跑完,调用backward(),每个张量对应的梯度就被自动算好,存放在.grad属性里。
这个机制的价值太大了。要知道在PyTorch之前,TensorFlow 1.x用的是静态计算图,你得先把计算图构建完整,再塞数据进去跑,调试起来非常痛苦。PyTorch采取动态图策略,边执行边记录,代码写到哪就执行到哪,改起来像写普通Python程序一样自然,这对研究实验和快速迭代是革命性的。对初学者来说,自动求导就是把高数和矩阵求导这些数学门槛直接移走了,你不需要自己推导梯度公式,模型照样能训练。
设备无关计算也很好理解:同样的代码,把数据放在CPU上能跑,放在NVIDIA显卡上也能跑,只是需要把张量搬到对应设备上。这是训练大模型的基本功,后面我会单独讲设备迁移的细节。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 张量创建与核心属性:写代码前必须搞清楚的六件事
2.1 最常用的七种创建方式
创建张量是每天写代码的第一个动作。我整理了一个速查表,是我平时用得最多的几类方法:
| 创建方式 | 用法示例 | 适用场景 |
|---|---|---|
| 从数据直接创建 | torch.tensor([1, 2, 3]) |
从已有列表/数组转张量 |
| 全零张量 | torch.zeros(2, 3) |
初始化掩码、占位、偏置 |
| 全一张量 | torch.ones(2, 3) |
初始化常数矩阵 |
| 单位矩阵 | torch.eye(3) |
生成对角矩阵 |
| 随机均匀分布 | torch.rand(2, 3) |
生成均匀分布数据,范围[0,1) |
| 随机正态分布 | torch.randn(2, 3) |
生成标准正态分布数据 |
| 序列 | torch.arange(0, 10, 2) |
生成等差数列 |
写代码的时候有一个最常见的坑:torch.Tensor(2, 3)和torch.tensor([2, 3])含义完全不同。torch.Tensor(2, 3)创建的是一个2行3列、数值未初始化的张量,内容是垃圾值;而torch.tensor([2, 3])创建的是包含2和3这两个元素的一维张量。大小写不同,结果天差地别,这个错误我在好几个刚入门的同学代码里都见过。
如果你需要连续区间,比如等差数列,用torch.linspace(0, 1, steps=5)会得到[0, 0.25, 0.5, 0.75, 1.0],这在画图、归一化、生成坐标时非常有用。与之相对的是torch.arange(0, 1, 0.25),由于浮点数精度问题,偶尔会有意料之外的边界行为,所以能用linspace的地方我一般不用arange。
2.2 核心属性:shape、dtype、device、requires_grad
每创建一个张量,都有几个随身携带的属性,你必须做到看到任意一个张量都能准确说出它的shape、dtype、device和requires_grad。
shape(或者说size())是最容易出问题的。有个学员跑来跟我说模型跑不通,我一问,他以为torch.rand(3, 224, 224)是224张3行224列的图,实际这是3个224x224的矩阵。PyTorch里shape的顺序是有约定的,图像数据一般是(batch, channels, height, width),文本序列一般是(batch, seq_len, hidden_dim),不遵守约定,后面就会到处踩维度对不上的坑。
dtype也很关键。默认的浮点类型在不同设备上不一样,CPU上是torch.float32,GPU上也是torch.float32,但如果你用torch.set_default_dtype()改过,或者从磁盘加载模型时精度不匹配,就可能出现精度下降甚至报错。混合精度训练时,我们经常需要把模型参数转成torch.float16,但注意,不是所有操作都支持半精度,比如某些归一化操作在FP16下精度损失严重,需要特殊处理。
device决定张量在哪块硬件上干活。CPU张量做运算,遇到大型矩阵计算会很吃亏,因为CPU没有那么多并行核心。GPU张量则要把数据从内存拷贝到显存,这个拷贝有开销,如果数据太小,反而比CPU还慢。所以写代码前必须想清楚数据放在哪、什么时候搬、搬几次。
requires_grad这个属性我们到第5部分展开,这里只需要记住:它决定PyTorch要不要为这个张量记录计算图。训练时只有需要求解梯度的参数才置为True,输入数据一般保持False,否则计算图会越攒越大,内存吃不消。
2.3 一个不算冷的知识:张量存储与视图(storage/view)的关系
这个知识点看着偏底层,但理解了能帮你省去很多性能优化的烦恼。
在PyTorch底层,一个张量的数据是存储在一块连续内存里的,叫作storage。张量本身只是一个“视图”,它记录了storage的地址、shape、stride(步长)、存储偏移量等元信息。你看到的二维矩阵,在内存里其实是一串连续的数字,只是通过shape和stride把它映射成多维结构。
为什么要讲这个?因为你做切片、转置、view操作时,很多都是返回一个新的视图,而不是复制一块新内存。好处是速度快、省内存,坏处是:如果你修改了视图,原来的张量内容也变了;而且某些操作要求张量在内存中连续,不连续的话会报错或者需要复制。
遇到contiguous()这个操作,很多新人一脸懵。举个例子:tensor.transpose(0, 1)之后,数据在内存里已经不是按顺序排列了,这时候调用contiguous()会重新分配一块内存把数据整理成连续排列。很多经典报错,比如view操作要求张量是contiguous的,就是因为没搞清楚这一步。
3. 张量的形状变换:深度学习中无处不在的reshape
3.1 view、reshape、flatten怎么选
形状变换是深度学习里的高频操作,新手和老手最大的区别,就是老手能一眼看出哪一步需要变换、用什么方法变换、变换之后内存是否连续。
view、reshape、flatten是三个最常见的形状变换方法,看起来都能改变shape,实际上有区别:
view:不复制数据,只改变视图的元信息——前提是原始张量在内存中连续。如果不连续,会直接报错。reshape:只要元素总数对得上,就能变换。底层自动判断到底用视图还是复制内存,如果必须复制,就分配新内存。这个叫做“尽可能返回视图,不行就复制”。flatten:专门用来把一个维度以上的张量压成一维,本质是reshape(-1)的语义化版本,一般用在把卷积层输出的特征图展平,送进全连接层的那个环节。
从熟练度角度说,我建议这样使用:如果明确知道张量连续,用view最快也最省内存;如果不确定,用reshape最保险;语义上要表达“展平”时用flatten,代码可读性更好。
有一个额外注意点:flatten有一个start_dim参数,可以指定从哪个维度开始展平。比如一个形状为(2, 3, 4, 5)的张量,flatten(start_dim=1)的結果是(2, 60),保留了batch维度。这在处理批量数据时特别常用,因为全连接层需要二维输入(batch, features),而卷积输出是四维。
3.2 transpose与permute:维度交换的正确姿势
二维矩阵转置用tensor.T或者tensor.transpose(0, 1)。但深度学习里我们更多处理三维、四维张量,批量换维度就要用permute。
transpose一次只交换两个维度,比如tensor.transpose(1, 2)把第1维和第2维交换。permute则可以一次性把所有维度打乱重排,比如tensor.permute(0, 2, 1, 3)。
这里有个高频坑:转置之后忘了调contiguous(),导致后续的view操作报错说张量不连续。我写一个典型的错误示范:
python复制# 错误示范
x = torch.randn(4, 32, 28, 28) # 假设是4张32通道的28x28图像
x = x.transpose(1, 2) # 变成 (4, 28, 32, 28)
x = x.view(4, -1) # 报错!因为transpose后不连续
正确做法是加一个contiguous():
python复制x = x.transpose(1, 2).contiguous()
x = x.view(4, -1)
这种问题在你处理Transformer结构、注意力机制时特别常见。因为QKV矩阵的维度重排几乎离不开transpose和permute,很多初学者第一次实现attention就是因为漏掉contiguous()而卡住。
我自己的习惯是:需要连续内存的操作(view、flatten、nn.Linear的输入)之前,如果数据经过转置或切片获取,都会在心里默默问一句“这个张量还连续吗”,不确定的时候直接补一个.contiguous(),虽然有时候会多一次内存复制,但至少不会爆炸。
3.3 squeeze与unsqueeze:增减维度的实战场景
增维和降维是另一个被问爆的点。squeeze用于删除大小为1的维度,unsqueeze用于在指定位置插入大小为1的维度。
为什么需要这种操作?因为有些计算要求特定维度的张量才能对齐。比如你有4个向量,每个向量有8个元素,形状是(4, 8),现在想把这4个向量变成拼接后的二维张量,不需要增维。但如果你想对每个向量分别做某种处理,可能就需要先unsqueeze变成(4, 1, 8),处理完再squeeze回来。
另一个经典场景是注意力机制里的mask操作。假设你有一个(batch, seq_len)的mask张量,要加到(batch, num_heads, seq_len, q_len)的注意力分数上,就要在指定维度unsqueeze(1)或unsqueeze(2),否则广播规则对不上。
使用squeeze时要小心默认行为:不带参数时,它会删除所有大小为1的维度。如果你的某个维度碰巧也是1,但你有意保留它,就会被误删。所以我会尽量带参数写,比如squeeze(1),只删第1维,这样语义更明确。
3.4 实战示例:CNN中特征图变换的完整链路
把形状变换知识点串起来,用一个CNN分类任务的特征图流程来演示。
假设输入是一张3通道的224x224图片,batch大小为8。数据进入PyTorch后的形状是(8, 3, 224, 224)。
经过卷积层之后,假设输出32个通道,尺寸缩小到112x112,形状变成(8, 32, 112, 112)。再接一个池化层,变成(8, 32, 56, 56)。
这个时候要接全连接层做分类,全连接层要求输入形状为(batch, features),所以需要展平。常用代码:
python复制x = x.flatten(1) # 形状 (8, 32*56*56) = (8, 100352)
x = fc(x) # 全连接层输出 (8, num_classes)
如果你用的是x = x.view(x.size(0), -1),效果一样,但flatten(1)语义更清晰。如果数据在展平之前经过深层卷积的转置操作,记得先contiguous()。
如果你处理的是Transformer结构,一般在输入阶段就会有这样一段:
python复制# 假设输入 x 形状是 (batch, seq_len, feature_dim)
# 如果数据是 (batch, feature_dim, seq_len),需要 permute 成前者
x = x.permute(0, 2, 1).contiguous()
深度学习模型本质上就是张量形状不断变换的过程。把每次shape变化前后的维度都写清楚,模型结构基本就不会出大错。
4. 张量的运算与广播机制:高性能计算的秘密
4.1 逐元素运算与矩阵乘法
张量运算大体分成两类:逐元素运算和线性代数运算。
逐元素运算,就是两个形状相同的张量,对应位置直接做加减乘除:
python复制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.]) 注意这是逐元素乘,不是矩阵乘
特别注意*和@的区别。*是逐元素乘法,@是矩阵乘法(或者对批量数据做批量矩阵乘)。刚入门的人经常把这两个搞混,尤其是处理(batch, n, m)张量时,用*会导致shape对不上或者计算错误。
矩阵乘法在机器学习里太常见了。线性层y = Wx + b本质上就是矩阵乘加偏置。在PyTorch里,nn.Linear帮你把这步封装好了,但理解底层逻辑仍然重要——因为当你要自己实现Bert那样的注意力机制时,Q @ K.transpose(-2, -1)这种操作就是核心。
PyTorch还有一个torch.matmul,它做的事情是“智能矩阵乘法”:根据输入维度自动决定是逐元素乘还是矩阵乘,支持各种批量维度组合。还有更高层的torch.bmm,专门处理(batch, n, m) @ (batch, m, p),也就是批量矩阵乘法,底层走的是BLAS库,速度很快。如果你想做矩阵乘并且都是明确的二维矩阵,用@就行;如果是批量数据,用torch.bmm或者torch.matmul都行。
4.2 广播机制:两条规则与常见陷阱
广播机制值得单独拿出来讲,因为它是PyTorch里最容易让新手产生玄学报错的地方。
广播的规则说起来很简单:两个张量从最后一个维度开始对齐,维度大小要么相等,要么其中一个为1,要么其中一个缺失。
举几个例子:
python复制# 普通广播
a = torch.randn(3, 4)
b = torch.randn(4)
c = a + b # shape (3, 4),b被广播到3行
# 更明显的例子
x = torch.randn(4, 3)
mean = x.mean(dim=0, keepdim=True) # shape (1, 3)
x_norm = x - mean # 广播减法,属于每个特征去均值
注意keepdim=True很重要,如果不加,mean后x形状是(3,),和(4, 3)对齐规则刚好也对得上,结果也成立,但可读性差,而且去掉维度后语义就不对了。
常见的陷阱:两个张量形状是(4, 3)和(3,),可以广播成(4, 3)。但如果是(4, 3)和(2,),就不能广播,因为最后一个维度3和2不相等且都不是1,直接报错。
很多人在实现注意力机制时写(batch, seq_len, hidden) @ (batch, hidden, seq_len)得到(batch, seq_len, seq_len),下一步要加到mask上,mask形状是(seq_len, seq_len)或者(batch, seq_len),这时候如果广播规则掌握不熟,就会报出看不懂的错。
我的建议是:每次做运算前,先心里过一遍两个张量的shape从尾部对齐是否满足规则。不满足就补unsqueeze,不要硬凑,否则报错信息能让你debug到崩溃。
4.3 与NumPy互操作、设备迁移、精度注意
numpy()和from_numpy()是PyTorch与NumPy互通的桥梁。但这里有个大坑:CPU上的张量,两者共享底层内存,修改一方另一方也会变。
python复制x = torch.ones(3)
y = x.numpy()
y[0] = 100
print(x) # tensor([100., 1., 1.])
这个特性在工程上可能造成隐蔽bug。如果你并不想共享内存,就加一个.clone()或.copy()。
设备迁移这块实际操作很多。模型训练之前先把数据搬到GPU:
python复制device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
x = x.to(device)
model = model.to(device)
但是注意,模型搬到GPU之后,它的参数也变成GPU张量了,如果你把一个CPU张量喂给模型,会直接报设备不一致的错误。这时候最好养一个习惯:所有输入数据在进入模型之前统一执行.to(device)。
再提醒一个数据处理阶段的坑:很多人喜欢在NumPy里做完预处理再转成torch张量,这没问题,但注意转之前确定dtype。图像像素值在[0,255]是uint8类型,直接转成torch后做正经浮点运算可能会出问题。正确做法是:
python复制img = img.astype(np.float32) / 255.0 # 归一化
img_tensor = torch.from_numpy(img).permute(2, 0, 1) # HWC转CHW
这个HWC转CHW的步骤做图像处理极其常见,本质就是第3部分的permute。搞懂原理后会发现,深度学习里很多“魔法操作”都是张量基本功的排列组合。
5. 张量与自动求导:PyTorch最核心的设计
5.1 requires_grad与计算图
PyTorch之所以能在深度学习框架里杀出重围,跟自动求导机制关系密切。
当你把一个张量设为requires_grad=True时,PyTorch会追踪所有基于它进行的运算,构建一个记录操作顺序的有向无环图(DAG)。图的叶子节点是输入张量(包括模型参数),边是运算操作(如加法、乘法、卷积等),节点是中间结果。
调用loss.backward()时,PyTorch从loss节点出发,沿着反向方向一步步求导,把梯度值传递给每个requires_grad=True的张量,填到它的.grad属性里。
这个机制对新手最大的影响是:当你设置了requires_grad=True,但后续运算中用到了不可导的操作(比如取整、argmax),梯度就断了,反向传播会报错或者梯度为None。另外,默认情况下PyTorch只在叶子节点上保存梯度,中间变量除非你调用.retain_grad(),否则查不到grad。
如果你参与推理阶段,不希望记录计算图浪费内存,就用torch.no_grad()上下文管理器:
python复制with torch.no_grad():
output = model(x)
评估模型、生成测试输出、推理时几乎都要加这一句,否则计算图会被一直保留,内存越吃越多。
5.2 一个完整训练循环中张量的角色
把前面所有内容串起来,看一个典型训练循环里张量是怎么流转的。
python复制for epoch in range(num_epochs):
for batch_x, batch_y in dataloader:
batch_x = batch_x.to(device) # 数据张量搬到GPU
batch_y = batch_y.to(device)
optimizer.zero_grad() # 清空上一步的梯度
outputs = model(batch_x) # 前向传播,返回预测张量
loss = criterion(outputs, batch_y) # 计算损失张量
loss.backward() # 反向传播,填充每个参数的grad
optimizer.step() # 用grad更新参数
这段代码里,每一行几乎都在操作张量:
batch_x是输入数据张量,requires_grad默认为False。outputs是模型前向传播的预测结果,由于模型参数requires_grad=True,predict张量本身被计算图记录,requires_grad也是True。loss是损失标量,0维张量,也是通过计算图生成的。loss.backward()执行反向传播,模型参数获得.grad。optimizer.step()用param.data -= lr * param.grad之类的方式更新参数。注意这里是param.data,意图是更新参数时不再把更新过程也计入计算图。
有一个我之前见过的问题:loss.backward()之后,如果你想看某个中间特征的梯度,必须在中途用register_hook或者retain_grad()保留,否则默认只保存叶子节点。如果你只是想调试一下,可以临时设retain_grad(),用完再撤掉。
5.3 实战案例:从零实现一个线性回归
为了不要把自动求导停留在理论,我们看一个最小可复现的例子。目标是用张量技术手动实现一个线性回归,不依赖nn.Linear。
python复制import torch
# 生成模拟数据 y = 2x + 3 + 噪声
torch.manual_seed(42)
x = torch.linspace(-1, 1, 100).reshape(-1, 1) # shape (100, 1)
y_true = 2 * x + 3 + 0.1 * torch.randn_like(x)
# 参数初始化
w = torch.randn(1, 1, requires_grad=True)
b = torch.zeros(1, 1, requires_grad=True)
learning_rate = 0.1
for epoch in range(200):
# 前向传播:手动计算 y_pred
y_pred = x @ w + b
# 损失:均方误差
loss = ((y_pred - y_true) ** 2).mean()
# 反向传播
loss.backward()
# 手动更新参数(不追踪梯度)
with torch.no_grad():
w -= learning_rate * w.grad
b -= learning_rate * b.grad
# 清空梯度
w.grad.zero_()
b.grad.zero_()
if epoch % 20 == 0:
print(f'epoch {epoch}, loss {loss.item():.4f}')
print(f'w: {w.item():.4f}, b: {b.item():.4f}')
注意几件事:x @ w + b中,w和b都是(1, 1)形状,广播机制让每个样本的预测都能对齐。loss.item()用于把0维张量转成Python浮点数打印,这里不能用float(loss)某些情况下会报错,.item()是标准做法。
这个例子虽然简单,但把张量创建、矩阵运算、自动求导、梯度更新、梯度清零全部串起来了。如果你能不看任何提示独立写出来,并且能解释每个步骤为什么这么做,我觉得你的张量基础已经超过80%的初学者了。
6. 常见问题与排查技巧:我踩过的那些张量坑
6.1 常见报错速查表
把自己和身边人在张量上遇到过的报错整理成了一张速查表,遇到直接对照。
| 报错信息 | 大概率原因 | 解决方法 |
|---|---|---|
view size is not compatible with input tensor's size and stride |
在非连续张量上调用view | 先.contiguous()再view |
Expected all tensors to be on the same device |
CPU和GPU张量混在一起运算 | 统一用.to(device) |
Expected scalar type Float but found Double |
输入是float64(Double)但模型期望float32 | 转成torch.float32 |
mat1 and mat2 shapes cannot be multiplied |
线性层输入维度不对 | 检查前一层的输出shape,用.flatten(1)接FC层 |
RuntimeError: element 0 of tensors does not require grad |
对requires_grad=False的张量调backward | 检查哪些张量需要梯度 |
CUDA out of memory |
显存不够 | 降低batch size;用with torch.no_grad()推理;减少计算图的保留 |
trying to backward through the graph a second time |
同一计算图被重复backward,默认PyTorch会释放图 | 第一次backward前加retain_graph=True(但一般建议改代码避免) |
这里面最经典、出现频率最高的是前四个。其中device不一致的问题在单机多卡或者迁移学习时特别常见——预训练模型参数默认在CPU上,你直接塞给GPU数据就报这个错。解决方式是加载模型后立刻.to(device)。
6.2 内存与速度优化技巧
张量操作直接影响内存占用和速度,分享几个实战技巧。
第一,能用视图绝不用副本。切片、view、transpose都不会复制数据,尽量用它们。clone()会复制内存,能少用就少用。判断一个操作是否复制数据,可以看文档或者看底层行为。
第二,及时释放不用的张量。在循环里运行推理时,用del删除大张量,或者直接让变量引用的旧对象失去引用,Python的垃圾回收会处理。调torch.cuda.empty_cache()可以在显存不够时清理缓存,但它不是万能的——它清的是PyTorch缓存池里的空间,不是释放你还在引用的张量。
第三,requires_grad能关就关。推理阶段一定用torch.no_grad()包住。我自己写评估代码时忘了加,导致16GB显存爆掉,加了之后显存占用直接下降一半,这个优化效果是立竿见影的。
第四,用混合精度训练。在支持CUDA的设备上,用PyTorch自带的torch.autocast把一部分操作降到FP16,能显著提升速度并降低显存占用,同时保持精度。现在主流的训练脚本里基本都会用它。
第五,批量操作优于循环。写代码时尽量把循环改成矩阵批量运算。比如你想对100张图做处理,循环一次处理一张,不如把100张图拼成一个(100, C, H, W)张量,一次调用模型。GPU是为大规模并行设计的,单张图推理不仅慢,GPU利用率还低。
这些优化点每个都值得单独拎出来写一篇,但在张量层面,最核心的就是:理解数据在内存里的布局,充分利用视图机制,避免无意义的复制;理解设备差异,避免频繁在CPU和GPU之间搬数据;理解计算图和自动求导机制,避免保留不必要的图和梯度。
扩展:张量在不同硬件与场景中的适配
有同学会问,张量只能用在CPU和NVIDIA GPU上吗?不是的。PyTorch张量已经适配了多种硬件后端。除了CUDA,还有Apple Silicon的MPS后端(通过mps设备调用),以及各种AI芯片厂商的适配方案。不同硬件下的张量操作接口基本一致,差异主要体现在性能和某些算子的支持度上。
举个实际例子,如果你的电脑只有CPU没有独立显卡,也是完全可以跑深度学习的,只是速度慢。训练小型模型、跑通代码、验证思路都没问题。很多人一开始总担心“没有GPU还能不能学深度学习”,其实环境配置跟上、数据规模控制好,完全能学。真正需要GPU的是大规模训练场景。
TensorFlow和PyTorch之间张量概念相通,从一个框架切到另一个框架,只要把张量的基本操作吃透了,适应成本很低。技术选型是另一回事,但底层数学原理和数据结构设计思想是高度一致的。
结尾:一点个人经验
从最开始对着torch.tensor和torch.Tensor的差异一脸懵,到能随手写出一段张量操作流畅的训练代码,我觉得最关键的不是去背API文档,而是动手多敲、多debug。每遇到一个shape相关的报错,不要急着复制粘贴到搜索引擎,先自己拿纸笔画一下张量的shape变化,把每一步中间维度写出来,错在哪一目了然。这样踩过几次坑之后,你对张量维度的直觉会变得非常准。
另外,创建张量之前,先想清楚这个张量会被谁用、会在哪一步被变换、会被送到哪个设备。动手前全局思考,是最能帮你远离隐性bug的习惯。张量这块地基打得稳,后面学CNN、Transformer、各种新模型都会顺畅很多。
