ARTICLE DETAIL

资讯详情

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

人群活动流量时空预测实战:深度学习模型与数据工程全解析

人群活动流量时空预测实战:深度学习模型与数据工程全解析 简介基于深度学习的人群活动流量时空预测模型是一篇发表于《测绘学报》2021年第50卷第4期的学术论文PDF面向地理信息、数据挖掘与智能交通等领域的研究人员和学生。该文针对传统时空预测方法难以顾及空间多尺度特征的问题提出融合空间多尺度特征的时空网络模型MST-Net通过并联卷积提取空间特征、引入注意力机制的门控循环单元提取时间特征最终以全连接层输出预测结果。论文还给出了在两个真实社交媒体签到数据集上的试验对比显示该方法在RMSE和MAPE指标上优于多种基线模型有助于读者理解时空预测中的特征提取与融合思路。资源为单个PDF文件大小5.46MB内容清晰完整已有243人学习适合需要参考深度学习时空建模方法、开展相关研究的读者下载使用。1. 人群活动流量时空预测在做什么把整座城市变成一张会滚动的流量表走进城市运行指挥中心大屏上跳动的往往不是单个摄像头画面而是一张被切成几千个小方格的数字地图——每个方格代表下一个 15 分钟会新涌入多少人、又会离开多少人。这种需求的主角就是基于深度学习的人群活动流量时空预测模型它不是预测某个地铁站的进出客流而是同时预测一整张时空流量矩阵。常见落地场景包括应急疏散、商圈选址、共享车辆调度和景区限流。难点在于区域之间的流量是相互牵引的一个商圈拥挤会在半小时后外溢到邻域普通时序模型只看时间维度而深度学习可以把“时间循环 空间依赖”放进同一个网络里建模这也是它这几年明显压过传统时间序列方法的原因。2. 把人群活动流量变成深度学习能吃的时空数据任何深度学习模型的起点都是数据张量的形状。人群活动流量的原始形态通常是点数据一条 GPS 轨迹、一条手机信令、一次闸机刷卡。要让基于深度学习的时空预测模型能训练必须先把离散点聚合到有规律的网格-时间张量上。这一章做完你会得到形状为(时间片数, 网格行, 网格列, 通道数)的四维流量张量后面所有模型都吃这个输入。2.1 三个常见数据源轨迹点、信令事件、闸机刷卡做人群活动流量预测数据源决定了你能预测的尺度和精度。我这里把从业者最常用的三类数据源整理成一张对比表选型时先对着表格判断手头数据适合做网格级还是场所级预测。数据源原始形态更新频率优势主要坑GPS 轨迹网约车/单车/手机定位带时间戳的经纬度点序列1~10 秒空间精度高能还原完整出行路径定位漂移严重室内和隧道几乎不可用隐私合规成本高手机信令基站/栅格级设备事件流、位置更新记录分钟级覆盖全人群天然反映区域间流动基站密度不均郊区格子大一跳几个网格闸机/刷卡地铁、场馆、园区进出事件秒级客流精确到人时间戳可靠只覆盖单一场所或线路无法表达跨区域流量我一般会建议如果目标是“整个城市尺度、区域间流量”优先用手机信令或 GPS 轨迹如果目标是“某个商圈、大型场馆内部的汇聚与疏散”刷卡和闸机数据就够了不需要碰复杂轨迹数据。2.2 网格切分和时间切片流量矩阵的构造数据源选好后第一步是给研究区域套一个矩形网格。常见做法是把经纬度范围均匀切成H行W列同时把连续时间切成等长的“时间片”例如 15 分钟一片。于是人群活动流量被建模成每个网格在每个时间片内的两个标量流入量和流出量。判断一次“流入”或“流出”需要比较同一设备前后两条记录的位置如果上一时刻在网格 A这一时刻到了网格 B那么网格 A 计一次流出网格 B 计一次流入。下面是一段可以直接落地的聚合代码。假设轨迹表有device_id、timeUnix 秒、lon、lat四个字段import numpy as np import pandas as pd def aggregate_flow(pts, lon_min, lat_min, lon_max, lat_max, grid_rows32, grid_cols32, slice_seconds900, t_startNone): 把轨迹点聚合成 (T, H, W, 2) 流量张量。 通道 0 表示 outflow流出通道 1 表示 inflow流入。 # 经纬度映射到网格坐标 pts pts.copy() pts[col] (pts[lon] - lon_min) / (lon_max - lon_min) * grid_cols pts[row] (pts[lat] - lat_min) / (lat_max - lat_min) * grid_rows pts[col] pts[col].clip(0, grid_cols - 1).astype(int) pts[row] pts[row].clip(0, grid_rows - 1).astype(int) if t_start is None: t_start pts[time].min() pts[slice_id] ((pts[time] - t_start) // slice_seconds).astype(int) # 按设备排序用 shift 拿到同设备上一条记录 pts pts.sort_values([device_id, time]) prev pts.groupby(device_id)[[row, col, slice_id]].shift() moved prev.notna().all(axis1) \ ((prev[row] ! pts[row]) | (prev[col] ! pts[col])) # 只保留发生了网格间移动的记录 move_df pts.loc[moved] prev_df prev.loc[moved] T int((pts[time].max() - t_start) // slice_seconds) 1 flow np.zeros((T, grid_rows, grid_cols, 2)) # 前一格计 outflow当前格计 inflow np.add.at(flow, (prev_df[slice_id].values, prev_df[row].values, prev_df[col].values, 0), 1) np.add.at(flow, (move_df[slice_id].values, move_df[row].values, move_df[col].values, 1), 1) return flow这段代码的核心是groupby shift同一设备的上一条记录位置就是“移动前的位置”。之后用np.add.at做增量计数避免普通索引加减带来的边界丢数。使用时主要调四个参数grid_rows/grid_cols决定空间分辨率slice_seconds决定时间分辨率lon_min/lat_min/lon_max/lat_max决定研究范围。注意网格越细、时间片越短张量越大且越稀疏通常 32×32 网格配 15 分钟片是一个比较容易入门的起点。2.3 先跑一个历史均值 baseline模型没搭数据先验证很多人拿到流量矩阵后直接开训深度学习模型等到模型效果不如“把上周同时段搬过来”时才发现数据聚合出了问题。所以我强烈建议在搭网络之前先写一个历史均值 baseline。这个 baseline 做的事情很简单要预测明天上午 9 点的流量就把过去四周里每天上午 9 点的流量取平均作为预测。它虽然不学空间依赖但能快速暴露时间片对齐、时区、夏令时、数据缺失等问题。def last_weeks_mean_baseline(train_flow, target_slice_id, weeks4): train_flow: (T, H, W, C)按 7 天循环排列 target_slice_id: 要预测的时间片索引 返回: 前 weeks 周同一时刻流量均值 slice_per_day 24 * 60 * 60 // 900 # 按 15 分钟片计算一天 96 片 candidates [] for w in range(1, weeks 1): past_id target_slice_id - w * slice_per_day if past_id 0: candidates.append(train_flow[past_id]) if len(candidates) 0: return np.zeros_like(train_flow[0]) return np.mean(candidates, axis0)这段代码的意义在于给后面的模型定一个“必须打败的下限”。“预测等于上周同一时刻”这个朴素方法在流量平稳时段非常强如果你的深度学习模型连它都打不过先别调参回头检查切片对齐和网格划分。我在多个项目里都发现流量分布出现一两个小时的系统性偏移几乎全是 timestamp 单位或时区换算漏了。3. 时空预测模型怎么选ConvLSTM 与图网络的适用边界数据张量准备好之后下一步是选模型。这一步没有绝对最优只有“你的网格形状 你的流量分布”最适合哪种结构。围绕标题里的“深度学习”和“时空预测”这一章我会讲两条常见路线规则网格上的 ConvLSTM以及不规则区域上的图卷积网络顺便说一下各自为什么有效、什么时候会翻车。3.1 为什么普通 LSTM 不够普通 LSTM 输入是一维序列如果把流量矩阵直接摊平成向量喂进去每个网格的空间邻接关系就丢了。比如网格(3,4)和(4,4)在物理上相邻但在一维向量里可能隔了几百个位置LSTM 很难学到“隔壁溢出会影响我”这种关系。另一种直觉做法是把每个网格当作独立时间序列分头训练这又忽略了区域联动。而基于深度学习的时空预测模型核心思想是用“卷积处理空间、循环处理时间”。ConvLSTM 把 LSTM 里的矩阵乘法换成卷积操作某个位置的输入门、遗忘门不再只依赖该位置的历史还依赖周围 3×3、5×5 范围内的历史这样空间扩散、片区联动就内化进网络参数里了。3.2 用 ConvLSTM 做端到端时空编码ConvLSTM 的单元结构和 LSTM 几乎一一对应区别只在门计算部分。下面是一份简化的 PyTorch 实现去掉了很多工程装饰但网络结构是完整的可以直接接到后面训练代码里import torch import torch.nn as nn class ConvLSTMCell(nn.Module): def __init__(self, in_channels, hidden_channels, kernel_size3): super().__init__() self.hidden_channels hidden_channels pad kernel_size // 2 # 输入 x 和上一时刻隐藏态 h 拼接后一起卷积 self.conv nn.Conv2d( in_channels hidden_channels, 4 * hidden_channels, kernel_sizekernel_size, paddingpad ) def forward(self, x, h, c): # x: (B, C, H, W)h/c: (B, hidden, H, W) gates self.conv(torch.cat([x, h], dim1)) i, f, g, o gates.chunk(4, dim1) i torch.sigmoid(i) f torch.sigmoid(f) o torch.sigmoid(o) g torch.tanh(g) c_new f * c i * g h_new o * torch.tanh(c_new) return h_new, c_new class ConvLSTMEncoder(nn.Module): def __init__(self, in_channels2, hidden_channels64, out_channels2, num_layers2, kernel_size3): super().__init__() self.hidden_channels hidden_channels self.num_layers num_layers cells [] for i in range(num_layers): cin in_channels if i 0 else hidden_channels cells.append(ConvLSTMCell(cin, hidden_channels, kernel_size)) self.cells nn.ModuleList(cells) # 最后一层 hidden 映射成目标通道数 self.output_conv nn.Conv2d(hidden_channels, out_channels, 1) def forward(self, x): # x: (B, T, C, H, W) B, T, C, H, W x.shape h [torch.zeros(B, self.hidden_channels, H, W, devicex.device) for _ in range(self.num_layers)] c [torch.zeros_like(h[i]) for i in range(self.num_layers)] last_hidden None for t in range(T): xt x[:, t] for i, cell in enumerate(self.cells): h[i], c[i] cell(xt, h[i], c[i]) xt h[i] last_hidden xt return self.output_conv(last_hidden)核心参数有两个hidden_channels和kernel_size。hidden_channels64在 32×32 网格上已经能承载不错的容量kernel_size3是默认选择如果你发现预测结果有明显的单点极值可以换成 5 增大感受野。num_layers2是时序模型的常规选择层数加到 4 以上在小数据集上很容易过拟合而且训练时间成倍上涨。forward里我取最后一个时间步的隐藏态直接过一个 1×1 卷积输出未来一个时间片的流量预测。如果你想一次预测未来多个时间片可以改为把last_hidden继续解耦成多个输出头或者让模型按时序解卷。新手入门先做“一步预测”最容易跑通。3.3 不规则区域改用图网络邻接矩阵的两种构造法网格模型假设城市是规整矩形但真实业务里行政区、商圈边界不是矩形网格太粗会把两个流量特征完全不同的片区硬拼在一起。遇到这种情况更常见做法是改用图网络把每个兴趣区域商圈、地铁站影响区、街道区块看作图的节点节点特征是该区域的历史流量序列边表示区域之间的流动强度。图卷积要做的事就是让每个节点在更新特征时按边的权重聚合邻居特征。邻接矩阵构造方法我常用两种距离 k 近邻法计算每个区域中心的经纬度距离每个节点只连最近的 k 个节点边的权重取距离的倒数。优点是简单稳定适合没有先验知识的情况。历史流量相关法统计一段时间内两个区域的进出流量序列计算 Pearson 相关系数超过阈值就建一条边。优点是把“空间上远、但通勤强相关”的两个区域也连上缺点是有可能出现逻辑上不合理的边。import numpy as np def graph_adjacency_by_distance(centers, k8): centers: (N, 2) 每个区域的中心经纬度 返回: (N, N) 的对称邻接矩阵每行只保留 k 个最近邻 n len(centers) dist np.sqrt(((centers[:, None, :] - centers[None, :, :]) ** 2).sum(-1)) adj np.zeros((n, n)) for i in range(n): nearest np.argsort(dist[i])[:k 1] # 1 是因为包含自己 for j in nearest: if j ! i: adj[i, j] 1.0 / (dist[i, j] 1e-6) return adj这段代码得到的邻接矩阵会作为图卷积的输入。注意每一行保留的邻居数不一定完全一致因为可能有距离为 0 的重叠节点所以argsort后再做一次掩码比较稳妥。实际项目里我会同时把邻接矩阵的度归一化做到图卷积层内避免高连接节点把邻居特征过度放大。选择网格 CNN 还是图网络核心判断标准是区域边界是否天然规整区域数量是否在几十量级。网格适合城市级大范围预测图网络适合片区级精细管理。4. 训练和评估时空流量预测的指标、划分与关键参数模型结构定下来后训练过程里最容易出问题的不在反向传播而在数据划分、指标选择和评分逻辑。这一章用一份可直接复用的评估代码讲清楚三件事用什么指标看模型、怎么划分数据不泄露、哪些训练参数最影响最终效果。4.1 评价指标MAE、RMSE、MAPE 以及流量场景下 MAPE 的陷阱时空流量预测最常用的指标是 MAE、RMSE 和 MAPE。MAE 反映平均绝对误差RMSE 对峰值误差更敏感MAPE 是相对误差百分比。看上去和普通回归问题一样但流量数据有个特殊性凌晨时段大部分网格流量是 0 或个位数直接算 MAPE 会把一个相当好的模型拉到几百上千。正确做法是对参与 MAPE 的网格做一次流量门槛过滤。下面是一个把三个指标一起算的评估函数实际跑评估时直接传入预测和真值即可import numpy as np def evaluate_flow(pred, true, min_flow1.0): pred / true: (T, H, W, C) 或 (N, H, W, C) min_flow: 只有真值大于该阈值的网格才参与 MAPE 计算 pred np.asarray(pred) true np.asarray(true) mae np.mean(np.abs(pred - true)) rmse np.sqrt(np.mean((pred - true) ** 2)) mask true min_flow if mask.sum() 0: mape float(inf) else: mape np.mean(np.abs((pred[mask] - true[mask]) / true[mask])) * 100.0 return {mae: mae, rmse: rmse, mape: mape}这里min_flow1.0的意思是只有真实流量大于 1 人的网格才参与 MAPE可以避免“预测 0 人、真实 1 人”这种毫无意义的 100% 误差。商业项目里我一般把min_flow设成“该网格全天流量的第 25 百分位”比固定 1 更稳。另外报告指标时要把 MAE 和 RMSE 一起给因为流量峰值场景下 RMSE 能暴露模型是不是只预测均值、对高峰毫无反应。4.2 数据划分的时间陷阱随机划分是严重的泄漏很多刚入门的人会把所有样本随机打乱按 7:1:2 切成训练、验证、测试。这在常规分类问题里没问题但时空流量预测是强自相关时序数据相邻时间片之间的流量高度相似。如果随机划分训练集里会混进测试时间段相邻的样本模型等于“见过”了测试期附近的流量水平结果虚高上线必翻车。正确做法是按时间顺序切分。比如有 60 天数据前 42 天训练第 43~50 天验证最后 10 天测试。而且要保证验证集和测试集之间也留出至少一天间隔因为前一天午夜和后一天凌晨的流量仍会串扰。def time_series_split(flow, train_ratio0.7, val_ratio0.1): flow: (T, H, W, C) 按时间顺序切分不做任何打乱 T flow.shape[0] train_end int(T * train_ratio) val_end int(T * (train_ratio val_ratio)) train flow[:train_end] val flow[train_end:val_end] test flow[val_end:] return train, val, test注意代码里没做随机打乱这是有意为之。如果你发现测试集结果比验证集差一截先怀疑训练分布和测试分布发生了漂移比如测试集跨进节假日时段。这种漂移不能靠调模型解决只能靠给模型增加节假日特征或者单独训练节假日模型。4.3 训练参数滑动窗口、预测步长、batch 与学习率流量预测训练样本的构造方式和普通图像任务不同。每个训练样本不是一张图而是一段连续历史窗口加一个未来窗口。通常我会用过去 8~12 个时间片对应 2~3 小时预测未来 1~4 个时间片。滑动窗口在构造时相邻样本之间会重叠会引入一定冗余但数据集规模不大时这是可接受的。训练参数方面常见基线如下参数常见取值说明history_len8~16历史时间片数量太短学不到周期太长训练变慢pred_len1~6预测未来几个时间片越长误差累积越大batch_size16~64卡显存够就大一点小 batch 更容易收敛不稳定learning_rate1e-3 起步1e-4 收尾可配合 ReduceLROnPlateau 调参epochs30~80流量模型收敛慢30 轮以内通常欠拟合一段最小训练循环大概长这样可以直接套进上一章的 ConvLSTM 编码器def train_one_epoch(model, train_loader, optimizer, loss_fn, device): model.train() total_loss 0.0 for x, y in train_loader: # x: (B, T, C, H, W)y: (B, C, H, W) x, y x.to(device), y.to(device) optimizer.zero_grad() pred model(x) loss loss_fn(pred, y) loss.backward() optimizer.step() total_loss loss.item() * x.size(0) return total_loss / len(train_loader.dataset)这里的loss_fn我常用nn.L1Loss()因为流量数据的峰值误差如果使用 L2 损失模型会为了减小峰值 RMSE 而倾向于输出平滑估计把高峰低估。换成 MAE 损失后模型对峰值更敏感当然训练稳定性会略差。如果发现训练 loss 震荡大可以切回 SmoothL1Loss它是 MAE 和 MSE 的折中。5. 时空预测实战避坑5 个让模型翻车的流量细节前面几章给了可运行的代码但真正投入生产的流程里让模型“看起来准确、用起来很脆”的细节往往不在网络结构上。这一章是我做过多个城市流量项目后最想按头让你记住的 5 条血泪经验每一条都是现象、原因、解决逐项对齐。5.1 数据缩放混进全局统计测试集信息被提前偷看现象训练 loss 很低测试集指标也漂亮得惊人但换到下一周真实数据后误差大得不像同一个模型。原因对流量张量做归一化时用了整个时间段的均值和最大值。测试集和验证集的统计信息就混进了特征缩放器。深度学习模型在训练时等于间接见过测试期的数值范围评价自然失真。解决先按时间切分数据再用训练集的均值和标准差去缩放训练、验证、测试三个部分。如果用的是 MinMaxScaler一定要scaler.fit(train_flow)之后再transform其他部分。保存模型时把缩放器一起存下来预测阶段先对输入做同样的缩放输出再反缩放回来。5.2 MAPE 被零流量网格拉爆现象四维指标里 MAE 看着正常MAPE 却几千上万。人眼看预测结果热力图和真实值非常接近但指标就是不涨面子。原因凌晨 2 点的流量矩阵绝大部分网格都是 0真实值为 0 时 MAPE 计算出现除零或极小值放大单个网格的误差就能把整体指标拉垮。解决评估时不要对所有网格计算 MAPE。先构造一个活跃网格掩码比如true min_flow才参与计算。更严格的做法是把一天按时段切分分别报白天高峰和夜间低谷的指标因为夜间预测在业务上本身不重要。你可以在第 4.1 节的evaluate_flow基础上加一个time_mask参数快速得到白天时段的独立 MAPE。5.3 节假日和大型活动是模型的黑匣子现象工作日和普通周末预测很好一到节假日、演唱会散场、大型展会开幕就崩预测值明显偏低。原因人群活动流量受到外部事件强烈调制而模型输入只有历史流量网络只能学到“工作日与周末的周期”学不到“明天放假”这种一次性先验。解决在输入通道里拼接外部特征。最常见做法是给每个时间片加一个is_holiday通道再给每个网格加一个事件强度通道事件发生的网格置 1周边网格按距离衰减。把这两个通道拼在流量矩阵后面模型输入从 2 通道变成 4 通道ConvLSTM 首层in_channels同步调整。对大型活动单独用活动发生日数据做增量训练比提高网络容量更省事。5.4 网格设小了流量矩阵稀疏到没法训练现象把 5km×5km 的研究区域切到 100m×100m 网格结果大部分网格 24 小时流量都是 0模型训练半天输出基本全是 0正则化参数怎么调都无效。原因网格太细导致流量信号被稀释。深度学习需要的是一块相对稠密的数据张量如果正样本网格比例低于 5%模型会倾向输出全 0 以压低损失。解决回到 2.2 节的聚合函数先统计每个网格的日均流量过滤掉过低流量的网格把它们归为一个“背景网格”类别不参与主模型预测。或者直接放大网格边长直到活跃网格比例超过 20% 再训练。记住网格分辨率不是越高越好高到信号稀疏时反而损失了空间相关性。5.5 多步预测误差滚雪球现象预测未来 1 个时间片误差很小预测未来第 4 个时间片误差翻倍并且误差大概率和“直接照搬历史同时刻均值”差不多。原因多步预测有两种做法。一种是模型每步输出一个值再反馈回输入迭代预测另一种是网络直接输出未来多个时间片。前者训练时输入的都是真实历史但预测时输入的是模型自己的输出训练和推理分布不一致误差逐级累积。解决训练阶段用 scheduled sampling即训练时按一定概率把真实上一时刻结果喂回序列概率从 1 逐渐降到 0让模型逐渐适应自己的输出误差。另一个简单方案是改成多任务输出模型最后一层不只有一个输出头而是并行输出未来 4 个时间片每个时间片独立计算损失。评估时分步长报告指标别把第 1 片和第 4 片的误差混在一起写不然你根本不知道模型到底退化到哪一步。6. 验证一套时空预测模型的可用性从残差热力图到峰值误差阈值模型训练完指标也出来了最后一步不是看总 MAE而是验证这套模型能不能被业务真正用起来。我的验证流程分三步。第一步画残差热力图。把真实流量作为横轴、预测误差作为纵轴按网格散点画出来。理想的形状是误差随流量增大而增大但相对误差保持稳定。如果看到真实流量为 200 时误差达到 150 以上说明模型把高峰区域严重低估了这种误差对应急调度是致命的。第二步按小时分桶评估。把测试集的预测按一天 24 个小时切分分别计算每个小时的 MAE。你会发现最差的时段通常是早高峰和晚高峰前后而不是凌晨。如果凌晨误差都压不住数据采集本身可能有问题。第三步对照业务阈值。与运营方确认一个可接受的峰值误差比如“预测流量 1000 人时误差不超过 200 人”或“峰值时段 MAE 不超过 15%”。模型在全天平均指标上达标并不难难的是峰值时段达标。我个人的习惯是每版模型上线前先拿“上周同时段均值”挂一个对照曲线只报相对提升率不报绝对值。因为绝对值会随季节和城市活动波动而相对提升率能说明模型真正学到了时空结构。某个冬天做过一次区域疏散演练模型在峰值时段比 baseline 只提升了 6%当时觉得效果不明显后来把网格从 500m 调到 300m峰值误差立刻降了 20%。可见流量峰值预测的瓶颈经常不在网络深度而在空间分辨率是否匹配业务颗粒度。希望这一套从数据聚合到验证的流程能帮你在自己的流量数据上少走些弯路。本文还有配套的精品资源点击获取
返回列表