MATLAB学线性代数:从矩阵到SVD图像压缩实战指南

📅 发布时间:2026/8/31 13:03:01
MATLAB学线性代数:从矩阵到SVD图像压缩实战指南
学线性代数时最痛苦的事情是什么不是公式难算而是算完之后不知道它有什么用。高斯消元解三元一次方程组要一页草稿纸手算特征多项式时漏一个符号就得从头再来好不容易算出特征值又搞不清楚特征向量到底在物理世界里代表什么。但等你真正开始写代码做图像处理、做机械臂运动学、做推荐系统你会发现所有问题又都绕回到矩阵和向量上。MATLAB 是我见过最接近线性代数“自然语言”的编程工具。Matrix Laboratory 这个名字已经说明了一切矩阵不是它处理的对象而是它的基本存在方式。你用A*B表示矩阵乘法用A\b解线性方程组用eig(A)求特征值——这些命令几乎就是把数学课本上的符号原封不动搬进代码里。相比 C 或 Java 里先写三层循环才能完成一个矩阵乘法MATLAB 的学习曲线对线性代数初学者友好得多。但这篇文章想给的判断是MATLAB 学线性代数不要只把它当成高级计算器。真正有价值的是把理论和计算对接起来——看到一个公式马上想“这个在 MATLAB 里怎么算”看到一个矩阵分解马上想“它解决哪类实际问题”。这篇免费 MATLAB 教程会从矩阵创建的细节一直讲到 SVD 图像压缩实战每一节都配有完整代码你可以直接复制运行。如果你正在学线性代数或者在科研、工程项目里突然发现自己要处理大量矩阵运算我建议你一边读一边在 MATLAB 里敲代码。理论看不懂的地方用代码算一遍代码跑不通的地方回到理论想一遍。这个方法比任何单一形式的刷题或背笔记都有效。1. 为什么 MATLAB 是学线性代数最合适的工具先看一个非常实际的问题线性代数里最基础的操作——矩阵乘法用不同工具写出来差距有多大如果只用 Python 的 NumPy其实也还算方便A B一行就能算完。但在很多通用编程语言里你要写成三重循环// C语言实现两个二维矩阵相乘仅作示例不是推荐写法 for (int i 0; i m; i) { for (int j 0; j p; j) { C[i][j] 0; for (int k 0; k n; k) { C[i][j] A[i][k] * B[k][j]; } } }每次写这段代码你都不得不分心去管理下标i、j、k这不是线性代数本身的问题而是编程语言的表达问题。你在“翻译”数学公式而不是直接写公式。MATLAB 里就是一句话C A * B;这个差异在学习阶段会被放大。线性代数的核心不是算一个乘法结果而是理解“矩阵乘法对应线性变换的复合”这一层含义。如果你把精力花在三重循环的下标管理上就很难再关注本质。另一个原因是 MATLAB 的数据容器高度统一。标量、向量、矩阵、多维数组、稀疏矩阵这些在数学上有区别的概念在 MATLAB 里都围绕着“矩阵”这个核心展开。变量默认就是矩阵创建出来不用声明类型这大大降低了从公式到代码的摩擦。当然MATLAB 不是唯一选择。Python 的 NumPy/SciPy 也很强大而且免费。但 MATLAB 在交互式探索、内置可视化、以及和 Simulink 等工具链的衔接上对初学者和工程师更友好。尤其是线性代数这门课MATLAB 的命令行几乎就是“线性代数公式翻译器”这一点很难替代。所以这篇文章的思路是以 MATLAB 为实验台把线性代数的每个核心概念都变成可运行的代码。你不需要提前掌握太多 MATLAB 知识只需要按照本文的章节顺序操作即可。2. 先学会“造矩阵”创建、拼接与索引线性代数的一切都从矩阵开始。在 MATLAB 里创建一个矩阵最直观的方法是直接按行输入分号表示换行% 直接输入一个 3x3 矩阵 A [1, 2, 3; 4, 5, 6; 7, 8, 9];这里注意逗号或空格分隔同一行的元素分号分隔不同行。很多人初学时会用方括号加逗号写成A [1, 2, 3; 4, 5, 6]这是对的但如果把[]换成()就成了索引含义完全不同。这是一个很重要的区分[]是数组构造符用来拼矩阵。()是索引符用来取元素或子矩阵。{}是元胞cell构造符用来装不同类型的内容。实际项目里经常需要生成特殊矩阵比如全零矩阵、全一矩阵、单位矩阵、随机矩阵% 常用矩阵生成函数 Z zeros(3, 4); % 3行4列全零矩阵 O ones(2, 5); % 2行5列全一矩阵 I eye(4); % 4x4单位矩阵 R rand(3, 3); % 3x3均匀分布随机矩阵 N randn(4, 2); % 4x2标准正态分布随机矩阵 D diag([1, 2, 3]); % 以 [1,2,3] 为对角元素的对角矩阵冒号表达式也是生成向量和矩阵的高频工具。1:5得到[1 2 3 4 5]1:0.5:3得到从 1 到 3、步长 0.5 的向量。冒号还承担了“全选”的功能这点在矩阵切片时非常关键。关于索引MATLAB 和 Python 最大的区别是下标从 1 开始不是从 0 开始。学 MATLAB 的线性代数你要习惯这一点A [10, 20, 30; 40, 50, 60; 70, 80, 90]; a11 A(1, 1); % 第1行第1列结果是 10 row2 A(2, :); % 第2行所有列 col3 A(:, 3); % 第3列所有行 sub A(1:2, 2:3); % 子矩阵第1到2行、第2到3列这里我强烈建议你把A(:, 3)、A(2, :)读出来“第2行全选”、“第3列全选”。这种读法能帮你把“矩阵是二维表”的直觉内化。如果创建矩阵时维度写错了MATLAB 会直接报错比如zeros(3, 4)和zeros(3, 4, 2)得到的是完全不同的东西。前者是二维矩阵后者是三维数组。在线性代数里我们很少用到三维数组但图像处理中会碰到——一张彩色图像就是行 x 列 x 3的三维数组这也是 MATLAB 图像处理的基础。小节结论MATLAB 的矩阵创建非常直观核心注意点是索引从 1 开始方括号构造与圆括号索引不要混淆。这部分熟练以后后面的矩阵运算才有基础。3. 矩阵运算最容易踩坑*与.*、与.这是 MATLAB 初学者最容易出错的地方也是面试题和课程作业里最常出现的陷阱。MATLAB 里有两套运算符一套是矩阵运算一套是数组运算按元素运算。矩阵乘法A * B要求A的列数等于B的行数。数组乘法A .* B要求A和B维度完全一致对应位置相乘。同样的区别也出现在除法和幂运算上A / B矩阵右除、A \ B矩阵左除、A ./ B对应元素相除、A ^ 2矩阵自乘两次、A .^ 2每个元素平方。看一个具体的例子A [1, 2; 3, 4]; B [1, 0; 0, 1]; C1 A * B; % 矩阵乘法结果还是 [1,2;3,4]因为 B 是单位阵 C2 A .* B; % 数组乘法对应位置相乘结果 [1,0;0,4]如果你的本意是“两个矩阵的对应元素相乘”但写成了A * B那么当维度恰好匹配时MATLAB 会按矩阵乘法计算返回一个你完全没想到的结果。反过来如果你本意是矩阵乘法写成了A .* B结果同样匪夷所思。这种错误不报错只产生错误结果非常隐蔽。还有转置。很多教材里矩阵转置用右上角T表示在 MATLAB 里有两种写法A共轭转置。如果A是实数矩阵等价于普通转置如果是复数矩阵会先取共轭再转置。A.非共轭转置。不管实数复数都只转置不取共轭。大多数线性代数教材里的转置是“普通转置”所以如果你处理的是复数矩阵用A.更安全如果处理的是实数矩阵用A也没问题。但工程中遇到复数信号时这两个结果差距很大。向量的内积和外积是矩阵乘法在向量上的特例。内积就是行向量乘列向量x [1; 2; 3]; y [4; 5; 6]; dot_xy x * y; % 结果是一个标量32外积是列向量乘行向量得到一个矩阵outer_xy x * y; % 结果是一个 3x3 矩阵理解这两者的区别是后续理解正交、投影、最小二乘的基础。另外MATLAB 提供了官方函数dot和cross但用x * y这种写法能帮你把矩阵乘法的维度规则刻在脑子里。小节结论看到运算符号先问自己是“矩阵运算”还是“数组运算”。多写点测试代码用size()查看维度能减少大量低级错误。4. 线性方程组Axb请优先使用左除\解线性方程组Ax b是线性代数最重要的应用场景之一。最稳妥、最“MATLAB 风格”的写法是A [2, 1; 1, 3]; b [5; 6]; x A \ b;这个反斜杠\是左除运算符它读作“左除”意思是求解A*x b。MATLAB 会自动选择合适的方法来求解比如对小型稠密矩阵走 LU 分解对大型稀疏矩阵走迭代法。你不需要手动选择算法这是 MATLAB 相比手写高斯消元最大的便利。很多初学者习惯这样写x inv(A) * b;这不是不能用但不推荐。inv(A)需要先求逆矩阵再做矩阵乘法数值上更容易放大误差而且当A接近奇异病态时inv(A)的结果本身就不稳定。用A \ bMATLAB 可以通过算法检测异常情况给你更可靠的解。这里还要纠正一个常见的理解偏差行列式不等于可解性判断的唯一标准。很多人学了det(A)就习惯性地用“行列式不为零说明有唯一解”来判断。这个说法在理论上没错但计算上很危险。因为一个矩阵可能行列式非常接近零比如1e-20理论上它仍然可逆但数值上已经严重病态。更可靠的判断是看条件数condition numbercond(A)条件数越大说明线性方程组对b的微小扰动越敏感解越不可靠。条件数接近 1 的矩阵是良态的条件数达到1e10级别就要警惕结果误差。同样是求逆工程上还有一个更稳定的工具伪逆pinv。当矩阵不是方阵或者接近奇异时pinv能给出最小范数最小二乘解。这个在使用超定方程组做数据拟合时非常常见。% 超定方程组最小二乘数据点个数大于未知数个数 t (0:0.1:1); y 2 * t 1 0.1 * randn(size(t)); % 模拟带噪声的数据 % 拟合 y a*t b构造设计矩阵 X [t, 1] X [t, ones(size(t))]; coef X \ y; % 最小二乘解 a_fit coef(1); b_fit coef(2);这里的X \ y并不是解一个精确方程因为方程个数多于未知数一般不存在严格解。MATLAB 的左除会自动给出最小二乘意义上的最优解。这就是为什么工程上用\比用inv更合适的原因——同一个符号既能解方阵方程组也能处理超定问题。小节结论解线性方程组记住一条铁律能用\就不要用inv。判断矩阵是否病态不要单看det要看cond。5. 特征值与特征向量eig的细节和几何意义特征值和特征向量是线性代数的核心内容之一。MATLAB 里用eig函数A [4, 1; 2, 3]; % 只求特征值 lambda eig(A); % 同时求特征向量和特征值 [V, D] eig(A);返回的V是特征向量矩阵每一列是一个特征向量D是对角矩阵对角线上的元素是对应的特征值。验证方法很简单A * V(:, 1) - D(1, 1) * V(:, 1)理论上这个结果应该是零向量。由于浮点计算误差实际结果可能是1e-15量级的微小向量这在正常范围内。在工程应用里eig最常见的场景是判断系统的稳定性、主成分方向、振动模态。比如在结构力学里特征值对应固有频率的平方特征向量对应振型在数据分析里协方差矩阵的特征值对应方差贡献特征向量对应主成分方向。当矩阵很大且你只需要少数几个特征值/特征向量时建议用eigs而不是eig。eigs是迭代法可以指定求解最大的几个或最小的几个特征值内存和耗时更低。% 求最大3个特征值/特征向量稀疏大矩阵更推荐 [Vk, Dk] eigs(A, 3, largestabs);需要提醒的是特征向量不唯一。任何一个特征向量乘以一个非零常数仍然是同一特征值对应的特征向量。MATLAB 返回的特征向量已经做了归一化模长为 1但方向可能不同。不同版本的 MATLAB 或不同算法可能给出方向相反的特征向量这不影响理论正确性但如果你在对比两次计算结果要注意这一点。相似对角化也是线性代数里的重点如果A有 n 个线性无关的特征向量那么A可以写成% A V * D / V A_restore V * D / V;用 MATLAB 验证err norm(A - A_restore, fro);如果err非常小说明相似对角化成立。这里用norm(..., fro)是指 Frobenius 范数即矩阵所有元素平方和再开方衡量两个矩阵的整体差异。理解这个公式比手算对角化更快也更接近实际工程中的验证方式。小节结论eig是一次性求出全部特征信息eigs适合大型稀疏矩阵只求少数特征值。验证特征分解是否正确用范数误差是最直接的。6. 矩阵分解LU、QR、SVD 分别解决什么问题矩阵分解是线性代数中进阶到工程应用的桥梁。很多初学者觉得 LU、QR、SVD 只是考试题里“化简矩阵”的步骤实际上它们是数值计算和数据分析的灵魂。三种分解的定位可以用下表概括分解函数核心用途适用场景LU 分解lu解线性方程组、求行列式高斯消元的矩阵化表达QR 分解qr最小二乘、正交化、特征值迭代基础数据拟合、数值稳定性要求高的场景SVD 分解svd降维、压缩、伪逆、主成分分析图像压缩、推荐系统、降噪、矩阵低秩近似LU 分解的本质是把A分解成下三角矩阵L和上三角矩阵U的乘积A [2, 1, 1; 4, 3, 3; 8, 7, 9]; [L, U, P] lu(A); % P*A L*U注意这里引入了置换矩阵P。实际计算中为了保证数值稳定性需要进行行交换所以P*A L*U。你如果只用两个输出参数[L, U] lu(A)得到的L不一定是严格的下三角矩阵因为 MATLAB 会把它和行交换信息打包在一起。很多人第一次用lu时都困惑过这一点要提醒自己三个输出参数[L, U, P]才是最容易理解的版本。QR 分解把A分解成正交矩阵Q和上三角矩阵RA [1, 2; 3, 4; 5, 6]; [Q, R] qr(A); % A Q*R其中 Q*Q IQR 分解在最小二乘问题里比正规方程(A*A)更稳定。因为计算A*A会平方化条件数导致数值精度丢失而 QR 分解不会。这也是为什么A\b在求解超定线性方程组时比你自己写(A*A)\(A*b)更可靠。SVD 分解是这三种里最让我觉得“值得单独学透”的[U, S, V] svd(A); % A U*S*VS是对角矩阵对角线上的奇异值按从大到小排列。奇异值衰减的速度反映了矩阵的“有效秩”。很多实际数据矩阵奇异值会迅速衰减这意味着我们可以丢弃很小的奇异值用低秩矩阵近似原矩阵从而实现压缩、去噪、降维。用 SVD 求伪逆也是一个重要技巧A_pinv pinv(A); % 等价于基于 SVD 的伪逆在工程里处理病态矩阵时pinv远比inv可靠。因为它可以截断接近零的奇异值避免数值爆炸。小节结论LU 是解方程的基础工具QR 是最小二乘的稳定选择SVD 是数据分析与压缩的核心。如果只学一个优先学 SVD它对后续机器学习、信号处理、图像处理的帮助最大。7. 可视化像《线性代数的本质》一样“看”变换3Blue1Brown 的《线性代数的本质》让很多人第一次真正“看见”了线性变换。动画里那些网格变换、向量旋转其实就是矩阵在起作用。但动画是别人的自己动手在 MATLAB 里画一遍理解会更牢固。最基础的向量可视化是用quiver画箭头% 画两个向量 figure; quiver(0, 0, 1, 2, 0, r, LineWidth, 2); hold on; quiver(0, 0, 3, 1, 0, b, LineWidth, 2); axis equal; grid on; xlabel(x); ylabel(y); legend(向量 (1,2), 向量 (3,1)); title(二维向量可视化);画线性变换前后的网格是理解矩阵乘法的极佳练习。假设一个线性变换矩阵A [1, 0.3; 0.2, 1]它会把原来的基向量e1[1,0]^T和e2[0,1]^T变成新位置。你可以先画一组网格点再用矩阵乘法变换所有点观察网格如何被拉伸和旋转% 生成网格点 [x, y] meshgrid(-2:0.5:2, -2:0.5:2); pts [x(:); y(:)]; A [1, 0.3; 0.2, 1]; % 变换所有点新点 A * 旧点 new_pts A * pts; x_new reshape(new_pts(1, :), size(x)); y_new reshape(new_pts(2, :), size(y)); figure; subplot(1, 2, 1); plot(x, y, k.); axis equal; title(变换前); subplot(1, 2, 2); plot(x_new, y_new, r.); axis equal; title(变换后);运行这段代码你会直观看到矩阵A如何扭曲整个坐标平面。这和《线性代数的本质》里“矩阵是线性变换的数值表达”完全对应。行列式的几何意义也可以用面积可视化来理解。2x2 矩阵的行列式绝对值表示单位正方形在变换后的面积缩放倍数。如果行列式为负说明变换过程中发生了“翻转”。在 MATLAB 里画两个向量围成的四边形再用det计算面积能把这个抽象概念变得非常具体。还可以画特征向量的几何意义特征向量是变换后方向不变的向量。你可以在平面上画几个方向的单位向量用矩阵去变换它们观察哪些方向在变换后仍然保持原方向只有特征向量所在的直线满足这个性质。这一步如果亲手做一遍比背十遍“特征向量是经过变换后不改变方向的向量”都管用。8. 实战案例用 SVD 实现图像压缩这一节把前面的知识串起来。SVD 的一个经典工程应用是图像压缩把图像看成矩阵做奇异值分解只保留较大的奇异值就能用远少于原始数据的存储量近似重建图像。在开始之前先确保你能读取一张图片。MATLAB 自带一些示例图像也可以用你自己的图片。假设你用imread读入一张彩色图然后转为灰度图% 读取图像请替换为你本地的图片路径 img imread(你的图片.jpg); % 如果是彩色图先转灰度 if size(img, 3) 3 gray double(rgb2gray(img)); else gray double(img); end [m, n] size(gray); fprintf(图像尺寸: %d x %d\n, m, n);做 SVD 分解并使用不同数量的奇异值重建% SVD 分解 [U, S, V] svd(gray); % 取前 k 个奇异值重建图像 k 20; % 保留奇异值个数可调整 U_k U(:, 1:k); S_k S(1:k, 1:k); V_k V(:, 1:k); % 重建图像 gray_reconstructed U_k * S_k * V_k; % 显示原图与重建图 figure; subplot(1, 2, 1); imshow(uint8(gray)); title(原始灰度图); subplot(1, 2, 2); imshow(uint8(gray_reconstructed)); title([只保留前 , num2str(k), 个奇异值]);这段代码关键在U(:, 1:k)取前 k 列矩阵S(1:k, 1:k)取前 k 个奇异值组成的对角矩阵V(:, 1:k)取前 k 列。三者相乘得到一个近似矩阵gray_reconstructed。% 计算压缩效果 num_original m * n; num_compressed m * k k n * k; % U_k S_k V_k 需要保存的元素总数 fprintf(原始数据量: %d\n, num_original); fprintf(压缩后数据量(k%d): %d\n, k, num_compressed); fprintf(压缩比: %.2f%%\n, 100 * (1 - num_compressed / num_original)); % 计算重建误差 recon_error norm(gray - gray_reconstructed, fro) / norm(gray, fro); fprintf(相对误差: %.4f\n, recon_error);运行起来你会看到即使只保留 20 个奇异值图像主体轮廓依然清晰。奇异值下降得越快压缩效果越好。这就是“矩阵的低秩近似”在最真实工程问题里的体现。如果想观察“不同 k 值下的重建效果”可以把上面的代码放进循环里对比 k10、k50、k200 的结果。你会发现 k 一开始增加时细节迅速恢复到某个阈值之后提升变慢。这个“阈值”对应的奇异值衰减点就是矩阵的有效秩。% 对比不同 k 值的影响 k_list [10, 30, 70, 150]; figure; for i 1:length(k_list) k k_list(i); Uk U(:, 1:k); Sk S(1:k, 1:k); Vk V(:, 1:k); img_recon Uk * Sk * Vk; subplot(2, 2, i); imshow(uint8(img_recon)); title([k , num2str(k)]); end这个案例把矩阵分解、数据压缩、误差计算和可视化全部串联起来了。做完这一遍你对 SVD 的感知会比单纯看公式深刻得多。9. 常见问题与排查方法用 MATLAB 学习或工程实践中下面这些问题出现频率很高我整理成了一张排查表问题现象可能原因排查方式解决方案矩阵乘法报错矩阵维度必须一致A*B中 A 的列数不等于 B 的行数用size(A)和size(B)查看维度调换乘法顺序或改用.*使用了.*仍然报错两个矩阵形状不一致比如一个是 3x3另一个是 3x1检查两个矩阵的行数和列数用reshape调整维度A\b解出的结果非常离谱矩阵A条件数过大接近病态cond(A)查看条件数改用pinv(A)*b或正则化方法eig求出的特征向量每次运行方向不同特征向量方向本就不唯一检查重建误差重建正确即可用符号或固定归一化方式统一imshow显示全黑或全白矩阵类型是 double 且范围不是 0~255用min(gray(:))和max(gray(:))检查范围转成uint8或用imshow(mat, [])内存不足矩阵过大特别是一次性保留多个分解结果whos查看变量占用用single精度、剔除不需要的变量、用稀疏矩阵lu返回的 L 不是下三角输出参数个数不对查看文档doc lu用[L, U, P] lu(A)A^2结果和预期不同你本意是每个元素平方但要A.^2观察结果是否是矩阵乘法改成A.^2解超定方程组结果方差很大直接用正规方程导致数值不稳定对比A\b和(A*A)\(A*b)的结果优先用A\b这里单独提醒一个最常见的错误数字和矩阵混用时容易出问题。比如你写A [1, 2; 3, 4]然后想取第一行写成A(1)。MATLAB 会返回 1因为它把矩阵按列优先的顺序展开A(1)是第一个元素不是第一行。要取第一行必须写A(1, :)。这是从其他语言转过来的用户最容易踩的坑。另一个错误在图像处理里很常见用svd分解图像矩阵时忘了把图像从uint8转成double。如果不转换svd在某些版本里也能运行但数值精度和内存效率都会受影响。更稳妥的做法是在做数值运算前先用double转换最后显示时再转回uint8。10. 学习路径与工程建议如果你现在刚接触 MATLAB 和线性代数我建议按下面的路径走第一步先掌握矩阵创建、索引、基本运算和向量可视化这部分对应本文第 2、3、7 节。能熟练地把一个公式翻译成矩阵代码再去谈后面的分解和特征值。第二步用线性方程组和特征值把理论课上的习题在 MATLAB 里验证一遍。大部分教材习题都可以直接用A\b和eig求解你不用再花时间手算而是把注意力放在“为什么这样算”上。第三步做一个小项目。SVD 图像压缩、QR 最小二乘拟合数据、LU 分解求解电路方程都是很好的选择。项目不用大但一定要完整跑通理解输入和输出分别是什么。第四步在工程中继续扩展。如果你做信号处理去了解奇异值和频谱的关系如果做机器学习去了解 PCA 和 SVD 的等价性如果做自动控制去了解特征值和系统稳定性的联系。在 MATLAB 使用习惯上有几点建议一是多用doc命令。遇到不认识的函数直接doc 函数名阅读官方文档。MATLAB 自带文档质量很高比我见过的大多数第三方教程都清晰。二是善用 Live Script实时脚本。扩展名为.mlx可以把文字、公式、代码、运行结果和图像混排在同一份文档里。学习线性代数时用 Live Script 写“概念解释 代码验证 结果分析”比单纯写.m文件更高效整理复习资料也方便。三是要养成检查数值误差的习惯。计算A\b后可以随手算一下残差norm(A*x - b)看看它是否足够小。如果残差很大说明数值稳定性有问题而不是简单认为“代码跑通了”。四是处理大矩阵时优先考虑稀疏矩阵。MATLAB 里用sparse函数创建稀疏矩阵很多线性代数函数都支持稀疏输入能显著节省内存和计算时间。五是命名规范要清晰。矩阵变量尽量用大写字母标量用小写函数名避免和内置函数冲突。比如不要定义名为size、sum、max的变量否则后续调用同名内置函数时会出各种奇怪问题。最后想说MATLAB 只是一把趁手的工具真正重要的还是线性代数本身。矩阵的本质是线性变换的数值表达特征值告诉我们变换沿哪些方向伸缩SVD 告诉我们复杂数据能拆成哪些“成分”的叠加。把这些概念想通了再回头看 MATLAB 的每个命令你会发现每一条都对应一个清晰的数学意义。遇到不会的运算先想一想这个操作在数学上是什么遇到抽象的数学概念马上到 MATLAB 里画一画、算一算。两条路同时走线性代数就不再是背公式的课程而是一套可以随时用来解决实际问题的思维工具。