简介这是一份面向机器学习初学者的Python课程大作业源码包选择合肥地区过去一年的PM2.5月度数据作为样本完整实现基于线性回归的空气质量预测。项目不仅包含数据读取与清洗、梯度下降公式推导与代码实现、矩阵模型构建还提供了训练好的模型文件和预测结果示例可直接运行或对照学习。包内共21个文件主要有3个Python脚本、12个CSV数据集、1个npy模型文件、若干图片和说明文档整体约2.58MB目录中训练集、测试集、中间数组及结果文件划分明确方便按步骤排查与调参。目前已有280人学习/下载。通过源码可以掌握线性回归在真实数据集上的应用流程学会利用梯度下降更新参数、用矩阵运算加速计算并结合可视化图片、结果示例与项目文档理解课程设计中的关键逻辑适合作为课程作业参考或算法入门实践。1. 机器学习大作业-基于线性回归的PM2.5预测源码能复现到什么程度如果你正在为一门机器学习课程设计找现成项目又不想从零调参基于线性回归的PM2.5预测这种题目看起来朴素实际坑相当多。这个源码包正是拿合肥地区过去一段时间比如过去一年每月的平均值的 PM2.5 数据训练一个线性回归模型预测今年某个月的空气质量值。它把训练、预测、评估、画图四件事都做完了还留下 train.csv、test.csv、模型权重、中间特征矩阵等一整套文件不是那种只有一个主文件的半成品。适合正在做课程大作业、需要复现并在答辩时讲清楚原理的人——先跑通一遍再换成自己所在城市的数据就能交差。2. 数据组织与特征拼接先把 train.csv 和 test.csv 的格式对齐2.1 先认识这几个 csv哪些是输入哪些是中间产物解压后是一个 Machine-Learning-master 目录README.md 和课程样例给了基本使用说明但真正干活的是下面这批文件。我建议拿到手第一件事不是直接跑 PredictionofPM2.5.py而是把 csv 文件按角色分清楚避免后面读错文件。文件角色作用train.csv原始训练输入合肥地区历史 PM2.5 数据可能包含日期和多个污染物列test.csv / testdata.csv测试输入与 train.csv 同格式用于构建测试特征sampleSubmission.csv提交样例输出格式参照最后 predict.csv 可以仿照它写arrayx.csv特征工程中间产物滑动窗口切出来的特征矩阵 Xarrayy.csv特征工程中间产物每个窗口对应的真实值 yx_t.csv / concatenateX.csv特征矩阵调试产物转置或拼接了偏置列的最终矩阵predict.csv预测结果模型在测试集上的输出ans.csv评估对照测试集真实值evalu.py 靠它算误差s_gra.csv绘图数据真实值、预测值、月份的对照表model.npy模型权重训练完的 theta预测时直接加载image.png / demo.jpg / a th.jpg可视化结果损失曲线或预测对比图这些文件里真正要亲手改的是 train.csv 和 test.csv其余中间产物都是脚本生成的。第一批踩坑往往发生在读数据阶段train.csv 的列名和 README 里写的不完全一致或者日期列解析失败。先做一步读取确认import pandas as pd raw pd.read_csv(train.csv) print(raw.columns.tolist()) print(raw.head())逻辑说明先看列名再决定后续按哪一列解析日期、哪一列作为 PM2.5 数值。很多课程数据集的列名带空格或大小写混用直接 groupby 会报 KeyError。参数说明read_csv 的 encoding 参数在遇到中文列名时常用 utf-8-sig否则 Windows 下容易乱码。合肥的数据如果是小时级监测记录需要先聚合到月。摘要里写的是过去一年每个月的平均值所以这个聚合步骤是核心前置操作。def to_monthly(df, value_colpm2.5): df df.copy() df[date] pd.to_datetime(df[date]) df[month] df[date].dt.to_period(M) return df.groupby(month)[value_col].mean().reset_index() monthly to_monthly(raw) print(monthly)逻辑说明to_datetime 负责把字符串日期转成时间类型dt.to_period(M) 把所有日期归到所在月份最后 groupby 按月求平均得到类似2024-01、2024-02...的月均值序列。参数说明value_col 指定目标列如果原始列叫 PM2.5 而不是 pm2.5这里要手动对齐groupby 之后会丢掉其他污染物信息这个作业只需要 PM2.5 单变量回归所以没问题。2.2 特征矩阵怎么拼从 arrayx.csv 到 concatenateX.csv线性回归做时间序列预测核心思路是用过去连续 N 个月的月均值作为特征预测下一个月。这份资源里 window 一般取 6也就是拿前半年拟合后半年。import numpy as np window 6 X, y [], [] for i in range(window, len(monthly)): X.append(monthly[pm2.5].values[i - window:i]) y.append(monthly[pm2.5].values[i]) X np.array(X).reshape(len(X), -1) y np.array(y).reshape(-1, 1) np.savetxt(arrayx.csv, X, delimiter,) np.savetxt(arrayy.csv, y, delimiter,)逻辑说明循环从第 window 行开始每次取前 6 个月的 PM2.5 值作为一条样本第 7 个月的值作为标签。数组最终形状是 (样本数, 6)每一行就是一个滑动窗口。参数说明window 太小模型学不到趋势太大合肥这种一年只有 12 个月均值的数据样本量会急剧减少6 是一个比较平衡的值。样本量不足时也可以把 window 降到 3代价是特征信息变少。这里的 listx.csv 我一般理解为窗口的起始月份编号用来在答辩时说明第 i 行特征对应的是哪 6 个月。它不影响训练但排查特征错位时很有用。x_t.csv 则是一次调试时把特征矩阵转置后保存的视图方便核对矩阵维度主流程不依赖它。真正交给模型的是 concatenateX.csv它比 arrayx.csv 多了一列全 1Xb np.hstack([np.ones((X.shape[0], 1)), X]) np.savetxt(concatenateX.csv, Xb, delimiter,)逻辑说明线性回归的矩阵形式 h_theta(X) X·theta 里theta 的第一项对应截距 bias。如果不拼这列全 1模型只能拟合过原点的直线PM2.5 月均值永远不会是 0预测必然系统性偏低。参数说明np.hstack 是横着拼列要求两个数组行数一致这也是为什么不直接改 arrayx.csv而是另存一个 concatenateX.csv 的原因——保留中间产物便于反复检查。3. 核心实现PredictionofPM2.5.py 里的矩阵模型与梯度下降3.1 模型形式与两个求解方式这份作业的模型是标准多元线性回归h_theta(X) X·theta。X 是上一步拼好的特征矩阵theta 是待学习的权重向量输出是 PM2.5 月均值。梯度下降公式在 README 和课程样例里都给了核心是矩阵形式批量更新theta : theta - (alpha / m) * X.T · (X·theta - y)这个公式里 X.T·(X·theta - y) 是一次性算出所有样本的梯度方向再取平均。为什么要用矩阵形式而不是写 for 循环逐样本更新因为矩阵运算一次把所有样本的误差聚合了训练 2000 轮也就 2000 次矩阵乘法而逐样本更新在月均值这种样本数只有几十的数据集上虽然也能收敛但代码啰嗦且容易在答辩时被追问为什么不用向量化。正规方程是另一个闭式解theta (X.T·X)^(-1)·X.T·y一步出结果。但作业明确要求梯度下降公式所以主脚本走迭代路线正规方程可以作为对照实验。两者结果应当非常接近如果差异过大说明特征没对齐或标准化处理不一致。梯度下降的超参数一般这样设参数含义常见取值说明alpha学习率0.01PM2.5 数值在几十到几百标准化后 0.01 起步安全iters迭代次数2000 到 5000看损失曲线是否已收敛window特征窗口6过去 6 个月预测下一个月m样本数由数据决定12 个月年均值时样本数只有 6 个左右3.2 训练与保存主流程标准化、迭代、落盘 model.npyPredictionofPM2.5.py 的主流程可以拆成四步读 concatenateX.csv 和 arrayy.csv、StandardScaler 标准化、梯度下降训练、np.save 保存 theta。标准化这一步非常关键合肥 PM2.5 月均值秋冬能到 120夏季可能只有 30特征列之间数值差异大梯度下降在这样的尺度下很容易震荡。import numpy as np import pandas as pd from sklearn.preprocessing import StandardScaler X pd.read_csv(concatenateX.csv, headerNone).values y pd.read_csv(arrayy.csv, headerNone).values scaler StandardScaler() X_scaled scaler.fit_transform(X[:, 1:]) X_scaled np.hstack([np.ones((X_scaled.shape[0], 1)), X_scaled]) theta np.zeros((X_scaled.shape[1], 1)) alpha, iters 0.01, 3000 def gradient_descent(X, y, theta, alpha, iters): m len(y) cost_history [] for i in range(iters): h X.dot(theta) errors h - y theta theta - (alpha / m) * X.T.dot(errors) cost float(np.mean(errors ** 2) / 2) cost_history.append(cost) if len(cost_history) 1 and abs(cost_history[-2] - cost) 1e-8: break return theta, cost_history theta_final, costs gradient_descent(X_scaled, y, theta, alpha, iters) np.save(model.npy, theta_final)逻辑说明fit_transform 只对真实特征列做标准化bias 那列全是 1不需要也不应该被标准化。梯度下降内层循环先算预测值 h再算残差 errors然后用 X.T.dot(errors) 得到梯度最后更新 theta。cost_history 记录每轮损失用于判断收敛。参数说明m 是样本总数学习率除以 m 是为了把梯度平均到每个样本1e-8 的阈值是提前停止条件损失变化小于这个值就认为收敛可以手动调小但没必要3000 轮在这个数据量下通常一两秒就跑完。预测脚本加载 model.npy 时有一个容易忽略的点model.npy 里只存了 theta没有存 scaler 的 mean 和 std。所以预测时要么重新读训练集做一遍 fit要么把均值方差也存下来。这份资源里预测脚本的做法是重新加载 train.csv 重建特征再做同样的标准化实际跑的时候我建议直接改成保存三个数组到同一个 npz 里省得每次重新 fit。注意np.save 保存的是一维数组时加载后要确认 shape 是否为 (特征数, 1)否则后续矩阵乘法会广播出形状错误。4. 评估与可视化evalu.py 和 s_gra.csv 怎么配合用4.1 评估指标按什么口径算训练完不能只看训练集损失小就交差测试集才是老师真正看的东西。这份资源里的 evalu.py 读 predict.csv 和 ans.csv逐行对比预测值和真实值。评估指标建议算三个RMSE、MAE、R²。import numpy as np import pandas as pd pred pd.read_csv(predict.csv)[pm2.5].values true pd.read_csv(ans.csv)[pm2.5].values rmse np.sqrt(np.mean((pred - true) ** 2)) mae np.mean(np.abs(pred - true)) r2 1 - np.sum((true - pred) ** 2) / np.sum((true - np.mean(true)) ** 2) print(fRMSE{rmse:.2f} MAE{mae:.2f} R2{r2:.3f})逻辑说明RMSE 对大误差敏感预测值里如果有一个月和真实值差了 40 µg/m³RMSE 会被明显拉高MAE 更平均地反映整体偏差R² 表示模型解释了真实值方差的多少越接近 1 越好。参数说明真实值和预测值里如果存在 NaNpandas 默认在求和时会变成 NaN所以评估前最好加 np.nan_to_num 或 dropna。合肥月均值量级一般在 30 到 120 之间RMSE 在 10 左右已经算不错R² 在小样本上容易虚高不要只看 R²。4.2 把真实值和预测值画到同一张图s_gra.csv 是绘图直接的数据源里面通常有 month、true、pred 三列。训练完模型后用测试集月份生成预测值再把真实值和预测值拼进同一个 DataFrame最后存成 s_gra.csv 并画图。import matplotlib.pyplot as plt import pandas as pd df pd.read_csv(s_gra.csv) plt.figure(figsize(10, 4)) plt.plot(df[month], df[true], labeltrue, markero) plt.plot(df[month], df[pred], labelpred, markers) plt.legend() plt.savefig(demo.jpg, dpi200) plt.close()逻辑说明真实值用实线圆点预测值用方块线两张图叠在一起能直观看出哪个月预测偏大、哪个月偏小。demo.jpg 应该就是这一步的产物image.png 和 a th.jpg 可能是不同窗口或不同学习率下的对比图。参数说明dpi200 保证图片在论文里插进去不模糊列名如果和上面代码不一致先用 df.columns.tolist() 看实际列名再替换 df[month] 里的字段名。s_gra.csv 的另一个用途是存档。答辩时老师问你哪个月预测最差直接打开 csv 按残差排序就能回答。我一般会额外加一列 abs_error方便按误差大小过滤。df[abs_error] (df[true] - df[pred]).abs() print(df.sort_values(abs_error, ascendingFalse).head())逻辑说明按绝对误差降序排排在最前面的就是模型表现最差的两个月通常对应秋冬季节的突变天。参数说明ascendingFalse 是降序head(3) 取前三行用来在答辩材料里写模型在污染突变月份误差较大这种结论。5. 避坑指南合肥PM2.5预测里最容易翻车的五个细节5.1 损失不降反升指数爆炸和 NaN 都是同一个原因现象gradient_descent 循环跑完print(cost_history) 发现损失不是下降而是每次都变大甚至跑到几十轮后出现 NaN。原因特征列没标准化PM2.5 数值在 120 时 X.T.dot(errors) 的结果量级会非常大alpha 虽然设了 0.01但乘上去之后 theta 直接飞掉。解决先做 StandardScaleralpha 从 0.001 开始试并且每 50 轮打印一次损失alpha 0.001 for i in range(iters): ... if i % 50 0: print(fiter {i}, cost {cost:.4f})逻辑说明alpha 太小收敛慢alpha 太大直接震荡0.001 到 0.01 之间是标准化后的安全区间。参数说明打印频率 50 轮一打印足够看到趋势不用每轮都打。5.2 时间序列不能随机 split不小心就偷看了未来现象作业里用了 train_test_split(X, y, random_state42)训练集 RMSE 好得离谱但换成测试集后预测曲线明显滞后。原因时间序列数据一旦随机划分后半段的样本会混进训练集模型提前学到了未来的走势测试集反而只留下随机噪声。解决按时间顺序划分train_test_split 里强制 shuffleFalsefrom sklearn.model_selection import train_test_split X_tr, X_te, y_tr, y_te train_test_split( X, y, test_size0.3, shuffleFalse, random_state0 )逻辑说明shuffleFalse 保持原始顺序前面的月份进训练集后面的月份进测试集模拟真实预测场景。参数说明random_state 固定成 0 是为了每次跑结果一致如果数据只有 12 个月均值test_size 不要取太大留 3 个月做验证差不多。5.3 model.npy 的维度对不上忘了 bias 那一列现象训练时读的是 concatenateX.csv特征矩阵有 7 列1 列 bias 6 列特征预测时只读了 6 列特征结果 np.load(model.npy) 之后 dot 报错或者没报错但所有预测值都整体偏移一个常数。原因theta 的第一个分量是截距 bias预测脚本必须给输入矩阵同样拼一列全 1。解决把训练时的偏置拼接逻辑复制到预测脚本X_test pd.read_csv(test.csv, headerNone).values X_test_scaled scaler.transform(X_test) X_test_scaled np.hstack([np.ones((X_test_scaled.shape[0], 1)), X_test_scaled]) pred X_test_scaled.dot(theta_final)逻辑说明预测端的特征构造必须和训练端完全一致包括窗口大小、特征顺序、是否加 bias 列。参数说明scaler 要用训练时那个 fit 过的 scaler不能在测试集上重新 fit否则分布就变了。5.4 0值到底是 0 还是缺测噪声数据会拖低整条曲线现象模型在冬季月份预测整体偏低残差图里有一两个点特别尖。原因PM2.5 监测仪器在故障或断电时会输出 0这个 0 不是真实浓度而是机器学习的噪声数据。如果直接用 0 值参与训练模型会以为那几个月空气质量特别好把权重往低拉。解决把等于 0 的值先替换成 NaN再插值monthly[pm2.5] monthly[pm2.5].replace(0, np.nan) monthly[pm2.5] monthly[pm2.5].interpolate()逻辑说明replace(0, np.nan) 把噪声标记成缺失interpolate 用线性插值填补。参数说明interpolate 默认线性法对月均值这种低频序列足够如果连续多个月都是 0说明那段时间数据质量太差直接剔除比填充更安全。5.5 中间 csv 的列名是最好的排查抓手现象读 arrayx.csv 时pandas 自动把列名设成 0 到 5读 concatenateX.csv 时又把第一行当成了列名导致特征矩阵少一行或维度对不上。原因np.savetxt 保存时没有写表头而 pd.read_csv 默认认为第一行就是列名。解决读中间 csv 一律显式指定 headerNone再用数值下标取列X pd.read_csv(arrayx.csv, headerNone).values y pd.read_csv(arrayy.csv, headerNone).values逻辑说明headerNone 告诉 pandas 所有行都是数据列名用 0 开始的整数替代。参数说明.values 会把 DataFrame 转成 ndarray后续矩阵运算需要的是 ndarray 而不是 DataFrame这个转换别省。6. 进阶把模型权重拿出来做特征排序和提前收敛6.1 标准化后的系数绝对值就是特征重要度线性回归出了名的可解释性强。模型训练完model.npy 里存着的 theta 直接反映了每个输入特征对 PM2.5 的贡献方向。特征全部标准化后权重绝对值越大说明该月份对预测目标的影响越强。theta np.load(model.npy).flatten() weights theta[1:] rank np.argsort(np.abs(weights))[::-1] for i, idx in enumerate(rank): print(f前第{i1}重要: 第{idx1}个月前值, 权重 {weights[idx]:.3f})逻辑说明theta[1:] 去掉 bias因为截距只负责整体平移不参与特征排序。np.argsort 返回排序后的索引[::-1] 翻转成从大到小。输出结果可以直接写进大作业的特征分析章节解释为什么春秋季节的 PM2.5 更容易受前一个月影响。参数说明这里比较的前提是训练时做了 StandardScaler否则不同月份数据的数值尺度不同权重不可比。6.2 用损失曲线判断要不要加大迭代次数gradient_descent 返回的 cost_history 不只是用来确认收敛还能判断当前数据量下模型是否欠拟合。把曲线画出来import matplotlib.pyplot as plt plt.figure(figsize(8, 3)) plt.plot(costs) plt.xlabel(iterations) plt.ylabel(cost) plt.savefig(image.png, dpi150) plt.close()逻辑说明如果曲线在最后几十轮还在明显下降说明 iters 不够加迭代如果曲线快速收敛后平坦说明 3000 轮足够。image.png 在原始文件里就是一张这类图我推测就是某次训练保存下来的损失曲线。参数说明costs 是梯度下降函数返回的列表长度等于实际迭代轮数提前停止会让它小于 iters。从那以后我每次提交大作业前都会强制走一遍完整流程训练模型、跑 evalu.py、看 s_gra.csv 画出的曲线确认 RMSE 没有突然恶化、预测值和真实值没有整体错位再导出 predict.csv。这个习惯帮我挡过至少两次因为忘加 bias 列而导致的翻车。希望帮到你。本文还有配套的精品资源点击获取