张量深入解析:从多维数组到深度学习代码实战
很多刚开始接触机器学习的同学第一次听到“张量 Tensor”这个术语时往往会被两个字吓住又是“张”又是“量”听起来像物理课里的相对论或者数学系高年级的抽象代数。但实际上张量在机器学习里的含义非常朴素它就是多维数组。你以前写 Python 时用过的列表、NumPy 里的 array在深度学习框架 PyTorch 和 TensorFlow 里都有一个更正式的名字——Tensor中文翻译就是“张量”。这篇文章的核心判断是**你不需要理解数学家眼中张量的严格定义也能把深度学习完整学下去。你真正需要掌握的是张量的形状shape、维度dimension、变形reshape和广播broadcasting这些“手感”层面的东西。**这也是很多视频课程在讲神经网络时默认你已经会了、但实际新手最容易卡住的部分。读完这篇文章你会搞清楚张量到底是什么标量、向量、矩阵和张量是什么关系为什么图像、文本、表格最终都会被变成张量在 NumPy 和 PyTorch 里怎么创建和操作张量以及你会在实际代码里遇到哪些和张量有关的报错。文章会从概念讲到代码再讲到排查思路建议先收藏再慢慢看。1. 为什么“张量”这个词让初学者头疼先聊一个很实际的现象。在 B 站、YouTube 或 Coursera 上看机器学习课程时前几节课通常很友好线性回归、逻辑回归、损失函数、梯度下降每个名词都能在数学和代码之间找到对应。等到课程进入神经网络章节老师开始说“我们把输入数据变成一个 tensor然后经过一个 linear layer再做 reshape最后接 softmax。”如果你没有提前接触过 NumPy到这里就会开始困惑tensor 是什么为什么图像长成了(64, 3, 32, 32)这种奇怪形状为什么reshape可以随便把数据变来变去难道不会把数据顺序搞乱吗broadcast又是什么东西为什么一个形状是(32, 1)的张量可以和一个形状是(1, 64)的张量直接相加这些问题不解决后面的代码看起来就像“魔法”模型跑通了但你不知道自己敲的每个数字是什么意思。换一个数据集、换一个输入尺寸代码就报错而且报错信息往往还是英文术语比如RuntimeError: The size of tensor a (64) must match the size of tensor b (32) at non-singleton dimension 3这类报错本质上就是张量形状没对齐。所以与其说“张量”是一个数学概念不如说它是机器学习和深度学习代码世界里的基础数据类型。你读懂了张量就读懂了深度学习代码的“字母表”。这也是我把“张量深入解析”单拎出来写一篇文章的原因——它是很多入门者从“会调包”到“懂代码”之间的第一道坎。2. 张量到底是什么从标量、向量、矩阵到张量2.1 数学层面的定义在数学里张量的严谨定义需要用到多重线性代数和坐标变换涉及逆变、协变这些概念。但在机器学习的实际使用中你可以采用一种更实用、更简化的理解方式标量Scalar一个单独的数例如5、3.14。在张量术语里它是一个 0 维张量。向量Vector一列数例如[1, 2, 3]。它有一个维度轴在张量术语里是 1 维张量。矩阵Matrix一个二维表格例如 3 行 4 列的数据。在张量术语里是 2 维张量。张量Tensor维度大于等于 3 的数组的统称例如一个形状为(3, 4, 5)的三维数组。但在深度学习框架里所有维度包括 0 维、1 维、2 维的数组都叫张量。看下面这个对照表会更直观数学名称张量维度形状示例Python 列表示例说明标量0 维()5一个数向量1 维(3,)[1, 2, 3]一列数矩阵2 维(3, 4)[[1,2,3,4], ...]二维表格三阶张量3 维(2, 3, 4)嵌套三层列表多个矩阵叠起来四阶张量4 维(2, 3, 4, 5)嵌套四层列表更复杂的数据块你可以这样理解张量是“数”的容器维度就是在不断“套娃”。一个矩阵由多个向量构成一个三阶张量由多个矩阵构成一个四阶张量由多个三阶张量构成。2.2 为什么深度学习框架不区分向量和矩阵一个值得注意的细节是在数学里向量、矩阵、张量常常被分开称呼但在 PyTorch、TensorFlow 里它们统一都叫 tensor。你调用torch.tensor([1, 2, 3])它返回的既是一个一维数组也是一个“张量”。这种设计的好处是统一、简洁。深度学习中所有操作——加法、矩阵乘法、卷积、拼接——都定义在张量类型上不管它是几维。你不需要为向量单独写一套代码为矩阵再写另一套代码。当然代价就是很多新手看到shape时不太确定自己手里这个 tensor 是“行向量”还是“列向量”这个问题我们在第 5 节结合代码再展开。2.3 一个容易混淆的点张量 vs 张量场如果你搜索“张量”这个词可能会看到 “张量场”“黎曼张量”“应力张量”这些物理和微分几何里的概念。这些虽然有学术联系但和你写机器学习代码几乎没有关系。在机器学习语境下你可以放心地把张量当成“多维数组”的同义词。等到以后想深入研究底层框架实现时再去了解数学意义上的张量也不迟。不要因为这个词的学术背景而感到压力它并不比“列表”高端多少只是多了一些形状上的约定。3. 张量为什么在机器学习中如此重要理解了张量是“多维数组”之后下一个问题是为什么机器学习离不开它3.1 数据表示的统一标准机器学习处理的数据类型很多但最终都可以转成张量表格数据每一行是一个样本每一列是一个特征。一批 100 个样本、每个样本 20 个特征就变成一个形状为(100, 20)的二维张量。图像数据一张彩色图片可以看作一个三维数组高度、宽度、颜色通道RGB。一批 64 张图片就变成(64, 3, 256, 256)这样的四维张量batch、channel、height、width简写 NCHW。文本数据一段文字需要先转成 token id再通过词嵌入变成向量。一个批次里 32 句话、每句话最多 50 个 token、每个 token 映射成 300 维向量就变成(32, 50, 300)的三维张量。时间序列数据一批 128 条序列、每条序列有 100 个时间步、每个时间步 5 个特征对应(128, 100, 5)。可以说张量是机器学习世界里的“通用货币”。无论原始数据是图片、文字还是数据库里的表进入模型之前都会被兑换成张量。3.2 批量计算的效率来源传统 Python 处理数据时最简单的方式是写 for 循环。但 Python 循环本身较慢大数据集上尤其明显。张量配合 GPU 能实现高效的并行计算因为 GPU 的架构更适合同时处理大量相同运算而不是一个接一个地执行指令。举个例子你要计算 100 万个数的平方和。用 Python 列表加 for 循环可能需要几十毫秒用 NumPy 数组或 PyTorch 张量直接做x ** 2底层调用的是编译好的 C/CUDA 代码速度可以快几个数量级。更重要的是神经网络的反向传播也建立在张量运算的基础上每一层的前向计算和梯度计算都是张量操作。3.3 自动微分的基础现代深度学习框架之所以能自动求梯度是因为它们会把张量上的每个运算都记录下来形成一张计算图。如果你想手动用 for 循环自己算梯度代码会非常复杂而当你把所有数据组织成张量、用框架提供的张量运算完成前向传播时框架就能自动沿着计算图反向传播梯度。简单说张量既是数据的容器也是自动微分这个机制能运转起来的前提条件。4. 用 NumPy 理解张量的核心操作在真正进入 PyTorch 之前先用 NumPy 打底是最好的路径。NumPy 的 ndarray 和 PyTorch 的 Tensor 在创建、索引、变形等操作上非常相似而 NumPy 更轻量适合做实验。4.1 创建不同维度的 ndarrayimport numpy as np # 0维张量标量 scalar np.array(5) print(标量:, scalar, 形状:, scalar.shape, 维度:, scalar.ndim) # 1维张量向量 vector np.array([1, 2, 3]) print(向量:, vector, 形状:, vector.shape, 维度:, vector.ndim) # 2维张量矩阵 matrix np.array([[1, 2, 3], [4, 5, 6]]) print(矩阵:\n, matrix, \n形状:, matrix.shape, 维度:, matrix.ndim) # 3维张量 tensor_3d np.arange(24).reshape(2, 3, 4) print(3维张量:\n, tensor_3d, \n形状:, tensor_3d.shape, 维度:, tensor_3d.ndim)运行这段代码你会发现shape是张量的核心属性之一它告诉你“这个张量在每个维度上有多少个数”。ndim则告诉你张量有几个维度。4.2 变形操作 reshape我们在处理数据时经常需要调整张量形状最常见的任务是把 28×28 的图片展平成 784 维的向量把(batch, height, width, channel)调整为(batch, channel, height, width)把 1 维序列按窗口切成多个小片段。reshape的核心规则是新形状的元素总数必须等于旧形状的元素总数。比如(2, 3, 4)有 24 个元素你可以变形为(24,)、(6, 4)、(4, 6)、(2, 12)但不能变成(5, 5)25 个元素。import numpy as np x np.arange(12) # 从 1 维变为 3 行 4 列 matrix x.reshape(3, 4) print(matrix) # 结果 # [[ 0 1 2 3] # [ 4 5 6 7] # [ 8 9 10 11]] # 用 -1 让 numpy 自动推导某个维度 matrix2 x.reshape(2, -1) print(matrix2.shape) # (2, 6) # 展平 flat matrix2.reshape(-1) print(flat.shape) # (12,)这里特别要提醒reshape(-1)是一种非常常见的写法表示“我自己也不想算总数你帮我自动推导”。但不要滥用——如果原数据本身的语义顺序不对强行 reshape 会把数据搞乱。比如一张图片从原始的二维像素排列变成三维的(color, height, width)顺序错了画面就会花掉。4.3 广播机制 broadcasting广播是张量操作里最容易出 bug、也最体现“张量思维”的特性。简单理解当两个张量形状不完全一致时NumPy/PyTorch 会自动把较小的张量扩展到较大的张量然后执行运算。扩展是有规则的并非随便乱扩。规则是从最后一个维度开始对齐如果两个维度相等继续比较前一个维度如果其中一个维度是 1则复制这个维度以匹配对方如果一个维度完全缺失也按 1 处理。看几个实际例子import numpy as np # 例1向量加标量 a np.array([1, 2, 3]) b 5 print(a b) # [6 7 8] # 例2矩阵每一行加同一个向量 matrix np.arange(6).reshape(2, 3) row np.array([10, 20, 30]) print(matrix row) # 结果 # [[10 21 32] # [13 24 35]] # 例3形状为 (3,1) 的列向量和形状为 (1,3) 的行向量相加 col np.array([[1], [2], [3]]) row2 np.array([[10, 20, 30]]) print(col row2) # 结果 # [[11 21 31] # [12 22 32] # [13 23 33]]第三个例子的背后逻辑是(3,1)和(1,3)先按列/行复制成(3,3)的矩阵再逐元素相加。如果两个张量的维度完全对不上比如(3,4)和(2,5)就会报错。很多机器学习代码里bias项的形状是(num_features,)数据形状是(batch, num_features)二者相加时广播机制会自动把 bias 应用到每个样本上这正是广播在模型里最常见的用途。5. 在 PyTorch 中认识真正的“机器学习张量”NumPy 是很好的入门工具但实际训练模型时你更常用的是 PyTorch 或 TensorFlow 中的 Tensor。原因有两个一是 PyTorch 的张量支持 GPU 加速二是它可以参与自动微分。本节的示例以 PyTorch 为主版本以你安装的稳定版为准代码思路具有通用性。5.1 创建张量及基本属性import torch # 从 Python 列表创建 a torch.tensor([1, 2, 3]) print(a.dtype) # torch.int64 print(a.shape) # torch.Size([3]) # 从 NumPy 数组创建 import numpy as np np_arr np.array([1.0, 2.0, 3.0]) b torch.from_numpy(np_arr) print(b.dtype) # torch.float64 # 创建指定形状的全 0 / 全 1 张量 zeros torch.zeros(2, 3) ones torch.ones(2, 3) print(zeros.shape, ones.shape) # 创建随机张量 rand_t torch.randn(2, 3) print(rand_t)这里需要特别关注的属性是dtype。PyTorch 默认的浮点类型是torch.float32但在某些老代码里可能是torch.float64。如果两个张量 dtype 不一致运算时可能报错或产生意料之外的精度问题。5.2 设备移动让张量在 GPU 上运行深度学习中计算量太大通常会把张量从 CPU 移动到 GPU# 判断是否有 GPU device torch.device(cuda if torch.cuda.is_available() else cpu) print(当前设备:, device) x torch.randn(3, 3) x x.to(device)这段代码表示如果环境有 CUDA 显卡就把张量放到 GPU 上否则留在 CPU。注意一个常见坑CPU 张量和 GPU 张量不能直接做运算。如果你得到一个错误提示说 “Expected all tensors to be on the same device”一般就是因为有两个张量分别在 CPU 和 GPU 上。5.3 张量与自动微分这是 PyTorch 张量和 NumPy 数组最大的区别。通过设置requires_gradTruePyTorch 会记录张量上的运算并在调用backward()时自动计算梯度。import torch # 创建一个需要梯度的张量 x torch.tensor([2.0], requires_gradTrue) # 定义一个简单函数 y x^2 y x ** 2 # 反向传播 y.backward() # 查看梯度 dy/dx 2*x 4 print(x.grad) # tensor([4.])在这个例子中你不需要手动推导导数和写求导公式PyTorch 的张量运算图会自动帮你算出结果。这也是张量能支撑整个深度学习训练闭环的关键前向计算是张量运算反向求梯度也是张量运算二者共享同一套数据结构。5.4 unsqueeze 与 squeeze增维和减维实际编码中很多模型要求输入有特定维度。比如某个卷积层期望输入是四维(batch, channel, height, width)但你的数据是二维(height, width)这时就需要增维。import torch # 原形状 (28, 28) img torch.randn(28, 28) # 在第 0 维增加 batch 维度变成 (1, 28, 28) img_batch img.unsqueeze(0) print(img_batch.shape) # torch.Size([1, 28, 28]) # 再在第 1 维增加通道维度变成 (1, 1, 28, 28) img_channel img_batch.unsqueeze(1) print(img_channel.shape) # torch.Size([1, 1, 28, 28]) # 反过来压缩维度 squeezed img_channel.squeeze() print(squeezed.shape) # torch.Size([28, 28])squeeze()在不传参数时会把所有长度为 1 的维度都去掉传参数时可以只压缩指定的维度比如squeeze(0)。6. 机器学习模型中常见的张量形状搞懂张量操作以后还有一个非常实际的问题模型代码里那些张量形状到底代表着什么这里我给你整理三类最常见的数据形状理解了它们以后看模型代码会轻松很多。6.1 表格数据结构化数据假设你有 1000 条用户数据每条有 20 个特征年龄、收入、点击次数等模型输入通常是(batch_size, num_features)例如一个训练批次取 64 条样本张量形状就是(64, 20)。可以看到PyTorch 或 sklearn 里很多模型的输入都是“行是样本列是特征”这种二维结构。6.2 图像数据图像在深度学习框架里通常用四维张量表示PyTorch 默认布局为 NCHW(batch, channels, height, width)TensorFlow 默认布局为 NHWC(batch, height, width, channels)一张 RGB 彩色图片高度 256、宽度 256那么单个图片就是(3, 256, 256)。一个批次 32 张图片在 PyTorch 里就是(32, 3, 256, 256)。很多新手第一次看到(64, 3, 32, 32)时容易懵其实翻译过来就是64 张图片、每张图片 3 个颜色通道、每张图片高 32 像素、宽 32 像素。6.3 文本数据文本数据不能直接变成张量需要先做 tokenization再映射为向量。常见的流程是句子拆成 token词或子词每个 token 对应一个 id用词嵌入层把 id 映射为稠密向量。一个批次 32 句话、每句话长度固定为 50 个 token、每个 token 映射成 300 维向量最终输入形状就是(batch_size, sequence_length, hidden_size) (32, 50, 300)注意这里的hidden_size在不同模型里叫法不同在 BERT 系列里常是 768 或 1024在词向量时代常是 100、200 或 300。具体取值取决于模型配置不要死记。理解了这三种形状你再看模型代码时至少能判断某个张量代表的是图像、文本还是表格数据调试时也能更快定位问题。7. 张量操作常见问题与排查思路这一部分是我觉得对初学者最实用的内容。这里整理了几个做项目时几乎一定会遇到的报错和问题。问题现象可能原因排查方式解决方案The size of tensor a must match the size of tensor b两个张量形状不匹配常见于矩阵乘法或拼接操作打印两个张量的 shape确认对齐关系使用reshape、transpose、permute调整形状Expected all tensors to be on the same device一个张量在 CPU另一个在 GPU打印tensor.device确认位置用.to(device)把所有张量放到同一设备Expected object of scalar type Long but got Float数据类型不匹配例如标签应为整数却在传浮点数查看tensor.dtype用.long()或.float()转换类型Trying to backward through the graph a second time在同一个计算图上重复调用backward且没有设置retain_graphTrue检查循环中是否重复反向传播按步重新构造计算图或在需要时设置retain_graphTruea leaf Variable that requires grad is being used in an in-place operation对requires_gradTrue的叶子张量做了原地修改如x 1、x.add_(1)搜索代码中的_结尾方法改用非原地操作例如x x 1使用np.ndarray时形状对但训练效果差数据没有归一化或特征尺度差异过大观察数据的取值范围对特征做标准化或归一化处理排查张量问题时一个通用思路是先打印涉及的张量形状和 dtype再检查设备位置最后才去看具体算法逻辑。大多数张量相关报错都集中在形状、类型、设备这三个维度上而不是算法本身。8. 张量的工程最佳实践除了解决报错更重要的是一开始就避开这些坑。下面这几条建议来自实际项目的常见教训可以帮你减少调试时间。8.1 写代码前先在草稿纸上画形状很多初学者在写模型时不确定某一层的输入输出形状是什么就直接写然后跑再被报错打脸。更稳妥的做法是明确输入数据的原始形状推演每一个操作之后形状如何变化用一行print(x.shape)验证。如果你不确定 reshape 后数据顺序会不会乱先用小数据例如torch.arange(24).reshape(2,3,4)打印出来看看比直接在大数据集上调错要快得多。8.2 先统一 dtype再谈运算在深度学习中模型参数通常是torch.float32你的输入数据也应该尽量是torch.float32。如果输入是float64很多层跑起来都会有问题或者白白增加内存消耗。常见的做法是x x.float() label label.long()其中标签一般用long类型整数输入特征用float类型。8.3 合理使用设备管理在一开始就把设备定义好device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) data data.to(device) label label.to(device)这样后面代码里不需要到处写if cuda的判断。但也要注意不是所有张量都需要放到 GPU 上。例如一些只用于记录日志的标量或列表放在 CPU 上就可以不要做无谓的迁移否则反而会带来额外的传输开销。8.4 注意梯度累积和清零训练循环里常常忘记optimizer.zero_grad()。如果你用了 PyTorchloss.backward()默认会把梯度累加到张量的.grad里。如果不先把梯度清零下一轮的梯度就会和上一轮叠加导致参数更新异常。建议每次反向传播前都执行optimizer.zero_grad() loss.backward() optimizer.step()8.5 避免对叶子张量做原地操作当你对一个张量设置了requires_gradTrue后尽量不要对其使用x.add_(...)、x[0] ...这类原地修改。如果需要新值直接创建新变量。这不仅是为了避免框架报错也是为了让计算图保持可追踪方便调试。9. 总结与下一步学习方向这篇文章从最基础的问题出发把“张量”这个概念讲透了张量本质上是多维数组机器学习中的一切数据——表格、图像、文本——最终都会变成张量张量的形状、变形、广播和自动微分是深度学习代码里的底层能力实际报错大多数来自形状不匹配、dtype 不一致、设备不一致这三类问题。下一步建议你用一个小数据集亲手做一次练习读取一批图片或一段文本打印它的形状尝试用reshape、permute和unsqueeze改变它的结构再写一个最简单的线性模型观察张量在正向传播和反向传播中的变化。这个练习做完你对张量的手感就会完全不一样。如果你想继续深入有两个方向值得探索一是张量在底层的内存布局理解view和reshape的区别需要知道什么是“内存连续”和“stride”二是自动微分的实现原理弄清楚 PyTorch 的计算图是如何记录每个张量运算的。这两个方向都属于“进阶”但有了这篇文章打底你已经具备了理解它们的前提条件。建议收藏本文当你后面遇到张量相关的报错时可以根据第 7 节的表格快速定位。机器学习的学习曲线就是这样概念看似抽象但拆开揉碎之后都是可以逐步掌握的工程知识。