最近把李宏毅老师2022年机器学习课程的hw1重新完整做了一遍——新冠阳性人员数量预测。这个作业我当年第一次写的时候只跑了线性回归就交差这次借着重做把数据切分、模型设计、训练陷阱到提交细节全捋了一遍收获比想象中多。先说结论hw1看起来只是个“预测确诊人数”的小任务实际上把时间序列回归的完整流程都串起来了非常适合用来建立对机器学习项目的基本手感。如果你正准备做这个作业或者刚学完回归和优化想找个小项目练手这篇文章应该能帮你少走不少弯路。下面我会把题目拆解、数据处理、模型选型、训练调试和踩坑实录一条线讲清楚代码部分用PyTorch为例但思路迁移到TensorFlow或者纯NumPy实现也没问题。1. 这到底是个什么作业先搞懂题目再动手1.1 一句话读懂hw1hw1的任务很直白给你某个地区过去一段时间每天的新冠阳性人员数量让你预测未来若干天的人数。原始数据通常是按天排列的序列每行包含日期和当天新增确诊数也可能附带一些额外的数值特征具体以你拿到的数据集字段为准。这个任务本质上是一个时间序列回归问题。所谓回归就是预测连续值所谓时间序列就是数据点之间存在先后顺序不能简单打乱。用前N天的确诊人数去预测后M天的人数是最常见的滑窗做法也是这个作业默认的解题方向。我在实际做的时候把N设成7天预测未来1天到3天的数值效果比较稳定。注意这不是分类问题。有人一开始会想着做“明天会涨还是跌”的分类但作业要求的是预测具体阳性人数输出层应该是一个连续值损失函数用MSE均方误差而不是交叉熵。1.2 这个作业真正在训练什么能力很多初学者拿到题目第一反应是“套模型”但李宏毅老师的作业设计从来不只是让选个神经网络。hw1真正想让你体会的是三件事第一完整的数据处理流程。原始数据不是拿来就能喂给模型的要做缺失值处理、归一化、滑窗切分、训练集测试集划分每一步都会直接影响最终结果。第二从baseline开始迭代的意识。作业最基础的方案是用前几天的数据做线性回归这个方案简单到不需要神经网络但它能跑出一个分数让你知道后续所有花里胡哨的模型至少要比它好。先有baseline再有优化这是工业界和学术界通用的工作方式。第三时间序列数据不能乱切的直觉。如果拿随机抽样的方式划分训练集和测试集数据泄露会非常严重。因为第t天和第t1天之间的相关性极强随机打乱会让模型“偷看”到未来信息导致在测试集上虚高。后面我会详细说验证集该怎么切。2. 数据长什么样怎么切才不会翻车2.1 数据结构与常见坑点我这次使用的数据大概长这样第一列是日期第二列是当天新增确诊的阳性人数后面还有一些可选的属性列。课程原始提供的数据往往夹杂着缺失日期或者连续零值比如某些天没有上报数据导致人数为0。这些零值不一定代表真实情况直接当作正常样本喂进去会让模型学出错误的周期性。我的处理办法是先用pandas读入按日期排序检查连续天数的间隔。如果发现个别缺失用前后几天的均值填充如果缺失比率很低也可以用线性插值。对于连续出现的0值需要结合上下文判断是真实清零还是数据缺失如果确定是缺失可以视为异常值做平滑。归一化这一步很多人会忽略但它非常关键。确诊人数可能从几百到几万波动直接喂给模型数值大的特征会主导梯度更新模型很难收敛。我采用min-max归一化把数据缩放到0到1之间from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() data[cases_scaled] scaler.fit_transform(data[[new_cases]])注意fit_transform只能用在训练集上验证集和测试集要用训练集拟合好的scaler去transform。这不是细节问题而是防止信息从测试集流到训练过程。2.2 滑窗切分用前N天预测后M天时间序列预测最常规的做法是把监督学习式的样本构造出来。假设我们用过去7天的数据预测未来1天的数据那么对于第t天输入特征X就是第t-6到第t天的阳性人数标签y就是第t1天的人数。每往后挪一天就产生一个新样本。用代码实现大概是这样的def create_sequences(data, input_days7, output_days1): X, y [], [] for i in range(len(data) - input_days - output_days 1): X.append(data[i:iinput_days]) y.append(data[iinput_days:iinput_daysoutput_days]) return np.array(X), np.array(y)这里有几个参数需要决策input_days前多少天、output_days预测未来多少天。我试过input_days7、14、28三组配置结果差异不是特别大但7天在训练效率和精度上最均衡。output_days如果大于1比如一次预测未来3天本质上是在做多步预测难度会更高baseline分数也会明显变大。2.3 训练集、验证集、测试集到底怎么分这是整个作业里最容易出错的地方。很多同学习惯用train_test_split随机分配这在普通表格数据里没问题但在时间序列里是绝对错误的。原因很简单今天确诊人数和明天确诊人数高度相关随机shuffle后验证集里会出现大量和训练集相邻甚至几乎相同的样本模型等于变相看到了答案。正确做法是按时间顺序切分。通常把前80%作为训练集最后10%到15%作为验证集最后一部分作为测试集。我在做的时候用了更保险的方式训练集用前70%验证集用接下来的15%测试集用最后15%。这样能保证三个集合在时间轴上完全没有交叉。提示因为滑窗会用到前N天数据所以相邻的样本本身有重叠信息即使按时间顺序切分训练集最后一个样本和验证集第一个样本也可能共享部分时间点。严格来说这是可接受的因为模型推理时本来就需要前N天数据做输入。更严格的方案是让验证集从时间上完全晚于训练集——我就是这么做的效果靠谱。3. 模型从简到繁先上baseline再谈优化3.1 最简单的回归模型长什么样hw1最基础的模型其实就是一层线性层。输入的7天数据展平成7维向量输出1维预测值。如果不用PyTorch直接用NumPy算最小二乘也能出结果。但既然是课程作业用PyTorch写一个线性层是练手的好机会import torch import torch.nn as nn class LinearRegressor(nn.Module): def __init__(self, input_dim7): super().__init__() self.linear nn.Linear(input_dim, 1) def forward(self, x): x x.view(x.size(0), -1) return self.linear(x)这个模型的参数非常少总共只有8个7个权重加1个偏置所以训练非常快也不容易过拟合。但它的表达能力有限只能学到输入和输出之间的线性关系。如果真实数据的生成过程包含明显的非线性线性模型的分数就会比较低。这就是为什么后面需要更复杂的模型。3.2 从线性到MLP增加非线性的代价和收益当线性模型的结果不够好下一步是加一个隐藏层变成最简单的MLP。比如输入7维隐藏层64个神经元再用ReLU激活最后输出1维。这个小小的改动会带来什么效果呢模型能学到“最近几天连续上升/下降”这种非线性趋势而不仅仅是把所有输入乘以固定权重的加权和。我有一个建议在动手之前最好先用线性模型跑通整个流程然后把模型替换成MLP对比分数。你会发现MLP在训练集上的loss通常更低但验证集上的表现未必更好这就是“过拟合”第一次在现实中找上你。对抗过拟合的办法一般是加正则化、加dropout或者减小隐藏层维度。我实际使用的MLP结构里隐藏层用的是64维加ReLU输出处不加激活函数。因为这是回归任务最后一层如果加sigmoid会限制输出范围必须在0到1之间反而不利于预测真实值虽然归一化后目标确实在0到1但训练不稳定时可能输出越界建议不要限制最后一层。3.3 要不要上self-attention或者Transformer搜这个作业相关热词的时候很多人会看到“李宏毅 self-attention”、“Transformer”这些关键词想趁这个机会直接上Transformer。我的态度是可以玩但没必要作为主方案。hw1的数据量通常只有几百天每个样本长度只有7到14个时间步Transformer的核心优势在于捕捉长距离依赖在这个任务上没有发挥空间。你硬做一个自注意力层训练速度慢、参数多最终分数大概率还不如线性回归加特征工程。但是如果你想深入理解attention机制完全可以在这个作业上做一个小实验实现一个单层的self-attention替换掉MLP中的一部分和baseline对比。这个过程的价值在于加深对注意力机制的理解而不是为了刷分。李宏毅老师在课程里反复强调模型是工具理解数据特性才是关键。对于小样本时间序列简单模型往往是更好的起点。4. 训练流程与参数细节每一步都算数4.1 损失函数和优化器的选择逻辑回归任务的默认损失函数是MSE均方误差计算方式为预测值和真实值差值的平方再取平均。为什么不是MAE平均绝对误差呢因为MSE对大的误差惩罚更重这会让模型更努力避免严重的偏差同时也更方便求导。缺点是对异常值敏感如果数据里有几天确诊人数出现极端波动MSE会把模型拉向这些异常点。我在实验中发现当数据中有一些极端峰值时MAE反而更稳定。如果你发现模型的预测结果在某些尖峰附近特别离谱可以试试把损失函数换成MAE或Huber Loss。优化器我用的是Adam学习率设成1e-3。Adam的优势是自适应调节每个参数的学习率对新手非常友好。相比之下SGD对学习率的选择更敏感需要更仔细地调参。如果要用SGD学习率通常要设得更低比如1e-4或5e-4并且要配合学习率衰减。代码示例import torch.optim as optim model LinearRegressor(input_dim7) criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lr1e-3)4.2 训练循环里容易被忽略的细节训练循环本身不复杂但有几个细节让很多人踩坑。第一个是把输入转成float32。PyTorch默认的浮点类型是float32如果你的数据是float64或int类型模型会报类型错误。我习惯在构建DataLoader之前统一做一次astype(np.float32)。第二个是每个epoch打乱训练集顺序。对于普通表格数据shuffle有助于训练稳定但对于时间序列这里需要分情况讨论。如果你用的是滑窗构造的样本每个样本本身已经是一个独立序列那么对样本顺序做shuffle是合理的因为每个样本内部的时间顺序没有被破坏。我自己是只对训练集shuffle验证集和测试集保持时间顺序这样能保证评估结果稳定。第三个是梯度清零。optimizer.zero_grad()必须放在每次反向传播之前否则梯度会累加导致参数更新异常。这个错误很隐蔽因为loss不会立刻变大而是慢慢变得不稳定。训练循环大致如下for epoch in range(200): model.train() train_loss 0.0 for X_batch, y_batch in train_loader: optimizer.zero_grad() pred model(X_batch) loss criterion(pred, y_batch) loss.backward() optimizer.step() train_loss loss.item() * X_batch.size(0) train_loss / len(train_loader.dataset) model.eval() val_loss 0.0 with torch.no_grad(): for X_batch, y_batch in val_loader: pred model(X_batch) loss criterion(pred, y_batch) val_loss loss.item() * X_batch.size(0) val_loss / len(val_loader.dataset)4.3 batch_size和epoch怎么调batch_size的选择会影响训练速度、稳定性和最终泛化能力。在我的实验中数据量比较小整个训练集只有几百个样本batch_size设成32或64都可以。如果设成1梯度噪声太大loss曲线会剧烈震荡不容易收敛如果设成整个训练集full batch每次更新计算量小但收敛方向单一也容易陷入局部最优。32是一个比较稳妥的起点。epoch的数量不能拍脑袋定我通常的做法是同时记录每个epoch的训练loss和验证loss如果验证loss已经几十个epoch没有下降就提前停止。更规范的做法是Early Stopping也就是保存验证集loss最低那次epoch的模型参数训练结束后重新加载它。5. 踩坑实录与排查技巧5.1 训练loss变成NaN这是最常见的报错。loss变成NaN通常有两个原因一是学习率太大梯度更新越过最优点导致数值爆炸二是输入数据里有NaN或者inf模型在反向传播时把梯度也带坏了。排查方法很简单第一步打印数据的np.isnan().any()和np.isinf().any()排除数据问题第二步把学习率调小到原来的十分之一试试。如果数据正常、学习率调小后loss仍然变成NaN就要检查是否在模型中使用了不合适的激活函数或者在归一化时用的scaler包含除零的情况。我在实验中有一次把学习率从1e-3调到1e-2loss到第3轮就变成NaN调回1e-3才稳定。这个例子说明Adam虽然自适应但初始学习率也不是可以随便设置的。5.2 预测结果几乎是一条平线当你发现模型预测的阳性人数无论是训练集还是验证集都差不多是同一个常数说明模型基本没学到有效信息只在输出训练集的平均值。这种情况在hw1里非常常见尤其是线性模型。原因可能是输入特征与目标的线性相关性太弱或者输入数据的分布范围太小模型找不到有效的权重方向。我建议按顺序排查先看一眼归一化是否做对了训练集和验证集是否用同一个scaler然后画一条时间序列的曲线看看输入特征和目标之间是否存在明显的滞后相关性最后再确认模型容量是否过小。如果数据本身有很强的周周期性比如一周内某天规律性升高可以增加input_days让模型有机会学到“一周前”这个信息。5.3 本地验证集分数和测试集提交分数差很多这是一个非常折磨人的问题。本地验证集loss很低但提交后分数差得离谱通常逃不出两个原因第一个是数据切分不一致。训练时你用了随机切分或不同的比例但测试集其实是在未来时间段的数据上做评估这导致本地指标失真。解决办法就是严格按照时间顺序切分且验证集区间一定要在训练集后面。第二个是归一化处理时测试集数据被混入scaler的拟合过程。如果你先对整个数据集做fit_transform再切分测试集的信息已经暗含在归一化参数里这会让你在验证集上过于乐观。正确做法是先切分再用训练集fit scaler最后transform验证集和测试集。5.4 常见问题速查表问题现象可能原因解决方案loss为NaN学习率过大或数据含NaN调低学习率检查数据预测值是常数模型欠拟合特征无效增大input_days检查归一化训练好但验证差过拟合减小模型容量加dropout或正则验证好但提交差数据泄露或切分不一致按时间切分先fit后transformloss下降缓慢学习率太低或数据未归一化调高学习率或检查数据分布最终预测出现负值输出层未限制范围可以接受或后处理为06. 把作业吃透之后能力边界在哪里6.1 这个作业与Transformer、self-attention课程内容的衔接很多同学是因为后续课程讲到attention和Transformer才回头咀嚼hw1的。我在重做的时候试着对比了一下hw1的数据长度太短任务太简单self-attention和Transformer的优势确实发挥不出来。但这个作业恰好提供了一个“失败对照组”——当你学到了attention机制回来在hw1上跑一跑会深刻体会到注意力机制更擅长的场景是长序列、高维特征和复杂依赖关系不是所有任务都能无脑上大模型。不过这不是说hw1不值得做。恰恰相反这个作业作为课程的第一个实战项目难度控制得很好模型从线性回归到MLP再到更复杂结构都能跑形成的分数梯度会让你直观感受到模型复杂度和数据量之间的关系。6.2 从作业到工程还可以怎么扩展如果你不满足于交作业想把预测做得更接近工业级有几个扩展方向非常合适。工程上更常见的做法不只是用前N天阳性人数做输入而是把星期几、节假日、前一天增长率、7天滑动平均等派生特征一起拼接进输入向量。特征工程对时间序列的提升往往比模型结构升级更明显。我在做完基础版本后把“星期几”作为one-hot特征拼进去验证集loss大概降低了5%到10%。另外一个值得尝试的方向是预测结果的置信区间而不是单点预测。阳性人数预测天然带有不确定性单点预测只能给出期望值但实际决策需要知道最坏情况和最好情况。用分位数回归或者MC Dropout可以输出预测分布这会让整个作业从“课程练习”变成“能讲故事的项目”。实操建议把最终提交的预测结果保存成CSV后一定要自己画一张预测值和真实值的对比图。我见过太多提交分数看起来不错、但画出来波形和真实数据存在明显相位偏移的情况。如果预测曲线整体比真实曲线慢半拍说明模型只是在“复读前一天的数据”并没有真正学到动态规律。这种情况下可以考虑增加输入窗口或者引入差分特征。最后再分享一个我实际操作中的体会hw1这类时间序列回归作业成绩好坏并不只是模型选型决定的数据处理和验证集划分占据了一半以上的权重。很多人执着于换模型、调参却没有认真审视自己到底把哪些数据信息无意中喂给了模型。把数据切分、归一化、样本构造这三步做扎实哪怕只用线性回归也能拿到一个相当可观的基础分数。这个意识是做完hw1之后最值钱的东西。