行业资讯
深度学习张量核心指南:从多维数组到PyTorch/TensorFlow实战
1. 从“数”到“张量”为什么我们需要它如果你刚开始接触深度学习翻开任何一本教材或者打开一个PyTorch、TensorFlow的教程第一个迎面撞上的概念很可能就是“张量”Tensor。它无处不在数据是张量模型的参数是张量计算的结果也是张量。你可能会困惑这不就是多维数组吗为什么非要发明“张量”这么个听起来高大上的词直接用“数组”、“矩阵”不就好了这恰恰是理解张量的第一个关键。我们可以把数据的组织形式看作一个不断进化的过程。一个单独的数字比如3.14它是一个标量Scalar。标量是“零维”的它只有大小没有方向就像温度计上的一个读数。当我们把一组标量有序地排列起来比如[1, 2, 3, 4]我们就得到了一个向量Vector。向量是“一维”的它不仅有大小通常还有方向在物理和几何中在计算机里它就是一个一维数组。如果把多个向量要求长度相同并排堆叠比如[[1, 2, 3], [4, 5, 6]]我们就得到了一个矩阵Matrix。矩阵是“二维”的有行和列它可以表示一幅灰度图像每个元素是一个像素的亮度或者一个线性方程组的所有系数。那么张量是什么张量是标量、向量、矩阵向更高维度的自然扩展。你可以粗暴地把它理解为“多维数组”。但这个定义只说了“形”没说到“神”。张量的“神”在于它定义了一套在各种变换下保持不变的运算规则。简单来说矩阵乘法有特定的法则而张量运算如张量积、缩并是矩阵运算在更高维空间中的推广并且保证了无论你从哪个坐标系去看这些数据它们之间的关系运算结果是不变的。这个性质在物理学如广义相对论描述时空弯曲和工程学中至关重要。在深度学习中我们虽然暂时不用关心复杂的坐标变换但继承了“多维数组”这个强大的容器概念。为什么深度学习离不开张量想象一下你要处理的数据一张RGB彩色图片其形状可能是(高度, 宽度, 3)3代表红、绿、蓝三个通道——这是一个三维张量。一个视频片段是连续的多帧图片形状可能是(帧数, 高度, 宽度, 3)——这是一个四维张量。一个批次的图片形状是(批次大小, 高度, 宽度, 3)——这又是一个四维张量。神经网络中全连接层的权重可能是一个(输入特征数, 输出特征数)的二维矩阵而卷积层的权重卷积核可能是一个(输出通道数, 输入通道数, 核高度, 核宽度)的四维张量。张量以一种统一、高效的方式容纳了从原始数据到模型参数的一切并且其维度结构直观地反映了数据的物理或逻辑意义。理解张量就是理解深度学习数据流动的基石。2. 拆解张量的“维度”形状Shape是你的导航图当我们说一个张量是N维的我们指的是它有N个轴Axis或者叫阶Rank。这个“维度”的概念是初学者最容易混淆的地方之一。它和向量空间里的“维度”如三维空间有关联但在张量的语境下更准确的称呼是形状Shape。形状是一个由整数构成的元组它描述了张量在每个轴上的大小。理解形状是理解一切张量操作的前提。2.1 从零维到四维一个具象化的旅程0维张量标量形状为()。就是一个单独的数。在PyTorch中torch.tensor(3.14)就是一个标量。它常用于表示损失值Loss、准确率等单一指标。import torch scalar torch.tensor(42) print(scalar.shape) # 输出torch.Size([]) print(scalar.dim()) # 输出0 表示维度数1维张量向量形状为(n,)。n是向量的长度。它可以表示一个样本的特征向量。例如在鸢尾花数据集中一朵花的“萼片长、萼片宽、花瓣长、花瓣宽”可以构成一个形状为(4,)的张量。vector torch.tensor([1.0, 2.0, 3.0, 4.0]) print(vector.shape) # 输出torch.Size([4])2维张量矩阵形状为(m, n)。m是行数n是列数。它可以表示一个批次batch的样本数据其中m是批次大小n是特征数。例如一个包含32朵鸢尾花数据的批次形状就是(32, 4)。一个全连接层的权重矩阵形状是(in_features, out_features)。matrix torch.tensor([[1, 2, 3], [4, 5, 6]]) print(matrix.shape) # 输出torch.Size([2, 3])3维张量形状为(c, h, w)或(s, m, n)等。这是处理序列数据或彩色图像的关键。在计算机视觉中通道优先Channel-First格式(通道, 高度, 宽度)是PyTorch的默认格式。一张RGB图片就是(3, 224, 224)。在自然语言处理中一个句子单词序列经过嵌入层后可能表示为(序列长度, 批次大小, 词向量维度)。# 模拟一个批次大小为1的RGB图片 image_tensor torch.randn(1, 3, 224, 224) # (batch, channel, height, width) print(image_tensor.shape) # 输出torch.Size([1, 3, 224, 224])4维张量形状通常为(b, c, h, w)。这是深度学习中最常见的张量形状之一代表了一个批次的图像数据。b是批次大小batch sizec是通道数h是高度w是宽度。GPU的并行计算能力非常适合处理这种规整的批次数据。batch_of_images torch.randn(16, 3, 224, 224) # 16张RGB图片 print(batch_of_images.shape) # 输出torch.Size([16, 3, 224, 224])2.2 理解形状的“物理意义”死记硬背形状的数字没有意义。关键在于为每个轴赋予逻辑含义。以常见的图像批次(b, c, h, w)为例第0轴b样本索引轴。batch_of_images[0]取出这批里的第一张图片形状为(c, h, w)。第1轴c通道/特征轴。batch_of_images[:, 0, :, :]取出所有图片的红色通道形状为(b, h, w)。第2、3轴h, w空间轴。batch_of_images[:, :, 100, 150]取出所有图片所有通道在位置(100, 150)的像素值形状为(b, c)。注意框架间的格式差异。PyTorch默认使用(N, C, H, W)通道优先而TensorFlow历史版本默认使用(N, H, W, C)通道最后。在处理预训练模型或不同框架代码时这是一个经典的“坑”。需要使用permute(PyTorch) 或transpose(TensorFlow) 进行转换。2.3 张量的“元数据”不仅仅是形状除了形状理解一个张量还需要关注数据类型dtypetorch.float32,torch.int64,torch.bool等。这决定了张量中元素的数值类型和精度直接影响计算速度和内存占用。混合精度训练就是利用float16和float32的结合来加速。设备devicetorch.device(cpu)或torch.device(cuda:0)。张量存放在CPU内存还是GPU显存中。这是深度学习加速的核心所有参与运算的张量必须在同一设备上否则会报错。存储Storage底层连续的内存块。像view()这样的操作之所以高效就是因为它不复制数据只改变了看待这块存储的“视图”形状和步长。3. 张量的核心运算不仅仅是“加减乘除”张量的运算构成了深度学习模型的前向传播和反向传播。理解这些运算尤其是它们的广播Broadcasting机制是写出正确、高效代码的关键。3.1 元素级运算Element-wise Operations这是最简单的一类运算在两个形状完全相同的张量之间对对应位置的元素进行运算。包括加、减-、乘*、除/、比较等。a torch.tensor([[1, 2], [3, 4]]) b torch.tensor([[5, 6], [7, 8]]) print(a b) # 输出tensor([[ 6, 8], [10, 12]]) print(a * b) # 输出tensor([[ 5, 12], [21, 32]])3.2 广播机制Broadcasting深度学习中的“魔法”这是张量运算中最强大也最容易出错的概念。广播允许在不同形状的张量之间进行运算。其核心规则是从后向前从最右边的轴开始逐轴比较两个张量的形状。如果两个轴的大小相等可以进行运算。如果其中一个轴的大小为1则该轴会被“广播”复制扩展以匹配另一个张量的大小。如果某个轴在一个张量中存在在另一个张量中不存在即维度缺失则虚拟地为其添加一个大小为1的轴然后应用规则2。广播的本质是一种内存高效的隐式复制它避免了实际创建重复数据。示例1标量与任意张量运算。标量被广播到与另一个张量相同的形状。a torch.tensor([[1, 2, 3], [4, 5, 6]]) # shape (2, 3) b 10 # 标量 视为 shape (1,) 然后广播为 (1, 1) 最终广播为 (2, 3) print(a b) # 输出tensor([[11, 12, 13], [14, 15, 16]])示例2向量与矩阵相加。这是最常见的广播场景之一。matrix torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) # shape (3, 3) vector torch.tensor([10, 20, 30]) # shape (3,) # vector 形状 (3,) - (1, 3) - 广播为 (3, 3) # 相当于 vector 被复制成了三行[[10,20,30], [10,20,30], [10,20,30]] print(matrix vector) # 输出 # tensor([[11, 22, 33], # [14, 25, 36], # [17, 28, 39]])示例3更复杂的广播。在批次操作中极其有用。batch torch.randn(4, 3, 224, 224) # 4张图片 mean_per_channel torch.tensor([0.485, 0.456, 0.406]) # ImageNet的RGB均值 shape (3,) # mean_per_channel 形状 (3,) - (1, 3, 1, 1) - 广播为 (4, 3, 224, 224) # 这意味着对每张图片的每个通道都用同一个均值去减 normalized_batch batch - mean_per_channel.view(1, 3, 1, 1) # 显式reshape便于理解踩坑提示广播失败最常见的错误是The size of tensor a (x) must match the size of tensor b (y) at non-singleton dimension z。这通常意味着在某个非1的维度上两个张量的大小不匹配。仔细检查形状使用tensor.shape打印并手动模拟广播过程是调试的不二法门。3.3 矩阵乘法与张量缩并矩阵乘法或torch.matmul规则是(m, n) (n, p) (m, p)。这是神经网络全连接层的基础。A torch.randn(2, 3) # (2, 3) B torch.randn(3, 4) # (3, 4) C A B # 结果形状 (2, 4)批量矩阵乘法torch.bmm用于三维张量(b, m, n) (b, n, p) (b, m, p)。这在处理序列数据如Transformer时很常见对批次中的每个样本独立做矩阵乘法。更一般的张量缩并torch.einsum这是一个极其强大的工具可以用爱因斯坦求和约定简洁地表达复杂的张量运算如点积、外积、转置、对角化、求和等。学习它的基础语法能极大提升你对张量运算的理解。# 矩阵乘法 A torch.randn(2, 3) B torch.randn(3, 4) C torch.einsum(ik,kj-ij, A, B) # 等价于 A B # 批量矩阵乘法 batch_A torch.randn(5, 2, 3) batch_B torch.randn(5, 3, 4) batch_C torch.einsum(bik,bkj-bij, batch_A, batch_B) # 等价于 torch.bmm # 计算向量点积 v1 torch.randn(3) v2 torch.randn(3) dot_product torch.einsum(i,i-, v1, v2) # 等价于 torch.dot(v1, v2)3.4 重塑与维度操作改变“视图”这些操作通常不复制底层数据只是改变了访问数据的“视图”因此非常高效。view()/reshape()改变张量的形状但要求总元素数不变。view()要求张量在内存中是连续的否则会报错reshape()在可能的情况下返回视图否则返回副本。首选reshape()。a torch.arange(12) # shape (12,) b a.reshape(3, 4) # shape (3, 4) c a.reshape(2, 2, 3) # shape (2, 2, 3)permute()/transpose()交换张量的轴维度。permute()可以一次性重新排列所有维度顺序transpose()通常交换指定的两个维度。a torch.randn(2, 3, 224, 224) # (batch, channel, height, width) b a.permute(0, 2, 3, 1) # 变为 (batch, height, width, channel) TensorFlow常用格式 c a.transpose(1, 2) # 交换第1和第2轴变为 (batch, height, channel, width) 这个形状不常见可能用于特定操作squeeze()/unsqueeze()删除或添加大小为1的维度。常用于适配不同操作的输入要求。a torch.randn(1, 3, 1, 224) # shape (1, 3, 1, 224) b a.squeeze() # 删除所有大小为1的轴 shape (3, 224)。 危险可能删除非预期的轴。 c a.squeeze(dim0) # 只删除第0轴 shape (3, 1, 224) d a.squeeze(dim2) # 只删除第2轴 shape (1, 3, 224) e d.unsqueeze(dim1) # 在第1轴插入一个维度 shape (1, 1, 3, 224)4. 张量的高级理解存储、步长与连续化要真正玩转张量避免一些隐蔽的性能陷阱和运行时错误你需要窥探其内部机制。4.1 存储Storage与步长Stride每个张量都有一个指向一维连续内存块Storage的视图。stride步长是一个元组表示为了在每个维度上移动一个元素需要在存储中跳过的元素数量。shape和stride共同定义了张量的视图。x torch.arange(12).reshape(3, 4) print(x) # tensor([[ 0, 1, 2, 3], # [ 4, 5, 6, 7], # [ 8, 9, 10, 11]]) print(x.stride()) # 输出(4, 1) # 解释要移动到下一行第0维需要在存储中跳过4个元素因为每行有4列。 # 要移动到下一列第1维只需要跳过1个元素。像permute()这样的操作只改变了stride数据在内存中的物理顺序没变。这解释了为什么某些操作如转置后取对角线可能效率不高因为访问模式不再是连续的。4.2 连续化Contiguous当张量的元素在内存中不是按逻辑顺序连续存储时它就是非连续的non-contiguous。某些操作如view()要求张量是连续的。你可以用is_contiguous()检查并用contiguous()方法如果需要的话会复制数据使其连续。x torch.arange(12).reshape(3, 4) y x.t() # 转置 shape变为 (4, 3) print(y.is_contiguous()) # 输出False # y在内存中的顺序是 [0, 4, 8, 1, 5, 9, ...] 不是 [0, 1, 2, ...] try: z y.view(12) # 会报错因为y是非连续的 except RuntimeError as e: print(e) # 输出view size is not compatible with input tensors size and stride... z y.contiguous().view(12) # 先连续化再改变视图 print(z) # 输出tensor([0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11])4.3 原地操作In-place Operations以_为后缀的操作是原地操作如add_(),transpose_()。它们会直接修改原张量的数据不创建新张量可以节省内存。但必须极其小心因为这会破坏计算图在需要自动求导requires_gradTrue时可能导致错误或未定义行为。在模型训练中除非你非常清楚在做什么例如在优化器更新参数时否则应避免对叶变量leaf variable使用原地操作。a torch.tensor([1, 2, 3], requires_gradTrue) b torch.tensor([4, 5, 6], requires_gradTrue) c a b loss c.sum() loss.backward() # 正常计算梯度 print(a.grad) # 输出tensor([1., 1., 1.]) # 危险操作 a.add_(b) # 原地操作改变了a的值 # 此时a的梯度计算历史被破坏再次backward会出错或得到错误结果5. 实战中的张量从数据加载到模型输出让我们串联起所有概念看一个简单的图像分类流程中张量是如何流动的。5.1 数据加载与预处理假设我们使用PyTorch的DataLoader加载CIFAR-10数据集。从磁盘读取一张JPEG图片得到的是一个PIL Image对象或numpy数组H, W, C。转换为张量transforms.ToTensor()将其转换为PyTorch张量并自动将值范围从[0, 255]缩放到[0.0, 1.0]同时将形状从(H, W, C)转换为(C, H, W)。此时张量形状为(3, 32, 32) dtype是torch.float32。标准化transforms.Normalize(mean[0.5,0.5,0.5], std[0.5,0.5,0.5])对每个通道进行减均值、除标准差操作。这里利用了广播mean和std是形状为(3,)的向量被广播到(3, 32, 32)的图片张量上。组成批次DataLoader将多个比如64个处理后的图片张量堆叠stack起来。stack操作在现有维度前增加了一个新的批次维度得到形状为(64, 3, 32, 32)的四维张量。5.2 模型前向传播以一个简单的CNN为例输入input_tensor形状为(64, 3, 32, 32)。卷积层nn.Conv2d(3, 16, kernel_size3)。卷积核权重是一个四维张量(16, 3, 3, 3)。卷积运算可以看作一种特殊的张量缩并。输出张量形状为(64, 16, 30, 30)假设padding0, stride1。激活函数F.relu()。这是一个元素级操作输入输出形状不变。池化层nn.MaxPool2d(2)。在空间维度上进行下采样输出形状变为(64, 16, 15, 15)。展平在进入全连接层前需要将空间特征图展平为一维向量。x x.view(x.size(0), -1)。这里x.size(0)是批次大小64-1是自动计算出的维度16*15*153600。展平后形状变为(64, 3600)。全连接层nn.Linear(3600, 10)。权重矩阵形状为(3600, 10)。进行矩阵乘法(64, 3600) (3600, 10)得到输出形状(64, 10)即64个样本每个样本对应10个类别的得分logits。5.3 损失计算与梯度损失函数criterion nn.CrossEntropyLoss()。它接受预测值(64, 10)和真实标签(64,)每个标签是0-9的整数。内部计算涉及张量的log_softmax和nll_loss。反向传播loss.backward()。框架会沿着这个由张量运算构成的计算图利用链式法则为所有requires_gradTrue的张量主要是模型参数计算梯度。这些梯度本身也是张量形状与对应的参数张量完全相同。5.4 常见问题排查形状不匹配错误这是最最常见的错误。例如全连接层输入特征数不匹配。养成习惯在模型forward函数的每个关键步骤后打印张量形状print(x.shape)或者使用调试器。设备不匹配错误RuntimeError: Expected all tensors to be on the same device。确保你的模型和数据在同一个设备CPU/GPU上。使用.to(device)进行统一迁移。数据类型错误某些操作要求特定的数据类型。例如argmax通常返回int64而作为索引使用时必须确保是整数类型。使用.to(torch.long)或.long()进行转换。“视图”错误在对非连续张量进行view()操作时报错。先用contiguous()处理或者更安全地使用reshape()。理解张量就是理解深度学习框架中数据流动的语言。它从简单的多维数组开始但其背后的广播、视图、连续化等概念是写出高效、正确代码的保障。最好的学习方式就是多动手在Jupyter Notebook或脚本中创建各种形状的张量尝试不同的操作并时刻用.shape、.stride()、.device、.dtype来检查你的“作品”。当你能够自如地操纵张量的形状和运算时你就掌握了打开深度学习模型黑盒的第一把钥匙。
郑州网站建设
网页设计
企业官网