简介:这份资源是面向数据科学学习者与机器学习入门者的完整项目源码,围绕景泰小区水产量预测场景,用LSTM长短期记忆网络对历史水量时间序列建模,解决未来水产量精准预测的问题。项目共156个文件,压缩包约16.74MB,以27个Python脚本承担数据处理、模型构建与训练预测,50个pth权重文件保存训练好的模型参数,另有19张png图表、15个yaml配置、6个html与6个js文件搭建网页交互界面,4个css样式表负责界面美化,并附带xlsx、csv、sqlite3等数据文件与readme、requirements说明。已有276人学习下载。读者可从中获得一套可直接运行的LSTM预测工程范例,理解时间序列建模、模型持久化、前后端页面联调与依赖配置的完整流程,适合作为课程设计、毕业设计或机器学习练手项目的参考模板。
1. 景泰小区水产量预测:从LSTM模型到可视化页面的完整落地
景泰小区的日供水量数据我盯了三个月,最头疼的不是模型精度,而是数据本身——每天一个采样点,一年才365条,遇到节假日和夏季高峰还会突变。用传统ARIMA跑出来的曲线总是慢半拍,直到换成LSTM才把滞后问题压下去。这个标题讲的就是用Python搭LSTM预测模型、用JavaScript和HTML/CSS做前端展示的完整方案,适合有Python基础、想跑通时序预测全链路的水务或物业数据从业者。整套东西不依赖云服务,本地就能跑,前端页面直接打开HTML文件即可查看预测结果。下面按数据准备、模型训练、前端对接、避坑、调优的顺序拆开讲。
2. 数据准备与LSTM输入构造:把日供水量变成监督学习样本
2.1 景泰小区数据长什么样,先做三件事
拿到手的原始数据通常是一张Excel表,两列:日期和当日供水量(单位立方米)。常见做法是先做三件事——缺失值检查、异常值剔除、按时间排序。景泰小区这种规模,日供水量一般在200到800立方米之间波动,如果某天出现0或者超过2000,基本是抄表故障或管道爆管,直接按前后三天的均值补上。
import pandas as pd import numpy as np # 读取原始数据,日期列解析为datetime df = pd.read_excel('jingtai_water.xlsx', parse_dates=['date']) df = df.sort_values('date').reset_index(drop=True) # 缺失值用前向填充,异常值用3倍标准差截断 df['water'] = df['water'].fillna(method='ffill') mean, std = df['water'].mean(), df['water'].std() df['water'] = df['water'].clip(mean - 3*std, mean + 3*std) # 检查是否有重复日期 assert df['date'].duplicated().sum() == 0, '存在重复日期' print(df.describe())这段代码的逻辑是先保证时间轴连续且唯一,再做统计层面的清洗。clip用3倍标准差截断,比直接删除更稳,因为删掉后时间轴会断,后面构造滑动窗口时容易错位。参数上,ffill适合供水量这种变化平缓的序列,如果是气温类突变数据,用插值更合适。
2.2 滑动窗口构造:LSTM要的是三维张量
LSTM的输入不是一维序列,而是(样本数, 时间步长, 特征数)的三维张量。景泰小区单变量预测,特征数为1,时间步长决定用过去多少天预测下一天。我一般先用7天,因为供水有明显的周周期,周末和工作日模式不同。
def create_sequences(data, look_back=7): X, y = [], [] for i in range(len(data) - look_back): X.append(data[i:i+look_back]) y.append(data[i+look_back]) return np.array(X), np.array(y) # 归一化到0-1,LSTM对尺度敏感 from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) scaled = scaler.fit_transform(df[['water']]) look_back = 7 X, y = create_sequences(scaled, look_back) # 重塑为 (样本, 时间步, 特征) X = X.reshape((X.shape[0], X.shape[1], 1)) print(X.shape, y.shape) # 例如 (358, 7, 1) (358,)look_back=7意味着每个样本用前7天预测第8天。归一化必须用训练集的scaler去transform测试集,否则信息泄露,这是时序预测里最常见的翻车点。reshape那一步如果漏了,Keras会报维度错误,新手容易卡在这里。
2.3 训练集测试集切分:不能随机打乱
时序数据切分和普通机器学习不一样,绝对不能train_test_split(shuffle=True)。常见做法是按时间顺序,前80%做训练,后20%做测试。景泰小区三年数据约1095条,前876条训练,后219条测试。
split = int(len(X) * 0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:] # 反归一化函数,预测完要还原成实际水量 def inverse_transform(arr): return scaler.inverse_transform(arr.reshape(-1, 1)).flatten()切分后训练集和测试集在时间上不重叠,测试集完全模拟“用历史预测未来”的场景。如果打乱,模型会看到未来数据,评估结果虚高,上线后直接崩。这个坑我在早期项目里踩过,血泪经验。
3. 用Python搭LSTM模型:结构、编译与训练参数怎么定
3.1 模型结构:两层LSTM加Dropout就够了
景泰小区数据量不大,不需要堆很深。我一般用两层LSTM,第一层返回序列给第二层,第二层只返回最后时间步,后面接Dropout和全连接。Dropout设0.2,防止过拟合。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model = Sequential([ LSTM(64, return_sequences=True, input_shape=(look_back, 1)), Dropout(0.2), LSTM(32, return_sequences=False), Dropout(0.2), Dense(1) ]) model.compile(optimizer='adam', loss='mse', metrics=['mae']) model.summary()第一层64个单元,第二层32个,这个配置在几百到几千条数据上比较稳。return_sequences=True是让第一层输出完整序列,第二层才能继续处理。如果两层都设False,第二层就没法接。损失函数用MSE,因为供水量预测对大小误差都敏感;如果更关注相对误差,可以换MAPE,但MAPE在水量接近0时会爆炸,景泰小区最低也有200左右,问题不大。
3.2 训练参数:epochs、batch_size和早停
训练不是越多越好。我一般设epochs=100,batch_size=16,加EarlyStopping监控验证损失,patience=10。这样既不会欠拟合,也不会白跑。
from tensorflow.keras.callbacks import EarlyStopping early_stop = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True) history = model.fit( X_train, y_train, epochs=100, batch_size=16, validation_split=0.1, callbacks=[early_stop], verbose=1 )validation_split=0.1从训练集尾部再切10%做验证,不碰测试集。restore_best_weights=True保证训练结束后模型回到验证损失最低的那一轮,不用手动存checkpoint。batch_size=16在几百条样本上比32更稳,梯度更新更频繁,收敛曲线更平滑。
3.3 预测与评估:反归一化后看真实误差
训练完在测试集上预测,记得反归一化再算MAE和RMSE。景泰小区日供水量预测,MAE控制在30立方米以内算可用,超过50就要检查数据或调结构。
y_pred = model.predict(X_test) y_pred_inv = inverse_transform(y_pred) y_test_inv = inverse_transform(y_test) from sklearn.metrics import mean_absolute_error, mean_squared_error 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}, RMSE: {rmse:.2f}')反归一化必须用同一个scaler,且reshape成(-1,1)。如果预测结果全是0到1之间的小数,说明忘了反归一化,这是新手最常见的黑匣子问题。评估完可以把预测值和真实值存成JSON,给前端用。
import json result = { 'dates': df['date'].iloc[-len(y_test_inv):].dt.strftime('%Y-%m-%d').tolist(), 'actual': y_test_inv.tolist(), 'predicted': y_pred_inv.tolist() } with open('predict_result.json', 'w') as f: json.dump(result, f)这个JSON就是前后端的接口文件,前端用fetch读取后画图。日期格式统一成YYYY-MM-DD,避免JavaScript解析时区问题。
4. 前端展示:HTML/CSS/JavaScript怎么接LSTM预测结果
4.1 页面结构:一个canvas加一个数据表格
前端不复杂,一个HTML文件,引入Chart.js画折线图,下面放表格显示每日预测值。CSS负责居中布局和响应式,JavaScript负责读取JSON并渲染。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>景泰小区水产量预测</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> body { display: flex; flex-direction: column; align-items: center; font-family: sans-serif; } .chart-container { width: 80%; max-width: 900px; margin: 20px auto; } table { border-collapse: collapse; width: 80%; max-width: 900px; } th, td { border: 1px solid #ccc; padding: 8px; text-align: center; } th { background: #f5f5f5; } </style> </head> <body> <h2>景泰小区日供水量预测</h2> <div class="chart-container"> <canvas id="waterChart"></canvas> </div> <table id="dataTable"> <thead><tr><th>日期</th><th>实际值</th><th>预测值</th></tr></thead> <tbody></tbody> </table> <script src="app.js"></script> </body> </html>body用flex纵向居中,chart-container限制最大宽度,避免大屏上图表拉太长。表格样式简单,重点是数据可读。Chart.js从CDN引入,本地打开也能用,只要网络能访问CDN。如果完全离线,把Chart.js下载到本地改路径即可。
4.2 JavaScript读取JSON并画图
app.js里用fetch读取同目录下的predict_result.json,然后构造Chart.js的配置。注意fetch在本地文件协议下可能被浏览器拦截,常见做法是起一个简单HTTP服务,比如python -m http.server 8000。
fetch('predict_result.json') .then(response => response.json()) .then(data => { const ctx = document.getElementById('waterChart').getContext('2d'); new Chart(ctx, { type: 'line', data: { labels: data.dates, datasets: [ { label: '实际值', data: data.actual, borderColor: '#2196F3', fill: false }, { label: '预测值', data: data.predicted, borderColor: '#FF5722', borderDash: [5, 5], fill: false } ] }, options: { responsive: true, scales: { y: { beginAtZero: false, title: { display: true, text: '供水量 (m³)' } } } } }); // 填充表格 const tbody = document.querySelector('#dataTable tbody'); data.dates.forEach((date, i) => { const row = `<tr><td>${date}</td><td>${data.actual[i].toFixed(1)}</td><td>${data.predicted[i].toFixed(1)}</td></tr>`; tbody.insertAdjacentHTML('beforeend', row); }); }) .catch(err => console.error('数据加载失败:', err));borderDash让预测线变成虚线,和实际值区分开。toFixed(1)保留一位小数,避免表格里出现一长串浮点数。insertAdjacentHTML比innerHTML +=性能好,数据量大时不会反复重绘整个表格。如果控制台报CORS错误,就是文件协议的问题,起HTTP服务即可。
4.3 CSS细节:让图表和表格在手机上也能看
景泰小区的物业人员可能用手机查看,所以加一段媒体查询,小屏时表格字体缩小、图表宽度100%。
@media (max-width: 600px) { .chart-container, table { width: 95%; } th, td { padding: 4px; font-size: 12px; } h2 { font-size: 18px; } }这段CSS不复杂,但很实用。width: 95%留出边距,font-size: 12px保证表格不溢出。如果表格列多,还可以加overflow-x: auto让表格横向滚动。前端整体思路就是轻量,不引入框架,一个HTML加一个JS文件搞定。
5. 避坑与排查:LSTM水产量预测里最容易翻车的5个地方
5.1 预测曲线整体平移,滞后明显
现象:预测值形状和实际值很像,但总是晚一天,峰值对不上。原因:look_back太小,模型看不到足够的周期信息;或者数据没有做差分,趋势项被LSTM当成噪声。解决:把look_back从7调到14,或者对数据做一阶差分后再训练,预测完再累加回去。景泰小区夏季高峰连续多天,14天窗口能覆盖两个完整周周期。
5.2 损失降到很低但预测全是均值
现象:训练loss很小,MAE看着也不错,但画出来预测线几乎是一条水平线。原因:归一化后数据范围被压缩,模型学到“预测均值”就能让MSE很小,这是LSTM在弱周期数据上的常见退化解。解决:改用MAE做损失函数,或者在损失里加对峰值的惩罚权重。另一个办法是增加特征,比如把星期几做one-hot编码加进去,给模型提供额外区分信息。
5.3 前端fetch报错,页面空白
现象:浏览器控制台报Fetch API cannot load file:///...或CORS错误。原因:直接用双击HTML文件的方式打开,浏览器禁止本地文件fetch。解决:在项目目录下运行python -m http.server 8000,然后访问http://localhost:8000。这是最省事的本地服务方式,不需要装nginx。
5.4 反归一化后数值量级不对
现象:预测值在0到1之间,或者变成几万。原因:忘了反归一化,或者scaler用错。解决:检查inverse_transform是否用了训练时的scaler,且输入reshape成(-1,1)。如果预测值几万,说明scaler的data_min_和data_max_被测试集污染了,必须重新用训练集fit。
5.5 模型保存后加载预测结果不一致
现象:训练完直接预测正常,保存成h5再加载,预测结果变了。原因:保存时没保存scaler,加载后用了新的scaler或者没归一化。解决:用joblib把scaler一起存下来,加载时先load scaler再transform输入。
import joblib joblib.dump(scaler, 'scaler.pkl') # 加载时 scaler = joblib.load('scaler.pkl')这个坑很隐蔽,因为模型权重没问题,问题出在预处理。养成“模型和预处理器一起存”的习惯,能省很多排查时间。
6. 进阶技巧:用多步预测和滚动更新让景泰小区模型更实用
单步预测只能看明天,物业更想知道未来7天。多步预测有两种做法:直接多输出和滚动预测。直接多输出是把Dense层改成7个单元,一次输出7天;滚动预测是用预测出的明天当输入,再预测后天。我一般用滚动预测,因为实现简单,且能复用单步模型。
def forecast_next_days(model, last_sequence, scaler, days=7): preds = [] seq = last_sequence.copy() for _ in range(days): pred = model.predict(seq.reshape(1, look_back, 1), verbose=0) preds.append(pred[0, 0]) seq = np.append(seq[1:], pred, axis=0) return scaler.inverse_transform(np.array(preds).reshape(-1, 1)).flatten()last_sequence是测试集最后7天的归一化数据。每次预测完把结果追加到序列尾部,去掉最老的一天,保持窗口长度不变。这样滚动7次就能得到未来一周的预测。注意滚动预测误差会累积,一般3天以内比较准,7天只能看趋势。
另一个技巧是加“星期特征”。景泰小区周末用水明显低于工作日,把星期几做sin/cos编码加到输入里,能提升5%到10%的精度。
df['dayofweek'] = df['date'].dt.dayofweek df['dow_sin'] = np.sin(2 * np.pi * df['dayofweek'] / 7) df['dow_cos'] = np.cos(2 * np.pi * df['dayofweek'] / 7)这样特征数从1变成3,input_shape要改成(look_back, 3)。sin/cos编码比one-hot更紧凑,且能表达“周日和周一接近”的周期性。
验证多步预测效果,我习惯用“滚动原点”评估:每次用历史数据预测下一天,然后真实值加入历史,再预测下一天。这样模拟上线后的真实场景,比一次性预测7天更靠谱。
| 评估方式 | 优点 | 缺点 |
|---|---|---|
| 直接多输出 | 一次出结果,速度快 | 误差不累积但结构固定 |
| 滚动预测 | 实现简单,可复用单步模型 | 误差会累积,3天后偏差变大 |
| 滚动原点评估 | 最接近真实上线场景 | 计算量大,每次都要重新预测 |
我现在的习惯是:模型训练完先看单步MAE,再用滚动原点跑一遍测试集,两个指标都达标才往前端推。景泰小区这套跑下来,单步MAE在25左右,滚动7天第7天MAE约45,物业用来做周调度够用了。如果哪天数据量涨到几千条,可以把LSTM换成GRU,参数少一点,训练更快。希望帮到你。
本文还有配套的精品资源,点击获取