恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
自定义找规律任务:用简单神经网络理解监督学习与泛化
首页
资讯中心
/
自定义找规律任务:用简单神经网络理解监督学习与泛化
自定义找规律任务:用简单神经网络理解监督学习与泛化
发布时间:2026/10/21 19:29:21
这篇是深度学习系列学习的第四篇。前几篇我们搭过环境、写过最基础的线性层、也手动调过激活函数今天来玩一个所有初学者都值得亲手做一遍的实验不碰现成的图像数据集而是自己定义一个找规律任务再用最朴素的深度神经网络把它找出来。这个任务小到几十秒就能跑完却能把监督学习里最重要的一些概念——训练、泛化、过拟合、调参——全部串起来。适合刚学完神经网络基础、想动手建立直觉的同学也适合已经能跑通现成教程、但总觉得缺了点“掌控感”的人。1. 为什么要自己造“找规律”任务而不是用现成数据集1.1 机器学习找规律和人类找规律有什么不一样先看一串数字112358……正常人不出两秒就能看出来每一项是前两项之和这就是斐波那契数列。人类找规律的方式是归纳先看样本、猜规则、再拿新样例验证。机器找规律则是另一条路它不会抽象出“斐波那契”这样的符号化规则而是在一个预设的函数家族里通过数据去调整参数让函数输出逼近真实答案。用专业一点的说法我们把一个找规律问题建模成输入x到输出y的映射假设存在某个函数 f(x; θ)其中θ是待学习参数。机器要做的事情就是根据一组(x, y)样本找到一组θ让 f(x; θ) 在所有样本上尽可能接近真实的 y。这个过程在机器学习里就是典型的监督学习在深度学习里就是神经网络的训练。所以“找规律”不是一个零散的比喻它本质上就是函数拟合。这也引出一个关键区别人类找规律时函数家族是开放的我们可以在脑子里随意蹦出“递推”或者“平方”这种想法而机器完全不同它只能在给定的函数家族里搜索。如果你的函数家族里压根没有能表达真实规律的函数——比如用一个线性模型去学分段规律——那无论怎么调参、怎么加数据它也找不到。这件事在后面会反复出现。1.2 自己造任务的四个理由现在很多入门教程一上来就让你跑现成的图像识别数据集训练几百个轮次、改几行代码然后等结果。说实话跑完之后你对学习过程的理解依然很模糊因为那些数据集有三个明显问题。第一反馈周期太长。图像数据一轮训练就要几十秒甚至几分钟你想观察一次改动的影响得盯着屏幕等好久新手很难建立“动作-结果”之间的因果直觉。第二结果难追踪。分类错了你不知道是数据标注的问题、模型结构的问题还是学习率的问题排查难度很大。第三真实规律不可见。猫和狗的区分规律到底是什么样子模型学到的是耳朵还是胡须你看不出来只能通过准确率间接猜测。自己造任务恰恰能绕开这三个问题。规律是你亲手写进代码里的模型有没有学到、学到什么程度一眼就能看穿。数据量可以控制在几千个点几十秒就能跑完一轮实验。最关键的是你可以随意修改规律本身的复杂度、噪声强度、数据分布观察模型行为如何跟着变化这种“对着显微镜做实验”的感觉是刷十个现成数据集都换不来的。1.3 今天要解决的两个任务为了让整个过程有递进我准备了两类规律。任务A是热身题y 2x 1。线性映射没什么好说的跑通流程用。任务B才是真正带点“找规律”味道的核心题我设计了一个分段函数当 x 0 时y x² - 1 当 x ≥ 0 时y 3x 2。这是一条在零点附近有明显形状变化的曲线左侧是抛物线右侧是一条斜线。人类一眼就能看出“哦这是分段规律”但一个线性模型永远学不会它必须靠神经网络里的隐藏层和激活函数来逼近。为了让实验更接近真实世界的规律我还会往数据里加一点随机噪声这相当于给规律抹上一层灰看模型还能不能把它扒出来。选择分段函数而不是更复杂的周期函数原因很简单它的规律足够直观但又不是简单线性正好能展示神经网络相对传统线性模型的优势又不会让问题难到需要上卷积、注意力这些超出当前阶段的东西。一个只有三百多个参数的小网络就能把这条曲线大致还原出来。2. 核心实现拆解数据、网络与训练配置2.1 写一个能控制噪声的数据生成器自己构造任务第一步就是造数据。我习惯用均匀分布在目标区间内随机采样而不是生成等间隔的序列。原因有两点均匀随机采样更接近现实场景中样本的分布而且采样点之间没有“隐藏的排序信息”能避免模型无意中利用输入顺序。等间隔生成虽然画图好看但会让模型对密集区拟合过头稀疏处却几乎没有样本。数据生成的核心逻辑很简单核心代码就这几行import numpy as np def make_data(n2000, noise0.05, seed42): rng np.random.default_rng(seed) x rng.uniform(-3, 3, sizen) y np.where(x 0, x**2 - 1, 3*x 2) y y rng.normal(0, noise, sizen) return x.astype(np.float32), y.astype(np.float32)这里的关键在np.where它一次性把分段规则写清楚了x小于0的时候走抛物线分支否则走直线分支。rng.normal则是往规律上叠加噪声噪声的标准差是0.05相对y的量级来说是很小的扰动。为什么要加噪声因为真实世界的规律几乎不会干净到每个点都精确落在曲线上测量误差、环境干扰、人为偏差都会让数据偏离理想规律。如果数据完全没有噪声模型确实能近乎完美地拟合但那只是一种理想状态。有噪声以后模型的目标就不是“记住每个点”而是“透过噪声看规律”这就逼着你思考泛化和过拟合的问题。2.2 标准化被99%的教程跳过却影响很大的一步很多人在自己造数据实验时第一步就踩坑数据范围对不上模型怎么都训不收敛。这里说的就是特征尺度问题。看这个分段规律x的取值范围是[-3, 3]y的取值范围大约是[-1, 11]。两者量级差异虽然不算离谱但已经足够干扰梯度下降的方向。你可以这么理解如果拿两个不同量纲的指标去算距离数值大的那个会完全主导结果小数值指标几乎被淹没。神经网络的优化也有类似逻辑输入和目标的尺度不一致会让损失曲面变得非常扁长梯度更新就会走得很别扭。解决办法就是标准化。我通常把x和y都做一次零均值、单位方差的变换x (x - x.mean()) / x.std() y (y - y.mean()) / y.std()这样处理之后x和y都大致落在[-2, 2]附近数值尺度统一了训练过程就稳很多。注意这里的y也做了标准化很多人只会标准化输入却忘了输出。如果输出不标准化MSE的数值会很大反向传播时梯度也偏大容易导致训练震荡。做这一步还有一个额外的好处训练结束后打印出来的loss可以直观地理解因为y的方差被压到1附近loss从1往下降降到0.01以下基本就说明拟合得不错了。标准化有一点要注意严谨的做法是用训练集的均值、标准差来标准化训练集和测试集避免把测试集的信息泄漏进训练过程。不过在数据量两千个点的小实验里用全量统计量去做标准化影响几乎可以忽略。我在代码里为了简洁用了全量统计量但你自己写工程化的代码时还是应该养成只用训练集统计量的习惯。2.3 网络要多宽多深321个参数为什么够用对于这种低维输入、低维输出的找规律任务网络结构完全不需要复杂。我用的是一个最简单的多层感知机import torch.nn as nn model nn.Sequential( nn.Linear(1, 16), nn.ReLU(), nn.Linear(16, 16), nn.ReLU(), nn.Linear(16, 1) )这个网络只有三个线性层而且中间只用了16个神经元但参数量可以算一下第一层有1×16个权重加上16个偏置共32个参数第二层是16×16的权重加上16个偏置共272个参数第三层是16×1个权重加1个偏置共17个参数。加在一起只有321个参数。用321个参数去逼近一个分段函数这在理论上完全可行因为ReLU网络本质上是在做“分段线性逼近”。每一段线性函数可以由若干个ReLU基元的组合拼出来16个神经元足以还原一条在零点处有弯折的曲线。这也能解释一个初学者普遍的疑问深度学习是不是一定需要很大的网络答案是看任务复杂度很多小实验根本不需要大模型大模型反而更容易产生优化困难或过拟合。还有一点很多人容易忽略多层线性层堆叠在一起如果不加激活函数数学上仍然等价于一个线性层。比如Linear→Linear→Linear系数矩阵相乘之后还是线性变换模型永远学不会分段函数。只有插入了ReLU这种非线性激活函数网络才真正获得表达非线性规律的能力。这个点我在实践里见过太多次了有人在代码里漏写了ReLU然后怎么调学习率都救不回来因为模型结构从一开始就不具备表达目标规律的能力。2.4 损失函数、优化器和学习率怎么选训练配置这部分我尽量给出一套省心的默认组合再解释为什么是它。任务是在连续值上做预测所以损失函数用均方误差MSE这个没有任何悬念。优化器我建议用Adam学习率设为0.01。Adam对学习率的敏感度比SGD低自带自适应调整在这个小实验里基本一把过。如果你偏要用SGD加动量也不是不行但学习率要降到0.01以下才稳不然前期loss会上蹿下跳。批次大小我习惯设为128。数据总共2000个点分16批左右每轮能看到多次参数更新收敛速度比全量梯度快又比极端小的batch稳定。轮数设300轮对小实验来说足够多了后期loss的下降速度已经明显变缓。这些参数不是拍脑袋定的都是基于这个任务的特点样本量小、维度低、规律简单。如果你把任务换成更复杂的图像分类这套配置就得改但作为理解训练过程的最小实验它恰好是那个“不用怎么调就能工作”的组合。3. 完整代码与训练过程实录3.1 最小可运行版几十行代码从数据到预测把前面所有片段拼起来就是一份可以直接复制运行的最小实现。我这里保留画图部分因为可视化对判断模型是否学到规律非常重要。import numpy as np import torch import torch.nn as nn import matplotlib.pyplot as plt torch.manual_seed(42) def make_data(n2000, noise0.05): rng np.random.default_rng(42) x rng.uniform(-3, 3, sizen) y np.where(x 0, x**2 - 1, 3*x 2) y y rng.normal(0, noise, sizen) return torch.from_numpy(x.astype(np.float32)).view(-1, 1), torch.from_numpy(y.astype(np.float32)).view(-1, 1) x, y make_data() # 标准化 x (x - x.mean()) / x.std() y (y - y.mean()) / y.std() # 划分训练集与测试集 perm torch.randperm(x.shape[0]) train_idx, test_idx perm[:1600], perm[1600:] xtr, ytr, xte, yte x[train_idx], y[train_idx], x[test_idx], y[test_idx] # 网络定义 model nn.Sequential( nn.Linear(1, 16), nn.ReLU(), nn.Linear(16, 16), nn.ReLU(), nn.Linear(16, 1), ) opt torch.optim.Adam(model.parameters(), lr0.01) loss_fn nn.MSELoss() bs 128 for epoch in range(300): model.train() idx torch.randperm(xtr.shape[0]) for i in range(0, xtr.shape[0], bs): xb xtr[idx[i:ibs]] yb ytr[idx[i:ibs]] opt.zero_grad() pred model(xb) loss loss_fn(pred, yb) loss.backward() opt.step() if epoch % 50 0: model.eval() with torch.no_grad(): train_loss loss_fn(model(xtr), ytr).item() test_loss loss_fn(model(xte), yte).item() print(fepoch {epoch:3d} | train {train_loss:.6f} | test {test_loss:.6f}) # 可视化 model.eval() with torch.no_grad(): x_plot torch.linspace(-3, 3, 400).view(-1, 1) x_plot_norm (x_plot - x.mean()) / x.std() y_plot model(x_plot_norm) * y.std() y.mean() y_true torch.where(x_plot 0, x_plot**2 - 1, 3*x_plot 2) plt.figure(figsize(8, 5)) plt.scatter(x.numpy(), y.numpy() * y.std().item() y.mean().item(), s1, alpha0.3) plt.plot(x_plot.numpy(), y_plot.numpy(), r, linewidth2, labelmodel) plt.plot(x_plot.numpy(), y_true.numpy(), k--, linewidth2, labeltrue) plt.legend() plt.xlabel(x) plt.ylabel(y) plt.title(Model Fitting vs. True Rule) plt.show()这份代码有几个细节值得说明。一是数据生成后我用view(-1, 1)把一维数组变成了二维张量因为PyTorch的线性层要求输入是(batch, features)的形状这里features就是1。二是标准化之后预测结束要反标准化回去否则你画出来的模型曲线不在原始坐标系里没法跟真实规律直接对比。三是eval模式和torch.no_grad()的使用这种没有梯度计算的阶段能省内存也能避免无意中把预测过程的张量计入计算图。3.2 训练过程实录loss是怎么一点点降下来的用上面的参数跑一遍打印出来的loss大致是下面这个趋势epoch训练集MSE测试集MSE00.9421500.938210500.2073100.2110201000.0085200.0091302000.0016800.0017203000.0012100.001260刚初始化时模型输出接近均值MSE大约等于标准化后y的方差也就是1左右。前50轮loss快速下降说明模型很快抓住了一条大致正确的轮廓。100轮之后下降速度明显放缓进入了精细调整阶段。从200轮到300轮loss只从0.00168降到0.00121进步空间已经不大了。这里有个很值得关注的细节训练集和测试集的loss非常接近说明模型没有明显过拟合。噪声的标准差是0.05折算到标准化后的尺度上噪声方差大约是0.0002左右所以模型的MSE到0.001附近已经逼近了“不可约误差”意思是它把规律中可学的部分基本学完了剩下的波动来自随机噪声这部分误差换任何模型都消除不掉。如果你把可视化代码跑出来会看到红色模型曲线和黑色真实曲线几乎重合但在x0附近的弯折处会有一点点圆滑偏差。这是ReLU分段线性逼近的特性用更多神经元可以改善但完全消除需要很大的宽度和精细训练不一定值得。3.3 亲手改过的几个参数表现对比与体会我建议你在跑通之后一定不要停在默认参数上多改几个值亲身感受一下变化。我试过几组改动这里把经验写下来。尝试一学习率调到0.1。前几十轮loss明显震荡一度不降反升后面才慢慢压下来最终结果略差于0.01而且过程的稳定性差很多。尝试二学习率调到0.0001。收敛慢到让人失去耐心300轮结束训练loss还在0.4以上根本不够用。这两个实验能直观告诉你学习率不是越大收敛越快也不是越小越稳它影响着优化路径的整体行为。尝试三把中间层的ReLU全部去掉。这应该是最值得看一眼的改动。网络退化成纯线性模型300轮之后训练loss大概卡在0.65左右再也下不去因为分段函数超出了线性模型的表达能力。学出来是一条直线无论你怎么调学习率都没救。尝试四把隐藏层宽度从16改成4。模型仍然能学到整体趋势但分段点附近的曲线明显粗糙有些地方会出现奇怪的折线。宽度从16改成64结果相对更平滑但训练时间变长在数据量小的情况下更容易出现过拟合。这说明网络宽度要和任务复杂度匹配不是越大越好。这些改动都是几行代码的事却能把学习率、模型容量、非线性这几个核心概念变成你自己的直觉。4. 进阶实验当规律变复杂模型还能顶住吗4.1 换三种规律尖角、阶梯和平滑曲线基础的分段函数跑通之后好奇心一定会被勾起来模型到底能处理多复杂的规律我换了三种典型规律做了对比。第一次换成 y |x|。这是一个V形函数在x0处有一个尖角。ReLU网络本身就是分段线性函数所以它天然适合逼近这种折线16个神经元可以学得不错。但和真实规律对比时你会发现模型在尖角附近会有一点圆化很难做到完全锐利因为光滑逼近尖角需要的分段段数更多本质上是宽度不够。第二次换成阶梯函数 y round(x)。这个规律对神经网络来说比预想的难因为阶梯有多个高度跳变而梯度下降天生喜欢平滑的变化。用16个神经元去拟合阶梯损失会一直维持在较高水平曲线看起来像楼梯被“磨圆”了跳变边缘模糊成一团。这个实验很有价值它让你感受到神经网络擅长的是连续光滑映射对付跳跃性的规则很吃力。第三次换成 y sin(x) 0.5x。这是个光滑但有起伏的周期函数。前100轮loss下降很快300轮后曲线几乎完美贴合。对比下来你就能总结规律越是光滑连续的函数网络学得越快越好越是不连续、跳跃、尖锐的函数需要越大的容量或者越精细的训练策略。4.2 噪声才是隐藏的魔王规律本身的复杂度只是难度的一部分另一个隐藏变量是噪声强度。我把同一套分段规律分别用三组噪声生成数据完全无噪声、噪声0.05、噪声0.5。无噪声的极端情况下模型可以逼近到MSE接近零但代价是更容易把样本点的个性学进去这时候你稍微扩大网络就会出现锯齿状拟合。噪声0.05就是上一节的默认场景模型依然稳健。噪声0.5时规律已经淹没在噪声里模型拟合出的曲线会来回抖动测试loss很难低于噪声带来的下限无论怎么加轮次或加宽度都无济于事。这件事背后有一个很实用的概念叫“不可约误差”只要真实规律是固定的数据里的随机噪声就构成了任何模型都突破不了的下限。所以当你发现测试loss已经降到接近噪声方差的水平就不要再纠结于提升精度了应该先想办法减少数据噪声或者收集更高质量的数据而不是一味加网络容量。4.3 训练集和测试集判断模型是否真的找到了规律在找规律任务里最忌讳的就是拿训练集误差来评价模型。训练loss很低有三种可能真的学到了规律、死记硬背了样本点、或者拟合了噪声里的虚假模式。区分这几种情况只能靠模型在没见过数据上的表现。我建议把数据划分成80%训练、20%测试并且固定一个随机种子让每次实验的可比性更高。训练过程中同时打印训练集和测试集loss如果训练loss下降但测试loss开始回升就是过拟合的经典信号如果两者都很高且不再下降则是欠拟合或者模型结构不对比如漏了激活函数。可视化也是一个高效判断手段。把模型的预测曲线和真实规律曲线画在一起你一眼就能看出它是不是在“模仿形状”。如果曲线在某些区间对得很准、在另一些区间偏离往往是数据分布不均匀造成的针对性补充该区域的样本往往比增加层数更有效。5. 踩坑记录与排查技巧5.1 高频问题现象、原因、解法速查表下面这张表是我在多次重跑这个实验时积累下来的高频问题按现象、原因、解法三列整理。遇到问题时先查表现属于哪一类再对照原因去排查。现象可能原因解决思路loss卡在高位比如0.6以上怎么调都不降网络里没有激活函数或激活函数被误删模型退化成线性在隐藏层后加ReLU确认至少有两个非线性层loss前期震荡甚至先升后降且收敛慢学习率太大把Adam的学习率从0.1调到0.01或0.005训练loss很小测试loss却大一个数量级模型把噪声也拟合了过拟合减小网络宽度、增加训练样本、加L2正则化预测曲线在x0附近和真实规律差距明显分段点附近采样密度不够或神经元数量不足增加该区间采样密度或把隐藏层宽度从16提到32训练数据和测试数据切分后结果差异很大数据划分不公平偶然性太大固定随机种子或改成多次实验取平均模型在训练范围内表现好出了范围就离谱外推超出模型能力把预测范围限制在训练数据覆盖区间内外推需要换模型思路用未标准化数据训练时loss波动剧烈输入输出量级差异太大对x和y都做标准化这些坑看似琐碎但每一个背后都对应着一个训练的核心机制。比如第一个问题根本原因在于深层线性叠乘还是线性第二个问题根本原因在于学习率对梯度更新步长的控制第三个问题本质就是泛化与过拟合的拉锯。5.2 三个初学者最容易踩的认知误区误区一网络越大越好。在小型找规律任务上321个参数已经足够换成上千参数的模型反而更容易过拟合训练也不稳定。深度学习的“大”是相对复杂任务的不是越多越好模型容量要和数据复杂度匹配。误区二训练loss越低越好。loss低并不是终极目标因为低loss可能是记住了全部样本点换一批数据就原形毕露。判断一个规律是否真被学到的标准是它在没见过数据上是否依然表现良好而不是训练集上的绝对值。误区三数据越多越好。数据量的作用要分场景。在样本只有几十个时增加数据确实能明显缓解过拟合但当样本已经足够覆盖规律时再多数据也只是让曲线更平滑而已。更重要的通常是数据质量——噪声控制、分布覆盖、分段边界的采样密度这些往往比数量更关键。最后分享一点个人体会。这个实验的核心其实是把“找规律”这项听起来很玄的能力拆解成一件件具体的事定义规律、造数据、设计网络、调参数、观察损失。每一步都非常简单连起来却构成了深度学习最基本的闭环。我每次带新同学入门时都会让他们先自己做一遍这种“自定义规律”的实验等这个流程跑通了再去看卷积、注意力那些复杂架构心里会踏实得多。你可以大胆把规律改成完全不同的形状比如指数曲线、对数曲线、带跳变的分段函数甚至试试两个变量共同决定输出。整个步骤都不用变只要换数据生成函数模型就会自去寻找新的映射关系。把系列系列的实验做成你自己的试验台比单纯跑十个教程都更能建立那种“我真正掌控了模型”的感觉。