
我最早学机器学习那阵心里一直有个疙瘩线性代数是考完就忘了可一到真正写模型、调参、推公式的时候矩阵又无处不在。直到我老老实实用TensorFlow 2把一套线性回归从零实现了一遍才彻底想通——机器学习的一次前向传播、一次梯度更新本质上就是一次次矩阵运算。而像PCA降维、推荐系统、岭回归解析解这些经典操作背后全是线性代数里那些耳熟能详的概念只不过在TensorFlow 2里被封装成了几行干净的API。这篇文章我打算从实操角度把TensorFlow 2的线性代数能力完整过一遍从张量创建、矩阵乘法、求逆到特征值分解、奇异值分解、求解线性方程组再到梯度计算和einsum进阶玩法。核心代码都会给出你可以直接复制到自己的环境里跑通。本文适合正在学机器学习的朋友也适合已经在用TensorFlow写模型但想加深底层理解的开发者如果你正在准备期末复习或项目实践这里的公式到代码的对照也能帮上不少忙。我的运行环境是Python 3.10 TensorFlow 2.13 CPU版代码在GPU环境下完全兼容。1. 先搞清楚TensorFlow 2里的“线性代数”到底指什么1.1 机器学习与线性代数不只是计算公式很多人把线性代数理解为“计算课”这其实低估了它。在机器学习里线性代数是整套系统的基本表达语言。一个最简单的神经网络隐藏层计算过程就是h relu(W x b)其中W是权重矩阵x是输入向量b是偏置向量。你看前向传播的每一步都是矩阵-向量乘法多层网络本质上是矩阵变换的复合。再往后看反向传播计算梯度时链式法则本质上是一连串雅可比矩阵的连乘线性回归有解析解θ (XᵀX)⁻¹Xᵀy这里面同时用到了转置、矩阵乘法、求逆三样东西PCA降维要对协方差矩阵做特征值分解推荐系统里的矩阵分解也是靠SVD这类工具。所以我不是吓唬你线性代数没吃透机器学习学起来确实像盖楼没打地基。我还想强调一点学线性代数不是为了考试时手算三阶行列式而是为了看懂模型、排查问题、优化性能。比如你训练一个模型发现loss不降先检查特征矩阵是不是存在严重共线性你写自定义层时维度对不上脑子里有矩阵形状的敏感度很快就能定位问题。这就是我把话放在前头的原因理解矩阵运算背后的几何意义比记住某条公式更重要。1.2 tf.linalg一个被低估的宝藏模块TensorFlow 2把大量线性代数运算集中在了tf.linalg模块里。别小看这个模块它是除了keras.layers之外我觉得最值回票价的API集合。常用的包括tf.linalg.inv求逆、tf.linalg.pinv求伪逆、tf.linalg.det行列式、tf.linalg.eigh特征值分解、tf.linalg.svd奇异值分解、tf.linalg.solve解线性方程组、tf.linalg.lstsq最小二乘、tf.linalg.norm范数、tf.linalg.trace迹以及可以统一很多运算的tf.einsum。和NumPy的numpy.linalg对比最大的区别在于三点第一输出的都是Tensor对象可以无缝接入模型训练流程第二底层能跑在GPU/TPU上对大矩阵的运算速度远超CPU第三整个计算图支持自动微分你可以对一个包含矩阵分解操作的函数求梯度这在传统线性代数工具里是做不到的。当然如果只是做原型验证用NumPy也很顺手但一旦要放到训练循环里或者要对某个自定义层里的矩阵运算求导就得用TensorFlow了。我自己的习惯是数据处理和快速验证用NumPy模型内部的线性代数运算一律用tf.linalg。两者配合起来既保证了开发效率又兼顾了性能和可微性。下面就开始进入实战环节。2. TensorFlow 2线性代数核心操作实战2.1 创建张量从一维到多维在TensorFlow里张量Tensor就是多维数组。标量是0维向量是1维矩阵是2维再往上是批量张量或多维特征张量。你可以把它想象成一套快递盒0维是一个没有标签的球1维是一排球2维是一张球组成的网格3维就是一沓这样的网格摞在一起。维度越高嵌套越深但每个元素依然是数字。创建张量的方式很灵活。最常用的是tf.constant和tf.Variable前者是常量不可变后者是可训练变量会自动累积梯度。举例import tensorflow as tf # 向量和矩阵 vec_a tf.constant([1.0, 2.0, 3.0]) mat_a tf.constant([[1.0, 2.0], [3.0, 4.0]], dtypetf.float32) # 全零、全一、单位矩阵 zeros_mat tf.zeros([3, 3]) ones_vec tf.ones([4]) identity tf.eye(3, dtypetf.int32) # 随机张量常用作权重初始化 random_mat tf.random.normal(shape[2, 4], mean0.0, stddev0.1) # 可训练变量 w tf.Variable(tf.random.normal([3, 1], stddev0.01))这里有个新手容易忽略的点dtype。TensorFlow里的线性代数运算对dtype很敏感float32和float64混用会直接报错。默认情况下tf.constant([1, 2, 3])是int32但矩阵求逆、特征分解这类运算要求浮点类型所以我在创建矩阵时会习惯性地加上dtypetf.float32或tf.float64。还有一点tf.Variable和tf.Tensor完全不同。变量可以被赋值更新常量不行。在很多教程代码里tf.Variable被用来存放模型权重而线性代数推导时用tf.constant更安全因为不会有意外修改的风险。调试阶段我经常用print(x.shape)和print(x.dtype)来确认每一步的输入都符合预期。需要理解矩阵的基本印象一个[m, n]的矩阵是m行n列。mat_a.shape返回(2, 2)表示2行2列。后续所有维度错误排查都依赖你到底有没有搞清楚自己手里是个什么形状。2.2 矩阵乘法前向传播的地基矩阵乘法是整个深度学习里出现频率最高的运算没有之一。TensorFlow里用tf.matmul(a, b)也可以直接写成a b后者更简洁。矩阵乘法的维度规则是A的形状是(m, k)B的形状是(k, n)结果形状是(m, n)。内层维度必须相等也就是A的列数必须等于B的行数。用生活场景类比就像流水线上两道工序第一道工序有k种零件第二道工序刚好接收这k种零件才能继续往下走。a tf.constant([[1.0, 2.0], [3.0, 4.0]]) # (2, 2) b tf.constant([[5.0, 6.0], [7.0, 8.0]]) # (2, 2) c a b # (2, 2) print(c.numpy()) # [[19. 22.] # [43. 50.]]这里要特别注意a * b是逐元素相乘Hadamard积a b才是矩阵乘法。刚上手时特别容易搞混尤其是从NumPy转过来的同学因为NumPy里*对二维数组也是逐元素乘。神经网络里的前向传播y W x b用的就是矩阵乘法如果误用了逐元素乘整个模型就算废了。批量矩阵乘法也经常用到。比如一批数据X的形状是(batch_size, n_features)权重矩阵W是(n_features, n_classes)直接X W得到(batch_size, n_classes)这就是标准批量前向传播。如果是三维以上的张量比如注意力机制里的Q、K、V矩阵形状是(batch_size, heads, seq_len, dim)你也可以直接q k只要最后两维满足维度匹配规则前面的批量维度会自动对齐。我在实际项目中踩过的坑是写自定义层时某个矩阵需要先做转置再乘结果忘了转置直接a b维度倒是能对上但计算完全错误。所以每次写矩阵乘法前我都会在草稿纸上写出两个形状或者干脆加一行assert a.shape[-1] b.shape[-2]能节省大量排查时间。2.3 转置、求逆与伪逆让矩阵“可解”转置可能是我用得最多的操作之一。tf.transpose(a)会把矩阵的维度对调对二维矩阵来说就是行列互换。如果你处理的是向量一维向量转置后看起来没变化因为它只有一维真正需要转置的通常是二维矩阵或更多维张量。还有一个常用的perm参数可以指定维度交换顺序比如把(batch, seq, dim)换成(batch, dim, seq)这在注意力机制里非常常见。求逆是线性代数的经典操作。tf.linalg.inv直接给出方阵的逆矩阵。但我要提醒你工程上直接求逆不是最优解尤其是矩阵规模较大或者接近奇异时求逆既慢又不稳定。最典型的例子是线性回归的解析解θ (XᵀX)⁻¹Xᵀy教科书会教这个公式但实际工程里更推荐用tf.linalg.solve或tf.linalg.lstsq来解。原因在于求逆的计算复杂度是O(n³)而且数值误差会被放大。如果矩阵是奇异矩阵行列式为0、不可逆或者根本不是方阵就需要用到伪逆tf.linalg.pinv也就是摩尔-彭罗斯广义逆。伪逆在最小二乘问题、欠定方程组里非常有用。比如你有一个(m, n)的矩阵m n方程数小于未知数正规的逆矩阵不存在但pinv能给出一个最小范数解。A tf.constant([[2.0, 0.0], [0.0, 4.0]]) inv_A tf.linalg.inv(A) print(inv_A.numpy()) # [[0.5 0. ] # [0. 0.25]] # 伪逆用在长方形矩阵上 B tf.constant([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]) # (3, 2) pinv_B tf.linalg.pinv(B) print(pinv_B.shape) # (2, 3)理解求逆和伪逆的区别最简单的方式是看解方程的角度。一个可逆方阵表示方程组有唯一解求逆相当于一口气算出所有变量而超定方程组样本多于特征通常没有精确解只能求最小二乘意义下的最优解这正是伪逆的用武之地。3. 特征值分解和奇异值分解的实践应用3.1 特征值分解从PCA到矩阵对角化特征值和特征向量的定义很简短对矩阵A如果存在非零向量v和标量λ使得A v λ v那么v是特征向量λ是特征值。几何意义就是矩阵A作用于向量v后只会让v伸缩而不会改变方向。你可以想象有一根弹簧顺着它的轴线拉方向不变只是长度变了特征向量就是那根轴线方向特征值就是拉长或压缩的比例。深度学习里最常见的特征值分解应用是PCA降维。PCA的本质是找到一个正交变换把数据映射到方差最大的方向上。具体步骤是先对数据做中心化计算协方差矩阵C 然后对C做特征值分解取特征值最大的前k个特征向量作为主方向。在TensorFlow 2里这一步非常简洁# 假设 X 是 (n_samples, n_features)先中心化 X tf.random.normal([100, 5]) X_mean tf.reduce_mean(X, axis0) X_centered X - X_mean # 协方差矩阵 C tf.matmul(X_centered, X_centered, transpose_aTrue) / tf.cast(tf.shape(X)[0], tf.float32) # 特征值分解返回特征值和特征向量 eigenvalues, eigenvectors tf.linalg.eigh(C) # 按特征值降序排序 idx tf.argsort(eigenvalues, directionDESCENDING) eigenvectors tf.gather(eigenvectors, idx, axis1) # 取前2个主成分 principal_components X_centered eigenvectors[:, :2]我想提醒你一个细节tf.linalg.eigh专门针对实对称矩阵或复Hermitian矩阵因为协方差矩阵天然是实对称的。eigh内部使用了更高效的算法并且保证特征值有序默认升序。如果你用通用的tf.linalg.eig去分解对称矩阵也能得到结果但速度更慢、数值稳定性更差。所以在PCA这类场景里请使用eigh。如果你手头有经典的线性代数教材作参考比如很多人用过的基础教材特征值那一章会花不少篇幅教人手算特征多项式、求行列式。工程上我们完全不需要手算数值方法才是主流但我依然建议你把公式推导过一遍这样才明白eigh到底在算什么。3.2 奇异值分解更稳定的矩阵分解工具奇异值分解SVD是比特征值分解更通用的工具。任意一个实矩阵都可以分解为A U Σ Vᵀ其中U和V是正交矩阵Σ是对角矩阵对角元素叫奇异值。SVD不仅适用于方阵也适用于任意形状的矩阵这一点让它成了无数算法的主力。TensorFlow里的tf.linalg.svd返回三个结果奇异值s、左奇异向量u、右奇异向量v。重建公式是A u diag(s) transpose(v)注意这里的v是右奇异向量矩阵本身不是转置结果和NumPy里返回Vh的习惯并不一样重构时一定要记得手动转置。这样设计的好处是你可以拿到v的原始定义方便做特征向量计算。A tf.constant([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]) # (3, 2) s, u, v tf.linalg.svd(A, full_matricesFalse) # 重建验证 sigma tf.linalg.diag(s) A_hat u sigma tf.transpose(v) print(tf.reduce_max(tf.abs(A - A_hat)).numpy()) # 接近0SVD最有代表性的应用有三个。第一个是低秩近似和图像压缩只保留最大的若干个奇异值丢弃小的就能用更低维度近似原矩阵存储量大幅下降。第二个是推荐系统里的矩阵分解把用户-物品评分矩阵分解成用户隐向量、奇异值、物品隐向量的乘积再做召回和排序。第三个是矩阵秩的数值判断奇异值中非零的个数就是矩阵的秩但浮点运算下更实用的做法是统计大于某阈值的奇异值数量。SVD比特征值分解稳定的原因在于SVD不需要矩阵是方阵也不要求矩阵满秩它直接从矩阵本身的结构出发数值算法更稳健。所以在面对病态矩阵、非方阵、共线性严重的场景时SVD通常是更安全的选择。4. 求解线性方程组与最小二乘法4.1 用tf.linalg.solve解线性方程组解线性方程组Ax b是线性代数最基础的场景之一。机器学习里的解析解、控制理论里的状态估计都离不开它。TensorFlow提供了tf.linalg.solve它的用法非常直观传入系数矩阵A和右侧向量b返回解向量x。A tf.constant([[2.0, 1.0], [1.0, 3.0]]) b tf.constant([1.0, 2.0]) x tf.linalg.solve(A, b) print(x.numpy()) # [0.2 0.6]你可能会问既然x inv(A) b也能解为什么非要用solve答案是效率和稳定性。求逆需要完整算出A⁻¹再乘一次向量计算量是O(n³)还要额外做一次矩阵乘向量而solve底层通常使用LU分解或Cholesky分解直接把b带入求解过程避免显式求逆既快又稳。我的比喻是求逆相当于为了去10楼先把整栋楼的电梯都修一遍solve则是直接找到一条能到10楼的路线后者显然更省事。请注意solve要求A必须是方阵且可逆。如果矩阵奇异会直接抛出InvalidArgumentError提示矩阵不可解。在实际项目中我发现很多新人拿到一个长方形矩阵就去solve结果报错。此时要先判断问题属于“方程组数量等于未知数”还是“样本多于特征”后者通常要用lstsq。4.2 最小二乘与tf.linalg.lstsq现实世界的机器学习问题几乎都是超定方程组样本数量远大于特征数量也就是方程个数大于未知数个数。此时Ax b往往没有精确解只能找让残差平方和||Ax - b||²最小的近似解这就是最小二乘。TensorFlow的tf.linalg.lstsq正是为此准备的。用法同样简单# 构造一组线性数据 y 2*x 3 噪声 t tf.linspace(-1.0, 1.0, 50) X tf.stack([t, tf.ones_like(t)], axis1) # (50, 2) y 2.0 * t 3.0 tf.random.normal([50], stddev0.1) # 最小二乘求解系数 coeffs tf.linalg.lstsq(X, y) print(coeffs.numpy()) # 近似 [2.0, 3.0]底层实现通常走QR分解或SVD路线数值稳定性比教科书里的正规方程θ (XᵀX)⁻¹Xᵀy好很多。这里我踩过一个非常经典的坑用正规方程时如果XᵀX接近奇异特征之间有强相关性求逆结果会有很大的数值误差甚至完全跑偏但换成lstsq就稳多了因为它不会直接构造XᵀX这个病态矩阵。lstsq接的输入第一个参数是设计矩阵第二个参数是观测值。如果观测值是多维的它也会自动按列求解非常方便。我做线性拟合时经常把数据组织成(n_samples, n_features)的矩阵最后一列加个1作为偏置项然后交给lstsq一把推出所有系数。这个方法虽然古老但用来做基线模型、验证数据质量、检查特征间的共线性依然特别好使。5. 进阶玩法梯度、einsum与自动微分5.1 GradientTape与雅可比矩阵现代深度学习框架的杀手锏是自动微分而自动微分底层和线性代数密不可分。tf.GradientTape会把前向计算过程中涉及到的操作记录下来然后通过反向传播计算梯度。反向传播的每一步都是雅可比矩阵与梯度的连乘。举个例子计算一个简单函数的导数x tf.Variable(3.0) with tf.GradientTape() as tape: y x ** 3 dy_dx tape.gradient(y, x) print(dy_dx.numpy()) # 27.0因为 3 * 3^2 27如果要算雅可比矩阵也就是输出向量对输入向量的偏导数矩阵可以用tape.jacobianx tf.Variable([1.0, 2.0, 3.0], dtypetf.float32) with tf.GradientTape() as tape: y x * x # 逐元素平方 jac tape.jacobian(y, x) print(jac.numpy()) # 对角矩阵每个元素是 2x_i很多人写自定义模型时不理解为什么可以随便改中间的矩阵运算只要保证可微就行。其实只要你的运算里没有不可导的跳变比如除以零、tf.clip_by_value激进截断GradientTape都能正确地把梯度算出来。这就是用Linear algebra组件的好处矩阵乘法、求逆、SVD在TensorFlow里都是可导操作你甚至可以写一个损失函数里面包含SVD照样反向传播。我在实验里试过对tf.linalg.svd的结果求梯度确实能跑通这是传统线性代数库绝对给不了的。5.2 einsum用爱因斯坦求和记号优雅地消灭循环如果说tf.linalg是线性代数工具库那么tf.einsum就是一套通用张量运算语言。爱因斯坦求和记号的核心思想是用字符串描述一个运算比如ij,jk-ik表示矩阵乘法。TensorFlow会解析这个表达式并优化执行你根本不用写循环。用途极广下面几个例子覆盖了最常见的场景# 矩阵乘法 x tf.random.normal([2, 3]) y tf.random.normal([3, 4]) z tf.einsum(ij,jk-ik, x, y) # 转置后相乘ij,jk - ki 相当于 A.T B # 批量矩阵乘法bij,bjk-bik # 对角线求和ii-相当于 trace # 向量点积 a tf.constant([1.0, 2.0, 3.0]) b tf.constant([4.0, 5.0, 6.0]) dot tf.einsum(i,i-, a, b)在我看来einsum最大的价值是让代码意图一目了然。你写tf.einsum(bij,bjk-bik, a, b)别人一看就知道是批量矩阵乘法而如果写一堆tf.expand_dims、tf.transpose、tf.matmul读起来就痛苦很多。对性能敏感的场景einsum还会自动做计算调度减少中间变量的内存占用。不过有个小坑einsum的表达式写错后报错信息有时比较隐晦比如InvalidArgumentError: einsum does not support repeated indices。我的建议是先用小尺寸样例验证表达式是否符合预期再把真实数据套进去。我就吃过这个亏在多头注意力里用einsum实现QK^T乘积时下标顺序写反跑了几步才发现梯度完全不对。6. 常见问题与排查技巧实录6.1 维度不匹配报错实战排查矩阵维度错误是TensorFlow新手遇到最多的报错。典型的错误信息长这样InvalidArgumentError: Matrix size-incompatible: In[0]: [2,3], In[1]: [4,5]。这句话直接告诉你左边矩阵形状是(2,3)右边是(4,5)中间维度3和4对不上。遇到这个问题我的排查顺序是先打印所有涉及的张量形状确认到底谁和谁相乘然后检查是否需要先tf.transpose再检查批量维度是否一致。特别容易忽略的是(batch, seq, dim)和(dim, hidden)相乘时没有问题但设计成(batch, heads, seq, dim)后里面某一个维度就要小心对齐。建议在关键位置加一个断言assert a.shape[-1] b.shape[-2], f维度不匹配: {a.shape} vs {b.shape}调试时多用print(tensor.shape)比看报错信息更直观。我一般在自定义层的第一行就打印输入形状跑一次小样确认没问题再取消打印。这个方法看似笨但能省下大量在报错堆栈里翻来翻去的时间。6.2 数值问题NaN、奇异矩阵、float32精度数值问题是我在TensorFlow线性代数实战中遇到的最隐性的一类坑。第一种是NaN。NaN的来源很多数据里本身存在无穷值或NaN、梯度爆炸、求逆时遇到奇异矩阵。我遇到最典型的场景是特征矩阵存在完全共线的两列导致XᵀX不可逆这时候用tf.linalg.inv大概率得到NaN或无穷值。解决方案是改用tf.linalg.pinv或tf.linalg.lstsq并检查特征相关性。第二种是float32精度问题。TensorFlow默认使用float32在大多数模型训练场景没问题但做特征值分解、SVD时如果矩阵条件数很大float32的精度可能不够导致特征向量结果有明显误差。我的做法是复现数学推导、验证算法正确性时把dtype换成float64正式训练模型时再切回float32因为训练的前向反向传播对精度宽容得多。第三种是奇异矩阵的处理。你在实验里可能会碰到行列式为0、solve直接报错的情况。一个很实用的技巧是给矩阵加一个小的对角噪声比如A A 1e-6 * tf.eye(n)叫做Tikhonov正则化能有效缓解奇异性。这个思路在岭回归里用得很多底层逻辑都一样。我把常见问题整理成了一张速查表方便你直接对照现象可能原因排查方向常用解决方案Matrix size-incompatible矩阵乘法维度不匹配打印两矩阵shape检查内层维度转置、reshape、修正下标NaN出现在loss/权重数据含NaN、梯度爆炸、奇异矩阵检查输入数据、梯度范数、矩阵条件数清洗数据、梯度裁剪、改用pinv/lstsqsolve报错矩阵不可解矩阵奇异或非方阵检查矩阵秩和形状加对角噪声、改用lstsq/pinv特征向量结果异常float32精度不足对比float64结果临时切float64验证einsum报错下标表达式书写错误检查字符串里的维度符号小规模数据先行验证最后再分享一个我自己的实践习惯学TensorFlow线性代数最舒服的姿势是“带着公式来带着代码走”。我会在纸上把线性回归解析解、PCA推导手推一遍然后立刻用tf.linalg把每一步翻译成代码。手推公式能让我理解为什么要有转置和求逆写代码能让我快速验证自己的理解对不对。书上的公式和实际API对不上的时候多半是维度约定不同这时不要硬套先查API文档再手算一次。另外一个我自己很受用的技巧是把tf.linalg和GradientTape组合起来做“数学实验”。比如想验证某个矩阵分解性质直接建一个小张量前向计算后求梯度看梯度是否符合理论值。这种方法比用纸笔推导快得多而且能锻炼对张量形状的直觉。如果你想把这一块练扎实建议从这三件事开始把线性回归的解析解手推一遍再用代码实现对一组高维数据做PCA并对比sklearn的结果用一个简单的用户-物品评分矩阵跑SVD分解理解推荐系统里隐向量的含义。做完这三步你再看TensorFlow里的很多模型代码会突然觉得顺眼许多——因为底下跑的那些东西无非就是一座巨大的线性代数运算场。