ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

GeoSANE:知识蒸馏驱动的地理空间表示学习,破解冷启动难题

GeoSANE:知识蒸馏驱动的地理空间表示学习,破解冷启动难题

做地理空间相关的模型,最头疼的往往不是模型结构,而是数据。坐标本身只是一串数字,如果没有大量用户在某个区域留下的签到、轨迹或是行为痕迹,你很难知道这个地方到底“意味着什么”。冷启动一出现,整个下游任务就跟着拉胯。GeoSANE 这个思路有意思的地方在于,它把“学习地理空间表示”这件事的底层逻辑换掉了:不再依赖海量地理行为数据,而是从预训练模型内部已有的知识里,蒸馏出一套地理语义表示。换句话说,模型自己当老师,坐标只负责当学生。这篇文章我把思路拆开讲清楚,再附上具体的实现细节和踩坑记录,适合做 GeoAI、位置推荐、轨迹挖掘,以及一切被地理冷启动折磨的研究者和工程同学参考。

1. 为什么地理空间表示学习绕不开数据这道坎

1.1 传统方法是怎么做地理表示的

之前很长一段时间,地理空间表示学习的主流做法和 NLP 里的词向量高度相似。把用户轨迹里的位置点当成“词”,用户的一条签到序列当成“句子”,然后套 Word2Vec 的 CBOW 或者 Skip-gram 去训练。训练完之后,每个经纬度坐标或者区域 ID 就对应一个稠密向量,语义相近的地点在向量空间里距离也会更近。

这套方案在签到数据密集的核心城区效果确实不错。比如某城市中心商圈和附近的交通枢纽,因为大量用户频繁在周边活动,模型很容易学到“它们属于同一片生活功能区”这个信息。还有一个变体是把地理网格化,用 GeoGrid 的方式把城市切成均匀格子,然后和文本描述、图像特征做联合嵌入,本质上还是没有跳出“用行为数据喂表示”的思路。

数据驱动本身没有错,但它隐含了一个前提:你得有足够多、足够均衡的人的行为记录。这个前提在很多真实场景里根本不成立。

1.2 数据驱动方案的三座大山

第一座山是覆盖率。城市核心区数据饱和,但城乡结合部、新开发区、旅游区这些地方,用户行为稀疏得可怜。模型在密集区学得风生水起,一到稀疏区就输出一批“无意义向量”,甚至直接把位置映射到完全不相关的语义簇里。

第二座山是长尾分布。热门地点只有几百个,长尾地点有几万个。模型训练时注意力基本被热门地点带走,长尾地点的表示要么没收敛,要么互相纠缠在一起,区分度极差。我做实验的时候遇到过这种情况:两个地理位置完全不相干的偏远区域,在向量空间里的余弦相似度居然高达 0.9 以上,明显是模型偷懒给它们打上了同一个“默认标签”。

第三座山是时效性问题。城市功能是动态变化的,一个区域去年是工业园,今年改造完变成了文创街区,行为数据要过很久才会反映这种变化。换句话说,纯数据驱动的方法拿到的是“历史平均画像”,对快速变化的区域天然滞后。

这三座山叠加起来的本质只有一句话:数据里没有的知识,模型学不到。所以如果能把“获取知识的来源”从数据换成一个本身就已经具备地理常识的预训练模型,上面这些数据相关的麻烦就能绕开很大一部分。

1.3 换个思路:向模型要知识

地理知识真的一定要从用户轨迹里来吗?站在人的视角看,你判断一个陌生地方是“大学城”还是“物流园区”,靠的是地名、周边描述、功能综述这些文本信息,而不是你在这个地方打过多少次卡。那既然语言模型在训练阶段已经看过海量包含地理概念的文本,它内部其实已经有一套关于地理语义的隐式知识。

GeoSANE 的核心设定就在这里:不再从人类行为轨迹里统计地理语义,而是把预训练语言模型当成一个知识源,让它把某个地理位置的文本背景编码成教师信号,再让一个轻量的地理编码器去学习坐标到语义空间的映射。

这个思路听起来简单,但做起来有很多细节问题:文本描述怎么来?文本编码出的语义向量怎么和坐标对齐?坐标和文本身在完全不同的模态空间里,怎么设计训练目标让它们可以互相转换?下面我会把这些问题一个一个拆开。

2. GeoSANE 核心思路:模型当老师,坐标当学生

2.1 整体架构拆解

GeoSANE 的整体流程可以分成两段:训练阶段和推理阶段。

训练阶段有两个模型参与:教师模型负责“读描述、出语义”,学生模型负责“读坐标、出向量”。教师模型是一个固定的预训练语言模型,训练过程中不更新参数。学生模型是一个地理编码器,输入经纬度坐标,输出一个低维向量,训练目标是让这个向量尽可能接近教师模型对同一位置文本描述编码出的向量。

推理阶段只有学生模型工作:给定任意一个经纬度坐标,直接前向一次,得到它的地理语义向量。这个向量可以直接用在聚类、检索、分类或者作为特征喂给下游模型。

用一句话概括:蒸馏的时候让物理坐标学习“语义坐标”,推理的时候拿到的就是一个自带语义属性的地理编码器。

2.2 教师模型:从文本描述中提取地理语义

教师模型的选择其实比较开放。通用中文场景我会优先考虑中文文本表示能力较强的预训练模型,英文场景则选对应的英文模型。模型越大效果越好,但考虑到实际部署成本,一般来说 base 规模的基本够用,更重要的操作是对输入文本的构造。

同一个位置,用不同粒度的描述去喂教师模型,出来的语义是不同的。比如一个商场,你用“某市某区某某购物中心”这种地名串描述,模型编码出的主要是行政归属信息;但如果你换成“集餐饮、购物、影院于一体的大型综合商业体”,编码出的就是功能语义。为了兼容这两种需求,我会把文本模板设计成多视角拼接:

[区域行政归属] + [地理实体名称] + [功能属性描述] + [周边环境描述]

这四个部分的顺序固定,中间用分隔符隔开。这样教师模型的输出向量里既有“这个位置在哪”的定位信息,也有“这个地方是干什么的”的功能信息,还混合了一部分周边环境语义。

文本来源可以是公开的地理数据库条目、区域百科词条、POI 类目说明等。质量要求是宁缺毋滥,因为教师模型输出的向量是学生模型对齐的目标,如果一个位置的描述本身写得含糊甚至错误,学生模型就会学到一个有偏的语义。

2.3 学生模型:轻量地理编码器结构与输入表示

学生模型的目标是“从坐标直接回归语义”,架构不能太复杂,否则推理成本和工程化难度都会上升。我用的是多层感知机配合残差连接和 LayerNorm,输入维度是 2,输出维度是 256。

这里有一个特别关键的问题:经纬度直接输入模型是不行的。两个地理上非常靠近的点,经纬度数值上可能只差小数点后几位,但这个差值对模型来说太小了,很难区分;而全球尺度下经纬度数值本身又没有归一化的边界。我建议的处理方式是在输入层做两类特征拼接:

一类是尺度化坐标。把经纬度经过 sin 和 cos 编码映射到多个不同尺度上,小尺度可以区分街区级别的位置差异,大尺度可以保留城市甚至省级的粗粒度位置信息。这个过程类似位置编码,让模型同时感知“我在哪个城市”和“我在城市的哪个角落”。

另一类是相对位置上下文。因为地理空间有很强的局部相关性,直接对单个坐标建模会丢失邻域信息。我采用的方式是在训练时随机采样目标坐标周围一定半径内的几个邻近点,把它们的尺度化坐标也拼进来,让模型感知局部环境密度和方位关系。

2.4 蒸馏损失:让坐标向量对齐语义空间

学生模型输出的是 256 维向量,教师模型输出的维度可能不一样,所以必须在训练前把教师输出层接一个线性投影,对齐到同一个维度。这个投影矩阵也是训练时一起学习的。

损失函数采用对比损失和均方误差损失的组合。对比损失的作用是拉近同一位置的坐标表示和语义表示,同时推开不同位置的表示;均方误差则强制两个表示在数值上尽量接近。温度参数控制对比损失的“锐利程度”,太大会让所有位置都混在一起,太小会让模型对微小的文本差异极度敏感,容易过拟合。

训练时每一批样本的构造方式是这样的:给定一批位置,每个位置都有一条对应的文本描述,教师模型算出语义向量,学生模型算出坐标向量,然后计算互信息层面的对齐损失。这个方案在实现上其实就是一个标准的跨模态蒸馏框架,只是把图像或者音频换成了地理坐标。

3. 实操细节:从数据准备到训练调参

3.1 区域描述文本怎么收集和清洗

这是整个流程里最耗时间的一步。你需要给训练集里每一个位置准备一条“可读的文本描述”,而这条描述的语义质量会直接决定最终表示的质量。

我拿某座城市的模拟数据做过一次完整实验,文本来源主要有三块:地理数据库中的 POI 名称和类目描述、城市功能区的公开介绍、以及周边地标实体的说明文本。把这些信息按照“行政归属 + 实体名 + 功能描述 + 周边环境”的模板拼接成最终文本。这里要注意,拼接不是简单把内容塞在一起,不同来源的信息要用自然语言连词过渡一下,避免出现“某区购物中心餐饮影院便利店”这种生硬的堆叠。

清洗环节有几个容易踩的坑:

  • 重复描述要去掉。很多 POI 名称虽然不同,描述却完全一样,会导致训练数据语义雷同。
  • 描述太短的文本要丢弃或补全。少于 20 个字的描述,教师模型很难编码出有区分度的向量。
  • 描述里如果有地理实体名称,要保留,因为实体名称本身就包含强烈的语义信号。
  • 排除明显错误的行政区归属信息,比如把位置描述写到了错误的区县名下,这会直接污染教师输出。

3.2 坐标归一化与位置编码的坑

经纬度输入最大的问题就是尺度不统一。经度的跨度是 0 到 180,纬度的跨度是 0 到 90,而且在不同纬度上,经度单位长度对应的实际距离是不一样的。如果直接把原始数值喂给模型,模型学到的很可能是“经度大小”这个虚假特征,而不是真正的空间邻近关系。

我自己的做法是先做球面坐标转换,把 WGS-84 经纬度转成地心坐标系下的三维直角坐标,然后再做尺度化编码。这个做法的好处是绕开了经度和纬度在物理尺度上的不一致性,同时三维直角坐标本身也保留了欧氏距离的直觉——两个坐标在地球表面越接近,它们在三维空间里的距离也越近。

但这会带来一个新问题:三维直角坐标只有三个分量,信息量不够。所以我在尺度化编码阶段会把这些分量再做多频正弦变换,得到一组高维特征。这一步其实借鉴了 Transformer 位置编码的思路,频率从小到大,可以同时捕捉微观和宏观两个层面的地理位置关系。

3.3 关键超参选择

我实际跑下来最影响结果的是三个参数:温度系数、对比损失权重、以及训练批次大小。

温度系数的经验范围在 0.1 到 1.0 之间。0.5 左右是一个相对稳妥的起点。温度太低会让模型只关注最难区分的负样本,导致训练震荡;温度太高会让所有表示都趋于平稳,难以拉开差距。我最后用的是 0.3,因为地理语义之间的边界本身比较模糊,稍微低一点的温度可以让不同功能区之间的区分更清楚。

对比损失权重我设置在 0.7,均方误差损失权重为 0.3。如果对比损失权重太高,训练会偏向于“把不同位置的向量分开”,忽略“和语义向量对齐”这个目标;反过来太高则会让向量在数值层面强行对齐,反而丢失对比学习的判别能力。7:3 是我测试了几组权重之后相对平衡的选择。

批次大小建议至少 128 起步。地理空间里的负样本非常多,小批次会导致对比学习的负样本覆盖不足,模型容易把完全不相关的区域误判为相似。硬件允许的话,批次大小可以继续往上提,收益递减但从经验上看比调学习率更明显。

3.4 训练流程与显存优化备忘

训练时教师模型的输出可以提前算好缓存下来,不需要每个 epoch 都重新过一遍预训练语言模型。这个优化极其关键,因为教师模型的参数量远大于学生模型,如果你把它放进训练链路里一起反向传播,显存占用会直接爆炸。在我的实验里,一次性把训练集所有位置的教师向量离线计算并保存成二进制文件,训练时直接从文件读取,训练速度几乎不受教师模型规模影响。

学生模型的训练代码框架大致如下:

# 假设 train_dataset 每个样本包含: # coord: 归一化后的坐标特征, shape=(feature_dim,) # teacher_emb: 教师模型对该位置文本描述编码后的向量, shape=(proj_dim,) for epoch in range(start_epoch, end_epoch): for batch in train_dataloader: coord = batch["coord"].to(device) teacher_emb = batch["teacher_emb"].to(device) student_out = student_model(coord) # L2 对齐损失 loss_mse = (student_out - teacher_emb).pow(2).mean() # 对比损失,使用批内负样本 student_norm = F.normalize(student_out, dim=-1) teacher_norm = F.normalize(teacher_emb, dim=-1) logits = student_norm @ teacher_norm.T / temperature labels = torch.arange(batch_size).to(device) loss_cls = F.cross_entropy(logits, labels) loss = 0.7 * loss_cls + 0.3 * loss_mse loss.backward() optimizer.step()

一个容易忽视的细节:训练中随机采样邻近点作为上下文特征时,采样半径不能固定不变。城市中心区域空间密度高,固定的小半径会采到大量相同位置,导致上下文特征信息量低;偏远区域密度低,固定半径又可能采不到任何邻近点。我最后是让半径根据当前坐标周围的历史样本密度自适应调整,密度高的地方用小半径,密度低的地方用大半径。

4. 评估实验:下游任务怎么验证表示质量

4.1 零样本能力测试

GeoSANE 最值得验证的场景是零样本迁移:训练时完全没有见过某个区域的数据,推理时给它任意坐标,看它能不能给出合理的地理语义表示。

我做过一个接近这种条件的实验:把某城市全部 POI 数据按区域切分,训练集只覆盖一部分城区,然后拿另一部分城区的坐标直接过模型,把输出的向量做聚类,再和真实的功能区标签对比。结果是:即使训练时完全没有见过测试城区的任何文本描述或行为数据,聚类得到的簇依然能比较好地对齐商业区、住宅区、工业区这些功能标签。

后面我又试了更极端的做法,直接把训练数据换到另一个城市,然后对目标城市做零样本推理。因为地理坐标系统是统一的,学生模型学习的是“坐标到语义”的映射,而不是某个城市的特有模式,所以跨城市迁移的效果比预期好。这也说明了从模型而非数据学习表示的一个本质优势:模型的先验知识是全球统一的地理常识,不依赖特定城市的数据覆盖。

4.2 位置语义相似度检索

另一个常用评估方式是检索任务。给定一个查询坐标,计算它和候选位置集合里所有位置的表示相似度,然后看排名靠前的结果是否符合人类直觉。

我拿某城市的一个体育场馆当查询点跑了一轮检索,排名靠前的结果依次是这个体育场馆周边的大型商场、邻近公园、以及同一片区的文化场馆。这个排序说明表示向量里既保留了功能相近性,也保留了空间邻近性。比较惊喜的是,跨区域的功能相似性也体现出来了:查询点和其他城区的一个大型会展中心相似度也不低,尽管它们物理距离很远,但语义上是同一类公共活动空间。

这里要特别提醒一点,检索结果的好坏和训练文本描述的关系非常大。如果某个位置的描述里写的是“大型综合商业体”,但实际现在主要功能已经变成写字楼,检索结果就会偏。文本描述是静态的,现实功能是动态的,这个偏差不是模型本身能修正的,需要在应用层面定期更新语料。

4.3 迁移到下游真实任务

做表示学习的最终目的还是服务下游。我在两个任务上测试了迁移效果。

一个是 POI 类型分类任务。用 GeoSANE 表示作为分类器输入,在标注数据量只有原来 30% 的情况下,准确率比直接使用原始经纬度特征高出不少。另一个是位置推荐任务,把用户历史访问位置的向量做平均池化作为用户特征,再和候选位置向量做内积打分,离线指标比基于传统行为共现矩阵的方案更稳定,尤其是对没有历史行为的新用户,冷启动推荐效果明显改善。

不过要说明的是,GeoSANE 在下游任务中的定位是特征工程的一部分,而不是一个端到端解决方案。它擅长的是把地理位置变成有语义的输入,真正做决策的模型还需要根据任务重新设计。把表示层和任务层解耦,反而让它的复用性更高。

5. 落地场景与扩展边界

5.1 适合的应用场景

第一个适合的场景是城市功能区块识别。对城市做网格化,每个网格的中心点过一遍 GeoSANE,得到向量后聚类,就能快速得到城市的功能分区图谱。因为不需要任何用户行为数据,所以对新城区、卫星城的分析特别友好。

第二个场景是位置语义检索和相关推荐。不管是 LBSN 里的 POI 推荐、旅游路线规划,还是基于位置的同城社交匹配,都可以把 GeoSANE 表示直接当成位置侧的 Embedding 来用。它带来的最大价值是解决了新位置的冷启动问题,一个新录入系统的酒店或者餐厅,只要拿到坐标就能给出表示,不依赖任何用户的评价或签到。

第三个是轨迹特征增强。把轨迹数据里的经纬度序列换成 GeoSANE 表示序列,再喂给下游的轨迹分类或轨迹预测模型,可以让模型更容易捕捉“经过了什么类型区域”这个语义层面的模式,而不仅仅是几何层面的路径形状。这一点在出行行为理解上有很大的价值。

5.2 不适合的场景与注意事项

如果任务本身极度依赖精确的绝对位置,比如导航、地理围栏判断,那 GeoSANE 表示并不适合直接作为唯一输入。它的语义性很强,但空间精确性天然受限,和坐标精度不是一个维度的东西。

另外,模型表示的是“位置的一般语义”,而不是“某个位置的实时状态”。如果一个区域在快速变化中,比如大型临时活动、道路封闭、季节性功能切换,GeoSANE 表示是感知不到的,必须配合实时数据源才能处理。我见过有人直接把这种静态表示拿去预测实时流量,效果不好,原因就在这里。

还有一点是关于尺度偏斜的。模型的语义对齐依赖训练文本覆盖范围内的知识,如果训练语料里某类区域描述极少,模型对这类区域的表示会相对不稳定。比如训练语料里几乎都是城市区域,乡村和自然保护区的表示就可能不够细腻。要缓解这个问题,在文本收集阶段就要有意识地保证描述类型的多样性。

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

6.1 表示空间漂移问题

训练初期我遇到了一个奇怪现象:学生模型的损失下降正常,但输出的表示整体分布和教师模型始终有偏差,即便损失已经收敛,两组向量分布的中心仍然有明显偏移。

后面排查发现原因在于损失组合。均方误差只约束了向量数值上的距离,对比损失则只约束了相似度排名,它们都没有约束向量分布的整体形态。解决方式是在损失里加了一项分布匹配约束,具体是对学生输出做一次 BatchNorm,然后让 BatchNorm 输出的统计量和教师模型输出对齐。加了这层约束之后,分布偏移问题基本消失。

6.2 文本描述质量导致语义偏差

测试模型时发现有些位置的表示明显不对,比如一个中学的表示学和物流园区混在了一起。检查对应文本描述后发现,那条文本的描述模板在拼接时把“周边有大型物流集散地”这段环境描述放在了和实体描述同等重要的位置,教师模型被周边环境信息带偏了。

这个问题的本质是模板设计里不同部分的信息没有做权重区分。后来我把模板设计成带层级的结构:行政归属和实体名称是主语义,功能描述和环境描述控制在次要位置,必要时在文本里加“主要”这样的限定词。修改完模板之后重新生成教师向量再训练,语义混乱的情况明显减少。

6.3 训练不收敛或震荡问题

还有一次训练 loss 一直在 1.2 到 1.5 之间来回跳动,怎么调学习率都不行。最后发现问题出在批次内的负样本选择策略上:训练集里的位置分布极度不均匀,大部分坐标集中在几个主城区,导致随机采样的一个批次里很多样本来自同一片区,负样本之间的区分度极差。

最直接有效的调整是在批次采样阶段限制重叠率。每次采样前先把训练集按空间网格分桶,然后从不同桶里尽量均匀地抽样本,保证同一个批次里的位置在空间上有足够的分散度。强制分散之后,对比学习的正负样本质量明显提升,loss 曲线变得平滑。

6.4 推理部署时的坐标系统不一致问题

这个坑是在把模型接到实际业务系统时遇到的。训练时我用的坐标是经过地图 API 转换后的 GCJ-02 坐标系统,但线上业务传过来的经纬度是 WGS-84 原始坐标,两种坐标系在城区范围内偏移可以达到几百米。毫不知情地直接上线后,模型输出的表示全部是错的。

排查了很久才发现是坐标系统不一致。这个问题说出来很简单,但在真实落地的链路里很容易被忽略。建议在模型入口处强制接入一个坐标系统校验和转换模块,并对输入坐标做合法性检查,避免脏数据直接进入模型。

最后分享一点个人体会

做到后面我最大的感受是,地理空间表示学习真正难的地方从来不是模型有多复杂,而是怎么找到一种稳定、可控、可解释的知识来源。GeoSANE 让我比较满意的一点是它把原来的数据依赖问题,转化成了一个文本质量管理和跨模态蒸馏问题,虽然文本收集和清洗依然繁琐,但至少路径是明确的,不会出现“数据不够所以模型无解”这种死局。如果你手头正好也被地理冷启动困扰,我建议先别急着堆数据和模型,花一周时间收集一批高质量的地块描述文本,蒸馏一个小编码器出来,可能比你在行为数据那边折腾一个月更有收获。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进