☰
基于LSTM+Django的空气质量监测与预测系统全流程实践
2026/10/18 17:34:47 网站建设 项目流程

简介:一份基于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 负责站在台前

如果让我只挑一个毕业设计方向,那一定是“基于LSTM+Django的空气质量监测与预测系统”。很多第一次接触这个题目的人会以为难点在模型: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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

两张表都用 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'}, timeout=10) data = resp.json() records = [] for item in data['records']: records.append(MonitorRecord( station_name=item['station'], monitor_time=datetime.fromisoformat(item['time']), pm25=item.get('pm25'), pm10=item.get('pm10'), so2=item.get('so2'), no2=item.get('no2'), o3=item.get('o3'), co=item.get('co'), aqi=item.get('aqi'), )) MonitorRecord.objects.bulk_create(records, ignore_conflicts=True)

这里用 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_col='time') 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_back=24): 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_back=24) 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_sequences=True 保留完整序列输出给第二层,第二层只返回最后一个时间步的结果,然后接一个全连接层输出预测浓度。units 从 64 开始试,数据量小就降到 32;Dropout 取 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_sequences=True, input_shape=(24, 1)), Dropout(0.2), LSTM(32, return_sequences=False), Dropout(0.2), Dense(1) ]) model.compile(optimizer='adam', loss='mse', metrics=['mae']) model.summary() history = model.fit( X_train, y_train, epochs=100, batch_size=32, validation_split=0.1, callbacks=[EarlyStopping(patience=10, restore_best_weights=True)], verbose=1 ) 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(f'MAE: {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 请求'}, status=405) model = get_model() scaler = get_scaler() records = list(MonitorRecord.objects.order_by('-monitor_time')[:24][::-1]) if len(records) < 24: return JsonResponse({'error': '历史数据不足 24 条'}, status=400) history = np.array([r.pm25 for r in records], dtype=np.float32).reshape(-1, 1) scaled = scaler.transform(history) sequence = scaled.reshape(1, 24, 1) preds = [] for _ in range(24): pred = model.predict(sequence, verbose=0)[0, 0] preds.append(pred) pred = pred.reshape(1, 1, 1) sequence = np.concatenate([sequence[:, 1:, :], pred], axis=1) preds = np.array(preds).reshape(-1, 1) preds_inv = scaler.inverse_transform(preds).flatten() return JsonResponse({'prediction': preds_inv.tolist()})

滚动预测的关键在 np.concatenate 这一步:窗口每次向右滑动一格,新预测值进入序列末尾,模型看到的是自己之前预测的结果而不是真实值,这更接近真实使用场景。predict(verbose=0) 抑制推理日志,避免循环输出刷屏。前端模板我直接用 ECharts 的 CDN 版本,不需要 npm 构建,对课程设计来说最省事。

<!-- monitor/templates/monitor/dashboard.html --> <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>空气质量监测与预测</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> </head> <body> <h1>PM2.5 最近 24 小时趋势</h1> <div id="chart" style="width: 100%; height: 400px;"></div> <button onclick="loadPrediction()">预测未来 24 小时</button> <div id="predChart" style="width: 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 ndim=3,found ndim=2。我见过不少人在这一步卡住,明明训练时没报错,存完模型再加载就出问题。

原因:训练脚本里已经做过 reshape,但推理代码里从数据库取出数据后直接丢给模型,形状是 (24, 1) 而不是 (1, 24, 1)。LSTM 层的输入要求是 (样本数, 时间步, 特征数),缺少了样本维度这一维,模型接口自然拒绝。

解决:在喂给模型之前统一执行 sequence.reshape(1, 24, 1)。最稳妥的做法是写一个预处理函数,接收原始列表和历史长度,内部完成归一化与 reshape,所有视图调用同一个函数。调试时可以打印 np.shape 确认维度,不要想当然。

5.2 数据泄漏:测试集效果虚假的“高分”

现象:模型在测试集上 MAE 只有 2 到 3,图表里预测线和真实线几乎完全重合,但放到真实环境预测就完全偏离。答辩时如果评委让现场演示预测,当场就会穿帮。

原因:归一化时对整个数据集调用了 scaler.fit_transform,scaler 的 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_TZ=True,时间以 UTC 存储,渲染时如果没有激活本地时区就按 UTC 输出;数据库表建立时如果没有指定 utf8mb4 字符集,中文写入会变成乱码。

解决:在 settings.py 里把 USE_TZ 设为 False,TIME_ZONE 设为本地时区,数据库连接参数加上 OPTIONS 指定 charset='utf8mb4'。表已经建好的情况下,用 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 等级,用于对比预测是否合理。

最后说一个我从这些工程问题里养成的习惯:拿到时间序列数据,先画一张原始曲线图,看看有没有断点、毛刺和突变;再画训练集与测试集的划分线,确认没有乱序;最后才写模型代码。这个顺序救过我很多次,也让我在答辩被问“你的数据质量怎么保证”时能直接回答问题。毕业设计做完之后,这套系统换一个数据集就能迁移到其他城市或场景,值得投入的从来不是模型的新奇程度,而是数据链路的鲁棒性。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询