尧图精选

标量到张量:深度学习中的维度与矩阵运算实战

🕒 发布时间:2026/9/9 3:06:31 📁 来源:尧图网络
写这个系列第三篇的时候我正好在带一个新来的实习生调一个分类模型的训练脚本。他卡在一个看起来很基础的地方loss.backward()一直报维度不匹配查了半天发现是自己在构造标签的时候把[batch]的张量写成了[batch, 1]然后广播机制又把整个事情变得不可预测。事后我跟他聊了很久发现他对标量、向量、矩阵、张量这四者的运算规则其实没有一个系统的框架全靠报错驱动学习。这恰好也是我想在这一篇里集中解决的问题。前两篇我们聊了标量、向量、矩阵的基本定义和它们之间的区别这篇我打算把重心放到“运算”和“工程落地”上为什么向量的点积结果是标量矩阵乘法为什么是这样定义的在实际写深度学习代码时张量的维度变化到底该怎么想才不会乱同时结合几个常见的实际应用场景——混淆矩阵、向量数据库、Embedding 模型——把这些数学概念从课本里拽出来落到你真正会写的那几行代码上。1. 从标量到张量维度到底是怎么一步步长出来的1.1 先给“维度”一个不啰嗦的框架很多人学到这里就乱了根源在于把“维度”这个词在不同语境下混着用。在数学和深度学习的框架里一个数据有几个“轴”它就说是几维的这个维度不是你感知上的空间维度而是“索引一个数据元素需要几个坐标”。标量是 0 维的就是一个单独的数比如温度 36.5你不需要任何坐标就能拿到这个值。向量是 1 维的比如班级里五个人身高组成的数串你只需要一个下标就能指出第几个人的身高。矩阵是 2 维的比如五个人三科成绩的成绩表你需要“第几个人”和“第几科”两个坐标才能锁定一个分数。张量就是把这个思路继续往外推3 维、4 维、5 维……每一维就是一个“索引轴”。我给学生讲的时候特别喜欢用快递柜打比方。标量就是你手上的一件快递向量是一排柜子你报一个柜号就能取。矩阵是一个货架你需要“第几排第几列”才能找到。张量就是整个仓库可能需要“几号楼、几层、几排、几列”才能精确定位。深度学习里的数据基本上都是“整个仓库”级别的。1.2 深度学习为什么一定要用张量如果你训练过图像模型就一定见过这种形状[batch_size, channels, height, width]这就是一个标准的 4 维张量。batch_size是一次同时喂进去多少张图channels是颜色通道RGB 就是 3灰度就是 1后面两个是图片的高和宽。你很难用矩阵去表示这种数据因为矩阵只有两个轴装不下批量和通道这两个信息。张量就是为此而生的。自然语言处理里更典型一个 batch 的文本经过 Embedding 之后通常是[batch_size, seq_len, hidden_size]。seq_len是句子长度hidden_size是每个词映射到的向量维度。这三个轴分别表示“哪一句话”“句子里的第几个词”“这个词用多少维的向量表示”。你要是只学过矩阵面对这种 3 维数据大概率会懵因为你不知道哪两个轴可以合并、怎么合并才不会丢信息。这也是为什么现在的深度学习框架把所有数据都叫 Tensor。PyTorch 里一切的输入输出都是torch.TensorNumPy 里叫ndarray本质都是多维数组。理解了张量就是“带任意多个轴的数组容器”后面所有的shape、reshape、transpose才有讨论的落脚点。1.3 轴顺序的约定是个大坑张量有了多个轴之后轴的先后顺序就不是小事了。同一个数据用[N, C, H, W]和[N, H, W, C]存计算机内存里的排布逻辑是完全不同的。图片的像素点在内存中按行存储如果你想用[N, H, W, C]的顺序每个像素的 RGB 三个值是连续排在一起的但如果你用[N, C, H, W]那就是所有 R 通道先连成一片然后是所有 G 通道最后才是 B 通道。PyTorch 默认用[N, C, H, W]TensorFlow 早期默认用[N, H, W, C]这导致同一个模型在两套框架之间迁移时经常需要permute或者transpose来调整轴顺序。我自己就曾经把一个用 PyTorch 训练好的图像预处理流程直接搬到一个 TensorFlow 推理服务里结果颜色通道对不上模型输出的准确率掉了一大截。原因就是通道轴位置变了模型看到的“红绿蓝”和训练时不一致。这种问题报错信息很弱甚至不报错它只会在效果上折磨你。所以在写代码之前一定要先确认你手里的数据是什么 shape目标 API 期望什么 shape然后显式地做转换。不要依赖框架给你偷偷调整因为它们大多数时候不会调。2. 向量乘法深度拆解为什么向量的乘积会是标量2.1 点积的两种定义视角向量点积是深度学习里出现频率最高的运算之一公式看起来很简单两个等长向量对应位置元素相乘再求和。例如[1, 2, 3]和[4, 5, 6]的点积就是1×4 2×5 3×6 32。结果是一个数也就是标量所以很多人会直接问“为什么向量的乘积是标量”。严格说不是所有向量乘积都是标量。两个向量之间至少有两种乘法点积产出标量叉积产出向量。点积之所以叫“点积”因为它结果的维度比原来少了一维叉积在三维空间中产出一个垂直于原来两个向量张成平面的新向量它还保留着方向。从几何上看点积的另一种定义是a · b |a||b|cosθ其中 θ 是两个向量的夹角。这个定义更能解释为什么结果是标量它刻画的是“一个向量在另一个向量方向上的投影长度”乘上“另一个向量的长度”。投影是一个数长度是一个数乘起来当然还是一个数。夹角为 0 时两个向量方向完全一致点积取最大值夹角为 90 度时方向垂直点积为 0。这个“点积为 0 意味着垂直”的性质在机器学习里特别常用比如正交基、去相关、注意力分数计算。2.2 神经网络里的点积到底在算什么深度学习里最密集使用点积的地方就是线性层。一个没有偏置的线性变换可以写成y W^T x这里的乘法的核心就是逐行计算权重和输入的点积。全连接层的每个输出神经元做的事情可以理解为把输入向量和该神经元对应的权重向量做点积得到一个标量然后可选地加偏置、过激活函数。Transformer 里的自注意力机制更是把点积用到了极致。Q和K的每一行都是向量用Q·K^T计算注意力分数本质上就是在算每一对 token 的向量点积。点积大说明方向相近也就是语义上更相关点积小则说明不太相关。你想一下这跟余弦相似度本质上是同一件事只是差一个归一化常数。理解到这一层你就不会再把注意力机制当成什么神秘的魔法了。关于范数也值得多说一句因为热词里反复出现“向量范数”。范数就是衡量向量“长度”的一种方式。L1 范数是各元素绝对值之和L2 范数是各元素平方和再开根号。L2 范数在正则化里极其常见权重衰减惩罚的就是模型参数的 L2 范数平方。L1 范数则因为它在零点不可导的特殊性质能带来稀疏解常常用于特征选择。为什么同一个“长度”有不同的算法因为你要的“长度”语义不同。L2 强调整体大小L1 强调分量总和这就好比衡量个人的“工作量”你可以用总时长也可以用任务件数两者都不是错的只看你要哪个维度。2.3 一个特别容易踩的数学坑维度匹配点积要求两个向量长度相等。在代码里这意味着[3]和[3]可以直接dot但[3]和[4]就会报错。很多人刚开始写注意力代码时经常遇到query向量的维度是[batch, seq, head_dim]key的维度是[batch, seq, head_dim]看起来一样但中间多了两层矩阵乘法后某一个轴的顺序悄悄换了结果怎么都对不上。我的建议是在关键矩阵乘法前后各加一条打印shape的语句把每一维都打出来。笔试考试你可以靠心算维度但真正调模型的现场打印是最快的定位方式。等代码稳定后再把打印删掉或者放到一个DEBUG开关后面不要觉得打印很丢人调试是深度学习从业者的基本生存技能。3. 矩阵乘法不要死记规则要理解“变换的复合”3.1 矩阵乘法那条古怪规则的由来刚开始学矩阵乘法的时候几乎所有教材都会教“左行右列”法结果矩阵第 i 行第 j 列的元素等于左边矩阵第 i 行和右边矩阵第 j 列的点积。很多人背下了这个规则但完全不知道为什么。这里我建议换个角度矩阵不是数字表格矩阵是“一种动作”。如果你把矩阵理解为对向量的操作那么Ax的意思就是“把向量 x 通过矩阵 A 做一个线性变换”。两个矩阵相乘AB的含义就变成了“先做 B 变换再做 A 变换”。这跟函数复合f(g(x))是一个道理。这个视角解释了矩阵乘法为什么不像逐元素乘法那样直观也解释了为什么AB通常不等于BA你先旋转再缩放和先缩放再旋转结果很可能不一样。这就好比你先穿袜子再穿鞋和先穿鞋再穿袜子是完全不同的体验。矩阵乘法的顺序本身就是变换的顺序不能随意交换这是理解整个线性代数的钥匙。具备这个视角之后你再看卷积神经网络里的卷积核、全连接层的权重矩阵、循环神经网络里的状态转移矩阵你看到的就不再是一堆数字而是一系列叠加在数据上的动作。3.2 几类特殊矩阵在深度学习里的真正位置热词里提到了对称矩阵、初等矩阵、正交矩阵、对角矩阵这些在深度学习里不是考知识点而是真实工具。对称矩阵满足A^T A它的特征值都是实数、特征向量可以取正交。协方差矩阵就是对称矩阵PCA 主成分分析本质上就是在对协方差矩阵做特征分解。你在做数据白化、特征去相关时看到它不要觉得只是一个数学概念。对角矩阵除了对角线上有值其他位置全为 0它做的事情是“把每个分量独立缩放”。批量归一化层在推理时对每个通道的缩放如果忽略平移项本质上就是一个对角变换。因为对角矩阵不涉及不同维度的混合它是最简单、最容易被人类解释的一类线性变换。正交矩阵满足A^T A I它保持向量的长度和夹角不变所以它是旋转的矩阵表示。在深度学习中权重初始化时常常用到正交初始化就是希望初始的线性变换不至于一开始就扭曲信息的距离结构。正交矩阵不会放大梯度这对深层网络的训练非常友好。初等矩阵则是通过对单位矩阵做一次初等行变换得到这类矩阵是高斯消元的基石。虽然直接写模型的人很少会去构造初等矩阵但理解它有助于你理解矩阵求逆、行列式和线性方程组的求解逻辑。很多调试场景里你需要手推一个小例子去验证算法这时初等变换的手感都是很重要的基本功。3.3 线性层就是矩阵乘法不要神化它深度学习里最常见的nn.Linear(in_features, out_features)本质上做的是y xW^T b。这里W的形状是[out_features, in_features]x的形状是[batch, in_features]。矩阵乘法之后你最后得到的形状是[batch, out_features]。为什么 W 要转置这是 PyTorch 的约定nn.Linear内部存的是[out_features, in_features]形状的权重计算时对输入做x W.T原因在于它想让你定义的每个神经元对应一行权重这样写代码的时候直觉比较顺。我见过不少新手在这个地方卡壳明明定义好了模型forward 里一算就是维度爆炸。如果你理解了矩阵乘法维度匹配原则——[m, n]乘以[n, p]得到[m, p]——你会发现所有线性层的 shape 推算都可以在 30 秒内手算完成。写代码前花半分钟把权重矩阵的 shape 列出来远比在 GPU 上报错后瞎猜高效得多。先算后写永远是矩阵相关代码的第一准则。4. 深度学习框架里的张量实操shape 世界的生存法则4.1 reshape、view、transpose、permute 的区别这一节是给准备写 PyTorch 代码的人看的但理解这些思想换到 TensorFlow、JAX 同样适用。reshape和view都用来改变张量形状但不完全一样。view要求在内存中数据是连续存储的因为它只是改变了“如何解读”这段内存不搬运数据。reshape更宽容如果数据不连续它会自动在背后拷贝出一份连续的数据再变形。所以如果你只是调整维度优先用view如果要从transpose的结果继续变形或者做了切片之后要变形直接用reshape更安全。强行对不连续张量用view会报错这类错误信息看多了你就知道什么叫“内存布局决定你能做什么”。transpose是交换两个轴permute是任意重排所有轴。比如一个[batch, seq, hidden]的张量要变成[batch, hidden, seq]可以用transpose(1, 2)也可以permute(0, 2, 1)。后者更通用。要注意的是transpose 之后张量通常变成非连续内存再想 view 就会出问题必须先contiguous()这一点几乎踩遍每一个新手。4.2 广播机制把维度自动“补齐”的魔法与陷阱NumPy 和 PyTorch 都有广播机制它允许不同形状的张量做逐元素运算时自动扩展。基本原则是从最后一个轴开始比较如果两个维度相等或者其中一个为 1或者其中一个缺失那么这个维度就可以广播否则报错。举例来说[3, 1]和[1, 4]相加结果是[3, 4]。第一个张量在第二个维度上相当于被复制了 4 次第二个张量在第一个维度上被复制了 3 次。这是非常高效的操作因为你并没有真的创建那一堆重复数据内存里只保留原始数据。但广播机制也是隐性 bug 的重灾区。回到开头那个实习生的例子标签[batch, 1]和模型的输出[batch, num_classes]计算损失时可能会因为在某个维度上自动广播而产生一个你以为正确、但含义完全错误的结果。这种错误通常不报错只在验证集指标上露出马脚。所以我一般建议所有跟逐元素运算相关的张量尽量在构造时就显式给出完整 shape不要依赖广播去“顺手”补维度如果确实依赖广播务必在关键节点打印结果 shape 确认。4.3 从零开始手推一个张量的维度变化我拿一个常见的文本分类模型来举例帮你建立“手推维度”的习惯。输入是一个 batch 的句子先转成 token id形状是[batch32, seq_len128]。经过nn.Embedding(vocab_size, hidden_size768)之后变成[32, 128, 768]。如果你后面接的是 LSTMLSTM 默认接收[seq_len, batch, hidden]的输入所以你需要把前两个轴交换一下也就是x.transpose(0, 1)得到[128, 32, 768]。LSTM 的输出还是[seq_len, batch, hidden]你再把它transpose(0, 1)回到[32, 128, 768]然后取最后一个时间步的输出或者做全局池化得到一个[32, 768]的向量。最后接一个nn.Linear(768, num_classes10)输出[32, 10]跟标签[32]做交叉熵。整个链条里每个节点你都能用笔写出 shape那么任何一步报错你都能迅速定位。不夸张地说深度学习的代码调试有一半的功夫是在调 shape。谁能心算出每一步的 shape谁就能在报错面前保持冷静而不是瞎试。5. 数学工具的真实战场混淆矩阵、向量数据库与 Embedding5.1 混淆矩阵分错与分对全写在一张表里混淆矩阵是整个深度学习评估体系里的一个经典矩阵应用尤其多分类任务几乎天天用。它的大小是[类别数, 类别数]第 i 行第 j 列表示“真实类别是 i但模型预测成 j”的样本数量。对角线上的值就是预测正确的数量非对角线都是错误。用 PyTorch 或 NumPy 自己写混淆矩阵非常快核心思路就是按真实标签和预测标签两两配对计数。如果用了 sklearn一行confusion_matrix(y_true, y_pred)就出来了。但如果你理解了它的定义即便没有 sklearn 也能在 10 行以内实现这对理解 precision、recall、F1 这些指标特别有帮助。precision 是从预测角度看的预测为正例的样本里有多少是真正的正例recall 是从真实角度看的真实的正例样本里有多少被找了出来。这两者常常互斥所以有了 F1 这个调和平均。在一个类别极不平衡的数据集里只看 accuracy 会被“全预测成多数类”的假象欺骗但混淆矩阵可以让你一眼看到少数类被分成了什么样。我看到很多新手在评估模型时只看一个 loss 和一个准确率这远远不够。至少打一次混淆矩阵把所有类别的分布都看清楚再决定下一步是加数据还是调阈值。5.2 向量数据库把点积和余弦相似度变成工程基础设施热词里“向量数据库”出现频率非常高这里我要多说几句因为它可能是“向量”这个概念离工程场景最近的一次。Embedding 模型会把一句话、一张图、一个实体编码成一个向量。这些向量通常在几百到几千维之间可以理解成实体在高维空间里的坐标。语义相近的内容它们向量的夹角小、点积大、余弦相似度高语义无关的内容夹角大、相似度低。向量数据库做的事情就是维护海量实体向量并提供“给定一个查询向量快速找出与其最近的前 K 个向量”的能力。这里面的“近”可以是欧氏距离、点积、余弦相似度具体用哪一个由你选择的索引类型决定。比如 Faiss 里的IndexFlatIP用内积IndexFlatL2用欧氏距离。Milvus 和 pgvector 这类系统则是把这个能力交付成可水平扩展的数据库服务。回答一个被反复问的问题AI 智能体的企业知识库是不是存放在向量数据库里答案是知识库的原文通常还是存在传统数据库或对象存储里但知识的“语义索引”存在向量数据库里。流程是把文档切成 chunk每个 chunk 用 embedding 模型转成向量存入向量数据库用户提问时同样把问题转成向量去向量库里检索最相关的 chunk再把检索结果组装成上下文交给大模型生成回答。没有向量数据库这个按语义检索的过程就得写暴力线性扫描数据量一上来就慢得没法用。5.3 本地部署向量模型一个极简的实操参考热词里有人问“mac 怎么本地部署向量模型”这可能是指在本机跑一个 embedding 模型。以text2vec-large-chinese或者BAAI/bge-large-zh-v1.5为例如果你有 GPU 或一个足够大的内存用sentence-transformers库加载模型其实非常简单。先pip install sentence-transformers然后from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-large-zh-v1.5) sentence 我今天去了西湖边散步 embedding model.encode(sentence) print(embedding.shape)输出通常是[1024]或者[768]这样的向量。你把一批句子的向量都算出来然后用 Faiss 建索引、查询这就是一个最简的本地语义检索服务了。Mac 上跑这类模型完全可行只是速度取决于你的硬件CPU 推理不会特别快但做演示和小规模验证没有压力。这里有个实战提醒不同 embedding 模型输出的向量维度可能不一样在使用向量数据库时同一个集合里的向量必须来自同一个模型否则算出来的相似度没有任何意义。另外很多模型在encode前会自带文本预处理比如加 prompt、处理最大长度不同版本的库行为可能略有不同上线前一定要用几个已知语义关系的句子做一遍冒烟测试。6. 实操避坑维度错误、广播陷阱与调试经验6.1 高频报错与排查速查表深度学习框架的报错信息虽然各不相同但高发问题其实就那么几类。我把这几年遇到最多的整理成一张表方便你遇到问题时快速对照。报错现象常见原因解决思路Matrix multiplication shape mismatch矩阵乘法内维不相等打印两个矩阵的 shape按[m,n]×[n,p]规则检查Expected 2D tensor but got 3D把 3 维张量直接传给只接受 2 维的层先 reshape 或 permute把 batch 维和其他维合并view size not compatible with tensor size变形后元素总数不一致或对非连续张量用 view检查元素总数必要时先 contiguous()Broadcast shape mismatch两个张量某些维度既不相等也不为 1打印两个 shape手动对齐最后一个维度看CUDA error: device-side assert triggered标签值超出类别范围检查标签是否从 0 开始、是否包含 -1 等非法值RuntimeError: expected scalar type Float but found Long数值类型不一致对张量调用.float()或检查数据加载时是否转成了对应 dtype这张表并不能覆盖所有情况但它能帮你节省大量时间。我自己的习惯是看到一个报错不要马上改代码先花一分钟重复“打印 shape、打印 dtype、打印数值范围”这三部曲。绝大多数维度类错误在打印后几秒钟内就能定位。6.2 一个真实案例手写多头注意力时的维度混乱我去年给团队写一个轻量级 Transformer 模块自己也在多头注意力上踩过一次很低级的坑写出来给大家当反面教材。我当时定义num_heads8head_dim64输入x的形状是[batch16, seq50, hidden512]。我需要把输入拆成 8 个头每个头拿[16, 50, 64]。直观的想法是x.view(16, 50, 8, 64)这一步没问题。但接着我想把头的维度放到前面写了x.permute(0, 2, 1, 3)此时形状是[16, 8, 50, 64]也正确。然后我为了把 batch 和 head 合并起来计算注意力直接用了view(16 * 8, 50, 64)。这里就出事了。因为permute之后内存已经不连续直接view报错。我当时很自信地觉得“数据形状一样view 应该没问题”被报错提醒后才想起contiguous()的事。改成x.permute(0, 2, 1, 3).contiguous().view(16 * 8, 50, 64)才通过。这个错误很小但它说明了一个道理写代码时不能只看逻辑形状还要留意内存布局。而且这种错误在新手身上几乎每天都会发生因为它不涉及高深数学纯粹是框架底层机制的问题。建议你使用view前养成一个条件反射如果这个张量刚做过 transpose、permute、切片、拼接这类操作先.contiguous()或者直接用reshape。6.3 数值稳定性与矩阵运算溢出还有一个容易被忽略的问题是数值溢出。深度学习里很多损失函数和激活函数都涉及指数运算比如 softmax、交叉熵、注意力分数缩放。如果分数值本身很大exp很容易溢出成inf导致 loss 变成 NaN。解决方法通常是“减去最大值”这个经典技巧softmax(x) exp(x - max(x)) / sum(exp(x - max(x)))。数学上这个变换不改变结果但数值上能把指数函数的输入范围拉到(-inf, 0]从而避免溢出。PyTorch 的CrossEntropyLoss内部已经做了这类稳定化处理所以大多数情况下你不用担心但如果你自己实现损失函数或注意力模块就一定要记得这件事。矩阵乘法本身也有累积误差问题。在训练初期数值量级正常时问题不大但是当模型设计不合理导致中间激活值过大矩阵乘法的结果就会膨胀随后的归一化层或者 softmax 就可能因为巨大的输入而饱和或溢出。遇到这种情况优先检查网络中间层的输出数值分布是否合理而不是急着调学习率。一个简单的做法是在 forward 的若干关键节点插入tensor.abs().mean()的打印看看量级是不是在以指数级放大。这种排查方式看起来很土但在很多实际问题里比任何花哨工具都管用。6.4 写代码前先手推维度一份个人小习惯说了这么多避坑经验最后分享一个我一直坚持的习惯凡是涉及矩阵乘法和张量变形的核心模块写代码之前先在注释里写下 shape 变换链。比如我写一个 self-attention 的 forward开头大概是这样的# x: [batch, seq_len, hidden] # q, k, v: [batch, seq_len, hidden] - view - [batch, seq_len, heads, head_dim] # - permute - [batch, heads, seq_len, head_dim] # scores: q k.T - [batch, heads, seq_len, seq_len] # attn: softmax(scores / sqrt(head_dim)) # out: attn v - [batch, heads, seq_len, head_dim] # - permute - [batch, seq_len, heads, head_dim] # - view - [batch, seq_len, hidden]这几行注释看起来不起眼但它能让你在写代码的时候保持清醒每一步都清楚自己在做什么。很多新手写代码是“边写边猜”遇到报错再回头修这个模式在简单项目里能撑过去但一旦模块复杂起来猜的成本会高到可怕。手推维度不是竞赛技巧就是一种工程素养。真正到了大模型时代输入动辄几百亿参数任何一点 shape 理解不到位都会被放大成灾难性的训练失败。从最简单的标量、向量、矩阵、张量开始把这些基本功打扎实才是所有后续进阶最可靠的底座。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →