资讯详情

资讯详情

随机森林与Django/Vue构建AQI预测系统:从模型训练到前后端联调全攻略

前不久一个学弟抱着选题表来找我开口就问“‘基于随机森林的空气质量指数预测系统’标题里又是大数据又是深度学习还带Django和Vue这我一个人能做完吗”这个场景我太熟了每年毕设季都有同学被这种题目唬住。其实把题目拆开看就是三件事用随机森林训练一个AQI预测模型用Django把模型包成接口用Vue做一个可视化界面。每一件事单拿出来都不算难真正有门槛的是把它们串成一个完整闭环还要在答辩时讲得清楚。这篇文章就按我实际带项目的流程来写从算法选型到前后端联调从数据清洗到答辩准备把关键的坑都提前标出来给准备做类似题目的同学一条能直接照着走的路。1. 选题拆解随机森林、大数据和深度学习这套组合该怎么落地1.1 题目拆开看三个关键词各管哪一段先把这个题目里最容易被误解的概念捋清楚。随机森林是Bagging集成学习算法属于经典机器学习严格说并不算“深度学习算法”——深度学习指的是多层神经网络那一路线比如CNN、LSTM、Transformer。但毕设题目里写“大数据深度学习算法”的同学非常常见这不是什么硬伤关键在于开题答辩时你能不能把逻辑圆回来。我的做法是在系统里加一个“LSTM对比实验”模块。主模型用随机森林做预测并落地部署对比实验用LSTM跑同一份数据说明两者的效果差异。这样一来“深度学习算法”在项目中就有了真实落点而不是只在标题里挂一个名词。至于“大数据”放在毕设语境下通常指海量历史观测数据的存储、清洗和分析流程而不是让你真的搭一套Hadoop集群。如果只有几千天的日级监测数据用MySQL或SQLite完全足够重点是体现出“数据量大时流程是否跑得通”而不是堆机器。技术栈上Django负责后端服务数据存取、训练接口、预测API、用户管理都可以放在这里。Vue负责前端展示空气质量等级看板、历史趋势图、特征重要性柱状图、预测结果页。两者通过RESTful API通信就是典型的前后端分离架构也是现在企业里最主流的Web开发方式之一。1.2 整体技术架构和项目模块划分我习惯把这类系统分成四层来设计答辩画架构图时也清爽层级技术组件核心职责数据层MySQL/SQLite 定时采集脚本存储城市信息、历史监测数据、预测结果算法层scikit-learn pandas joblib数据清洗、特征工程、模型训练与持久化后端服务层Django Django REST Framework提供数据接口、预测接口、模型更新接口前端展示层Vue3 Vite ECharts空气质量可视化、历史查询、预测展示再往下拆项目里至少要有这几个模块数据采集与预处理、随机森林训练与评估、LSTM对比实验、Django API服务、Vue可视化界面。模块之间用清晰的接口边界隔离不要揉成一坨。项目目录我会这样组织air_quality_project/ ├── backend/ │ ├── airproj/ # Django项目配置 │ ├── apps/ │ │ ├── monitor/ # 数据管理模块 │ │ └── prediction/ # 预测API模块 │ ├── ml_models/ # 训练好的模型文件 │ └── manage.py └── frontend/ ├── src/ │ ├── api/ # axios封装 │ ├── views/ # 页面组件 │ └── components/ # 可视化图表组件 └── package.json前后端分离项目最怕的就是代码混在一起。后端只做数据服务和预测服务前端只做展示和交互两边用接口文档对齐字段。这样写代码和写论文都轻松因为你可以分别描述“系统架构”和“模块设计”两章的内容。2. 算法选型的底层逻辑为什么AQI预测更适合随机森林而不是LSTM2.1 随机森林是“一群决策树投票”随机森林的原理一句话就能讲明白训练很多棵决策树每棵树基于不同的数据子集和不同的特征子集来学习最后把所有树的预测结果取平均作为最终输出。打个比方一个专家可能会判断失误但一百个背景各异的专家投票整体判断就会稳定很多。这里的“背景各异”对应的是算法里的两个随机性一是对训练样本做有放回的bootstrap抽样二是每次节点分裂时随机挑选一部分特征来寻找最优分割点。这两个随机操作让每棵树之间产生了“差异”而差异就是集成学习的价值来源。单棵决策树很容易过拟合训练集上表现极好、测试集上一塌糊涂随机森林通过大量低相关树的平均显著降低了方差泛化能力要好得多。而且它对数据的分布没有强假设几乎不需要做特征归一化就能直接用这跟后面要讲的深度学习形成了鲜明对比。在AQI预测这个场景里输入特征是温度、湿度、风速、气压、PM2.5、PM10、SO₂、NO₂、O₃、CO等监测指标输出是AQI数值。随机森林能自动捕捉特征之间的非线性交互关系比如“高湿度高PM2.5”对AQI的叠加影响不需要手写交互项这对环境数据来说非常实用。2.2 和深度学习对比谁更适合这个场景刚接触这个题目时几乎所有人都会想空气质量是一个时间序列那是不是该用LSTM这是直觉但毕设项目里要冷静比较一下成本收益。LSTM的优势在于能建模长距离的时间依赖比如今天污染物的积累会影响明天甚至后天的空气质量。但它有个前提——你得有足够多的连续数据来训练那些门控单元和权重矩阵。我见过不少同学拿两三年的日级数据总共不到1000条样本去训LSTM结果测试集R²还不如线性回归原因很简单数据量撑不起模型复杂度。随机森林对中小规模数据集非常友好几千条样本就能训练出稳定的模型而且超参数没那么多调参成本低。它还能直接输出特征重要性论文里“特征分析”这一章可以直接用模型结果来支撑这一点在答辩时非常加分。我并不是说深度学习完全不能用。如果数据量大到几万条小时级数据LSTM确实可能更强。但那是优化方向不是毕设保底方案。更稳妥的做法是主系统用随机森林确保跑通论文里加一个LSTM对比实验说明两者差异——这既回应了题目里的“深度学习”又体现了你的工作量。对比维度随机森林LSTM数据需求量几千条即可用越大越好小数据易过拟合可解释性特征重要性直接可得黑盒难以解释调参难度少网格搜索可控参数多训练耗时特征缩放不需要必须归一化部署成本模型体积小、加载快需配套深度学习运行时2.3 核心超参数怎么调我的调参经验随机森林虽然调参简单但也不能上来就默认参数跑。我一般按这个顺序调第一是n_estimators即树的棵数。从100开始每增加100棵观察测试集表现一般到300左右边际收益就会变得很小。这个参数不是越大越好越大模型文件越大、加载越慢对API服务的响应时间影响明显。第二是max_depth和min_samples_leaf这两个控制单棵树的复杂度。max_depth过深会让单棵树过拟合min_samples_leaf设置得大一些能强制树学到更泛化的规律。我常用的起点是max_depth15、min_samples_leaf2。第三是max_features节点分裂时随机选取的特征数。回归问题里常用的值是sqrt表示取特征总数的平方根。这个值影响单棵树的多样性也影响训练速度。from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [200, 300], max_depth: [10, 15, 20], min_samples_leaf: [1, 2, 4], max_features: [sqrt] } model RandomForestRegressor(random_state42, n_jobs-1) grid GridSearchCV(model, param_grid, cv5, scoringr2, n_jobs-1) grid.fit(X_train, y_train) print(grid.best_params_)一个经验细节直接跑全量网格搜索很浪费时间我会先在少量数据上用较小的搜索空间看趋势确定合理区间后再把完整训练集放进去跑一次。另外随机森林的random_state要固定否则每次训练结果都不一样论文里的实验数据就没法复现了。3. 数据决定上限清洗流程和特征工程怎么做得扎实3.1 数据来源与字段说明模型效果的上限由数据质量决定这个公式在空气质量预测里体现得特别明显。AQI的官方定义是六个污染物分指数IAQI中的最大值而不是六个污染物的平均所以原始数据里必须包含至少六个国标污染物浓度再加上气象因子模型才有意义。我常用的数据来源是公开的历史空气质量数据集可以拿到城市级别的日级监测数据字段一般包括日期、AQI、PM2.5、PM10、SO₂、NO₂、O₃、CO气象数据用第三方开放平台补齐包含温度、湿度、风速、风向、气压。两份数据按日期做inner join合并就得到一份完整的训练集。字段类型说明date日期观测日期pm25数值PM2.5浓度μg/m³pm10数值PM10浓度so2数值二氧化硫浓度no2数值二氧化氮浓度o3数值臭氧浓度co数值一氧化碳浓度temp数值平均气温℃humidity数值平均相对湿度%wind_speed数值平均风速m/spressure数值大气压强hPaaqi数值空气质量指数标签对于毕设来说一个城市三到五年的日级数据就够用了样本量在一千到两千之间。这里可以跟“大数据”扣一下真正的“大”体现在数据采集、清洗、存储这条链路的工程化而不是堆数据量。在论文里写清楚数据处理流程比只写“数据量大”更有说服力。3.2 缺失值与异常值的处理流程真实环境监测数据远没有教科书数据那么干净。采集设备偶尔停机网络传输丢包都会造成缺失。我处理缺失值的原则是连续缺失不超过3天用前后线性插值填补超过3天直接删除该段记录或用同城市同季度的历史均值补。异常值处理更关键。比如PM2.5出现负值那是设备故障直接剔除出现500以上的极值要人工核查一下是否属实。我写了一个简单的清洗脚本用Z-score方法标记离群点再叠加上限约束import pandas as pd import numpy as np def clean_data(df): df df.copy() for col in [pm25, pm10, so2, no2, o3, co]: df[col] df[col].clip(lower0) mean df[col].mean() std df[col].std() lower, upper mean - 3 * std, mean 3 * std df[col] df[col].mask(df[col] lower, mean) df[col] df[col].mask(df[col] upper, mean) return df.interpolate(methodlinear, limit_directionboth)注意Z-score的3σ阈值在有极端污染事件时会误杀真实值。比如沙尘暴天气PM10上千那可能是真实数据不应当成异常。所以我会先看描述性统计和箱线图结合常识判断再决定是剔除还是保留。3.3 特征相关性分析和一个容易被忽略的细节清洗完之后做一遍相关性分析能帮你快速建立对数据的直觉。我习惯输出一张皮尔逊相关系数热力图重点关注PM2.5与AQI高度正相关温度与PM2.5在冬季表现为负相关低温不利于扩散风速与污染物浓度负相关风越大越容易吹散污染。这些规律写进论文的“特征分析”章节比单纯罗列数据字段有说服力得多。风向这个特征要注意处理方式。风向是0°到360°的循环变量直接把角度作为数值输入模型会让模型误以为350°和10°距离很远实际它们只差20°。我一般把风向拆成sin和cos两个分量保留方向信息的同时避免循环断裂。还有一个容易被忽略到致命的细节训练时特征列的顺序必须和预测时传入的数据顺序完全一致。随机森林对特征顺序不敏感但在工程上问题很大——如果训练时特征是[pm25, pm10, temp, humidity]预测时前端传来的JSON恰好是{temp: 20, humidity: 60, pm25: 80, pm10: 100}你用pandas构造DataFrame时列顺序变了模型虽然不会报错但预测结果会稀烂。解决方法是把训练用的特征列表用joblib单独保存一份预测前强制按这个列表顺序构造DataFrame。另外树模型不需要做归一化这是它和LSTM的一个重要区别。如果你同时跑LSTM对比实验LSTM那边的数据必须做MinMaxScaler归一化而且预测时要逆变换回原始量纲不然输出值会很离谱。4. Django后端模型持久化、REST API与定时训练4.1 数据库表设计与项目结构后端我用的是Django 4 Django REST Framework数据库选MySQL方便展示“大数据量存取”的场景但毕设用SQLite也能跑。表结构不要设计得太复杂能支撑功能就行。我项目中至少需要三张表表名核心字段作用Cityid、name、province、code城市基础信息AirHistoryid、city、date、各污染物浓度、气温、湿度、风速、气压、aqi历史监测数据PredictionResultid、city、date、predicted_aqi、created_at模型预测结果Django项目里我会默认创建一个叫airproj的配置目录然后用python manage.py startapp monitor和python manage.py startapp prediction创建两个业务app。一个app负责历史数据的增删查另一个app负责预测API和模型加载职责分离后代码量会分散读起来不至于一头雾水。4.2 用joblib把随机森林模型装进Django项目模型训练好之后不能每次都重新跑训练你需要把训练结果落盘Django服务启动时加载到内存。sklearn官方推荐的持久化方案是用joblib而不是pickle因为joblib对numpy数组的序列化效率高很多。import joblib # 训练完成后保存 joblib.dump(best_model, ml_models/rf_aqi.joblib) joblib.dump(feature_cols, ml_models/feature_cols.joblib)加载时最忌讳的是在每次预测请求里都load一次模型文件那会让接口响应时间飙到几百毫秒以上。正确做法是做一个单例封装进程内只加载一次后续复用# apps/prediction/services/predictor.py import joblib from django.conf import settings class AQIPredictor: _instance None def __new__(cls): if cls._instance is None: cls._instance super().__new__(cls) cls._instance.model joblib.load(settings.MODEL_PATH) cls._instance.features joblib.load(settings.FEATURE_PATH) return cls._instance def predict(self, row: dict): import pandas as pd df pd.DataFrame([row], columnsself.features) pred self.model.predict(df)[0] return round(float(pred), 2)注意model_path不要写死成绝对路径用BASE_DIR / ml_models / rf_aqi.joblib拼接这样项目换机器也能跑。4.3 API设计、预测流程与跨域处理API我设计了这几个端点够用且不啰嗦GET /api/cities/城市列表GET /api/history/?city_id1某城市历史监测数据供前端画趋势图POST /api/predict/传入当前特征数据返回预测AQIGET /api/features/返回模型的特征重要性供前端柱状图使用预测流程本质上是把前端提交的JSON洗成模型需要的顺序再调用AQIPredictor.predict()返回结果。一个常见的问题是前端传的字段和后端特征列表名称不一致我在序列化器里做了显式字段映射宁可前端多传也不要漏传。跨域问题用django-cors-headers解决在settings里配置允许的来源白名单。开发环境里Vue的代理转发通常指到http://127.0.0.1:8000配置好CORS后两边联调会顺畅很多。CORS_ALLOWED_ORIGINS [ http://localhost:5173, http://127.0.0.1:5173, ]4.4 定时训练与模型更新空气质量预测模型不能训一次就永远用污染排放结构、气象规律都在变化模型需要定期更新。这里我推荐用Django管理命令加系统crontab的方式而不是引入Celery——毕设项目里Celery太重了管理命令加定时任务完全够用。# apps/prediction/management/commands/retrain.py from django.core.management.base import BaseCommand class Command(BaseCommand): help 重新训练随机森林模型并更新持久化文件 def handle(self, *args, **options): # 从数据库拉取最新数据执行清洗和训练 # 训练完成后替换 ml_models 目录下的模型文件 self.stdout.write(self.style.SUCCESS(模型更新完成))crontab配置0 2 * * * cd /path/to/air_quality_project /usr/bin/python3 backend/manage.py retrain logs/train.log 21每天凌晨两点跑一次既避开数据库高峰期又保证第二天的预测用的是最新模型。这个设计在论文里可以写成“模型滚动更新机制”是系统架构上的一个亮点。5. Vue前端可视化大屏的组件拆解与联调细节5.1 脚手架搭建、路由与整体布局前端我是用Vue3 Vite起步UI框架选了Element Plus图表用ECharts。相比Vue2加webpackVite的启动速度在开发阶段优势太明显了改代码热更新几乎是秒级对经常要调接口数据的长开发流程来说非常舒服。路由设计我保持简单明了/dashboard总览看板展示今日实测AQI、预测AQI、等级分布/history历史趋势查询按城市和日期区间筛选/model模型信息页展示特征重要性柱状图和数据概况页面布局统一是顶部标题栏加侧边菜单、右侧内容区的形式。顶部放系统名称和当前时间侧边菜单放路由链接内容区根据路由渲染对应组件。这个布局在视觉上专业实现也不复杂。5.2 axios封装与开发环境代理配置请求封装我习惯创建一个src/api/index.js统一管理接口地址和axios实例import axios from axios const request axios.create({ baseURL: /api, timeout: 15000, }) request.interceptors.response.use( response response.data, error { console.error(接口请求失败:, error) return Promise.reject(error) } ) export const getCityList () request.get(/cities/) export const getHistory (cityId) request.get(/history/?city_id${cityId}) export const predictAQI (payload) request.post(/predict/, payload) export const getFeatureImportance () request.get(/features/)开发环境最大的坑是跨域。前端跑在5173端口Django跑在8000端口浏览器会拦截跨域请求。除了后端配CORS前端更优雅的方案是配置Vite代理让开发服务器的/api请求自动转发到8000// vite.config.js export default defineConfig({ plugins: [vue()], server: { proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true, } } } })这样前端代码里所有请求都写成相对路径部署到生产环境时只要把Vue构建产物交给Django托管同一个域名下不存在跨域问题前端代码一行都不用改。5.3 ECharts可视化组件的动态数据绑定ECharts在Vue里的正确用法是封装成独立的子组件而不是在页面里堆一堆init代码。以空气质量趋势折线图为例父组件从后端拉历史数据通过props传给子组件子组件watch到数据变化后更新图表、必要时重建实例。// components/TrendChart.vue script setup import * as echarts from echarts import { ref, watch, onMounted, onBeforeUnmount } from vue const props defineProps({ data: { type: Array, default: () [] } }) const chartEl ref(null) let chart null function render() { if (!chart) { chart echarts.init(chartEl.value) } chart.setOption({ xAxis: { type: category, data: props.data.map(d d.date) }, yAxis: { type: value, name: AQI }, series: [ { type: line, name: 实测AQI, data: props.data.map(d d.actual), smooth: true }, { type: line, name: 预测AQI, data: props.data.map(d d.predicted), smooth: true } ] }) } watch(() props.data, render, { deep: true }) onMounted(() { render() window.addEventListener(resize, () chart chart.resize()) }) onBeforeUnmount(() { chart chart.dispose() }) /script注意组件卸载时要调用chart.dispose()释放实例否则在Vue的组件切换场景下会出现内存泄漏。窗口resize的事件监听也要在卸载时移除这些都是浏览器环境下ECharts常见的坑。5.4 联调中容易踩的边界情况前端联调阶段我遇到过一个特别隐蔽的问题接口返回的日期字段是Django的ISO格式字符串比如“2024-01-01”但Vue这边某些地方把它当成了时间戳处理导致页面显示NaN。后来统一在axios响应拦截器里把date字段转成标准格式问题才彻底解决。另一个常见问题是模型对极端输入的兜底。如果用户在前端手动输入一组明显不合理的数据比如湿度200、PM2.5为-5模型会输出一个怪异的结果。我的做法是在后端预测之前做简单校验超出合理范围的字段直接拒绝返回400错误提示而不是让模型去“硬算”。毕设答辩时老师问“系统健壮性如何”这一点就能答得很体面。大数据量的渲染性能也要考虑。如果历史数据有好几千条一次性传给ECharts折线图渲染和交互都会卡。批次处理是简单有效的方案前端分页设施加日期区间筛选默认只加载最近90天的数据需要更多再请求下一页。这个优化点不用写太多代码但能让页面流畅度有明显提升。6. 性能验证、答辩准备与踩坑实录6.1 模型评估指标与结果展示方式模型好不好不能靠感觉要用统一的量化指标。我用的是回归问题三件套MAE平均绝对误差、RMSE均方根误差和R²决定系数。MAE和RMSE反映预测误差的大小R²反映模型对真实值变异性的解释程度三者搭配使用。from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score y_test ... # 真实AQI y_pred ... # 预测AQI mae mean_absolute_error(y_test, y_pred) rmse mean_squared_error(y_test, y_pred, squaredFalse) r2 r2_score(y_test, y_pred)以华北某城市连续三年的日级数据为例我跑出来的结果大致是训练集R²接近0.95测试集R²在0.87左右MAE在15上下RMSE在22左右。这个量级的误差意味着模型能把AQI等级判断准确的概率在八成以上对于“轻度污染”“中度污染”的分界判断失误的情况比较少见。论文和答辩里展示模型性能不要只放一堆数字要放一张“真实值-预测值对比图”和一张“残差分布图”。对比图能直观看出两条曲线走势是否贴合残差分布能体现模型是否存在系统性偏高或偏低。这两张图一放实验部分的说服力立刻就不一样了。6.2 答辩高频问题怎么答答辩老师对这类毕设项目的高频提问我总结了五个方向。第一个是“为什么用随机森林而不是深度学习”回答思路是随机森林在中小数据量下稳定、可解释性强、训练成本低同时项目里已加入LSTM做了对比实验。第二个是“数据从哪来”要能说清来源国家和城市、时间跨度、数据量级。第三个是“预测结果怎么验证”除了R²、MAE还可以补充AQI等级预测准确率这个指标更贴近业务含义。第四个是“系统怎么部署”回答DjangouwsgiNginxVue构建后由Django托管静态文件数据库独立部署。第五个是“模型失效了怎么办”回答定时重训加输入校验兜底。这里有个技巧答辩前把你数据里最典型的几组预测案例背下来。比如某年冬季某次重污染过程模型是否提前捕捉到AQI急升趋势。这种具体案例比任何理论表述都容易打动评委也能证明你是真的跑过模型、处理过数据而不是只写了代码没有深入做过实验。6.3 实测过程中踩过的坑最后集中写几个我实测下来最典型、也是最容易让新手卡住的坑。第一个是特征顺序不一致。这个我前面提过但值得再强调一遍训练时用pandas读CSV列顺序是A、B、C、D预测时前端传JSON是D、C、B、A构造DataFrame后模型不会报错但预测值偏差极大。我的对策是把特征列顺序和模型一起持久化所有预测入口都强制用这个顺序。第二个是日期格式和时区的坑。Django默认使用UTC时间前端传“2024-01-01”如果没做时区处理可能在库里存成前一天。空气质量数据分析是精确到天的日期错位一天就会污染训练数据。我在项目里统一启用Django的USE_TZTrue并且所有日期时间都走ISO格式字符串传输不准用时间戳。第三个是模型文件过大的问题。n_estimators调到300棵后joblib文件体积很容易涨到一百多兆。加载一次要几秒钟接口首调超时是常事。我后来在settings里做了一层懒加载第一次请求才初始化predictor并在上方显示loading状态。还有一招是压缩存储joblib.dump(model, path, compress3)能让文件体积缩到原来的三分之一左右。第四个是SQLite并发写入报错。如果你用SQLite存历史数据和预测结果开启多个线程同时写库时会出现“database is locked”。毕设数据量不大可以把所有写数据库的操作都串行化或者直接换成MySQL后者在生产场景下更可靠论文里写“MySQL”也比“SQLite”更符合大数据主题。这个项目带过几轮之后我最大的体会是毕设评价的权重里系统的完整性和实验的严谨性往往比单个模型分数的高低更重要。你不需要把R²做到0.99但你得能说清楚数据怎么来的、模型为什么选它、每次预测的结果如何验证、系统万一出故障怎么兜底。把这些闭环做到位答辩时的底气自然就不一样。真到了做LSTM对比实验觉得耗时太长的时候记住一句话先把主流程跑通所有的“更好”都是它后面的延伸。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →