1. 动手之前,先搞清楚这个作业到底在干嘛
第一次看到“预测PM2.5”这个题目,很多人第一反应是:这不就是个回归问题嘛,拿线性回归怼上去不就完事了?
真上手你才会发现,这个作业卡人的地方根本不在模型,而在数据处理。训练集给的是台湾某地区一整年的观测数据,每个月20天,每天24个小时,每个小时有18项观测指标——PM2.5、PM10、NO2、SO2、CO、O3、温度、湿度、风速、雨量等等。但作业只让你用前9个小时的数据去预测第10个小时的PM2.5,而且只给了一列PM2.5作为预测目标。
换句话说,这是一个典型的多变量时间序列预测问题,但被刻意简化成了“用过去9小时的PM2.5(加上其他气象指标)预测下1小时的PM2.5”。我当年做这个作业的时候,最直观的感受是:李宏毅老师想让你练的不是调参,而是“怎么把一个原始数据集变成模型能吃进去的矩阵”。
1.1 核心需求解析
先别急着写代码,把需求拆开来看:
- 数据:一年365天,每个月取前20天,每天24小时,共12×20×24=5760个小时的观测记录。
- 特征:每个小时有18个指标(包括PM2.5本身),其中可能有缺失值(RAINFALL字段大量是0或者空值)。
- 任务:用连续9个小时的全部18项指标,去预测第10个小时的PM2.5值。
- 评价指标:均方根误差(RMSE),越小越好。
- 工具限制:作业要求不用任何现成的机器学习库(比如scikit-learn),必须自己手写梯度下降。
看到“手写梯度下降”就知道,这个作业的核心考察点不在模型架构,而在你能否把线性回归的数学原理落实到代码里,以及对数据预处理、特征矩阵构建、归一化、学习率调参这些基本功是否熟练。
1.2 为什么这个作业值得认真做
说实话,现在随便调个库跑回归,几行代码就出了结果,但那种“能跑”和“理解原理”之间隔着一条鸿沟。这个作业逼你手动实现以下环节:
- 从原始表格里切出训练集和验证集,处理缺失值
- 手工构造滑窗特征矩阵(9小时窗口 × 18维度)
- 手动做标准化/归一化,防止梯度爆炸或收敛过慢
- 亲手实现梯度下降,并理解学习率、迭代次数、损失函数曲线之间的关系
这些能力放到真实业务里,才是真正值钱的部分。现在很多工业场景下,模型本身都是现成的,真正难的恰恰是数据清洗和特征工程。所以这个作业虽然是课程任务,但做完之后你收获的是一个完整的机器学习项目流程认知。
2. 数据预处理的细节,决定你后面省不省心
这个作业最坑的地方不是模型,而是数据。我拿到原始CSV的时候,第一眼看到那个RAINFALL字段全是0或者空白,差点以为是数据没加载对。
2.1 训练数据到底长什么样
原始train.csv的大致结构是:
- 每一行是一个指标(AMB_TEMP、CH4、CO、NO、NO2、NOx、O3、PM10、PM2.5、RAINFALL、RH、SO2、 THC、WS_HR、WD_HR、WS_HR等18项),注意这里行才是指标,列才是时间点。
- 列名是日期+小时,比如“2014/1/1 1”、“2014/1/1 2”……一路排下去。
- 所以你要做的第一件事,是把数据转置成“行为时间点、列为特征”的格式。
这一步搞反了,后面全乱。我第一次就是没转置,强行用原始格式去构建特征,结果维度全对不上,排查了半天才意识到问题。
2.2 缺失值的处理策略
原始数据里,RAINFALL的缺失值是用“NR”(Not Reported)表示的,而不是NaN。拿到数据后第一件事就是全局搜索一下,把NR替换成数值0。
这里有个小分歧:是把降雨量缺失直接填0,还是用前后均值插值?我的做法是填0,主要原因是这个字段绝大多数时间本来就是0(没下雨),少数缺失大概率也是无降雨记录,填0不会引入明显偏差。但如果你发现某个指标大量缺失,那就不能简单填0了,得考虑插值或者干脆删掉那个特征。
另外,PM2.5本身有没有缺失值?我在实际数据里没遇到,但稳妥起见,还是要做一次全局缺失值扫描,用numpy的np.isnan或者pandas的isnull检查一下。如果某个小时的PM2.5是缺失的,最简单的处理是把那一条样本整个删掉,因为时间序列预测中,用插值补出来的目标值会污染训练标签。
2.3 特征矩阵的构建方式
这是整个作业的核心工程点。你要把连续9小时的多维观测数据,转换成一个二维矩阵:每一行是一个样本,每一列是9×18=162个特征。
我当时是这么写的:
def build_features(raw_data, n_hours=9): """ raw_data: shape = (hour, feature_dim) 返回: X shape = (num_samples, n_hours * feature_dim) y shape = (num_samples,) """ num_samples = raw_data.shape[0] - n_hours feature_dim = raw_data.shape[1] X = np.zeros((num_samples, n_hours * feature_dim)) y = np.zeros((num_samples,)) for i in range(num_samples): # 取第 i 到 i+8 小时的观测,展平成一维向量 X[i] = raw_data[i : i + n_hours, :].flatten() # 预测第 i+9 小时的 PM2.5,假设 PM2.5 是第 9 列(索引8) y[i] = raw_data[i + n_hours, 8] return X, y注意几个细节:
- PM2.5的列索引要确认清楚:18项指标里PM2.5到底排第几位,不同版本的作业数据可能不同,别想当然。
- 你不是只预测每天的第10小时,而是“任意连续9小时预测下1小时”,所以滑窗的步长是1,样本数会非常多。整个训练集大概能生成五千多个样本。
- 我没有把一天24小时单独切分开,而是把所有小时连在一起滑动。这个选择在理论上有点争议——跨天的样本(比如23点预测0点)实际上时间不连续,但作业没有明确要求,所以大多数解法都是直接全序列滑窗。
构建完之后,记得把X和y打乱顺序,否则模型会学到时间顺序的偏见。打乱用np.random.permutation就行,注意要把X和y按同一个索引顺序打乱。
3. 手写线性回归的完整推导与实现
这个作业的核心算法就是线性回归,但你必须自己实现梯度下降,不能用现成的库。如果你的线性代数基础比较扎实,这一步不难;如果基础一般,我给你把公式拆开揉碎了讲。
3.1 损失函数和梯度公式
线性回归的模型形式是:
y = X·w + b
其中X是样本矩阵(形状为n×162),w是权重向量(162维),b是偏置(标量)。为了简化代码,通常把b合并进w,即在X最后一列加一列全1,这样模型变成y = X·w,w变成163维。
损失函数用均方误差(MSE):
L = (1/2n) · Σ (y_pred - y_true)²
这里加个1/2是为了求导时消掉那个2,纯粹为了公式好看,不影响优化结果。不过作业最后评价用的是RMSE,也就是sqrt(MSE),所以计算损失时你可以多算一步。
梯度推导:
∂L/∂w = (1/n) · Xᵀ · (Xw - y)
这就是全部核心。用代码实现就是:
def gradient(X, y, w): n = X.shape[0] pred = X @ w grad = (X.T @ (pred - y)) / n return grad3.2 梯度下降的三种停法
梯度下降的核心就是反复执行w = w - lr·grad,但具体怎么停,有讲究:
- 固定迭代次数:比如跑1000次、10000次。简单粗暴,但你可能跑完发现还没收敛,或者早收敛了还在空转。
- 损失变化小于阈值:比如连续几次迭代损失下降少于1e-6就停。这个更智能,但要注意阈值别设太小,否则可能因为浮点误差永远停不下来。
- 验证集RMSE不再下降:这是最实用的做法,因为它直接盯住了你的最终目标。如果验证集RMSE连续多个epoch不降反升,说明开始过拟合了,赶紧停。
我当时的做法是组合1和3:先设一个最大迭代次数(比如5万次),每100次打印一次损失和验证集RMSE,同时设置early stopping条件,连续500次迭代验证损失不降就提前终止。
3.3 学习率的调试经验
学习率是线性回归里最玄学的超参数。太大,损失会震荡甚至爆炸;太小,收敛慢到你怀疑人生。我当时试过的经验值:
- 0.001:稳妥但慢,跑几千次loss还在稳步下降,适合想观察整个收敛过程的时候用。
- 0.01:常用起点,大部分情况下都能正常收敛。
- 0.1:速度很快,但有时候会震荡,需要配early stopping。
- 1.0:直接爆炸,loss变成NaN。别问我怎么知道的。
判断学习率是否合适的标准:看loss曲线。如果是平滑下降,可以试着调大一点;如果是锯齿状震荡,调小;如果是先降后突然冲高,那说明学习率太大,在最优解附近反复横跳。
有一种更省心的方案是自适应学习率,比如Adagrad,这也是课程后面会讲到的内容。但如果作业要求是普通梯度下降,那就别炫技,老老实实调参。我当时为了省事,用了一个简单的学习率衰减策略:每5000次迭代,学习率乘0.9,效果不错,收敛快且稳定。
4. 归一化到底该怎么做,为什么它如此重要
很多新手拿到数据直接开跑,结果发现loss大得离谱,或者梯度直接爆炸。原因就是没做归一化。
4.1 为什么必须归一化
看看你的特征:PM2.5的值可能在几十到几百之间,而雨量字段大量是0或者个位数,温度可能二三十度,风速可能零点几。各特征之间的数值范围差异巨大。如果直接扔给梯度下降,那些数值大的特征会主导梯度方向,导致权重更新不均匀,收敛速度极慢,甚至根本不收敛。
打个比方:你在二维平面上从(100, 0.1)这个位置走向原点,如果x方向和y方向的学习率一样,那x方向要走100步的距离,y只要走0.1步,梯度下降就会在x方向上大步流星、在y方向上原地踏步,路径非常扭曲。
4.2 标准化和归一化的选择
常见做法有两种:
- 零均值标准化(Z-score):x' = (x - μ) / σ,适用于特征近似正态分布的情况。
- 最大最小归一化(Min-Max):x' = (x - min) / (max - min),把数据压到[0,1]区间。
我测试下来,这个作业用Z-score效果更好。原因很简单:气象数据里有些字段的分布并不是均匀分布在[min, max]区间,而是集中在某个值附近(比如雨量几乎全为0),Min-Max归一化会被极端值压扁,反而Z-score能更好地保留相对差异。
具体实现:
def zscore_normalize(X_train, X_val, X_test=None): mean = X_train.mean(axis=0) std = X_train.std(axis=0) std[std == 0] = 1 # 防止除零 X_train_norm = (X_train - mean) / std X_val_norm = (X_val - mean) / std if X_test is not None: X_test_norm = (X_test - mean) / std return X_train_norm, X_val_norm, X_test_norm, mean, std return X_train_norm, X_val_norm, mean, std注意一个关键细节:必须用训练集的均值和标准差去归一化验证集和测试集,而不是用各自的均值标准差重新算。原因很简单:模型在训练时看到的特征分布是训练集的分布,验证集和测试集只是模拟“未来数据”,你必须用训练集的统计量去转换它们,才能保证数据分布的一致性。这是面试中很爱考的一个细节。
我当时做实验对比了一下,不做归一化时,loss基本在几千的水平,训练几千次都降不下去;做了归一化之后,loss能顺利降到个位数,验证集RMSE也稳定在5-6之间。差别就是这么大。
5. 模型评价与调参优化,如何稳定拿到更好的分数
这个作业的最终评价标准是RMSE,我把自己折腾出来的经验写出来,帮大家少走弯路。
5.1 训练集和验证集怎么切
不建议随机打乱后按比例切。因为原始数据是时间序列,如果你随机打乱,训练集和验证集会包含相邻时间的样本,信息泄露非常严重。我当时是按时间顺序切分的:取前80%(大概4600个样本)做训练,后20%(约1150个样本)做验证。这样更接近真实预测场景。
不过要注意:切分之前还是要做一次全局shuffle,但shuffle之后要按顺序切,这听起来有点矛盾,实际操作是:
- 先构建所有滑窗样本
- 打乱样本顺序(消除时间偏见)
- 按比例切分:前80%训练,后20%验证
- 用训练集的统计量做归一化
如果你不想打乱也行,直接按原始时间顺序切,效果也差不多。我自己测试过,两种方式最终RMSE差异不大,但按时间切更符合逻辑。
5.2 如何判断模型是否收敛
训练过程中,每隔一定迭代次数打印一次损失。我一般这么看:
- 训练loss下降,验证loss也在下降:健康状态,继续跑。
- 训练loss下降,验证loss持平或上升:过拟合,降低模型复杂度或加正则化。
- 训练loss和验证loss都在高位震荡:学习率太大,调小一点。
一个比较好用的技巧是:把每次迭代的loss画出来。不需要什么高级的可视化工具,matplotlib画个折线图就够了。看到loss曲线趋平,说明模型学得差不多了。如果发现5000次迭代后loss还在稳步下降,那就加大迭代次数。
5.3 加正则化项能不能提升分数
标准线性回归很容易过拟合,尤其是特征维度高达162的情况下。作业本身没有强制要求加正则化,但加了L2正则化(岭回归)几乎肯定能提升验证集分数。
L2正则化的损失函数变成:
L = (1/2n) · Σ(y_pred - y)² + λ·Σw²
梯度变成:
∂L/∂w = (1/n)·Xᵀ(Xw - y) + 2λ·w
我当时测试了不同的λ,发现λ=0.001左右最合适,验证集RMSE稳定在5左右。如果你不加正则化,RMSE一般在5.5到6之间,差距挺明显的。
不过要注意,λ太大会让模型欠拟合。我试过λ=0.1,结果验证集RMSE反而升高了,因为权重被压得太小,模型表达能力不足。
5.4 特征维度的进一步扩展
基础模型是用全部18个特征×9小时=162维。我测试过只用PM2.5这一列数据(9维特征)的效果,RMSE大概在7左右,比用全部特征差不少。这说明其他气象指标对PM2.5预测确实有贡献。
另一个可行扩展是将特征中加入PM2.5的滞后项(比如前9小时之外再往前推几小时),或者加入一小时前的差值(delta feature)。但这些扩展属于锦上添花,作业要求不高的话,用基础162维特征就足够了。我在实际测试中,加入额外滞后项后RMSE只下降了0.2左右,性价比不高,但如果你想要更高的分数,可以试试。
6. 实操过程中的坑与排查技巧
这个部分是我最想写的内容,因为很多坑不自己踩一遍,光看公式是看不出来的。
6.1 数据形状搞错的症状
最典型的现象:你按照直觉把CSV读进来,然后用data[:, i]去取某一列数据,结果发现取出来的是某些乱七八糟的字符串。
这是因为原始CSV的行是指标、列是时间点,你读进来之后的data.shape可能是(18, 5784),而你的习惯是(样本数, 特征数)。所以读完之后先print一下shape,确认方向对不对,再继续操作。
我当时是这么处理的:
import pandas as pd import numpy as np df = pd.read_csv('train.csv', header=0, index_col=0) # df 的每一行是一个指标,每一列是一个时间点 data = df.values.T # 转置成 shape = (时间点数, 指标数)转置之后,data[i]就代表第i小时的18项观测值,data[i, 8]就是第i小时的PM2.5(具体索引以实际数据为准)。
6.2 缺失值“NR”没处理干净
如果你在做数值运算时报错,说什么could not convert string to float: 'NR',那必然是缺失值没替换。我建议在读完CSV后立刻执行:
data = np.where(data == 'NR', '0', data).astype(np.float32)注意,要先替换再转类型,不然字符串转float直接报错。这个操作要放在转置之前还是之后都可以,但务必在构建特征矩阵之前完成。
6.3 模型预测结果全是同一个值
如果你发现模型预测出来的PM2.5全都接近同一个数,比如都预测成6.0左右,那很可能是你的权重初始化有问题,或者学习率太小导致模型根本没怎么学。这种情况通常表现为:训练集loss很高且几乎不下降。
排查方法:打印一下w的值,看看是不是接近于全零或特别小的随机数。如果w都是零附近,那模型一开始的输出就是偏置b的值,接近训练集均值。这种情况下调大学习率,或者初始化权重时用稍微大一点的随机值(比如np.random.randn(163) × 0.1)。
6.4 梯度爆炸导致loss变成了NaN
梯度爆炸在数值型特征未归一化时非常常见,但在归一化之后也可能发生,原因是学习率太大。如果你发现loss在某次迭代后直接变成nan,把学习率调小三个数量级(比如从0.1调到0.0001),再重新跑。
另外,如果数据中有极端异常值,比如某个PM2.5记录是999或者负数,也会导致梯度爆炸。建议在预处理阶段看一下PM2.5的分布,把明显异常的值剔除或做封顶处理(clip)。
6.5 验证集RMSE计算要注意的事项
作业要求RMSE,计算公式很简单:
rmse = np.sqrt(np.mean((y_pred - y_true) ** 2))但要注意单位。如果预测值是原始尺度(比如几十到几百),RMSE就是5.0这种级别;如果你把y也做了归一化再算RMSE,那算出来的RMSE是归一化尺度下的,需要反归一化回原始尺度再算,否则数字会很小,产生幻觉。
建议:预测结果永远保持在原始尺度,不要对目标值做归一化。如果要归一化,那也一定要用反变换还原后再计算RMSE。
7. 参考实现与结果分析
为了让整个流程更完整,我把最终的参考实现骨架贴出来,并附带我跑出来的典型结果,让你心里有个底。
7.1 完整参考代码骨架
import numpy as np import pandas as pd # 1. 读取数据并转置 df = pd.read_csv('train.csv', header=0, index_col=0) data = df.values.T.astype(np.float32) # 替换NR后转float # 2. 构建滑窗特征 def build_features(data, n_hours=9, target_idx=8): n_samples = data.shape[0] - n_hours X = np.zeros((n_samples, n_hours * data.shape[1])) y = np.zeros((n_samples,)) for i in range(n_samples): X[i] = data[i:i+n_hours].flatten() y[i] = data[i+n_hours, target_idx] return X, y X, y = build_features(data) # 3. 打乱并切分 idx = np.random.permutation(len(X)) split = int(len(X) * 0.8) train_idx, val_idx = idx[:split], idx[split:] X_train, y_train = X[train_idx], y[train_idx] X_val, y_val = X[val_idx], y[val_idx] # 4. 归一化 mean, std = X_train.mean(axis=0), X_train.std(axis=0) std[std == 0] = 1 X_train = (X_train - mean) / std X_val = (X_val - mean) / std # 5. 添加偏置项 X_train = np.hstack([X_train, np.ones((X_train.shape[0], 1))]) X_val = np.hstack([X_val, np.ones((X_val.shape[0], 1))]) # 6. 梯度下降 + L2正则化 w = np.random.randn(X_train.shape[1]) * 0.1 lr = 0.01 lambda_val = 0.001 epochs = 20000 for epoch in range(epochs): grad = (X_train.T @ (X_train @ w - y_train)) / len(y_train) + 2 * lambda_val * w w -= lr * grad if epoch % 1000 == 0: pred = X_val @ w rmse = np.sqrt(np.mean((pred - y_val) ** 2)) print(f"Epoch {epoch}, RMSE: {rmse:.4f}")这段代码跑出来的结果,参考RMSE在5.0左右浮动。不同随机种子、不同学习率、不同正则化参数会有一点差异,但大致在这个区间。
7.2 典型实验结果对照
我用不同的配置做了几组测试,结果如下(RMSE越低越好):
| 配置 | 验证集RMSE |
|---|---|
| 无归一化 + 学习率0.01 | loss无法正常收敛,RMSE高达20+ |
| 有归一化 + 学习率0.01 | 6.20 |
| 有归一化 + 学习率0.001 | 5.80 |
| 有归一化 + L2正则化0.001 + 学习率0.01 | 5.05 |
| 有归一化 + L2正则化0.001 + 学习率0.001 + 5000次迭代 | 5.12 |
| 只用PM2.5单一特征 + 归一化 + L2正则化 | 7.35 |
从这张表能看出来:归一化的作用最大,其次是正则化。学习率的影响主要体现在收敛速度和稳定性上,最终RMSE差别不大。单一特征明显不如多特征,说明其他气象变量提供了有效信息。
7.3 预测结果的分布检查
跑完模型后,我习惯把预测值和真实值画在一起对比,不是必须的步骤,但对于理解模型行为很有帮助。我测试时观察到的基本规律是:
- 对于PM2.5值较低的时段(比如空气质量好的日子,真实值在10以下),预测值往往偏高一点,模型倾向于向均值回归。
- 对于PM2.5值异常高的时段(比如雾霾天,真实值超过100),预测值往往偏低很多。这很正常,因为极端样本数量少,模型很难学到这么极端的模式。
- 整体预测和真实的相关系数在0.9左右,算是不错的水平。
如果你的预测结果在极端情况下偏差特别大,可以考虑单独对目标值做log变换压缩范围,但这会改变RMSE的尺度,作业不一定允许,所以谨慎使用。
8. 如果你是新手,我建议按这个顺序来做
这个作业虽然看起来不复杂,但如果你基础一般,一上来就摊开全部代码反而容易懵。我自己带过一些学弟学妹,发现按下面这个顺序推进效率最高。
8.1 第一步:先跑通最简单的基线
不要一开始就想着加正则化、调学习率、做归一化。先把代码写出来,能跑,能输出一个RMSE,哪怕很烂,也先跑通。
最简单的基线和这个配置:
- 不归一化(或者只做最简单的归一化)
- 不切验证集(或者只做最简单的时间切分)
- 学习率设成0.01
- 迭代2000次
- 看一下loss曲线,看一下RMSE是多少
这个阶段的目标是“跑通”,不是“跑好”。跑通之后你对整个流程有了完整感知,后面再一个个加东西才有参照。
8.2 第二步:加上归一化
这是性价比最高的一步。你会发现加上归一化之后,loss下降速度肉眼可见地变快,RMSE大幅下降。这能帮助你直观理解归一化在数值优化中的重要作用。
8.3 第三步:加正则化、调学习率
在此基础上,再加L2正则化,尝试不同的λ值。同时把学习率从0.1到0.0001都扫一遍,记录不同配置下的RMSE。这个过程能帮你建立起“超参调优”的基本直觉。
8.4 第四步:尝试特征扩展
如果还想追求更好的结果,可以尝试:
- 只用PM2.5单特征做对比实验,体验特征工程的影响。
- 加入前几小时的PM2.5变化率(差分)作为额外特征。
- 尝试不同滑窗长度(比如用12小时预测1小时)。
这个阶段的目标不是非得拿到多高的分,而是通过对实验变量的控制,搞懂每个环节对最终结果的影响有多大。这种能力比死磕一个RMSE数值重要得多。
9. 作业做完之后,你还能往哪些方向延伸
这个作业虽然只是入门级的线性回归,但它背后的思路可以迁移到很多真实场景。我自己做完之后,顺手把代码改了几下,试着预测了其他指标(比如温度、NO2浓度),也都跑通了。这说明整个处理流程不是针对PM2.5定制的,而是一个通用的时间序列预测框架。
9.1 把线性回归替换成其他模型
等你写完线性回归,完全可以把它替换成岭回归、Lasso回归、甚至简单的神经网络,框架不用大改。这也是后续课程作业的常见套路——同样的数据处理流程,换个模型就是新的作业。
我记得当时有同学直接用sklearn的LinearRegression跑了一遍,发现比自己手写的效果略好一点,原因可能是库里的求解器用了闭式解(normal equation),而不是梯度下降。如果你有兴趣,也可以自己实现一下normal equation:
w = (XᵀX)⁻¹Xᵀy
这个方式不需要调学习率,一步到位,但计算量大(需要求逆矩阵),特征维度特别高时不太适用。作为和梯度下降的对比实验,值得跑一遍。
9.2 把滑窗思路迁移到其他领域
滑窗建特征是一种非常通用的方法,不止适用于空气质量预测。股票价格预测、电力负荷预测、传感器异常检测——只要有时间序列数据,都可以用类似的方式构建特征,然后丢给模型。
我有一次帮朋友处理一个工厂传感器数据预测任务,数据格式比这个作业复杂得多,有多台设备、多维指标、缺失率还高。但我发现核心流程和这个作业完全一样:清洗缺失值、滑窗建特征、归一化、训练回归模型。当时我心里暗暗感谢这门课,因为底层思路真的是通用的。
9.3 关于这个作业的延伸思考
这个作业虽然简单,但它隐含了一个现实问题:我们预测空气质量,光靠过去9小时的数据够吗?显然不够。真实的空气质量预测会用到气象预报数据、交通流量数据、工业排放数据,甚至卫星遥感数据。模型也会从线性回归变成随机森林、XGBoost、LSTM等等。但无论模型多复杂,第一步永远是数据处理——把原始观测变成模型能吃的特征矩阵。
所以,这个作业的真正价值,是让你在没有任何ML库辅助的情况下,亲手搭建一遍机器学习流程的全链路。等你在后续课程里学到更复杂的模型时,你会发现,很多难题其实都卡在数据处理上,而模型本身反而是一行代码的事。这个作业就是帮你提前把数据处理这关过了。
10. 写在最后的几条实在建议
我不是什么理论大师,只是把这个作业从头到尾折腾过好几遍,踩了不少坑,下面这些建议都是实操出来的经验。
第一,不要一开始就追求完美,先跑通再说。很多人写代码喜欢一步到位,结果被各种bug卡住,一卡就是半天。先能用、能出结果,哪怕结果很差,你也有了一个参照物。后面的优化每一步都有数据支撑,而不是瞎调。
第二,手动实现一遍梯度下降真的很有用。虽然调库很轻松,但你手写一遍就会对梯度、学习率、损失函数这些概念有更直观的理解。以后遇到模型不收敛的情况,你能更快地定位到问题根源。
第三,多用print和图表看中间过程。我当时每跑一次实验,都会把loss曲线、预测值和真实值的散点图打印出来。这些可视化信息比单看一个RMSE数值有用得多,能一眼看出是否存在过拟合、是否收敛、是否有异常样本。
第四,学会做对比实验。不要满足于“跑出一个结果”,可以试试改变一个变量,看结果怎么变。比如有归一化和没有归一化差多少?有无正则化差多少?不同滑窗长度差多少?这个过程比那个最终分数本身更有价值。
我自己的体会是:这个作业做完,你对“机器学习的项目全流程”就有了最基本的体感。后面学逻辑回归、神经网络甚至深度学习的时候,你会发现很多东西都是在重复这个作业里你已经做过的事:清洗数据、构建特征、调参与评估。把这个作业吃透,后面路会顺畅不少。