MATLAB基础学习笔记:矩阵创建、索引、运算与向量化实战
MATLAB基础学习笔记1、矩阵基础知识我第一次正经学MATLAB是从帮室友改一段图像处理作业代码开始的。当时他在循环里用A(i)(j)取矩阵元素报错报了半小时我凑过去一看就乐了——MATLAB里矩阵索引不是这么写的它压根不需要括号套括号这种别扭语法。也正是从那次开始我意识到想在MATLAB里写出干净、能跑的代码矩阵基础这关绕不过去。这篇笔记写给几类人刚装好MATLAB、被界面和命令行吓住的新手从Python或C转过来、习惯用list和数组思维写东西的人还有那些要交大作业、想快速把矩阵操作盘熟的同学。我会把矩阵的创建、索引、运算、变形这些高频操作讲透顺带说一下我踩过的坑。掌握了这篇的内容后面学图像处理、数据拟合、仿真建模都会顺手很多。1. 为什么说矩阵是MATLAB的“头号公民”1.1 MATLAB这个名称本身就说明了一切MATLAB的全称是Matrix Laboratory也就是“矩阵实验室”。它从诞生那天起就把矩阵当作最核心的数据形式。你在MATLAB里写的每一个数值、每一组数据本质上都是矩阵——一个单独的整数5是1x1矩阵一行数据[1 2 3]是1x3矩阵一张灰度图片是MxN矩阵一张RGB彩色图片则是MxNx3的三维数组。这个“万物皆矩阵”的设计理念决定了你在MATLAB里写代码的方式和在其他语言里完全不同。我在Python里处理一组数据时脑子里想的是“列表推导式”“for循环遍历”到了MATLAB里我脑子里想的是“构建矩阵”“矩阵运算”。这是思维方式的转变不是语法细节的修补。1.2 矩阵在真实应用里长什么样举个具体的例子。假设你要做一个灰度图像处理的小程序读进来的图像在MATLAB里就是一个二维矩阵矩阵里的每个数值代表该像素点的亮度。想提取图像的左上角100x100区域做滤波不需要写双重循环一条索引语句就搞定了。再比如做数值计算解线性方程组Ax b在MATLAB里直接写成x A\b底层用的是高斯消去或LU分解但使用者根本不需要关心这些细节矩阵运算被封装得干干净净。这也是为什么MATLAB在矩阵运算相关的科研和工程领域生命力这么强。与其说是语言本身有多高级不如说它对“数据以矩阵形式组织、以矩阵运算直接操作”这件事的支持做到了极致。理解了这一点下面讲的所有内容就都有了落脚点。1.3 矩阵与数组的关系辨析很多人刚接触时会被“矩阵”和“数组”这两个词搞糊涂。简单来说MATLAB里的数值数组和矩阵在存储层面是同一回事只是概念侧重不同数组强调数据的排列结构维度、形状矩阵强调线性代数语义乘法、转置、求逆。在MATLAB里它们用同一套容器存储只是运算时有区别——普通乘法*走的是线性代数规则点乘.*走的是逐元素规则。这个概念先记在心里后面第四章我会详细展开因为这是新手最容易踩的雷区。2. 创建矩阵的八个高频姿势与内存预分配那点事2.1 最原始的敲字输入法在MATLAB命令行里直接敲矩阵是最直观的创建方式。规则非常简单同一行元素用空格或逗号分隔不同行之间用分号分隔整块内容用方括号包起来。比如A [1 2 3; 4 5 6; 7 8 9]这样就创建了一个3x3矩阵。如果不想让每次命令都在命令行刷出结果就在语句末尾加个分号A [1 2 3; 4 5 6; 7 8 9];这个分号还能用来分隔同一行里的多条语句比如a 1; b 2;可以一次执行两件事。我刚开始时经常忘记打分号结果命令行被巨大的矩阵输出刷了满屏找半天看不到自己要的结果。习惯成自然之后只要不是特意想看输出我都会带上分号。小矩阵这么敲没问题但超过几十个元素还手敲就太傻了。比如要生成一组从0到10间隔0.5的数据手敲得疯掉这时候要用下一招。2.2 冒号表达式与linspace、logspace冒号表达式是MATLAB里生成规则序列的利器格式是“起始值:步长:终止值”。步长可以省略默认是1x 1:10; % 生成 1 2 3 4 5 6 7 8 9 10 y 1:2:10; % 生成 1 3 5 7 9 z 10:-1:1; % 生成 10 9 8 ... 1注意步长为正时起始值要小于终止值为负时相反否则生成的是空矩阵。这个空矩阵真的会参与运算后面循环语句里不小心写反了范围很可能就是“什么都没发生”却找不到原因。如果有明确“取多少个点”的需求用linspace更合适。它按等间隔在区间内生成指定数量的点x linspace(0, 2*pi, 100); % 在0到2π之间取100个点 y sin(x); % 直接对向量求正弦得到100个结果还有一个相对小众的logspace按对数等间隔生成点在做频率分析时用处很大。我后来做控制系统的伯德图频率轴就是logspace(-2, 2, 500)生成的那叫一个漂亮。2.3 zeros、ones、eye、rand这些“工厂函数”实际项目中经常需要预定义大小的矩阵。预先用zeros等函数把容器建好再往里面填值效率远比“边循环边扩增矩阵”高得多。这个技巧叫“内存预分配”在循环里尤其关键。Z zeros(3, 4); % 3行4列全零矩阵 O ones(2, 5); % 2行5列全一矩阵 I eye(5); % 5阶单位矩阵 R rand(3, 3); % 0到1之间的均匀分布随机矩阵 N randn(3, 3); % 标准正态分布随机矩阵这些函数还支持“只传一个参数”的写法比如zeros(5)会生成5x5方阵。我经常用zeros(size(A))来生成和某个现有矩阵同尺寸的零矩阵这在保存计算结果时非常方便。2.4 空矩阵的妙用空矩阵写作[]看起来没什么用实际场景里却是神器。想删除矩阵的某一行或某一列直接把它赋值为空矩阵就行M [1 2 3; 4 5 6; 7 8 9]; M(2, :) []; % 删掉第2行M变成 2x3 M(:, 1) []; % 再删掉第1列M变成 2x2这个技巧我在数据清洗时经常用比如把某个不满足条件的行筛选出来然后删掉一行代码就解决问题。3. 索引与切片新手翻车最多的重灾区3.1 下标索引、线性索引和end的用法MATLAB的索引从1开始这点和Python/Java完全相反也是最容易让从其他语言转过来的人栽跟头的点。我当年用Python写脚本写习惯了到了MATLAB里总想找A[0]结果要么报错要么取出来的东西和预想完全不同。取矩阵某个元素的语法是A(行, 列)。注意这里用圆括号不是方括号——方括号是创建矩阵用的。取子矩阵用冒号表示范围A magic(4); % 生成一个4x4的魔方矩阵 B A(2, 3); % 取第2行第3列的元素 C A(1:2, 3:4); % 取第1~2行、第3~4列的子矩阵 D A(:, 2); % 取第2列全部元素 E A(2:end, :); % 取第2行到最后一行的全部列end关键字在索引里太好用了代表“这一维度的最后一个下标”省去先查size再计算边界的麻烦。注意end只能在索引语句里用不能赋值给变量这是我早期犯过的错——想存end的值结果直接报语法错误。还有一个不太显眼但很实用的线性索引MATLAB的矩阵在内存里是按列顺序存储的你可以只传一个下标来按这个顺序取元素。也就是说A [1 2 3; 4 5 6; 7 8 9]; A(5) % 输出 8因为按列展开是 1 4 7 2 5 8 3 6 9这个性质在需要遍历全部元素时很好用比如配合for k 1:numel(A)的结构。3.2 逻辑索引用一个0/1向量挑选数据逻辑索引是用一个同尺寸的逻辑矩阵0或1去挑选原矩阵中的元素结果是一列按顺序排的向量。它配合条件表达式使用是数据筛选的利器x [1 5 3 8 2]; y x(x 3); % 取出大于3的元素得到 [5 8]再比如清洗数据时想把某个数组中的NaN值全部去掉data [1.2; NaN; 3.4; NaN; 5.6]; clean data(~isnan(data)); % ~isnan 是逻辑取反逻辑索引和普通下标索引的区别我一开始也是搞了很久才真正记住普通索引x([1 2 3])取的是指定位置上的元素逻辑索引x(logical([1 0 1]))取的是对应位置值为1的元素。前者需要整数向量后者需要逻辑向量。混着用时MATLAB会把数值向量当下标用逻辑向量才能做条件筛选。注意不是所有情况都自动转换用错了很容易报错或得到莫名其妙的结果。3.3 索引越界与自动扩展看似报错实则纵容其他语言里数组越界是致命错误MATLAB里读越界会直接报错但写越界却会“自动扩展”。比如A [1 0]; A(3) 9; % A自动变成 [1 0 9]这在有些场景是便利更多时候是隐患。特别是循环里不小心写偏了一个下标矩阵会被悄悄撑大程序不报错但结果完全不对。排查起来非常费劲。我的建议是能预分配就预分配让矩阵的尺寸始终在你掌控之中不要依赖自动扩展这个“纵容”机制。3.4 索引相关的常见报错与排查思路写MATLAB的人多多少少都见过这几类报错。我整理一下自己踩过的报错信息常见原因解决方法Index exceeds the number of array elements读取下标越界检查循环边界或end使用位置Array indices must be positive integers or logical values下标出现了0、负数或小数检查索引变量是否被算成了浮点数Subscript indices must either be real positive integers or logicals索引表达式里混入了无效类型确认逻辑索引用的确实是逻辑数组排查索引问题我有个笨但管用的土办法在报错前插入断点用whos检查涉及的变量尺寸用size确认边界。多试几次基本都能定位到问题。4. 矩阵运算里最容易混淆的乘法、除法和转置4.1 普通乘法*和点乘.*的本质区别这一节我说它是矩阵基础里最重要的部分一点不过分。普通乘法*走的是线性代数规则要求左边的列数等于右边的行数结果矩阵的尺寸是“左行数 x 右列数”点乘.*则是逐元素相乘要求两个矩阵尺寸完全一致结果尺寸也和它们一样。A [1 2; 3 4]; B [2 0; 1 3]; C A * B; % 矩阵乘法 % 结果 [4 6; 10 12] D A .* B; % 逐元素乘法 % 结果 [2 0; 3 12]我见过太多新手把这两个搞混尤其是在拟合、积分、图像处理场景中。比如要计算y x.^2 2*x如果写成x^2矩阵乘法直接报“尺寸不匹配”写成x.^2才是逐元素平方。这里我总结一条经验只要你脑子里想的是“对应位置元素做运算”就用带点版本只要你想的是“线性代数意义上的整体运算”才用不带点版本。下面这张表建议刻在脑子里运算语法语义尺寸要求矩阵乘法A * B线性代数乘法A的列数B的行数逐元素乘法A .* B对应位置相乘同尺寸矩阵乘方A ^ 2矩阵自己乘自己方阵逐元素乘方A .^ 2每个元素平方任意矩阵矩阵除法A / B右除等于 A * inv(B)方阵且可逆逐元素除法A ./ B对应位置相除同尺寸4.2 左除\与右除/解方程组的最佳姿势解线性方程组Ax b用的是左除A [2 1; 1 3]; b [5; 8]; x A \ b; % 结果 x [1.4; 2.2] 左右严格验证一下是否满足 A*xb左除背后的算法会根据矩阵结构自动选择可能是LU分解、Cholesky分解或者QR分解不需要使用者操心。千万别在MATLAB里用x inv(A) * b解方程组虽然数学上等价但数值稳定性差、速度慢而且当矩阵接近奇异时误差会被放大。我第一次写数值计算代码时就用过inv(A)*b后来做大型矩阵时直接被卡到差点以为死机换成左除之后快了一个数量级。右除/解决的问题是xA b这种形式即未知向量在右边写到MATLAB里就是x b / A。实际编程中需求相对少一些但要记住两个除号方向相反含义不同。4.3 转置的两种写法和复数场景的坑转置用单引号A表示矩阵A的转置。这对于实数矩阵没有任何问题。但一旦遇到复数就要小心了A做的其实是共轭转置也就是先转置再把每个元素取共轭。如果你只想做转置、不想取共轭要用点转置A.。B [12i, 34i]; B % 结果含共轭 B. % 纯转置不含共轭这个细节在做频域分析、电路计算时很容易出问题。我自己的习惯是只要确定是实数矩阵用哪个都无所谓一旦涉及复数域老老实实区分和.别嫌烦。4.4 行列式、求逆与矩阵秩的前置条件det(A)求矩阵行列式rank(A)求秩inv(A)求逆矩阵。这些函数用起来简单但有几个前提要心里有数矩阵必须是方阵才能求行列式和逆行列式为0或接近0时矩阵奇异逆矩阵不存在或极不稳定rank可以用于非方阵。我在做数据拟合时经常算rank来判断特征是否共线一旦发现秩亏缺就该考虑去掉某些变量而不是盲目求逆。5. reshape、拼接与扩展矩阵变形全家桶5.1 reshape按列优先重组别被顺序坑了reshape(A, m, n)可以把矩阵变成m行n列的新矩阵。注意MATLAB是按列优先的顺序重新排列元素也就是说原始矩阵的所有元素先按列展开成一串再按顺序填入新矩阵。这个顺序和很多别的语言不一样我刚开始用reshape时总想按行展开结果得到和预期完全相反的结果。A [1 2 3; 4 5 6]; % 2x3 B reshape(A, 3, 2); % 结果 1 5 % 4 3 % 2 6如果不想管行数和列数希望其中一维自动推断可以用[]C reshape(A, 2, []); % 自动算出列数是35.2 拼接三板斧cat、horzcat、vertcat把两个矩阵拼在一起最直观的写法是方括号里用分号垂直拼接或逗号水平拼接A [1 2]; B [3 4]; C [A; B]; % 垂直拼接得到 2x2 D [A, B]; % 水平拼接得到 1x4当矩阵数量更多、拼接维度更高时用cat(dim, A, B)更清晰dim指沿着第几个维度拼接。水平拼接对应cat(2, A, B)垂直对应cat(1, A, B)。还有一个细节拼接时要求除了拼接维度之外的尺寸完全一致否则报错。比如垂直拼接要求列数相同水平拼接要求行数相同。这个约束在批量组图时特别容易踩到——图像尺寸不一致往往会在这里报错而不是你真的哪儿写错了。5.3 repmat与kron重复矩阵扩展数据的场景repmat(A, m, n)把矩阵A整体重复m行、n列常用于构造大规模测试数据或者把一个序列扩展成矩阵形式。比如v [1 2 3]; M repmat(v, 3, 1); % 生成 3x3每行都是 1 2 3kron(A, B)则做Kronecker积作用是把B整体放大成A的尺寸后逐块展开。这个函数在有限元、系统辨识里偶尔能用上但在基础阶段先了解存在即可。我再补一个小技巧meshgrid和ndgrid可以把一维向量扩展成网格矩阵做三维曲面图和二元函数计算时离不开它们。6. 让矩阵运算跑得更快的向量化思维6.1 能用矩阵运算解决的事尽量别写for循环MATLAB的循环速度虽然在近几个版本里提升明显但和向量化运算相比仍有差距。我在老版本上做实验时一个10万级的循环能跑到让人怀疑人生换成向量写法用不到一秒就出结果。核心原因在于MATLAB的数值计算底层调用高度优化的线性代数库LAPACK、BLAS矩阵运算能直接调用这些库的高性能实现而循环只能逐元素解释执行。举个例子计算y sin(x)在100万个点上的值x linspace(0, 2*pi, 1e6); % 写法1循环容易忘预分配还慢 y zeros(size(x)); for k 1:numel(x) y(k) sin(x(k)); end % 写法2向量化 y sin(x);两种写法结果完全一样但写法2更简洁也更贴近MATLAB的设计哲学。我自己的习惯是写出循环版本先跑通逻辑然后回头想想能不能用矩阵运算整体表达。这一步对初学者来说尤其重要因为它会潜移默化地培养“矩阵思维”。6.2 sum、mean、max等统计函数的维度参数矩阵运算里很多内置函数默认沿第一个非单一维度操作但你可以通过维度参数来控制。比如sum(A)对矩阵A默认求各列之和返回一个行向量如果想求各行之和用sum(A, 2)。max和min除了返回数值还能同时返回下标A magic(3); [maxVal, idx] max(A, [], 2); % 每行最大值及其所在列这个“空参数[]”负责占位好把维度参数写到第三个位置别漏了。我第一次用max(A, 2)时就犯了错——它把2当成了与A逐元素比较的标量结果得到了一堆2而不是按行取最大值。这种角落里的小坑光看文档不容易注意写错了才会印象深刻。6.3 实测对比向量化到底快了多少我之前在给学生演示时做过一个简单测试矩阵大小取1000x1000分别用循环和向量化做逐元素平方求和。循环版本跑了约0.12秒向量化版本约0.005秒差距20倍以上。这个数字会随机器和版本浮动但趋势非常稳定。等矩阵规模上到几千万级差距会更夸张。对于一个要跑几十轮迭代的算法这个差距直接决定你是能喝杯咖啡等结果还是干脆别跑。另外还有一个潜在的性能杀手在循环里不断扩增数组。每扩一次MATLAB都要重新分配内存并复制整个数组代价随数组增长呈平方级上升。解决办法依然是预分配——用zeros提前把目标数组建好循环里只做赋值不做扩增。6.4 向量化思维怎么练我有个笨办法拿到一段循环代码逐句看它到底在“对每个元素做什么”。如果做的事情一样只是元素位置在变那就能用矩阵运算替代。比如“对每个元素加1”写成A1“对每行求和”写成sum(A,2)“把每个元素大于0的取出来”写成A(A0)。练多了之后你会发现很多看似复杂的操作其实都能用几个矩阵函数组合出来。这个习惯一旦养成后面学图像处理、信号处理、优化算法都会轻松很多。这篇笔记写到这矩阵基础的几个大块就都覆盖了创建、索引、运算、变形、性能优化。最后分享一个我自己的体会学MATLAB不要只看文档一定要打开命令行一边敲一边试。报错了就盯着报错信息看大部分问题都能从报错里找到蛛丝马迹。矩阵这东西手敲过一遍比看十遍教程都管用。下一篇笔记我打算写写条件和循环结构以及怎么跟今天讲的矩阵操作配合起来用。