news 2026/10/3 13:45:20

多项式拟合正弦曲线:机器学习入门实验,理解过拟合与正则化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多项式拟合正弦曲线:机器学习入门实验,理解过拟合与正则化

简介:这份资源面向机器学习初学者与课程实验学习者,围绕多项式拟合正弦曲线这一经典课题,提供完整的Python实现与实验报告。内容涵盖最小二乘法解析解、带2范数惩罚项的正则化优化、梯度下降与共轭梯度法的手写实现,并引导读者通过调整数据量、超参数与多项式阶数,直观理解过拟合现象及其克服方法。资源包共4个文件,包含3个py源码与1份doc实验报告,压缩包约581KB,源码按数据生成、最小二乘求解、梯度下降等模块拆分,注释详尽,便于对照实验报告逐步复现。目前已有183人学习下载。读者可借此掌握从数据加噪、模型拟合到优化求解的完整流程,理解无正则与有正则损失函数的差异,并学会在不依赖自动微分框架的前提下自行推导梯度、编写迭代优化代码,适合作为课程作业参考或自学练手材料。

1. 多项式拟合正弦曲线:为什么它是机器学习入门最值得亲手跑一遍的实验

多项式拟合正弦曲线,是很多人翻开《机器学习》或吴恩达课程后遇到的第一个"能看见结果"的实验。它不依赖真实数据集,不需要爬虫,也不用配置复杂环境,只要 Python 加 NumPy、Matplotlib 就能跑通。但恰恰是这个小实验,把机器学习里最核心的几个概念——模型容量、欠拟合、过拟合、训练集与验证集、正则化——全部串在了一起。正弦曲线本身是光滑的周期函数,用多项式去逼近它,阶数低了拟合不上,阶数高了在两端疯狂震荡,这种"肉眼可见的翻车"是任何教科书插图都替代不了的。这篇笔记面向的是想真正动手复现一遍的读者:从环境搭建、数据生成、模型训练,到画出对比图、看懂误差曲线,再到把代码整理成一份能交的实验报告。如果你正在准备机器学习期末复习,或者刚学完 Python 想找个练手项目,这个实验的性价比非常高。

2. 先把数学讲透:多项式拟合正弦曲线到底在拟合什么

2.1 从泰勒展开到最小二乘:多项式为什么能逼近正弦

正弦函数在数学上可以展开成无穷级数,去掉高阶项之后就是一个多项式。这给了我们一个直觉:只要阶数足够,多项式能在一定区间内逼近正弦曲线。但机器学习里的做法不是去算泰勒系数,而是把问题转化成"给定一批带噪声的观测点,找一组多项式系数让预测值和真实值差距最小"。

形式化地,设多项式为:

y = w0 + w1·x + w2·x² + ... + wM·x^M

其中 M 是阶数,w 是待求系数。我们手上有一批训练样本 (x_i, t_i),t_i 是带噪声的目标值。目标是最小化平方误差:

E(w) = 1/2 · Σ (y(x_i, w) - t_i)²

这是一个标准的线性最小二乘问题。虽然它对 x 是非线性的(因为有 x²、x³),但对系数 w 是线性的,所以可以直接求解析解,也可以用梯度下降。解析解的形式是:

w = (XᵀX)⁻¹ Xᵀt

其中 X 是范德蒙德矩阵,每一行是 [1, x_i, x_i², ..., x_i^M]。这个公式在阶数不高、数据量不大时非常稳定,也是很多入门代码直接用的方式。

理解这一点很关键:多项式拟合正弦曲线不是"神经网络",它没有隐藏层、没有激活函数,本质是一个线性回归模型套了一层特征变换。你把 x 扩展成 [1, x, x², ..., x^M],剩下的就是普通线性回归。这也是为什么它适合入门——所有复杂度都来自"特征工程"(选阶数),而不是模型结构本身。

2.2 阶数 M 是唯一的超参数:欠拟合与过拟合的分界线

在这个实验里,唯一需要你手动决定的超参数就是阶数 M。M 太小,模型表达能力不够,曲线太平,拟合不上正弦的起伏,这叫欠拟合。M 太大,模型开始记住训练数据里的噪声,曲线在数据点之间剧烈震荡,尤其在区间两端会飞出天际,这叫过拟合。

常见的观察结果是:M=0 和 M=1 明显欠拟合,M=3 已经能大致跟上正弦的形状,M=9 在训练点上几乎完美但两端开始翘,M=15 以上基本就是灾难现场。这个"从欠拟合到刚好再到过拟合"的过程,是理解模型容量最直观的方式。

但要注意,过拟合不是单纯由 M 决定的,它和训练样本数量也强相关。同样的 M=9,如果只有 10 个训练点,过拟合会非常严重;如果有 100 个点,可能看起来还不错。所以这个实验真正想让你体会的是:模型复杂度要和数据量匹配。

2.3 训练误差下降不代表模型变好:验证集的作用

如果你只画训练误差随 M 变化的曲线,会发现它几乎单调下降——M 越大,训练误差越小,M 足够大时甚至能降到接近零。但这不代表模型变好了,只代表它把训练数据背下来了。

正确做法是留出一部分数据作为验证集(或测试集),同时观察训练误差和验证误差。典型曲线是:训练误差一直降,验证误差先降后升,那个最低点对应的 M 就是相对合理的阶数。这条"U 形曲线"是机器学习里最经典的图之一,也是这个实验报告里必须呈现的内容。

在实际写代码时,我一般会生成两批数据:一批用于训练,一批用于验证,两批都加同样的高斯噪声。然后对每个 M 分别训练、分别计算两边的均方误差,最后画在一张图上。

3. 用 Python 从零跑通:数据生成、模型训练与可视化

3.1 环境准备与依赖安装

这个实验对环境的依赖非常轻,只需要 Python 3.8 以上,加上 NumPy 和 Matplotlib。如果你还没装 Python,去官网下载安装包即可,安装时记得勾选"Add Python to PATH"。装好之后,在命令行里执行:

pip install numpy matplotlib

如果你用 VS Code,建议同时装一下 Python 扩展,这样可以直接在编辑器里运行和调试。不需要 sklearn,也不需要 PyTorch,这个实验用纯 NumPy 就能完成,反而更能看清每一步在做什么。

提示:如果你后续想对比 sklearn 的 PolynomialFeatures + LinearRegression,也可以装 scikit-learn,但建议先用 NumPy 手写一遍,理解矩阵构造过程。

3.2 生成带噪声的正弦数据

先写数据生成部分。核心是:在 [0, 2π] 区间内均匀取点,计算 sin 值,再加上高斯噪声。

import numpy as np import matplotlib.pyplot as plt # 固定随机种子,保证每次运行结果一致 np.random.seed(42) # 生成训练数据:20 个点,均匀分布在 [0, 2π] N_train = 20 x_train = np.linspace(0, 2 * np.pi, N_train) # 真实正弦值 + 高斯噪声(均值 0,标准差 0.3) t_train = np.sin(x_train) + np.random.normal(0, 0.3, N_train) # 生成验证数据:另外 20 个点,位置稍微错开 N_val = 20 x_val = np.linspace(0, 2 * np.pi, N_val) + 0.05 t_val = np.sin(x_val) + np.random.normal(0, 0.3, N_val) # 用于画真实曲线的密集点 x_plot = np.linspace(0, 2 * np.pi, 200) y_plot = np.sin(x_plot)

这段代码里有两个参数值得注意。一个是噪声标准差 0.3,它决定了数据点偏离正弦的程度;噪声越大,过拟合越容易发生。另一个是训练点数量 20,这个数量对 M=9 来说偏少,正好能制造出明显的过拟合现象。如果你想观察数据量对过拟合的影响,可以把 N_train 改成 100 再跑一次,对比 M=9 的曲线形状。

验证集的 x 坐标我特意加了 0.05 的偏移,避免和训练点完全重合,这样验证误差更能反映模型在"没见过的位置"上的表现。

3.3 构造范德蒙德矩阵并求解系数

接下来是核心部分:给定阶数 M,构造特征矩阵并求系数。

def fit_polynomial(x, t, M): """ 用最小二乘法拟合 M 阶多项式 x: 输入坐标,形状 (N,) t: 目标值,形状 (N,) M: 多项式阶数 返回: 系数向量 w,形状 (M+1,) """ # 构造范德蒙德矩阵,每一列是 x 的 0 到 M 次幂 X = np.vander(x, M + 1, increasing=True) # 最小二乘解析解:w = (X^T X)^(-1) X^T t # 用 lstsq 比直接求逆更稳定 w, _, _, _ = np.linalg.lstsq(X, t, rcond=None) return w def predict_polynomial(x, w): """用求得的系数预测""" M = len(w) - 1 X = np.vander(x, M + 1, increasing=True) return X @ w

这里用np.linalg.lstsq而不是手动求逆,是因为当 M 较大时,XᵀX 可能接近奇异,直接求逆数值不稳定。lstsq内部用 SVD 分解,能给出更可靠的结果。np.vander的increasing=True表示列的顺序是从 x⁰ 到 x^M,和我们的公式一致。

如果你手动实现求逆版本,可以写成w = np.linalg.inv(X.T @ X) @ X.T @ t,但在 M=15 以上时可能会看到明显的数值误差,甚至报奇异矩阵错误。这也是一个值得在实验报告里提一句的坑。

3.4 画出不同阶数的拟合曲线对比图

有了拟合和预测函数,就可以批量跑不同阶数并画图了。

fig, axes = plt.subplots(2, 2, figsize=(12, 8)) M_list = [1, 3, 9, 15] for ax, M in zip(axes.ravel(), M_list): w = fit_polynomial(x_train, t_train, M) y_pred = predict_polynomial(x_plot, w) ax.scatter(x_train, t_train, color='red', s=20, label='训练数据') ax.plot(x_plot, y_plot, 'g--', label='真实 sin(x)') ax.plot(x_plot, y_pred, 'b-', label=f'M={M} 拟合') ax.set_ylim(-2, 2) ax.legend() ax.set_title(f'多项式阶数 M={M}') plt.tight_layout() plt.show()

运行后你会看到四张子图。M=1 是一条直线,完全跟不上正弦;M=3 已经能看出正弦的轮廓;M=9 在训练点上贴合得很好,但两端开始有轻微上翘;M=15 则在两端剧烈震荡,甚至超出了 y 轴范围。这个对比图是实验报告里最有说服力的部分。

set_ylim(-2, 2)是为了让四张图尺度一致,方便对比。如果你发现 M=15 的曲线飞出太远,可以适当放宽范围,但那样其他图就看不清了,所以固定范围更好。

3.5 计算训练误差与验证误差并画 U 形曲线

最后一步是把误差量化,画出误差随 M 变化的曲线。

def mse(x, t, w): """均方误差""" y_pred = predict_polynomial(x, w) return np.mean((y_pred - t) ** 2) M_range = range(0, 16) train_errors = [] val_errors = [] for M in M_range: w = fit_polynomial(x_train, t_train, M) train_errors.append(mse(x_train, t_train, w)) val_errors.append(mse(x_val, t_val, w)) plt.figure(figsize=(8, 5)) plt.plot(M_range, train_errors, 'o-', label='训练误差') plt.plot(M_range, val_errors, 's-', label='验证误差') plt.xlabel('多项式阶数 M') plt.ylabel('均方误差') plt.yscale('log') # 对数坐标,因为误差跨度大 plt.legend() plt.grid(True) plt.show()

这张图通常会呈现:训练误差随 M 增大持续下降,验证误差先降后升,在 M=3 到 M=6 之间有一个低谷。yscale('log')是为了让误差跨度大的时候也能看清趋势,如果你觉得对数坐标不直观,也可以改成线性坐标,但 M=15 的验证误差可能会把其他点压扁。

到这里,一个完整的多项式拟合正弦曲线实验就跑通了。代码总量不到 80 行,但覆盖了数据生成、模型定义、训练、评估、可视化全流程。

4. 避坑与排查:这个实验里最容易翻车的 5 个地方

4.1 现象:M=15 时程序报奇异矩阵错误或系数异常大

原因:直接用np.linalg.inv(X.T @ X)求逆时,高阶范德蒙德矩阵的条件数非常大,XᵀX 接近奇异,数值误差被放大。

解决:改用np.linalg.lstsq,它内部用 SVD 分解,对病态矩阵更鲁棒。如果坚持用求逆,可以加一个很小的正则项,也就是岭回归的思路:w = inv(X.T @ X + λI) @ X.T @ t,λ 取 1e-6 到 1e-3 之间。

4.2 现象:每次运行结果都不一样,曲线形状变化很大

原因:没有固定随机种子,每次生成的噪声不同,训练数据就不同。

解决:在生成数据前加np.random.seed(42)。如果你想让实验报告更严谨,可以跑多次取平均误差,但入门阶段固定种子就够了。

4.3 现象:验证误差曲线一直下降,没有出现 U 形

原因:可能是验证集和训练集来自同一批点,或者验证集太小、噪声太低。也可能是 M 的范围不够大,还没进入过拟合区域。

解决:确认验证集的 x 坐标和训练集不完全重合;把 M 范围扩大到 20 甚至 25;适当增大噪声标准差,让过拟合更容易出现。

4.4 现象:M=9 的曲线看起来很好,但 M=15 也没有明显过拟合

原因:训练点数量太多,比如 N_train=100,此时 M=15 相对数据量来说并不算高。

解决:这其实不是 bug,而是说明模型复杂度和数据量要匹配。如果你想看到明显过拟合,把 N_train 降到 10 到 15 之间再试。

4.5 现象:画图时中文显示成方框

原因:Matplotlib 默认字体不支持中文。

解决:在画图前设置字体,例如plt.rcParams['font.sans-serif'] = ['SimHei'],或者直接把图里的标签改成英文。如果是在 Linux 或 Mac 上,SimHei 可能不存在,可以换成Arial Unicode MS或DejaVu Sans。

5. 把实验写成报告:结构、图表与一个进阶技巧

一份能交的机器学习实验报告,核心不是代码有多长,而是能不能把"现象—原因—结论"这条线讲清楚。我一般会按这个结构组织:实验目的、实验原理、实验环境、实验步骤、实验结果与分析、实验结论。其中"实验结果与分析"是重点,要放三张图:不同阶数的拟合曲线对比图、训练误差与验证误差随 M 变化的曲线图、以及一组最优 M 下的拟合效果图。

在写分析时,不要只写"M=3 最好",要写出依据:验证误差在 M=3 时取得最小值,同时训练误差和验证误差差距不大,说明没有明显过拟合。再对比 M=15,训练误差很小但验证误差很大,两者差距悬殊,说明模型记住了噪声。这种"用数据说话"的写法,比单纯描述曲线形状更有说服力。

如果你想让报告更有深度,可以加一个进阶实验:固定 M=9,改变训练样本数量(比如 10、20、50、100),观察验证误差的变化。你会发现,随着数据量增加,同一个 M 的过拟合程度会减轻。这个实验能直观说明"数据量是抵抗过拟合的第一道防线"。

另一个值得尝试的技巧是加 L2 正则化。在损失函数里加一项 λ·||w||²,然后观察不同 λ 下 M=9 的拟合曲线。λ 很小时曲线仍然震荡,λ 适中时曲线变得平滑,λ 很大时曲线又变得过于平坦。这个对比能帮你理解正则化系数的作用,也是很多机器学习课程里的经典练习。

我自己做这个实验时,最大的教训是:一开始只盯着训练误差看,觉得 M 越大越好,结果画出来的曲线在两端飞得离谱,还以为是代码写错了。后来才明白,训练误差小不等于模型好,验证集才是照妖镜。这个习惯后来一直跟着我——不管做什么模型,先留验证集,再看训练误差和验证误差的差距。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 13:28:48

【LSSVM回归预测】基于matlab蝙蝠算法优化最小二乘支持向量机BA-LSSVM回归预测【含Matlab源码 109期】

⛄一、运行结果 ✅博主简介:热爱科研的Matlab仿真开发者,修心和技术同步精进,Matlab项目合作可私信。 🍎个人主页:海神之光 🏆代码获取方式: 海神之光Matlab王者学习之路—代码获取方式 ⛳️座右铭:行百里者,半于九十。 更多Matlab仿真内容点击👇 Matlab图像处理…

作者头像 李华
网站建设 2026/10/3 13:27:10

CogImageFileTool深度解析:VisionPro图像数据流枢纽

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华