【推荐系统3】向量召回:i2i召回、u2i召回
目录
i2i(Item-to-Item)召回
Word2Vec
Skip-Gram(跳字模型)
CBOW(Continuous Bag of Words)(连续词袋模型)
代码实操(Skip-Gram + 负采样)
Item2Vec
代码最大的变化:
EGES(Enhanced Graph Embedding with Side Information)
代码主要的变化:
Airbnb:将业务目标融入序列
U2I召回
双塔模型(Two-Tower Model)
相关代码
FM(Factorization Machine)
DSSM(Deep Structured Semantic Model)
YouTubeDNN
本文章为阅读FunRec推荐系统的笔记,并整合网络其他资料和自己的理解写成。
i2i召回的核心是序列建模,到头来就是在说如何更好的利用序列和物品的相似性;
u2i召回的核心是双塔架构,预测这个用户会喜欢什么商品。用户塔和物品塔使得物品向量可以离线预计算,而用户向量可以实时计算。
但用户的行为始终是动态的,简单的向量表示似乎还是不能充分描述用户的兴趣和动态的行为模式...
i2i(Item-to-Item)召回
Word2Vec
通过神经网络学习单词在上下文环境中的语义关联,使语义相近的词具有相似的向量表示。
主要有以下两种模型
Skip-Gram(跳字模型)
中心词来预测上下文词

优化:负采样优化
负采样的直观解释是:对于真实的词对,我们希望增加它们的相似度;对于随机采样的负样本词对,我们希望降低它们的相似度。
CBOW(Continuous Bag of Words)(连续词袋模型)
上下文词来预测中心词
代码实操(Skip-Gram + 负采样)
from gensim.models import Word2Vec
from gensim.utils import simple_preprocess# 1. 准备数据:模拟文本语料(句子列表,每个句子是分词后的单词列表)
corpus = ["我 喜欢 吃 苹果 香蕉","我 喜欢 吃 橙子 葡萄","他 喜欢 喝 牛奶 咖啡","他 喜欢 喝 果汁 茶"
]
# 分词(simple_preprocess 自动 lowercase 并过滤短词)
tokenized_corpus = [simple_preprocess(sentence) for sentence in corpus]# 2. 训练 Word2Vec(Skip-Gram 模型,开启负采样)
model = Word2Vec(sentences=tokenized_corpus,vector_size=10, # 词向量维度(简化为10维,实际可设50-300)window=2, # 上下文窗口大小sg=1, # 1=Skip-Gram,0=CBOWnegative=5, # 负采样数量(每个正样本配5个负样本)epochs=10, # 训练轮次min_count=1 # 最少出现1次的词才保留(简化数据用)
)# 3. 查看结果:验证语义相似性
print("=== Word2Vec 结果 ===")
# 查看"苹果"的向量
print("苹果的词向量:", model.wv["苹果"][:5], "...") # 只显示前5维
# 找与"苹果"最相似的词(应输出"香蕉""橙子"等水果)
print("与苹果最相似的词:", model.wv.most_similar("苹果", topn=2))
# 找与"牛奶"最相似的词(应输出"咖啡""果汁"等饮品)
print("与牛奶最相似的词:", model.wv.most_similar("牛奶", topn=2))
Item2Vec
Item2Vec直接采用Word2Vec的Skip-Gram架构,将每个用户的交互历史视为一个集合而非序列,忽略了交互的时间顺序。
代码最大的变化:
user_item_interactions = {"user1": ["item1", "item2", "item3"], # 用户1买了商品1、2、3(水果类)"user2": ["item1", "item2", "item4"], # 用户2买了商品1、2、4(水果类)"user3": ["item5", "item6", "item7"], # 用户3买了商品5、6、7(家电类)"user4": ["item5", "item6", "item8"] # 用户4买了商品5、6、8(家电类)
}
把句子转换成集合
EGES(Enhanced Graph Embedding with Side Information)
将用户交互历史简单视为无序集合,忽略了时序信息可能丢失重要的用户行为模式。
一、构建商品关系图

最终构建的图如图(c),在b图里面使用随机游走可以生成大量的商品序列用于后续的embedding学习。
二、融合辅助信息解决冷启动
使用 GES(Graph Embedding with Side Information) 方法引入了商品的辅助信息,可以学习到更准确的embedding
代码主要的变化:
# 准备数据:物品关系图 + 辅助信息(物品类别)
# 物品ID:0-5;物品类别(辅助信息):0=水果,1=jiadian(用one-hot表示)
item_categories = torch.tensor([[1, 0], # item0:水果[1, 0], # item1:水果[1, 0], # item2:水果[0, 1], # item3:jiadian[0, 1], # item4:jiadian[0, 1] # item5:jiadian
], dtype=torch.float32)# 构建物品关系图(边表示“被同一用户购买”,如item0和item1被user1购买,连边)
edges = [(0, 1), (1, 2), # 水果类内部连接(3, 4), (4, 5), # jiadian类内部连接(0, 3) # 跨类别连接(模拟有用户同时买水果和jiadian)
]# 转换为DGL图(无向图,需双向添加边)
u, v = zip(*edges)
g = dgl.graph((list(u) + list(v), list(v) + list(u))) # 无向图:边(u,v)和(v,u)# 定义简化版 EGES 模型(图卷积+辅助信息拼接)
class SimpleEGES(nn.Module):def __init__(self, item_emb_dim=8, category_dim=2):super().__init__()# 基础物品嵌入(图结构学习的嵌入)self.item_emb = nn.Embedding(6, item_emb_dim) # 6个物品,8维嵌入# 融合层:将基础嵌入(8维)与辅助信息(2维)拼接,输出最终嵌入(10维)self.fuse_layer = nn.Linear(item_emb_dim + category_dim, item_emb_dim)def forward(self, g, item_ids):# 图卷积:用简单均值聚合邻居信息(模拟GES的图嵌入)with g.local_scope():g.ndata['emb'] = self.item_emb.weight # 初始化节点嵌入g.update_all(dgl.function.copy_u('emb', 'msg'), # 发送邻居嵌入dgl.function.mean('msg', 'neigh_emb')) # 聚合邻居均值# 基础嵌入 = 自身嵌入 + 邻居均值(增强结构信息)base_emb = g.ndata['emb'] + g.ndata['neigh_emb']# 融合辅助信息(物品类别)category_info = item_categories[item_ids] # 获取输入物品的类别fused_emb = torch.cat([base_emb[item_ids], category_info], dim=1) # 拼接final_emb = self.fuse_layer(fused_emb) # 线性层融合
相比 Item2Vec,EGES 加入了 “物品关系图”(结构信息)和 “物品类别”(辅助信息),让嵌入更精准。
Airbnb:将业务目标融入序列
1. Airbnb设计了全局上下文机制:让最终目标与序列中的每一个备选信息都形成正样本对进行训练,无论它们在序列中的距离有多远。
为什么word2vec对顺序敏感?窗口大小选用多少?
上下文窗口的 “局部顺序依赖”
代码中常常默认window=5
2. Airbnb的另一个创新是改进了负采样策略:迫使模型学习同一地区内物品的细微差别
3. 冷启动解决方案:对于每天新增的物品,Airbnb采用了基于属性的初始化策略。系统根据新物品的属性找到相似的已有物品,使用它们embedding的均值来初始化新房源的向量表示。这种方法有效解决了98%以上新房源的冷启动问题。
U2I召回
(这个用户会喜欢什么商品)
双塔模型(Two-Tower Model)
用户塔(User Tower)专注于理解用户——处理用户的历史行为、人口统计学特征、上下文信息等,最终输出一个代表用户兴趣的向量u。
物品塔(Item Tower)则专精于刻画物品——整合物品的ID、类别、属性、内容特征等,输出一个表征物品特性的向量v。
相关代码
用ai生成了一个简单的双塔模型代码:
class TwoTowerModel(nn.Module):def __init__(self, num_users, num_items, num_categories, max_age):super(TwoTowerModel, self).__init__()self.max_age = max_age # 年龄归一化用# ----------------------# (1)用户塔:输出64维用户向量# ----------------------# 用户ID嵌入(16维)self.user_id_emb = nn.Embedding(num_users, 16)# 性别嵌入(8维)self.gender_emb = nn.Embedding(2, 8)# 浏览历史商品嵌入(每个商品16维,共3个)self.history_emb = nn.Embedding(num_items, 16)# 用户塔DNN(输入维度=16+1+8+16=41)# 16(user_id) + 1(age) + 8(gender) + 16(history_avg) = 41self.user_dnn = nn.Sequential(nn.Linear(41, 128),nn.ReLU(),nn.Linear(128, 64) # 输出64维用户向量)# ----------------------# (2)物品塔:输出64维商品向量# ----------------------# 商品ID嵌入(32维)self.item_id_emb = nn.Embedding(num_items, 32)# 商品类别嵌入(16维)self.category_emb = nn.Embedding(num_categories, 16)# 物品塔DNN(输入维度=32+16+1=49)# 32(item_id) + 16(category) + 1(price) = 49self.item_dnn = nn.Sequential(nn.Linear(49, 128),nn.ReLU(),nn.Linear(128, 64) # 输出64维商品向量)# ----------------------# (3)相似度计算相关# ----------------------self.temperature = 0.1 # 温度系数self.sigmoid = nn.Sigmoid() # 二分类激活函数def forward(self, user_feat, item_feat):"""前向传播:输入用户特征和商品特征,输出预测概率user_feat: [batch_size, 4] → [user_id, age, gender, hist1, hist2, hist3](实际是6维,因浏览历史3个ID)item_feat: [batch_size, 3] → [item_id, category, price]"""# ----------------------# 处理用户特征 → 生成用户向量# ----------------------# 拆解用户特征:user_id(0), age(1), gender(2), history(3-5)user_id = user_feat[:, 0] # [batch_size]age = user_feat[:, 1].float() / self.max_age # 归一化到0-1 → [batch_size]gender = user_feat[:, 2] # [batch_size]history = user_feat[:, 3:6] # 浏览历史3个商品ID → [batch_size, 3]# 嵌入层计算user_id_emb = self.user_id_emb(user_id).squeeze(1) # [batch_size, 16]gender_emb = self.gender_emb(gender).squeeze(1) # [batch_size, 8]history_emb = self.history_emb(history) # [batch_size, 3, 16]history_avg = torch.mean(history_emb, dim=1) # 平均池化 → [batch_size, 16]# 拼接用户特征(注意age要扩维:[batch_size] → [batch_size, 1])user_feat_concat = torch.cat([user_id_emb,age.unsqueeze(1),gender_emb,history_avg], dim=1) # [batch_size, 41]# DNN输出用户向量 + L2归一化user_vector = self.user_dnn(user_feat_concat) # [batch_size, 64]user_vector = torch.nn.functional.normalize(user_vector, p=2, dim=1) # L2归一化# ----------------------# 处理商品特征 → 生成商品向量# ----------------------# 拆解商品特征:item_id(0), category(1), price(2)item_id = item_feat[:, 0] # [batch_size]category = item_feat[:, 1] # [batch_size]price = item_feat[:, 2].float() / 1000 # 归一化到0-1 → [batch_size]# 嵌入层计算item_id_emb = self.item_id_emb(item_id).squeeze(1) # [batch_size, 32]category_emb = self.category_emb(category).squeeze(1) # [batch_size, 16]# 拼接商品特征(price扩维)item_feat_concat = torch.cat([item_id_emb,category_emb,price.unsqueeze(1)], dim=1) # [batch_size, 49]# DNN输出商品向量 + L2归一化item_vector = self.item_dnn(item_feat_concat) # [batch_size, 64]item_vector = torch.nn.functional.normalize(item_vector, p=2, dim=1) # L2归一化# ----------------------# 计算相似度 → 输出预测概率# ----------------------# 内积计算相似度similarity = torch.sum(user_vector * item_vector, dim=1, keepdim=True) # [batch_size, 1]# 温度系数调节similarity = similarity / self.temperature# 转为二分类概率pred = self.sigmoid(similarity) # [batch_size, 1]return pred, user_vector, item_vector # 返回预测值+用户向量+商品向量
FM(Factorization Machine)
FM因子分解机 , 为双塔模型的雏形
特征的交互通过特征对应的隐向量的内积来建模
FM应用于召回任务:区分出用户特征集、物品特征集,主要是将FM的二阶交互项按照用户和物品特征进行拆分
DSSM(Deep Structured Semantic Model)
本节讲述深度结构化语义模型的双塔实现
创新点:通过深度神经网络替代线性变换,实现了非线性表达能力
如图:两塔之间的交互仅在最终的内积计算时发生。

双塔模型的细节:
- 对用户塔和物品塔输出的embedding进行归一化:保持一致性
- 温度系数调节:在归一化后的向量计算内积后,除以温度系数τ:调节softmax分布的“锐利程度”,较小的τ使得模型预测更加“确定”。
YouTubeDNN
(将用户下一个可能观看的视频归类到具体的某一个视频类别中)
从匹配到预测用户下一行为
主要创新点:
- 非对称的时序分割:对于作为预测目标的用户观看记录,只使用该目标之前的历史行为作为输入特征。
- 负采样策略:为了高效处理数百万类别的softmax,模型采用重要性采样技术,每次只对数千个负样本进行计算
- 用户样本均衡:为每个用户生成固定数量的训练样本,避免高活跃用户主导模型学习。
