十年没碰数学了,重新捡起来时发现很多基础都模糊了。机器学习离不开数学,这篇算是给自己补课,尽量写得让普通人也能看懂。
一、开篇:我们要解决什么问题?
想象你是一个健身房的教练,每天都有会员来问:“教练,我体脂率多少?”
你手里有一把体脂秤,但这玩意儿不便宜,也不是每个会员都愿意测。但你发现一个规律:体脂率好像和身高、体重、年龄、性别有关系。
于是你想:能不能找到一个公式,输入这四个数字,就能估算出体脂率?
这就是回归问题的本质——根据已知信息(输入),预测未知结果(输出)。
用数学语言重新描述
- 输入(特征):身高、体重、年龄、性别 → 记作 $x$,是一个包含 4 个数字的向量
- 输出(目标):体脂率 → 记作 $y$,是一个数字
- 我们要找的:一个函数 $f$,让 $f(x)$ 尽可能接近真实的 $y$
但现实中,这个 $f$ 我们不知道是什么。我们只有一堆测量数据:
会员1: 身高175cm, 体重70kg, 25岁, 男 → 体脂率18.5%
会员2: 身高165cm, 体重55kg, 30岁, 女 → 体脂率25.2%
会员3: 身高180cm, 体重80kg, 22岁, 男 → 体脂率15.8%
...这些数据记作 $\{(x_1, y_1), (x_2, y_2), ..., (x_n, y_n)\}$,其中 $n$ 是会员数量。
二、第一个关键问题:为什么数据有误差?
你可能注意到了,即使两个人身高体重年龄性别都一样,体脂率也可能不同。
为什么?
- 测量误差:体脂秤本身有误差(±1-2%)
- 个体差异:有人肌肉多,有人脂肪多
- 隐藏因素:饮食习惯、遗传、运动量等等我们没记录的信息
- 测量时机:早上和晚上测,喝水前后测,结果都不一样
所以,真实情况应该写成:
$$y_i = f(x_i) + \varepsilon_i$$其中:
- $f(x_i)$ 是"真实的体脂率"
- $\varepsilon_i$ 是"噪声"(各种误差和未知因素)
最小二乘法要做的事情,就是从这些带噪声的数据中,找出最可能的 $f$。
三、最小二乘法的核心思想:让误差尽可能小
假设我们猜测函数 $f$ 的形式是:
$$f(x) = \theta_0 + \theta_1 \times \text{身高} + \theta_2 \times \text{体重} + \theta_3 \times \text{年龄} + \theta_4 \times \text{性别}$$这里 $\theta_0, \theta_1, \theta_2, \theta_3, \theta_4$ 是五个未知的参数,我们要通过数据把它们找出来。
怎么找?看预测有多准
对于每个会员 $i$,我们可以算出:
- 预测值:$f(x_i) = \theta_0 + \theta_1 \times x_{i1} + \theta_2 \times x_{i2} + ...$
- 真实值:$y_i$(实际测量的体脂率)
- 误差:$e_i = y_i - f(x_i)$
如果预测准,误差就小;如果预测不准,误差就大。
为什么用"平方"误差?
最小二乘法的名字来源于它的目标:让所有误差的平方之和最小。
$$\text{总误差} = \sum_{i=1}^{n} (y_i - f(x_i))^2$$你可能会问:为什么不直接用误差 $e_i$,而要用平方 $e_i^2$?
三个原因:
1. 正负误差不能抵消
如果直接加误差:
- 会员1:预测18%,实际20%,误差 = -2%
- 会员2:预测22%,实际20%,误差 = +2%
- 总误差 = -2% + 2% = 0%?
看起来很准,但其实两个都预测错了!
用平方就没这个问题:
- 总误差 = $(-2)^2 + 2^2 = 4 + 4 = 8$
2. 平方会"惩罚"大误差
看这两组预测:
| 会员 | 方案A误差 | 方案B误差 |
|---|---|---|
| 1 | 1% | 5% |
| 2 | 1% | 1% |
| 平方和 | 2 | 26 |
方案A比较均衡,方案B有一个离谱的预测。平方误差会让方案B的总分更差,这符合我们的直觉——宁可都差一点,也不要有一个特别离谱的。
3. 数学上好处理
平方函数处处可导,方便用微积分求最优解。这是技术细节,初学者可以先跳过。
四、动手计算:用四个会员的数据找参数
假设我们有这四个会员的数据:
| 编号 | 身高(cm) | 体重(kg) | 年龄 | 性别 | 体脂率(%) |
|---|---|---|---|---|---|
| 1 | 175 | 70 | 25 | 男(1) | 18.5 |
| 2 | 165 | 55 | 30 | 女(0) | 25.2 |
| 3 | 180 | 80 | 22 | 男(1) | 15.8 |
| 4 | 170 | 60 | 28 | 女(0) | 22.1 |
第一步:把数据写成矩阵
为了方便计算,我们把数据排列成表格形式(矩阵):
$$X = \begin{bmatrix} 1 & 175 & 70 & 25 & 1 \\ 1 & 165 & 55 & 30 & 0 \\ 1 & 180 & 80 & 22 & 1 \\ 1 & 170 & 60 & 28 & 0 \end{bmatrix}$$$$y = \begin{bmatrix} 18.5 \\ 25.2 \\ 15.8 \\ 22.1 \end{bmatrix}$$$$\theta = \begin{bmatrix} \theta_0 \\ \theta_1 \\ \theta_2 \\ \theta_3 \\ \theta_4 \end{bmatrix}$$第二步:用公式直接算出最优参数
这里有个神奇的公式(推导过程很复杂,但用起来很简单):
$$\theta = (X^T X)^{-1} X^T y$$翻译成人话就是:
- $X^T$ 是 $X$ 的转置(行列互换)
- $(X^T X)^{-1}$ 是矩阵的逆(类似倒数)
- 最后左乘 $X^T y$
第三步:用 Python 计算
import numpy as np
# 输入数据(第一列是1,代表常数项)
# 为了简洁,这里只使用 4 个数据
X = np.array([
[1, 175, 70, 25, 1], # 常数,身高,体重,年龄,性别
[1, 165, 55, 30, 0],
[1, 180, 80, 22, 1],
[1, 170, 60, 28, 0]
])
# 体脂率
y = np.array([18.5, 25.2, 15.8, 22.1]) # 这里也是 4 个数据(y 的长度必须等于 X 的行数)
# 计算最优参数
theta = np.linalg.inv(X.T @ X) @ X.T @ y
print("找到的参数:", theta)对于数学基本知识薄弱的同学,简单解释一下:
- 为什么 $y$ 的长度必须等于 $X$ 的行数?
我们的模型是:
$$ y_i = \theta_0 + \theta_1 x_{i1} + \theta_2 x_{i2} + \cdots + \theta_d x_{id} + \varepsilon_i $$theta = np.linalg.inv(X.T @ X) @ X.T @ y
X.T:特征矩阵 X 的转置X.T @ X:特征矩阵与其转置的乘积(得到方阵)np.linalg.inv():求矩阵的逆@:Python中的矩阵乘法运算符
这也就是前面提到的公式: $\theta = (X^TX)^{-1}X^Ty$
假设算出来 $\theta = [15.2, -0.05, 0.25, 0.18, -2.5]$
第四步:解读结果
$$\text{体脂率} = 15.2 - 0.05 \times \text{身高} + 0.25 \times \text{体重} + 0.18 \times \text{年龄} - 2.5 \times \text{性别}$$每个系数告诉我们什么?
- $\theta_0 = 15.2$:基础值(当其他都是0时的体脂率)
- $\theta_1 = -0.05$:身高每增加1cm,体脂率降低0.05%(个子高的人体脂率往往低)
- $\theta_2 = 0.25$:体重每增加1kg,体脂率升高0.25%(符合直觉)
- $\theta_3 = 0.18$:年龄每增加1岁,体脂率升高0.18%(年龄大了新陈代谢慢)
- $\theta_4 = -2.5$:男性(编码为1)比女性(编码为0)体脂率低2.5%
验证一下:预测会员1的体脂率
$$\begin{align} \text{预测值} &= 15.2 - 0.05 \times 175 + 0.25 \times 70 + 0.18 \times 25 - 2.5 \times 1 \\ &= 15.2 - 8.75 + 17.5 + 4.5 - 2.5 \\ &= 25.95\% \end{align}$$等等,会员1的真实体脂率是18.5%,我们预测了25.95%,差了7.45%!
这很正常!因为:
- 我们只有4个样本,数据太少
- 真实关系可能不是线性的
- 体脂率受很多我们没测量的因素影响
但这就是最小二乘法的结果——在这4个样本上,没有任何其他参数能让总平方误差更小。
五、进阶:当直线不够用时
上面的模型假设体脂率和各个特征之间是直线关系。但现实往往更复杂。
$$f(x) = \theta_0 + \theta_1 \times \text{体重} + \theta_2 \times \text{体重}^2$$$$f(x) = \theta_0 + \theta_1 \times \sin(x) + \theta_2 \times \cos(x)$$基函数的概念
我们把输入 $x$ 先变换一下,得到新的特征 $\phi(x)$,然后在这些新特征上做线性回归。
例子1:多项式基函数
- 原始输入:$x = [\text{体重}]$
- 变换后:$\phi(x) = [1, \text{体重}, \text{体重}^2, \text{体重}^3]$
例子2:三角基函数
- 原始输入:$x$
- 变换后:$\phi(x) = [1, \sin(x), \cos(x), \sin(2x), \cos(2x), ...]$
虽然模型对原始输入 $x$ 是非线性的,但对参数 $\theta$ 仍然是线性的,所以还能用最小二乘法!
实际例子:拟合复杂曲线
$$y = \frac{\sin(\pi x)}{\pi x} + 0.1x + \text{噪声}$$$$\phi(x) = [1, \sin(0.5x), \cos(0.5x), \sin(x), \cos(x), ..., \sin(7.5x), \cos(7.5x)]$$import numpy as np
import matplotlib.pyplot as plt
# 生成50个训练点
n = 50
x = np.linspace(-3, 3, n)
pix = np.pi * x
y_true = np.sin(pix) / pix + 0.1 * x
y = y_true + 0.05 * np.random.randn(n) # 加噪声
# 构建31个基函数(1个常数项 + 15对sin/cos)
Phi = np.ones((n, 31))
for j in range(1, 16):
Phi[:, 2*j-1] = np.sin(j * 0.5 * x)
Phi[:, 2*j] = np.cos(j * 0.5 * x)
# 最小二乘求解
theta = np.linalg.inv(Phi.T @ Phi) @ Phi.T @ y
# 在1000个点上预测
X_test = np.linspace(-3, 3, 1000)
Phi_test = np.ones((1000, 31))
for j in range(1, 16):
Phi_test[:, 2*j-1] = np.sin(j * 0.5 * X_test)
Phi_test[:, 2*j] = np.cos(j * 0.5 * X_test)
y_pred = Phi_test @ theta
# 画图
plt.figure(figsize=(10, 6))
plt.plot(X_test, y_pred, 'g-', linewidth=2, label='拟合曲线')
plt.plot(x, y, 'bo', markersize=5, label='训练数据')
plt.legend()
plt.title('用三角基函数拟合复杂曲线')
plt.xlabel('x')
plt.ylabel('y')
plt.grid(True, alpha=0.3)
plt.show()运行后你会看到,虽然训练数据只有50个点且带噪声,但拟合曲线非常平滑地捕捉到了真实函数的形状。
六、加权最小二乘:不是所有数据都同等重要
回到健身房的例子。你发现有些数据来自专业的医用体脂仪(误差±0.5%),有些来自便宜的家用体脂秤(误差±2%)。显然,前者更可信。
怎么让模型"重视"可靠的数据?给每个样本加个权重 $w_i$:
$$\text{总误差} = \sum_{i=1}^{n} w_i \times (y_i - f(x_i))^2$$- 可靠的数据:$w_i = 10$(权重大,影响大)
- 不可靠的数据:$w_i = 1$(权重小,影响小)
其中 $W$ 是对角矩阵,对角线上是各个权重 $w_1, w_2, ..., w_n$。
例子:
# 假设前两个样本很可靠,后两个不太可靠
W = np.diag([10, 10, 1, 1]) # 对角权重矩阵
# 加权最小二乘
theta_weighted = np.linalg.inv(X.T @ W @ X) @ X.T @ W @ y这样算出来的参数会更贴近可靠的数据。
七、大数据时代:当数据多到算不动
前面的公式 $\theta = (X^T X)^{-1} X^T y$ 有个问题:需要计算矩阵的逆。
如果有100万个样本,1000个特征,$X^T X$ 是一个 1000×1000 的矩阵,求逆需要几个GB的内存,而且计算慢得要死。
随机梯度下降:一次只看一个样本
换个思路:我们不一次性用全部数据,而是每次随机抽一个样本,朝着误差减小的方向调整参数。
具体步骤:
随机初始化参数 $\theta$(比如全部设为0,或者随机小数)
重复以下步骤:
- 随机选一个样本 $(x_i, y_i)$
- 计算这个样本的预测误差:$e_i = f_\theta(x_i) - y_i$
- 调整参数:$\theta \leftarrow \theta - \varepsilon \times e_i \times \phi(x_i)$
这里 $\varepsilon$ 叫学习率(步长),通常是一个小数,比如 0.01。
直到参数不再变化(收敛)
为什么这样能行?
想象你在一个山谷里,想找到最低点(对应误差最小)。你看不到全局,但能感觉到脚下的坡度。你就沿着下坡的方向走一步,再看脚下的坡度,再走一步……最终会走到山谷底部。
每个样本的误差 $e_i$ 就是局部的坡度。虽然每次只看一个样本,但只要方向大致正确,多走几步也能到达最优点。
Python 实现
# 随机梯度下降求解
def sgd_least_squares(X, y, learning_rate=0.01, max_iter=10000):
n, d = X.shape
theta = np.random.randn(d) * 0.01 # 随机初始化
for iteration in range(max_iter):
# 随机选一个样本
i = np.random.randint(n)
# 计算预测值和误差
pred = X[i] @ theta
error = pred - y[i]
# 更新参数
theta_new = theta - learning_rate * error * X[i]
# 检查是否收敛
if np.linalg.norm(theta_new - theta) < 1e-6:
print(f"收敛于第 {iteration} 次迭代")
break
theta = theta_new
return theta
# 使用SGD
theta_sgd = sgd_least_squares(X, y, learning_rate=0.01)
print("SGD结果:", theta_sgd)
# 对比解析解
theta_exact = np.linalg.inv(X.T @ X) @ X.T @ y
print("解析解:", theta_exact)运行后你会发现,SGD 的结果和解析解非常接近,但计算速度快得多(尤其在数据量大时)。
学习率的选择
- 太大(如 1.0):参数跳来跳去,不收敛
- 太小(如 0.0001):收敛太慢,要迭代很多次
- 刚好(如 0.01):又快又稳
实践中常用学习率衰减:开始大一点(比如0.1),跑几千次迭代后逐渐减小(比如0.01,0.001)。
八、核方法:处理复杂关系的另一种方式
有时候我们不想手动设计基函数,而是让模型"自动"学习复杂的关系。核方法是一个经典技巧。
高斯核函数
$$K(x, c) = \exp\left(-\frac{\|x - c\|^2}{2h^2}\right)$$这个函数有个特点:
- 当 $x$ 接近 $c$ 时,$K(x, c) \approx 1$(很相似)
- 当 $x$ 远离 $c$ 时,$K(x, c) \approx 0$(不相似)
其中 $h$ 叫带宽,控制"相似"的范围。
核模型的思路
对于每个训练样本 $(x_i, y_i)$,我们在 $x_i$ 的位置放一个"钟形曲线"(高斯核),高度是 $\theta_i$。
$$f(x) = \sum_{i=1}^{n} \theta_i \times K(x, x_i)$$直觉理解:
- 如果 $x$ 靠近会员1,那 $K(x, x_1)$ 很大,$\theta_1$ 的影响就大
- 如果 $x$ 远离所有训练样本,所有 $K(x, x_i)$ 都很小,预测值接近0
用 SGD 训练核模型
def gaussian_kernel(x, centers, h=0.3):
"""计算x到所有中心点的高斯核"""
n = len(centers)
K = np.zeros(n)
for i in range(n):
K[i] = np.exp(-((x - centers[i])**2) / (2 * h**2))
return K
# 训练数据
x = np.linspace(-3, 3, 50)
y = np.sin(np.pi * x) / (np.pi * x) + 0.1 * x + 0.05 * np.random.randn(50)
# 用SGD训练
theta = np.random.randn(50) * 0.01
learning_rate = 0.1
for iteration in range(10000):
i = np.random.randint(50) # 随机选样本
# 计算核向量
K_i = gaussian_kernel(x[i], x, h=0.3)
# 预测和误差
pred = K_i @ theta
error = pred - y[i]
# SGD更新
theta = theta - learning_rate * error * K_i
if iteration % 1000 == 0:
total_error = np.mean((y - [gaussian_kernel(x[j], x) @ theta
for j in range(50)])**2)
print(f"迭代 {iteration}, 误差 {total_error:.4f}")这个方法在处理高维、非线性问题时特别有用。
九、几何直觉:投影的魔法
最小二乘法背后有个美妙的几何解释。
把数据想象成空间中的点
每个样本是一个点:
- 输入 $x_i$ 决定点的位置
- 输出 $y_i$ 是这个点在某个方向上的投影
所有可能的预测值 $f(x_i)$ 构成一个子空间(比如一个平面)。
观测值带噪声,偏离了子空间
真实的输出 $\Phi\theta^*$(如果没有噪声)一定在这个子空间里。但实际观测值 $y$ 因为有噪声,偏离了子空间。
最小二乘法做的事
把观测值 $y$ 垂直投影到子空间上,得到 $\hat{y}$。这个投影就是最接近 $y$ 的、在子空间里的点。
$$\hat{y} = \Phi \hat{\theta} = \Phi (\Phi^T \Phi)^{-1} \Phi^T y = \Pi_\Phi y$$其中 $\Pi_\Phi$ 是投影矩阵。
关键洞察:投影操作去掉了垂直于子空间的噪声成分,保留了有用的信号。
十、统计性质:为什么最小二乘法可靠?
无偏性
$$E[\hat{\theta}] = \theta^*$$翻译成人话:虽然每次测量有误差,但多测几次平均一下,估计是准确的。
渐近一致性
即使我们的模型不完美(比如真实关系是非线性的,但我们用了线性模型),只要样本数量足够多,最小二乘估计会收敛到"最优的线性近似"。
十一、实战建议
1. 从简单模型开始
不要一上来就用复杂的模型。先试试线性回归,看看效果如何。如果不够好,再考虑:
- 增加多项式项($x^2, x^3$)
- 使用三角函数基
- 尝试核方法
2. 数据预处理很重要
标准化:把特征缩放到相同范围(比如都在0-1之间)
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_scaled = scaler.fit_transform(X)去除异常值:极端数据会严重影响结果
3. 交叉验证
不要只在训练数据上测试!留出一部分数据(比如20%)作为测试集,看模型在没见过的数据上表现如何。
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
# 在训练集上拟合
theta = np.linalg.inv(X_train.T @ X_train) @ X_train.T @ y_train
# 在测试集上评估
y_pred = X_test @ theta
test_error = np.mean((y_test - y_pred)**2)
print(f"测试误差:{test_error}")4. 可视化很重要
画图能帮你快速发现问题:
- 散点图:看数据分布
- 拟合曲线:看模型效果
- 残差图:看误差分布
import matplotlib.pyplot as plt
# 预测vs真实
plt.scatter(y, y_pred, alpha=0.5)
plt.plot([y.min(), y.max()], [y.min(), y.max()], 'r--')
plt.xlabel('真实值')
plt.ylabel('预测值')
plt.title('预测效果')
plt.show()
# 残差分布
residuals = y - y_pred
plt.hist(residuals, bins=30)
plt.xlabel('残差')
plt.ylabel('频数')
plt.title('残差分布(应该接近正态分布)')
plt.show()十二、总结:最小二乘法的精髓
回到开头的问题:我们想根据身高体重年龄性别预测体脂率。
最小二乘法给了我们一套完整的方案:
- 建模:假设输出是输入的某种函数(线性、多项式、核函数等)
- 训练:找到参数,让预测值和真实值的平方误差最小
- 求解:
- 数据少:用解析解 $\theta = (X^T X)^{-1} X^T y$
- 数据多:用随机梯度下降
- 评估:在新数据上测试,看预测是否准确
这个方法简单、高效、可靠,是机器学习的基石。后续学习的岭回归、LASSO、支持向量机、神经网络,都是在这个基础上发展出来的。
最重要的:动手实践!把上面的代码跑一遍,改改参数看看效果。数学公式只有在代码里跑起来,才会真正变成你的工具。
十三、附录:常见问题
Q1: 为什么公式里有个 $1/2$?
$$J(\theta) = \frac{1}{2}\sum_{i=1}^{n}(y_i - f(x_i))^2$$答:纯粹为了求导方便。求导时平方会带出一个2,除以2之后正好抵消。不影响最优解的位置。
Q2: 矩阵不可逆怎么办?
如果特征之间完全线性相关(比如"身高(cm)“和"身高(m)“同时存在),$X^T X$ 会不可逆。
解决方法:
- 去掉冗余特征
- 用岭回归(加一个小正数到对角线上)
- 用伪逆
np.linalg.pinv()
Q3: 学习率怎么选?
经验值:
- 开始试 0.1
- 如果不收敛,减半:0.05, 0.025, 0.0125…
- 如果收敛太慢,翻倍:0.2, 0.4…
也可以用学习率衰减:
learning_rate = initial_lr / (1 + decay_rate * iteration)Q4: 怎么知道模型拟合得好不好?
看几个指标:
- 均方误差(MSE):$\frac{1}{n}\sum_{i=1}^{n}(y_i - \hat{y}_i)^2$,越小越好
- R²分数:1 - MSE/Var(y),接近1最好
- 可视化:画拟合曲线,看是否符合直觉
Q5: 过拟合怎么办?
如果训练误差很小,但测试误差很大,说明过拟合了。
解决方法:
- 减少特征数量或基函数数量
- 增加训练数据
- 使用正则化(岭回归、LASSO)
- 交叉验证选择模型复杂度
希望这篇文章能帮你真正理解最小二乘法!记住:看懂不如做懂,做一遍胜过看十遍。
十四、知识图谱
最小二乘法
├── 基本形式:min ||Φθ - y||²
├── 解析解:θ = Φ†y = (ΦᵀΦ)⁻¹Φᵀy
├── 加权形式:min ||W^(1/2)(Φθ - y)||²
├── 核方法:将Φ替换为核矩阵K
├── 理论性质
│ ├── SVD分解理解
│ ├── 几何解释(正交投影去噪)
│ └── 统计性质(无偏性)
└── 大规模算法
└── 随机梯度下降(SGD)十五、关键公式总结
| 概念 | 公式 |
|---|---|
| 目标函数 | $J_{LS} = \frac{1}{2}\sum_{i=1}^n(f_\theta(x_i)-y_i)^2$ |
| 正规方程 | $\Phi^T\Phi\theta = \Phi^Ty$ |
| 最小二乘解 | $\hat{\theta}_{LS} = (\Phi^T\Phi)^{-1}\Phi^Ty$ |
| SGD更新 | $\theta \leftarrow \theta - \varepsilon \phi(x_i)(f_\theta(x_i)-y_i)$ |
| 预测投影 | $\hat{y} = \Pi_\Phi y$ |

