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

相关推荐
k4m7v2pz6 小时前
Bevy 0.14.2 玩家精灵不渲染(只有背景在动)排查全记录
macos·rust·bevy
老王生涯8 小时前
rust开发环境配置-Windows & GNU
开发语言·windows·rust
Yeauty8 小时前
自建 HLS 第一问:fMP4 还是 TS?用 Rust 在进程内把两种都跑出来
开发语言·后端·rust
SmalBox8 小时前
【节点】[Comparison节点]原理解析与实际应用
unity3d·游戏开发·图形学
Flynt9 小时前
我用Biome替换ESLint+Prettier在项目上踩了不少坑
rust·eslint·前端工程化
Yeauty10 小时前
2026 年在 Rust 里处理音视频,该走哪条路?
rust·ffmpeg·音视频·视频
TDengine (老段)1 天前
TDengine Go 与 Rust 连接器 — 高性能异步访问
大数据·数据库·物联网·golang·rust·时序数据库·tdengine
魔力女仆1 天前
【RUST AI】把 TTS 搬进浏览器:kokoroi-rs 的 WASM 实践
人工智能·rust·wasm
米尔的可达鸭1 天前
UDP 通信深度实验:从 Windows FastPath 到 ICMP 端口不可达的完整排查
windows·websocket·网络协议·rust·udp·wireshark·tcpdump