矩阵点除(./)、左除(\)和右除(/)这三个符号,我在带新人的时候几乎每批都有人问,而且问法高度一致:“不都是除法吗,为什么 MATLAB 要搞三个?”更麻烦的是,这三种写法在很多情况下都能跑通、都不报错,但结果完全不是一回事,于是错误被悄悄带进了后面的仿真、拟合和控制算法里,等到结果对不上再回头查,往往已经翻了几十个脚本。我自己就干过这么一次:把逐元素归一化写成了A\b,程序安安静静跑完,曲线看着也挺“像那么回事”,最后发现单位全错。
这篇东西就是想把这件事一次性讲透。它属于 MATLAB 数值计算里最基础、但踩坑率最高的部分,涉及线性代数求解、逐元素运算、最小二乘、稀疏求解和性能取舍。刚入门的学生可以照着把三种除法的语义对齐;已经写了一阵子代码、但一直靠“试出来”的人,可以在这里弄清\背后到底调了什么求解器,以及为什么要避免inv。文中所有例子都可以直接粘进命令行复现,参数计算过程和预期结果我也会写清楚,方便你对照。
1. 三个符号到底各自在算什么
1.1./是逐元素除法,逐的是“位置”不是“矩阵”
A./B的语义极其朴素:结果的第(i,j)个元素等于A(i,j)除以B(i,j)。它跟线性代数没有任何关系,纯粹是两个数组在同一位置上做标量运算,和.*、.^是同一族。理解这一点之后,很多困惑会自动消失:./从来不“解方程”,它只是把矩阵拆成一堆格子,挨个做除法。
尺寸规则有两条。老版本(R2016b 之前)要求两边尺寸完全一致,或者其中一个是标量;新版本引入了隐式扩展,3×4的矩阵除以1×4的行向量是合法的,效果相当于每一行都被同一个行向量逐元素除一遍——这正是“按列归一化”最省事的写法。不过隐式扩展是把双刃剑,它让原本应该报错的尺寸不匹配变成静默计算,所以我在写这类代码时习惯在注释里标清哪一维是“按行”哪一维是“按列”,隔两周回来看不至于自己都读不懂。
还有一点容易被忽略:./和.\是一对,A.\B等价于B./A。方向搞反了不会报错,只是结果全变成倒数关系。同理A.\B在两边都是标量时a.\b就是b/a,注意分母是a而不是b。
1.2\是解方程,不是“求逆再乘”
A\b读作“左除”,数学含义是求解线性方程组A*x = b。注意它的定位:MATLAB 从设计之初就没打算让你算A的逆,\直接给出方程的解,中间怎么算由求解器自己决定。这是整个 MATLAB 数值体系里最重要的一条设计哲学——你要的是解,不是逆矩阵。
\的形状规则比./严肃得多。当A是n×n方阵、b是n×1列向量时,得到唯一解;当A是m×n且m>n(超定),得到最小二乘意义下的解,此时残差norm(A*x-b)不为零是正常的;当m<n(欠定),得到的是无穷多解中范数最小的那个,MATLAB 会给出一个warning提示秩亏。这三种情况共用一个符号,靠尺寸和秩自动分流,方便的同时也埋了坑:你以为自己在解方程,其实在做最小二乘拟合,残差悄悄存在但没人告诉你。
尺寸不匹配时它会干脆报错。比如A是4×4、b是1×4行向量,A\b直接抛Matrix dimensions must agree,因为A*X = b这个等式在维度上根本不成立。这个报错信息老被人误解成“\不支持行向量”,其实它只是提醒你该转置了。
1.3/是右除,本质上是左除的转置镜像
A/B求解的是X*B = A,也就是X = A*inv(B)。它和左除存在一个漂亮的恒等关系:
(A\B)' % 等于 B'/A' (A/B)' % 等于 B'\A'这个转置关系不是巧合,是定义使然。所以从能力上讲,右除完全是冗余的——任何A/B都能写成(B'\A')'。那为什么还要保留?因为在处理“行向量”“行样本”这类数据时,右除写起来更符合直觉。典型场景是:你有一个1×n的系数行向量w,想解w*B = y,直接写w = y/B一行搞定,不用来回转置。统计学里做最小二乘时按样本为行组织数据,右除也常常比左除顺手。
需要提醒的是/的优先级和结合性。在 MATLAB 里*、/、\、.*、./、.\处于同一优先级且左结合,所以a/b\c会被解析成(a/b)\c,而不是a/(b\c)。我自己被这个坑过一次:写1/2\3期望得到类似1/(2\3)的东西,实际得到的是0.5\3 = 6。结论很简单,混用除法符号时无脑加括号,别指望运算符优先级救你。
1.4 一张表把三者钉死
| 写法 | 官方函数名 | 数学含义 | 尺寸要求 | 典型用途 |
|---|---|---|---|---|
A./B | rdivide | 逐元素A(i,j)/B(i,j) | 同尺寸或可隐式扩展 | 归一化、逐点缩放、单位换算 |
A.\B | ldivide(点形式) | 逐元素B(i,j)/A(i,j) | 同尺寸或可隐式扩展 | 少用,容易与\混淆 |
A\B | mldivide | 解A*X = B | 行数匹配 | 解方程组、最小二乘、拟合 |
A/B | mrdivide | 解X*B = A | 列数匹配 | 行向量求系数、样本为行的回归 |
表里A.\B那一行的“少用”不是随口说的。它和A\B在视觉上只差一个点,在代码评审里几乎必然被看漏,而两者结果可能差出十万八千里。我的建议是:逐元素场景统一用./,永远不写.\,需要反向除法时写成B./A,语义清楚,也不会被误读。
2. 为什么 MATLAB 要这样设计
2.1 矩阵除法在数学上本来就是个“伪概念”
在标量世界里,a/b就是a乘以b的倒数。但矩阵没有“除以一个矩阵”这回事,只有“乘以逆矩阵”。而逆矩阵在数值上又极其敏感:一个条件数一万的矩阵,求逆之后有效数字能掉四位数。如果 MATLAB 老老实实提供div(A,B)并且内部实现成A*inv(B),那等于鼓励所有人走上数值最不稳的那条路。
所以设计者的选择是:不在语法层提供矩阵除法,只提供方程求解。\和/表面像除法,实质是“求解器入口”。你写x = A\b,MATLAB 拿到的信息是“已知 A 和 b,要求 x”,它可以选择 LU、Cholesky、QR,甚至对稀疏矩阵换一套完全不同的算法。如果换成x = inv(A)*b,那信息就锁死成“先求逆再乘”,求解器再聪明也没得选。这就是为什么官方文档反复强调inv几乎不该出现在生产代码里——不是它错,而是它剥夺了算法选择权。
2.2 逐元素运算单独设一套符号的必要性
*和.*、/和./、^和.^这三对,是 MATLAB 用符号区分“矩阵运算”和“数组运算”的核心手段。这跟很多语言不一样:在 Python 的 NumPy 里,*是逐元素,矩阵乘法要用@;MATLAB 反过来,裸符号是线性代数,加点才是逐元素。这个约定对做控制、信号、数值分析的人很友好,因为他们的工作里矩阵乘法是主角;但对做数据清洗、图像处理的人就有点别扭,因为他们八成时间都在做逐元素运算。
理解了这个取舍,你就明白为什么会出现“A*B报错而A.*B正常”这种事了:不是 MATLAB 挑剔,是两个符号本来就指向不同的数学对象。同理,A/B在B是标量时会退化成逐元素缩放(A/5等于A./5),这也是唯一一个两种语义重合的角落——正因为重合,标量场景怎么写都对,很多人就误以为矩阵场景也一样,坑就是这么来的。
2.3 标量场景是最大的“温柔陷阱”
我见过的最典型的错误链是这样的:先用A/2验证代码,结果正确;然后把2换成矩阵B,A/B不报错,但结果面目全非。原因就在于A/2走的是“每个元素除以 2”,而A/B走的是“解X*B = A”,两者在B是标量时才碰巧一致。
所以判断某个位置该用哪种除法,别看不报错,看右边那个东西是不是标量。是标量,两种写法等价(A/s与A./s结果相同,且s\A与A/s也相同,注意s\A等于A/s而不是A./s的反向);一旦右边是矩阵,就必须明确自己到底要逐元素还是要解方程。这条判据我贴在工位显示器边上贴了很久,比任何口诀都管用。
2.4\与/在工程习惯上的分工
左除和右除在能力上完全等价,选哪个纯粹是“让代码读起来像数学”。判断方法是看未知量在等式的哪一边:未知量在右边写成X*B = A就用/,未知量在左边写成A*X = B就用\。听起来很琐碎,但代码可读性就是这么一点点攒起来的。
还有一个实际考量是缓存友好性。MATLAB 是列优先存储,A\b对方阵做 LU 分解时按列访问,连续内存命中率高;A/B内部做的是(B'\A')'的转置运算,多了一层转置开销。矩阵规模大的时候这个差异会体现出来,所以能用左除就别用右除,除非行向量的语义确实让代码更清楚。我自己的习惯是数据处理阶段用右除保持可读性,进了核心算法循环一律统一成左除,方便后面做性能分析。
3.\背后到底调用了什么
3.1 方阵默认走 LU 分解
对一般的稠密方阵,A\b走的是带部分主元的 LU 分解,即把A拆成置换矩阵P、单位下三角L和上三角U,然后解两个三角方程组。部分主元的作用是每一步挑当前列绝对值最大的元素换到对角线上,把乘数控制在 1 以内,避免小主元放大误差。这也是为什么A\b在最坏情况下也比你手写的“高斯消元”稳——那套换行策略是内建在库里的。
这里有个实操层面的好处:如果你要对同一个A解很多个右端项,不要循环写A\b1、A\b2、A\b3。直接把B拼成n×k的矩阵,一次A\B就够,MATLAB 会自动对多个右端项批量前代回代。我做过一个场景,A是2000×2000,右端项 100 个,循环写法比一次性求解慢了一个数量级,差距完全来自重复的分解过程。这个优化几乎零成本,只需要在拼矩阵时注意内存翻倍的问题。
3.2 三角矩阵走回代,省掉整个分解
如果A是上三角或下三角(比如你自己构造的递推系统),\会识别出来直接做前代或回代,跳过 LU 分解。这个优化幅度很可观,因为三角求解的计算量只有O(n²),而完整 LU 是O(n³)。所以如果你的问题本身是三角结构,千万别多此一举去做inv,也别为了“好看”把矩阵乘来乘去破坏三角性。
识别是靠矩阵属性还是靠数值检查,这里面有个界限:MATLAB 会看你有没有显式声明三角属性(比如用triu构造的矩阵不会自动带属性),但内部求解时确实会做结构判断。稳妥的做法是明确知道自己给的是三角系统,而不是指望求解器猜。我见过有人在迭代过程中不断更新矩阵元素,把原本的三角结构改成了满阵却不自知,性能莫名其妙掉了,就是这类问题。
3.3 对称正定走 Cholesky,省一半计算量
如果A是 Hermitian 正定(对称正定是实数情形),Cholesky 分解A = R'*R的计算量约为 LU 的一半,存储也省一半。常见场景是正规方程A'*A*x = A'b里的A'*A——只要A列满秩,A'*A必然对称正定,此时 Cholesky 就派上用场了。
但这里有个所有数值计算教材都会强调的禁忌:别真的去构造A'*A再解。虽然数学上x = (A'*A)\(A'*b)和x = A\b等价,但前者的条件数是后者的平方。原始A条件数一千,A'*A就是一百万,双精度下有效数字直接从十三位掉到十位以内,参数稍微病态一点结果就不可信了。直接写A\b,MATLAB 对超定系统会用 QR 分解处理,数值稳定性好得多,计算量也更小。
3.4 超定和欠定各走各的路
m>n的超定系统,A\b给的是最小二乘解,内部用 Householder 反射做 QR 分解,A = Q*R,然后解R*x = Q'*b。QR 的好处是不需要构造正规方程,条件数不会被平方放大。m<n的欠定系统则给出最小范数解,MATLAB 用 QR 加列主元挑出线性无关的列,剩下的自由变量取零,得到的是无穷多解里范数最小的那个。这两种情况都会在返回值里体现,但不会报错,所以每次对长方形矩阵用\,都该顺手检查一下尺寸和秩,别默认自己在解方阵。
秩亏的判断阈值是max(size(A))*eps(norm(A)),比这个小的奇异值会被当成零。这个阈值对绝大多数工程问题合理,但对量纲差异极大的矩阵(比如一行是 1e6 量级、另一行是 1e-6 量级)就可能误判。遇到这种情况,先做列归一化(A ./ vecnorm(A)),再求解,稳定性会明显改善。这个技巧在拟合幂次差异大的物理模型时特别有用。
3.5 稀疏矩阵会换一整套求解器
只要A是sparse类型,\立刻切换到稀疏求解路径:稀疏 LU、稀疏 Cholesky、稀疏 QR,各自有对应的排序策略来减少填充。填充是非零元素在分解过程中新增的位置,填充越多内存和计算越吃亏,所以排序算法(比如最小度排序)在这里是核心。经验上,只要矩阵非零元占比低于 10%,稀疏存储就值得上;占比高到 30% 以上时,稀疏求解器的额外开销可能反而不划算,具体要看你自己的结构,用tic/toc实测最靠谱。
另外提醒一句:sparse和full混用要小心。A是稀疏、b是稠密时结果通常还是稠密,内存会翻倍;反过来把稠密矩阵硬塞进稀疏容器,转换本身就要花时间。这类问题在剖面分析里一眼就能看出来,别靠猜。
3.6 求解路径速查
| 矩阵特征 | 默认算法 | 计算量级 | 备注 |
|---|---|---|---|
| 稠密方阵 | LU + 部分主元 | O(n³) | 一般情形 |
| 三角矩阵 | 前代 / 回代 | O(n²) | 结构已知时最快 |
| Hermitian 正定 | Cholesky | 约 LU 的一半 | 不要手造A'*A |
超定m>n | QR | O(mn²) | 结果为最小二乘解 |
欠定m<n | QR + 列主元 | O(m²n) | 结果为最小范数解 |
| 稀疏 | 稀疏 LU / Cholesky / QR | 依赖填充 | 非零占比低才划算 |
这张表的意义不是让你背,而是遇到“为什么同一个式子换台机器结果不一样”时有排查方向。算法路径不同,舍入误差的累积方式就不同,最后一位数字有差异很正常,差异大到影响结论就得回去看条件数。
4. 三类除法的实操写法
4.1 逐元素归一化:./的主场
最典型的场景是把一组不同量纲的数据归一化到统一范围。比如有三列数据,分别是速度、温度、压力,量级差了五个数量级,想按列各自除以该列的最大值:
X = [12.5 310.2 1.02e5; 18.1 298.7 1.15e5; 9.4 305.5 0.98e5]; scale = max(X, [], 1); % 1x3 行向量,各列最大值 Xn = X ./ scale; % 隐式扩展,每列各除各的关键在于./配合隐式扩展,一行完成逐列缩放。如果写成X/scale,语义立刻变成解Y*scale = X,scale是1×3,X是3×3,维度上不成立,直接报错;万一尺寸凑巧合法,结果会是完全无关的东西。这个例子里我把scale做成行向量是刻意的,隐式扩展时行向量沿行方向广播,正好对应“每列除以自己的最大值”。
再补一个容易翻车的写法。有人为了省事写X ./ max(X),max(X)对矩阵返回的是各列最大值组成的行向量,看起来没问题——但如果你某天把X转置了,max(X)就变成各行的最大值,./广播的方向也跟着变,结果默默错了。所以归一化的基准向量一定要显式指定维度,写max(X,[],1)或者max(X,[],2),多打几个字符,省掉未来两小时调试。
4.2 多项式拟合:\的主场
给定一组观测点,想拟合一个二次多项式,构造范德蒙矩阵然后左除:
t = (0:0.1:2)'; % 21x1 y = 1.5 - 0.8*t + 2.2*t.^2 + 0.02*randn(size(t)); V = [ones(size(t)) t t.^2]; % 21x3 超定 coef = V \ y; % 3x1,最小二乘解 resid = norm(V*coef - y); % 残差,量级应与噪声相当这里V是21×3,超定,\自动走 QR 给出最小二乘解。注意y必须是列向量,如果y不小心是行向量,V\y会报维度错——这个错误提示其实是在帮你,比静默出错强得多。算完一定要看残差,如果残差量级远大于噪声水平,说明模型阶次不够或者数据里有离群点,这时候调求解器参数是没用的,得回去看模型。
对应的右除写法是coef' = y'/V',能算但没必要。我在拟合这类问题上坚持用左除,因为数据组织成“一行一个样本”还是“一列一个样本”是随时可调的,没必要为了迎合符号而转置来转置去。真要按样本为行组织,那就统一转置一次,全程用右除,风格一致比什么都重要。
4.3 坐标系变换:/顺手的地方
假设你有一批点在基B下的坐标,想换到标准基。设B的每一行是一个基向量,点坐标是行向量p,那么标准基下的坐标q满足q = p*B,反解就是p = q/B。这里用/完全是因为数据按行组织,写出来和数学式子一一对应:
B = [1 0.2; 0 1.1]; % 2x2 基矩阵,行向量为基 q = [3.4 5.1]; % 标准基下的坐标(行向量) p = q / B; % 求解 p*B = q check = p * B; % 回代验证,应等于 qcheck和q的差应该在eps量级,如果不是,说明B接近奇异或者你搞错了转置方向。这种“解完立刻回代验证”的习惯我强烈建议养成,一行代码的成本,能挡住大部分方向性错误。特别是/和\混用的代码里,回代验证几乎是唯一可靠的检查手段。
4.4 一个完整的参数对照实验
想彻底看清三者的差异,跑下面这段,把输出对比着看:
A = [4 1; 2 3]; b = [1; 2]; x_left = A \ b; % 解 A*x = b,2x1 x_elem = A .\ b; % 逐元素 b./A,注意方向 x_right = b' / A'; % 解 X*A' = b',与 x_left 转置一致 disp([x_left, x_elem, x_right']) disp(norm(A*x_left - b)) % 应接近 0 disp(norm(A.*x_elem - b)) % 一般不为 0预期结果是x_left与x_right'完全相等(在浮点误差内),而x_elem的数值跟它们毫无关系。最后两行的残差对比是最有说服力的:A\x的残差是机器精度量级,A.*x那个式子在数学上就不成立,残差随便多大。这个实验我让新人做过好几次,做完之后基本不会再混。
顺带说一个标量的特例,很多人在这里产生错觉。设s = 2,A是方阵,那么s\A等价于A/s,也等价于A./s,三者结果一致,因为标量除法没有方向之分。但A\s就不一样了,它求的是A*x = s,结果是一个列向量A⁻¹*s,和s./A(逐元素倒数乘s)完全不同。左边是标量、右边是矩阵时,\和./彻底分道扬镳,这个角落是错误高发区。
5. 常见问题与排查实录
5.1 报错 “Matrix dimensions must agree”
这个报错在./和\下的含义完全不同,得分清楚。在./下,它表示两个数组尺寸既不相等也无法隐式扩展,比如3×4 ./ 2×4——这时该检查是不是有一边少了转置。在\下,它表示A的行数和B的行数对不上,因为A*X = B要求A的列数与X的行数一致、A的行数与B的行数一致。最常见的具体情形就是b写成了行向量。
排查顺序我一般是这样:先size(A)、size(b)打出来看,别凭记忆;再判断自己到底要逐元素还是解方程;最后确认转置方向。有件事值得单独说:如果你的b本来就是一维数据,用b(:)强制成列向量比b'安全得多,因为b(:)对行向量和列向量都返回列向量,而b'对复数会顺带做共轭转置,把虚部符号翻掉。复数场景下这个坑极其隐蔽,b.'(非共轭转置)才是你想要的。
5.2 警告 “Matrix is singular to working precision”
这个警告的意思是:求解器判断你的矩阵在双精度下接近奇异,解不可信。原因通常是三类。第一类是矩阵真的降秩,比如两列线性相关,常见于特征里有重复项或者构造设计矩阵时漏了去重。第二类是量纲失衡,某几列数值差了十几个数量级,导致条件数爆掉,这时做列缩放就能救。第三类是迭代过程中矩阵退化,比如某个参数在优化里被推到边界,让某一行变成其他行的线性组合。
处理方式对应三种。降秩的,先rank(A)看有效秩,再考虑去掉冗余列或者改用pinv求最小范数解。量纲失衡的,先做A ./ vecnorm(A)归一化再解,解出来记得把系数缩放回去。迭代退化的,那属于模型本身的问题,得回去看参数约束,改代码没用。
pinv这个函数值得单独提一句:它基于奇异值分解,对秩亏矩阵给出最小范数最小二乘解,代价是计算量比\大不少(SVD 大约是 QR 的两倍以上)。所以它不是“更安全的\”,而是“处理病态问题的专用工具”,正常情况下别拿它当默认选项。
5.3 结果“看着对但差一点”:先算条件数再看残差
这是最难查的一类问题,因为不报错、不警告,数值上就有偏差。诊断有固定套路,两步走:
c = cond(A); % 条件数,粗略估计会损失 log10(c) 位有效数字 r = norm(A*x - b); % 残差,衡量解代回方程后的偏差条件数一万,双精度十六位有效数字就只剩十二位左右,这还算健康;条件数到了 1e12,基本只能信三四位,这时候任何“我的结果应该精确到小数点后六位”的期待都是错的。要注意残差小不等于解准——这是数值线性代数里最反直觉的结论之一。病态系统上,一个完全错误的x也可能给出很小的残差,因为它只是在满足方程的某个方向上有巨大误差,而那个方向对残差贡献很小。所以判断解的质量要看条件数和相对误差估计,别只盯残差。
实操层面的建议是:如果你怀疑解不准,用A\b算一遍,再用pinv(A)*b算一遍,两者差得多就说明系统确实病态,这时候该考虑正则化或者增加观测数据,而不是调容差参数。
5.4 整数类型和取整陷阱
MATLAB 里对整数类型做./会保持整数类型,并且向零取整,int32(7)/int32(2)得到3而不是3.5。这个行为和 C 语言的整除一致,但在数值计算里极其危险:你以为是浮点除法,实际在做截断,误差累积起来能把结果整体拉偏。我见过一个信号处理脚本,中间变量被定义成int16做逐点除法,最后整条曲线系统性偏低,查了很久才发现是取整。
解决办法有两个:需要精确除法就先把类型转成double,double(A)./double(B);确实要做整数除法但想控制取整方式,用idivide(A, B, 'floor'),第三个参数支持'fix'(默认,向零)、'floor'、'ceil'、'round'。另外A\b在整数类型下也会有很多限制,基本不建议在整数类型上做线性求解,一上来就转double是更省心的选择。
5.5 常见问题速查
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
Matrix dimensions must agree | 尺寸不匹配或漏转置 | size打印确认,一维数据用(:)转列 |
Matrix is singular警告 | 降秩、量纲失衡、迭代退化 | 查rank,列归一化,检查模型约束 |
| 结果与手算差很多但不报错 | 方阵场景误用了逐元素除法 | 确认右边是不是标量,改用\ |
| 浮点结果末位不一致 | 求解路径不同导致舍入差异 | 看cond,差异大则系统病态 |
| 整数结果被截断 | 整数类型除法向零取整 | 先转double,或用idivide |
同一A循环求解很慢 | 每次重复分解 | 把右端项拼成矩阵,一次求解 |
6. 性能和习惯上的一些取舍
6.1 关于inv的那点事
inv(A)*b和A\b在数学上等价,在数值上不等价。inv(A)显式算出逆矩阵,要额外做一次完整分解加一次矩阵乘法,计算量大约是A\b的三倍量级,而且舍入误差更大——因为先把逆算出来存成矩阵,误差被“固化”了,再乘以b会二次放大。所以凡是能写A\b的地方,都不该出现inv。
但我不喜欢“永远不要用inv”这种绝对化的说法。有些场景确实需要逆矩阵本身,比如协方差矩阵求逆后要用它的对角元做方差估计,或者要做 Sherman-Morrison 更新。这类情况用inv是合理的,只要你清楚自己在要矩阵而不是要解。判断标准很简单:你需要那个矩阵吗?需要就用inv,只需要解就用\。
6.2 隐式扩展带来的行为变化
R2016b 之后,./、.*、+、-都支持隐式扩展,尺寸兼容时不再报错。这让代码短了很多,但也让“本该报错”的错误变成了“静默算错”。比如你想让一个3×3矩阵逐行除以三个不同的数,结果不小心把除数做成了列向量,隐式扩展会把它按行广播,得到的是逐列相除,完全不是你要的效果。
我的习惯是在做广播运算时,在注释里写清广播方向,比如“scale是 1×n,按行广播,每列各除各的”,并且关键位置用assert把尺寸钉死:
assert(isequal(size(scale), [1, size(X, 2)]), 'scale 必须是行向量');这条断言在开发阶段能省掉大量排查时间,上线时注释掉或者留着都行,成本可以忽略。
6.3 稀疏矩阵上的除法选择
稀疏矩阵用\时,一定要保证右端项也是稀疏或者至少尺寸合理。一个常见的低效写法是对大规模稀疏系统循环求解,每次都要重新做一次稀疏分解,而分解恰恰是最贵的部分。正确做法还是拼矩阵一次求解,或者显式用decomposition对象把分解缓存下来复用:
dA = decomposition(A, 'lu'); % 分解一次 x1 = dA \ b1; % 多次复用 x2 = dA \ b2;decomposition这个对象在需要反复求解同一个系统时特别好用,尤其是参数扫描、时间步进这类场景。它还能让你显式指定分解类型,避免求解器“猜”错路径。我在做瞬态问题的时候基本都会先把矩阵分解缓存起来,循环里只做回代,速度提升非常直观。
6.4 我自己的几条硬规矩
用久了会形成一些不讲道理但很好用的习惯,分享几条。第一,看到/和\混在同一个表达式里,立刻拆成两行,别让优先级来决定你的结果。第二,矩阵除法写完后一定跟一行回代验证,norm(A*x-b)或者norm(x*B-A),成本一行,收益巨大。第三,逐元素操作用显式的./,不要依赖标量退化,今天右边是标量,明天需求变了换成向量,靠退化写出来的代码立刻崩。第四,长方形矩阵用\之前先size和rank各看一眼,你是在解方程还是在做拟合,心里得有数。
最后分享一个排查小技巧:如果一段涉及矩阵除法的代码结果可疑,把中间矩阵存成mat文件,单独开一个脚本用\、pinv、/三种方式各算一遍,把结果和残差一起打出来对比。差异出现在哪一步,问题就在哪一步。这个笨办法我用过很多次,比盯着代码读有效率得多。
这些符号本身不难,难的是每次写的时候都记得自己在做什么运算。前面那些踩过的坑,说到底都是“符号写对了,语义想错了”。