资讯详情 基于LSTM+Django的空气质量监测与预测系统全流程实践
📅 2026/10/18 17:34:51
简介一份基于LSTM与Django的空气质量监测及预测系统源码定位明确帮助正在完成毕业设计、期末大作业或课程设计的学生快速搭建一套可演示、可扩展的完整Web系统也适合初学者学习深度学习模型与Web开发结合的项目范式。项目围绕空气质量数据展开涵盖LSTM时序预测、Django后端API、前端可视化页面与核心代码注释模型训练、数据归一化、视图路由等关键环节均有说明从数据处理到结果展示链路完整。压缩包共260个文件约7.07MB主要有164个scss样式文件、23个js脚本、15个py源文件、15个pyc编译文件、8个html页面以及csv数据文件、sqlite3数据库、xml配置等前端样式、后端逻辑、模型训练、数据存储分布清晰。内置真实空气质量数据集提供分析、省份统计等模块页面部署后即可运行可复用时序预测模块也便于替换数据二次开发。目前已有384人学习下载对于追求高分、注重完整度与代码规范性的学习者来说是一份性价比很高的参考实现。1. 为什么这个选题值得做LSTM 负责预测Django 负责站在台前如果让我只挑一个毕业设计方向那一定是“基于LSTMDjango的空气质量监测与预测系统”。很多第一次接触这个题目的人会以为难点在模型LSTM的隐藏层、时间步、损失函数怎么调觉得把预测精度跑上去就大功告成。但实际上真正让 A 同学这类人熬夜返工的是数据链路——从环境监测接口或传感器拿到数据清洗入库再用 Django 把实时指标和预测曲线展示出来。整条链路里LSTM 反而是最容易跑通的一环。这套系统的价值在于把两件事串起来了一是用 LSTM 对 PM2.5 这类时间序列做多步预测二是用 Django 做一个能实时查询、可视化、带预测功能的 Web 平台。适合的人群很具体计算机、数据科学、环境工程相关专业需要完成完整项目的学生以及想入门时间序列预测但缺少真实业务场景的开发者。它不要求你懂底层的神经网络推导但要求你能把一个模型放进一个 Web 系统里稳定跑起来——这正是毕业设计要考察的工程能力。2. 系统架构与数据链路从数据采集到预测展示的完整设计2.1 四层架构与选型理由这套系统的整体结构我习惯拆成四层数据采集层、数据存储层、模型预测层、Web 应用层。采集层负责定时拉取监测站的历史数据和实时数据存储层用 MySQL 保存原始记录和预测结果模型预测层跑 LSTM 训练与推理Web 层用 Django 对外提供页面和接口。每一层之间只通过明确的数据格式通信这是项目后期调试时最省心的做法。选型理由先讲 Django。常见做法是用 Flask 写轻量接口但毕业设计要交文档、要演示、要展示后台管理能力Django 自带的 ORM、Admin 后台、模板语法和用户认证模块能省掉大量重复代码。你不需要手写 SQL 的增删改查ORM 会帮你处理答辩时打开 Admin 后台给评委看数据管理界面比贴 JSON 接口有说服力得多。模板系统也让页面渲染变得直观不用单独搭前端项目。LSTM 的选型理由则要从数据本身讲。空气质量指标是典型的时间序列当前时刻的 PM2.5 浓度与过去 24 小时甚至 48 小时的变化趋势强相关这种长短期依赖关系恰好是 LSTM 的设计目标。相比传统 ARIMA 需要人工定阶、难以处理多变量输入LSTM 可以直接把 PM2.5、PM10、SO2、NO2、O3、CO 六个指标拼成多维度输入让模型自己学习污染物之间的耦合关系。对课程设计而言这种“黑匣子”特性反而是优点——你不用花大量篇幅推导数学公式把精力放在数据质量和工程链路上效果通常不会差。2.2 数据表设计与采集脚本落地不管数据来源是公开数据平台还是自建传感器落到数据库里的结构都差不多。我一般建两张表一张存监测原始记录一张存模型预测结果。原始表以监测时间和站点做联合唯一索引避免重复采集预测表存放模型每一次推理输出的未来 N 小时结果带一个模型版本字段方便以后换模型做对比。CREATE TABLE monitor_record ( id INT AUTO_INCREMENT PRIMARY KEY, station_name VARCHAR(64) NOT NULL, monitor_time DATETIME NOT NULL, pm25 FLOAT, pm10 FLOAT, so2 FLOAT, no2 FLOAT, o3 FLOAT, co FLOAT, aqi INT, UNIQUE KEY idx_station_time (station_name, monitor_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE prediction_result ( id INT AUTO_INCREMENT PRIMARY KEY, station_name VARCHAR(64) NOT NULL, predict_time DATETIME NOT NULL, target_time DATETIME NOT NULL, pm25_pred FLOAT, model_version VARCHAR(32), KEY idx_predict (station_name, predict_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;两张表都用 utf8mb4 字符集否则存中文站点名会出现乱码monitor_record 的联合唯一索引是防重复采集的第一道防线脚本重跑也不会把数据翻倍。采集脚本我通常写成 Django 的 management command好处是能直接复用项目的 settings 配置和 ORM 模型不用单独维护一套数据库连接。# monitoring/management/commands/fetch_data.py import requests from datetime import datetime, timedelta from django.core.management.base import BaseCommand from monitoring.models import MonitorRecord class Command(BaseCommand): help 拉取最近一小时空气质量数据 def handle(self, *args, **options): url https://example-air-api.example.com/latest # 以实际文档为准 resp requests.get(url, params{city: test_city}, timeout10) data resp.json() records [] for item in data[records]: records.append(MonitorRecord( station_nameitem[station], monitor_timedatetime.fromisoformat(item[time]), pm25item.get(pm25), pm10item.get(pm10), so2item.get(so2), no2item.get(no2), o3item.get(o3), coitem.get(co), aqiitem.get(aqi), )) MonitorRecord.objects.bulk_create(records, ignore_conflictsTrue)这里用 bulk_create 批量写入并开启 ignore_conflicts配合数据库层的唯一索引重复执行脚本只会跳过已存在的记录。requests 请求要设置 timeout避免某个站点接口长时间挂起导致采集任务卡死。接口响应结构在不同平台差异很大写代码前先到其文档里确认字段名、时间格式和分页方式这一步骤省下来的调试时间足够你再写一个模块。3. 用 Keras 训练空气质量 LSTM 模型数据预处理、滑窗构建与参数调优3.1 数据清洗与滑窗样本构建训练 LSTM 的第一步不是建模型而是把原始监测数据整理成“监督学习”格式。原始表里只有一条条按时间排列的浓度记录LSTM 需要的是“用过去 24 个时刻的特征预测下一个时刻”这样的样本对。这一步叫滑窗窗口大小通常记为 look_back。我建议先只拿 PM2.5 单变量把整个链路跑通再扩展成多变量这样出问题时排查范围小。数据清洗包含四个方面缺失值处理、异常值剔除、时间排序、归一化。空气质量数据偶尔会有传感器离线导致的空值我习惯先用前向填充再做线性插值异常值用 3σ 准则剔除把超过均值三倍标准差的点替换为前后两时刻的均值。时间列必须升序排列并重设索引否则滑窗会被打乱模型学到错误的时间依赖关系。import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler df pd.read_csv(air_history.csv, parse_dates[time], index_coltime) df df[[pm25]].ffill().dropna() # 3σ 异常值处理 mean, std df[pm25].mean(), df[pm25].std() df[pm25] df[pm25].mask( (df[pm25] mean 3 * std) | (df[pm25] mean - 3 * std) ).interpolate() scaler MinMaxScaler(feature_range(0, 1)) scaled scaler.fit_transform(df[[pm25]].values) def create_sliding_window(data, look_back24): X, y [], [] for i in range(look_back, len(data)): X.append(data[i - look_back:i, 0]) y.append(data[i, 0]) return np.array(X), np.array(y) X, y create_sliding_window(scaled, look_back24) X X.reshape((X.shape[0], X.shape[1], 1)) train_size int(len(X) * 0.8) X_train, X_test X[:train_size], X[train_size:] y_train, y_test y[:train_size], y[train_size:]归一化放在滑窗之前MinMaxScaler 把浓度压缩到 0 到 1 之间LSTM 的激活函数在这个区间内梯度更稳定。注意 fit_transform 只在训练集上调用测试集要用 transform 单独处理这是后面避坑章节要展开讲的细节在这里先记住原则。滑窗函数返回的 X 是二维数组必须 reshape 成 (样本数, 时间步, 特征数) 的三维结构特征数为 1 也要显式写出来否则模型会报维度错误。3.2 模型结构、训练参数与评估单变量基线模型我一般用两层 LSTM 加 Dropout。第一层设置 return_sequencesTrue 保留完整序列输出给第二层第二层只返回最后一个时间步的结果然后接一个全连接层输出预测浓度。units 从 64 开始试数据量小就降到 32Dropout 取 0.2 可以有效抑制过拟合因为空气质量数据本身噪声不小。损失函数用均方误差 MSE优化器用 Adam它对学习率不敏感毕业设计阶段不用手动调学习率。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping model Sequential([ LSTM(64, return_sequencesTrue, input_shape(24, 1)), Dropout(0.2), LSTM(32, return_sequencesFalse), Dropout(0.2), Dense(1) ]) model.compile(optimizeradam, lossmse, metrics[mae]) model.summary() history model.fit( X_train, y_train, epochs100, batch_size32, validation_split0.1, callbacks[EarlyStopping(patience10, restore_best_weightsTrue)], verbose1 ) model.save(aqi_lstm.h5)epochs 设 100 但不一定跑满EarlyStopping 会在验证集损失连续 10 轮不下降时自动停止并恢复最佳权重。batch_size 取 32 是经验值太大了收敛慢太小了梯度震荡明显。训练完成后用测试集做一次评估反归一化再计算误差指标不要直接用归一化后的数据评估不然指标数值对答辩老师没有直观意义。from sklearn.metrics import mean_absolute_error, mean_squared_error y_pred model.predict(X_test) y_pred_inv scaler.inverse_transform(y_pred.reshape(-1, 1)) y_test_inv scaler.inverse_transform(y_test.reshape(-1, 1)) mae mean_absolute_error(y_test_inv, y_pred_inv) rmse np.sqrt(mean_squared_error(y_test_inv, y_pred_inv)) print(fMAE: {mae:.2f} μg/m³, RMSE: {rmse:.2f} μg/m³)反归一化必须用训练时保存的那个 scaler 对象在训练脚本里用 joblib.dump 把它存成文件后面 Django 加载模型时还要用到它做同样的逆变换。评估时如果 MAE 在 10 到 20 之间对这个课题来说已经是能交差的结果如果出现几千的数值先检查是不是反归一化时形状不对或者漏了这一步。4. 把模型接入 Django实时监测页面与预测接口的实现4.1 项目初始化与模型加载封装Django 项目的初始化没有太多花样我在项目根目录创建应用之后第一件事是改 settings 里的数据库配置和时区。数据库改用 MySQL 时需要装 mysqlclient 驱动在 DATABASES 里填上库名、用户名、密码和主机地址时区要设置为本地时区并关闭 USE_TZ否则查询出来的时间总是比实际早 8 个小时。django-admin startproject air_quality python manage.py startapp monitor创建应用后记得在 INSTALLED_APPS 里注册 monitor然后运行 makemigrations 和 migrate 生成数据表。模型文件放在项目根目录下的 models/ 文件夹里不要放在应用目录下方便统一管理。加载模型和 scaler 的代码单独写成一个模块避免每个视图函数重复加载模型文件几十 MB每次请求都加载一次性能会很难看。# monitor/model_loader.py import os import joblib from tensorflow.keras.models import load_model BASE_DIR os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 模块加载时只执行一次 _lstm_model None _scaler None def get_model(): global _lstm_model if _lstm_model is None: _lstm_model load_model(os.path.join(BASE_DIR, models, aqi_lstm.h5)) return _lstm_model def get_scaler(): global _scaler if _scaler is None: _scaler joblib.load(os.path.join(BASE_DIR, models, scaler.pkl)) return _scaler用懒加载的方式把模型初始化推迟到第一次实际调用启动开发服务器时不用等模型加载完用模块级全局变量缓存模型对象后续请求直接复用这是 Django 集成本地模型文件的常见做法。BASE_DIR 用绝对路径拼接避免从不同目录启动服务时找不到模型文件。4.2 视图函数与 ECharts 可视化视图层要做的事有两件一是把最近 24 小时的实测数据传给模板渲染成折线图二是接收前端传入的预测起点调用 LSTM 模型输出未来 24 小时的浓度预测。实测数据直接查数据库预测部分需要把最近 24 个时刻的值取出来做归一化、reshape 成 (1, 24, 1) 的输入格式预测完再反归一化。多步预测用滚动方式每预测出一个新值就把它拼到序列末尾同时丢弃最前面的旧值保持窗口长度恒定。# monitor/views.py import json import numpy as np from django.shortcuts import render from django.http import JsonResponse from .models import MonitorRecord from .model_loader import get_model, get_scaler def dashboard(request): records MonitorRecord.objects.order_by(-monitor_time)[:24] records list(reversed(records)) times [r.monitor_time.strftime(%H:%M) for r in records] values [r.pm25 if r.pm25 is not None else 0 for r in records] return render(request, monitor/dashboard.html, { times: json.dumps(times), values: json.dumps(values), }) def predict_api(request): if request.method ! GET: return JsonResponse({error: 只支持 GET 请求}, status405) model get_model() scaler get_scaler() records list(MonitorRecord.objects.order_by(-monitor_time)[:24][::-1]) if len(records) 24: return JsonResponse({error: 历史数据不足 24 条}, status400) history np.array([r.pm25 for r in records], dtypenp.float32).reshape(-1, 1) scaled scaler.transform(history) sequence scaled.reshape(1, 24, 1) preds [] for _ in range(24): pred model.predict(sequence, verbose0)[0, 0] preds.append(pred) pred pred.reshape(1, 1, 1) sequence np.concatenate([sequence[:, 1:, :], pred], axis1) preds np.array(preds).reshape(-1, 1) preds_inv scaler.inverse_transform(preds).flatten() return JsonResponse({prediction: preds_inv.tolist()})滚动预测的关键在 np.concatenate 这一步窗口每次向右滑动一格新预测值进入序列末尾模型看到的是自己之前预测的结果而不是真实值这更接近真实使用场景。predict(verbose0) 抑制推理日志避免循环输出刷屏。前端模板我直接用 ECharts 的 CDN 版本不需要 npm 构建对课程设计来说最省事。!-- monitor/templates/monitor/dashboard.html -- !DOCTYPE html html langzh head meta charsetUTF-8 title空气质量监测与预测/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body h1PM2.5 最近 24 小时趋势/h1 div idchart stylewidth: 100%; height: 400px;/div button onclickloadPrediction()预测未来 24 小时/button div idpredChart stylewidth: 100%; height: 400px;/div script const times {{ times|safe }}; const values {{ values|safe }}; const chart echarts.init(document.getElementById(chart)); chart.setOption({ xAxis: { type: category, data: times }, yAxis: { type: value, name: μg/m³ }, series: [{ type: line, data: values, name: 实测 PM2.5 }] }); async function loadPrediction() { const resp await fetch(/api/predict/); const data await resp.json(); if (data.prediction) { const predChart echarts.init(document.getElementById(predChart)); const nextHours Array.from({ length: 24 }, (_, i) ${i 1}h); predChart.setOption({ xAxis: { type: category, data: nextHours }, yAxis: { type: value, name: μg/m³ }, series: [{ type: line, data: data.prediction, name: 预测 PM2.5 }] }); } else { alert(data.error || 预测失败); } } /script /body /html模板变量用 safe 过滤器直接输出的 JSON 数组Django 默认会转义 HTML 字符。对于数字和逗号构成的 JSON 数组转义不会破坏结构但为了稳妥还是显式标记 safe。这里的核心不是前端写得有多花哨而是把 I/O 闭环打通前端点击按钮fetch 发起请求视图查询最新 24 条数据模型推理前端重新渲染。这部分逻辑清晰了其余页面几乎都是模板套用。5. 运行与部署避坑5 个高频事故的排查记录5.1 维度报错LSTM 输入必须显式三维现象调用 model.predict 时抛出 ValueError提示 Input 0 of layer lstm is incompatible后面跟着 expected ndim3found ndim2。我见过不少人在这一步卡住明明训练时没报错存完模型再加载就出问题。原因训练脚本里已经做过 reshape但推理代码里从数据库取出数据后直接丢给模型形状是 (24, 1) 而不是 (1, 24, 1)。LSTM 层的输入要求是 (样本数, 时间步, 特征数)缺少了样本维度这一维模型接口自然拒绝。解决在喂给模型之前统一执行 sequence.reshape(1, 24, 1)。最稳妥的做法是写一个预处理函数接收原始列表和历史长度内部完成归一化与 reshape所有视图调用同一个函数。调试时可以打印 np.shape 确认维度不要想当然。5.2 数据泄漏测试集效果虚假的“高分”现象模型在测试集上 MAE 只有 2 到 3图表里预测线和真实线几乎完全重合但放到真实环境预测就完全偏离。答辩时如果评委让现场演示预测当场就会穿帮。原因归一化时对整个数据集调用了 scaler.fit_transformscaler 的 min 和 max 是在包含测试集的数据上计算的相当于测试集的信息提前泄露给了模型。模型在训练时“见过”测试集的取值范围效果虚高是必然的。解决先切分训练集和测试集再执行 scaler.fit(X_train)最后分别 transform。这是整个项目里最隐蔽的坑我建议在论文里主动写出这一点会很加分说明你理解数据泄漏的含义。5.3 预测曲线整体滞后滚动预测的方式不对现象预测曲线的形状和真实曲线很像但整体向右平移了几个小时看起来像是“照着真实值抄了一遍但慢半拍”。原因单步预测时每一步都用真实值作为下一步的输入而不是用模型自己的预测值。这样短期预测确实很准但滞后是必然的因为模型永远只看到过去没法真正外推。解决使用滚动预测预测出一个值后把它拼接进输入序列替换掉最旧的真实值如第四章代码所示。这样模型在第二步起就基于自身预测继续推导更贴近真实的长期预测场景。5.4 模型加载失败路径问题与内存问题并存现象python manage.py runserver 启动正常但访问页面时报 FileNotFoundError提示找不到 aqi_lstm.h5或者进程直接崩溃。原因load_model 使用的是相对路径而 Django 启动时的工作目录取决于你从哪里执行 runserver。如果从项目根目录启动没问题但从上一级目录启动就会找不到文件。另一个常见问题是同时用 CPU 版本的 TensorFlow每次加载模型开销很大重复加载导致内存溢出。解决用 os.path.join(BASE_DIR, models, aqi_lstm.h5) 构造绝对路径把路径管理统一放在 settings 或 model_loader 模块里。模型加载只执行一次并缓存不要写在视图函数内部。5.5 时间偏差与中文乱码时区和编码问题现象页面上显示的时间比实际时间早 8 个小时数据库里中文站点名显示为问号。原因Django 默认开启 USE_TZTrue时间以 UTC 存储渲染时如果没有激活本地时区就按 UTC 输出数据库表建立时如果没有指定 utf8mb4 字符集中文写入会变成乱码。解决在 settings.py 里把 USE_TZ 设为 FalseTIME_ZONE 设为本地时区数据库连接参数加上 OPTIONS 指定 charsetutf8mb4。表已经建好的情况下用 ALTER TABLE 修改字符集。这个坑在任何涉及 Django 的毕设里几乎都会遇到早改早省心。6. 让项目从「能跑」到「高分」预测效果验证与答辩展示技巧模型跑通、页面能显示之后项目还处于“能交差”的阶段。真正拉开分差的是你有没有做效果验证和横向对比。我建议在论文里设计一张评估表至少包含三行单变量 LSTM、多变量 LSTM、基线模型 ARIMA。指标列放 MAE、RMSE 和 R²。答辩时评委看到你有对比实验比看十张预测图都更有说服力。模型MAE (μg/m³)RMSE (μg/m³)R²ARIMA 基线18.526.30.78单变量 LSTM14.220.70.85多变量 LSTM11.817.90.89做对比实验时有个细节ARIMA 的输入序列长度要跟 LSTM 的 look_back 对齐否则对比不公平。多变量 LSTM 就是把六个污染物指标一起送入模型输入形状从 (样本数, 24, 1) 变成 (样本数, 24, 6)第一层 LSTM 的 input_shape 相应改为 (24, 6)。如果数据量不大六个特征会让训练变慢可以先用皮尔逊相关系数筛出与 PM2.5 最相关的三个变量。展示层面还有一个值得做的优化在页面顶部加一个空气质量等级色条根据 AQI 数值把优、良、轻度污染、中度污染、重度污染映射成不同颜色。实测曲线和预测曲线颜色区分开图例写清楚评委一眼就能看懂。预测接口返回的数据可以多给一个字段——当前时刻的实测 AQI 等级用于对比预测是否合理。最后说一个我从这些工程问题里养成的习惯拿到时间序列数据先画一张原始曲线图看看有没有断点、毛刺和突变再画训练集与测试集的划分线确认没有乱序最后才写模型代码。这个顺序救过我很多次也让我在答辩被问“你的数据质量怎么保证”时能直接回答问题。毕业设计做完之后这套系统换一个数据集就能迁移到其他城市或场景值得投入的从来不是模型的新奇程度而是数据链路的鲁棒性。希望帮到你。本文还有配套的精品资源点击获取