重庆火灾预测实战:多源时序与CNN+LSTM+Attention模型
简介面向具备Python基础与数据分析、机器学习背景的研发和技术人员这份实践方案围绕重庆火灾点分析与预测展开内容覆盖多源数据导入与准备、逐年逐月火点频次统计与可视化、气象因素关联分析以及结合注意力机制和CNN的LSTM模型构建、训练与评估并给出火险等级评估和改进建议可用于自然灾害预警系统开发中的数据驱动火灾预测场景。资源为1个docx文档压缩包约18KB便于按步骤查阅与修改。文档中提供可运行的Python代码示例与逐步解释涵盖数据预处理、特征工程、时间序列划分、模型参数调整等关键环节提示根据实际数据灵活改动。目前已有56人学习适合需要系统性参考火灾预测建模流程的读者。1. 重庆火灾点预测的数据链路多源时序与深度模型如何衔接重庆的火点集中在夏季伏旱和秋季秸秆焚烧两个窗口温度骤升、湿度骤降这类短期气象突变往往先于火点出现 3 到 7 天单独看某一张表很难发现规律把火灾记录、人口密度、土地利用、植被覆盖、气象观测五类数据放到同一条时间轴上之后才有完整的建模基础。这套 Python 实现的重庆火灾点分析与预测方案前端用热力图和相关性检验摸清火点频次的年季分布后端用结合注意力机制的 CNNLSTM 模型滚动预测未来火点频次再按预测值划分火险等级。整个链路从多源 CSV 到预警等级输出是闭环的适合有数据分析基础、正在做自然灾害预警系统原型的研发人员。下面按数据处理、可视化验证、模型构建、评估校准四个环节逐层拆开所有代码都有可替换的参数位。2. 多源数据合并与时间对齐人口、土地利用、植被、气象特征入库2.1 数据文件与字段语义多源数据合并是整条链路里最不性感但最影响结果的一环。原始方案里涉及的五个 CSV 角色各不相同实际项目里文件名可能不一样但字段角色基本可以对应到下面这张表文件典型字段在 pipeline 中的角色fire_data.csvdate, fire_point, location事件主体生成目标值与频次统计population_data.csvdate, population人口密度火源活动的静态特征land_use_data.csvdate, land_use_type建设用地、林地、农田分布vegetation_data.csvdate, vegetation_type植被覆盖类型可燃物属性weather_data.csvdate, temperature, humidity, wind_speed气象驱动时序特征主力要提醒一句如果 fire_data 的每一行代表一条火点记录那么 groupby 之后 count 得到的是周期内火点的条数如果 fire_point 本身存的是像元数或频次数值就应该用 sum 而不是 count。原方案里 fire_point 承担目标列的角色按“一行一条记录”理解最容易和后面的统计代码对上。2.2 left join 合并策略与时间粒度对齐多表合并的关键是关联键的粒度。原始方案用 date 做关联键、left join 保留全部火灾记录这个选择是对的因为火灾记录是事件主体人口、土地利用、植被是环境快照用内连接会把没有火灾记录的日期全部删掉模型就失去了“什么条件下没有火点”的负样本训练出来的模型会系统性高估火点概率。import pandas as pd fire_data pd.read_csv(fire_data.csv) population_data pd.read_csv(population_data.csv) land_use_data pd.read_csv(land_use_data.csv) vegetation_data pd.read_csv(vegetation_data.csv) weather_data pd.read_csv(weather_data.csv) merged_data pd.merge(fire_data, population_data, ondate, howleft) merged_data pd.merge(merged_data, land_use_data, ondate, howleft) merged_data pd.merge(merged_data, vegetation_data, ondate, howleft) merged_data pd.merge(merged_data, weather_data, ondate, howleft)这几行 merge 本身不难难点在 key 的粒度一致性。date 如果带小时而人口和土地利用是日级数据左连接会匹配不到产生大量 NaNdate 如果混用了时区俄就要先统一时区再做关联。操作上建议先统一格式再截断到日级merged_data[date] pd.to_datetime(merged_data[date]) merged_data[date] merged_data[date].dt.date2.3 缺失值处理与时间特征提取原方案对缺失值统一用 fillna(0)代码里的fillna(0, inaxis0)还有个笔误inaxis 应该是 axis。这里要特别说明人口、气温这类数值列填 0等于告诉模型“这一天人口为零、气温为零”这在业务语义上是错的会让后面相关性分析失真。比较稳的做法是数值列做线性插值类别列做前向填充numeric_cols [population, temperature, humidity, wind_speed] merged_data[numeric_cols] merged_data[numeric_cols].interpolate( methodlinear, limit_directionboth ) categorical_cols [land_use_type, vegetation_type] merged_data[categorical_cols] merged_data[categorical_cols].fillna(methodffill) merged_data[categorical_cols] merged_data[categorical_cols].fillna(unknown)注意新版 pandas 已经把fillna(methodffill)标记为弃用推荐直接写成df.ffill()上面的写法兼容旧版本跑起来会有 FutureWarning但不影响结果。类别列前向填充后如果仍有空值补一个 unknown 兜底避免模型输入出现 NaN。最后从 date 里拆出 year 和 monthmerged_data[year] merged_data[date].dt.year merged_data[month] merged_data[date].dt.month这两个派生列有两个用处一是为后面的分组热力图提供聚合维度二是保留火点频次里的年周期和季节周期信号让模型知道当前样本落在哪个时间位置。3. 火点频次的时空可视化热力图、趋势与气象相关性交叉验证3.1 逐年逐月火点频次的统计口径数据合并完先别急着建模。区域火灾的时空聚集性很强先用可视化确认火点集中分布在哪几个月、哪几年异常比直接调参重要得多。原始代码用 groupby count 生成逐年逐月频次再转透视表画热力图fire_frequency merged_data.groupby([year, month])[fire_point].count().reset_index() pivot_table fire_frequency.pivot_table( indexyear, columnsmonth, valuesfire_point, aggfuncsum, fill_value0 ) import matplotlib.pyplot as plt import seaborn as sns plt.figure(figsize(12, 8)) sns.heatmap(pivot_table, cmapYlOrRd, annotTrue, fmtd) plt.title(Fire Points Frequency from 2008 to 2020) plt.xlabel(Month) plt.ylabel(Year) plt.show()这里要留意 pandas 的 API 变化。旧版本的pivot(year, month, fire_point)可以用位置参数新版本要求显式传 index、columns、values而且当 index 出现重复值时 pivot 会直接抛错pivot_table 则靠 aggfunc 完成聚合。fill_value0 是第二个细节不存在的年-月组合会被透视表填成 NaN热力图会显示成白格实际业务含义是“这年这个月没有火点记录”填 0 才符合语义。count 统计的是 fire_point 列的非空值数量如果某行 fire_point 本身是 NaN这一行不会计入频次所以 groupby 前要确保目标列没有缺口否则热力图会出现偏低的月份。3.2 年度趋势与季节聚集特征热力图能看出季节簇但看不出整体是在上升还是下降。要看趋势需要换成分年折线图yearly_fire_frequency merged_data.groupby(year)[fire_point].count() plt.figure(figsize(10, 6)) yearly_fire_frequency.plot(kindline, markero) plt.title(Yearly Fire Points Trend from 2008 to 2020) plt.xlabel(Year) plt.ylabel(Fire Points Frequency) plt.show()折线图出现明显断崖或尖峰时要怀疑数据采集口径变化比如更换了卫星传感器或调整了火点判识阈值这类突变不是气候变化或人为因素导致的深度学习模型很难学出来也不应该去学。实际做法是把异常年份单独标记出来在训练集里过滤掉再做模型避免模型把数据质量问题当成规律拟合。重庆本地还有个特征值得注意7 到 10 月是全年火点最密集的窗口如果热力图显示火点散布到了冬春两季大概率是混入了非火灾热源需要回看原始样本排查。3.3 气象驱动的散点与相关性验证原始方案里的温度-火点散点图适合看趋势但要下结论还缺一步相关性系数。火灾频次是典型的零膨胀分布无火日远多于有火日把所有散点画在一起时坐标原点附近会堆成一大片人眼很难判断真实关系。比较合理的做法是聚合到月份粒度同时对比温度和湿度两个方向因为火点频次通常和温度正相关、和湿度负相关两个特征一起看能交叉验证monthly_weather merged_data.groupby(month)[[temperature, humidity]].mean() monthly_fire merged_data.groupby(month)[fire_point].count() monthly_combined pd.concat([monthly_weather, monthly_fire], axis1) print(monthly_combined.corr())月份粒度只有 12 个点corr 的输出只能做方向参考不能做显著性判断。如果这里看到 temperature 与 fire_point 的正相关系数较高、humidity 为负说明气象特征值得进模型如果某个特征的相关系数接近 0它在神经网络里大概率贡献有限可以考虑剔除。这一步的价值是把特征筛选前置而不是等到模型训练完再做特征重要性分析省掉不少试错成本。4. 构建 Conv1DAttentionLSTM 模型从归一化、滑窗到维度对齐4.1 归一化特征与目标需要独立 MinMaxScaler模型部分先讲归一化因为这是整段代码里最容易复制错的地方。原始方案用同一个 MinMaxScaler 对特征和目标分别 fit_transform实际运行时隐患很大特征列和目标列的量纲、分布完全不同公用一个 scaler 会把 fire_point 压到和 population 相同的数值区间反归一化时很难对齐。更稳的做法是给特征和目标各建一个 scalerfrom sklearn.preprocessing import MinMaxScaler feature_cols [population, temperature, humidity, wind_speed] target_col fire_point feature_scaler MinMaxScaler() target_scaler MinMaxScaler() scaled_features feature_scaler.fit_transform(merged_data[feature_cols]) scaled_target target_scaler.fit_transform(merged_data[[target_col]])fit_transform 只应该在训练段执行一次测试段一律用训练好的 scaler 做 transform。原因是 scaler 内部的 min/max 是训练数据的统计量测试集一旦参与 fit等于把未来的极值信息泄漏给训练过程验证指标会被高估部署到新数据时还会因为极值超界发生特征压缩失真。4.2 滑动窗口生成与时间序列切分时间序列模型不能随机打乱样本一旦打乱未来信息就会混进训练集预测结果立刻失真。标准的做法是按时间顺序切 8:2再用 time_steps 窗口把连续序列切成监督学习样本import numpy as np train_size int(len(scaled_features) * 0.8) train_features scaled_features[:train_size] train_target scaled_target[:train_size] test_features scaled_features[train_size:] test_target scaled_target[train_size:] def create_sequences(data, target, time_steps10): X, y [], [] for i in range(len(data) - time_steps): X.append(data[i:i time_steps]) y.append(target[i time_steps]) return np.array(X), np.array(y) train_X, train_y create_sequences(train_features, train_target, time_steps10) test_X, test_y create_sequences(test_features, test_target, time_steps10)time_steps10 表示用最近 10 个时间片预测下一个时间片的值。这个参数的合理取值要看数据的自相关长度如果火点频次和前 7 天的气象条件相关性最强窗口取 7 到 14 比较合适窗口太小模型拿不到完整的气象演变过程窗口太大会把无关的历史噪声卷进来训练时间也跟着翻倍。create_sequences 循环里len(data) - time_steps作为上界生成的样本数会比原始行数少 time_steps 个这是正常现象不需要补样本。4.3 网络结构卷积提取局部模式、注意力定位关键时间步原始结构是 CNN → LSTM → Attention → LSTM → Dense。设计意图很明确Conv1D 提取相邻时间步的局部变化模式比如连续三天高温低湿这类组合信号第一层 LSTM 学习时间依赖注意力层对时间步加权突出对预测贡献最大的窗口第二层 LSTM 聚合后输出频次。但原代码有两个地方在真实环境里跑不通。第一个是 Flatten 的位置问题。Conv1D 输出是三维张量(batch, steps, filters)MaxPooling1D 只压缩时间步维度仍然是三维。原代码在池化后接 Flatten会把张量压成二维而 LSTM 要求三维输入(batch, time_steps, features)这里直接报维度错误。常见做法是去掉 Flatten保留三维形状让卷积输出直接进 LSTM。第二个是AttentionWithContext不是 Keras 内置层需要自己继承 Layer 实现可训练权重。这里给一个能直接跑通、保留原始设计意图的替代结构from keras.models import Model from keras.layers import Input, Conv1D, MaxPooling1D, LSTM, Dense, Multiply inputs Input(shape(time_steps, train_X.shape[2])) x Conv1D(filters32, kernel_size3, activationrelu, paddingsame)(inputs) x MaxPooling1D(pool_size2)(x) lstm_out LSTM(units64, return_sequencesTrue)(x) attn_weights Dense(1, activationsigmoid)(lstm_out) attn_context Multiply()([lstm_out, attn_weights]) x LSTM(units32)(attn_context) outputs Dense(1)(x) model Model(inputs, outputs) model.compile(optimizeradam, lossmse) model.summary()这个改法里Dense(1, activationsigmoid) 对每个时间步输出一个 0 到 1 的权重Multiply 把权重乘回 LSTM 的输出实现“重要时间步放大、无关时间步压低”的效果。整体结构和原方案一致但不需要自定义层。paddingsame 也必须保留否则 Conv1D 会把时间步从 10 缩到 8再经过池化就只剩 4 步信息损失过大。各层输出形状如下层输出形状作用Input(None, 10, 4)10 个时间步 x 4 个特征Conv1D(32, kernel_size3)(None, 10, 32)提取局部跨日变化模式MaxPooling1D(2)(None, 5, 32)压缩时间分辨率LSTM(64, return_sequencesTrue)(None, 5, 64)对每个时间步编码时序信息Dense(1, sigmoid) Multiply(None, 5, 64)注意力权重加权LSTM(32)(None, 32)聚合为向量Dense(1)(None, 1)回归输出注意这里的注意力实现是简化变体用于把链路跑通生产环境建议用 keras 内置的 Attention 层或自行实现带打分函数的注意力机制。4.4 训练参数与验证曲线判读训练参数不多但每个都有讲究。epochs 开到 100 是因为 LSTM 收敛慢但同时一定要盯验证集 loss配合早停防止过拟合from keras.callbacks import EarlyStopping early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) history model.fit( train_X, train_y, epochs100, batch_size32, validation_data(test_X, test_y), callbacks[early_stop], verbose1 )batch_size32 适合中等规模数据样本量小可以降到 16。patience10 表示验证 loss 连续 10 个 epoch 没有改善就停止训练同时恢复最佳权重。这里验证集其实对应未来时间段的数据正符合时间序列预测场景我们就是要看模型在“没见过的未来”上表现如何。验证 loss 曲线如果出现先降后升的 U 型说明模型开始背训练集了早停会自动把权重回滚到拐点位置如果验证 loss 从一开始就不降问题基本出在前面几步要么是特征没归一化要么是滑动窗口造出来的样本太少LSTM 根本没学到东西。5. 预测评估与火险等级阈值校准用验证集反推分级界限5.1 MAE/MAPE 与 RMSE 的互补原始方案用 RMSE 作为唯一评估指标RMSE 对大幅误差特别敏感而火灾频次分布本身倾斜偶尔几次大幅漏报会主导误差值掩盖常见场景的真实表现。建议把 MAE 和 MAPE 一起打印from sklearn.metrics import mean_squared_error, mean_absolute_error predictions model.predict(test_X) pred_inv target_scaler.inverse_transform(predictions) test_inv target_scaler.inverse_transform(test_y) rmse np.sqrt(mean_squared_error(test_inv, pred_inv)) mae mean_absolute_error(test_inv, pred_inv) mape np.mean(np.abs((test_inv - pred_inv) / (test_inv 1e-6))) * 100 print(fRMSE: {rmse:.2f}, MAE: {mae:.2f}, MAPE: {mape:.2f}%)反归一化一定要用 target_scaler不能用训练特征用的 scaler否则数值范围完全对不上。MAPE 计算时给分母加了 1e-6 做平滑避免测试集里出现 0 火点导致除零如果测试集里 0 值特别多MAPE 会被异常放大这时以 RMSE 和 MAE 为主更稳妥。5.2 用混淆矩阵思维校准分级阈值原方案里pred 10判 Low、pred 50判 Medium、否则 High这个阈值是经验值缺少验证依据。更合理的做法是在验证集上枚举候选阈值把阈值二分类化看每个阈值下的漏报率再反推业务上可接受的分级界限actual_binary (test_inv.flatten() 20).astype(int) for threshold in [5, 10, 20, 30, 50]: pred_binary (pred_inv.flatten() threshold).astype(int) tp np.sum((pred_binary 1) (actual_binary 1)) fn np.sum((pred_binary 0) (actual_binary 1)) if (tp fn) 0: recall tp / (tp fn) print(f阈值 {threshold:2d}火警召回率 {recall:.2f})这里的 actual_binary 用 20 作为“真实大火点日”的判定线threshold 是“预测值超过多少就预警”的启动线。召回率代表真实高火点日里有多少天被模型提前抓到预警系统里漏报的代价远大于误报所以阈值应该向召回率高的方向倾斜。实际观察预测值会发现 LSTM 在回归任务里普遍偏保守预测分布向均值收缩高火点日容易被低估这时阈值定太高会漏报、定太低会天天报警。校准完阈值再回头对照 MAE 值的分布看高火险月份是否被压到可接受范围整套评估闭环才算真正落地。本文还有配套的精品资源点击获取