1. 先看懂问题:城市时空预测到底难在哪
1.1 任务定义与四大典型场景
城市时空预测,说白了就是利用城市里各类传感器和设备产生的时空数据,预测未来一段时间某个空间位置上会发生什么。最典型的就是导航软件里的“前方路段15分钟后拥堵”,你看到的不是拍脑袋的结果,而是模型根据历史车流、天气、时间做了推演。再比如共享单车平台要提前调度车辆,平台得知道明天早高峰哪个地铁口会涌进几百辆车;气象部门要预测空气污染扩散,也需要逐小时的逐网格浓度预报。
这类任务的输入可以抽象成一张又一张的“时空快照”:假设把城市划分成若干个网格或路网节点,每个时间步长下,每个网格都有若干维特征(流量、速度、人群密度、空气质量等)。模型要学的就是从历史快照序列里读取空间依赖和时间依赖,然后外推未来快照。围绕这个抽象,衍生出四类典型场景:
| 场景 | 预测对象 | 典型粒度 | 时序特性 |
|---|---|---|---|
| 交通流量预测 | 车流量/速度/通行时间 | 路网节点或网格,5~15分钟 | 早晚高峰强周期 |
| 人群流动预测 | 进站/出站/区域内活动量 | 站点或功能区,15~30分钟 | 节假日突变 |
| 空气质量预测 | PM2.5/臭氧浓度 | 网格,1小时 | 气象驱动、长时依赖 |
| 共享出行需求预测 | 订单量/车辆需求 | 网格,30~60分钟 | 天气敏感、事件驱动 |
我最早接触这类任务是在某智慧城市项目里做交通流量预测。当时团队用的是一个经典的时空图卷积模型,在单一城区数据集上精度还不错。但项目做到第二年,新城区数据接入,整个模型直接失灵。因为新城区道路拓扑不同、POI分布不同、数据缺失率也不同,原本学到的空间结构根本迁移不过去。这也是我当时对“通用时空预测”这个概念产生兴趣的直接原因。
1.2 老模型的三个硬伤
讲通用化之前,先说说现有模型到底哪里不行。我把这几年踩过的坑归纳成三条:
第一,模型与场景强绑定。绝大多数时空预测模型在架构设计阶段就把场景假设写死了。图卷积类模型要求输入是路网拓扑图,你把网格数据塞进去就报错;Transformer类模型稍微灵活点,但输入输出格式也是定死的。换一个任务,哪怕只是把“预测30分钟”改成“预测6小时”,很多模型就要重新设计解码器。这种强绑定导致开发成本极高,每接一个新场景就是一次从零开始。
第二,数据分布差异大。城市不同区域的数据分布差异之大,超出很多人的直觉。市中心早晚高峰曲线陡峭,郊区曲线平缓;工作日和休息日的周期完全不同;突发交通管制、极端天气这些异常事件又会彻底打乱规律。真正常见的是:一个模型在A城区调到最优,放到B城区,精度下降20%~30%是家常便饭。你很难用一套参数同时适配多个分布差异巨大的数据集。
第三,标注成本不可控。很多时空预测方法依赖额外标注做辅助任务,比如用区域功能属性、事件标签来增强表征。但标注城市网格的功能类别、标注特殊事件的时间点,都需要大量人工。做学术研究可以不计成本,落地时老板只看ROI。项目组最怕的就是模型论文里写得漂亮,一上生产环境要标注几万条数据,直接劝退。
换个角度看,这三个硬伤指向同一个核心痛点:城市里每一个新场景都从零训练专用模型,代价太高,而城市场景又实在太多、太碎了。
2. 为什么是“提示”:通用化路线的三岔口
2.1 三条技术路线的对比
想解决“一个模型管多个场景”的问题,业界其实有过几条探索路线,我评估下来各有优劣。
第一条是多任务学习。思路很直接:把交通、人群、空气质量预测放到同一个模型里,共享底层编码器,各自保留任务头。听起来很美,实际很难训。因为不同任务的数据量、噪声水平、收敛速度都不一样,联合训练时很容易出现某个任务主导梯度、其他任务学不动的情况。调loss权重就够调几个月。
第二条是大规模预训练加微调。先在海量时空数据上预训练一个通用编码器,下游任务来了就微调。这条路的问题是:微调阶段仍然需要针对每个任务单独改结构、单独跑流程。哪怕只微调最后几层,一个场景一个模型的事实没有改变。而且城市数据标注本来就少,预训练和微调之间的“鸿沟”比NLP领域更严重。
第三条是提示学习。这个思路借鉴了NLP里的Prompt范式:同一个语言模型,通过不同的“提示”来切换任务模式,而不是改模型结构。放到时空预测里,就是设计一种“任务提示”,告诉模型当前要预测什么、在什么粒度上预测、按什么格式输出。模型本身是通用的,变化的是提示。这正好解决了前两条路线的核心矛盾:模型只维护一份,任务之间靠提示切换。
我之所以认这条路线,还有一个很实际的原因:它具有天然的模块化属性。模型主干可以长期保持不变,新场景接入时只需要设计新的提示模板,这非常贴合工程上“核心稳定、外围扩展”的架构原则。
2.2 提示赋能的设计哲学
“提示赋能”四个字,关键在于“赋能”不在模型里,而在模型外。模型的本体是一个通用的时空序列编码器,它学的是所有城市时空数据共有的规律:近邻相似性(空间上相近的地方行为相似)、周期性(早晚高峰、周末效应)、事件冲击后的衰减恢复等。这些规律是所有场景共享的底座知识。
提示承担的角色是“任务描述符”。给模型看交通流量任务的提示,它就提取与车流相关的表征;给空气质量任务的提示,它就提取与污染物扩散相关的表征。用大白话讲:模型像一名老工程师,技能是通用的,你告诉他今天要看交通,他就调出交通方面的经验来分析;明天要看空气,他又调出空气相关的经验。提示就是那个“告诉他今天看什么”的指令。
这套设计哲学还有一层关键含义:**缓解了对标注数据的依赖。**传统方法做新任务需要标注数据来重新拟合,而提示方案只需要设计提示词——这个工作依赖的是对任务的理解,而不是数据的规模。我在实践里体会到,这非常契合城市场景碎片化的现实:可能某个新任务只有几百条历史数据,但凭着通用的底座知识加准确的提示,输出结果依旧可用。
3. 模型架构与核心实现
3.1 整体框架拆解
这一节把模型架构掰开揉碎讲清楚。整体上是“编码器-提示器-解码器”三段式结构,加上一个贯穿始终的时空Token化模块。
先看整体数据流。原始输入是一组历史时空快照,形状通常是[T_in, N, D],其中T_in是历史时间步数,N是空间单元数量(网格或节点),D是每个空间单元的特征维度。这些快照先经过时空Token化,变成Token序列;然后与提示Token拼接,一起送入统一编码器;编码器输出的表征再经过提示感知解码器,映射到目标时空维度。
这里有一个与经典Transformer做对比的好例子。标准Transformer做翻译,输入是源语言的Token序列和任务无关的表示;而这个架构的特殊之处在于,Token序列中包含了一类“可学习提示Token”,它们的职责不是表达数据内容,而是表达任务语义。这就像做翻译时,除了句子本身,你还给模型传递了“这是一段法律文本,要用正式语气翻译”的额外指令。
3.2 时空Token化:把城市切成模型能吃的样子
输入数据的Token化是第一步,也是最容易被低估的一步。我的经验是这一步做不好,后面的注意力机制全在浪费时间。
对于网格类数据,可以直接把每个网格在某个时间步的特征作为一个Token。对于路网数据,需要先用图嵌入或者随机游走方法把节点映射成稠密向量,再作为Token。我在实现中采用的是“空间分块+时间滑窗”的组合:空间上把城市按固定网格切分,时间上取最近24个步长作为输入窗口。
这样做的好处是把时间建模完全交给Transformer的自注意力机制去处理,空间结构则保留在Token的特征向量里。Token序列的长度由T_in x N决定,例如24个时间步乘200个网格,就是4800个Token。对于标准Transformer来说这个长度偏大,所以我在实际架构里加了空间下采样——先把200个网格按区域聚合到50个区块,减小序列长度,等解码阶段再上采样回原分辨率。
# 时空Token化的简化示意 def spatiotemporal_tokenize(x, spatial_group, temporal_window): # x: [B, T, N, D] 原始输入 B, T, N, D = x.shape # 空间聚合: N -> N' x_agg = aggregate_by_group(x, spatial_group) # [B, T, N', D] # 时间滑窗采样: 取最近temporal_window个时间步 x_win = x_agg[:, -temporal_window:, :, :] # [B, T_win, N', D] # 展平为Token序列 tokens = x_win.permute(0, 2, 1, 3).reshape(B, N' * T_win, D) return tokens3.3 提示构建:三类提示的组合使用
提示模块是整个模型的灵魂,也是我调试时间最长的部分。我把它拆成三类提示的组合:
任务类型提示,用一个可学习的向量表示“当前模型要做的是交通预测还是空气质量预测”。这相当于一个任务ID,告诉编码器该激活哪类时空模式。比如交通任务的提示向量会引导注意力更关注早晚高峰特征,空气质量任务的提示向量会引导注意力更关注风向、湿度等气象相关特征。
数据形态提示,描述当前输入的数据粒度(网格还是路网)、时间分辨率(5分钟还是1小时)、空间单元数量。这个提示很重要,因为城市数据的异构性不止在任务层面,还在数据组织层面。一个统一编码器如果不知道当前输入是“50个网格每小时”还是“200个路段每15分钟”,很难做出正确判断。
输出约束提示,告诉解码器预测的范围(未来6步还是一个步长)、输出分布假设(流量预测用对数正态分布更合适,需求预测用泊松分布更合适)。这直接影响解码器最后一层的激活函数和损失函数选择。
在具体实现上,这三类提示各自对应一组可学习的Embedding向量,最后拼接(或相加)得到一个统一的提示向量,输入到编码器的每一层。我做了一个关键设计决策:提示向量不仅在第一层输入,而是每一层Transformer都注入一次。这样做的理由是:只注入第一层,后续层的注意力模式会逐渐“遗忘”任务语义;每层注入则让任务信息始终参与特征交互。
# 提示注入的层级别实现示意 for layer in encoder_layers: # 每层都注入提示向量 h = layer(h, prompt_emb) # prompt_emb: [B, P, D] 每层重复注入3.4 注意力适配与解码输出
提示Token与数据Token一起进入自注意力层之后,Attention矩阵里就多了一块“任务上下文”对应的区域。注意力机制天然允许不同Token之间交互,数据Token可以“查询”提示Token获取任务信息。但这里有一个细节:提示Token不应被数据Token过度影响。比如当某天的数据有异常噪声时,如果任务提示被噪声表征污染,模型可能对任务语义产生错误判断。
我的做法是采用非对称注意力掩码。提示Token可以关注数据Token,但数据Token只能通过受限方式关注提示Token。具体来说,提示Token在注意力计算时可以看到全部数据Token,而数据Token在计算Attention时对提示Token使用较小的权重偏置。这相当于给提示信息加了“只出不进”的隔离层,保证任务语义的纯净。
解码器部分相对直接。经过编码器处理的表征通过一个全连接层,映射到预测时空张量。但我在解码前会做一次“提示条件化”:把输出提示与编码器最后一层表征做Cross-Attention,让输出特征中带有明确的输出格式信息。这一步在实验里大约有2%~3%的相对精度提升,值得保留。
解码的最终形状需要根据任务变化。交通速度预测输出[B, T_out, N, 1],空气质量预测输出[B, T_out, N, C](C为污染物种类数)。这个维度变化由输出约束提示携带,解码器依据它动态调整输出头的维度。
4. 实验设计、效果对比与泛化性验证
4.1 数据集与基线选择
实验阶段我们用了三种类型共五个数据集,确保覆盖面足够:交通流量类用两个不同城市的路网数据;人群流动类用地铁刷卡和一个商圈人流数据;空气质量类用城市的监测站点浓度数据。每个数据集都按时间顺序切分,前70%训练,中间10%验证,后20%测试,严格防止信息泄露。
基线模型选了四类代表:
- 传统统计方法用历史平均(HA)和差分整合移动平均自回归(ARIMA)
- 通用时空图模型用STGCN和Graph WaveNet
- 纯Transformer用标准时空Transformer
- 多任务/预训练路线选了一个共享编码器的多任务模型
所有基线都选了在各自任务上较优的配置。这里我一直强调:做这一类对比实验,调基线的时间必须舍得花,否则结果不具备说服力。我当时为了把Graph WaveNet在路网数据上跑到它论文里的水准,花了将近一周调参。
4.2 主实验结果:一个模型,五个场景
核心实验是:用同一个预训练完成的通用底座,在五个任务上只修改提示,不重新训练模型参数。评测指标用MAE和MAPE(平均绝对百分比误差)。
| 数据集 | 最佳基线 | 基线MAPE | 本文模型MAPE | 相对提升 |
|---|---|---|---|---|
| 城市A交通流 | Graph WaveNet | 11.82% | 9.97% | 15.7% |
| 城市B交通流 | STGCN | 13.45% | 11.24% | 16.4% |
| 地铁客流 | 时空Transformer | 9.64% | 8.35% | 13.4% |
| 商圈人流 | Graph WaveNet | 15.82% | 12.68% | 19.8% |
| 空气质量 | 时空Transformer | 18.27% | 15.14% | 17.1% |
“一个模型,五个场景”这句话说起来轻松,实际上实验做了很久,因为要证明这个“只改提示不重训模型”的设定不是噱头,需要严格冻结编码器和解码器参数,只在提示Embedding上做少量迭代优化。从结果看,跨任务的泛化能力没有因为通用化而显著牺牲精度,这是整个工作最有说服力的一点。
4.3 消融实验:提示设计的贡献度量化
为了搞清楚到底哪部分设计在起作用,我做了三组消融:
第一组去掉任务类型提示,只保留数据和输出提示。MAPE平均恶化约6%,说明任务语义对特征提取方向的引导非常关键。
第二组把提示只在第一层注入(而不是每层注入)。MAPE平均恶化3.5%,验证了我在3.3节提到的“每层注入保持任务语义”的判断。
第三组去掉非对称注意力掩码,让数据Token和提示Token完全双向交互。MAPE平均恶化2%左右,但在异常事件多的数据集上恶化超过5%。这说明提示信息的“只出不进”隔离在处理异常数据时尤其重要。
消融实验还有一种更直观的验证方式:可视化注意力热力图。去掉任务提示后,模型对空间区域的关注近乎均匀分布,看不出任务特征;加回提示后,交通预测模型的注意力明显集中在主干道和历史拥堵点,空气质量模型的注意力集中在工业区和监测站点附近。这让我确信,提示确实在引导模型“看该看的地方”。
4.4 少样本与冷启动测试
通用模型最值得期待的就是少样本能力。我用微调数据量做梯度测试:分别用每个任务全量数据的100%、20%、5%、1%训练提示模块(底座仍冻结),观察精度变化。
| 数据量 | 交通流MAPE | 空气质量MAPE |
|---|---|---|
| 100% | 9.97% | 15.14% |
| 20% | 10.31% | 15.68% |
| 5% | 11.06% | 16.72% |
| 1% | 12.48% | 18.93% |
1%数据下精度退化在可接受范围内,这是传统从头训练完全做不到的——传统模型用1%数据训练,MAPE会在25%以上,甚至直接不收敛。这个测试对真实项目有很强的参考价值,因为很多新场景刚接入时的数据量就是那么可怜。
5. 实操心得与避坑指南
5.1 训练策略上的三类坑
第一类是提示Token的初始化问题。我一开始把所有提示向量都随机初始化,结果训练早期很不稳定,某个任务的提示向量偶尔会被更新到异常区域,导致整个表征崩塌。后来改成“预热初始化”:先用任务数据的中心向量初始化任务提示,再用小型自编码器把提示向量压缩到稳定区域,最后才接入主模型联合优化。这个改动显著提升了训练稳定性。
第二类是冻结与解冻的节奏。底座的预训练和提示的适配,不是一次训练就能完成的。我的经验是分三阶段:第一阶段在大量混合数据上预训练底座,冻结提示模块;第二阶段冻结底座,只训练提示模块;第三阶段联合微调,但底座学习率设置为提示模块的十分之一。这种渐进式训练比直接端到端训练效果更可控。
第三类是关于批大小和序列长度的矛盾。时空Transformer的Token序列通常比较长,直接导致显存占用高。我踩过一个坑:在某个数据集上,为了把序列塞进显存,把批大小从小降到了4,结果模型不收敛。后来用梯度累积解决,保持了等效批大小32的效果,才稳定训练。如果你也遇到显存不足的问题,先别急着降批大小,试试梯度累积。
5.2 评测上的隐蔽陷阱
时空预测模型的评测有一个特别容易翻车的点:同一数据集不同切分方式结果差异极大。有些人按时间顺序切分,有人随机打乱切分,后者的MAE会非常好看但毫无意义——因为训练集里混入了未来的数据。在对比不同论文结果时,必须确认评测协议完全一致,否则数字没有可比性。
还有一个细节:网格类数据和路网类数据的评测粒度不同。网格数据按区域平均MAE,路网数据按节点MAE,两者不能直接比较。我在论文实验里用相对提升而非绝对数值来对比,就是为了抹平这种差异。
另外,异常事件(交通事故、极端天气)对MAE的影响非常大,单次异常事件可能让整体MAE上升3%。如果测试集里恰好包含了这种事件,结果会偏差很大。我在做实验时会额外记录“剔除异常时段后的MAE”,用来评估模型在常规情况下的真实水平。
5.3 移植到自有数据时的五个改造建议
如果你想把这套思路用在自己的城市数据上,我整理了五条最核心的建议:
数据质量检查优先于模型调参。时空数据的缺失模式(随机缺失还是连续缺失)、空间单元对齐方式(网格偏移还是节点错位)对模型效果影响极大。花一周清洗数据,比花一周调模型参数收益更大。
提示设计要扎根于你对业务的理解。不能照搬论文里的提示模板,要分析自己的数据有哪些特征:如果数据存在极端事件,就加一个“事件提示”;如果数据有明显季节性,就加“日期语义提示”。提示越贴合业务,模型越聪明。
预训练数据的选择要广。底座模型需要见多识广才能通用,所以预训练阶段尽可能收集不同城市、不同类型的时空数据。我试过只用单一城市数据预训练,再用到其他城市,效果打七折。
输出约束提示要谨慎。输出分布的假设影响损失函数和最终结果的可解释性。交通流和空气污染物浓度的分布特征完全不同,必须用不同的输出提示来指定分布假设。
最后,实际部署时要注意提示版本的迭代管理。我发生过一个事故:升级底座模型后忘了同步更新旧的提示向量,导致线上推理结果突然劣化。建议将底座版本和提示版本做绑定管理,升级时必须成对上线。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| 训练不收敛 | 提示初始化不稳定 | 用数据中心向量做提示预热初始化 |
| 多个任务效果此消彼长 | 联合训练梯度竞争 | 分阶段训练,先底座后提示再联合微调 |
| 新任务效果达不到预期 | 提示与任务语义不匹配 | 重新设计提示模板,增加任务类别细粒度 |
| 异常事件预测崩溃 | 数据Token污染提示Token | 确认非对称注意力掩码生效 |
| 显存不足 | 序列长度过长 | 用梯度累积代替降批大小,或空间下采样 |
| 不同数据集效果差异大 | 评测协议不一致 | 统一数据切分方式,统一评价指标 |
6. 从论文到现实:我对这个方向的几点判断
做这个项目期间,最大的体会是:通用化不是白拿的,它需要你愿意在底座上花费数倍的预训练成本,换取新场景接入时的效率和稳定。我当时为预训练底座准备了相当于五个标准数据集混合的训练语料,训练时间接近单品模型的十倍。但换来的是:后来团队接入第六个、第七个场景时,每个场景从准备数据到上线只用了不到一周。
我个人在实际操作中最满意的一点是,这个方案极大地降低了新场景启动时对标注专家的依赖。以前做一个新城市,需要业务专家梳理区域特征、历史事件,才能配置出一个好模型;现在只需要业务专家花半天时间设计提示模板,剩下的事情交给底座。这让算法团队从“每城一调”的重复劳动中解放出来,有精力去做真正高价值的探索。
如果想让这个方向继续往前走,我觉得有几个切入点值得继续尝试。第一,把提示从离散向量升级成结构化模板,加入更多业务规则约束;第二,把底座扩展到轨迹级别数据,覆盖更多城市感知模态;第三,探索提示的自动生成,让模型根据新任务的数据分布自动设计提示,而不是依赖人来设计。这几个方向对工程落地都有直接的价值。
最后分享一个容易被忽略的技巧:提示模块的训练数据可以掺入少量目标任务的“伪样本”。所谓伪样本就是用规则生成的近似数据,它的数量可以很大,质量虽然不高,但能帮提示模块预先建立任务边界的初步感知。我在冷启动测试中发现,掺入伪样本后,再遇到真实数据的1%少量标注,精度还能额外提升2%左右。这是从一次失败的实验反转发现的,一开始我觉得伪样本没价值,后来对比实验才发现它对提示优化有正向作用。做这一类通用模型,多留几个心眼总没错。