简介:这套机器学习预测系统汇总面向希望系统掌握预测建模的学习者与开发者,围绕贝叶斯网络、马尔科夫模型、线性回归、岭回归、多项式回归、决策树回归和深度神经网络七类方法展开,可用于课程设计、模型对比实验与预测类项目入门。压缩包共12个文件,约1.3MB,以Python脚本为主体,配合Excel与CSV示例数据、UI界面文件及docx、md说明文档,既能查看各算法的实现思路,也便于在统一数据上运行和比较结果。已有136人学习,说明其具备一定的参考与复用价值。读者可从中获得从概率图模型、统计回归到深度学习的完整示例链路,借助数据与界面代码快速搭建演示系统,并依据文档理解参数设置、模型差异与适用场景,为后续调参与集成学习打下基础。
1. 七个预测模型塞进一个压缩包:它们共享的其实只有一条流水线
同一份回归数据,喂给线性回归和深度神经网络预测,误差可能差出三倍;同一份序列数据,喂给马尔科夫模型和决策树回归,结论甚至方向相反。这种分歧往往不是调参没调好,而是每个模型的归纳偏置(inductive bias)在数据上留下的指纹。一份把线性回归、岭回归、多项式回归、决策树回归、贝叶斯网络、马尔科夫模型、深度神经网络预测收拢在一起的机器学习预测系统,价值从来不在于模型数量多,而在于它把七种前提假设摆在同一张桌子上:哪些误差来自模型结构,哪些来自数据本身,一眼就能对上号。
认清这套底层机制,等于拿到一张「模型为什么会错」的地图。后面几章沿着这张地图逐层往下拆:先看最小二乘框架下的三种变体如何在数值层面分化,再看两种非线性路线在几何上的本质差别,然后是概率图模型怎么把预测变成一次条件概率查询,最后落到七个模型接进同一套评估流程时最容易出问题的五个位置。
2. 最小二乘的三副面孔:正规方程的病态、岭回归的 λI 与多项式升维的代价
2.1 正规方程解的到底是什么
线性回归只写了一句话的假设:目标值与特征之间是线性关系,误差独立同分布且方差恒定。在这句假设下最小化残差平方和 ||y − Xw||²,对 w 求梯度令其为零,得到 XᵀXw = Xᵀy。这个式子叫正规方程,XᵀX 是 d×d 的 Gram 矩阵。
关键在于:Gram 矩阵的条件数是设计矩阵条件数的平方。设计矩阵条件数 10³ 在一堆量纲不一致的特征里非常常见,平方之后 Gram 矩阵就到 10⁶;float64 的有效十进制位大概 15 到 16 位,平方一下只剩 7 到 8 位可信。所以教科书里那句w = inv(X.T @ X) @ X.T @ y在真实数据上会给出离谱结果,不是公式推错了,是数值路径选错了。
工程上的做法是绕开显式求逆。SVD 把 X 分解成 UΣVᵀ,代入可得 w = VΣ⁻¹Uᵀy,条件数只被放大一次而不是平方。np.linalg.lstsq走的就是这条路。还有一件容易被忽略的事:当特征数 d 大于样本数 n 时 Gram 矩阵必然奇异,正规方程有无穷多组解,lstsq 返回的是其中范数最小的一组——这个细节正是理解岭回归为什么「一定有唯一解」的入口。
2.2 岭回归:λ 加的不是噪声,是给奇异值垫了个地板
把 SVD 代进岭回归的目标函数 ||y − Xw||² + λ||w||²,解出来是这样:
w_ridge = Σᵢ [σᵢ² / (σᵢ² + λ)] · (uᵢᵀy / σᵢ) · vᵢ
方括号里那项是收缩因子。σᵢ 远大于 √λ 的方向,因子接近 1,系数几乎不动;σᵢ 远小于 √λ 的方向,因子接近 0,那个方向承载的信息被整体丢掉。这说明 λ 并不是「把所有权系数均匀压小」,而是按每个奇异值方向的信噪比做选择性放弃。
这就解释了岭回归为什么在共线性严重的数据上格外好用。共线性意味着存在几个极小的奇异值,那些方向上的系数估计方差极大,普通最小二乘会把噪声放大成巨大的系数,符号还会随机翻转;岭回归直接按比例把这些方向压掉,代价是引入偏差。偏差换方差,这就是它的全部逻辑,没有什么玄学成分。
还有一个实现层面的细节:截距项不参与惩罚。如果惩罚截距,模型对 y 的整体平移就不再等变——目标值加一个常数 c,预测本该也加 c,但被惩罚的截距做不到。所以手写岭回归时要把 Gram 矩阵的第 0 个对角元置零;用现成库且开启截距估计时,内部一般已经这么处理了。
λ 怎么定:留一交叉验证在岭回归上存在闭式解,不需要真的训练 n 次。另一个好用的量是有效自由度 trace(X(XᵀX+λI)⁻¹Xᵀ) = Σ σᵢ²/(σᵢ²+λ),它随 λ 单调下降,可以当成「模型实际用掉了几参数」来读,比盯着 λ 的绝对值更有意义。
2.3 多项式回归:换了基,没换模型
多项式回归经常被当成另一类模型来讲,其实它还是线性回归。参数 w 仍然线性地进入预测,变的只是设计矩阵的列:从 [1, x] 换成 [1, x, x², …, x^d]。所以它有闭式解,也能直接套岭回归,前面两节的所有结论原样成立。
真正的麻烦在数值上。x 的各次幂之间高度相关,拼出来的设计矩阵接近 Vandermonde 矩阵,条件数随阶数快速上升。在 [-1, 1] 区间还算温和,一旦特征量纲是 [0, 100],degree 到 6 时 Gram 矩阵条件数能冲到 10¹² 量级,解出来的系数符号在正负之间乱跳,曲线在样本点之间剧烈震荡。这是踩坑重灾区:训练集 R² 是 0.99,测试集一塌糊涂。
解决办法有两条。一条是把 x 中心化再标准化,把数值范围压回 [-1, 1] 附近,简单但有效;另一条是换正交多项式基(Chebyshev、Legendre),让基函数在样本上的内积近似对角,Gram 矩阵接近对角阵。第二条更彻底,因为正交基的系数之间几乎不相关,每个系数都有相对独立的含义,不会出现「改一个系数整条曲线全变」的连锁反应。数值分析里做函数逼近默认用正交基,原因就在这里。
2.4 三条路径写在同一套接口下
import numpy as np def design_linear(x): # 截距列固定放第 0 列,后续所有正则化都跳过它 x = np.asarray(x, dtype=float).reshape(len(x), -1) return np.hstack([np.ones((x.shape[0], 1)), x]) def fit_ols(X, y): # lstsq 走 SVD,不显式构造 X^T X,条件数只被放大一次而不是平方 w, *_ = np.linalg.lstsq(X, y, rcond=None) return w def fit_ridge(X, y, lam): d = X.shape[1] reg = lam * np.eye(d) reg[0, 0] = 0.0 # 截距不惩罚,保证对 y 的平移等变 # 用 solve 而不是 inv:内部走 Cholesky/LU,数值上稳一个量级 return np.linalg.solve(X.T @ X + reg, X.T @ y) def design_poly(x, degree): x = np.asarray(x, dtype=float) # 先中心化再缩放;不做这一步,degree=6 时 Gram 矩阵条件数可达 1e12 z = (x - x.mean()) / (x.std() + 1e-12) cols = [np.ones_like(z)] + [z ** k for k in range(1, degree + 1)] return np.vstack(cols).Tdesign_linear和design_poly返回的都是设计矩阵,形状统一为 (n, d+1),所以fit_ols/fit_ridge可以直接复用,切换模型只是换一个建矩阵的函数。lam建议在 1e-3 到 1e3 之间取对数网格扫,配合交叉验证选点;degree和lam必须联合调,单独把 degree 往上推、λ 却固定不变,等于只放开了容量没加任何约束。
想验证数值问题是否真的存在,加一行np.linalg.cond(X.T @ X)打印出来看:条件数过了 10⁸ 就该考虑换基或者加正则,过了 10¹² 基本可以判定结果不可信。
3. 两种非线性:决策树的轴对齐分裂与神经网络的表示学习差在哪
3.1 回归树的不纯度:方差缩减就是平方误差的再分配
回归树在每个节点上只做一件事:找一个特征和一个阈值,把样本切成两半,让两半内部的平方误差之和最小。父节点的平方误差是 Σ(yᵢ − ȳ)²,分裂后是 SSE_L + SSE_R,两者之差就是这次分裂带来的方差缩减。
为什么回归用方差而不是基尼系数或信息熵:那两者衡量的是类别分布的纯度,前提是标签离散。回归里「多数类」这个概念没有意义,能让平方误差最小的常数预测是均值,所以不纯度自然定义成方差。
由此推出一条常被忽略的结论:叶节点的输出是叶内样本的均值,这个值只在平方损失下才最优。如果业务上更在意绝对误差,叶节点该输出中位数,树对离群点的敏感度会立刻降一个档次。很多实现把这两件事混着用——用 MSE 训练、用 MAE 汇报——然后困惑为什么一个离群点能把整棵树的预测拽偏。
3.2 分裂点搜索:排一次序,前缀和把代价压到 O(1)
对某个特征而言,候选切点只有 n−1 个(排序后相邻样本的中点)。暴力做法是对每个候选重新算左右两边的均值再算 SSE,复杂度 O(n²)。实际实现用前缀和。
核心恒等式是一段样本的平方误差和 = Σy² − (Σy)²/n。只要预处理出 Σy 和 Σy² 的前缀和,任意一段区间的 SSE 都是 O(1)。整体流程于是变成:对特征排序 O(n log n),扫描一遍候选切点 O(n),单个特征 O(n log n),d 个特征 O(d·n log n)。
import numpy as np def best_split(X, y): n, d = X.shape # 父节点 SSE,作为「这次分裂必须让误差下降」的初始门槛 best_sse = float(((y - y.mean()) ** 2).sum()) best = (None, None, best_sse) # (特征下标, 阈值, 子节点 SSE 之和) for j in range(d): order = np.argsort(X[:, j]) xs, ys = X[order, j], y[order] csum = np.cumsum(ys) # Σy 的前缀和 csq = np.cumsum(ys ** 2) # Σy² 的前缀和 k = np.arange(n - 1) # 候选切点:左子节点为 [0..k] nl, nr = k + 1, n - k - 1 sl, sr = csum[:-1], csum[-1] - csum[:-1] ql, qr = csq[:-1], csq[-1] - csq[:-1] # SSE = Σy² - (Σy)²/n,左右两边各算一次再相加 sse = (ql - sl ** 2 / nl) + (qr - sr ** 2 / nr) i = int(np.argmin(sse)) if sse[i] < best[2]: best = (j, (xs[i] + xs[i + 1]) / 2.0, sse[i]) return bestbest_sse的初值取父节点 SSE,作用是让「不降低误差的分裂」直接被拒绝,天然形成一个极简的预剪枝。xs[i]和xs[i+1]是排序后相邻的两个样本,取中点作为阈值是最稳妥的做法——取其中任何一个都会让边界条件变得依赖浮点比较。返回值里带上 SSE,是为了让上层递归能顺便算出这次分裂带来的方差缩减,做后续的代价复杂度剪枝。
3.3 轴对齐分裂的硬边界:分段常数函数的局限
树的预测函数是分段常数,折痕永远平行于坐标轴。真实关系如果是 y = x₁·x₂,等值线是一条条双曲线,树只能用一堆小矩形去拼,想达到固定精度所需的叶子数随维度指数增长。
这就解释了两个常见现象。一是树对特征的单调变换免疫,把 x 换成 log x 结果完全不变,因为分裂只看顺序不看数值;二是树对特征的线性组合极其敏感,把 x₁ 换成 x₁ + x₂ 之后整棵树要推倒重学。这也是特征工程在树模型上依然有效的原因,只是形式和线性模型完全不同——线性模型需要的是让关系变线性,树需要的是让关系变轴对齐。
随机森林和梯度提升能缓解这个限制但不能消除:它们靠集成降低方差,折痕方向依然钉死在坐标轴上。理解了这一点,就不会在明明需要做特征旋转的问题上死磕树模型。
3.4 神经网络:把轴对齐的折痕换成学出来的方向
一个带 ReLU 的多层感知机,输出是输入空间上的分段线性函数。每个 ReLU 单元定义一张超平面,激活就是在超平面两侧取不同的线性片。折痕的法向量由权重矩阵学出来,不再绑定在坐标轴上,所以 y = x₁·x₂ 这种对角结构可以用很少的参数表达。
但「分段线性」这四个字值得停下来想一想:它不是万能光滑函数逼近器,它是一族折痕方向可学习的分段线性函数。
import numpy as np def mlp_forward(x, params): # x: (n, d);params: [(W1, b <p> <a href="https://download.csdn.net/download/qq_51320133/89279594" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>