共计 3420 个字符,预计需要花费 9 分钟才能阅读完成。
引言
多项式拟合是机器学习中一种简单但强大的回归方法,它通过构造多项式函数来拟合数据点。比如在预测房价时,房屋面积和价格的关系可能不是简单的直线,而是一个曲线。这时候多项式回归就能派上用场了。
但是多项式回归有一个致命弱点——容易过拟合。所谓过拟合,就是模型在训练数据上表现很好,但在新数据上表现很差。这就像是一个学生死记硬背了所有练习题答案,但遇到新题目就不会做了。

验证方法的选择
在评估模型性能时,我们需要把数据分成训练集和验证集。常见的分割方法有:
- 留出法(Hold-out):简单地将数据分为训练集和验证集两部分
- 优点:计算量小
-
缺点:结果受数据分割影响大,特别是数据量少时
-
k 折交叉验证(k-fold Cross Validation):将数据分成 k 份,轮流用 k - 1 份训练,1 份验证
- 优点:充分利用有限数据,结果更稳定
- 缺点:计算量大
对于小数据集(样本量 <1000),k 折交叉验证是更好的选择,因为它能更准确地评估模型性能。
实战代码
首先安装必要的 NuGet 包:
Install-Package MathNet.Numerics
Install-Package MathNet.Numerics.Data.Text
Install-Package ScottPlot
1. 生成多项式特征矩阵
using MathNet.Numerics.LinearAlgebra;
/// <summary>
/// 生成多项式特征矩阵
/// </summary>
/// <param name="x"> 输入向量 </param>
/// <param name="degree"> 多项式次数 </param>
/// <returns> 特征矩阵 </returns>
public static Matrix<double> PolynomialFeatures(Vector<double> x, int degree)
{var features = Matrix<double>.Build.Dense(x.Count, degree + 1);
for (int i = 0; i <= degree; i++)
{features.SetColumn(i, x.PointwisePower(i).ToArray());
}
return features;
}
2. k 折交叉验证实现
using System;
using MathNet.Numerics.LinearAlgebra;
using MathNet.Numerics.Statistics;
/// <summary>
/// 执行 k 折交叉验证
/// </summary>
/// <param name="X"> 特征矩阵 </param>
/// <param name="y"> 目标向量 </param>
/// <param name="k"> 折数 </param>
/// <param name="degree"> 多项式次数 </param>
/// <param name="seed"> 随机种子 </param>
/// <returns> 平均验证误差 </returns>
public static double KFoldCrossValidation(
Matrix<double> X,
Vector<double> y,
int k = 5,
int degree = 2,
int? seed = null)
{var rand = seed.HasValue ? new Random(seed.Value) : new Random();
var indices = Enumerable.Range(0, X.RowCount).ToArray();
// 打乱索引
for (int i = 0; i < indices.Length; i++)
{int j = rand.Next(i, indices.Length);
(indices[i], indices[j]) = (indices[j], indices[i]);
}
var foldSize = (int)Math.Ceiling((double)X.RowCount / k);
var errors = new List<double>();
for (int fold = 0; fold < k; fold++)
{
// 划分训练集和验证集
var valStart = fold * foldSize;
var valEnd = Math.Min(valStart + foldSize, X.RowCount);
var trainIndices = indices.Take(valStart)
.Concat(indices.Skip(valEnd))
.ToArray();
var valIndices = indices.Skip(valStart)
.Take(valEnd - valStart)
.ToArray();
var X_train = X.SubMatrix(trainIndices);
var y_train = y.SubVector(trainIndices);
var X_val = X.SubMatrix(valIndices);
var y_val = y.SubVector(valIndices);
// 训练模型
var weights = X_train.QR().Solve(y_train);
// 计算验证误差
var predictions = X_val * weights;
var error = predictions.Subtract(y_val).L2Norm() / Math.Sqrt(valIndices.Length);
errors.Add(error);
}
return errors.Mean();}
3. 可视化误差曲线
using ScottPlot;
// 准备数据
var degrees = Enumerable.Range(1, 10).ToArray();
var trainErrors = new double[degrees.Length];
var valErrors = new double[degrees.Length];
for (int i = 0; i < degrees.Length; i++)
{var degree = degrees[i];
var X_poly = PolynomialFeatures(x, degree);
// 训练误差
var weights = X_poly.QR().Solve(y);
var predictions = X_poly * weights;
trainErrors[i] = predictions.Subtract(y).L2Norm() / Math.Sqrt(X_poly.RowCount);
// 交叉验证误差
valErrors[i] = KFoldCrossValidation(X_poly, y, k: 5, degree: degree, seed: 42);
}
// 绘制图表
var plt = new Plot(800, 600);
plt.AddScatter(degrees.Select(d => (double)d).ToArray(), trainErrors,
label: "训练误差", color: System.Drawing.Color.Blue);
plt.AddScatter(degrees.Select(d => (double)d).ToArray(), valErrors,
label: "验证误差", color: System.Drawing.Color.Red);
plt.XLabel("多项式次数");
plt.YLabel("均方根误差");
plt.Legend();
plt.SaveFig("error_curve.png");
避坑指南
- 特征缩放的重要性
- 高阶多项式会产生极大 / 极小的特征值
-
建议对特征进行标准化:
var x_scaled = (x - x.Mean()) / x.StandardDeviation(); -
随机种子的影响
- 交叉验证结果会受到数据分割的影响
-
设置固定随机种子 (如上面的 seed:42) 可以确保结果可复现
-
小样本量的处理
- 当样本量 <100 时,建议:
- 使用留一法(LOO),即 k 等于样本量
- 增加交叉验证重复次数
- 考虑使用更简单的模型
思考题
- 如何实现早停策略自动选择最优多项式次数?
-
可以监控验证误差,当其开始上升时停止增加多项式次数
-
岭回归与交叉验证在防治过拟合上各有什么优缺点?
- 岭回归通过正则化直接约束模型复杂度
- 交叉验证通过验证集间接评估模型泛化能力
- 两者可以结合使用效果更佳
希望这篇教程能帮助你掌握多项式拟合的核心技巧。记住,模型不是越复杂越好,找到偏差和方差的最佳平衡点才是关键。
正文完
