🦀 Rust + WASM 实战系列 第 21 篇 阅读时间:约 5 分钟 | 实战可运行
📌 写在前面
PCA 找"数据的主方向"(无监督 )------这一篇做"监督拟合 ":给一些 (x, y) 数据点,找一条直线最佳逼近。
公式超简洁:
β=(XTX)−1XTy
一行矩阵公式 搞定"线性回归"------但这行公式是怎么来的?[§ 一](#§ 一 "#%E4%B8%80%E6%A0%B8%E5%BF%83%E5%85%AC%E5%BC%8F%E4%BB%8E%E6%9C%80%E5%B0%8F%E4%BA%8C%E4%B9%98%E5%88%B0%E6%AD%A3%E8%A7%84%E6%96%B9%E7%A8%8B") 从最小二乘法的直觉出发,一步一步把它推导出来 。代码侧用 nalgebra 库调用 try_inverse() 求逆就行。
🚀 TL;DR
两个真实图像应用:
| 应用 | 场景 | 公式 |
|---|---|---|
| 图像去噪 | 扫描旧照片有"上面亮、下面暗"的条纹 | 拟合 y=a⋅row+b,逐行减去 |
| 颜色校准 | 显示器偏色(拍标准色卡拟合修正) | 拟合 actual=m⋅target+b,反向应用 |
核心算法 就是 β = (X^T X)⁻¹ X^T y------矩阵求逆。
📖 目录
- 核心公式:从最小二乘到正规方程
- [Demo 1:图像去噪(行均值拟合)](#Demo 1:图像去噪(行均值拟合) "#%E4%BA%8Cdemo-1%E5%9B%BE%E5%83%8F%E5%8E%BB%E5%99%AA%E8%A1%8C%E5%9D%87%E5%80%BC%E6%8B%9F%E5%90%88")
- [Demo 2:颜色校准(拍色卡拟合)](#Demo 2:颜色校准(拍色卡拟合) "#%E4%B8%89demo-2%E9%A2%9C%E8%89%B2%E6%A0%A1%E5%87%86%E6%8B%8D%E8%89%B2%E5%8D%A1%E6%8B%9F%E5%90%88")
- 关键代码(用库调用)
- 前端效果展示
- 踩坑提醒
- 接下来
一、核心公式:从最小二乘到正规方程
这一节完整推导 β=(XTX)−1XTy------从"最小二乘是什么"到"矩阵形式",一步一步。
1.1 最小二乘是什么?------拟合的直觉
问题 :给 n 个数据点 (x1,y1),(x2,y2),...,(xn,yn),找一条直线 y=ax+b 最佳逼近。
为什么不要求直线穿过所有点 ?因为数据有噪点------同一 x 对应的 y 不会完全相同(拍照亮度会浮动、传感器有误差、人为测量也会偏)。强行穿过每个点,反而会被噪点带偏。
什么叫"最佳"? ------需要一个量化标准来衡量"偏离程度"。
最小二乘的标准 :让所有点到直线的纵向距离的平方和最小。
为什么是"纵向距离"(沿 y 方向)而不是"垂直距离"?因为我们要预测 y ------给一个 x,希望预测的 y^ 和真实 y 接近。垂直距离在数学上更对称,但不直接对应预测误差。
1.2 优化目标:残差平方和
对每个点定义残差(真实值减预测值):
ei=yi−(axi+b)
优化目标:
a,bminS(a,b)=i=1∑nei2=i=1∑n(yi−axi−b)2
为什么用"平方"而不是"绝对值"或"立方"?
- 可微 :平方处处可微,绝对值在 ei=0 处不可微 → 平方更容易求极值
- 放大离群点 :平方让远离直线的点被"惩罚得更狠"------直线倾向于贴近主体数据,不被极端点带偏
- 闭式解:最小化平方和 → 线性方程组(直接有解析解,不用迭代优化)
1.3 怎么求最小值?------求导
S(a,b) 是关于 a 和 b 的二元连续可微函数 。极值点 ⟺ 偏导数都为 0(微积分基本定理):
∂a∂S=0,∂b∂S=0
只要解出这两个方程,就能找到极值点。这里是极小值 而非极大值,因为 S(a,b) 是 a、 b 的凸二次函数(开口向上的抛物面)------凸函数的局部极小 = 全局最小。
1.4 手算:对 a 和 b 求偏导
把 S 展开,链式法则:
∂a∂S=i∑2(yi−axi−b)⋅(−xi)=−2i∑xi(yi−axi−b)
∂b∂S=i∑2(yi−axi−b)⋅(−1)=−2i∑(yi−axi−b)
令两个偏导数都等于 0(除以 −2):
i∑xi(yi−axi−b)=0⋯(1)
i∑(yi−axi−b)=0⋯(2)
展开(把 a 和 b 移到右边):
i∑xiyi=ai∑xi2+bi∑xi⋯(1′)
i∑yi=ai∑xi+b⋅n⋯(2′)
两个方程、两个未知数 a、 b------理论上能解。但还有 5 个累加和散落在公式里,看着乱。下面把它们打包成矩阵。
1.5 写成矩阵形式------正规方程
把 (1')(2') 写成矩阵:
∑xi2∑xi∑xinab=∑xiyi∑yi
关键观察 :左边矩阵的元素恰好等于一个 XTX,右边向量恰好是 XTy------其中 设计矩阵:
X= x1x2⋮xn11⋮1 ,y= y1y2⋮yn
验证 XTX(一个 2×n 矩阵乘 n×2 矩阵):
XTX=x11x21⋯⋯xn1 x1x2⋮xn11⋮1 =∑xi2∑xi∑xin✓
验证 XTy( 2×n 矩阵乘 n×1 向量):
XTy=x11x21⋯⋯xn1 y1y2⋮yn =∑xiyi∑yi✓
所以方程 (1')(2') 恰好就是:
XTXab=XTy
这叫正规方程 。两边左乘 (XTX)−1(前提: XTX 可逆,见 [§ 六踩坑](#§ 六踩坑 "#%E5%85%AD%E8%B8%A9%E5%9D%91%E6%8F%90%E9%86%92")):
β=(XTX)−1XTy,其中 β=ab
这就是 § 写在前面那行"超简洁"的公式------只是 4 个矩阵乘法的打包写法。
1.6 设计矩阵的几何意义
为什么 X 要加一列 1?
- X 的第 i 行 xi,1 乘以 β=a,bT 得到预测值 y^i=axi+b
- 这一列 1 就是截距项 ------没它就只能拟合过原点的直线( y=ax)
这个模式可以推广------只要模型是"参数的线性组合"就能用正规方程:
| 模型 | 设计矩阵 X 的行 |
|---|---|
| y=ax+b | xi,1 |
| y=ax2+bx+c | xi2,xi,1 |
| y=a1x1+a2x2+b | xi,1,xi,2,1 |
| k 阶多项式 | xik,xik−1,...,xi,1 |
正规方程对所有"线性模型"都成立------这就是为什么它在机器学习里是"基础方程"。
二、Demo 1:图像去噪(行均值拟合)
应用场景
扫描的旧照片经常有这种系统性行噪点:
- 上面亮、下面暗(扫描灯光不均)
- 或某些行特别亮(扫描仪传感器问题)
算法
- 计算每行像素的平均亮度 → 得到 n 个点 (rowi,avg_brightnessi)
- 最小二乘拟合: avg_brightness=a⋅row+b
- 从原图每行减去拟合值 → 去除系统性行噪点
scss
原图: 处理后:
┌──────────────┐ ┌──────────────┐
│ ░░░ 亮 │ │ │
│ ░ 中等 │ → │ 一致亮度 │
│ ▓ 暗 │ │ │
└──────────────┘ └──────────────┘
(上面亮/下面暗) (统一了)
三、Demo 2:颜色校准(拍色卡拟合)
应用场景
显示器偏色(偏暖/偏冷)------用标准色卡 (已知 RGB 值)拍照,得到实际显示的 RGB,拟合修正系数。
算法
- 准备 n 组数据点 (targeti,actuali):每组是 3 维 RGB
- 对每通道独立拟合: actual=m⋅target+b
- 对原图应用反向变换 : corrected=(actual−b)/m
ini
目标红色 (255, 0, 0) → 实际显示 (260, 10, -5)
↓
拟合 m=1.02, b=10
↓
反向:corrected_r = (260 - 10) / 1.02 = 245
四、关键代码(用库调用)
Demo 1:行均值去噪
rust
#[wasm_bindgen]
pub fn denoise_row_mean(pixels: &[u8], width: u32, height: u32) -> Vec<u8> {
let w = width as usize;
let h = height as usize;
// 1. 每行平均亮度
let mut row_means: Vec<f64> = Vec::with_capacity(h);
for y in 0..h {
let mut sum = 0.0;
for x in 0..w {
let idx = (y * w + x) * 4;
sum += (pixels[idx] as f64 + pixels[idx + 1] as f64 + pixels[idx + 2] as f64) / 3.0;
}
row_means.push(sum / w as f64);
}
// 2. 最小二乘拟合 y = ax + b(手算公式)
let n = h as f64;
let x_mean = (h as f64 - 1.0) / 2.0;
let y_mean = row_means.iter().sum::<f64>() / n;
let mut num = 0.0;
let mut den = 0.0;
for (i, &y) in row_means.iter().enumerate() {
num += (i as f64 - x_mean) * (y - y_mean);
den += (i as f64 - x_mean).powi(2);
}
let a = if den != 0.0 { num / den } else { 0.0 };
let b = y_mean - a * x_mean;
// 3. 逐行减去拟合值
let mut out = pixels.to_vec();
for y in 0..h {
let offset = a * y as f64 + b;
for x in 0..w {
let idx = (y * w + x) * 4;
out[idx] = (pixels[idx] as f64 - offset).clamp(0.0, 255.0) as u8;
out[idx + 1] = (pixels[idx + 1] as f64 - offset).clamp(0.0, 255.0) as u8;
out[idx + 2] = (pixels[idx + 2] as f64 - offset).clamp(0.0, 255.0) as u8;
}
}
out
}
Demo 2:颜色校准(用库)
rust
/// 单通道最小二乘:actual = m * target + b
fn solve_channel(target: &[f64], actual: &[f64]) -> (f64, f64) {
let n = target.len();
// 构造设计矩阵 X = [target, 1](n × 2)
let mut x_mat = DMatrix::<f64>::zeros(n, 2);
for i in 0..n {
x_mat[(i, 0)] = target[i];
x_mat[(i, 1)] = 1.0;
}
let y_vec = DVector::from_vec(actual.to_vec());
// 正规方程:β = (X^T X)^(-1) X^T y
let xtx = x_mat.transpose() * &x_mat;
let xty = x_mat.transpose() * y_vec;
let beta = xtx.try_inverse().unwrap() * xty;
(beta[0], beta[1]) // (m, b)
}
核心就 1 行库调用 :β = (X^T X)⁻¹ X^T y------xtx.try_inverse() * xty。
五、前端效果展示


六、踩坑提醒
1. 协方差矩阵奇异时 try_inverse 返回 None
rust
// ❌ 直接解引用会 panic
let inv = xtx.try_inverse().unwrap();
// ✅ 用 unwrap_or 兜底
let inv = xtx.try_inverse().unwrap_or(DMatrix::identity(2, 2));
2. 校准点 < 2 组不能拟合
rust
if n < 2 {
return pixels.to_vec(); // 至少 2 组才能确定一条直线
}
3. 校准的反向变换有除零风险
rust
if mr.abs() > 1e-6 {
out[idx] = ((r - br) / mr).clamp(0.0, 255.0) as u8;
} else {
out[idx] = r as u8; // m 接近 0:直接复制
}
4. 行均值去噪对整行偏色 有用,对局部噪点没用
- ✅ 适用:扫描灯光不均、传感器行漂移
- ❌ 不适用:椒盐噪点、随机噪点(这些应该用前面任务 25 的去噪)
七、接下来
任务 22:深挖 PCA:从最大化方差到最小化重建误差 从优化问题本身推导 PCA 的数学原理。
关键 insight:"最大化方差" ≡ "最小化重建误差" ≡ 协方差矩阵的特征向量------三个看似不同的视角其实指向同一个数学对象。
这一篇的"求偏导 = 0 → 联立方程组 → 矩阵形式"推导套路和[§ 1.4-1.5](#§ 1.4-1.5 "#14-%E6%89%8B%E7%AE%97%E5%AF%B9-a-%E5%92%8C-b-%E6%B1%82%E5%81%8F%E5%AF%BC")一脉相承------PCA 和最小二乘都是"凸优化 → 求偏导 → 矩阵方程"。
一句话总结
最小二乘 = 找一条直线最佳拟合数据点 ------ β=(XTX)−1XTy。
代码 1 行库调用 :
xtx.try_inverse() * xty。两个真实应用:图像去噪(行噪点)+ 颜色校准(显示器偏色)。
📦 项目地址 :pixel-math-wasm 🦀 Rust + WebAssembly 实战系列
🏷️ 标签 :#Rust #WebAssembly #图像处理 #最小二乘 #回归 #矩阵求逆 #nalgebra