简介:基于LSTM与Django构建的空气质量监测及预测系统,是一份完整的高分毕业设计项目,面向计算机相关专业学生与Python初学者,适用于毕业设计、期末大作业或课程设计场景。系统涵盖空气质量数据采集、LSTM时序预测模型、Django后端逻辑、前端可视化与后台管理模块,代码注释详细,简单修改配置即可部署运行。压缩包共260个文件、约7.07MB,包含15个Python脚本、15个依赖缓存文件、164个SCSS样式及JS/CSS等静态资源,还有HTML模板、CSV历史与实时数据集、SQLite数据库,以及地图与项目配置文件,目录结构清晰,便于逐层阅读。从内容预览可见,项目提供实时监测、历史趋势、省份分析等多个页面,并配套pm2.5数据集,覆盖从数据处理到预测展示的完整链路。目前已有384人学习下载,适合需要快速搭建空气质量系统或学习LSTM+Django整合的读者,可直接作为毕设源码运行,也方便二次开发与论文对照。
1. 空气质量监测及预测系统到底解决什么问题:从 PM2.5 历史数据到未来浓度的完整闭环
在毕业设计里选「Python实现基于LSTM+Django的空气质量监测及预测系统」这个题目,很多人第一反应是"能跑起来给老师演示就行",但真正让答辩加分的是把 LSTM 和 Django 两条技术线揉在一起的能力。Django 负责把监测数据的采集、存储、展示串成完整业务闭环,LSTM 负责从历史时序数据里学出 PM2.5 的变化规律并预测未来若干小时的浓度。这套系统不是纯理论的神经网络 Demo,而是一个能落地、能演示、能扩展成真实监测平台的最小可用项目,适合想同时展示后端开发和深度学习能力的同学。
2. 系统拆解与选型:LSTM 凭什么预测空气质量,Django 在系统里管哪一块
2.1 监测与预测两条主线的职责划分
写代码之前先做需求拆分。常见的做法是把系统拆成两条相对独立的主线:监测线和预测线。
监测线负责数据采集、入库和展示。数据采集器定时从空气质量监测站点抓取 PM2.5、PM10、SO2、NO2、CO、O3 六项常规污染物的逐小时浓度,落地到数据库,然后通过网页把历史变化趋势用折线图展示。这条线技术含量不高,但它是整个系统的数据基础。没有干净完整的历史数据,LSTM 模型再厉害也只会空转,预测精度没有任何保障。
预测线负责从历史数据里学习规律并输出未来值。它的工作流程是:把历史数据按固定时间窗口切成训练样本,用 LSTM 训练网络学习污染物的时变规律,训练完成后把模型参数保存为文件。系统运行时,Django 后端加载这个模型文件,接收最近 N 小时的监测数据,返回未来 M 小时的预测浓度,前端再把预测曲线续在历史曲线后面。
两条线的衔接通过数据表完成。监测表存原始数据,预测表存模型输出的结果,页面上共用同一个时间轴。顺序不能颠倒,一定是先跑通监测线再做预测线。见过不少同学先花两个星期调 LSTM 参数,最后发现数据采集模块还没写,只能手工造小批量样例来演示,时间全浪费了。
我一般建议按「数据采集接入 → 数据库建模 → LSTM 训练脚本 → Django 接口串联 → 前端图表联调」的顺序推进,每一步都有可验证的中间产物。数据采集后先查数据库确认有数,LSTM 训练后先画出预测曲线确认趋势跟得上,最后才进 Django 做接口。这样做不会到联调阶段才发现链路断了,留给答辩的时间也更充裕。
2.2 LSTM 门控机制与时间序列预测原理
LSTM(Long Short-Term Memory)是循环神经网络 RNN 的一种变体,专门解决普通 RNN 在长序列上的梯度消失问题。普通 RNN 反向传播时,梯度要沿着时间步连乘,序列一长,梯度要么指数爆炸要么指数衰减,网络学不到久远时刻的信息。PM2.5 浓度受前一天甚至前几天的气象条件影响,这种长期依赖恰恰是普通 RNN 的软肋。
LSTM 的核心设计是在每个时间步维护一条细胞状态(cell state),像传送带一样承载信息,通过三个门控机制控制信息的写入与丢弃。遗忘门决定上一时刻的状态中哪些信息要遗忘,输入门决定当前时刻的新信息写入多少,输出门决定当前时刻输出哪些信息。这种结构让 LSTM 能在几十个时间步的跨度内保留关键特征,比普通 RNN 稳定得多。
具体到本项目,PM2.5 小时浓度是一个典型的单变量或多变量时间序列。单变量方案只把 pm25 序列作为输入,多变量方案把 PM10、SO2、NO2、CO、O3 以及可选的气象变量一起作为输入。多变量在理论上能捕捉污染物之间的相关性和气象对扩散条件的影响,效果通常更优,但特征对齐和数据采集的范围也更大。
对有毕业设计需求的同学,我的建议是先做多变量的小步尝试,至少六项污染物都放进去。训练时经常会发现加入其他污染物后 MAE 比单变量低,答辩时你能拿出对比数据讲。相比之下只拿 pm25 一条线做,模型结构太简单,论证深度不够。热词搜索里「空气质量预测」相关的资料也大多按多变量 LSTM 展开,顺着这个方向做,参考实现更容易找到。
2.3 Django 的 MTV 架构如何承接业务闭环
Django 在本项目里承担数据模型定义、接口暴露、页面渲染三层职责。MTV 模式下 Model 和数据表对应,Template 负责 HTML 渲染,View 负责业务逻辑与数据传递。URL 路由在两者之间做映射,没有独立的 Controller。
具体到实现,AirQuality 模型对应监测数据表,Prediction 模型对应预测结果表。Model 定义完后,Django admin 后台自动生成增删改查界面,调试时值非常方便。View 层写两类代码:一类是页面渲染视图,把查询结果传给模板;另一类是 JSON 接口,供前端图表异步拉取数据。URL 层把 /dashboard/、/api/history/、/api/predict/ 分别映射到对应的视图函数。
为什么选 Django 而不是 Flask 或 FastAPI?Django 自带 admin、ORM、模板引擎、表单校验,能省掉大量重复工作。ORM 对 SQLite 到 MySQL 的切换只需要改配置,迁移命令也是现成的。Flask 更轻,但所有组件要自己拼装,开发周期拉长。FastAPI 性能好且自动生成接口文档,可它对模板渲染和 admin 支持偏弱,如果前端不是前后端分离架构,用起来不如 Django 顺手。以「快速做出完整系统」为首要目标,Django 是低成本的选择。
3. 数据准备与 LSTM 模型训练:从原始数据到能出预测结果的模型文件
3.1 数据来源与预处理
空气质量数据常用的公开来源有中国环境监测总站的逐小时发布数据,以及各类开源数据集。爬虫方案比较常见,但需要注意目标页面结构经常变化和反爬机制的存在。更稳妥的做法是下载现成的历史数据集转成 CSV,再把 CSV 导入数据库,保证训练可复现。
拿到原始数据后第一件事不是写模型,而是清洗。典型问题有三个:时间戳不连续、单小时数据缺失、极端异常值。时间戳不连续直接导致序列断裂,需要重采样补齐;缺失值我一般先用前向填充(forward fill),连续缺失超过 3 小时则用前后均值插值;异常值用 3σ 原则识别,也就是数值偏离均值超过三倍标准差就视为异常,替换成相邻时刻的均值。
清洗后的数据统一存成 DataFrame,索引设为时间戳,列包括 pm25、pm10、so2、no2、co、o3,如果还有气象站在加 temperature、humidity、wind_speed、pressure 等字段。列顺序在整个项目里要保持一致,后期写 Django 接口时要读取哪些字段,在训练脚本里就固定下来,否则模型接口和训练脚本容易字段对不上。
3.2 滑动窗口样本构造与归一化
LSTM 训练样本不是独立的一行行数据,而是滑动的序列窗口。假设要用过去 24 小时预测未来 1 小时的 PM2.5 浓度,那一个样本就是连续 24 行的特征矩阵,标签是第 25 行的 PM2.5 值。窗口滑动步长为 1 小时,这样 1000 小时的原始数据大约能构造出 975 个样本。
时间步长 time_step 是必须调的超参数,常见取 24、48 或 72。取 24 意味着空气质量的日变化周期完整包含在窗口里,一般比取 12 效果好很多。窗口太长计算量变大,模型未必能从更多历史里提炼出有效信息,没有明显提升时不要太贪心。
归一化使用 MinMaxScaler,把所有特征压缩到 0 到 1 之间。这里有一个毕设中特别容易踩的坑:MinMaxScaler 必须在训练集上 fit,再用同一个 scaler 去 transform 测试集。如果先对全量数据 fit 再划分,测试集的信息泄漏进训练集,测试指标虚高,答辩被问两句就露馅。
from sklearn.preprocessing import MinMaxScaler # 假设 df 是清洗后的 DataFrame,按时间升序排列 feature_cols = ['pm25', 'pm10', 'so2', 'no2', 'co', 'o3'] data = df[feature_cols].values # 先划分再归一化,测试集不参与 fit train_size = int(len(data) * 0.8) train_raw, test_raw = data[:train_size], data[train_size:] scaler = MinMaxScaler(feature_range=(0, 1)) scaler.fit(train_raw) # 只用训练数据估计 min/max train_scaled = scaler.transform(train_raw) test_scaled = scaler.transform(test_raw)这段代码的关键是先按时间顺序切出 80% 作为训练集,在训练集上完成 scaler 的拟合。注意 feature_range 默认是 (0, 1),如果数据分布特别偏,也可以考虑 RobustScaler。训练集和测试集分别 transform 之后,接下来才能构造窗口样本。
构造窗口样本的循环如下。窗口滑动采用复制切片操作,内存开销略大,但代码清晰、不会出越界错误,适合毕设阶段的工程量级:
import numpy as np def create_sequences(data, time_step=24): X, y = [], [] for i in range(len(data) - time_step): X.append(data[i:i+time_step, :]) y.append(data[i+time_step, 0]) # 预测目标为 pm25 列 return np.array(X), np.array(y) X_train, y_train = create_sequences(train_scaled, time_step=24) X_test, y_test = create_sequences(test_scaled, time_step=24) # 输出形状:(n_samples, time_step, n_features) print(X_train.shape, y_train.shape)X 的形状是 (样本数, 时间步长, 特征数),LSTM 的输入要求就是这种三维张量。把 pm25 放在 feature_cols 第一位,y 取第 0 列就是 PM2.5 的标签。实际代码里列顺序如果不同,索引也要跟着改,别抄完就忘这个前提。
3.3 搭建 Keras LSTM 网络并训练
模型搭建用 Keras 最直观。两层 LSTM 加一层全连接,中间用 Dropout 防过拟合。第一层 LSTM 带 return_sequences=True,输出完整序列给第二层;第二层不带 return_sequences,只输出最后一个时间步的隐含状态,再接 Dense 输出单值。
import tensorflow as tf from tensorflow.keras.models import Model, load_model from tensorflow.keras.layers import LSTM, Dense, Dropout, Input from tensorflow.keras.optimizers import Adam # time_step 和 feature_num 要与构造样本时保持一致 feature_num = X_train.shape[2] inp = Input(shape=(time_step, feature_num)) x = LSTM(64, return_sequences=True)(inp) x = Dropout(0.2)(x) x = LSTM(32, return_sequences=False)(x) x = Dropout(0.2)(x) out = Dense(1)(x) model = Model(inp, out) model.compile(loss='mse', optimizer=Adam(learning_rate=0.001), metrics=['mae']) print(model.summary())第一层 LSTM 用 64 个神经元,第二层 32 个,是中小规模时序任务的常见起点。Dropout 加在两层 LSTM 输出之后,取 0.2,能抑制过拟合又不会明显欠拟合。损失函数用均方误差 MSE,因为预测目标是连续数值;二分类任务才用交叉熵。Adam 优化器初始学习率 0.001,一般不需要手动衰减,配合后续回调就够。
训练时加上早停和模型保存两个回调。EarlyStopping 监控验证集损失,patience 设 10,连续 10 个 epoch 没有改善就停止。ModelCheckpoint 保存验证损失最小的权重,防止训练过程中最好的一版被后期覆盖。
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint early_stop = EarlyStopping(monitor='val_loss', patience=10, verbose=1) checkpoint = ModelCheckpoint('models/air_lstm.h5', monitor='val_loss', save_best_only=True, verbose=1) history = model.fit( X_train, y_train, batch_size=32, epochs=100, validation_split=0.1, callbacks=[early_stop, checkpoint], verbose=1 )注意 validation_split=0.1 表示从训练集尾部切 10% 作为验证集,是尾部而不是随机抽样,因为时间序列随机打乱会破坏时序依赖。epochs 不要一开始就设很大,先用 50 试跑,观察 val_loss 走势再决定增减。训练时间方面,几千条样本在两三层 LSTM 上,CPU 跑 50 个 epoch 也就几分钟,没必要急着上 GPU。
3.4 训练过程评估与模型导出
训练完成不是看训练集损失有多低,关键是验证集损失和测试集的实际预测效果。先用 history 回调画出损失曲线,确认训练集和验证集两条曲线没有明显分离。如果 val_loss 在某个 epoch 后持续升高而 train_loss 还在下降,就是典型的过拟合信号,把 Dropout 调大或把 LSTM 神经元减半再试。
测试集上的验证逻辑是拿模型预测输出逆归一化,再和真实值对比,计算平均绝对误差 MAE 或均方根误差 RMSE。注意逆归一化必须用之前保存的那个 scaler,不能重新 fit,否则量纲对不上。
from sklearn.metrics import mean_absolute_error, mean_squared_error # 在测试集上预测并逆归一化 pred_scaled = model.predict(X_test) # scaler 的 inverse_transform 需要完整特征维度 pred_full = np.zeros((len(pred_scaled), len(feature_cols))) pred_full[:, 0] = pred_scaled[:, 0] pred_inv = scaler.inverse_transform(pred_full)[:, 0] # y_test 同样是归一化后的 pm25 值,做逆变换获取真实量纲 y_test_full = np.zeros((len(y_test), len(feature_cols))) y_test_full[:, 0] = y_test y_test_inv = scaler.inverse_transform(y_test_full)[:, 0] mae = mean_absolute_error(y_test_inv, pred_inv) rmse = np.sqrt(mean_squared_error(y_test_inv, pred_inv)) print(f'MAE: {mae:.2f} ug/m3, RMSE: {rmse:.2f} ug/m3')MAE 在 10 μg/m³ 以内对小时尺度预测来说已经是不错的表现。把逆归一化的真实值和预测值画在同一个时间轴上,人工观察曲线跟随程度,这一步在答辩时是必须展示的,远比贴一堆训练指标有用。模型保存为 Keras H5 格式,后面 Django 接口通过 load_model 加载使用。
4. 用 Django 把模型接成系统:数据入库、预测接口与前端图表
4.1 Django 项目骨架与 App 划分
Django 项目创建好之后,按业务边界拆 App。常见做法是把 App 拆成三个:core 负责页面渲染和基础运维,monitor 负责监测数据采集与展示,predict 负责 LSTM 模型加载和预测接口。App 拆细一点,路由和模型文件更清晰,也比全部塞在单 App 里好维护。
建立工程骨架的命令如下:
django-admin startproject air_quality_sys cd air_quality_sys python manage.py startapp monitor python manage.py startapp predict python manage.py startapp core在 settings.py 里注册这三个 App,然后配置数据库。毕设阶段直接用默认 SQLite 就能跑通,几万行数据毫无压力。如果数据量大或老师要求 MySQL,再按 django.db.backends.mysql 改配置,需要额外安装 mysqlclient 依赖。
4.2 数据模型定义与后台管理
monitor App 里的 AirQuality 模型定义如下。时间用 DateTimeField,六项污染物用 FloatField,再加一个采集源标记字段。预测结果模型单独放在 predict App,存预测时间和各污染物预测浓度。
from django.db import models class AirQuality(models.Model): station = models.CharField(max_length=50, default='default') monitor_time = models.DateTimeField(db_index=True) pm25 = models.FloatField(null=True, blank=True) pm10 = models.FloatField(null=True, blank=True) so2 = models.FloatField(null=True, blank=True) no2 = models.FloatField(null=True, blank=True) co = models.FloatField(null=True, blank=True) o3 = models.FloatField(null=True, blank=True) class Meta: db_table = 'air_quality' ordering = ['monitor_time']db_index=True 给 monitor_time 加索引,时间范围查询会明显变快。null=True 对应数据库允许空值,实际清洗时缺失值已处理过,这里留空是为了兼容早期导入数据时可能出现的空字段。ordering 指定默认排序,前端查历史数据时少写一个 order_by。
用 admin 注册模型,在浏览器里检查数据内容很方便:
from django.contrib import admin from .models import AirQuality admin.site.register(AirQuality)注册完执行 python manage.py makemigrations 和 python manage.py migrate 生成数据库表,例行操作,不需要手动写 SQL。
4.3 数据采集与入库
监测数据来源如果是爬虫,最稳妥的做法是写一个独立 Python 采集脚本,定时抓取数据后批量写入数据库。批量写入用 bulk_create 而不是逐条 save。
# monitor/crawler.py import requests import pandas as pd from .models import AirQuality def fetch_realtime(): # 假设从接口拿到 JSON,这里用简化的 mock 数据结构 url = 'http://your-data-source/api/realtime' resp = requests.get(url, timeout=10) records = resp.json()['data'] objects = [] for r in records: obj = AirQuality( monitor_time=r['time'], pm25=r['pm25'], pm10=r['pm10'], so2=r['so2'], no2=r['no2'], co=r['co'], o3=r['o3'], ) objects.append(obj) AirQuality.objects.bulk_create(objects) # 一次批量写入注意出现重复时间戳会直接产生重复记录,所以批量写入前先用 monitor_time 做去重判断,或者给 monitor_time 加 unique 约束。批量写入在大数据量时效率提升明显,几万条写入只是秒级。
定时调度用系统 crontab 就行。在 Linux 服务器上最简单是每小时跑一次采集脚本:
0 * * * * cd /path/to/project && python manage.py runscript monitor.crawler.fetch如果没有 django-extensions,可以写成独立脚本,通过 os.environ.setdefault 提前导入 Django 环境再调用。
4.4 把 LSTM 模型封装成预测接口
预测接口是系统最核心的一段逻辑。写一个独立 service 模块负责加载模型和预处理数据,视图只负责接收请求、调用 service、返回 JSON。这样模型加载一次后就驻留在进程内存里,不会每个请求都重新读 H5 文件。Keras 模型在 Django 开发服务器下重复加载很慢,冷启动一下就要几秒,必须做全局缓存。
# predict/services.py import numpy as np from tensorflow.keras.models import load_model from monitor.models import AirQuality _model = None def get_model(): global _model if _model is None: _model = load_model('models/air_lstm.h5') return _model def build_recent_input(hours=24): # 查最近 hours 条数据,逆序排列成时间升序 rows = AirQuality.objects.order_by('-monitor_time')[:hours] rows = list(reversed(rows)) # 列顺序必须与训练时一致 arr = np.array([[r.pm25, r.pm10, r.so2, r.no2, r.co, r.o3] for r in rows]) return arrmodel 加载放在模块级变量,用 None 判断做单次加载。同一进程多线程并发调用 Keras 模型预测时存在资源竞争,毕设场景最好加 threading.Lock 把预测包起来,这个细节放到第五章专门讲。视图函数使用 JsonResponse 返回 JSON:
# predict/views.py import json from django.http import JsonResponse from django.views.decorators.http import require_POST from .services import predict_next @require_POST def api_predict(request): body = json.loads(request.body) result = predict_next( pm25=body.get('pm25'), pm10=body.get('pm10'), so2=body.get('so2'), no2=body.get('no2'), co=body.get('co'), o3=body.get('o3'), ) return JsonResponse({'success': True, 'prediction': result})前端通过 POST 请求把最新一小时的六项浓度传进来,服务端返回预测值。用 @require_POST 限制请求方法,避免 GET 请求携带 body 造成的兼容性问题。
4.5 前端页面与图表展示
页面用 Django 模板渲染框架内容,具体数据通过异步请求获取,图表使用 ECharts。前端不需要复杂框架,一个模板文件加一个 JS 文件就够。历史数据接口按时间范围返回数组,预测数据返回数值序列,两个序列拼成一个图。
ECharts 折线图核心配置:
// static/js/dashboard.js async function loadDashboard() { const hist = await fetch('/api/history/?hours=48').then(r => r.json()); const pred = await fetch('/api/predict/', { method: 'POST', body: JSON.stringify({ hours: 24 }) }).then(r => r.json()); const chart = echarts.init(document.getElementById('chart')); chart.setOption({ tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: hist.times.concat(pred.times) }, yAxis: { type: 'value', name: 'μg/m³' }, series: [ { name: '历史PM2.5', type: 'line', data: hist.values, smooth: true }, { name: '预测PM2.5', type: 'line', data: pred.values, smooth: true, lineStyle: { type: 'dashed' } } ] }); }历史部分画实线,预测部分画虚线,视觉上一眼能区分。时间轴拼接时注意预测的第一个时间点应该是历史最后一个时间点的下一小时,否则会在图上重叠。
5. 系统联调避坑:毕设阶段最容易翻车的 5 个细节
5.1 文件编码与中文乱码
现象:页面控制台打印的时间戳全是乱码,爬虫抓回来的站点名称在数据库里显示成问号。
原因:Windows 环境下 Python 文件默认编码可能与数据源不一致,requests 返回文本用了错误编码解码;Django 模板文件保存编码不是 UTF-8。
解决:爬虫代码显式指定 resp.encoding = 'utf-8';存储端如果换 MySQL 则设置表为 utf8mb4;Django settings 里设置 DEFAULT_CHARSET = 'utf-8',模板文件统一用 UTF-8 保存。不要依赖系统默认编码,Windows 和 Linux 行为不一致,这是最容易踩的第一个坑。
5.2 模型接口超时与冷启动
现象:第一次发起预测请求后端卡了 5 秒以上,前端直接超时;第二次之后就快了。
原因:load_model 在进程启动后第一次调用才执行,H5 权重加载和计算图重建耗时严重,每次新起 Django 开发服务器都会发生。
解决:把模型加载提前到模块导入阶段完成,或者用独立模型服务。开发阶段可以在 App 的 apps.py ready() 方法里加载,并用全局变量缓存;部署时把模型推理拆成独立进程,Django 通过 HTTP 调用它。简单场景下一个线程锁加全局缓存已经够用。
提示:判断模型是否被重复加载,在 service 模块打印一行日志,观察每次预测请求是否都有加载日志即可。
5.3 数据泄漏:归一化时用了测试集的信息
现象:测试集 MAE 显示只有 3 μg/m³,实际演示时效果很差,预测曲线严重滞后真实值。
原因:写代码时先对全量数据 fit_transform,再划分训练集和测试集;或者窗口切分时把测试集时间窗口的前向信息混进样本。MinMaxScaler 的 min/max 在全量数据上计算,测试集的分布信息泄漏到了训练过程。
解决:先前 80% 划分,再在训练集上单独 fit,再分别 transform。窗口构造时严格只用过去数据预测未来,不能把目标值本身错位到输入窗口里。检验方法是打印训练集 max 和测试集 max,如果两者几乎相等就说明对方泄漏了。
5.4 前端预测时间轴偏移
现象:预测曲线整体向右平移了一格,看起来预测结果只是历史值的延迟。
原因:训练标签设置错误。比如第 i 条样本的标签取了 data[i+time_step] 本身,而不是 data[i+time_step+1],那预测结果实际上是当前时刻浓度而不是未来时刻。前端拼接历史与预测序列时,预测起始点重叠了历史最后一个点,也会产生视觉偏移。
解决:先确认训练代码里标签索引是 i+time_step 还是 i+time_step+1,再在前端拼接时让预测第一个点从历史下一小时开始。答辩演示时直接在图上高亮过渡区间,一眼能看出是否偏移。
5.5 Django 开发服务器下并发推理的隐患
现象:两个浏览器同时打开页面,其中一个请求预测接口报错 500,或者模型预测结果出现 NaN。
原因:Django 开发服务器默认多线程,同一个 Keras 模型实例被多个线程同时调用 predict,TensorFlow 计算图会话状态存在竞争。模型加载过程的全局变量缓存也没有线程锁保护,并发访问下会被多次加载。
解决:加一个 threading.Lock,把模型加载和 predict 调用都包在锁里。更稳妥的方案是让模型常驻独立进程,Django 通过 JSON 传输数据,预测进程内部按顺序推理,从机制上避免竞争。
import threading from tensorflow.keras.models import load_model _model_lock = threading.Lock() _model = None def predict_sequence(inputs): global _model with _model_lock: if _model is None: _model = load_model('models/air_lstm.h5') return _model.predict(inputs, verbose=0)锁机制能保证并发下模型只被加载一次,推理也串行执行。毕设的并发量远低于真实生产系统,这个方案在性能和正确性之间是平衡的。
6. 从「能跑」到「能答辩」:验证方法与三个进阶优化
6.1 用滚动回测验证预测稳定性
单次测试集评估只能说明模型在某一时间段的拟合情况。更严格的做法是滚动回测:从测试集起点开始,每次用前 N 个真实值预测下 1 小时,把预测值作为下一次输入序列的一部分继续预测,连续滚动 48 小时。滚动预测的误差通常大于单步预测,因为误差会在迭代中累积,能更真实地反映系统在线运行时的表现。答辩时能拿出滚动回测曲线,说服力比单次预测强很多。
6.2 加入气象特征与多步预测
如果想让项目体现深度,在六项污染物基础之上加温度、湿度、风速、气压等气象特征,预测效果往往有明显改善。多步预测可以采用 seq2seq 结构:Encoder 用 LSTM 编码历史窗口,Decoder 逐步输出未来 6 小时或 24 小时的预测值。这样模型复杂度提升一个档次,也更容易解释 LSTM 在时序建模上的优势。
6.3 接口压测与部署验证
答辩前一定要做一次接口压测。简单的做法是用 curl 循环请求预测接口:
for i in $(seq 1 20); do curl -s -X POST http://127.0.0.1:8000/api/predict/ \ -H "Content-Type: application/json" \ -d '{"hours": 24}' -o /dev/null -w "%{time_total}\n" done观察单次响应时间是否稳定在可接受范围。如果平均耗时超过 1 秒,先检查是不是模型每次都被重复加载,再考虑把模型推理移到独立进程。部署环境用 gunicorn 替代 runserver,静态文件交给 Nginx 托管,数据库迁移到 MySQL 之前先做好备份。
整个系统做下来最大的体会是,毕设的完成度不取决于模型多花哨,而在于每个环节是否都能被验证。我在训练 LSTM 阶段曾经因为归一化泄漏拿到一份漂亮的测试报告,实际一画曲线就露馅,从此养成了一个习惯:任何评价指标都要配上可视化曲线才算数,尤其是时间序列这种天然带先后顺序的任务。希望帮到你。
本文还有配套的精品资源,点击获取