
1. 找到切入点为什么我用回归任务练手深度学习先说结论如果你想真正入门深度学习回归任务是最容易被低估的练习场。很多人一上来就扎进图像分类、目标检测这些门槛偏高的方向忽略了一个事实——回归模型的结构更简单、损失函数更容易理解、训练过程也更直观非常适合把“深度学习到底在干嘛”这件事彻底搞明白。这次我选的实战题目是“新冠人口数预测”。严格来说这里预测的对象是确诊人数的累计值本质是一个时序回归问题。你可能觉得这个题目有点特殊但把内容技术化之后它就是一套标准的“历史数据进、未来数值出”的回归流程和预测房价、预测气温、预测股票走势没有本质区别。用这个题目来练手最大的好处是数据公开、口径统一、时间序列特征非常明显非常适合拿来演示完整的回归建模流程。这个项目适合谁如果你已经会用Python基础语法了解一点点神经网络的基本概念比如层、激活函数、损失函数但还没独立完成过任何一个深度学习项目那这篇内容就是给你准备的。我会把从数据预处理到模型训练再到评估调优的完整链路都走一遍所有坑都给你标出来。2. 整体设计与思路拆解2.1 回归任务在深度学习里的定位深度学习解决的核心问题可以粗暴分成两大类分类和回归。分类是“这图片里是猫还是狗”输出离散的类别回归是“明天的气温是多少度”输出连续的数值。回归任务虽然看起来简单但它涉及的建模思路——特征怎么构造、损失怎么计算、模型怎么评估——在后续做更复杂任务时全部用得上。疫情人口数预测这个题目要预测的是一个连续的数值未来某天的累计确诊人数所以天然是回归任务。而且它的数据形态是典型的时间序列每天一个值按时间排序。这比普通表格数据的回归多了一层“时序依赖”在建模时需要额外处理。2.2 方案选型为什么不用ARIMA而用深度学习拿到时间序列预测问题第一反应可能是ARIMA、指数平滑这些经典统计方法。我之前也试过ARIMA对平稳序列很友好但疫情数据这类曲线有明显的非线性和阶段性变化统计方法调参费劲效果还不稳定。深度学习模型的好处是可以自动学习非线性关系而且不需要你手动判断“序列是否平稳”省掉了一大堆预处理步骤。我最终选择的是PyTorch框架原因很实在生态成熟、社区资料多、调试方便。模型结构上我对比了两种方案普通全连接网络MLP把过去N天的数据拉平成一条向量输入网络结构最简单适合作为baseline。LSTM专门处理时序数据的循环网络理论上能捕捉长期依赖。实际跑下来MLP在数据量不大的情况下表现并不差LSTM虽然理论更强但训练更慢、对数据量要求也更高。我的建议是先用MLP打通整条流水线再换LSTM做对比优化不要一开始就上复杂的结构。2.3 核心思路滚动窗口构造训练样本处理时间序列回归最关键的转换思路就是“滚动窗口”。假设我们有100天的确诊人数数据想预测第101天的值那就取前N天作为一个输入窗口第N1天的值作为标签。然后窗口往后滑动一天取第2天到第N1天作为输入第N2天作为标签依此类推。用生活里的例子类比这就像你看过去一周的天气走势来猜明天的温度。你看的不是某一天的数据而是一段时间的变化规律。窗口大小N就是一个需要调试的超参数N太小模型看不到趋势N太大又引入了过多噪声。这个思路是整个项目的骨架理解透了后面的代码就只是细节。3. 数据获取与预处理实操3.1 数据源选取与导入这个项目的数据源我使用的是公开的流行病学时间序列数据格式为CSV包含日期和每日累计确诊人数两列。数据获取之后第一步永远是“看一眼再动手”别急着写模型。import pandas as pd import numpy as np import matplotlib.pyplot as plt df pd.read_csv(covid_data.csv) print(df.head()) print(df.info()) print(df.describe())这一步必须做的原因是排查脏数据。我见过太多人数据都没看清楚就开训最后模型效果离谱还找不到原因。需要留意三个点日期列的格式对不对、有没有缺失值、数值列有没有异常跳变比如某天数据突然变成0大概率是上报口径问题。3.2 缺失值与异常值处理在我处理这批数据时发现中间有几天的数据不稳定个别日期出现了回落现象。累计确诊人数的特征是“只增不减”一旦出现回落就是数据口径调整或者漏报了。处理策略有两个直接删除异常日期如果异常点不多删掉不影响整体。用前后均值填充适用于少量缺失的情况。# 将数值列转为float df[value] pd.to_numeric(df[value], errorscoerce) # 填补缺失值用前一个有效值填充确保单调递增 df[value] df[value].ffill() # 处理回落如果今天的值比昨天小保留昨天的值 df[value] df[value].cummax()注意cummax()这一步它保留了累计序列的单调性是处理累计型数据的一个关键技巧。如果你处理的是每日新增人数非累计就不需要这一步反而要小心负值。3.3 数据可视化与趋势观察预处理之后一定要做一个全局的曲线图。这个图不是用来发朋友圈的而是用来辅助判断建模策略plt.figure(figsize(12, 5)) plt.plot(df[value], colorsteelblue) plt.title(Cumulative Cases Over Time) plt.xlabel(Days) plt.ylabel(Cumulative Count) plt.grid(True, alpha0.3) plt.show()观察这个曲线的几个关键特征整体趋势是上升还是下降有没有明显的拐点增长趋势是线性还是指数型这些特征直接决定后面要不要做数据变换。我这次观察到的曲线在前段增长很陡后段逐渐平缓整体呈S型这说明原始数值范围跨度非常大从几百到几万直接送进模型的话数值大的特征会主导梯度更新所以归一化是必须的一步。3.4 归一化为什么必须做以及怎么做归一化不是“锦上添花”而是“不做不行”。假设原始数据的范围是0到50000而模型的激活函数比如Sigmoid或Tanh的输出范围在0到1之间两者的尺度完全不匹配。如果不归一化损失函数会呈现非常崎岖的地形梯度下降很难收敛甚至直接发散。我采用的是Min-Max归一化把所有值压缩到0到1之间from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() values scaler.fit_transform(df[[value]].values)这里有一个新手容易犯的错误fit和transform的顺序。必须先用训练集的数据fit出最小值和最大值再用同一套参数去transform验证集和测试集。如果你对全量数据做fit验证集和测试集的信息就泄漏到训练过程里了评估结果会虚高不少。4. 回归模型构建与训练实现4.1 构造训练样本滑动窗口代码实现归一化之后下一步就是把一维的时间序列转成“输入输出对”。这一步是核心环节我直接贴一段完整可用的代码def create_sequences(data, window_size): X, y [], [] for i in range(len(data) - window_size): X.append(data[i:iwindow_size]) y.append(data[iwindow_size]) return np.array(X), np.array(y) WINDOW_SIZE 10 X, y create_sequences(values, WINDOW_SIZE)这段代码做的事情遍历整个时间序列每次取window_size个连续点作为特征下一个点作为标签。最终得到一个二维数组X形状是(样本数, window_size)和一个一维数组y形状是(样本数,)。窗口大小选10意味着用过去10天的数据预测下一天。这个值可以后续调大调小对比效果。注意最后一个窗口后面没有标签了所以循环到len(data) - window_size就截止。4.2 训练集与测试集划分时间序列的数据切分不能随机打乱。这也是新手最容易踩的坑如果随机抽取训练集和测试集模型会“看到”未来数据测试集评估结果失真。正确做法是按时间顺序切割train_size int(len(X) * 0.8) X_train, X_test X[:train_size], X[train_size:] y_train, y_test y[:train_size], y[train_size:]80%训练、20%测试这是一个经验值。如果你追求更严格的效果评估还可以再切一部分作为验证集用于超参数调优。我这次先用训练集训练测试集只用于最终评估。4.3 模型结构与设计思路我使用了PyTorch搭了一个三层全连接网络。为什么选三层这是经过对比的一层线性模型表达能力太弱很难拟合趋势复杂的曲线层数太多又容易过拟合毕竟数据量也不算大几百条样本而已。三层的结构刚好在表达能力和复杂度之间取了个平衡。import torch import torch.nn as nn import torch.optim as optim class RegressionMLP(nn.Module): def __init__(self, input_size, hidden_size64, output_size1): super(RegressionMLP, self).__init__() self.fc1 nn.Linear(input_size, hidden_size) self.fc2 nn.Linear(hidden_size, hidden_size) self.fc3 nn.Linear(hidden_size, output_size) self.relu nn.ReLU() def forward(self, x): x self.relu(self.fc1(x)) x self.relu(self.fc2(x)) x self.fc3(x) return x模型结构解读输入层接window_size个特征10天数据第一个隐藏层64个神经元第二个隐藏层也是64个输出层1个神经元直接输出预测值。中间两层用了ReLU激活函数作用是引入非线性不然中间再多的层也等同于一层线性变换。4.4 损失函数与优化器选择回归任务最常用的损失函数是均方误差MSE公式是预测值与真实值差的平方再取平均。为什么用平方而不直接用绝对值因为平方对大误差的惩罚更重模型会优先去优化误差大的样本收敛方向更明确。model RegressionMLP(WINDOW_SIZE) criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lr0.001)优化器我选了Adam而不是传统SGD。Adam自带自适应学习率对学习率的初始值不那么敏感非常适合入门阶段——你不需要费劲手动设计学习率衰减策略。学习率设为0.001是Adam最常见的默认值一般都能得到还不错的结果。4.5 训练循环实现训练循环是深度学习的核心环节我把每一步都拆开讲清楚epochs 200 batch_size 32 dataset_size len(X_train) for epoch in range(epochs): model.train() total_loss 0 permutation torch.randperm(dataset_size) for i in range(0, dataset_size, batch_size): indices permutation[i:ibatch_size] batch_x torch.FloatTensor(X_train[indices]) batch_y torch.FloatTensor(y_train[indices]).unsqueeze(1) optimizer.zero_grad() outputs model(batch_x) loss criterion(outputs, batch_y) loss.backward() optimizer.step() total_loss loss.item() * len(indices) avg_loss total_loss / dataset_size if (epoch1) % 20 0: print(fEpoch [{epoch1}/{epochs}], Loss: {avg_loss:.6f})每一步的关键点optimizer.zero_grad()每一轮梯度清零。这是PyTorch的一个特性梯度默认会累积不清零的话上一轮的梯度会残留在参数上让更新方向乱掉。loss.backward()反向传播计算每个参数的梯度。optimizer.step()用梯度更新参数。随机打乱permutation打乱训练样本顺序让模型不会学到样本顺序带来的虚假规律。这里我用了小批量训练batch_size32而不是全量梯度下降好处是计算更快、梯度估计的噪声还能帮助跳出局部极小值点。5. 模型评估与有效性的判断标准5.1 训练误差曲线观察训练过程中的损失值变化是判断模型状态的第一手资料。我这次跑了200个epoch观察到损失从最初的几百几千迅速下降到100轮之后逐渐平稳。如果你看到损失持续下降但下降幅度在最后几十轮仍然很大那说明epochs设少了如果损失在某一数值反复震荡不再下降说明已经收敛。一个实用的判断技巧是画损失下降曲线图plt.plot(loss_history) plt.xlabel(Epoch) plt.ylabel(MSE Loss) plt.title(Training Loss Curve) plt.show()损失曲线应该是“快速下降后趋于平稳”的形状。如果曲线出现剧烈震荡长期不降大概率是学习率过高如果下降得异常慢可能是学习率过低。5.2 用图表定位模型的“高错误区间”在训练阶段损失值是一个全局的平均概念但全局平均损失低并不代表模型在每个时间段都表现得好。我习惯把“模型对所有样本的预测值”和“真实值”放到同一张图上对比能立刻看出预测在哪段区间失效了。同时会在图表下方叠加每个时间点的误差绝对值柱状图这样一眼就能定位“高错误区间”。实操方法训练完成后把训练集、测试集的全量预测跑出来画一张包含真实曲线和预测曲线的对照图。如果发现中间某一段预测值明显偏离真实值说明模型在那段时期没学到足够有效的特征这时候就需要回看数据——很可能那段时间数据本身有不规律的跳变。5.3 评估指标MAPE才是回归任务的试金石测试集上我用的核心评估指标是平均绝对百分比误差MAPE公式是def compute_mape(y_true, y_pred): return np.mean(np.abs((y_true - y_pred) / y_true)) * 100为什么不用MSE或者RMSE作为最终评估标准因为MSE的数值大小受量纲影响很难直观判断“这个误差到底算好还是坏”。MAPE给出了一个百分比比如5%意味着预测和真实值平均偏差5%直观且方便不同数据集之间对比。下面是一段完整的测试代码model.eval() with torch.no_grad(): test_x torch.FloatTensor(X_test) test_y torch.FloatTensor(y_test).unsqueeze(1) predictions model(test_x) # 反归一化还原真实数值 pred_values scaler.inverse_transform(predictions.numpy()) y_test_values scaler.inverse_transform(test_y.numpy()) mape compute_mape(y_test_values, pred_values) print(fTest MAPE: {mape:.2f}%)注意两点进入eval模式是为了关闭Dropout等只在训练阶段生效的操作用torch.no_grad()包裹预测过程能关闭梯度计算节省显存和时间。5.4 过拟合与欠拟合的判断和应对我在调试过程中把128个神经元的模型和64个神经元的模型做了对比发现128版本的训练损失更低但测试集效果反而差。这就是典型的过拟合模型把训练数据的细节和噪声都背下来了但对新数据的泛化能力下降。应对策略提供了三个方向增加数据量这里可以缩小窗口滑动步长把原来步长为1的窗口改为按0.5步长滑动对时间序列做插值相当于扩充样本数。添加正则化在损失函数中加上L2惩罚项约束权重不要太大。添加Dropout层训练时随机丢弃部分神经元强制模型学习更鲁棒的特征表示。self.dropout nn.Dropout(0.2) # 在forward中每个隐藏层后加上 # x self.dropout(self.relu(self.fc1(x)))Dropout率0.2是一个常见起步值太小没效果太大则模型欠拟合。6. 实际踩坑记录与排查方案6.1 数据泄漏问题我第一次做这个项目时犯了一个低级但影响巨大的错误先用全量数据的最大值和最小值做归一化再切分训练集和测试集。结果测试集的效果看起来好得离谱MAPE只有2%左右但我知道这个数字是虚的因为归一化时测试集的信息已经被模型“看到”了。正确的顺序是先切分再归一化而且只用训练集去计算归一化参数。这个坑非常隐蔽因为代码不会报错只会给你一个不真实的好结果。建议读者在做任何数据变换时都先问自己一句这个变换的参数来自哪些数据6.2 训练损失不下降的排查训练过程中我遇到过一次损失卡在0.7附近不下降的情况。排查思路按优先级排序学习率设置是否合理学习率过大导致震荡不收敛过小导致原地踏步。可以试着把学习率调大或调小各10倍看损失是否有变化。数据是否归一化如果特征数值范围和标签数值范围差距太大梯度会被大数值特征主导。检查数据和标签是否都做了归一化。激活函数是否饱和层数较多时浅层梯度容易消失。改用ReLU能有效缓解这个问题。我这次问题出在归一化时只处理了特征忘记了标签也放进去歪打正着让标签始终在原始量纲模型一开始根本不知道该朝哪个方向收敛。6.3 训练与预测时模式不一致另一个反复坑人的点训练时模型处于model.train()模式预测时忘切换成model.eval()。如果模型里有BatchNorm或Dropout层训练和预测的统计计算方式完全不同预测结果会莫名其妙地差很多。我在第一次加Dropout后就碰到了这个情况训练损失很正常测试预测值却忽高忽低排查了半天才发现是模式切换的问题。6.4 常见问题速查表问题现象可能原因排查与解决损失不下降学习率过大或过小尝试用lr0.1/0.01/0.001/0.0001分别测试测试效果好但真实场景差数据泄漏检查归一化参数的fit对象是否只有训练集训练损失降、测试损失不降过拟合加Dropout、L2正则化或扩大训练数据预测结果全是某个常数网络输出层忘记用线性激活回归任务的输出层不要加激活函数损失震荡剧烈batch_size太小或学习率太大增大batch_size或降低学习率6.5 训练数据的“噪声样本”对结果的影响这是我在用新收集的一段数据进行测试时发现的由于中途存在几天监测机制调整导致的数值跳变模型在那几天的预测误差明显增大。结构上讲跳变点本身就是极难预测的而模型却被迫把“跳变”当作规律的一部分去拟合结果就是所有值都被拉偏。我的处理方案很直接把跳变明显的那几天单独标记出来在训练时彻底删掉这些样本评估时也跳过这些时间点单独统计。删除后模型在其他时间段的表现更加稳定了整体MAPE下降了不少。当然这个处理的代价是模型对极端情况的预测能力更差了但本质上极端情况本来就不可由历史数据预测。7. 一些实操心得与后续扩展建议写到这儿核心流程已经完整走了一遍。最后再分享几点我在实际项目中的体会和一些可以继续深挖的方向。第一关于数据无论如何强调数据预处理的重要性都不为过。我在不同数据集上跑同一套模型结果差异非常大差别基本都出在数据质量上。做深度学习项目分配在数据上的时间占比至少要有50%模型结构的调整其实只是很小的一部分。第二关于模型迭代不要一上来就追求“最优架构”。先用最简模型把整个流程跑通是最重要的然后再一步一步加复杂度。我这次从MLP换到LSTM时发现提升并不明显反而训练时间增加了很多这提醒我模型复杂度并不总是好事要和数据量匹配。第三关于预测误差的进一步优化如果你想让模型更准可以考虑几个方向比如引入不止一列特征加上每日新增人数、检测数量等特征或者做差分处理把目标从预测数值变为预测变化量这些手段往往对时间序列回归的提升比换模型结构更有效。最后建议每一个刚入门深度学习的读者都完整地走一遍这个项目再行动。它麻雀虽小五脏俱全——数据获取、清洗、特征构造、模型搭建、训练调优、评估诊断一个环节都不少。把这条路走通一次之后无论遇到多么复杂的任务你都知道该从哪里下手了。