ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于LSTM与CNN的重庆火灾点时空预测:从多源数据到PyTorch落地

基于LSTM与CNN的重庆火灾点时空预测:从多源数据到PyTorch落地 简介这份资源面向具备Python基础与数据分析、机器学习背景的研发及技术人员聚焦重庆地区火灾点的分析与预测任务可用于自然灾害预警系统的开发实践。内容围绕多源数据导入与预处理、火灾趋势可视化、模型构建训练与预测、准确率验证及火险等级评估等环节展开并采用结合注意力机制的CNN-LSTM模型进行时间序列预测同时给出数据处理与模型调参方面的改进建议。资源包共1个docx文件约18KB以文档形式集中呈现实现步骤、代码示例与代码解释便于按章节对照学习。目前已有57人学习下载。读者可从中获得一套较完整的火灾点分析预测方案涵盖逐年逐月火点频次统计、气象数据相关性分析、特征归一化与时间序列划分等具体做法并理解注意力机制与CNN-LSTM组合建模的落地思路适合作为相关课题与预警项目的参考模板。1. 重庆火灾点分析与预测从多源数据到 LSTM 时空建模的完整落地路径重庆夏季连续高温叠加山地地形让森林火险点位预测变成一个典型的时空序列问题气象数据是时间序列地形和植被是空间特征历史火点则是稀疏的正样本。很多团队第一反应是直接上 CNN 做遥感影像分类但真正卡住落地的是数据对齐和样本不平衡而不是模型结构。这篇笔记围绕「Python 实现重庆火灾点分析与预测」这条主线把大数据处理、特征工程、LSTM 时序建模、CNN 空间特征提取这几块串成一条能复现的路径。适合已经会写 Python、想把这套方法迁移到本地火险预警或类似时空预测场景的工程师也适合刚入门深度学习、想找一个完整项目练手的读者。下面从数据获取讲到模型评估每一步都给出可运行的代码和参数说明。2. 数据获取与大数据预处理把气象、地形、历史火点拼成一张表2.1 三类数据源怎么选、怎么对齐做重庆火灾点预测数据源通常分三块。第一块是气象数据常见做法是从公开气象接口按站点或格点拉取逐日数据字段包括日最高温、日最低温、相对湿度、风速、降水量、连续无降水日数。第二块是地形与植被数据DEM 高程、坡度、坡向可以从公开栅格数据裁剪植被类型用 NDVI 或土地覆盖分类替代。第三块是历史火点记录一般包含经纬度、发生时间、过火面积。这三块数据的时间粒度和空间粒度都不一样。气象是逐日、站点或 0.1 度格点地形是静态栅格火点是事件级、带精确经纬度。对齐的核心思路是以「日 格点」为最小单元把火点落到格点上把气象插值到同一格点地形直接按格点取值。重庆辖区跨度大约在东经 105°11′ 到 110°11′、北纬 28°10′ 到 32°13′按 0.1 度切格点大约能得到 50×40 的网格对市级尺度够用。import pandas as pd import numpy as np # 气象数据假设已按站点日期整理好 weather pd.read_csv(weather_chongqing.csv, parse_dates[date]) # 火点数据经纬度时间 fires pd.read_csv(fire_points.csv, parse_dates[fire_date]) # 统一格点编号0.1 度网格 def to_grid(lon, lat, lon_min105.0, lat_min28.0, step0.1): gx ((lon - lon_min) / step).astype(int) gy ((lat - lat_min) / step).astype(int) return gx, gy weather[gx], weather[gy] to_grid(weather[lon], weather[lat]) fires[gx], fires[gy] to_grid(fires[lon], fires[lat]) # 火点标记某格点某天有火为 1 fires[label] 1 fire_daily fires.groupby([fire_date, gx, gy])[label].max().reset_index() fire_daily.columns [date, gx, gy, label] # 气象按格点日期聚合多站点落到同一格点取均值 weather_grid weather.groupby([date, gx, gy]).agg({ temp_max: mean, temp_min: mean, humidity: mean, wind_speed: mean, precip: sum, dry_days: max }).reset_index() # 左连接气象为主火点为标签 df weather_grid.merge(fire_daily, on[date, gx, gy], howleft) df[label] df[label].fillna(0).astype(int)这段代码的关键在最后一步左连接以气象格点为主表保证每个「格点-日」都有特征没有火点的位置标签补 0。参数上step0.1是精度和样本量的权衡调到 0.05 样本翻四倍但稀疏性更严重调到 0.2 会丢失山地微地形差异。dry_days取 max 是因为连续无降水日数对火险影响最大同一格点多个站点取最大值更保守。2.2 缺失值、异常值和样本不平衡的处理气象数据常见缺失尤其是偏远站点。直接 dropna 会丢掉大量正样本因为火点往往发生在数据稀疏的山区。我一般用「时间上前向填充 空间上同格点均值填充」两级策略同一格点内按日期排序前向填充填不上的用该格点历史同期均值。异常值方面温度超过 50℃、湿度为负、风速超过 40m/s 的基本是传感器故障直接置为 NaN 再走填充流程。火点数据要特别注意重复上报同一场火可能有多条记录按「格点 日期」去重是必要的。样本不平衡是这类问题的核心痛点。重庆一年火点数量有限正样本占比可能不到 1%。处理方式有三种一是负样本下采样按正负比 1:5 到 1:10 抽负样本二是用scale_pos_weight给正样本加权三是用 focal loss。我一般先下采样到 1:10 跑通流程再逐步放开负样本看模型是否退化。# 缺失值两级填充 df df.sort_values([gx, gy, date]) df[[temp_max, humidity]] df.groupby([gx, gy])[ [temp_max, humidity]].ffill() df df.fillna(df.groupby([gx, gy])[[temp_max, humidity]].transform(mean)) # 异常值处理 df.loc[df[temp_max] 50, temp_max] np.nan df.loc[df[humidity] 0, humidity] np.nan df df.fillna(df.groupby([gx, gy])[[temp_max, humidity]].transform(mean)) # 负样本下采样 pos df[df[label] 1] neg df[df[label] 0].sample(nlen(pos) * 10, random_state42) df_balanced pd.concat([pos, neg]).sample(frac1, random_state42).reset_index(dropTrue)提示下采样只在训练集做验证集和测试集保持原始分布否则评估指标会虚高。3. 特征工程与时空序列构建LSTM 要的滑动窗口怎么切3.1 气象时序特征和地形静态特征的组合LSTM 吃的是序列所以要把每个格点的历史若干天特征拼成一条样本。假设用过去 7 天预测第 8 天是否起火那么每条样本的形状是(7, n_features)。时序特征包括每天的最高温、最低温、湿度、风速、降水、连续无降水日数还可以加派生特征温差、连续高温日数、湿度日变化。地形特征是静态的高程、坡度、坡向、NDVI 对每个格点固定。静态特征不能直接塞进 LSTM 的序列维度常见做法有两种一是把静态特征复制到每个时间步和时序特征拼接二是在 LSTM 输出后与静态特征拼接再进全连接层。我倾向第二种因为静态特征不随时间变化放进序列里会让 LSTM 学不到有效时序模式。import numpy as np SEQ_LEN 7 FEATURES [temp_max, temp_min, humidity, wind_speed, precip, dry_days] def build_sequences(df, seq_lenSEQ_LEN): X_seq, X_static, y [], [], [] for (gx, gy), group in df.groupby([gx, gy]): group group.sort_values(date).reset_index(dropTrue) static group[[elevation, slope, aspect, ndvi]].iloc[0].values for i in range(len(group) - seq_len): seq group.loc[i:iseq_len-1, FEATURES].values label group.loc[iseq_len, label] X_seq.append(seq) X_static.append(static) y.append(label) return np.array(X_seq), np.array(X_static), np.array(y) X_seq, X_static, y build_sequences(df_balanced) print(X_seq.shape, X_static.shape, y.shape) # 期望输出(样本数, 7, 6) (样本数, 4) (样本数,)这段代码按格点分组后滑动切窗SEQ_LEN7是超参数重庆夏季火险往往在连续高温 5 到 7 天后达到峰值所以 7 天窗口比较合理。窗口太短捕捉不到累积效应太长会引入过多噪声且样本量减少。FEATURES列表里的字段顺序要和后续模型输入维度严格一致这是血泪经验顺序错了模型照样能跑但指标会莫名其妙地差。3.2 归一化和训练集验证集划分的时间陷阱时序数据的归一化必须用训练集的统计量不能全量归一化否则验证集信息泄漏。标准做法是按时间切分前 70% 时间做训练中间 15% 验证最后 15% 测试。不能随机切分因为随机切分会让未来信息泄漏到训练集。from sklearn.preprocessing import StandardScaler # 按时间切分 dates sorted(df_balanced[date].unique()) n len(dates) train_end dates[int(n * 0.7)] val_end dates[int(n * 0.85)] train_mask df_balanced[date] train_end val_mask (df_balanced[date] train_end) (df_balanced[date] val_end) test_mask df_balanced[date] val_end scaler StandardScaler() scaler.fit(df_balanced.loc[train_mask, FEATURES]) def scale_seq(X): shape X.shape X_flat X.reshape(-1, shape[-1]) return scaler.transform(X_flat).reshape(shape) X_seq_train scale_seq(X_seq[train_mask.values]) X_seq_val scale_seq(X_seq[val_mask.values]) X_seq_test scale_seq(X_seq[test_mask.values])参数说明StandardScaler对每个特征独立做零均值单位方差对温度、湿度这类量纲差异大的特征很必要。静态特征也要单独归一化高程动辄上千米不归一化会让网络难以收敛。注意scaler.fit只传训练集这是避免信息泄漏的关键一步。4. LSTM 与 CNN 混合模型搭建PyTorch 实现与训练调参4.1 模型结构设计和 PyTorch 代码纯 LSTM 能捕捉时序依赖但对局部气象模式的提取不如 CNN。常见做法是 CNN 做一维卷积提取时序局部特征再送 LSTM 学长期依赖最后和静态特征拼接分类。这个结构在火险预测里比纯 LSTM 稳定因为卷积层能平滑掉单日气象噪声。import torch import torch.nn as nn class FireRiskModel(nn.Module): def __init__(self, n_features6, n_static4, hidden64): super().__init__() # 一维卷积提取时序局部模式 self.conv nn.Conv1d(n_features, 32, kernel_size3, padding1) self.relu nn.ReLU() self.lstm nn.LSTM(32, hidden, batch_firstTrue, num_layers2, dropout0.3) # 静态特征分支 self.static_fc nn.Linear(n_static, 16) # 分类头 self.classifier nn.Sequential( nn.Linear(hidden 16, 32), nn.ReLU(), nn.Dropout(0.3), nn.Linear(32, 1) ) def forward(self, x_seq, x_static): # x_seq: (batch, seq_len, n_features) - (batch, n_features, seq_len) x x_seq.permute(0, 2, 1) x self.relu(self.conv(x)) x x.permute(0, 2, 1) # 回到 (batch, seq_len, 32) out, (h, c) self.lstm(x) h_last out[:, -1, :] # 取最后时间步 s self.relu(self.static_fc(x_static)) combined torch.cat([h_last, s], dim1) return self.classifier(combined).squeeze(-1)结构说明Conv1d的kernel_size3表示每次看连续 3 天的局部模式padding1保持序列长度不变。LSTM 用两层、隐藏维度 64、dropout 0.3层数和隐藏维度是算力和效果的平衡点重庆市级数据量下再大容易过拟合。静态分支单独用一个全连接层压到 16 维避免静态特征维度太低被时序特征淹没。分类头输出单个 logit配合BCEWithLogitsLoss使用。4.2 训练循环、损失函数和关键超参数损失函数用带权重的BCEWithLogitsLoss即使做了下采样正样本仍然偏少给正样本 3 到 5 倍权重能提升召回。优化器用 Adam学习率 1e-3配合ReduceLROnPlateau在验证损失不降时减半。from torch.utils.data import TensorDataset, DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) model FireRiskModel().to(device) # 正样本权重 pos_weight torch.tensor([3.0]).to(device) criterion nn.BCEWithLogitsLoss(pos_weightpos_weight) optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience3) train_ds TensorDataset( torch.FloatTensor(X_seq_train), torch.FloatTensor(X_static_train), torch.FloatTensor(y_train)) train_loader DataLoader(train_ds, batch_size64, shuffleTrue) for epoch in range(30): model.train() total_loss 0 for seq, static, label in train_loader: seq, static, label seq.to(device), static.to(device), label.to(device) optimizer.zero_grad() logits model(seq, static) loss criterion(logits, label) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() print(fEpoch {epoch}, Loss {total_loss / len(train_loader):.4f})参数说明batch_size64在显存允许下尽量大梯度更稳clip_grad_norm_的max_norm1.0防止 LSTM 梯度爆炸这是 RNN 类模型的后悔药。pos_weight3.0是经验值如果召回太低可以调到 5但精确率会下降。训练轮数 30 是配合早停用的实际以验证集 AUC 不再提升为准。4.3 评估指标怎么选才不被不平衡数据骗不平衡数据下准确率毫无意义全预测为 0 也能有 99%。必须看 AUC-ROC、PR-AUC 和召回率。火险预测场景里召回比精确率重要漏报一场火的代价远大于误报。我一般要求验证集召回不低于 0.8再看精确率能到多少。from sklearn.metrics import roc_auc_score, average_precision_score, recall_score model.eval() with torch.no_grad(): logits model(torch.FloatTensor(X_seq_val).to(device), torch.FloatTensor(X_static_val).to(device)) probs torch.sigmoid(logits).cpu().numpy() auc roc_auc_score(y_val, probs) pr_auc average_precision_score(y_val, probs) recall recall_score(y_val, (probs 0.5).astype(int)) print(fAUC: {auc:.4f}, PR-AUC: {pr_auc:.4f}, Recall: {recall:.4f})阈值 0.5 不是固定的可以按验证集 PR 曲线选一个让召回达标的阈值再应用到测试集。这一步很多人忽略直接用 0.5 会浪费模型的排序能力。5. 避坑与排查火点预测项目里最容易翻车的五件事5.1 现象验证集 AUC 0.95上线后一塌糊涂原因随机切分导致时间泄漏验证集里混入了训练集未来时段的数据。解决严格按时间切分训练集时间必须早于验证集和测试集且滑动窗口不能跨切分点。5.2 现象模型把所有样本预测为无火原因正样本权重没设或设得太小损失被负样本主导。解决先确认pos_weight生效再检查下采样后正负比必要时用 focal loss 替代 BCE。5.3 现象损失震荡不收敛原因LSTM 梯度爆炸或学习率太大。解决加clip_grad_norm_学习率从 1e-3 降到 1e-4 试同时检查输入是否归一化。5.4 现象同一格点预测结果每天跳变原因气象数据缺失填充引入了突变或窗口切分时格点顺序错乱。解决检查填充前后同格点特征的方差确认groupby后按日期排序再切窗。5.5 现象地形特征加入后指标反而下降原因静态特征量纲没归一化或直接拼进序列维度干扰了 LSTM。解决静态特征单独归一化走独立分支再融合不要复制到每个时间步。6. 进阶技巧用预测概率做火险等级分级和滚动预警模型输出的是概率直接二分类太浪费。实际业务里更有用的是把概率映射成火险等级低于 0.2 为低风险0.2 到 0.5 为中风险0.5 到 0.8 为高风险高于 0.8 为极高风险。这样一张重庆格点火险图每天更新比单点预测更有决策价值。def risk_level(prob): if prob 0.2: return 低 elif prob 0.5: return 中 elif prob 0.8: return 高 return 极高 # 对最新一天所有格点预测 latest df[df[date] df[date].max()] # 构造最新窗口后推理输出每个格点的风险等级 # probs model.predict(...) # latest[risk] [risk_level(p) for p in probs]滚动预警的做法是每天用最近 7 天数据推理一次把连续 3 天高风险以上的格点标为持续预警区。这个后处理能显著降低单日噪声带来的误报。验证方法上除了 AUC建议做一次历史回测拿过去两年的数据按时间滚动预测统计命中率和误报率这比单次切分更有说服力。我自己踩过最深的坑是早期用随机切分跑出 0.97 的 AUC兴奋了一周换成时间切分后掉到 0.78那才是真实水平。做时空预测切分方式比模型结构重要得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表