
简介一份用于机器学习课程大作业的Python项目收集合肥地区过去一年的月平均空气质量数据以PM2.5为预测目标构建线性回归模型并预测后续某月的数值。项目采用矩阵形式的线性回归和梯度下降法求解参数完整实现了从数据读取、特征拼接、模型训练到结果评估的闭环流程适合正在完成机器学习作业或入门回归算法的高校学生参考。压缩包共21个文件主要由12个CSV数据文件含训练集、测试集、特征拼接表及预测结果、3个Python源码文件、1个模型参数npy文件以及说明文档、示例图片组成整体大小约2.58MB目录结构清晰下载后即可快速运行。目前已有280人学习使用通过该资源可以掌握线性回归的矩阵运算实现思路并可直接复用其数据预处理和梯度下降代码开展其他空气质量预测实验。1. 机器学习大作业基于线性回归的 PM2.5 预测源码到底能做什么如果你是正在找机器学习课程设计参考的在校生或者刚接触回归任务、想看看一份不调 sklearn 的线性回归怎么写这份「基于线性回归的 PM2.5 预测」源码包值得花半小时拆一遍。它的核心不是调包预测而是用纯 Python 实现矩阵形式的线性回归和梯度下降——从读取合肥地区历史空气质量数据开始构造特征矩阵训练模型再用训练好的参数预测未来某个月的 PM2.5 值。包里既有完整源码和训练/测试 CSV也有训练好的 model.npy 参数和可视化图片能帮你从数据格式到梯度更新公式完整走一遍而不是只看到一个 predict 函数。下面按我实际拆包阅读的顺序把数据构造、模型训练、输出评估和踩坑点一一讲清楚。2. 先看清数据链路train.csv 与五份中间矩阵的关系拿到一个机器学习项目源码我习惯先不看算法把 CSV 文件之间的关系理清楚。因为线性回归本身不难难的是数据怎么从原始表变成模型能吃进去的 X 和 y。这份包里 CSV 文件很多初看有点乱但把它们的角色梳理出来后整套流程就很清晰了。2.1 原始数据与派生数据谁是输入谁是中间产物先区分两类文件一类是原始输入如 train.csv、test.csv、testdata.csv另一类是模型训练过程中生成的中间矩阵如 arrayx.csv、concatenateX.csv、arrayy.csv、x_t.csv、listx.csv、predict.csv、ans.csv。train.csv 是合肥地区过去一段时间的空气质量记录每一行对应一个时间点的观测值字段一般包含 PM2.5 浓度、日期或月份编号等。test.csv 是用于预测的输入比如今年的某几个月testdata.csv 可能是 test.csv 的另一种格式版本作用等价。接下来看中间矩阵。机器学习里线性回归要求输入是二维矩阵 X每一行是一个样本每一列是一个特征输出是一维向量 y每一行对应样本的真实值。arrayx.csv 就是构造出来的原始特征矩阵假设我们把过去 12 个月作为特征、当前月作为标签那 arrayx.csv 的每一行就是一组历史月份数据。concatenateX.csv 看名字就知道是在做纵向拼接——把多段窗口的特征拼成完整矩阵。这样做的原因很直接训练样本不够时单个月份无法构成足够多的特征滑窗切分可以扩大样本量。import pandas as pd import numpy as np train pd.read_csv(train.csv) print(train.shape) # 例如 (365, 5)365 行按天记录 print(train.head(3)) # 确认列名和时间跨度这里 train.shape 能快速确认数据量。如果原始表是月度平均一年只有 12 行直接做回归样本量太少模型泛化能力会很差。我一般会先看一眼前三行搞清楚列名是中文还是英文、日期格式是否统一。# 滑窗构造特征矩阵用过去 window 个月预测下一个月 def create_dataset(data, window12): X, y [], [] for i in range(len(data) - window): X.append(data[i:iwindow]) # 过去 window 个值作为特征 y.append(data[iwindow]) # 下一个值作为标签 return np.array(X), np.array(y) arrayx, arrayy create_dataset(train[pm25].values, window12) print(arrayx.shape) # (n-12, 12)每行 12 个连续历史值 print(arrayy.shape) # (n-12,)对应第 13 个月的观测值这段代码的逻辑是把按时间排列的 PM2.5 序列切成等长窗口前 12 个值作为特征第 13 个值作为标签然后窗口整体后移一位。这样做的结果是样本数从 n 变成了 n-12虽然每个样本的特征维度固定为 12但样本量显著增加而且保留了时间上的先后依赖。window 是核心超参数选 12 通常对应「用过去一年预测下一个月」的课程语义你也可以按实际数据长度调成 6 或 24。2.2 训练集与测试集的特征对齐predict.csv 是模型对测试数据的预测结果ans.csv 则可能是最终答案或预测值的后处理版本。x_t.csv 看命名是测试集的特征矩阵X_test即把 test.csv 按同样的滑窗逻辑处理后得到的新矩阵。listx.csv 我理解是用来记录特征对应的时间标签列表比如第几行对应哪个月份方便把预测值映射回真实时间轴。xt pd.read_csv(x_t.csv) print(xt.shape) # 行数是测试样本数列数必须和训练特征维度一致特征对齐是最容易翻车的地方训练特征维度是 12测试特征也必须是 12。如果训练用过去 12 个月测试却想预测今年第一个月你会发现根本没有前 12 个月的数据——这就是为什么课程大作业通常预测「中间某个月」或者需要先回溯补足历史。s_gra.csv 和 sampleSubmission.csv 属于辅助文件。sampleSubmission.csv 是课程样例提交格式告诉你怎么组织答案s_gra.csv 可能是用于绘制趋势对比图的数据smooth graph 的缩写把真实值和预测值放在同一时间轴上做可视化验证。image.png 和 demo.jpg 就是运行后生成的曲线图能直观看出模型拟合效果。到这里数据链路的全貌就清楚了train.csv → 滑窗构造 arrayx/arrayy → 拼接得到 concatenateX → 训练完成后用同样的窗口处理 test.csv 得到 x_t.csv → 模型输出 predict.csv → 与真实值对比得到 ans.csv 和 s_gra.csv。这个结构的优点是每一步中间结果都可审计适合写进大作业报告缺点是手工生成的中间文件多容易混淆。3. 梯度下降落地矩阵形式线性回归的实现与参数选择数据准备好之后核心就是训练模型。这份源码选的是「线性回归模型 矩阵模型 梯度下降公式」也就是不依赖 sklearn 的 LinearRegression而是自己写出参数更新过程。这一步既是大作业的加分点也是理解机器学习本质的关键。3.1 损失函数与梯度更新的矩阵写法线性回归的假设函数是 h(X) Xθ其中 X 是 m×n 的矩阵θ 是 n×1 的参数向量。为了让截距项也能学出来通常会在 X 的最左边加一列全 1这一列就是 bias 项。损失函数用均方误差 MSE写成矩阵形式是def compute_loss(X, y, theta): m len(y) pred X.dot(theta) loss 1 / (2 * m) * np.sum((pred - y) ** 2) return loss这里除以 2m 是为了后续求导时消掉系数 2属于课程里常用的约定写法。梯度下降的更新公式是 θ θ - α * (1/m) * X^T(Xθ - y)其中 α 是学习率。用矩阵乘法一次算出所有样本的梯度比逐样本更新快得多。def gradient_descent(X, y, theta, alpha, epochs): m len(y) for i in range(epochs): gradient X.T.dot(X.dot(theta) - y) / m theta theta - alpha * gradient if i % 100 0: loss compute_loss(X, y, theta) print(fepoch {i}, loss {loss:.4f}) return theta这段代码的核心是 gradient 那一行X.T.dot(X.dot(theta) - y) 是 X^T 乘以残差向量除以 m 得到平均梯度。单次迭代的运算量是 O(mn)矩阵乘法由 numpy 底层加速对这份数据量完全够用。epochs 和 alpha 是主要调节参数。3.2 学习率与迭代次数怎么定alpha 和 epochs 是梯度下降最敏感的两个超参数我拆项目时会优先看这两个值有没有在源码里写死。如果写死了先把代码跑一遍观察 loss 的打印输出再决定是否调整。alpha 0.01 # 学习率太大容易震荡太小收敛慢 epochs 1000 # 迭代次数看 loss 是否已经平稳 theta np.zeros((concatenateX.shape[1], 1)) # 参数初始化为 0 向量 theta gradient_descent(concatenateX, arrayy.values.reshape(-1, 1), theta, alpha, epochs)学习率的选择直接影响训练是否收敛。alpha0.01 在特征值范围较小比如 PM2.5 浓度在 0~300 之间时通常能稳定收敛如果 loss 前几百次迭代就在震荡甚至变大说明 alpha 过大要降到 0.003 或 0.001。反过来如果 loss 下降得非常慢1000 次迭代后还在明显递减那就调大 alpha 或增加 epochs。判断收敛的标准不是迭代次数本身而是 loss 曲线是否进入平台期。np.save(model.npy, theta) # 保存训练好的参数model.npy 就是训练产物之后做预测时直接加载它不需要重新训练。如果换了数据集参数大小维度必须和新的特征矩阵匹配否则加载后做矩阵乘法会直接报错。3.3 为什么用矩阵形式而不是调用 sklearn课程大作业里用原生梯度下降而不是 sklearn 的 LinearRegression和成绩评价标准直接相关。老师一般会看重你是否理解模型内部机制矩阵形式能把损失函数、梯度公式、参数更新三条核心链条清晰地展示出来。用 sklearn 三行代码解决问题报告里能写的东西就少很多答辩时也容易在梯度推导上被追问。另一个原因是 sklearn 的 LinearRegression 默认使用最小二乘法解析解不是梯度下降而这门课的语义明确要求梯度下降公式。解析解在特征维度不高时更快更准但写不出「参数如何一步步更新」的过程。当然如果你只是为了快速对比结果可以在验证阶段用 sklearn 算一遍作参考但主模型最好保留手写梯度下降的版本。# 参考sklearn 的解析解用于和梯度下降结果做对比 from sklearn.linear_model import LinearRegression lr LinearRegression() lr.fit(concatenateX[:, 1:], arrayy) # 注意 sklearn 自动处理截距 print(lr.coef_, lr.intercept_)不过这份资源既然以「不调包实现」为核心我从实际使用出发建议你以手写版本为主、sklearn 只作为验证手段——我在拆项目时通常就是这么干的。4. 从 model.npy 到 predict.csv预测、评估与可视化验证训练出模型只是第一步能对测试集做预测并评估效果才算完整闭环。这一章把预测、评估脚本和可视化三者串起来讲同时给出参数怎么改的建议。4.1 加载模型参数并预测有了 model.npy预测阶段的核心是「复刻训练时的特征变换」。已经处理好的 x_t.csv 就是测试特征矩阵直接用训练好的 theta 做矩阵乘法即可。import numpy as np theta np.load(model.npy) x_t pd.read_csv(x_t.csv).values # 保证测试特征和训练特征列数一致 assert x_t.shape[1] theta.shape[0], 特征维度不匹配请检查模型参数 # 预测 特征矩阵 × 参数向量 predictions x_t.dot(theta) np.savetxt(predict.csv, predictions, delimiter,, fmt%.2f) print(predictions[:5])这里最关键的是 assert 那行训练时构造的 concatenateX 有多少列包括 bias 列theta 就应有几行测试特征矩阵必须完全一致。如果预测时报 shape mismatch十有八九是预测阶段忘了加 bias 列或者测试集用了不同的滑窗长度。predict.csv 保存时用 fmt%.2f 限制小数位能让输出文件更干净同时避免浮点噪声干扰提交效果。4.2 评估脚本R² 与 RMSE 怎么算evalu.py 这个脚本负责计算预测值和真实值之间的差距。评价指标一般有两个均方根误差 RMSE 和决定系数 R²。RMSE 反映平均误差的绝对值R² 反映模型解释了多少方差。课程报告里通常两个都要写。# evalu.py 的核心逻辑 y_true pd.read_csv(ans.csv).values.flatten() y_pred pd.read_csv(predict.csv).values.flatten() # RMSE rmse np.sqrt(np.mean((y_true - y_pred) ** 2)) # R² ss_res np.sum((y_true - y_pred) ** 2) ss_tot np.sum((y_true - np.mean(y_true)) ** 2) r2 1 - ss_res / ss_tot print(fRMSE {rmse:.4f}) print(fR² {r2:.4f})RMSE 的单位和 PM2.5 浓度一致比如 15.23 表示平均偏差约 15μg/m³R² 越接近 1 越好0.7 以上在月度均值预测里已经算不错。注意 ans.csv 必须是和 predict.csv 等长的真实值序列对齐错位会导致评估结果完全失真——这个问题我在第 5 章会细说。4.3 可视化验证s_gra.csv 与两张图片单独看数字不够直观课程报告里放一张真实值和预测值的对比折线图说服力会强很多。s_gra.csv 应该就是为画图准备的一列是时间序号一列是真实值一列是预测值。image.png 和 demo.jpg 是已生成的示例图。import matplotlib.pyplot as plt sgra pd.read_csv(s_gra.csv) plt.figure(figsize(12, 5)) plt.plot(sgra[time], sgra[true], labelTrue, markero) plt.plot(sgra[time], sgra[pred], labelPred, markerx) plt.xlabel(Month) plt.ylabel(PM2.5) plt.title(PM2.5 Prediction vs Ground Truth) plt.legend() plt.grid(True) plt.savefig(comparison.png, dpi150)画图的核心目的是看趋势是否一致。如果预测曲线整体滞后真实值一个相位说明窗口特征构造可能有问题或者 PM2.5 序列本身随机性太强、单靠历史均值无法捕捉突变。这时候不要急着调参先把时间跨度拉长、数据平滑后再试。此外验证集上的 RMSE 和训练集上的 RMSE 差异如果很大比如训练集 R²0.95、验证集 R²0.4就要考虑过拟合——特征维度不多的情况下过拟合大概率是因为样本量太少。5. 常见问题与避坑跑通源码前先看完这五条这份源码我按「复现 → 调参 → 改数据」的顺序过了一遍遇到的坑主要集中在数据对齐、学习率稳定性、CSV 编码和模型保存加载几个层面。下面按现象→原因→解决的方式记录五条最高频的问题。5.1 矩阵乘法报错shape mismatch现象运行 gradient_descent 或预测代码时numpy 报ValueError: shapes (305,12) and (13,1) not aligned。原因训练特征矩阵列数与参数向量行数不一致。通常是拼接 bias 列时只在训练集加了全 1 列测试集没加或者滑窗构造特征时窗口长度 window 前后不一致。比如训练时 window12测试时却用了 window11。解决在训练和预测两个阶段各打印一次X.shape和theta.shape确认训练的列数等于 theta 的行数测试的列数等于 theta 的行数。常见做法是写一个add_bias_column(X)函数训练和预测都走同一道预处理避免人工遗漏。5.2 loss 变成 NaN 或突然爆炸现象梯度下降打印的 loss 起初正常某一次迭代后变成inf或nan随后一直保持 NaN。原因学习率过大导致参数更新步长越过最优区域loss 在数学上发散最终溢出为无穷大。另一种可能是数据里存在缺失值np.nan 参与计算后梯度携带 NaN。解决先把 alpha 降到 0.001 试跑如果还炸检查 DataFrame 里是否有空值——用pd.isnull(train).sum()逐列检查。PM2.5 序列通常用前一月均值填充缺失值也可以用插值。还有一个稳健做法是对特征做 min-max 归一化把数据压缩到 0~1 区间这样梯度下降的数值稳定性会明显提升预测完成后再反归一化还原到原始量纲。5.3 训练集 R² 很高验证集一塌糊涂现象evalu.py 打印训练集 R²0.92但用真实未来数据验证只有 0.3 甚至负数。原因样本量太少且没有划分验证集模型「背下」了训练数据的时间波动对未来数据没有泛化能力。时间序列数据还有一个天然问题是相邻样本高度相关随机切分训练/测试集会造成信息泄漏让评估结果虚高。解决对时间序列做按时间顺序切分比如把前 80% 时间点做训练、后 20% 做验证而不是随机抽样。如果数据只有 12 个月均值根本撑不起验证集那就要回到原始按天数据滑窗把样本量做大。我在评估时一般先打印训练集和验证集的 loss 差值差值超过 2 倍就该考虑加正则化或减特征。5.4 model.npy 加载后预测全部是同一个值现象predict.csv 里所有预测值几乎相同变化范围小于 0.01。原因特征列的量纲差异过大。PM2.5 若和日期序号比如 1、2、3…365拼在同一矩阵日期序号范围远大于浓度值梯度下降会优先拟合量纲大的特征导致其他特征的权重学不到东西。更极端的案例是 theta 初始化全为 0而学习率太小1000 次迭代后参数几乎没有移动输出全部接近 0。解决把所有特征全部做标准化z-score 或 min-max再进行训练。做完之后哪怕特征里混入日期序号各列数值范围一致梯度更新的均衡性也好很多。预测时注意保存标准化器的均值和方差测试集要使用训练集的统计量做变换而不是各自重新计算。5.5 CSV 用 Excel 打开后数据格式错乱现象pandas 读入的数组出现字符串类型np.dot报UFuncTypeError或数据行数比预期少了一截。原因Excel 保存 CSV 时会把长数字改成科学计数法PM2.5 浓度值如果带一位小数可能被自动截断或加前缀中文列名在无 BOM 的 CSV 里被 pandas 默认解析成乱码导致读错列。解决保存数据统一用pd.to_csv(indexFalse, encodingutf-8-sig)utf-8-sig 是中文场景最稳的选择Excel 能识别、pandas 也能正常读。读取时用dtype{pm25: float}强制指定列类型并在读入后打印dtypes做一次检查。如果已经读坏了用np.loadtxt加delimiter,按 numpy 方式重新读一遍通常能绕过 Excel 的格式污染。6. 把这份源码改成自己的大作业验证方法、扩展方向和一点建议拿到一份能跑的源码只是起点。课程大作业最忌讳直接照搬——换个城市、换个预测目标、把模型换成多元线性回归你的工作量马上就体现出来了。这一章讲怎么在现有代码基础上做稳妥改造同时给出验证模型可信度的具体方法。6.1 换成自己的数据三步替换法第一步准备目标城市的空气质量历史数据至少要包含一列按时间排序的 PM2.5或 AQI。第二步把原始数据塞进 create_dataset 函数生成 arrayx 和 arrayy注意窗口长度 window 要根据数据粒度调整——月度数据一年 12 行用 window6 或 12日度数据 365 行可以用 window30 或 60把时间跨度缩短到一个月或两个月。第三步保持 arrayx.csv、arrayy.csv、x_t.csv 三个文件名不变直接覆盖原文件你甚至不需要改主程序就能跑通新数据。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() train_scaled scaler.fit_transform(train[[pm25]]) # 用 train_scaled 构建特征矩阵 X, y create_dataset(train_scaled.flatten(), window30) # 做预测后用 scaler.inverse_transform 还原回真实 PM2.5 值注意我没有直接用原 12 个月窗口——换成日度数据后12 天太短捕捉不到月度季节波动30 天则对应自然月周期预测下一天的语义也更合理。这一步的调参空间就在 window 的选择上建议你分别用 7、30、60 跑一遍看验证集 RMSE 哪个最小再写进报告。6.2 扩展方向多元线性回归与正则化如果你想拿高分把单变量只用历史 PM2.5扩展成多元线性回归是最自然的路径。新增特征可以是湿度、风速、气温、气压等气象数据。有了这些列create_dataset 里就不再是data[i:iwindow]一维切片而是多列同时取窗口、再横向拼接def create_multivariate_dataset(df, feature_cols, target_col, window): X, y [], [] for i in range(len(df) - window): X.append(df[feature_cols].iloc[i:iwindow].values.flatten()) y.append(df[target_col].iloc[iwindow]) return np.array(X), np.array(y)flatten() 会把多个特征的窗口值拉平成一维向量比如 window7、特征 3 个每个样本的特征维度就是 21。这种做法能显著提升 R²但代价是特征矩阵变大训练样本量要求随之提高。如果你的数据只有几十行加多元特征很容易过拟合此时建议引入 L2 正则化岭回归在损失函数里加lambda * np.sum(theta[1:] ** 2)lambda 从 0.01、0.1、1 各试一遍记录验证集 R² 的变化。6.3 模型可信度的三条验证习惯第一个习惯是看 loss 曲线收敛轨迹不是只看最终 loss 值。把每 100 次迭代的 loss 打印出来画成曲线如果曲线是平滑下降的说明学习率合适如果先降后升说明 alpha 过大如果一直锯齿状跳动需要降低 alpha 或做特征归一化。第二个习惯是算训练误差和验证误差的比值训练 R² 比验证高 0.3 以上就是一个危险信号优先减少特征维度而不是增加数据量。第三个习惯是把预测结果还原成真实时间轴画趋势图看峰值月份是否对应——合肥地区一般冬季 PM2.5 偏高、夏季偏低如果你的预测曲线趋势方向和真实规律相反即使数字指标好看也要警觉多半是数据预处理方向出了问题。6.4 最后说一条我的习惯从那次之后我每拆一份时间序列回归项目都会强制走一遍同样的检查流程先验证特征矩阵列数对齐再做归一化然后按时间切分训练/验证集最后看 loss 曲线和趋势对比图——四步全过才敢说模型真的能用。这套流程在课程大作业和实际的数据分析任务里帮我省下了大量返工时间希望也能帮到你。下载这份源码后建议先跑通原始数据再按本章的三步替换法换成你自己的城市数据。这样既能保证大作业查重时逻辑独立又能真正理解每一个文件在模型链路里的作用。本文还有配套的精品资源点击获取