简介:这份PDF是一篇发表于《测绘学报》2021年第4期的学术论文,题为《基于深度学习的人群活动流量时空预测模型》,面向深度学习、时空数据挖掘及智能交通领域的研究人员。针对传统时空预测方法难以顾及空间多尺度特征和非线性关系的问题,论文提出MST-Net模型,通过并联卷积提取空间多尺度特征,并利用引入注意力机制的门控循环单元提取时间特征,最后经全连接层输出预测结果。模型在两组真实社交媒体签到数据集上进行验证,在RMSE和MAPE指标上均优于对比算法,可应用于人群活动流量、交通流、气候变化等预测场景。资源包仅含1个PDF文件,大小5.46MB,覆盖摘要、引言、模型结构、实验对比及结论全文,适合作为论文写作参考、模型复现依据或相关课题的参考文献。目前已有243人学习浏览,具有较高参考价值。
1. 人群活动流量预测的本质:一个时空序列预测问题
地铁闸机每分钟的进出站人数、商圈在夜幕降临前的人流密度、大型活动散场时某个路口的人群聚集速度,这些数据都有一个共同特征:它们既随时间演化,也随空间传播。街对面两栋写字楼的下班人流会在十五分钟后同时涌向同一个地铁口,而五百米外的另一个地铁口可能要到四十分钟后才迎来峰值。传统的时间序列模型把每个点位当成独立变量处理,天然丢失了这种空间联动关系。基于深度学习的人群活动流量时空预测模型,解决的就是"在多个空间位置上,根据历史观测序列,同时预测未来若干个时间步的流量值"这一任务。
这个问题和图像分类、自然语言处理常看到的深度学习任务都不太一样,它属于时空序列预测,输入是一个带有图结构或者网格结构的多元时间序列,输出是同样空间结构上的未来序列。常见的落地方向包括智慧城市的交通流量预测、基于手机信令数据的人群密度推演、应急管理中的踩踏风险预警。建模难点主要集中在三处:如何把地理空间表达成模型能读的图结构;如何平衡时间依赖关系与空间依赖关系的建模深度;如何在人流稀疏的区域不至于预测出负值或者偏差过大的数值。这篇文章沿着数据构建、模型设计、训练评估、工程排错这条路线,把这套预测模型的实现细节完整讲清楚,包括代码层面可以直接复用的部分。
2. 建模前的数据三件事:网格化、邻接矩阵和时间窗
2.1 把"人群活动流量"转换成时空图
建模的第一步不是搭神经网络,而是先把原始数据抽象成图结构。常见的数据形态有两种:一种是基于地理网格的统计值,比如把城市划分成 500m × 500m 的网格,统计每个网格里每五分钟的活跃人数;另一种是基于固定站点,比如地铁站、公交站点的刷卡数据或信令数据。无论哪种形态,本质上都构成一张图:节点是网格或站点,边代表空间关系,节点上的属性是随时间变化的流量值。
数据结构上通常用两个矩阵来表示:
- 特征矩阵 ( X \in \mathbb{R}^{N \times T} ),其中 N 是节点数,T 是时间步数,每个位置存储该节点在对应时刻的流量值;
- 邻接矩阵 ( A \in \mathbb{R}^{N \times N} ),其中 A[i][j] 表示节点 i 与节点 j 之间的空间关联强度。
如果原始数据是网格形式,还需要一个坐标矩阵记录每个网格的中心经纬度或平面坐标,用来计算邻接矩阵。这里有一个很多新手会忽略的点:网格大小直接决定了空间分辨率和计算开销的平衡。网格划分得越细,空间信息保留得越多,但节点数会急剧膨胀,邻接矩阵从 N × N 变成了稀疏大矩阵,训练时间和显存占用都会显著上升。实际做城市规划级别的大范围预测时,500m 到 1km 的网格是常见的选择,做商圈级别的精细预测则可以缩到 100m 以内。
2.2 邻接矩阵的三种构造方式
邻接矩阵是空间建模的核心,直接决定了图卷积能聚合哪些邻居的信息。我一般会在项目里对比以下三种构造方式,选效果最稳定的那个。
| 构造方式 | 公式/规则 | 适用场景 | 缺点 |
|---|---|---|---|
| 距离阈值法 | 两点距离小于阈值 d 时设为 1,否则为 0 | 节点分布均匀的场景 | 阈值敏感,稀疏区域容易断裂 |
| 高斯核加权法 | ( A_{ij} = \exp(-\frac{d_{ij}^2}{\sigma^2}) ),超出最大距离则置 0 | 流量随距离平滑变化的场景 | σ 需要调参,计算量稍大 |
| 流量相似度法 | 用历史流量序列的皮尔逊相关系数作为边权重 | 地理邻近但功能相似度高的区域 | 依赖历史数据质量,可能出现虚假相关 |
实际工程中最常用的是高斯核加权法,因为它在表达"空间越近、关联越强"的同时保留了对远距离连接的截断能力。下面给出一个可以直接运行的高斯核邻接矩阵构造函数,坐标格式是经纬度或平面坐标的二维数组。
import numpy as np def build_adjacency_matrix(coords, sigma=0.1, max_distance=0.5): """ 通过高斯核构造邻接矩阵 :param coords: (N, 2) 每个节点的坐标数组 :param sigma: 高斯核带宽,控制影响半径 :param max_distance: 超过该距离的边直接置0,保证稀疏性 :return: (N, N) 的邻接矩阵 """ n = coords.shape[0] # 计算成对欧氏距离矩阵 dist = np.sqrt(((coords[:, None, :] - coords[None, :, :]) ** 2).sum(-1)) # 高斯核加权 adj = np.exp(-dist ** 2 / (sigma ** 2)) # 超过最大距离的边截断置零 adj[dist > max_distance] = 0.0 # 对角线置零,避免自环 np.fill_diagonal(adj, 0.0) return adj这段代码里有几个参数值得细说。sigma控制的是空间衰减速度,sigma 越小,只有非常近的节点之间才有强关联;max_distance则是一个工程上的稀疏化手段,因为全连接的邻接矩阵一方面计算开销大,另一方面会把距离超远的节点强行拉上关系,反而引入噪声。在人群活动的场景里,我的经验是max_distance取网格边长 3 到 5 倍比较合理,sigma则根据实际预测效果在 0.05 到 0.3 之间做几次网格搜索即可。
2.3 用滑动窗口把时序数据切成训练样本
图结构确定后,接下来要把原始时间序列切成带标签的监督学习样本。这里要理解两个概念:历史窗口长度(history window)和预测步长(horizon)。历史窗口是模型每次读取的过去多少个时间步,预测步长是一次预测未来多少个时间步。
这两个超参的设置对人群活动流量预测有很强的领域特征。人流数据有典型的日周期性和周周期性,以五分钟为一个时间步的话,一天就是 288 个步长。这决定了历史窗口至少要包含一定长度的近期趋势,但不宜过长,因为循环网络和卷积网络对长序列的建模能力都有瓶颈。
def build_sliding_window(data, history=12, horizon=3): """ 将流量矩阵转为滑动窗口样本 :param data: (N, T) 每个节点的流量序列 :param history: 历史时间步数 :param horizon: 预测未来时间步数 :return: X: (样本数, history, N), Y: (样本数, horizon, N) """ n_nodes, total_steps = data.shape X, Y = [], [] for t in range(total_steps - history - horizon + 1): x_window = data[:, t:t + history] # (N, history) y_window = data[:, t + history:t + history + horizon] X.append(x_window.T) # 转成 (history, N) Y.append(y_window.T) # (horizon, N) return np.array(X), np.array(Y)代码中 X 的形状设计成(样本数, history, N),是考虑到大部分时空图模型在 PyTorch 中的输入布局习惯。值得强调的是,滑动窗口切分之后要按时间顺序划分训练集、验证集和测试集,不能随机打乱——因为流量数据是强时间依赖的,随机打乱会让模型在验证阶段"偷看"到未来的信息,评估出来的指标会虚高。一般我会按时间顺序取前 70% 做训练、中间 15% 做验证、最后 15% 做测试,这个比例在这个任务里比较稳健。
3. 时空耦合的建模核心:从 STGCN 到时空卷积块
3.1 为什么不能只用 CNN 或 LSTM
人群活动流量预测在模型选型上有一个很常见的误区:直接用 LSTM 对每个节点单独建模,或者把 N 个节点当成 N 个通道塞进 CNN 里。前者的问题是每个节点的模型完全独立,空间信息完全没有被利用——地铁站 A 的流量暴涨可能意味着地铁站 B 将在十分钟后暴涨,但如果两者模型参数独立,这种关联就永远不会被学到;后者的麻烦在于,CNN 的卷积核是按照欧几里得结构设计的,它假设相邻像素的语义关系是平移等变的,但城市里两个地理上相近的网格可能一个是商场、一个是居民区,功能结构并不相似,固定尺寸的卷积核对这种不规则空间关系几乎无能为力。
因此主流做法是采用**图卷积网络(GCN)**来处理空间维度,让每个节点的特征更新由它的图邻居加权聚合而来,聚合权重的底层假设则来自第 2.2 节构造的邻接矩阵。时间维度则交给时间卷积或门控循环单元来处理。这类模型的代表性架构是 STGCN(Spatio-Temporal Graph Convolutional Network),它的核心思想是把"空间图卷积"和"时间卷积"交替堆叠,形成时空卷积块,让信息不仅沿空间扩散,也沿时间传播。这类模型在多个公开交通数据集上的表现都明显优于纯时序模型,也是做人群流量预测时比较稳妥的起点。
3.2 图卷积的数学含义与一阶近似
图卷积从谱域图信号处理演化而来,完整的理论推导是:定义图拉普拉斯矩阵 ( L = D - A ),其归一化形式为 ( \tilde{L} = I - D^{-1/2} A D^{-1/2} ),图上的卷积操作可以借助傅里叶变换表达为频域的滤波运算。但直接用谱分解计算代价极高,因为矩阵特征分解的复杂度是 ( O(N^3) ),城市级别的节点数动辄几千上万,完全不可行。
因此实际工程中使用的是切比雪夫多项式近似或一阶近似。一阶近似是 Kipf 和 Welling 提出的 GCN 层,公式简洁:
( H^{(l+1)} = \sigma(\tilde{D}^{-1/2} \tilde{A} \tilde{D}^{-1/2} H^{(l)} W^{(l)}) )
其中 (\tilde{A} = A + I) 是加了自环的邻接矩阵,(\tilde{D}) 是 (\tilde{A}) 的度矩阵,(W) 是需要学习的参数矩阵,(\sigma) 是激活函数。这个一阶近似在计算上只需要做稀疏矩阵乘法,与节点数近似线性相关,可以处理大规模图。STGCN 采用的就是这种设计。
3.3 用 PyTorch 实现一个时空卷积块
下面给出一个简化但完整的时空卷积块实现,包含两个时间卷积单元和一个图卷积单元,结构上参考 STGCN 的核心设计思路但做了工程简化。输入特征布局为(batch, time_steps, num_nodes, num_features),这里假设每个节点每条时间步只取流量值这一个特征,也就是num_features=1。
import torch import torch.nn as nn class TemporalConv(nn.Module): """一维时间卷积块:沿时间维度做卷积,捕捉局部趋势""" def __init__(self, in_channels, out_channels, kernel_size=3): super().__init__() self.conv = nn.Conv2d( in_channels, out_channels, kernel_size=(kernel_size, 1), # 只在时间维度上滑动 padding=(kernel_size // 2, 0) ) self.bn = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU() def forward(self, x): # x: (B, C, T, N) -> (B, C_out, T, N) return self.relu(self.bn(self.conv(x))) class GraphConv(nn.Module): """一阶近似图卷积层""" def __init__(self, in_features, out_features, adj_norm): super().__init__() self.adj_norm = adj_norm # 预归一化邻接矩阵 (N, N) self.weight = nn.Parameter( torch.FloatTensor(in_features, out_features) ) nn.init.xavier_uniform_(self.weight) def forward(self, x): # x: (B, C, T, N),需要转成 (B*T, N, C) 做矩阵乘法 B, C, T, N = x.shape x = x.permute(0, 2, 3, 1).reshape(B * T, N, C) support = torch.matmul(x, self.weight) # (B*T, N, out) out = torch.matmul(self.adj_norm, support) # 聚合邻居信息 return out.reshape(B, T, N, -1).permute(0, 3, 1, 2) class STConvBlock(nn.Module): """时空卷积块:时间卷积 -> 图卷积 -> 时间卷积""" def __init__(self, in_channels, hidden_channels, adj_norm, kernel_size=3): super().__init__() self.temporal1 = TemporalConv(in_channels, hidden_channels, kernel_size) self.graph = GraphConv(hidden_channels, hidden_channels, adj_norm) self.temporal2 = TemporalConv(hidden_channels, hidden_channels, kernel_size) def forward(self, x): # x: (B, C_in, T, N) h = self.temporal1(x) h = self.graph(h) h = self.temporal2(h) return hadj_norm在传入模型之前需要做对称归一化处理,简单的实现方式是:
def normalize_adjacency(adj): adj = adj + np.eye(adj.shape[0]) # 加自环 degree = adj.sum(axis=1) # 度矩阵 degree_inv_sqrt = np.power(degree, -0.5) degree_inv_sqrt[np.isinf(degree_inv_sqrt)] = 0.0 degree_inv_sqrt_mat = np.diag(degree_inv_sqrt) # D^{-1/2} A D^{-1/2} norm_adj = degree_inv_sqrt_mat @ adj @ degree_inv_sqrt_mat return torch.FloatTensor(norm_adj)关于STConvBlock内部的通道数设置,hidden_channels是一个需要根据数据规模调节的参数。节点数少(如几十个地铁站)的场景,hidden_channels=32通常就够;城市级网格预测节点数上千时,可以加大到 64 或 128。每加一个时空卷积块,感受野就往外扩一层,但块数过多会带来过平滑问题——所有节点的特征趋向一致,流量预测值会趋同,这个现象在堆叠到第四层时往往开始明显,所以常规配置是堆叠两个 STConvBlock,效果与复杂度比较均衡。
4. 模型训练与评估:损失函数、评价指标和超参设置
4.1 损失函数的选择与数据归一化
人群活动流量预测的标签是实数值,直觉上会用均方误差(MSE)作为损失函数。但在实际项目中我发现 MSE 有两个隐患。第一,MSE 对离群点极其敏感,大型活动散场时段会出现个别节点的流量远高于均值的情况,模型会为了拟合这些尖峰而牺牲整体预测精度;第二,MSE 的单位是平方级,不同量级的节点(商圈 vs 居民区)对总损失的贡献差距悬殊,导致模型偏向预测人流多的节点。
因此我通常的做法是,在数据进入模型之前先做最大值归一化,把每个节点的流量映射到 0 到 1 之间,然后搭配 MSE 作为损失函数。这样做比标准 Z-Score 归一化更适合稀疏流量数据,因为 Z-Score 假设数据近似高斯分布,而人流数据往往严重右偏。
def minmax_normalize_per_node(data): """每个节点独立做最大最小值归一化""" eps = 1e-7 min_val = data.min(axis=1, keepdims=True) max_val = data.max(axis=1, keepdims=True) normalized = (data - min_val) / (max_val - min_val + eps) return normalized, min_val, max_val预测结束后,需要把模型输出反归一化回真实流量值再计算评估指标,而不是在归一化空间里算完指标就直接报告。否则 MAE 和 RMSE 的单位是归一化比例,读者或业务方看到数值会觉得模型好得离谱,但实际效果并没有那么理想。
4.2 评估指标中的 MAPE 陷阱
人群流量预测任务常用的评估指标有三个:MAE、RMSE 和 MAPE,它们的侧重点和适用边界需要区分清楚。
| 指标 | 公式 | 含义 | 在这个场景里的注意事项 |
|---|---|---|---|
| MAE | ( \frac{1}{n}\sum|y - \hat{y}| ) | 平均绝对误差 | 量纲清晰,业务方容易理解 |
| RMSE | ( \sqrt{\frac{1}{n}\sum(y - \hat{y})^2} ) | 均方根误差 | 对大误差敏感,适合衡量极端预测失误 |
| MAPE | ( \frac{1}{n}\sum|\frac{y - \hat{y}}{y}| ) | 平均绝对百分比误差 | 真实流量接近 0 时会爆炸,需要加保护 |
MAPE 在这个场景里的问题特别突出。凌晨时段很多网格的人流量为 0,即使模型预测为 0.5,MAPE 都会给出一个不可接受的巨大数值。所以处理办法通常有两种:一是只在地面真实流量大于某个阈值(比如 10 人/每五分钟)的样本上计算 MAPE;二是采用加权 MAPE,即以每个样本的真实流量作为权重。这两种方式得到的指标更符合业务直觉,同时也避免了分母为零的除法异常。
4.3 训练循环与调参配置
下面给出一段典型的训练循环骨架,包含归一化邻接矩阵计算、按时间顺序切分数据、训练和验证两段的损失记录。使用 Adam 优化器是时空图网络里非常稳定且主流的默认配置。
import torch.optim as optim def train_model(model, train_loader, val_loader, epochs, lr=1e-3): optimizer = optim.Adam(model.parameters(), lr=lr, weight_decay=1e-5) scheduler = optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', factor=0.5, patience=5 ) criterion = nn.MSELoss() for epoch in range(epochs): model.train() train_loss = 0.0 for x_batch, y_batch in train_loader: optimizer.zero_grad() y_pred = model(x_batch) # (B, horizon, N) loss = criterion(y_pred, y_batch) loss.backward() # 梯度裁剪:防止序列模型中的梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() train_loss += loss.item() * len(x_batch) train_loss /= len(train_loader.dataset) model.eval() val_loss = 0.0 with torch.no_grad(): for x_batch, y_batch in val_loader: y_pred = model(x_batch) val_loss += criterion(y_pred, y_batch).item() * len(x_batch) val_loss /= len(val_loader.dataset) scheduler.step(val_loss) print(f"Epoch {epoch:03d} | train loss: {train_loss:.6f} | val loss: {val_loss:.6f}")这里有两个参数配置值得特别说明。weight_decay=1e-5是很轻的 L2 正则,主要用于抑制时空卷积块在图节点数较多的场景下的过拟合风险;clip_grad_norm_的阈值设为 5.0,是因为时空卷积网络在训练初期损失下降较快,梯度的范数容易波动,裁剪一下能显著提升稳定性。学习率用ReduceLROnPlateau控制,patience 设为 5 的意思是验证损失连续 5 个 epoch 不下降才降低学习率,给模型充分的收敛时间。
5. 工程细节与排错:从数据泄漏到过平滑
5.1 时空数据的泄漏问题
不同于图像分类,时间序列预测里的数据泄漏问题隐蔽得多。一个高频出现的错误是:在切分训练集和验证集之前先对整个数据集做了 Z-Score 归一化,归一化的均值和方差是由全量数据计算出来的,包含验证集和测试集的信息,这属于轻微泄漏。虽然很多人觉得这种泄漏影响不大,但严格来说每次滑动窗口预测评估出的指标会比线上便宜一些。规范的顺序是:先用训练集计算归一化参数,再用相同的参数去转换验证集和测试集。特征变换只能从训练集统计归纳,这个习惯在深度学习模型里需要刻意养成。
另一个更隐蔽的泄漏是邻居信息的跨时间泄漏。在用滑动窗口切分时,如果两个相邻样本的时间窗口有一半重叠,那么它们本质上只在空间维度上稍有差异,验证集中几乎可以找到训练集样本的"近似复制品"。这种重叠会让指标虚高。因此我在实际项目中基本不使用随机划分,而是坚持按块划分——训练集、验证集、测试集在时间上严格不相交。
5.2 稀疏区域的预测值被低估
人群活动流量数据存在明显的长尾效应:少数繁华区域贡献了大量流量,而大多数网格的流量非常低甚至为零。模型在 MSE 损失下会优先拟合流量大的节点,导致稀疏节点预测值偏低。这类问题在业务上影响不小,因为在预警场景里,恰恰是那些平时人少、某天突然涌入大量人群的区域最需要准确预测。
应对方案是给损失函数按节点加权,权重与节点历史总流量成反比。实现起来很直接,构造一个(N,)的权重向量,训练时先扩充成(B, horizon, N),与预测误差逐元素相乘后再求均值。
def weighted_mse_loss(y_pred, y_true, node_weight): # node_weight: (N,),流量越小的节点权重越高 loss = (y_pred - y_true) ** 2 loss = loss * node_weight.unsqueeze(0).unsqueeze(0) return loss.mean()这个设置能明显改善稀疏节点的预测表现,但代价是繁华区域(如车站、商圈)的预测误差会略微上升,因为它分摊了部分模型的拟合能力。是否需要启用加权,取决于业务目标是整体均摊还是风险覆盖——做踩踏预警我会倾向于加权方案。
5.3 图卷积层数加深后的过平滑现象
过平滑是图神经网络特有的现象,随着图卷积层数增加,每个节点的表征会反复聚合邻居信息,导致所有节点的特征向量趋向同一方向,模型输出的预测值在空间维度上趋于雷同。反映在指标上就是验证损失在层数加到第三层、第四层后不再下降甚至回升,且预测出的热力图看起来"糊成一片"。
解决过平滑最有效的手段是先浅后深:优先用两层图卷积把空间信息聚合进来,检查街区间预测值的区分度,如果空间分辨率不足再增加层数。另外可以在图卷积层之间加入残差连接,让每层在聚合邻居信息的同时保留自身原始特征;这比单纯堆层更能保持空间语义的多样性。在 STGCN 类模型中,架构已经内置了时间卷积块穿插在每层图卷积之间,天然地打断了连续多层图卷积的直接堆叠,这也是这类架构在实际项目里稳定性较好的原因之一。
6. 验证模型是否真的学到了时空依赖:三个可用的自检方法
模型训练完之后,不能只看整体 MAE,还需要主动验证它学到的东西是否和业务假设一致。这里给出三个成本不高且有效的自检方法。
第一,预测后验热力图对比。随机挑一个工作日和一个节假日,把模型预测的下一小时人群流量和真实值一起画成热力图。重点观察早晚高峰时段:模型能否重现"居住区早晨的高流量向办公区迁移"这样的空间流动?如果热力图只是把当前时刻的流量分布做了模糊化处理,没有表现出空间转移,说明模型主要依赖时间卷积,图卷积的作用没有真正发挥。这时候要检查邻接矩阵是否过于稀疏,或者图卷积块是否有数?从两个时间数据帧之间的差异中提取流动方向的比我们常见的回溯期监测更进一步,更有可操作性。我可以补充一个更直接的诊断方法:随机挑选一个节点,构造"该节点当前流量上升、但它的图邻居流量全部下降"的测试样本,观察模型预测结果是否因为这个空间矛盾信号而发生明显变化。如果模型预测几乎不受影响,问题基本可以定位在图卷积路径上。
第二,消融实验对比梯度贡献。把训练好的模型用torch.profiler做前向和反向的梯度追踪,分别记录图卷积参数和时间卷积参数的梯度范数。如果图卷积参数的梯度长时期比时间卷积层小一个数量级,说明空间支路的贡献被时间支路压制了。常见的缓解手段是把两个支路的输出做可学习的加权求和,即让模型自己学会分配时空两个支路的置信度。
class STBlockWithLearnableFusion(nn.Module): """带可学习融合权重的时空卷积块""" def __init__(self, channel, adj_norm): super().__init__() self.t_conv = TemporalConv(channel, channel, kernel_size=3) self.g_conv = GraphConv(channel, channel, adj_norm) # 可学习的时空融合权重 self.fusion_weight = nn.Parameter(torch.tensor([0.5, 0.5])) def forward(self, x): temporal_out = self.t_conv(x) graph_out = self.g_conv(x) # softmax 归一化后加权求和 w = torch.softmax(self.fusion_weight, dim=0) return w[0] * temporal_out + w[1] * graph_out训练收敛后观察fusion_weight的值,如果时间支路的权重始终在 0.8 以上,说明空间信息的注入是无效的,需要回到邻接矩阵构造这一步去排查,重点检查sigma是否过大导致邻居信息被摊平、或者max_distance是否过小导致每个节点几乎没有有效的邻居节点。
第三,模型预测置信度下探测试。针对凌晨时段流量接近零的样本,人为构造一批"该节点未来三个时间步应为零"的测试输入,观察模型输出是否出现不合理的负值。深度学习回归模型在特征分布边缘时经常预测出负数,这不是模型实现错误,而是数据分布导致的回归特性,但业务上需要处理。遇到这个情况,在输出层之后加一个 ReLU 或 Softplus 激活函数即可,代价极小,同时能确保预测值严格非负。
这三个自检方法做完后,模型的时空建模能力大致能定位清楚了。最后提示一点:时空预测模型的评估不要只看平均误差,还要分开统计工作日在高峰时段和低峰时段的误差,因为群体活动的极端事件往往出现在高峰时段的异常波动里,那才是模型真正需要发力的地方。
本文还有配套的精品资源,点击获取