Rust图像处理第21节-最小二乘回归:用矩阵求逆解"拟合"问题

🦀 Rust + WASM 实战系列 第 21 篇 阅读时间:约 5 分钟 | 实战可运行

📌 写在前面

PCA 找"数据的主方向"(无监督 )------这一篇做"监督拟合 ":给一些 (x, y) 数据点,找一条直线最佳逼近

公式超简洁:
β=(XTX)−1XTy\beta = (X^T X)^{-1} X^T 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+by = a \cdot \text{row} + b y=a⋅row+b,逐行减去
颜色校准 显示器偏色(拍标准色卡拟合修正) 拟合 actual=m⋅target+b\text{actual} = m \cdot \text{target} + b actual=m⋅target+b,反向应用

核心算法 就是 β = (X^T X)⁻¹ X^T y------矩阵求逆


📖 目录

  1. 核心公式:从最小二乘到正规方程
  2. [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")
  3. [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")
  4. 关键代码(用库调用)
  5. 前端效果展示
  6. 踩坑提醒
  7. 接下来

一、核心公式:从最小二乘到正规方程

这一节完整推导 β=(XTX)−1XTy\beta = (X^T X)^{-1} X^T y β=(XTX)−1XTy------从"最小二乘是什么"到"矩阵形式",一步一步。

1.1 最小二乘是什么?------拟合的直觉

问题 :给 nn n 个数据点 (x1,y1),(x2,y2),...,(xn,yn) (x_1, y_1), (x_2, y_2), \dots, (x_n, y_n) (x1,y1),(x2,y2),...,(xn,yn),找一条直线 y=ax+by = a x + b y=ax+b 最佳逼近

为什么不要求直线穿过所有点 ?因为数据有噪点------同一 xx x 对应的 yy y 不会完全相同(拍照亮度会浮动、传感器有误差、人为测量也会偏)。强行穿过每个点,反而会被噪点带偏。

什么叫"最佳"? ------需要一个量化标准来衡量"偏离程度"。

最小二乘的标准 :让所有点到直线的纵向距离的平方和最小。

为什么是"纵向距离"(沿 yy y 方向)而不是"垂直距离"?因为我们要预测 yy y ------给一个 xx x,希望预测的 y^ \hat y y^ 和真实 yy y 接近。垂直距离在数学上更对称,但不直接对应预测误差。

1.2 优化目标:残差平方和

对每个点定义残差(真实值减预测值):
ei=yi−(axi+b) e_i = y_i - (a x_i + b) ei=yi−(axi+b)

优化目标
min⁡a,b S(a,b)= ∑i=1n ei2= ∑i=1n (yi−axi−b)2 \min_{a, b} S(a, b) = \sum_{i=1}^{n} e_i^2 = \sum_{i=1}^{n} (y_i - a x_i - b)^2 a,bminS(a,b)=i=1∑nei2=i=1∑n(yi−axi−b)2

为什么用"平方"而不是"绝对值"或"立方"?

  • 可微 :平方处处可微,绝对值在 ei=0 e_i = 0 ei=0 处不可微 → 平方更容易求极值
  • 放大离群点 :平方让远离直线的点被"惩罚得更狠"------直线倾向于贴近主体数据,不被极端点带偏
  • 闭式解:最小化平方和 → 线性方程组(直接有解析解,不用迭代优化)

1.3 怎么求最小值?------求导

S(a,b)S(a, b) S(a,b) 是关于 aa a 和 bb b 的二元连续可微函数极值点 ⟺ 偏导数都为 0(微积分基本定理):
∂S∂a =0, ∂S∂b =0 \frac{\partial S}{\partial a} = 0, \quad \frac{\partial S}{\partial b} = 0 ∂a∂S=0,∂b∂S=0

只要解出这两个方程,就能找到极值点。这里是极小值 而非极大值,因为 S(a,b)S(a, b) S(a,b) 是 aa a、 bb b 的凸二次函数(开口向上的抛物面)------凸函数的局部极小 = 全局最小。

1.4 手算:对 a 和 b 求偏导

SS S 展开,链式法则:
∂S∂a =∑i2(yi−axi−b)⋅(−xi)=−2∑ixi(yi−axi−b) \frac{\partial S}{\partial a} = \sum_i 2(y_i - a x_i - b) \cdot (-x_i) = -2 \sum_i x_i (y_i - a x_i - b) ∂a∂S=i∑2(yi−axi−b)⋅(−xi)=−2i∑xi(yi−axi−b)
∂S∂b =∑i2(yi−axi−b)⋅(−1)=−2∑i(yi−axi−b) \frac{\partial S}{\partial b} = \sum_i 2(y_i - a x_i - b) \cdot (-1) = -2 \sum_i (y_i - a x_i - b) ∂b∂S=i∑2(yi−axi−b)⋅(−1)=−2i∑(yi−axi−b)

令两个偏导数都等于 0(除以 −2-2 −2):
∑ixi(yi−axi−b)=0⋯(1) \sum_i x_i (y_i - a x_i - b) = 0 \quad \cdots (1) i∑xi(yi−axi−b)=0⋯(1)
∑i(yi−axi−b)=0⋯(2) \sum_i (y_i - a x_i - b) = 0 \quad \cdots (2) i∑(yi−axi−b)=0⋯(2)

展开(把 aa a 和 bb b 移到右边):
∑ixiyi=a∑ixi2+b∑ixi⋯(1′) \sum_i x_i y_i = a \sum_i x_i^2 + b \sum_i x_i \quad \cdots (1') i∑xiyi=ai∑xi2+bi∑xi⋯(1′)
∑iyi=a∑ixi+b⋅n⋯(2′) \sum_i y_i = a \sum_i x_i + b \cdot n \quad \cdots (2') i∑yi=ai∑xi+b⋅n⋯(2′)

两个方程、两个未知数 aa a、 bb b------理论上能解。但还有 5 个累加和散落在公式里,看着乱。下面把它们打包成矩阵。

1.5 写成矩阵形式------正规方程

把 (1')(2') 写成矩阵:
∑xi2 ∑xi ∑xi n a b = ∑xiyi ∑yi \begin{bmatrix} \sum x_i^2 & \sum x_i \\ \sum x_i & n \end{bmatrix} \begin{bmatrix} a \\ b \end{bmatrix} = \begin{bmatrix} \sum x_i y_i \\ \sum y_i \end{bmatrix} ∑xi2∑xi∑xinab=∑xiyi∑yi

关键观察 :左边矩阵的元素恰好等于一个 XTXX^T X XTX,右边向量恰好是 XTyX^T y XTy------其中 设计矩阵
X= x1 1 x2 1 ⋮ ⋮ xn 1 ,y= y1 y2 ⋮ yn X = \begin{bmatrix} x_1 & 1 \\ x_2 & 1 \\ \vdots & \vdots \\ x_n & 1 \end{bmatrix}, \quad y = \begin{bmatrix} y_1 \\ y_2 \\ \vdots \\ y_n \end{bmatrix} X= x1x2⋮xn11⋮1 ,y= y1y2⋮yn

验证 XTXX^T X XTX(一个 2×n2 \times n 2×n 矩阵乘 n×2n \times 2 n×2 矩阵):
XTX= x1 x2 ⋯ xn 1 1 ⋯ 1 x1 1 x2 1 ⋮ ⋮ xn 1 = ∑xi2 ∑xi ∑xi n ✓ X^T X = \begin{bmatrix} x_1 & x_2 & \cdots & x_n \\ 1 & 1 & \cdots & 1 \end{bmatrix} \begin{bmatrix} x_1 & 1 \\ x_2 & 1 \\ \vdots & \vdots \\ x_n & 1 \end{bmatrix} = \begin{bmatrix} \sum x_i^2 & \sum x_i \\ \sum x_i & n \end{bmatrix} \checkmark XTX=x11x21⋯⋯xn1 x1x2⋮xn11⋮1 =∑xi2∑xi∑xin

验证 XTyX^T y XTy( 2×n2 \times n 2×n 矩阵乘 n×1n \times 1 n×1 向量):
XTy= x1 x2 ⋯ xn 1 1 ⋯ 1 y1 y2 ⋮ yn = ∑xiyi ∑yi ✓ X^T y = \begin{bmatrix} x_1 & x_2 & \cdots & x_n \\ 1 & 1 & \cdots & 1 \end{bmatrix} \begin{bmatrix} y_1 \\ y_2 \\ \vdots \\ y_n \end{bmatrix} = \begin{bmatrix} \sum x_i y_i \\ \sum y_i \end{bmatrix} \checkmark XTy=x11x21⋯⋯xn1 y1y2⋮yn =∑xiyi∑yi

所以方程 (1')(2') 恰好就是:
XTX a b =XTy \boxed{X^T X \begin{bmatrix} a \\ b \end{bmatrix} = X^T y} XTXab=XTy

这叫正规方程 。两边左乘 (XTX)−1(X^T X)^{-1} (XTX)−1(前提: XTXX^T X XTX 可逆,见 [§ 六踩坑](#§ 六踩坑 "#%E5%85%AD%E8%B8%A9%E5%9D%91%E6%8F%90%E9%86%92")):
β=(XTX)−1XTy,其中 β= a b \boxed{\beta = (X^T X)^{-1} X^T y, \quad \text{其中 } \beta = \begin{bmatrix} a \\ b \end{bmatrix}} β=(XTX)−1XTy,其中 β=ab

这就是 § 写在前面那行"超简洁"的公式------只是 4 个矩阵乘法的打包写法

1.6 设计矩阵的几何意义

为什么 XX X 要加一列 11 1?

  • XX X 的第 ii i 行 xi,1 x_i, 1 xi,1 乘以 β=a,bT\beta = a, b^T β=a,bT 得到预测值 y^i =axi+b \hat y_i = a x_i + b y^i=axi+b
  • 这一列 11 1 就是截距项 ------没它就只能拟合过原点的直线( y=axy = ax y=ax)

这个模式可以推广------只要模型是"参数的线性组合"就能用正规方程:

模型 设计矩阵 X 的行
y=ax+by = ax + b y=ax+b xi,1 x_i, 1 xi,1
y=ax2+bx+cy = ax^2 + bx + c y=ax2+bx+c xi2,xi,1 x_i\^2, x_i, 1 xi2,xi,1
y=a1x1+a2x2+b y = a_1 x_1 + a_2 x_2 + b y=a1x1+a2x2+b xi,1 , xi,2 ,1 x_{i,1}, x_{i,2}, 1 xi,1,xi,2,1
kk k 阶多项式 xik, xik−1 ,...,xi,1 x_i\^k, x_i\^{k-1}, \\dots, x_i, 1 xik,xik−1,...,xi,1

正规方程对所有"线性模型"都成立------这就是为什么它在机器学习里是"基础方程"。


二、Demo 1:图像去噪(行均值拟合)

应用场景

扫描的旧照片经常有这种系统性行噪点

  • 上面亮、下面暗(扫描灯光不均)
  • 或某些行特别亮(扫描仪传感器问题)

算法

  1. 计算每行像素的平均亮度 → 得到 nn n 个点 (rowi,avg_brightnessi) (row_i, \text{avg\_brightness}_i) (rowi,avg_brightnessi)
  2. 最小二乘拟合: avg_brightness=a⋅row+b\text{avg\_brightness} = a \cdot \text{row} + b avg_brightness=a⋅row+b
  3. 从原图每行减去拟合值 → 去除系统性行噪点
scss 复制代码
原图:                    处理后:
┌──────────────┐          ┌──────────────┐
│ ░░░ 亮       │          │              │
│ ░ 中等       │     →    │   一致亮度    │
│   ▓ 暗       │          │              │
└──────────────┘          └──────────────┘
   (上面亮/下面暗)            (统一了)

三、Demo 2:颜色校准(拍色卡拟合)

应用场景

显示器偏色(偏暖/偏冷)------用标准色卡 (已知 RGB 值)拍照,得到实际显示的 RGB,拟合修正系数。

算法

  1. 准备 nn n 组数据点 (targeti,actuali) (\text{target}_i, \text{actual}_i) (targeti,actuali):每组是 3 维 RGB
  2. 对每通道独立拟合: actual=m⋅target+b\text{actual} = m \cdot \text{target} + b actual=m⋅target+b
  3. 对原图应用反向变换 corrected=(actual−b)/m\text{corrected} = (\text{actual} - b) / m 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\beta = (X^T X)^{-1} X^T y β=(XTX)−1XTy。

代码 1 行库调用xtx.try_inverse() * xty

两个真实应用:图像去噪(行噪点)+ 颜色校准(显示器偏色)


📦 项目地址pixel-math-wasm 🦀 Rust + WebAssembly 实战系列


🏷️ 标签#Rust #WebAssembly #图像处理 #最小二乘 #回归 #矩阵求逆 #nalgebra

相关推荐
Flynt7 小时前
pnpm 12 换上了 Rust 内核,我拿项目实测了一轮构建速度
rust·vite·前端工程化
传奇开心果编程11 小时前
【Xilem基础语法学与练】第8课:条件渲染(one_of)
学习·rust·前端框架
qwsaedca13 小时前
在Mac上跑 Kokoro TTS经验总结
rust·mac·tts
qwsaedca13 小时前
Kokoro TTS v1.1 voices 文件格式逆向分析
rust
老猿讲编程15 小时前
【Eclipse OpenSOVD学习之五】拓扑引擎(Topology)
学习·rust·eclipse·sovd
object not found15 小时前
Nuxt4去掉body中默认的边距
开发语言·后端·rust
梦醒沉醉1 天前
4、Rust参考手册——Crate和源文件
rust
chainbees1 天前
Windows 系统 Rust 运行环境搭建
rust
k4m7v2pz1 天前
从 Python 搬到 Rust:pyglet MIDI DAW 变成 egui 鬼畜采样器的迁移复盘
开发语言·python·rust