嵌入层(Embedding Layer)是深度学习模型中用于将离散型数据(如POI、时间、类别等)转换为连续向量表示的核心组件。在轨迹预测任务中,嵌入层的作用是将原始的非结构化或高维稀疏特征映射到低维稠密向量空间,从而捕捉语义、时空或序列模式。
(一)DeepMove和STAN中嵌入层设计对比
关键区别:
- DeepMove:分开处理时间和POI,靠注意力机制隐式学空间关系。
- STAN:显式建模空间距离,并和时间注意力联合优化。
1. 嵌入层的基本原理
- 输入:离散的ID或类别(如POI编号、时间戳、用户ID等)。
- 输出:固定维度的连续向量(如128维、256维)。
- 核心思想:通过训练学习一个嵌入矩阵(Embedding Matrix),其中每一行对应一个离散特征的向量表示。
2. 《DeepMove》中的嵌入层设计
2.1 多模态嵌入(Multi-modal Embedding)
DeepMove需要同时建模POI语义、时间周期性和用户行为序列,因此采用多模态嵌入:
- POI嵌入(语义嵌入):
- 输入:POI的唯一ID(如
venue_id
)。 - 方法:使用可训练的嵌入矩阵(
Embedding(num_pois, d_model)
),类似Word2Vec。 - 扩展:部分实现会预训练POI嵌入(通过Skip-gram模型捕捉POI共现模式)。
- 输入:POI的唯一ID(如
- 时间嵌入(周期性嵌入):
- 输入:签到时间戳(分解为小时、星期几等周期特征)。
- 方法:
- 对小时(0-23)、周几(0-6)分别建立嵌入表。
- 将小时嵌入和周几嵌入拼接或相加,形成时间表示。
- 目的:捕捉“早上通勤”vs“周末休闲”等周期性模式。
- 用户嵌入(可选):
- 部分版本会加入用户ID嵌入,以区分个体偏好。
2.2 嵌入融合
- 拼接(Concatenation):将POI嵌入、时间嵌入等拼接为综合向量(如
[poi_emb; time_emb]
)。 - 相加(Summation):若嵌入维度相同,直接相加(假设各模态贡献平等)。
2.3 特点
- 优势:通过分离语义和周期嵌入,显式建模人类移动的多模态动机。
- 局限:空间信息(如POI距离)仅能通过序列顺序隐式学习。
3. 《STAN》中的嵌入层设计
STAN需显式建模空间依赖性,因此嵌入层更复杂:
3.1 地理空间嵌入(Geospatial Embedding)
- POI位置嵌入:
- 输入:POI的经纬度坐标(如
(lat, lon)
)。 - 方法:
- 直接编码:将经纬度归一化后通过MLP映射为向量。
- 空间核函数:使用高斯核计算POI间距离矩阵,作为空间注意力权重的基础。
- 输入:POI的经纬度坐标(如
- POI类别嵌入:
- 类似DeepMove的语义嵌入,但可能与空间嵌入联合训练。
3.2 时间嵌入
- 绝对时间编码:
- 使用Transformer式的位置编码(正弦/余弦函数)或可学习嵌入。
- 与空间嵌入独立,后期通过注意力机制交互。
3.3 时空联合嵌入
- 空间注意力矩阵:
- 通过POI距离矩阵(如
exp(-d_ij^2 / σ)
)调整注意力权重,强制模型关注地理邻近的POI。
- 通过POI距离矩阵(如
- 动态融合:
- 时空注意力模块自动学习时间和空间维度的权重分配。
3.4 特点
- 优势:显式引入地理坐标,直接建模“就近访问”等空间规律。
- 局限:计算复杂度高(需处理所有POI对的距离关系)。
4. 嵌入层的具体实现示例
DeepMove的嵌入层代码(PyTorch风格)
import torch.nn as nn
class DeepMoveEmbedding(nn.Module):
def __init__(self, num_pois, d_model):
super().__init__()
self.poi_embed = nn.Embedding(num_pois, d_model) # POI语义嵌入
self.hour_embed = nn.Embedding(24, d_model) # 小时周期嵌入
self.weekday_embed = nn.Embedding(7, d_model) # 周几周期嵌入
def forward(self, poi_ids, timestamps):
# 从时间戳提取小时和周几
hours = (timestamps % 86400) // 3600
weekdays = (timestamps // 86400) % 7
# 获取各嵌入向量
poi_emb = self.poi_embed(poi_ids) # [batch, seq_len, d_model]
hour_emb = self.hour_embed(hours) # [batch, seq_len, d_model]
weekday_emb = self.weekday_embed(weekdays) # [batch, seq_len, d_model]
# 多模态融合(相加或拼接)
combined_emb = poi_emb + hour_emb + weekday_emb
return combined_emb
STAN的空间嵌入代码(距离矩阵计算)
def gaussian_kernel(dist_matrix, sigma=1.0):
return torch.exp(-dist_matrix ** 2 / (2 * sigma ** 2))
class STANGeospatialEmbedding(nn.Module):
def __init__(self, num_pois, d_model):
super().__init__()
self.poi_loc = nn.Parameter(torch.randn(num_pois, 2)) # 可学习的POI经纬度
self.loc_proj = nn.Linear(2, d_model) # 坐标→向量
def forward(self, poi_ids):
# 获取POI坐标并计算距离矩阵
locations = self.poi_loc[poi_ids] # [batch, seq_len, 2]
dist_matrix = torch.cdist(locations, locations) # [batch, seq_len, seq_len]
spatial_attn = gaussian_kernel(dist_matrix) # 空间注意力权重
# 坐标嵌入向量化
loc_emb = self.loc_proj(locations) # [batch, seq_len, d_model]
return loc_emb, spatial_attn
5. 两篇论文嵌入层的对比总结
维度 | DeepMove | STAN |
---|---|---|
核心目标 | 捕捉时序规律和语义 | 联合建模时空依赖 |
空间信息处理 | 隐式(通过序列顺序) | 显式(经纬度+距离矩阵) |
时间信息处理 | 周期嵌入(小时/周几) | 绝对位置编码 |
计算复杂度 | 低(线性变换) | 高(距离矩阵计算) |
可解释性 | 中等(注意力权重) | 高(空间注意力可视) |
6. 嵌入层的改进方向
- 动态嵌入:根据上下文动态调整POI表示(如Graph Neural Networks)。
- 层次化嵌入:对城市区域分层编码(如网格→行政区→城市)。
- 预训练嵌入:利用无监督学习(如对比学习)预训练POI表示。
(二)详解:STAN模型中嵌入层设计
嵌入类型 | 作用 | 例子 |
---|---|---|
POI位置嵌入 | 让模型知道哪些地方离得近 | “咖啡厅”和“书店”距离200米,向量相似 |
时间嵌入 | 让模型记住时间规律 | “早上8点总是去地铁站” |
时空联合嵌入 | 结合时间和空间做预测 | “工作日的早上,家→地铁站→公司” |
示例
假设用户的历史轨迹是:
- 08:00 家(POI 1)
- 08:30 地铁站(POI 2)
- 09:00 公司(POI 3)
- 时间注意力:模型发现“08:00”和“08:30”的权重高(因为是通勤时间)。
- 空间注意力:模型发现“家→地铁站”和“地铁站→公司”的距离很近。
- 预测下一个POI:结合时间和空间,模型可能推荐“12:00 公司附近的餐厅”。
1. POI位置嵌入(Geospatial Embedding)
问题背景
在位置推荐中,POI(兴趣点,比如“咖啡厅”、“地铁站”)的地理位置非常重要。人们通常更愿意去附近的地方(比如走500米去一家咖啡馆,而不是跑5公里)。
STAN需要让模型“知道”哪些POI离得近,哪些离得远。
POI位置嵌入的两种方法
(1) 直接编码(经纬度 → 向量)
- 输入:每个POI的经纬度(比如
[纬度=39.9, 经度=116.4]
)。 - 步骤:
- 归一化:把经纬度缩放到
[0,1]
范围(避免数值过大影响训练)。 - 映射到向量:用一个神经网络(比如
nn.Linear(2, d_model)
)把[lat, lon]
转换成d_model
维向量(比如128维)。# PyTorch示例 poi_loc = torch.tensor([39.9, 116.4]) # 北京某POI的经纬度 loc_emb = nn.Linear(2, 128)(poi_loc) # 变成128维向量
- 归一化:把经纬度缩放到
- 优点:简单直接,可学习。
- 缺点:模型需要自己从数据中学习“距离近=向量相似”,可能不够准确。
(2) 空间距离矩阵(显式建模POI距离)
- 目标:直接告诉模型“POI A和POI B的距离是X米”。
- 步骤:
- 计算所有POI两两之间的地理距离(比如用Haversine公式)。
- 用高斯核函数把距离转换成“相似度权重”(距离越近,权重越大):
Attention Weight = exp ( − d i j 2 2 σ 2 ) \text{Attention Weight} = \exp\left(-\frac{d_{ij}^2}{2\sigma^2}\right) Attention Weight=exp(−2σ2dij2)
其中:- (d_{ij}) 是POI i和POI j的距离。
- (\sigma) 是控制衰减速度的参数(比如设为500米)。
- 这个权重会用于后面的空间注意力(告诉模型更关注附近的POI)。
输出:# 计算POI距离矩阵(假设有3个POI) locations = torch.tensor([ [39.9, 116.4], # POI 1 [39.91, 116.41], # POI 2(距离POI 1约1.4公里) [40.0, 116.5] # POI 3(距离POI 1约14公里) ]) dist_matrix = torch.cdist(locations, locations) # 3x3距离矩阵 spatial_attn = torch.exp(-dist_matrix**2 / (2 * 1.0**2)) # σ=1公里
[[1.0000, 0.3679, 0.0000], # POI 1与自身的权重=1,与POI 2=0.36,与POI 3≈0 [0.3679, 1.0000, 0.0000], [0.0000, 0.0000, 1.0000]]
- 优点:显式建模空间关系,模型更容易理解“就近原则”。
- 缺点:计算量大(如果有1万个POI,需要计算1亿次距离!)。
2. 时间嵌入(Temporal Embedding)
问题背景
人类活动有很强的时间规律,比如:
- 早上8点:通勤(家→地铁→公司)。
- 晚上7点:下班(公司→餐厅→家)。
STAN需要让模型“记住”这些时间模式。
时间嵌入的两种方法
(1) 绝对时间编码(Transformer风格)
- 输入:签到时间戳(如
2023-10-01 08:30:00
)。 - 方法:
- 把时间转换成分钟数(或秒数)作为绝对位置。
- 用正弦/余弦函数生成固定编码(类似Transformer的位置编码):
P E ( t , 2 i ) = sin ( t 1000 0 2 i / d model ) P E ( t , 2 i + 1 ) = cos ( t 1000 0 2 i / d model ) PE(t, 2i) = \sin\left(\frac{t}{10000^{2i/d_{\text{model}}}}\right) \\ PE(t, 2i+1) = \cos\left(\frac{t}{10000^{2i/d_{\text{model}}}}\right) PE(t,2i)=sin(100002i/dmodelt)PE(t,2i+1)=cos(100002i/dmodelt) - 或者直接用可学习的嵌入表(类似POI嵌入)。
- 特点:
- 适合捕捉“时间点”特征(比如8:00总是去地铁站)。
- 但无法直接表达“周一vs周日”这种周期性。
(2) 周期时间编码(DeepMove风格)
- 输入:时间戳分解为小时、周几等。
- 方法:
- 对小时(0-23)、周几(0-6)分别建嵌入表:
hour_embed = nn.Embedding(24, 64) # 24小时,每个小时64维向量 weekday_embed = nn.Embedding(7, 64) # 7天,每天64维向量
- 从时间戳提取小时和周几:
timestamp = "2023-10-01 08:30:00" hour = 8 weekday = 6 # 周日
- 拼接或相加小时和周几的嵌入:
time_emb = hour_embed(hour) + weekday_embed(weekday)
- 对小时(0-23)、周几(0-6)分别建嵌入表:
- 特点:
- 显式建模周期性(比如“每周一去健身房”)。
- 但无法区分“2023年10月1日”和“2023年10月2日”的绝对差异。
3. 时空联合嵌入(Spatio-Temporal Joint Embedding)
核心思想
把POI位置和时间结合起来,让模型同时学习:
- “在工作日的早上,用户会从家去公司。”
- “在周末的下午,用户会从家去商场。”
实现方法
- 独立编码:
- POI位置嵌入(经纬度或距离矩阵)。
- 时间嵌入(绝对或周期编码)。
- 时空注意力:
- 时间注意力:计算历史轨迹中哪些时间点更重要。
- 比如“早上8点的签到比下午3点的更重要”。
- 空间注意力:计算哪些POI在空间上更相关。
- 比如“当前在咖啡厅,下一个可能去附近的书店”。
- 时间注意力:计算历史轨迹中哪些时间点更重要。
- 融合:
- 把时间和空间的注意力权重相加或相乘:
联合注意力 = ( 1 − λ ) 时间注意力 + λ ⋅ 空间注意力 \text{联合注意力} = (1- \lambda) \text{时间注意力} + \lambda \cdot \text{空间注意力} 联合注意力=(1−λ)时间注意力+λ⋅空间注意力 - 调整超参数(\lambda)控制时空权重(比如(\lambda=0.5)表示时间和空间同等重要)。
- 把时间和空间的注意力权重相加或相乘:
(三)详解:时间嵌入中绝对时间编码
1. 绝对时间编码是干什么的?
核心目标:让模型知道时间点的先后顺序和时间间隔。
比如:
- “08:00签到”和“08:30签到”是连续的,间隔30分钟。
- “08:00签到”和“20:00签到”是远离的,间隔12小时。
2. 方法一:正弦/余弦函数(Transformer风格)
(1) 直观理解
想象你要把时间(比如分钟数)转换成一个向量,但这个向量需要满足:
- 不同时间点的编码不同(比如08:00和09:00的编码不一样)。
- 时间间隔越近,编码越相似(比如08:00和08:30的编码比08:00和20:00更接近)。
正弦/余弦函数能完美做到这一点!因为它是一个周期性波动的函数,可以通过不同频率的波形组合表示时间。
(2) 具体步骤
假设时间是t
(比如t=480
分钟,表示08:00),要把它变成一个d_model
维的向量(比如d_model=4
):
-
生成不同频率的波形:
- 对向量的每一维(
i=0,1,2,3
),计算一个独特的频率:
frequency i = 1 1000 0 2 i / d model \text{frequency}_i = \frac{1}{10000^{2i / d_{\text{model}}}} frequencyi=100002i/dmodel1
例如:i=0
:frequency=1/1=1
(高频)i=1
:frequency=1/10000^(2/4)=1/100
(中频)i=2
:frequency=1/10000^(4/4)=1/10000
(低频)
- 对向量的每一维(
-
计算正弦和余弦值:
- 对偶数维(
i=0,2,...
)用正弦函数,奇数维(i=1,3,...
)用余弦函数:
P E ( t , 2 i ) = sin ( t ⋅ frequency i ) P E ( t , 2 i + 1 ) = cos ( t ⋅ frequency i ) PE(t, 2i) = \sin(t \cdot \text{frequency}_i) \\ PE(t, 2i+1) = \cos(t \cdot \text{frequency}_i) PE(t,2i)=sin(t⋅frequencyi)PE(t,2i+1)=cos(t⋅frequencyi) - 例如
t=480
(08:00):i=0
:PE(480,0)=sin(480*1) ≈ -0.998
i=1
:PE(480,1)=cos(480*0.01) ≈ 0.877
i=2
:PE(480,2)=sin(480*0.0001) ≈ 0.048
i=3
:PE(480,3)=cos(480*0.0001) ≈ 0.999
- 最终编码:
[-0.998, 0.877, 0.048, 0.999]
- 对偶数维(
-
为什么这样设计?
- 高频维度(如
i=0
)捕捉短时间变化(比如分钟级差异)。 - 低频维度(如
i=3
)捕捉长时间规律(比如上午vs下午)。 - 通过正弦/余弦的周期性,模型能自动理解“480分钟(08:00)”和“510分钟(08:30)”的编码接近。
- 高频维度(如
(3) 代码示例
import torch
import math
def positional_encoding(t, d_model):
pe = torch.zeros(d_model)
for i in range(0, d_model, 2):
freq = 1 / (10000 ** (2 * i / d_model))
pe[i] = math.sin(t * freq)
pe[i+1] = math.cos(t * freq)
return pe
# 示例:08:00(480分钟)的4维编码
print(positional_encoding(480, 4)) # 输出 ≈ [-0.998, 0.877, 0.048, 0.999]
3. 方法二:可学习嵌入表(类似POI嵌入)
(1) 直观理解
如果觉得正弦/余弦太数学,可以直接把时间戳当作一个“类别ID”,然后像POI嵌入一样,给它分配一个可学习的向量。
比如:
- 把一天的所有分钟数(0~1440)当作1441个类别,每个类别对应一个向量。
- 模型通过训练自己学习“08:00”和“08:30”的向量应该接近。
(2) 具体步骤
- 离散化时间:
- 把时间戳转换成分钟数(比如08:00 → 480,20:00 → 1200)。
- 建嵌入表:
- 初始化一个
(1441, d_model)
的矩阵(1441是因为分钟数从0到1440)。 - 每个分钟数对应一个
d_model
维向量。
- 初始化一个
- 训练调整:
- 模型通过数据自动学习向量之间的关系(比如08:00和08:30的向量相似)。
(3) 代码示例
import torch.nn as nn
# 假设d_model=4,最多1441分钟(24小时)
time_embed = nn.Embedding(1441, 4)
# 示例:08:00(480分钟)和08:30(510分钟)
t1 = torch.tensor([480]) # 08:00
t2 = torch.tensor([510]) # 08:30
print(time_embed(t1)) # 输出随机初始化的4维向量,例如 [0.2, -0.5, 1.1, 0.8]
print(time_embed(t2)) # 另一个随机向量,例如 [0.3, -0.4, 1.0, 0.9]
# 训练后,t1和t2的向量会变得相似!
(4) 优缺点
- 优点:简单直接,无需手动设计编码规则。
- 缺点:
- 需要更多数据(因为向量完全靠学习)。
- 无法泛化到未见过的时间(比如如果训练数据没有08:00,模型就不知道它的编码)。
4. 两种方法的对比
维度 | 正弦/余弦函数 | 可学习嵌入表 |
---|---|---|
是否需要训练 | 不需要(固定计算) | 需要(模型学习) |
泛化能力 | 强(任意时间点都有编码) | 弱(依赖训练数据覆盖的时间) |
计算复杂度 | 低(直接公式计算) | 高(需存储嵌入表) |
适用场景 | 时间范围大或数据少时 | 时间范围小且数据充足时 |
5. 举个实际例子
假设我们要对以下时间编码(d_model=4
):
t=480
(08:00)t=510
(08:30)t=1200
(20:00)
(1) 正弦/余弦结果
时间 | 编码向量(近似) |
---|---|
480 | [-0.998, 0.877, 0.048, 0.999] |
510 | [-0.996, 0.862, 0.051, 0.999] |
1200 | [0.914, -0.407, 0.120, 0.993] |
- 观察:480和510的编码接近(因为时间差小),而480和1200的编码差异大。
(2) 可学习嵌入表结果(训练后)
时间 | 编码向量(示例) |
---|---|
480 | [0.8, -0.2, 0.5, 1.0] |
510 | [0.7, -0.3, 0.6, 0.9] |
1200 | [-0.5, 0.4, -0.1, 0.3] |
- 观察:模型学到“早晨时间”的向量相似,与“晚上时间”的向量不同。
6. 总结
- 正弦/余弦编码:像用不同频率的“波浪”组合表示时间,数学性强但无需训练。
- 可学习嵌入表:像给每个时间点分配一个“学号”,模型自己调整“学号”的相似度。
类比:
- 正弦/余弦 ≈ 音乐的音符(固定规律,谁都能听懂)。
- 可学习嵌入 ≈ 方言(需要学习才能懂,但更灵活)。