多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

多项式拟合正弦曲线:从最小二乘到过拟合的Python实战

多项式拟合正弦曲线:从最小二乘到过拟合的Python实战 简介面向机器学习初学者与课程实验者的多项式拟合正弦曲线完整实现基于Python编写覆盖最小二乘法、正则化2范数惩罚项、梯度下降与共轭梯度优化并通过加入噪声的数据展示过拟合现象及改进方法。资源包共4个文件包含3个带详细注释的.py脚本分别负责数据生成与加噪、梯度下降迭代求解、最小二乘解析求解和1份Word实验报告整体压缩包仅581KB结构精简。报告内完整记录了实验目的、环境配置Windows10/Python3.7/PyCharm、实验步骤、结果分析并针对不同数据量、超参数与多项式阶数进行过拟合对比便于读者对照代码理解从损失函数构建到参数求解的全过程。已有182人学习适合用作机器学习课程设计、实验报告参考以及想从零实现优化算法、深入理解正则项作用的学习者。1. 多项式拟合正弦曲线为什么这个实验是“机器学习入门”绕不开的一道坎很多人在入门机器学习时第一个困惑是算法原理看了好几遍却不知道模型在训练时到底“学”了什么。多项式拟合正弦曲线这个实验恰好把“学习”这个词拉下神坛——它用一套不到两百行的 Python 代码告诉你所谓训练本质上是在一组候选函数里找一条离数据点最近的曲线所谓泛化是这条曲线在没见过的位置也能贴近真实规律。用正弦曲线作为拟合对象是刻意为之它光滑、非单调、形状鲜明低阶多项式拟合出来的是一条“大致走势”高阶多项式拟合出来的是一条“精确穿过每个点却剧烈抖动”的曲线——欠拟合和过拟合这两个机器学习里最重要的概念可以在这一张图上看穿。这套实验适合正在做课程作业的学生也适合想验证自己对最小二乘、正则化和模型选择理解程度的从业者。它需要的只是 numpy、matplotlib 和一点点线性代数基础。2. 先看懂要拟合什么多项式、正弦曲线与最小二乘的数学关系2.1 多项式的“次数”到底意味着什么多项式拟合的目标很简单给定一组采样点 (xi, yi)找一个次数为 d 的多项式让它在这些点上的取值尽量接近 yi。这个多项式一般写成f(x) w0 w1·x w2·x² ... wd·xᵈ这里的 w 是待求的系数也就是“模型参数”。次数 d 决定了这个函数家族的表达能力d1 是一条直线只能表达单调趋势d2 是一条抛物线能表达一个弯d越大函数能拐的弯越多形状越复杂。多项式拟合的核心工作是解出系数 w而对正弦曲线做拟合时d 这个数字会直接决定结果是“太简单”还是“太复杂”。这里要特别提醒一个初学者容易混淆的点多项式函数是关于变量 x 的非线性函数但它关于参数 w 是线性的——也就是说把 x 的各次幂看成特征拟合问题就变成了一个标准的线性回归问题。这个认知非常关键因为后面的最小二乘解法完全建立在“关于参数线性”这个性质上。2.2 最小二乘与正规方程拟合背后的闭式解“离数据点最近”需要一个定量标准。最常见的定义是均方误差把所有拟合值与真实值的差的平方加起来取平均最小化这个目标函数。写成数学形式就是J(w) (1/N) · Σ(yi - f(xi))²最小化这个函数不需要梯度下降迭代因为它的导数是关于 w 的线性方程。求导并令其为 0可以得到一组线性方程组写成矩阵形式就是著名的正规方程w (XᵀX)⁻¹Xᵀy这里的 X 是设计矩阵第 i 行是 [1, xi, xi², ..., xiᵈ]y 是观测值向量。numpy 里一行代码就能解出 w。不过在实现时我一般不用 (XᵀX)⁻¹ 直接求逆而是用伪逆函数 pinv它能处理矩阵奇异或近奇异的情况数值上更稳定后面在避坑部分会展开讲。2.3 为什么选正弦曲线实验设计里的“刻意安排”用正弦曲线做拟合对象不是随便挑的。sin(x) 的性质让这个实验天然具备教学价值第一它是非线性的任何低次多项式都无法完美逼近所以你能看到欠拟合的真实形态第二它在一个周期内先升后降再升形状不是单调的这迫使高阶多项式用大幅度摆动去拟合过拟合现象会非常明显第三正弦函数有一个真实的低维结构你知道“正确答案”是什么这样就能区分误差来自噪声还是来自模型本身的偏差。数据生成这一步通常是自己做采样然后人为加高斯噪声。加噪声是必要的——如果数据完美落在正弦曲线上低阶拟合的效果看上去也不错过拟合的反差就不够强烈。噪声幅度一般取 0.05 到 0.1 之间能观察到清晰的效果太小看不出区别太大会把所有拟合曲线都压得不像话。3. 用 Python 实现多项式拟合从零手写最小二乘到 polyfit 一行出结果3.1 环境与数据准备环境方面只需要 numpy 和 matplotlib这两个是机器学习实验最基础的依赖。如果你在本机还没装好用 pip install numpy matplotlib 或者在 Anaconda 里直接创建环境就行至于具体的 Python 安装教程和环境配置vscode 里装好 Python 插件、在设置里选中解释器路径就可以跑起来不用额外折腾。首先生成实验数据在一段区间内采样加上高斯噪声。import numpy as np np.random.seed(42) # 固定随机种子保证实验可复现 n_samples 12 x np.linspace(0, 2 * np.pi, n_samples) # 在 0 到 2π 之间均匀取 12 个点 y_true np.sin(x) # 真实的正弦曲线 y y_true np.random.normal(0, 0.1, n_samples) # 添加高斯噪声标准差 0.1这段代码里np.linspace 负责在指定区间内均匀采样第三个参数是样本数量。固定随机种子 np.random.seed(42) 这一步很重要——做实验必须可复现不然每次跑出来的图和数值都不一样实验报告也没法写。观测值 y 是真实正弦值加噪声噪声用 np.random.normal 生成0 是均值0.1 是标准差。3.2 手写最小二乘不把一切交给黑匣子虽然 numpy 提供了现成的拟合函数我仍然建议你至少手写一遍正规方程。这个代码能让你真正看到“训练”是在算什么def polyfit_manual(x, y, degree): # 构建设计矩阵 X每一列是 x 的 0 次幂、1 次幂……degree 次幂 X np.vstack([x**i for i in range(degree 1)]).T # 用伪逆求解正规方程 w (X^T X)^{-1} X^T y w np.linalg.pinv(X.T X) X.T y return w # 用 4 次多项式拟合带噪声数据 w_manual polyfit_manual(x, y, degree4) print(拟合系数, w_manual)逻辑说明np.vstack 把 x 的 0 次方到 4 次方按行堆叠转置后得到一个形状为 (12, 5) 的矩阵——12 个样本5 个特征从常数项到 x⁴。np.linalg.pinv 是伪逆比直接算逆矩阵更稳。 是矩阵乘法运算符X.T X 得到的是 5×5 的格拉姆矩阵再与 X.T y 相乘就得到了系数向量。参数说明degree 是多项式次数也是这个函数里唯一需要关心的超参数。degree 太小拟合不够灵活太大数值容易出问题。12 个样本点degree 一般不超过 8超过之后矩阵接近奇异伪逆虽然能出结果但数值已经不可靠了。3.3 用 np.polyfit 快速实现省力但要懂参数含义手写版验证了原理之后实际实验里直接用 numpy 封装好的函数即可# degree4 的多项式拟合返回系数从高次到低次排列 w_polyfit np.polyfit(x, y, deg4) # 生成密集采样点用于绘制光滑的拟合曲线 x_smooth np.linspace(0, 2 * np.pi, 200) y_smooth np.polyval(w_polyfit, x_smooth)逻辑说明np.polyfit 返回的系数顺序与前面手写版相反——最高次项系数在前常数项在最后。np.polyval 是多项式求值函数给定系数和自变量一次算出所有对应函数值。用 x_smooth 这个密集点集画图曲线才会光滑直接用原始 12 个 x 画出来的折线会显得很生硬。参数说明np.polyfit 的第三个参数 deg 就是多项式次数如果只填前两个参数会默认拟合 1 次多项式。它内部用的算法是基于 SVD 的最小二乘解数值稳定性比我手写的那版更好所以日常实验优先用 polyfit 是没问题的。但这个函数的输出是一个系数数组不像 sklearn 里的模型有现成的 predict 方法初学者容易忘了用它配套的 np.polyval 去求值。3.4 可视化欠拟合与过拟合在一张图上显形数据、拟合曲线都准备好了接下来的关键步骤是把它们画在同一张坐标系里import matplotlib.pyplot as plt plt.figure(figsize(8, 5)) plt.scatter(x, y, label带噪声的观测点, colorblack, zorder3) plt.plot(x_smooth, np.sin(x_smooth), label真实正弦曲线, linestyle--, colorgreen) plt.plot(x_smooth, y_smooth, label4 次多项式拟合, colorred) plt.xlabel(x) plt.ylabel(y) plt.legend() plt.title(多项式拟合正弦曲线degree4) plt.savefig(polyfit_deg4.png, dpi150) plt.show()这段代码用 plt.scatter 画原始采样点用两条曲线分别表示真实正弦和拟合结果。black 点和绿色虚线是“基准答案”红色实线是你模型给出的预测三者对比一眼就能看出拟合质量。zorder3 的作用是让散点显示在最上层不被曲线遮住。dpi150 保证导出图片在实验报告里足够清晰。到这一步你已经跑通了最小实验闭环。但只画一张图没有说服力因为你看不出多项式次数的选择对结果的影响。下一章会把这个实验往深度推——对比不同阶数、量化误差、引入验证集和正则化。这些才是“实验报告”里真正能拿分的内容。4. 阶数、噪声与验证集把拟合做到能解释、能比较、能选参4.1 一组对比实验把 1 到 12 次多项式全部跑一遍拟合质量不能靠肉眼判断要靠数值和对比。最常见的做法是在同一个数据集上分别用 1 次到 12 次多项式拟合计算每个模型在训练集上的均方误差然后画一条“误差随阶数变化”的曲线。代码可以这样写train_errors [] degrees range(1, 13) for d in degrees: w np.polyfit(x, y, degd) y_pred np.polyval(w, x) # 在原始训练点上做预测 mse np.mean((y - y_pred) ** 2) # 均方误差 train_errors.append(mse) plt.figure(figsize(8, 5)) plt.plot(list(degrees), train_errors, markero, colorblue) plt.xlabel(多项式次数 degree) plt.ylabel(训练集均方误差 (MSE)) plt.title(训练误差随多项式次数的变化) plt.savefig(train_error_vs_degree.png, dpi150) plt.show()这段代码的要点是对每个 degree 都做一次完整的“训练-预测-评估”循环。mse 是回归任务最常用的误差指标对误差取平方既避免了正负相消又放大了大误差的惩罚。你会看到训练误差随 degree 增大而单调下降degree 越大多项式越灵活它在训练点上的近似能力越强到 degree11 时12 个样本点被一个 11 次多项式完全穿过训练误差趋近于 0。这就是过拟合的数学表现。4.2 验证集与测试集误差的“U 形曲线”才是最关键的图训练误差单调下降不代表模型就好。一个真正有价值的实验必须引入验证集看模型在没见过的数据上表现如何。常见做法是把数据集划分为训练集和验证集在训练集上拟合在验证集上评估误差。更严谨一点可以用 K 折交叉验证但对这个实验来说简单随机划分或留出法就够用。from sklearn.model_selection import train_test_split x_train, x_val, y_train, y_val train_test_split( x, y, test_size0.4, random_state0 ) val_errors [] for d in degrees: w np.polyfit(x_train, y_train, degd) y_val_pred np.polyval(w, x_val) val_mse np.mean((y_val - y_val_pred) ** 2) val_errors.append(val_mse)train_test_split 把 12 个样本分成 7 个训练和 5 个验证test_size0.4 表示验证集占 40%。固定 random_state0 保证每次运行划分方式一致。此时如果把训练误差和验证误差画在同一个图上你会看到一条经典的“U 形”曲线训练误差一路下降验证误差先下降、在某个阶数附近到达最低点然后掉头向上猛增。验证误差的最低点对应的阶数就是在这个数据规模和数据噪声下比较合适的选择。这个现象是整个实验的“灵魂”。它是机器学习入门中被引用最多的一张图也是面试里常被追问的问题——“为什么训练误差低但模型不好”。亲眼见过 U 形曲线比背十遍过拟合的定义都有用。4.3 噪声幅度一个被忽视但影响极大的参数数据生成时的噪声标准差决定了一个拟合问题本身的难度。噪声越大数据点离真实曲线的离散程度越高越容易被高阶多项式“强行穿过”过拟合的 U 形曲线会越明显反之噪声接近 0 时即使高阶多项式也能在样本点之间合理插值过拟合现象会变得难以察觉。你在做这个实验时可以用以下代码对比三档噪声for noise in [0.01, 0.1, 0.5]: y_noisy y_true np.random.normal(0, noise, n_samples) errors [] for d in degrees: w np.polyfit(x, y_noisy, degd) y_pred np.polyval(w, x) errors.append(np.mean((y_noisy - y_pred) ** 2)) plt.plot(list(degrees), errors, labelfnoise{noise}) plt.legend() plt.show()注意这里每个噪声档位都要重新生成一组 y而不是在同一组数据上改标签。np.random.normal(0, noise, n_samples) 的第二个参数是标准差不是方差——如果想用方差表示要取平方。经验上noise0.01 的结果几乎贴着正弦曲线noise0.5 的验证误差曲线会在更小的 degree 处就开始反弹说明高噪声场景下模型更容易过拟合也更需要偏低的模型复杂度。4.4 正则化兜底当高阶数不可避免时该怎么办有些实验要求强制对比高次多项式比如固定 degree9的表现这时候过拟合几乎不可避免。一个标准解法是引入正则化项把优化目标从“只减小误差”改成“误差 惩罚项”惩罚的是系数的大小。岭回归是最常见的实现它的目标函数是J(w) (1/N)Σ(yi - f(xi))² λ·Σwⱼ²sklearn 里可以直接调用from sklearn.linear_model import Ridge from sklearn.preprocessing import PolynomialFeatures from sklearn.pipeline import make_pipeline model make_pipeline( PolynomialFeatures(degree9, include_biasFalse), Ridge(alpha0.01) ) model.fit(x.reshape(-1, 1), y) x_smooth_2d x_smooth.reshape(-1, 1) y_ridge model.predict(x_smooth_2d)PolynomialFeatures(degree9, include_biasFalse) 把单个 x 特征扩展成 9 个多项式特征Ridge(alpha0.01) 是带 L2 惩罚的线性回归。alpha 是正则化强度——alpha 越大系数被压得越小拟合曲线越平滑但逼近能力也越弱。正则化适合“必须用高次多项式但不想让它剧烈摆动”的场景实验报告里把它作为过拟合补救措施来写是很大的加分项。5. 拟合实验避坑手册五个我真实踩过的坑与排查路径5.1 拟合出来的曲线疯狂震荡甚至出现巨大的正负尖峰现象是degree 设为 9 或更高时拟合曲线在采样点之间像发了疯一样上下乱穿数值可以达到几十甚至上百完全不像正弦曲线。原因是典型的数值不稳定。多项式次数升高后设计矩阵 X 的各列之间高度相关XᵀX 的行列式趋近于 0条件数急剧增大直接求逆会放大微小的浮点误差。另一个原因是过拟合本身高阶多项式为了穿过每个样本点被迫在区间边缘做出大摆动来满足边界条件。解决办法有三条路一是降阶这是最直接的二是改用 np.polyfit它内部基于 SVD数值上比手写正规方程稳定三是引入正则化用 Ridge 压制系数大小。排查的时候可以先打印系数你会看到高阶项的系数动辄是 10³ 量级这就是病灶所在。5.2 训练误差接近 0但验证误差大得离谱现象是把 polyfit 的 deg 设为样本数减 1比如 12 个样本设 11训练误差几乎等于 0但换一组新数据预测误差爆炸式上升。原因是模型把训练数据里的噪声也当成了规律记了下来这正是过拟合的定义。要确认这一点把训练误差和验证误差画在同一张图上如果两条曲线在某个节点之后明显分离、误差方向相反就是过拟合的铁证。解决方法是用验证误差曲线的谷底选择阶数如果还需要高次数就加正则化同时增加样本量也能缓解。这个现象是这次实验最核心的知识点也是最值得在实验报告里展开讨论的内容。5.3 噪声标准差设成 0.1但结果有时候好有时候差现象是相同代码、相同参数多次运行后误差数值和曲线形状差异很大尤其是 degree 较高时“好的时候很好坏的时候离谱”。原因是随机种子没有固定。np.random.normal 每次调用都会生成不同的噪声数据变了模型自然就变了。解决方法是在生成数据之前调用 np.random.seed(42)之后所有随机操作包括 train_test_split 里的 random_state都会按确定序列运行。这也是实验报告写作的基本要求——可复现性。实验报告的“环境与参数说明”部分应当写清楚随机种子否则评审老师复现不出来会直接质疑结果。5.4 用 matplotlib 在中文系统上画图出现方框乱码现象是图例和标题里的中文全部变成空心方块导出到实验报告里非常难看严重影响分数。原因是 matplotlib 默认字体不含中文字符。解决方法是在画图脚本顶部显式指定支持中文的字体比如 Windows 上用 SimHeimacOS 上用 Arial Unicode MS或者干脆在画图时不写中文改用英文标签然后在实验报告里用文字说明含义。我的习惯是实验代码里用英文标签报告里用中文描述这样既避免了乱码问题又保持了代码的通用性。5.5 验证集划分不当导致误差曲线剧烈跳动现象是把 12 个样本按顺序划分成前 7 后 5验证误差曲线出现很多毛刺谷底位置不清晰甚至多次运行结果不一致。原因有两个一是样本量太小任何划分方式都会带来方差二是按顺序切分导致训练集和验证集的 x 分布不均匀——比如训练集集中在区间左半边验证集集中在右半边模型看不到完整分布验证误差自然不稳。解决方法是用 train_test_split 而不是手工切片并且设置 stratify 参数虽然回归任务里 stratify 不常用但可以按 x 是否大于中位数分层如果样本量允许做 K 折交叉验证取平均值曲线会平滑很多。对 12 个样本来说更推荐把实验扩充到 4060 个样本再谈划分。6. 把源代码整理成能交的实验报告结构与画图的落地经验代码跑通了、图也画出来了最后一步是把这些整理成一份像样的实验报告。很多学生的通病是把代码整段粘贴进 Word 文档再配一两张图就算交差——这恰恰丢掉了实验报告的核心价值。实验报告是用来证明你理解了这个实验不是用来展示代码长度的。我的建议是章节按这样的顺序组织实验目的一两句话说明要观察什么现象、实验原理多项式拟合的数学形式 最小二乘目标函数 正规方程推导、实验环境Python 版本、numpy/sklearn/matplotlib 版本、实验内容与结果数据生成方式、阶数对比实验、误差曲线、可视化图、讨论与分析欠拟合过拟合的观察结论、噪声影响、正则化效果、附录完整代码和运行说明。画图时注意三个细节第一所有图的横纵坐标必须有标签图例必须和曲线一一对应图片分辨率不低于 150 dpi第二对比实验的图要放在一起比如 degree1、4、11 三条拟合曲线放在同一个图里这样欠拟合和过拟合能直接对照第三误差变化曲线要标注谷底位置这是整篇报告的“论点”没有标注的图等于让读者自己找重点。我印象最深的是自己做这个实验时花了一个多小时纠结为什么 degree11 时曲线会冲到 2000 多后来打印出系数才发现高阶项数值已经失真了而正则化那一节当时没写进报告——现在回头看那才是整篇报告里最有价值的讨论内容。希望这篇笔记能帮你把边界、参数和坑一次看懂少走这几步弯路。本文还有配套的精品资源点击获取
返回列表