ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

向量矩阵张量:AI计算语法与工程实践详解

向量矩阵张量:AI计算语法与工程实践详解 很多人刚开始学AI以为最难啃的是微积分。可真到写代码的时候会发现最先拦住你的往往是线性代数——向量、矩阵、张量这些看起来像数学课的“抽象概念”其实是每一个模型骨架里的“语法规则”。这篇进阶篇数学篇第6讲就专门把这些计算掰开揉碎向量、矩阵、张量在AI里到底怎么运算、怎么用以及你写模型时踩过的那些形状错误、广播警告根源在哪里。适合已经会用框架搭模型、但数学细节一直似懂非懂的同学也适合准备面试前临时抱佛脚的人。1. 为什么说向量、矩阵、张量是 AI 的“计算语法”1.1 从单个数字到高维容器标量、向量、矩阵、张量一个普通的数字在数学里叫标量在代码里就是一个float或int。它没有任何方向也没有“批量”的概念。但AI模型每次推理都要处理成千上万个样本每个样本又包含几百上千个特征如果还一个数字一个数字地管理模型根本没法写。于是有了向量一串数字排成一行或一列通常表示一个样本的特征。比如某个用户有年龄、收入、活跃度三个数值就可以用一个三维向量[28, 15000, 0.8]表示。矩阵则是二维的像一个表格。它的第一个维度常常是“样本数”第二个维度是“特征数”。一个包含100个用户、每人3个特征的输入就是一个形状为(100, 3)的矩阵。张量的定义更宽泛它是任意维度的数组。三维张量可以表示视频或图像序列四维张量常用来表示一批图片五维、六维也偶有出现。对AI工程师来说张量就是数据的通用容器它的每一维都有一个名字比如batch_size、channel、height、width。搞定了维度命名张量就不可怕了。数据类型维度常见形状例子AI中的典型对象标量0维()或[]损失值、学习率向量1维(3,)单个样本的特征向量、词向量矩阵2维(100, 3)一批样本的特征表、单个线性层的权重张量3维及以上(16, 3, 224, 224)一批图片、注意力分数矩阵、序列批次1.2 “计算语法”到底指什么把数据装进张量只是第一步真正的关键是运算规则。同样是一个三维数组你可以把它当成“一批2D图片”也可以当成“一条序列的隐藏状态”这决定了维度怎么对位、乘法怎么执行。我常把向量、矩阵、张量的运算比作英语的语法单词是数据语法是数据之间的组合规则。你背了再多单词不知道主谓宾顺序也说不出完整句子。深度学习框架里torch.matmul、torch.reshape、torch.transpose这些函数就是语法规则而forward函数本质上就是把这些规则按一定顺序串起来描述数据如何从输入变成输出。而且GPU 芯片内部对矩阵乘法和张量搬运做了大量定制优化模型训练的速度瓶颈往往不在计算本身而在于你写的张量运算是否贴合底层硬件习惯。后面我会专门讲内存布局和广播问题这些看着不起眼却直接影响训练速度和显存占用。2. 向量运算点积、范数和相似度计算到底在算些什么2.1 点积AI里出场率最高的亲密关系度量向量点积的算法很简单对应元素相乘再把所有乘积加起来。写成公式就是a · b a1*b1 a2*b2 ... an*bn。在NumPy里一行代码搞定import numpy as np a np.array([1.0, 2.0, 3.0]) b np.array([2.0, 0.0, 1.0]) dot np.dot(a, b) # 1*2 2*0 3*1 5点积的几何意义是“一个向量在另一个向量方向上的投影长度”乘以“另一个向量的长度”。所以它天然适合度量两个向量的相似度方向越一致点积越大方向垂直时点积等于0方向相反时点积为负。这在AI里太常用了。自注意力机制的分数计算就是Q和K的点积除以维度的平方根推荐系统算用户向量和物品向量的匹配度用的也是点积或余弦相似度文本检索里计算查询向量和文档向量的相关性本质还是点积。可以说大部分模型的“相关性”概念都是用点积定义的。2.2 范数向量长度是正则化和归一化的底层逻辑范数用来衡量向量的大小。最常见的L2范数就是向量的欧几里得长度公式是sqrt(a1^2 a2^2 ... an^2)。L1范数则是绝对值之和。L2范数在训练里最出名的应用是权重衰减也就是给损失函数加上权重矩阵的L2范数平方逼着参数不要长得太大。L1范数则常用来做稀疏化让一部分特征权重变成0相当于自动做特征选择。归一化也是靠范数完成的。比如把向量a除以它的L2范数得到单位向量。两个单位向量再做点积结果就是余弦相似度数值稳定在 -1 到 1 之间。很多向量召回模型在输出层之后都会加一个L2归一化目的就是让点积结果只跟方向有关跟向量长度无关。我自己做相似度模型的时候就踩过这个坑不加L2归一化训练时模型会把所有向量都拉长来提高点积分数推理时结果完全不可控。加上归一化之后训练稳定很多。3. 矩阵运算线性层、批量推理与参数压缩背后的乘法逻辑3.1 矩阵乘法的形状对位先记住这一条规则矩阵乘法C A B要求A的列数必须等于B的行数。结果C的形状是“A的行数 × B的列数”。比如A形状(m, n)B形状(n, p)结果就是(m, p)。这个规则是初学者最容易卡壳的地方。看看这张表A形状B形状是否合法C形状(3, 4)(4, 2)合法(3, 2)(3, 4)(3, 4)不合法无(3, 4)(4,)合法(3,)为什么会这样因为矩阵乘法里A的每一行要和B的每一列做点积所以要求行的长度也就是A的列数和列的高度也就是B的行数一致。从另一种视角看矩阵乘法等于把B看作列向量的集合A B等价于对A的每一行分别与B的所有列向量做点积。这一下子就把矩阵乘法拆回第2章的向量点积了心理负担小很多。3.2 线性层的本质把“一组神经元”同时作用到“一批样本”上全连接层的公式是Y X W.T b。其中X是输入形状一般是(batch_size, input_dim)W是权重形状是(output_dim, input_dim)W.T转置成(input_dim, output_dim)b是偏置形状(output_dim,)。为什么要转置因为W的每一行本来就是一个神经元的权重向量。我们想让X的每个样本和W的每个神经元做点积得到一个(batch_size, output_dim)的矩阵。如果直接X W维度会变成(batch_size, input_dim)完全不是我们要的。转置一下XW.T就变成“每一行是样本每一列是神经元”的相似度矩阵输出里第i行第j列就是第i个样本对第j个神经元的原始激活值。这个操作把两层循环遍历样本、遍历神经元压缩成一次矩阵乘法。我刚开始学的时候很疑惑为什么框架里能直接写nn.Linear(in_features, out_features)底层不就是一个矩阵乘加偏置吗后来才意识到矩阵乘法的价值就是把“大量重复的点积”集中起来并行执行。CPU 和 GPU 底层的 BLAS 库对矩阵乘法做了极致的优化分块、内存缓存、SIMD 指令甚至多核并行。手动写for sample in batch: for neuron in weight:做点积速度连的百分之一都不到。3.3 矩阵运算中的批量与维度扩展模型训练时通常一次喂一个 batch而不是一个样本。假设 batch size 是 64输入特征数是 128输出神经元数是 256那么X是(64, 128)W是(256, 128)输出Z是(64, 256)。一次把 64×256 个点积全部算完这才是真正的“批量计算”。有时候你还会需要把多个权重矩阵堆起来那就需要三维张量。举个例子多头注意力里每个头都有自己的W_q、W_k、W_v头数为8每个头的输入维度是d_k。你当然可以 for 循环8次但更高效的做法是把8个权重矩阵叠成一个形状(8, input_dim, d_k)的三维张量然后一次torch.matmul完成多头的线性变换。这就是张量运算延展矩阵乘法的经典场景。4. 张量运算广播机制、维度变换与内存布局4.1 张量是带批次的矩阵图片、文本和序列的高维真相很多初学者看到四维张量就头晕其实只要记住“前面的维度是批量相关后面的维度是单样本内部结构”就够用了。图像分类中一批 RGB 图片的形状是(batch_size, channels, height, width)比如(16, 3, 224, 224)。16是图片张数3是RGB通道后面是每张图片的高宽。如果你把单张图片(3, 224, 224)当成“一张特殊的矩阵”那(16, 3, 224, 224)就是16个这样的矩阵叠在一起所以叫张量。自然语言处理中一句话经过词嵌入后得到(seq_len, hidden_size)一批句子则是(batch_size, seq_len, hidden_size)。注意力分数矩阵是(batch_size, num_heads, seq_len_q, seq_len_k)简直是一个套娃。但只要你给每个维度起了名字就不会乱。4.2 广播机制没有复制数据却像把数据扩展了一样两个张量做逐元素运算时形状不一定要完全一样。NumPy 和 PyTorch 遵循一套广播规则从右往左对齐维度每个维度要么相等要么其中一个等于1要么一个维度缺失。看这个例子import torch a torch.randn(3, 1) # 形状 (3, 1) b torch.randn(1, 4) # 形状 (1, 4) c a b # 结果形状 (3, 4)这里是(3, 1)和(1, 4)相加广播后相当于a的第1维复制4份b的第0维复制3份最终得到(3, 4)。广播看起来很方便但也埋了很多雷。有一次我调试一个模型发现某个中间结果在 batch size 大于1时出现奇怪的低精度排查半天才发现是形状(batch, 1)的向量和一个(batch, feature)的矩阵相加广播把向量“横向拉伸”到了所有特征维上导致每个样本都加了同样的偏置。这不是错误但完全不是我想要的。所以遇到形状警告或者奇怪的数值第一反应就是检查是不是广播在偷偷“帮倒忙”。4.3 维度变换是改“视图”而不是改“数据”view、reshape、permute、transpose是张量维度操作的四件套但它们的底层逻辑完全不同。permute/transpose只交换维度顺序不改变底层数据的排列顺序。view/reshape则要求两个视图的元素顺序一致如果之前做过transpose底层内存顺序已经不是原来的顺序直接view会报错或者产生错误结果。解决办法是先contiguous()让张量在内存里变得连续再view。我见过很多同学在attention实现里把(batch, heads, seq_len, d_head)转成(batch, seq_len, heads * d_head)时直接view报错然后一脸懵。原因就是之前做过transpose。正确的做法是permute之后.contiguous().view(...)。理解内存布局还有性能意义。GPU 对连续内存的访问效率远高于跳跃式访问。如果你把NCHW的图片张量transpose成NHWC虽然形状看着没问题但每次内存读取都不连续训练速度可能下降百分之二三十。所以尽量让数据从加载到计算都保持同一套布局别在模型中间来回转置。5. 手写一个线性层用 NumPy 复现前向传播和反向梯度5.1 前向传播一行矩阵乘法就够我们用一个小例子把前面所有知识串起来。假设输入X有3个样本每个样本4个特征线性层有2个神经元import numpy as np X np.array([ [1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 10.0, 11.0, 12.0], ]) # 形状 (3, 4) W np.array([ [0.1, 0.2, 0.3, 0.4], [0.5, 0.6, 0.7, 0.8], ]) # 形状 (2, 4)一行是一个神经元 b np.array([0.1, 0.2]) # 形状 (2,) Z X W.T b # (3, 2) print(Z)X W.T的形状是(3, 2)加上b时广播到每一行。这个Z就是两个神经元对三个样本的原始输出。如果后面接一个sigmoid它就是2个神经元的前向激活值。很多人会疑惑为什么不把W定义成(input_dim, output_dim)直接用X W当然也可以但 PyTorch 的nn.Linear官方实现里权重形状是(out_features, in_features)所以需要转置。我这里故意用同样的定义方式就是为了让你在看框架文档时能对上号。5.2 反向传播梯度也是矩阵乘法训练模型时反向传播算梯度本质上还是在做矩阵乘法。对于线性层Z X W.T b如果我们得到了损失对Z的梯度dZ那么权重的梯度就是dW dZ.T X。用刚才的数据试一下dZ np.array([ [1.0, -1.0], [0.5, 0.5], [-0.5, 0.2], ]) # 模拟上游梯度 dW dZ.T X # 形状 (2, 4) print(dW)这里dZ.T形状是(2, 3)X形状是(3, 4)结果恰好是(2, 4)和W形状一致。如果写成X.T dZ也行关键是矩阵乘法要让我们把“每个神经元对每个输入特征的梯度”都算出来。自己用 NumPy 写一遍这个流程比看十遍原理都管用。做完之后你会特别直观地理解为什么 autograd 能自动求梯度它只是在计算图的每个节点上应用了类似的矩阵乘法规则而已。5.3 加上激活函数非线性藏在逐元素操作里线性层串再多矩阵乘法所有层加起来仍然是一个线性变换因为矩阵乘法满足结合律。真正让深度学习变“深”的是激活函数ReLU、Sigmoid、Tanh 等都是逐元素的张量运算。用代码体现一下def relu(tensor): return np.maximum(tensor, 0) A1 relu(X W1.T b1) A2 relu(A1 W2.T b2)这里A1是(3, hidden_dim)的矩阵它同时是下一层的输入。所以张量的形状就是在这样的逐层变换中流转。每个 batch 的数据从(batch, input_dim)变成(batch, hidden_dim)再变成(batch, output_dim)整个模型就是一系列矩阵乘法和逐元素激活函数的组合。6. 我踩过的张量形状坑维度错位、广播误用和性能瓶颈6.1 形状错误“忘记转置”是头号坑我自己刚写 Transformer 时卡得最久的就是注意力分数计算。Q和K的形状都是(batch, heads, seq_len, head_dim)按理说要对最后两个维度做点积Q K.transpose(-2, -1)结果是(batch, heads, seq_len, seq_len)同时转置的位置还关系到K要不要先contiguous。有次我把K转置后直接view报错说“view size is not compatible with input size”。后来才意识到transpose会让内存布局变成非连续必须先.contiguous()。这个坑几乎人人都会踩一次解决办法就是记住transpose和permute之后如果要改变形状或者接入某些要求连续内存的操作先加contiguous()。6.2 广播误用相似度矩阵被悄悄“降维”另一个经典问题是计算两个矩阵两两之间的相似度。假设A是(m, d)B是(n, d)你想要(m, n)的相似度矩阵。最自然的写法是两层循环for i in range(m): for j in range(n): sim[i, j] np.dot(A[i], B[j])但这样既冗长又慢。高效写法是sim A B.T # 形状 (m, n)直接把所有点积一次算完。这个例子说明很多“看起来要循环”的场景其实都能用矩阵乘法表达。学会用替代显式循环不仅代码整洁速度优势尤其明显。在一组 512 个 query 和 512 个 key 上对比测试循环要几毫秒矩阵乘法可以到微秒级而且批量越大差距越明显。6.3 精度和内存隐藏的性能杀手我遇过一种情况模型在 PyTorch 上是float32但中间为了调试把数据转成 NumPy某些操作默认变成了float64再转回 Tensor 时显存直接翻倍。在batch size很大的场景下这一下就可能让 OOM 出现。解决办法是时刻关注dtype。NumPy 创建数组时默认float64如果你只是做数值验证没关系但如果你在模型训练循环里做自定义数据处理记得显式指定dtypenp.float32或者用torch.float32。另外GPU 上减少不必要的张量复制也很重要。大多数框架的view和reshape不会复制数据但contiguous()在某些情况下会触发新拷贝频繁调用会拖慢整体速度。我在调优时常用的一个原则是从一个张量到另一个张量尽量只改变每个维度的含义而不是反复交换维度顺序。如果非要换就把所有转置集中到数据预处理阶段不要在模型主循环里做。最后再补充一个很多人忽略的点不要迷信einsum。它确实能让复杂的高维矩阵乘法写得很简洁比如注意力里的torch.einsum(bqhd,bkhd-bhqk, Q, K)。但einsum的缺点是报错信息非常隐晦内部还可能走通用路径性能未必优于显式的加transpose。我的习惯是先用显式矩阵乘法调通功能确认需求后再考虑用einsum简化代码。这样踩的坑最少效率也最高。机器学习这条路走到越深越会发现数学不是“纸上公式”而是嵌在每一行代码里的思维框架。向量、矩阵、张量的运算规则就是AI的底层语法搞懂了它们你看模型源码会顺畅很多排查 bug 也不用靠瞎猜。希望这篇“计算语法”笔记能帮你把线性代数和实际模型的最后一段缝隙也补上。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表