☰
连续空间预测实战:用对比学习预测未来概念向量
2026/9/29 23:56:31 网站建设 项目流程

搞序列预测的人应该都有同感:模型明明能把历史拟合得非常好,一旦要回答“接下来会发生什么”,就开始打太极。分类器能告诉你“当前看到的是什么”,却很难告诉你“下一步会走向哪里”。我最近在做的这个项目,代号叫“Next Concept Prediction 连续空间预测 IV”,核心就是换一个思路——不再预测下一个离散标签,而是把“下一个概念”放进一个连续向量空间里直接回归出来,再去下游做判断。这个IV代表第四版,前几版踩了不少坑,这一版终于把几个核心问题理顺了。

这个思路适合谁?如果你在做行为预测、轨迹预测、机器人决策、自动驾驶中的交互意图估计,或者任何需要从历史窗口推断未来状态的序列方向,这篇文章应该能给你一些可以落地的经验。我会从项目思路、表征空间设计、训练策略、推理方案,一路讲到实际跑实验时踩过的坑和排查记录。不涉及太多炫技,基本都是我自己调出来的、能复现的实操细节。

1. 从“预测下一个标签”到“预测下一个概念”

1.1 为什么连续空间预测比分类更贴近真实问题

先说一个最直观的例子。假设你在做一个自动驾驶场景中的行人意图预测,行人站在路边,接下来可能是直行、左转、右转、停下,甚至可能在犹豫。如果你用分类模型,输出一个四类的softmax概率分布,你最终拿到的只是一组互斥的候选。但真实情况根本不是互斥的——行人可能先向左迈半步,又收回去,这个动作既不是“左转”,也不是“停下”,它是一个介于两者之间的过渡状态。分类模型天然表达不了这种东西。

连续空间预测的做法是:把“概念”编码成一个向量,比如“左转”是一个方向,“右转”是另一个方向,“犹豫”可能落在两者之间的某个位置,模型预测出来的就是一个点,落在哪里就代表最接近哪个语义状态。更重要的是,向量之间可以计算距离、方向、插值,这些都是离散标签做不到的。

我最早做这个项目的时候,也是一上来就接分类头,后来发现面对几百上千个概念类别的时候,分类器经常为了区分两个相似动作,把决策边界搞得非常奇怪,泛化性很差。后来我看了不少关于表征学习、对比学习的做法,才意识到:与其直接预测“第几个类别”,不如先让模型学会一个连续空间,在这个空间里,相似的未来状态天然靠得近,不同的状态天然离得远。预测任务从“选类别”变成“挪动坐标”,问题一下子顺了很多。

这个“预测向量”的思路,本质上也是现在很多自监督模型在做的事情:不预测具体的像素、具体的词,而是预测高维空间里的一个抽象表征。区别在于,我要预测的不是“当前”的表征,而是“未来一个窗口”的表征。这就是Next Concept Prediction的核心定义:输入历史窗口,编码成一个查询向量,再预测未来窗口应该落在哪个语义坐标上。

1.2 IV代演进:前三版踩过的坑

这个项目能走到第四版,是因为前面三个版本各有各的死法。

第一版我直接端到端回归原始观测坐标。比如输入过去32帧的坐标轨迹,输出未来16帧的坐标,然后用MSE(均方误差)去约束。结果很明显——坐标本身是低层物理量,包含大量噪声和无关细节,模型把大量容量花在拟合细碎抖动上,对“语义层面”的判断几乎没学到。预测出来的轨迹平滑是平滑,但完全没有概念性。

第二版我开始用分类思路,把未来状态离散成若干固定类别,用交叉熵训练。问题是类别多了以后,很多类别之间根本没有清晰的语义边界,尤其是人的动作,常常是连续过渡的,硬分类造成了大量标注噪声和类别不平衡。测试的时候模型经常在两个相似的类别之间反复横跳,非常不稳定。

第三版我试了共享编码器加LSTM预测头的方案,直接回归一个“平均向量”。这个版本的问题是“平均化”——模型为了减少损失,倾向于把预测输出压在几个真实簇之间的中间地带。这样做出来的预测向量,和哪个簇都不够近,下游无论是检索还是聚类都用不上力。这个现象特别典型,我后来才知道这叫“回归到均值”问题,很大程度上是因为损失函数没有拉开负样本,模型没压力去区分不同概念。

第四版也就是现在这个版本,核心做了四点改变:第一,用对比学习把表征空间先学好,让相似未来窗口聚拢、不同未来窗口推开;第二,预测头不再直接和原始特征对齐,而是和一个动量更新的编码器输出的未来表征对齐;第三,训练分成两个阶段,先学空间,再学预测,避免两个目标互相干扰;第四,加了一个“不确定性信号”,模型不仅能告诉你预测到哪,还能告诉你自己有多不确定。

1.3 核心需求拆解:无标签、流式、可扩展

聊完了演进过程,说点实际的需求约束。我手里拿到的数据大部分没有人工标注,只有一些传感器的时序记录,比如轨迹点、速度、加速度,还有一些视觉特征向量。传统做法是先人工标注,标出“哪些片段属于哪个概念”,成本高不说,概念边界还经常标不一致。所以这个项目从一开始就要求能够在无标签数据上做自监督预训练。

另一个需求是流式处理。现实场景不能等一整段轨迹结束再判断,要每帧或者每几帧就给一个预测结果,所以模型必须支持滑动窗口式的在线推理,不能在推理时引入未来信息,也不能依赖全局序列。编码器和预测头都只吃当前窗口之前的信息。

还有一个需求是可扩展性。新的概念、新的场景出现时,不应该把所有模型重新训练一遍。连续空间天然支持“动态添加”——新概念只要在向量空间里落一个点,预测头不需要重新学,因为预测目标本身就是这个空间里的坐标。这一点是分类模型很难做到的。

2. 表征空间设计:把“概念”变成可以算距离的向量

2.1 数据窗口怎么切:滑窗和预测步长的选择

先讲数据准备。整个过程的关键参数有三个:历史窗口长度T,预测步长F,滑窗步幅S。

我自己实验时用的是10Hz的序列数据,也就是说一秒钟10帧。T取32帧,大概是3.2秒的历史上下文;F取16帧,也就是预测未来1.6秒的状态;S取4帧,每滑过0.4秒生成一个训练样本。这三个数值需要根据你的数据特性调整,但有几条原则:

  • T不能太短,否则上下文信息不足,模型看到的是局部碎片,学不出“概念”;也不能太长,否则实时性差,而且窗口内容变化太慢,预测任务会变得过于简单,模型容易偷懒。
  • F决定了你要预测多远。F太短,预测的基本就是当前状态的简单外推,没有“概念跳变”的挑战;F太长,未来窗口和当前窗口的关联太弱,对比学习正样本都不好定义,损失很难收敛。
  • S影响样本数量和时间分辨率,S越小样本越多,但相邻样本高度相关,训练效率不高;S太大会丢中间状态。

构造正样本对的方式是这样的:一个窗口W_t代表时刻t之前的历史片段,它对应的未来窗口是W_{t+F}。在表征空间训练阶段,我们把这两个窗口分别编码成向量,希望它们在空间里靠近。那什么是负样本呢?最简单的做法是随机采样其他时间段的未来窗口,当成“错误答案”,希望它和当前历史窗口的编码拉开距离。这里的核心假设是:同一个序列里,相邻时间段的语义状态通常更接近,随机采样的负样本大概率在语义上离得较远。

这里有一个容易被忽略的点:在对比学习阶段,正样本对中的历史窗口和未来窗口用的是同一个编码器吗?不一定。我的做法是历史窗口用一个短时编码器,未来窗口用一个特征提取编码器,两个编码器结构可以不一样。甚至在多模态场景下,历史窗口是雷达点云特征,未来窗口是相机特征,只要把两者映射到同一个向量空间就行。

2.2 编码器结构与对比损失

编码器是整个空间的地基。我做实验的时候试过几种结构,最后稳定下来的是“一维CNN + GRU”的组合,而不是单纯的Transformer。原因很简单:序列长度只有32帧,特征维度不高,Transformer的优势发挥不出来,反而训练更慢、更容易过拟合。CNN负责提取局部时序特征,GRU负责把时序上下文压缩成一个固定维度的向量,这组合在中小规模数据集上性价比很高。

对比损失我用的是InfoNCE形式,核心思想是“在N个候选里找出那个正确的未来窗口”。公式大概是这样的:

[ L = -\log \frac{\exp(\text{sim}(z_q, z_k^+) / \tau)}{\exp(\text{sim}(z_q, z_k^+) / \tau) + \sum_i \exp(\text{sim}(z_q, z_k^-_i) / \tau)} ]

z_q是历史窗口编码后的查询向量,z_k^+是未来窗口的正样本表征,z_k^-_i是从一个负样本队列里采样的负样本表征。sim可以用余弦相似度,τ是温度参数。

这里说几个我自己调出来的经验值。温度τ默认从0.07起步,如果训练不稳定或正样本相似度上不去,往0.1调;如果负样本惩罚不够、不同类别的表征混在一起,往0.05调。负样本队列我用的长度是4096,每次采样256个负样本参与计算。队列里的表征由动量编码器输出,这样可以保证负样本的来源分布相对稳定,又不至于让训练变成“追着当前编码器跑”的情况。

动量编码器的更新方式很关键,不是直接拷贝,而是指数滑动平均:

[ \theta_k = m \cdot \theta_k + (1-m) \cdot \theta_q ]

这个m我取0.99。这个设计是从MoCo那套做法借鉴来的。为什么需要它?因为如果正样本和负样本全部来自实时更新的同一个编码器,训练过程中编码器一变,所有特征的分布就跟着变,对比学习的“目标”就变成了移动靶,很难收敛。动量编码器更新得慢,相当于给特征空间提供了一个相对稳定的参考系。我实测下来,没有动量更新,损失曲线明显更抖,表征质量也差一截。

还有一个细节:负样本队列是逐步填充的,需要预热。我在正式训练前会用当前编码器跑几百个batch,把队列填满,然后再开始更新模型。这一步不花多少时间,但能避免训练刚开始因为队列里是随机初始化特征导致的剧烈震荡。

2.3 为什么预测向量而不是预测概率分布

这一点值得单独展开。我试过用LSTM输出一个softmax概率分布来预测未来的概念类别,效果不好,原因不只是类别数多,更核心的问题是概率分布本质上是“互斥”的。它假设未来状态必须落在一个明确定义的类别里,但真实世界的未来往往是模糊的、多义的,而且不同概念之间有过渡和重叠。

向量表示不一样。向量的每个维度没有“概率”的语义,它只是一组几何坐标。模型可以输出一个介于A和B之间的点,表达“倾向于A但可能偏向B”;也可以输出一个远离所有已知簇中心但模长很小的点,表达“不确定”。这是概率分布很难优雅表达的事情。

在训练阶段,我还给预测头加了一个约束:预测出来的向量要被归一化到单位超球面上。也就是说,预测头输出的是方向,不是长度。这一步看着不起眼,实际影响很大。归一化之后,余弦相似度直接就是向量内积,相似度范围的分布更稳定,训练更顺。如果不归一化,预测头可以偷懒把向量输出得很长或很短,模型会钻空子。

为什么要归一化?做个类比:你问导航系统“去公园怎么走”,它应该给你一个方向,而不是一个“步数长度”。长度未知可以,但方向错了就全错了。在语义空间里,方向代表概念倾向,长度代表置信度。我这里把置信度问题单独拆出来用另一个头处理,预测方向就只负责方向。

3. 训练策略与推理落地:两阶段训练和滚动预测

3.1 阶段一:对比预训练,先把空间形态学好

整个训练我拆成了两个阶段。阶段一是表征学习阶段,用对比学习把“历史窗口”和“未来窗口”的对应关系,映射到连续语义空间里。这个阶段不训练预测头,只训练编码器,目标是让未来窗口表征在空间里形成有意义的簇结构。

这个阶段有几个实操要点。先说数据增强。有人会问,时序数据怎么做增强?我试过加噪声、随机mask、局部时间扭曲,都有用,但要注意分寸。我常用的组合是:以0.2的概率随机mask掉2~4帧,以0.1的概率在特征上加标准差0.02的高斯噪声。增强太猛,正样本对之间的语义关联会被破坏;增强太弱,模型容易记住具体帧而不是学概念。

训练参数我用的是:batch size 256,优化器AdamW,学习率3e-4,余弦退火调度,总共训练200个epoch。这个配置不算激进,一卡就能跑起来。

阶段一结束之后,我会做一次快速评估:把测试集里的未来窗口全部过一遍编码器,拿到向量后用t-SNE降维可视化。这一步很重要,能直观看到不同语义概念是不是聚成了簇。如果可视化结果是乱糟糟的一团,没有分群结构,不要去动预测头,先回来调对比损失的温度、负样本数量和数据增强策略。

我在做这一版的时候,第一次t-SNE出来效果就比较好:相似动作聚在一起,不同动作边界清楚。这一步给了我很大信心,说明空间结构是能学出来的。

3.2 阶段二:冻结编码器,只调预测头

阶段二的目标是让历史窗口的查询向量能“看”到未来。具体做法是:冻结阶段一训练好的编码器参数,拿历史窗口编码后的向量作为输入,训练一个轻量的预测头,目标是预测未来窗口的编码向量。

这个阶段我一开始图省事,直接把查询向量喂给三层MLP,输出和目标向量算MSE。结果损失降得很快,但预测出的向量依然是“均值脸”——总落在几个簇之间的中性位置,没有一个明确指向。我后来分析原因,发现是MSE对“离所有簇都比较近但都不完全贴近”的情况惩罚不够大。

于是我换了方案:预测头输出之后,不直接算MSE,而是把预测向量和未来窗口真实向量都做L2归一化,然后算1减去余弦相似度作为损失。同时加一个辅助的“推开”损失,对预测向量和负样本队列里的随机向量的相似度加以惩罚,促使预测结果不要躲到安全区。

这个“推拉开”的辅助项,做法很简单:取出预测向量和若干负样本表征,计算相似度,用softmax把相似度归一化,然后希望预测向量对应正样本的排名尽量靠前。它本质上是把一个点估计问题变成一个排序问题,逼迫预测头在空间里做出明确选择。

阶段二训练结束之后,我再做一次评估:把预测向量和真实未来向量一起做t-SNE可视化。这时候应该能看到,对于清晰单一的场景,预测向量基本落在真实未来簇的内部或边缘;对于模糊场景,预测向量会落在几个簇之间。

3.3 推理阶段:滚动窗口与不确定性度量

训练只是一个环节,真正落地的时候最怕的是模型“上线才会出事”。推理我采用的是滚动窗口方式:每收到S帧新数据,就切一个最近的T帧窗口,过一个编码器,再过预测头,输出一个预测向量。

光输出一个向量还不够,我还设计了一个不确定性度量。拿预测向量和空间里已知的若干“概念锚点”做对比,取距离最近的锚点距离和次近的锚点距离,两个距离的比值可以作为一个置信度信号。如果最近距离和次近距离差不多,说明模型在两个概念之间摇摆,预测结果的可信度要打折。另外一个信号是预测向量在空间里的邻域密度:如果预测向量周围邻居很少,说明落点比较孤立,很可能是一个少见状态甚至异常状态。

这两条结合起来,在落地的时候非常有用。比如在机器人控制场景里,置信度低的时候控制器选择保守策略,置信度高的时候执行激进决策。只给一个向量没有不确定性这个维度,下游很难做风险控制。

推理速度方面,我这个编码器加预测头,单次推理在CPU上大概5毫秒左右,GPU上可以做到1毫秒以内,完全能满足实时性要求。当然这是基于32帧小窗口的设计,如果你换成大Transformer,这个数字会差很多。

3.4 模型核心代码参考

这里放一段简化过的核心代码,方便大家对照思路。这不是完整工程代码,但核心结构和训练关键点都在里面。

import torch import torch.nn as nn import torch.nn.functional as F class ConceptEncoder(nn.Module): def __init__(self, input_dim, hidden_dim=128, out_dim=64, context_len=32): super().__init__() self.cnn = nn.Sequential( nn.Conv1d(input_dim, hidden_dim, 3, padding=1), nn.ReLU(inplace=True), nn.Conv1d(hidden_dim, hidden_dim, 3, padding=1), nn.ReLU(inplace=True), ) self.gru = nn.GRU(hidden_dim, hidden_dim, batch_first=True) self.head = nn.Sequential( nn.Linear(hidden_dim, out_dim), nn.ReLU(inplace=True), nn.Linear(out_dim, out_dim), ) def forward(self, x): # x: [B, T, D] x = x.transpose(1, 2) # [B, D, T] h = self.cnn(x).transpose(1, 2) # [B, T, H] _, hn = self.gru(h) # [1, B, H] out = self.head(hn.squeeze(0)) out = F.normalize(out, dim=-1) # 归一化到单位球面 return out class ConceptPredictor(nn.Module): def __init__(self, feat_dim=64, hidden_dim=128): super().__init__() self.net = nn.Sequential( nn.Linear(feat_dim, hidden_dim), nn.ReLU(inplace=True), nn.Linear(hidden_dim, feat_dim), ) def forward(self, h): out = self.net(h) out = F.normalize(out, dim=-1) return out

对比损失部分的简化代码如下:

def info_nce_loss(query, positive, negatives, temperature=0.07): # query: [B, D] 历史窗口查询向量 # positive: [B, D] 未来窗口正样本向量 # negatives: [B, N, D] 负样本向量 pos_sim = (query * positive).sum(-1) / temperature # [B] neg_sim = torch.einsum('bd,bnd->bn', query, negatives) / temperature logits = torch.cat([pos_sim.unsqueeze(1), neg_sim], dim=1) # 正样本排第一 labels = torch.zeros(query.size(0), dtype=torch.long) return F.cross_entropy(logits, labels)

动量编码器的更新代码就不贴了,公式很简单,每步训练之后执行一次指数滑动平均即可。有一点要提醒:动量编码器在对比预训练阶段用,在阶段二预测头训练时就不需要再更新了,直接用冻结的编码器输出目标向量就好,否则预测目标会变。

4. 常见问题与排查技巧实录

这部分我整理一下自己在实际调试过程中遇到的问题。很多是文档和论文里不会写的,只有自己跑起来才会遇到。

4.1 训练震荡与NaN:先查梯度,再查温度

我在阶段一训练到大概40个epoch的时候,出现过一次损失突然变成NaN的情况。第一反应是学习率太高,把学习率从3e-4降到1e-4,重新训练,跑到70个epoch又炸了。后来才发现问题出在温度参数上——负样本队列里有几条极端特征,和查询向量相似度特别高,除以0.07之后指数放大,数值溢出。

解决办法是给对比损失的logits加一个裁剪,把值限制在[-20, 20]区间,同时把温度从0.07提高到0.1,双管齐下之后再也没有NaN出现过。另外梯度裁剪也建议加上,clip值1.0足够,不会影响正常训练。

4.2 预测向量总是“均值脸”:损失函数要往“选择”上逼

前面提过,用纯MSE训练出来的预测头,输出总是落在簇与簇之间的模糊地带。这是连续空间预测里最典型的问题。我排查下来,原因不只是损失函数,还有一个重要因素:负样本在阶段二没有参与计算,模型缺少“选A不选B”的压力。

后来我在预测头损失里加了一个交叉熵风格的选项:预测向量和所有概念簇中心算相似度,把正确簇的排名拉高。这个和之前说的“推开负样本”是同一个思路。具体做法是维护一个概念锚点列表,每个概念有几条代表向量,每次算损失的时候,把预测向量和所有锚点的相似度过一遍softmax,用真实未来窗口所属概念的标签算交叉熵。注意这个标签可以很粗糙,即使有少量错标也没关系,因为还有对比损失在拉语义距离。

4.3 滞后效应:预测结果比真实状态慢半拍

我在早期版本里发现,预测向量往往和“上一个窗口的真实状态”更接近,而不是“未来状态”。换句话说,模型学会了延后,而不是预测。这个问题本质上是因为训练目标里,历史窗口和当前窗口的相似度太高了,模型发现直接把当前状态搬过去就能混过大部分损失。

对策有几个。一是增加预测步长F,从8帧调到16帧,强制模型学习更长距离的变化。二是给历史窗口编码器增加因果mask,防止模型偷偷看到窗口边界以外的信息。三是做一个简单的对比测试:跑一段真实轨迹,把模型输出的预测向量和历史窗口状态向量、未来窗口真实向量分别算相似度,如果前者明显偏高,说明滞后严重,需要继续调F或者增强负样本难度。

4.4 阈值失灵:相似度阈值在不同场景下不是常数

推理阶段要判断“预测向量落到哪个概念”,通常会给一个相似度阈值,超过阈值才算匹配。但阈值这东西特别容易出问题:在场景A跑得好好的阈值,换到场景B就完全失灵。原因是不同场景的特征分布不同,相似度的基线不一样。比如传感器噪声大,整体相似度就偏低;特征维度高,距离自然大。

我的做法是放弃固定阈值,改用“相对排名”或“归一化后的概率”。具体来说,对一个预测向量,我把它和所有概念簇中心比较,得到一个相似度列表,然后用softmax把它转换成“选择每个概念的概率”,只有当最高概率超过第二高概率的一定倍数,比如2倍,才判定为可信预测。这样阈值具有相对性,在不同场景下都有一定鲁棒性。

4.5 负样本不够“难”:加上困难负样本挖掘

对比学习的质量很大程度上取决于负样本质量。如果负样本都是很容易区分的遥远样本,模型学到的是一个松散的空间,真正有区分度的边界没人负责。我在阶段一训练中期,会定期从当前batch里找出那些“和正样本相似度最高但不是正样本”的样本,作为额外负样本重点参与损失计算。这个操作相当于给模型上难度,逼着它把近似概念的边界抠出来。

实现起来不复杂:编码器算完batch内所有向量后,两两计算相似度矩阵,对每个查询向量,找出相似度最高的那几条,反正这些本来就是负样本,直接参与对比损失就行。这样做的代价是额外计算一次相似度矩阵,batch不是特别大的时候完全可接受。

4.6 问题速查表

现象可能原因解决手段
损失不下降温度过高、增强太强降温度到0.05~0.07,减弱增强强度
训练震荡学习率过大、负样本队列未填充降低学习率、预热队列
NaN温度过低导致指数溢出logits裁剪、提升温度、梯度裁剪
预测均值化只有MSE,没有负样本压力加分类辅助损失、推远负样本
预测滞后预测步长太短、模型学到复制当前状态增大F、使用因果编码、检查信息泄露
相似度阈值不稳定场景特征分布变化改用相对排名、softmax概率比值

5. 这套方案还能怎么扩展

连续空间预测这套框架做到IV版本,其实已经能覆盖不少实际场景,但还有几个方向值得继续挖。

第一是分层时间尺度。现在只预测一个未来窗口,但真实世界是多重时间尺度的:下一秒怎么动,接下来几秒做什么,接下来一分钟的大意图。我计划在后面版本里加一个金字塔结构,底层预测近未来,高层预测远未来,每层共享同一个表征空间,只是预测头的输出目标不同。这样可以得到一个多分辨率预测结果,对决策系统来说信息量更丰富。

第二是多模态对齐。我有一次尝试把视觉特征和传感器数据都映射到同一个概念空间后,发现预测的稳定性比单模态高很多。这个方向可以继续深挖:用对比学习做跨模态对齐,让模型在某个模态信息缺失的时候,依然能从另一个模态得到足够线索。

第三是概念空间的在线更新。目前空间在训练完就基本固定了,但实际场景里经常出现新概念。我的想法是利用弹性权重巩固(EWC)或者原型重放的方式,在保留旧概念空间结构的同时,给新概念开辟新区域。这条路比较难,但一旦打通,这套系统的实用价值会大很多。

从V1到V4,这个项目最主要的收获其实就一句话:预测未来不一定要靠更复杂的时序模型,先把“未来”的表示空间建好,预测任务会变得非常简单。目前版本的对比预训练加预测头方案,已经能在一张消费级显卡上用几小时训完,部署到实时系统里也没有压力。如果你也在做类似方向,建议先把历史窗口和未来窗口的表征空间做出来看一眼,再考虑要不要上更复杂的模型结构。很多时候,空间没建好,模型再大也是浪费。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询