0. 序章
本书不会包含一些“经典但过时的算法”,例如:协同过滤、矩阵分解之类的经典推荐算法。
本书会聚焦一些日常实践问题,例如:
- 多任务推荐与多场景推荐怎么搞?
- 新用户与新物料的冷启问题
- 如何打开模型的黑盒,排查问题或找到下一步升级改进的方向?
- 算法工程师的噩梦“线下AUC涨了,线上AB指标却不涨!什么原因造成的?”冰山现象、数据穿越、还是老汤模型(是指基线模型训练太久了太强了)?
1. 推荐系统简介
推荐系统不只是推荐模型,后者只是前者的一个模块。推荐系统需要:
- 日志系统收集用户反馈,为推荐提供原始数据
- 大数据系统、流式计算系统,从原始数据中提取信息,将他们加工成模型需要的形式
- 在线学习系统及时更新模型
- 监控系统观察模型运行是否正常,并且能够自动报警
- A/B实验系统评估模型效果,并能够动态配置、改变推荐系统的运行方式
除算法工程师之外,需要前端、后端、产品、运营、用户增长等团队的通力合作。
推荐系统的作用:在信息过载的情况下,主动在人与信息之间建立高效的连接。
1.1 推荐系统架构
现代大型推荐系统常采用“召回(百万级)->粗排(1万)->精排(1千)->重排(1百)”四个环节构成。整体规律是:
- 推荐链路中越靠前,面对的候选集合越大,因此要采用技术较简单、精度略逊、速度较快的算法
- 推荐链路中越靠后,面对的候选集合越小,就有条件用技术较复杂、精度高、速度较慢的算法
简要描述一下这四个环节:
- 召回:候选集规模大,这一层只能使用极简模型/策略,通常会使用大量召回分路(每路只关注用户信息或物料信息的一个侧面),以数量弥补质量。另外,召回的数据分布上和精排有巨大差异,召回要过滤掉各种非常劣质的物料,而精排是在召回后的温室中优中选优,这就造成建模方法上有很大差异,绝非加速版本的精排。
- 粗排:小型推荐系统可能没有。召回和精排中间 tradeoff 产物,粗排面对的候选集比召回少的多,可以介入更多特征并使用更复杂的结构;但是,粗排又比精排简单很多,主流粗排模型仍然依赖“离线计算+在线缓存”模式来处理候选物料,仍然不能使用用户信息与物料信息交叉的特征和结构。
- 精排:通常是最关键的最卷的环节。这层候选数量已经足够小,可以使用更具创新的更复杂的模型;且在推荐链路中靠后,对业务目标影响更直接。精排的重点就是对物料信息和用户信息做充分交叉,涉及交叉特征&模型交叉结构。
- 重排:主要用于保障产品体验&长期价值,例如推荐结果呈现的多样性(去重、打散)。小型系统中通常是简单的几条规则,大型系统中也会使用到比较复杂的模型。
1.2 搜推广的区别和联系
本质来说,搜推广都是针对用户需求找到最匹配的信息。但是用户需求的表达方式,区分了搜索和推荐;信息服务的对象,区分了搜推和广告。共性上来说:
- 搜推广都遵循“先召回模块粗筛,再由排序模块精挑细选”的架构。
- 搜推广的要求高度的个性化,技术和论文大多通用。
搜索 vs.推荐:
- 显示意图 vs.隐示意图/偏好;搜索意图由用户 query 表达,当然 user 特征也会被考虑;推荐只能通过 user 特征来猜测。
- 搜索对准确性要求更高;过于发散&拓展的内容不适合展示为搜索结果(有产品体验的风险,看起来会像Bug)
搜推 vs.广告:
- 目标差异:搜推的目标是服务好用户,提升用户体验,来制造更多的流量;广告的目标是流量变现,需要兼顾用户、广告主、平台三方面的利益,复杂度更高。
- 排序建模差异:搜推排序主要是做 learn to rank,只要求顺序;但是广告需要预估准确的CTR/CVR指标,并会做修正校准,毕竟要考虑广告费用,考虑竞价投放的ROI
- 规模差异:大多数情况下,搜推的物料量级远大于广告,前者是原生场景内容,后者是引入广告主配置的内容
2. 特征工程
特征工程依然重要不过时,虽然DNN理论上能够拟合输入输出间的任何复杂函数关系,但是实践中还是非常困难,各种特征工程方法还是很难被直接替代。DCN(Deep Cross Network)的作者在论文里也提到DNN有时候连简单的二阶、三阶特征交叉都做不好。另外,一些“自动化特征工程”的建模方式性能代价很高,例如DIN 、SIM。
2.1 物料画像
- 物料自身属性;其入库时就能获取到的基础信息,例如:
- 视频推荐中:视频的作者、作者等级、作者粉丝数、投稿栏目、视频标题和简介、上传时间、时长、清晰度等
- 商品推荐中:商品标题与简介、封面图片、所属商铺、商铺等级、品牌、价格、折扣、物料方式、上架时间等
- 一个非常关键的特征是 ItemID:模型无需理解,只需记住,能够在物料侧提供最个性化的信息。这个粒度太细需要大量数据,但是大型推荐场景是可以支撑的
- 物料的类别与标签;不依赖用户反馈,仅通过分析物料内容就能获得的信息,例如:
- 使用自然语言处理算法(例如 BERT)分析物料的标题、摘要、评论,或是正文、视频字幕
- 使用计算机视觉算法(例如 CNN)分析物料的封面、视频的关键帧
类别上,例如一级分类“体育”、“电影”…,二级分类“足球”、“篮球”…
标签上,例如“NBA”、“乔丹”…,标签是灵活的,没有必要从属于某一类别
内容理解算法可以对类别、标签做一定置信度/偏好度的打分,例如对于某用户有 {“电影”:0.7, “NBA”:0.3}
- 基于内容的Embedding
同样还是基于例如 BERT、CNN 这样的模型,提取信息的时拿模型的某一层输出当做Embedding(例如 64维) - 物料动态统计;通过后验的用户反馈统计来刻画物料,是非常重要的特征。可以拆分两个维度做组合
- 时间粒度:全生命周期、过去一周、过去一天、过去一小时…
- 统计对象:CTR、平均播放进度、平均消费时长…
实际上这里面会引入偏差:过去的匹配上表现好,不代表人人表现都好;加剧马太效应,不利于新品冷启动。
- 用户给物料反向打标签
推荐系统中通常是将用户消费过的物料标签累计到用户身上,帮助形成用户画像(偏好特征,序列特征)。反向打标是上述逆转的流程。例如:- 通过动态反馈补位:例如某体育明星的娱乐新闻,可能被NLP算法打上“体育”标签,但实际反馈数据上带“娱乐”标签的用户更偏好,这个内容应该补充上“娱乐”标签。
- 实际上一些推荐理由中已经在用了:例如 “文青喜欢的电影榜第三名”、“数码迷最喜欢的手机”
2.2 用户画像
-
用户静态画像
- 人口属性:性别、年龄、职业、籍贯、安装APP列表… 以本书作者的经验来看,这些信息对推荐算法的作用不大,因为针对老用户,丰富的历史行为已经足够反映兴趣偏好所以静态标签不重要,而新用户本来就很难学好(模型被老用户主导,且新用户样本质量本身就不高)。
- UserID:与ItemID类似,这提供了用户侧最细粒度的个性化信息,主要针对高活用户(低活用户样本太少没意义)。
-
用户动态画像
通过用户历史行为提取出他的兴趣爱好;相对稳定的人口属性信息,这部分信息变化频繁,能够及时反映出用户的兴趣迁移。有两种方案:- 序列建模:端到端完成“提取用户兴趣”+“CTR建模”;最简单的case就是把用户一段时间内交互过的物料ItemID按时间顺序组成集合传入模型,让模型自动从中提取出用户兴趣;最最简单的方式就是对ItemID先做Embedding然后再做一次Pooling,更复杂一些就可以使用DIN、SIM引入Attention机制,做到“千物千面”。
- 偏好特征加工:独立做“提取用户兴趣”,大幅减少线上处理压力(当然,针对性减弱,做不到“千物千面”)依靠离线计算资源处理;基于统计指标设计,简单直白,易于理解,非常适合用于召回、粗排。常用的统计维度:
- 用户粒度:可以是单个粒度,也可以针对用户群体,有利于新用户冷启动
- 时间粒度:例如最近100次曝光,例如过去一月、一周、一小时
- 物料属性:例如视频的一二级类别、标签、作者,例如商品的分类、品牌、店铺、价位
- 动作类型:可以是正向的,例如点击、点赞、转发;可以是负向的,例如忽略、点踩
- 统计对象:例如次数、时长、金额
- 统计方法,例如加和、求平均、计算比例
2.3 特征交叉
用户和物料的特征交叉是前深度学习时代模型性能提升的重要手段。具体交叉方式上分为:
- 外积(笛卡尔积)交叉:两个特征 Field 内的 Feature 两两组合,组成一个新的Field(特征数量爆炸,N*M)。例如“用户电影兴趣Field {动作片、科幻片}”*“物料类型Field {终结者、机器人}”,交叉后的“科幻片*机器人”就是一个很强的特征。
- 内积(点积)交叉:选定一个维度(例如一级类目),将用户在这一维度上的特征&物料在这一维度上的特征都当做是稀疏向量,对这两个向量做内积,内积的大小反映了用户和物料在这一维度上的匹配程度。例如用户 {“坦克”:0.8, “台球”:-0.3} 内积 物料 {“坦克”:1.0, “二战”: 0.5}。
2.4 偏差特征
推荐模型依赖用户反馈的曝光点击数据训练,但是这些数据本身就有偏差(用户的选择/反馈并非完全处于其兴趣/偏好)。最为常见的是“位置偏差”(Position Bias),就是某些内容虽然曝光了,但是曝光的位置在用户视线之外,即使用户感兴趣也不会去点击,会造成伪负样本(False Negative Sample)。这块常见两种处理思路:
- 样本设计上纠偏:常见策略“Above Click”,就是负样本只使用用户浏览记录中,有点击的Session中,在点击位置之上的内容;保证负样本都是用户有注意到的。这个策略和搜索样本构造正负样本对时使用“Skip Click”类似。
- 模型引入偏差特征:例如针对 Position Bias,将曝光时的 Position 特征加入到模型中,让模型自己训练纠偏。有以下2个要点。
- Infer 时的处理:偏差特征属于 Dummy Feature,真正Infer的时没有这一特征,可以将所有的展示位置填成0,或者不输出。实际上对于 learn to rank 来说是需要比较大小,把这部分丢掉都可以。
- 偏差加入模型的位置:直接插入到模型最终logits,避免和其他正式采用的特征做交叉。
实践中,根据具体场景会有不一样的偏差特征,例如Youtube通过视频的“年龄”(上传越久的视频后验指标更好,模型排名更高)做偏差特征(减轻这一偏差);参见 【Deep Neural Networks for YouTube Recommendations 2016】
2.5 数值特征
2.5.1 数值特征处理
- 缺失值处理:常见做法是拿所有样本在该特征Field上的均值或中位数替代,也可以更精细一些选择该用户分层&物料分层的均值或中位数;更复杂的方案是专门训练模型来预测缺失值。当然,如果选择数值特征离散化,那缺失值就专门用一个VALUE来映射。
- 标准化:将不同取值范围的数值特征都压缩到一个数值范围内,方便模型学习收敛。常见 z-score 标准化(将数值分布压缩到均值=0、标准差=1的分布上);如果数值原始分布是长尾,那么进行开方、取对数的预处理再做标准化将更接近正态分布。
2.5.2 数据平滑
推荐系统中经常要计算各种比率作为特征,例如点击率、点赞率、购买率、复购率等。计算这些数值时总会有样本量太少导致计算结果不可信的问题。例如某一件商品曝光了1次,点击了1次,点击率就是100%了,显然不可信。为了客服小样本的影响,提高计算结果的置信水平,需要采用平滑方案。
常规置信区间 Confidence Interval 使用z检验,适用于大样本(至少 n n n>30),小样本上的区间估计不准确,z检验如下:
μ ^ ± z S E M , S E M = σ ^ n \hat{\mu} \pm zSEM,\ SEM=\frac{\hat{\sigma}}{\sqrt{n}} μ^±zSEM, SEM=nσ^
其中 SEM(Standard Error of Sample Mean)中使用了 Sample Deviation 近似总体标准差,在小样本条件下不准确。
搜推上常见的优化方案是 威尔逊区间平滑(Willson Score Interval),其基于 采样概率 p p p,在小样本 n n n 上预估其 总体概率 p ∗ p* p∗,Willson Interval 给出了极限(置信区间边界)下的总体均值的区间估计:
p ∗ = p + z 2 2 n ± z p ( 1 − p ) / n + z 2 / 4 n 2 1 + z 2 / n p^*=\frac{p+\frac{z^2}{2n} \pm z\sqrt{p(1-p)/n+z^2/4n^2}}{1+z^2/n} p∗=1+z2/np+2nz2±zp(1−p)/n+z2/4n2
这里同样包含Z值,纠正后:
- 均值估计为 p + z 2 2 n 1 + z 2 / n \frac{p+\frac{z^2}{2n}}{1+z^2/n} 1+z2/np+2nz2
- 区间估计为 z ∗ p ( 1 − p ) n + z 2 / 4 n 2 1 + z 2 / n z*\frac{\sqrt{p(1-p)n+z^2/4n^2}}{1+z^2/n} z∗1+z2/np(1−p)n+z2/4n2
当样本数量 n n n 增大&趋向无穷时,Willson Interval 趋向常规置信区间 Confidence Interval μ ^ ± z S E M , S E M = σ ^ n \hat{\mu} \pm zSEM, SEM=\frac{\hat{\sigma}}{\sqrt{n}} μ^±zSEM,SEM=nσ^
2.5.3 数据消偏
主要是针对用户反馈数据。例如上述“2.4”提及的 位置偏差 Position Bias,直接使用CTR统计特征的话,某些物料可能因为集中在局部位置上曝光,造成CTR统计偏差较大。这里可以通过数据消偏的方案进行优化。例如 “CoEC”(Click Over Expected Click)代替CTR衡量物料的受欢迎程度。计算公式如下:
C o E C = ∑ i = 1 N c i ∑ i = 1 N e c p i CoEC=\frac{\sum_{i=1}^{N}c_i}{\sum_{i=1}^{N}ec_{pi}} CoEC=∑i=1Necpi∑i=1Nci
- i表示第i个曝光样本,N是样本总数
- c i c_i ci(取值0/1)表示是否发生点击
-
p
i
pi
pi 是第i次曝光的位置,
e
c
p
i
ec_{pi}
ecpi 是该位置的平均CTR(个人认为最好通过随机样本测算)
C o E C CoEC CoEC 消除了展位带来的偏差,能更公平地反馈各物料受欢迎的程度
2.5.4 分桶离散化
推荐模型中,类别特征能更好地反映非线性关系、便于存储和计算。通常会把数值特征离散化分桶为类别特征。分桶的实现方式有三种:
- 等宽分桶:考虑特征值本身;将特征值域平均划分为 N 等份,每份算一个桶
- 等频分桶:考虑特征值在样本上的分布;将整个值域的 N 个分位数(Percentile)作为各桶的边界,保证落入各个分桶的样本数量大致相等
- 模型分桶:考虑特征值对样本label的区分度;例如拿特征值针对label拟合一棵单特征的决策树,然后根据特征值命中的叶子节点分桶(也可以尝试其他的建模处理方式)
2.6 类别特征
推荐模型主要使用高维&稀疏的类别特征,主要考虑到:
- 很多关键画像特征本身高维&稀疏:用户画像、物料画像中的一二级分类、标签,几万量级的标签很正常,而某个用户or某个物料只命中其中几个标签
- 推荐模型中输入特征和拟合目标间更多是非线性的关系:例如“用户年龄”和购买兴趣的关系,主要是不同年龄段(“少年”、“青年”、“中年”…)有不一样的购物需求,不是简单线性关系(bad case:“青年”习惯购买XXX,那么“中年”的喜欢程度翻倍)
- 系统性能上高维&稀疏更优:通过非零存储&排零计算;某个用户or某个物料具体只会命中少量标签(尽量特征总量很大,单点上非零特征不多),是一个查表的逻辑。
- 稀疏特征训练优化:
- 优化算法Optimization上,例如FTRL能为每维特征自适应地调节学习率,提升训练效率
- 特征交叉上,例如FM能通过因式分解(就是隐向量学习)提升训练效率
大量的类别特征在工程链路实际应用上,有2种方式:
- 建立映射表:所有的类别特征建立一个 index 的MAP映射表,模型中Embedding根据这个映射表大小设置,特征通过映射后的index直接取出相关Embedding向量
- 特征哈希(规避映射表的维护问题):所有的类别特征直接通过哈希映射成一个[0, N)的整数(越界了取余数),假设真正重要的特征不多,真正重要的特征发生哈希冲突(Hash Collision)的概率很小。
3. 推荐系统中的 Embedding
Embedding 的定义是无中生有的,我们假设已经有了这样的表征(实际上是随机初始化),再去通过优化算法训练。
当下推荐模型大量使用 高维稀疏类别特征 to Embedding,大部分的模型参数都是 Embedding 参数。
在推荐模型建模的 “Memory” & “Generalize” 两类问题中,Embedding 用于自动化地解决 “Generalize”。
假设模型能根据高维稀疏类别特征的输入,学好用户表征Embedding、物料表征Embedding,这里的 Embedding 是模型自行发挥的黑盒。
这其实类似NLP中词向量学习,本质上是让模型从“精确匹配”自行延展到“模糊匹配”。
3.1 共享还是独占Embedding
共享表示在模型的多个位置上使用同样的Embedding表征。
3.1.1 共享
优势:
- 能够缓解特征稀疏、数据不足所导致的训练不充分
- 节省Embedding矩阵存储空间(也是节省模型空间)
相关case:
- 用户行为序列建模:用户特征的历史ItemID行为序列 和 Item特征的ItemID 可以共用一个 ItemID-Embedding
- FM(Factorization Machine)建模:每个特征只有唯一一个Embedding与其他特征做交叉
3.1.2 独占
优势:
- 避免目标不同时相互干扰
相关case:
- 用户不同含义的反馈:点击历史/购买历史/收藏历史,APP安装/启动/卸载;这2个场景中的物料表义不同,避免相互干扰考虑使用多套ItemID-Embedding
- FFM(Fieldaware Factorization Machine):相对FM,每个特征对不同Field交叉时使用不同的Embedding,大幅增加了参数量(nk->fnk, f个field、n个特征、k的嵌入长度)
- CAN(Co-Action Network, 2021):将Item Embedding折叠为若干层MLP来和User Embedding交叉,达成同一物料对不同用户的表征明显差异(毕竟过多层MLP,包含非线性)
3.2 Parameter Server
PS(Parameter Server) 分布式模型训练架构就是用于解决推荐模型高维稀疏特征的问题。
传统的分布式计算 MapReduce 架构不足以解决分布式模型训练,训练数据分散到所有Slave节点是OK的,但是Master节点将模型最新参数广播到所有Slave节点上是不现实的(模型特征可上亿,单节点都吃不下,更不用说来回传递的带宽消耗)。
所以提出PS架构,由3部分构成:
- Worker(N个):从Server Pull最新模型参数(只取当前训练batch需要的参数),用Worker本地数据训练并计算梯度,向Server Push梯度
- Server(M个,其实就是Key/Value数据库):共同存储上亿参数(单节点只负责其中一部分,又称为Shard),应对Pull请求发送相关参数数值,应对Push请求聚合各Worker梯度并利用SGD算法(例如Adam、Adagrad)更新模型参数。
- Scheduler:负责整个PS集群管理,负责Pull请求路由到对应的Server节点
其是两种分布式范式的集合:
- Data Parallelism:海量的训练数据分散在各个节点上
- Model Parallelism:特征动辄上亿,必然分布在一个集群中;而又由于特征高度稀疏,不同Worker节点不太会在同一个特征上竞争,多个Worker相对解耦
真正应对Worker冲突,大致有以下策略:
- 并发同步 BSP(Bulk Synchronous Parallel):每一轮次等待所有Worker完成后等待,Server汇总梯度更新模型参数,再开始下一轮。一个慢节点就能拖垮整个集群。
- 异步并发 ASP(Aynchronous Parallel):每个Worker独立持续工作,不会互相等待,有明显速度优势,但是会有“梯度失效”问题(慢节点上报的梯度是针对很早之前的参数值的)
- 半同步半异步 SSP(Staleness Synchronous Parallel):是BSP和ASP的折衷方案,允许各Worker节点在一定迭代论述之内保持异步,差距过大则阻塞等待更新,是一种 tradeoff。
现代的PS架构会考虑:
- Embedding参数使用ASP策略更新(本身就是高维稀疏,冲突概率低)
- 同时DNN各层权重使用BSP策略更新,避免“梯度失效”问题
- 并且也都会避免存储膨胀而引入特征准入&逐出机制
4. 精排
本书作者将推荐算法模型梳理到 5个 维度/方向:
- a、记忆与拓展(Memory & Generalize):推荐模型既要记住高频、常见的连接,又要能泛化/扩展低频、小众、个性化的连接。传统机器学习的LR重在记忆(也有泛化能力,但是完全依赖特征工程),Embedding(“精确匹配”改为“模糊匹配”)增加了模型泛化扩展能力;经典的 Wide & Deep 就是记忆和拓展的设计体现。
- b、Embedding:将推荐算法由“根据概念的精确匹配”(生搬硬套)升级为“基于向量的模糊查找”(举一反三),极大增量而来模型的扩展能力,是推荐系统中所有深度学习模型的基础。
- c、高维稀疏的类别特征:推荐算法模型高度依赖这类特征,并衍生出很多相关技术,如下
- 为了加速高维稀疏特征的训练:基于 Parameter Server 的分布式架构
- 为了解决稀疏特征受训练机会不均衡的问题,很多优化算法为每个特征采用不同的学习率和正则系数
- 为了缓解单个类别特征表达能力弱的问题,采用Embedding扩展内涵,并通过各种交叉结果进一步扩展外延
- d、交叉结构:除了MLP这种隐式交叉方案,还有很多一阶、二阶、多阶等显式交叉方案;这块自动化交叉是之前推荐模型研究的热点。本章重点介绍。
- e、用户行为序列建模:用户在APP中的各种行为(浏览、点击、点赞、评论、购买…)组成的序列蕴含着用户最真实的兴趣(毕竟推荐的就是这些内容),是重要的特征数据。挑战是行为序列数据量大,且单个行为包含信息有限且随机性大,将这些行为序列压缩成固定长度的用户兴趣向量绝非易事。本章重点介绍。
4.1 交叉结构
4.1.1 超大规模LR(Logistic Regression)
在前深度学习时代,精排主要依赖LR,对于高维稀疏特征下的模型打分即:
C T R p r e d i c t = s i g m o i d ( ∑ j ∈ I w j ) , I = i ∣ x i = 1 CTR_{predict}=sigmoid(\sum_{j \in I}w_j), I={i|x_i=1} CTRpredict=sigmoid(j∈I∑wj),I=i∣xi=1
就是查找到所有非零特征的权重做加和。实际上就是一个评分卡,对每个特征都有一个评分值,对所有命中评分值做加和即可。
这种模式下模型表现对特征工程依赖极大,需要有大量的特征交叉设计,实际上输入的特征规模是海量的,十亿级乃至百亿级。这种极限的高维稀疏催生出了对优化器的两个要求:
- 实际生效的特征/权重尽量稀疏:不重要的特征权重置零,减少线上存储&查询压力
- 学习率区分:对于受训机会多的特征权重降低学习率,对于受训机会少的特征权重提升学习率;常用优化器 FTRL(Follow The Regularized Leader)兼顾了预测精度和解的稀疏性,并且对不同特征学习率也有依据出现频次的调节。
4.1.2 FM(Factorization Machine)半步跨入DNN
在LR的基础上,通过加入Embedding来做自动化的二阶特征交叉,具体公式为:
l
o
g
i
t
F
M
=
b
+
∑
i
=
1
n
w
i
x
i
+
∑
i
=
1
n
∑
j
=
i
+
1
n
(
v
i
v
j
)
x
i
x
j
logit_{FM}=b+\sum_{i=1}^nw_ix_i+\sum_{i=1}^n\sum_{j=i+1}^n(v_iv_j)x_ix_j
logitFM=b+i=1∑nwixi+i=1∑nj=i+1∑n(vivj)xixj
上式中的
v
v
v 就是新引入的Embedding,假设维度为k,即以nk替代了全量笛卡尔积的n^2,大大减少了交叉所需的参数量,并且给予稀疏的二阶交叉更多的训练可能,更强的泛化能力。
实际应用时,会简化相关计算,使用:
l o g i t F M = b + ∑ i ∈ I w i + 1 2 r e d u c e s u m [ ( ∑ i ∈ I v i ) 2 − ∑ i ∈ I v i 2 ] logit_{FM}=b+\sum_{i\in I}w_i+\frac{1}{2}reducesum[(\sum_{i\in I}v_i)^2-\sum_{i\in I}v_i^2] logitFM=b+i∈I∑wi+21reducesum[(i∈I∑vi)2−i∈I∑vi2]
FM 模型有进一步的改进版本 FFM(Field-aware Factorization Machine),将特征区分为不同field,某个field和其他不同field之间做交叉时会使用不同 Embedding;当有 k k k 个field时相当于把模型空间复杂度&时间复杂度扩大 k k k 倍。这一方法应用没有FM广泛,但是field-aware的思想后续被深度学习中特征交叉广泛借鉴。
FM 详细推导过程如下(对原书稍微做一点补充,后续有很多模型基于 FM 实现)
假设 Embedding 表征为
v
i
,
f
v_{i,f}
vi,f,其中
i
∈
I
i \in I
i∈I 表示命中的具体特征;
k
∈
K
k \in K
k∈K 表示 Embeeding 维度;则 FM 基础公式可以演化成:
l
o
g
i
t
F
M
−
p
a
r
t
=
∑
i
=
1
I
∑
j
=
i
+
1
I
v
i
v
j
=
1
2
∑
i
I
∑
j
I
v
i
v
j
−
1
2
∑
i
I
v
i
v
i
\begin{aligned} logit_{FM-part} &= \sum_{i=1}^I\sum_{j=i+1}^I v_iv_j \\ &= \frac{1}{2}\sum_{i}^I\sum_{j}^I v_iv_j - \frac{1}{2}\sum_{i}^Iv_iv_i \\ \end{aligned}
logitFM−part=i=1∑Ij=i+1∑Ivivj=21i∑Ij∑Ivivj−21i∑Ivivi
以上这步是做一个简化,二阶表征做全量的交叉再消去多余项,这样方便计算;然后将 Embeding 中 bit 粒度展开:
l
o
g
i
t
F
M
−
p
a
r
t
=
1
2
∑
i
I
∑
j
I
∑
k
K
v
i
,
k
v
j
,
k
−
1
2
∑
i
I
∑
k
K
v
i
,
k
v
i
,
k
=
1
2
∑
k
K
[
∑
i
I
∑
j
I
v
i
,
k
v
j
,
k
−
∑
i
I
v
i
,
k
v
i
,
k
]
\begin{aligned} logit_{FM-part} &= \frac{1}{2}\sum_{i}^I\sum_{j}^I\sum_{k}^K v_{i,k}v_{j,k} - \frac{1}{2}\sum_{i}^I\sum_{k}^K v_{i,k}v_{i,k} \\ &= \frac{1}{2}\sum_{k}^K[\sum_{i}^I\sum_{j}^I v_{i,k}v_{j,k} -\sum_{i}^Iv_{i,k}v_{i,k}] \\ \end{aligned}
logitFM−part=21i∑Ij∑Ik∑Kvi,kvj,k−21i∑Ik∑Kvi,kvi,k=21k∑K[i∑Ij∑Ivi,kvj,k−i∑Ivi,kvi,k]
以上考虑对
i
,
j
,
k
i,j,k
i,j,k 3个维度上数据求和,实际上
f
f
f 这个 Embedding 维度是独立的,可以先计算好每个
f
f
f 的结果再求和。类似地还能进一步优化:
l
o
g
i
t
F
M
−
p
a
r
t
=
1
2
∑
k
K
[
∑
i
I
v
i
,
k
(
∑
j
I
v
j
,
k
)
−
∑
i
I
v
i
,
k
2
]
=
1
2
∑
k
K
[
(
∑
i
I
v
i
,
k
)
(
∑
j
I
v
j
,
k
)
−
∑
i
I
v
i
,
k
2
]
=
1
2
∑
k
K
[
(
∑
i
I
v
i
,
k
)
2
−
∑
i
I
v
i
,
k
2
]
\begin{aligned} logit_{FM-part} &= \frac{1}{2}\sum_{k}^K[\sum_{i}^I v_{i,k}(\sum_{j}^Iv_{j,k}) -\sum_{i}^Iv_{i,k}^2] \\ &= \frac{1}{2}\sum_{k}^K[(\sum_{i}^I v_{i,k})(\sum_{j}^Iv_{j,k}) -\sum_{i}^Iv_{i,k}^2] \\ &= \frac{1}{2}\sum_{k}^K[(\sum_{i}^I v_{i,k})^2 -\sum_{i}^Iv_{i,k}^2] \\ \end{aligned}
logitFM−part=21k∑K[i∑Ivi,k(j∑Ivj,k)−i∑Ivi,k2]=21k∑K[(i∑Ivi,k)(j∑Ivj,k)−i∑Ivi,k2]=21k∑K[(i∑Ivi,k)2−i∑Ivi,k2]
以上对求和公式进一步提取公因式,化简成先求和再相乘;最后由于
v
i
v_i
vi 和
v
j
v_j
vj 背后是同一个 Embedding 矩阵,把相乘化简成平方。 实际上最后这个 二次多项式 展开就是所有项二次交叉相乘之和,这里只关心加和的结果所以可以这么优化(深度学习的 PNN 需要关心每一项交叉结果就没法这么优化了)。
4.1.3 Wide & Deep(Google 2016)
记忆&拓展 的经典应用。后续的DeepFM、DCN上都能看到 Wide & Deep 的影子。
- Deep 侧是一个标准的DNN(Embedding+MLP),具体公式为
l
o
g
i
t
d
n
n
=
D
N
N
(
C
o
n
c
a
t
(
E
m
b
e
d
d
i
n
g
(
x
d
e
e
p
)
)
)
logit_{dnn}=DNN(Concat(Embedding(x_{deep})))
logitdnn=DNN(Concat(Embedding(xdeep)))
- x d e e p x_{deep} xdeep 是深度网络的原始输入特征,被拆分为多个Field,每个Field对应一个Embedding
- E m b e d d i n g Embedding Embedding 每个独立的Embedding将相关Field内部的一个特征或多个特征(都是类别特征)映射城一个固定长度的稠密浮点数向量;命中多个特征的情况下要做Pooling(常规是 average pooling)
- C o n c a t Concat Concat 将多个Embedding向量拼接成一个大向量
- D N N DNN DNN 在上述大向量之上做多层 MLP,对特征做高阶隐式交叉
- Wide 侧就是一个LR,即 l o g i t w i d e = w w i d e ⋅ x w i d e logit_{wide}=w_{wide}\cdot x_{wide} logitwide=wwide⋅xwide
- Wide & Deep 共同训练, C T R p r e d i c t = s i g m o i d ( l o g i t w i d e + l o g i t d e e p ) CTR_{predict}=sigmoid(logit_{wide}+logit_{deep}) CTRpredict=sigmoid(logitwide+logitdeep),优化器上一般Wide使用FTRL(保障稀疏性),Deep采用DNN常规的Adagrad、Adam
4.1.4 DeepFM(Huawei 2017)
和 Wide & Deep 非常相似,只是在 l o g i t w i d e logit_{wide} logitwide 和 l o g t d e e p logt_{deep} logtdeep 的基础上多增加了一项 l o g i t f m = F M ( x f m , E m b e d d i n g ( x f m ) ) logit_{fm}=FM(x_{fm},Embedding(x_{fm})) logitfm=FM(xfm,Embedding(xfm))
- 这里的 x f m x_{fm} xfm 一般情况下和 x d n n x_{dnn} xdnn 一致
- 这里的 E m b e d d i n g Embedding Embedding 上 FM 和 DNN 共用(原论文中反复强调了这一点,这个 Embedding 兼顾了 low-order & high_order 2个交叉方向)
- FM(就是传统的FM)将二阶交叉结果(就是命中的Embedding做外积,并剔除对角线上的自交叉)直接输出来logit层
有很多相似的特征交叉的工作如下(对原书的额外补充)
- FNN(Factorization-machine supported neural networks,2016):大致是 Wide & Deep 只使用 Deep 部分,相关 Embedding 参数用 FM 做预训练初始化
- PNN(Product Neural Network,2016):Embedding 的 field 之间通过 inner product(FM也是 inner product)或者 outer product,交叉结果输入DNN(DeepFM是交叉结果直接合并到logits了);因为交叉结果需要展开接入DNN,所以性能消耗较大
- NFM(Neural Factorization Machine,2017):Embedding 的 field 之间做 Hadamard product,假设有 N N N 个 field & embedding size K K K,则乘积结果是 R N × N × K R^{N\times N\times K} RN×N×K,然后通过sum pooling直接压缩到 R K R^K RK 维表征后接入 DNN
- AFM(Attention Factorization Machine,2017):类似 NFM,将交叉项等权重的sum pooling改为基于attention权重的weighted sum pooling,attention权重( a i j a_{ij} aij)的计算额外引入一个非线性隐层( W b W\ b W b)和一个相关的输出层( h h h),计算过程即 a i j ′ = h T R e l u ( W ( v i ⨀ v j ) + b ) , a i j = e x p ( a i j ′ ) ∑ ( i , j ) e x p ( a i j ′ ) a_{ij}^{'}=h^TRelu(W(v_i \bigodot v_j)+b) \ ,\ a_{ij}=\frac{exp(a_{ij}^{'})}{\sum_{(i,j)exp(a_{ij}^{'})}} aij′=hTRelu(W(vi⨀vj)+b) , aij=∑(i,j)exp(aij′)exp(aij′)
- CIN(Compressed Interaction Network,2018)即 xDeepFM (eXtreme DeepFM,微软):类似 NFM,将交叉项等权重的sum pooling改为使用卷积合并(合并权重即一维的卷积核,全部需要独立学习),通过 M M M 个卷积核分别合并后压缩到 M × K M\times K M×K。并且,可以持续对压缩后表征再交叉上原始表征(这一操作类似 DCN)实现高阶交叉(所以叫 eXtreme);最终,各阶交叉结果都sum pooling进一步压缩到各自卷积核 M o r d e r − i M^{order-i} Morder−i 粒度上合并(例如做了三阶交叉,分别是10个、5个卷积,那么最终交叉合并结果是 10+5=15 维)喂入下游DNN
4.1.5 DCN(Deep & Cross Network,Google)
具体有两个版本。
DCN V1(2017)
其 Cross层 公式如下:
x l + 1 = x 0 x l T w l + b l + x l x_{l+1}=x_0x_l^Tw_l+b_l+x_l xl+1=x0xlTwl+bl+xl
- x 0 x_0 x0 为最基础的Cross输入,即各种类别特征的Embedding(fields都concat到一起了)和其他Dense特征拼接而成的基础输入,是一个长度为d的稠密的浮点数向量。
- x l , x l + 1 x_l, x_{l+1} xl,xl+1 分别是当前地l层的Cross Layer输入和输出,都是长度为d的向量
- w l , b l w_l, b_l wl,bl 是需要学习的Cross Layer的参数,都是长度为 d d d 的向量
实际上就是每个Cross层都对 上一层输出 和 原始层 做笛卡尔积交叉,然后交叉结果用 w l w_l wl 重新融合压缩到长度d(控制交叉后的维度爆炸)并附加到上一层输出上构成当前层输出。有L层理论上就能完成L+1 v 阶交叉。
DCN V2(2021)
主要是考虑V1版本的参数容量太小,只用了2个长度为 d d d 的向量;所以将向量 w w w 改进成矩阵 W d × d W^{d\times d} Wd×d,Cross层公式调整如下:
x l + 1 = x 0 ⨀ ( W l x l + b l ) + x l x_{l+1}=x_0\bigodot(W_lx_l+b_l)+x_l xl+1=x0⨀(Wlxl+bl)+xl
- 通过矩阵 W l W_l Wl 提取出各个位置上要交叉的结果,然后用直接按位点乘 ⨀ \bigodot ⨀ 原始输入 x 0 x_0 x0
- 考虑到 W l W_l Wl 的参数量比较大(例如1000^2),所以也可以将其分解为两个小矩阵 W l = U l V l T W_l=U_lV_l^T Wl=UlVlT
DCN 和 DNN 的融合
有两种方式:
- 串联(Stacked): C T R s t a c k e d = s i g m o i d ( D N N ( C r o s s N e t w o r k ( x 0 ) ) ) CTR_{stacked}=sigmoid(DNN(CrossNetwork(x_0))) CTRstacked=sigmoid(DNN(CrossNetwork(x0))),所有特征先经过Cross层充分交叉后再传入DNN进行隐式交叉
- 并联(Parallel): C T R p a r a l l e l = s i g m o i d ( C r o s s N e t w o r k ( x 0 ) + D N N ( x 0 ) ) CTR_{parallel}=sigmoid(CrossNetwork(x_0)+DNN(x_0)) CTRparallel=sigmoid(CrossNetwork(x0)+DNN(x0)),原始特征分开两路并行,经过Cross层和DNN层之后直接加和到logits
4.1.6 AutoInt (Automatic Feature Interaction,2019)
使用NLP中的Transformer来做推荐模型中的特征交叉。其核心是 SelfAttention 即:
A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T d k ) V Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V Attention(Q,K,V)=softmax(dkQKT)V
通过将原始输入多个 Embedding 各自映射到 QKV 中来充分交叉,并会叠加上 Multi-Heads 来增强表征。这块的具体代码实现中要注意Mask,padding相关的占位符要干掉(softmax之前的logits层中,相关位置减去一个极大值)
具体实践上,AutoInt 将原始输入的M个Embedding做N层Transformer交叉加工,然后将这M个充分交叉后向量Concat到一起传入DNN。相关的缺点是:
- 为了使用SelfAttention,要求各Field Embedding长度相等
- 有一定性能开销,M个Field做Transformer的时间复杂度是O(M^2)
大多实践中会把AutoInt作为一个特征交叉模块(嵌入到更大的推荐模型中),只选择一部分重要特征进入,减少计算量。
4.1.7 FiBiNet(Feature Importance and Biliner feature Interaction,微博 2019,对原书额外补充)
从 Bilinear-FFM 到 FiBiNet,然后持续探索Embedding特征门控,到 ContextNet、MaskNet,都是微博算法团队的持续产出。其中 MaskNet 被 Twitter 用于核心场景排序(“For You” 频道下精排 “heavy rank” 模块)并开源。
Feature Importance - SENET
原始的 Embedding 表征会保持,同时会并行增加一份动态筛选过的 Embeding 表征(field数量 和 Emb size 不变,只调整 Emb 数值,相当于动态地弹性地屏蔽掉一些 field)。
使用 SENET(Squeeze-Excitation network),这一方法后续也被应用到召回、粗排双塔中剔除冗余特征,提升双塔顶层表征质量。具体实现步骤如下:
- Squeeze:假设 Embeding 有 M M M 个field,各自长度都是 K K K,即 E = [ e 1 , e 2 , . . . , e M ] , ∈ R M × K E=[e_1,e_2,...,e_M], \in R^{M\times K} E=[e1,e2,...,eM],∈RM×K;各个 field 内做avg pooling(其他方式也行)压缩成向量 Z = [ z 1 , z 2 , . . . , z M ] , ∈ R M × 1 Z=[z_1,z_2,...,z_M], \in R^{M\times 1} Z=[z1,z2,...,zM],∈RM×1
- Excitation:对上述Emb压缩结果叠加两层NN做变换 A = σ 2 ( W 2 σ 1 ( W 1 Z ) ) , ∈ R M × 1 A=\sigma_2(W_2\sigma_1(W_1Z)), \in R^{M\times 1} A=σ2(W2σ1(W1Z)),∈RM×1,这里 σ \sigma σ 是激活函数,先经过 W 1 ∈ R M × M r , r > 1 W_1 \in R^{M\times \frac{M}{r}}, r>1 W1∈RM×rM,r>1 将向量 Z Z Z 压缩到 R M r R^{\frac{M}{r}} RrM,再经过 W 2 ∈ R M r × M W_2 \in R^{\frac{M}{r}\times M} W2∈RrM×M 恢复 R M R^M RM
- Re-Weight:最后 Embedding 中 M M M 个 field 按照上述 A ∈ R M × 1 A \in R^{M\times 1} A∈RM×1 加权(相当于权重低的field就被抑制了)
Bilinear feature Interaction
实际上这块之前就有 Bilinear-FFM 的论文,相当于对 Hadamard Product 哈达玛积 做了进一步复杂化(多叠加一层线性变化,这个变化的变量可训练)。
同样假设 Embeding 有
M
M
M 个field,各自长度都是
K
K
K,即
E
=
[
e
1
,
e
2
,
.
.
.
,
e
M
]
,
∈
R
M
×
K
E=[e_1,e_2,...,e_M], \in R^{M\times K}
E=[e1,e2,...,eM],∈RM×K,则最终交叉结果是
P
∈
R
M
×
(
M
−
1
)
2
×
K
P \in R^{\frac{M\times (M-1)}{2}\times K}
P∈R2M×(M−1)×K,总计有
M
×
(
M
−
1
)
2
\frac{M\times (M-1)}{2}
2M×(M−1) 交叉,每个交叉用
R
K
R^K
RK 的向量表征。
对于任一个交叉项计算公式如下:
p
i
j
=
v
i
W
i
j
⨀
v
j
,
W
i
j
∈
R
K
×
K
,
p
i
j
∈
R
K
,
v
i
∈
R
K
,
v
j
∈
R
K
p_{ij}=v_iW_{ij}\bigodot v_j,\ W_{ij}\in R^{K\times K},\ p_{ij}\in R^K,\ v_{i}\in R^K,\ v_{j}\in R^K
pij=viWij⨀vj, Wij∈RK×K, pij∈RK, vi∈RK, vj∈RK
即两个向量在 Hadamard Product 之外还要做一次
W
i
j
W_{ij}
Wij 的线性变化,
W
i
j
W_{ij}
Wij 有3种实现:
- Field-All Type:全局公用1个 W W W,全局 W W W 参数量为 K × K K\times K K×K
- Field-Each Type:每个field独立1个 W W W,全局 W W W 参数量为 M × K × K M\times K\times K M×K×K
- Field-Interaction Type(这个效果最好):每组field交叉独立1个 W W W,全局 W W W 参数量为 M × ( M − 1 ) 2 × K × K \frac{M\times (M-1)}{2}\times K\times K 2M×(M−1)×K×K
完成交叉后整个
P
∈
R
M
×
(
M
−
1
)
2
×
K
P \in R^{\frac{M\times (M-1)}{2}\times K}
P∈R2M×(M−1)×K flatten 后直接接入 DNN,这块参数量很大。
FiBiNet 对原始 Embedding 和 SENET 处理后的 Embedding 各自分别做 Bilinear feature Interaction,再合并接入DNN,DNN输出层参数量级为
M
×
(
M
−
1
)
×
K
M\times (M-1)\times K
M×(M−1)×K,比较大。
FiBiNet+
主要是做了性能优化:
- 干掉了 SENET 之后的 Bi-Linear(作者发现用处不大),SENET细节上有调整
- Bi-Linear 中哈达玛积操作改为内积,大幅缩减输出尺寸( R M × ( M − 1 ) 2 × K R^{\frac{M\times (M-1)}{2}\times K} R2M×(M−1)×K to R M × ( M − 1 ) 2 R^{\frac{M\times (M-1)}{2}} R2M×(M−1)),减小DNN处理压力
- 每个field输出时独立做一个LayerNorm
另外,作者有一些分享:Embedding size 比较大(>30)的时候 FiBiNet 优势会更加明显。
ContextNet(2021)
出发点基于 FiBiNet 中特征门控 SENet 生效后,对特征门控的进一步迭代;目标是设计推荐领域的Transformer(作者直接用Transformer效果并不好)。
将原始 Embedding 中每个 field 看作独立单词,持续根据上下文(即其他 Embedding)的信息改进自身的表征,这个过程使用多轮门控实现(每个field每轮门控都要感知整体 Embedding,实际上 SENet 就是感知整体 Embedding)。经过串联的多轮加工后合并所有field表征并直接输出结果(作者表示这里不做直接输出,而是再堆叠几层MLP的话效果会更好,但是由于模型审美原因没有那么做)。最终效果略弱于 MaskNet,由于关键设计一致,这里不再展开。
门控网络设计有以下 3*3=9种 可能的变种:
- 输入层:
- 每个field做一个bit(即 SENet)
- 每个field分成g个组,每组取k bit(即 FiBiNet+ 中的 SENet+)
- 全量Embedding输入(即 ContextNet / MaskNet)
- 输出层:
- 每个field对应一个权重 a i a_i ai(即 SENet)
- 每个field具体分组对应一个权重 a i a_i ai(即 FiBiNet+ 中的 SENet+)
- 全量Embedding中每个bit对应一个权重(即 ContextNet / MaskNet)
MaskNet(2021)
实际上是 ContextNet 同期迭代产出的模型。核心门控模块 MaskBlock 和 ContextNet 基本一致,整体结构设计上有两种:
- Serial MaskNet:串行结构,原始 Embedding 接入 “MaskBlock on Feature Embedding”,然后逐层输出叠加 “MaskBlock on MaskBlock”,最终直接输出结果(有点DCN的影子)
- Parallel MaskNet(Twitter使用了并行):并行结构,对原始 Embedding 并行接入多个 “MaskBlock on Feature Embedding”,多个输出合并输入到多层MLP再输出结果(有点MMOE的影子,假设不同的并行模块能学习到不同的特征交叉)
MaskBlock on Feature Embedding 模块处理流程
V
o
u
t
p
u
t
=
R
e
L
U
(
L
N
(
W
i
(
V
m
a
s
k
⨀
L
N
_
E
M
B
(
V
e
m
b
)
)
)
)
V_{output}=ReLU(LN(W_i(V_{mask}\bigodot LN\_EMB(V_{emb}))))
Voutput=ReLU(LN(Wi(Vmask⨀LN_EMB(Vemb))))
假设原始 Embedding V e m b V_{emb} Vemb 有 M M M 个field,长度均为 K K K, N = M ∗ K N=M*K N=M∗K;
- 输入:
- L N _ E M B ∈ R N LN\_EMB \in R^{N} LN_EMB∈RN:对原始 V e m b V_{emb} Vemb 特征分field分别做 Layer Normalization 再拼接到一起(尺寸不变)
- V m a s k ∈ R N V_{mask} \in R^{N} Vmask∈RN:有 “Instance-Guided Mask” 模块产出,其将原始 Embedding 特征整体输入处理后生成 Embedding 上 bit-wise 的最细粒度门控 Mask(尺寸不变)
- 门控筛选特征( V m a s k ⨀ L N _ E M B ∈ R N V_{mask}\bigodot LN\_EMB \in R^{N} Vmask⨀LN_EMB∈RN):将上述 “LN-EMB” 和 “Instance-Guided Mask” 处理结果做 Hadamard Product 哈达玛积(尺寸不变)
- 加工( W i ∈ R Q × N W_i \in R^{Q\times N} Wi∈RQ×N ):经过一个 Hidden Layer,这个隐层没有 bias,因为反正后面要过 LN,LN内部自带bias
- 输出(原文称为 LN-HID ∈ R Q \in R^{Q} ∈RQ):对隐层输出整体做 LN(Layer Normalization),过 ReLU 输出结果(作者有对比过,“先LN再激活” 效果优于 “先激活再LN”)
备注:如果是 串联模式下的 MaskBlock on MaskBlock,只需要将上述的 L N _ E M B ∈ R N LN\_EMB \in R^{N} LN_EMB∈RN 替换成上一层 MaskBlock 输出即可。
IGM(Instance-Guided Mask) 模块处理流程
V
m
a
s
k
=
W
2
(
R
e
L
U
(
W
1
V
e
m
b
+
b
1
)
)
+
b
2
V_{mask} = W_2(ReLU(W_1 V_{emb}+b_1))+b_2
Vmask=W2(ReLU(W1Vemb+b1))+b2
- 输入: V e m b ∈ R N V_{emb}\in R^N Vemb∈RN 原始 Embedding 全量内容
- 加工(原文称 Aggregation Layer): W 1 ∈ R T × N W_1 \in R^{T\times N} W1∈RT×N 通过一个全连接层聚合输入信息
- 输出(原文称 Projection Layer):
W
2
∈
R
Z
×
T
W_2 \in R^{Z\times T}
W2∈RZ×T 通过一个全连接层映射输出只能尺寸的结果
- 这里的 Z Z Z 通常等于 N N N,除非 MaskNet 串联模式下缩减了维度,那这里需要对齐
- 这里的 Z Z Z 要求小于 T T T,给与 Aggregation Layer 更多聚合处理空间
LN(Layer Normalization)
对于某一长度为
K
K
K 的向量
x
K
=
[
x
1
,
x
2
,
.
.
.
,
x
K
]
∈
R
K
x_K=[x_1,x_2,...,x_K] \in R^K
xK=[x1,x2,...,xK]∈RK 做 LN 转化到
h
K
∈
R
K
h_K \in R^K
hK∈RK
h
=
g
⨀
N
(
x
)
+
b
,
N
(
x
)
=
x
−
μ
δ
μ
=
1
K
∑
i
K
x
i
,
δ
=
1
K
∑
i
K
(
x
i
−
μ
)
2
h = g \bigodot N(x)+b,\ \ N(x)=\frac{x-\mu}{\delta}\\\mu=\frac{1}{K}\sum_i^Kx_i,\ \ \delta=\sqrt{\frac{1}{K}\sum_i^K(x_i-\mu)^2}
h=g⨀N(x)+b, N(x)=δx−μμ=K1i∑Kxi, δ=K1i∑K(xi−μ)2
其中
μ
\mu
μ 和
δ
\delta
δ 是 instance 粒度下的统计值,
而
g
∈
R
K
g \in R^K
g∈RK(gain,增益) 和
b
∈
R
K
b \in R^K
b∈RK(bias,偏置)是待学习的参数值,还是要给模型变换特征分布的空间。
LN 相比于 BN(Batch Normalization):
- 相同点:都引入一样的新参数(要给模型留下变换特征分布的空间),都能帮助模型更快收敛且减小梯度消失&梯度爆照
- 差异点:
- BN:
- 在 Batch 样本角度 Norm(纵向)
- Train过程中需要额外保存样本分布的 均值 & 标准差,Infer的时候用
- 不适用于输入不定长(RNN、transformer 等序列网络)或者 batchSize(当模型性能消耗太大,batch只能很小) 太小的情况
- LN:
- 在 单样本全特征角度 Norm(横向)
- BN:
LN 在 MaskBlock 的应用细节上:
- 原始 Embedding 输入时:要区分 field 分别做 LN,再Concat
- MaskBlock 的隐层 输出时:整体做 LN,再做 ReLU
4.2 用户行为序列建模
用户在APP中的各种行为(浏览、点击、点赞、评论、购买…)组成的序列隐藏这用户最真实的兴趣(毕竟推荐的就是这些内容),是重要的特征数据。
序列信息的构成(以用户最近观看的50个视频为例):
- 基础信息:每个视频的ItemID进行Embedding
- 时间差信息:计算观看该视频时刻距离本次请求时刻的时间差,将这个时间差桶化成一个整数后进行Embedding。这个信息非常重要,历史行为的影响肯定是会随时间衰减的。
- 其他辅助信息:相对次要,例如视频的元信息(作者、来源、分类、标签)和动作的程度(观看时长、观看完成度)
序列信息的最简单的处理:直接Pooling压缩,如下
- Sum Pooling: ∑ i e i \sum_ie_i ∑iei
- Average Pooling: 1 / N ∑ i e i 1/N \sum_ie_i 1/N∑iei
- Weighted-sum Pooling: 1 / N ∑ i w i e i 1/N \sum_iw_ie_i 1/N∑iwiei 权重根据时间差(时间越久权重越低)或动作程度计算
4.2.1 DIN (Deep Interest Network 阿里妈妈 2018)
上述Pooling的问题在于其将用户整个历史序列压缩到了一个固定长度的向量中,但是用户的兴趣应该是多元的、分散的,不同的候选物料应该和不同的历史序列局部去匹配。所以DIN借助Attention实现了这样“千物千面”的效果。
核心是
U
E
u
,
t
=
∑
j
=
1
H
A
(
h
j
,
t
)
h
j
UE_{u,t}=\sum_{j=1}^HA(h_j,t)h_j
UEu,t=∑j=1HA(hj,t)hj:
- U E UE UE 是用户兴趣 Embedding
- h j h_j hj 是用户历史交互的物料序列, t t t是当前候选物料
- A ( ) A() A() 是计算物料 h j h_j hj 在用户兴趣 Embedding 中的权重(原文中的 Activation Unit),使用 targe-Attention(候选物料去交叉所有历史记录)去实现的,本书作者实例代码上使用了 Multi-Heads 来实现
DIN 中用户序列的信息实际上和序列顺序无关、和时间无关。后续的 DIEN(Deep Interest Evolution Network 2019)引入 GRU 这样的 RNN 结构来建模用户兴趣演进(eg. 今天买苹果手机,明天可能要买机械键盘),但是模型性能消耗太大,并且更加场景化了(很多场景可能没有这么强的演化规律)。
序列内依赖建模
DIN 做了候选物料与序列的充分交叉,却没有考虑序列内的交叉关系。例如一个用户买过MacBook也买过iPad,这种组合将产生更强的信号。业界提出了一系列模型来反映不同历史序列之间的依赖性,当前较为常见的方案是双层Attention:
- 第一层:先对原始序列做一次 Multi-Head Self-Attention,完成序列内的交叉(强化一些序列内的表征,例如 iphone 和 ipad 相互加强)
- 第二层:套用DIN来做 target-Attention
4.2.2 长序列建模(Online) SIM(Search-based Interest Model)
动机:序列太短容易有噪声(包含了用户临时的一些意图),且无法反映一些周期性的行为,例如每周、每月的习惯
挑战:但是序列太长又会有性能问题,DIN的时间复杂度是O(BLd)而上述双层Attention的时间复杂度是O(BLL*d),B是batch size、L是序列长度、d是其中每个Embedding的长度。
该模型中,用户的短期兴趣依然会使用 DIEN(DIN的升级版)来加工处理(实际上DIN这种使用Attention来聚合序列建模的方式也被称为Soft-Search,毕竟是做了“软过滤”),长期兴趣使用Search-based Interest Model架构处理,分拆为两个阶段:
- GSU(Gneral Search Unit):从原序列中筛选(搜索)出与候选物料 t 相关的一个短序列SBS(Sub user Behavior Sequence),这一步将万量级的原始序列过滤到百量级。这里需要工程系统上落离线索引,具体上有如下两种实现方式
- Hard Search(精确匹配):拿候选物料t的某个属性(例如分类/标签),在用户完整的长期历史中搜索召回相同属性的历史物料,为了加速这一过程,阿里设计了UBT(User Behavior Tree)数据裤来加速(实际上就是为每个user做了一套基于分类的倒排索引),是一个双层HashMap(外层key为userId,内层key为商品类别,内存value为该用户该类别下买过的商品ID列表。
- Soft Search(模糊查找):实际上还是用 Embedding, 在用户长期行为序列中通过“近似近邻搜索算法”ANN(Approximate Nearest Neighbors)来查找距离最近的历史物料组成SBS。其Item Embedding原文中只提到用候选物料和长期行为序列构建一个的CTR模型来获得。
- ESU(Exact Search Unit):在SBS(百量级,一般在200以下)上套用 DIN 即可,称为精确搜索
4.2.3 长序列建模(Offline)
SIM 是在线实时计算的,如果没有那么充沛的计算资源和工程团队支持,拿可以尝试离线的长序列建模(将线上资源消耗转嫁到线下,并减少一些交叉复杂性)。
最简单的方式就是做长期兴趣特征,将用户在一定时间段内各个分类/标签下的行为做统计,加工成偏好类特征。
更复杂的方式是离线预训练一个辅助模型来提取用户长期兴趣。一般流程:
- 预训练该模型,输入是用户长期行为序列,输出是一个 Embedding 代表用户长期兴趣
- 训练完成后,将行为序列超过一定长度的用户都过一遍这个模型,得到代表这些用户长期兴趣的Embedding,存入线上例如Redis
- 当线上预测或者正式模型训练时获取这个 Embedding;这个预训练模型可以低频次更新(例如一天一次)
美团的超长用户行为序列建模就是使用了这样的方案,其具体训练模型时:
- 模型:双塔结构
- 样本:喂入模型的样本是三元<LS_A, SS_A, SS_B>,分别是同一用户A的长期行为序列和短期行为序列,以及随机采样的另一个用户B的短期行为序列;LS_A 喂入左塔得到表征 UL_A,SS_A和SS_B分别喂入右塔得到 UL_B
- 建模目标:cosine(UL_A, SS_A) 尽量大,同时 cosine(SS_A, SS_B) 尽量小
- Inference:建模完成后,左塔的预测向量UL即是用户的长期兴趣表征(没法做到SIM、DIN那样千物前面的交叉;但是线上性能压力小,只是用户侧增加一个特征而已)
5. 召回
DNN出现之前,召回主要是基于规则和统计,较少训练模型;DNN时代以向量召回为主。
5.1 传统召回算法
传统召回算法复杂度低,可解释性强,初创团队/小型系统/业务诉求能够快速响应。
5.1.1 基于物料属性的倒排索引(即 U2X2I)
离线将具备相同属性的物料集合起来,每个结合内部按照后验消费指标(例如CTR)降序排序。线上实时请求中,提取用户偏好的标签/关注的作者等信息,根据此倒排索引把对应的物料集合作为召回结果返回。
5.1.2 基于统计的协同过滤算法(CF,Collaborative Filtering)
这类传统协同过滤无需训练模型,完全基于统计,又分为两种:
- 基于用户的协同过滤(User CF,U2U2I,你的朋友们喜欢Y):给用户A找到与他有相似爱好的用户B,把B喜欢的东西推荐给A
- 基于物料的协同过滤(Item CF,U2I2I,喜欢X的用户也喜欢Y):用户A喜欢物料C,找到与C相似的其他物料D推荐给A。两种算法的计算过程差不多
这里以 Item CF 计算过程如下展开说明。
- 首先定义用户反馈矩阵 A ∈ R m × n A \in R^{\ m\times n} A∈R m×n,这里的 m m m 取用户数、 n n n 取物料数, A A A 中的每一个位置表示该用户和该物料的交互,可以是显式交互例如评分,也可以是隐式交互例如点击。整个A超级稀疏。
- 然后计算任两个物料之间的相似度 S = A T A ∈ R n × n S=A^TA \in R^{\ n\times n} S=ATA∈R n×n, S S S 中每一个位置表示相关的两个物料的相似度(相似度有多种不同的度量方案);这块计算有非常成熟的 MapReduce 分布式计算方案。(我个人思考一下,这个S不能直接计算,不然物料10万*10万的维度爆炸还是受不了;但是考虑到A高度稀疏,从User角度去遍历统计共现pair就能极大提速)
- 最后为用户 u u u召回时,使用 r u = A [ u , : ] S r_{u}=A[u,:]S ru=A[u,:]S 预估出来的 u u u 对所有物料的喜欢程度。从中选择预估值最大的前K个物料作为召回结果返回(即基于用户过的物料历史选择最相关的物料)。对于 Item CF 来说,由于 S S S 相对小,这一步也可以充分利用 MapReduce 来分布式计算。
5.1.3 基于模型的协同过滤算法,又称为矩阵分解 MF(Matrix Factorization)
还是和上述CF一致会定义用户反馈矩阵 A ∈ R m × n A \in R^{\ m\times n} A∈R m×n,但是会通过定义用户隐向量矩阵 U ∈ R m × k U \in R^{\ m\times k} U∈R m×k和物料隐向量矩阵 V ∈ R n × k V \in R^{\ n\times k} V∈R n×k,模型预估结果为 P = U V T P=UV^T P=UVT,通过 A A A 中有值的部分来迭代优化 U U U 和 V V V;最终 P P P 可以预测全量的用户&物料组合,召回是选择 P P P 最大的前K个物料返回。该方法的缺点是:
- 只使用了 UserID、ItemID 当特征,信息来源受限(实际上相当于只使用这2个特征的FM,或者说双塔模型)
- 对于未曾在训练集中出现过的新用户、新物料,无法给出预估结果
5.1.4 多路召回合并
考虑到“冗余防错”和“互相补充”的目的,推荐系统中通常会有多路召回,这些召回各自的结果需要合并进一个结果集再传递给下游的粗排或精排模块。合并要去重&截断。这里推荐的方案是(分路轮流插入,保证各分路能体现到下游):
- 每一路召回的内部做用户偏好排序
- 每一轮合并,遍历所有分路,每路 pop 出第一面插入合并结果集
- 重复以上合并过程若干轮,知道最终结果集被插满
5.2 向量化召回统一建模框架
当前推荐系统中的主流召回算法,向量化召回 EBR(Embedding-Based Retrieval)。本质上就是将召回问题,建模成向量空间内的近邻搜索问题,例如微软的DSSM、双塔模型、YouTube的召回算法、Airbnb的召回算法、Pinterest的PinSage等等。
可以区分为3类召回场景 U2I、U2I2I、U2U2I(这个拆分和CF类似,最常用的还是 U2I 的双塔)。
假设召回对应的两类实体分别为 Q、T(例如 Q=User、T=Item),召回可分为3个步骤:
- Train Model:训练一个模型M,将Q中的每个实例q和T中的每个实例t都映射到同一个向量空间中。
- Build Index:将T中实例(例如百万级)喂入模型M映射成向量,并都灌入Faiss或Milvus这样的向量数据库,建立索引。
- Online Inference:在线实时服务时,对于一个Q的实例q,通过模型M映射成向量 E m b q Emb_{q} Embq,然后请求向量数据库相关索引得到与其最相似的K个T类邻居向量 E m b t i Emb_{t_{i}} Embti,将这K个邻居作为召回结果返回。
对于建模这一层,实际上要解决以下4个问题:
- 如何定义正样本,即哪些q和t在向量空间内应该相近;
- 如何定义负样本,即哪些q和t在向量空间内应该较远;
- 如何将q和t映射成 Embedding;
- 如何定义优化目标,即损失函数。
5.2.1 如何定义正样本
区分召回场景:
- U2I 召回:q是用户、t是物料,一个用户和其交互过(点击、购买…)的物料应该相近
- I2I 召回:q是物料、t也是物料,同一用户在较短时间内交互过的物料应该相近(例如同个session内)
- U2U 召回:q是用户、t也是用户,例如孪生网络,q是用户一半的交互历史,t是用户另一半交互历史
5.2.2 如何定义负样本(重点)
先明确几个概念:
- 负采样(Negative Sampling) & 正采样(Positive Sampling):表述的是采样生成 负样本/正样本
- 过采样(Over Sampling) & 欠采样(降采样,Under Sampling):表述的是以 高于/低于 原频率的方式去采样
随机负采样
主要依靠随机负采样。
关键点在于 模型离线训练的数据分布应该与在线服务时保持一致。召回环节候选池很大,会包含大量的之前没怎么曝光过的候选,如果和精排建模一样使用“曝光未点击”的样本做负样本会因为 Sample Selection Bias 带来召回模型的偏差(离线训练看起来不错,但是线上服务时很多候选没见过,打分打偏)。
- 优势:能够保证线上效果。假设之前曝光不出去的物料,大部分也是差的,随机负采样会把他们抑制住。另外,对于热门物料还是能提供一定个性化的补充
- 劣势:模型基本上只会推荐热门物料。正样本集中在热门,负样本全局平均,会导致整体样本上热门物料点击率趋向于1.0,头部效应加剧。这部分可以做一些如下优化
- 正样本中 热门物料 降采样:设定 P p o s ( t i ) = α f ( t i ) P_{pos}(t_i)=\sqrt{\frac{\alpha}{f(t_i)}} Ppos(ti)=f(ti)α, f ( t i ) f(t_i) f(ti)为物料 t i t_i ti曝光频率, α ∈ [ 0 , 1 ] \alpha \in [0,1] α∈[0,1]为热门物料定义超参(例如只对频率>=0.01的内容降采样)
- 负样本中 热门物料 过采样:设定 P n e g ( t i ) = F ( t i ) b ∑ t ∈ V F ( t ) b P_{neg}(t_i)=\frac{F(t_i)^b}{\sum_{t \in V}F(t)^b} Pneg(ti)=∑t∈VF(t)bF(ti)b, F ( t i ) F(t_i) F(ti)为物料 t i t_i ti曝光频次, V V V代表所有物料, b ∈ [ 0 , 1 ] b \in[0, 1] b∈[0,1]为平滑超参数; b b b 越大过采样越强(0 即平均采样,1 即完全按照频次采样)
InBatch 负采样(自行补充的)
训练集只包含正样本,训练过程中在Batch中实时采样负样本。
- 优势:负样本采样比例和正样本一致,避免模型只推荐热门物料。并且不用离线准备负样本,降低成本
- 劣势:有严重的 离线训练 和 在线服务 不一致问题(模型训练过程中只见过极少的一部分物料)
训练时使用对比学习常用的 loss function,InfoNCE(Noise Contrastive Estimate),优化Loss如下:
l
o
s
s
=
−
l
o
g
e
x
p
(
s
+
/
τ
)
∑
i
k
e
x
p
(
s
−
/
τ
)
+
e
x
p
(
s
+
/
τ
)
loss=-log\frac{exp(s_{+}/\tau)}{\sum_i^k exp(s_{-}/\tau)+exp(s_{+}/\tau)}
loss=−log∑ikexp(s−/τ)+exp(s+/τ)exp(s+/τ)
- 包含 1个正样本 和 k个负样本,负样本是 InBatch 内所有其他样本,学习目标是要模型区分 正样本 和 噪声负样本(所以叫 NCE)
- 模型建模的是 s = f ( q i , t i ) − p ( t i ) s = f(q_i, t_i)-p(t_i) s=f(qi,ti)−p(ti),对于U2I召回场景,这里 f ( q i , t i ) f(q_i,t_i) f(qi,ti) 建模了 用户 q i q_i qi 和 物料 t i t_i ti 的相关性;然后 p ( t i ) p(t_i) p(ti) 是 物料 t i t_i ti 本身被选为负样本的概率(即其在训练样本中的频率),用于矫正热门物料预估(热门物料被选为负样本概率高,这里通过频率做矫正)
- τ ∈ ( 0 , 1 ) \tau \in (0, 1) τ∈(0,1) 是超参数温度系数,这一系数越小越放大模型预估 s s s,使得模型对于错误预估项更加敏感;即系数越小模型越远离负样本、越保守。
Mixed 混合负采样
一般负样本设计都要整合多种采样方式,例如整合上述的 随机负采样(离线样本中准备好、混合好) + InBatch负采样,整合后的训练Loss设计整合两部分:
- 对于负样本:不计算Loss
- 对于正样本:计算 InBatch 的 InfoNCE,实际上已经把 负样本 考虑进去了
这是 Easy Negative(随机负采样)和 Hard Negative(InBatch 负采样)相结合。
另外,InBatch负采样策略还能配合独立的物料Embedding cache来缓解负样本热门物料聚集,例如cache上之前多轮Batch的物料,这样在cache中更容易采样到冷门的物料。
5.2.3 如何映射成 Embedding
最大的特点是 排序鼓励交叉,召回要求解耦,相应的对比:
- 排序模型:无论从特征工程&模型结构设计上都会充分把 用户信息 和 物料信息 做交叉,把交叉特征、用户特征、物料特征整合到一个向量中再通过多层Fully Connection DNN进一步充分交叉。这些交叉计算不可能离线完成,都是实时计算的,精排层百量级候选扛得住。
- 召回模型:动辄百万级的候选扛不住交叉计算,q 和 t 会充分解耦,分别计算出 Embedding。正如“向量化召回统一框架”开头给出的 Train Model -> Build Index -> Online Inference 过程,只能做一次近似检索。
5.2.4 如何定义优化目标
精排常使用 Binary Cross-Entropy Loss(二分类交叉熵损失)来追求预测值的绝对精准;但是召回一般没有准确的负样本,所以常用多分类 Softmax Loss,只要求把正样本的预测值尽量高。另外,召回本身也是筛选下游排序的候选集,也不需要绝对精准,只需要把用户偏好的靠前。
NCE Loss(Noise Contrastive Estimation)
实际上述 【InBatch 负采样】部分已经使用了类似的 InfoNCE(是 NCE 的进一步拓展,把 binary classification 拓展成 category classification)。
从最开始的动机来看,把召回损失看成超大规模多分类问题(即每个候选物料都是一个独立的分类),对于任意一个正样本
u
i
u_i
ui、
t
i
t_i
ti,相关的 Softmax Loss 为:
L
s
o
f
t
m
a
x
=
−
l
o
g
e
x
p
(
u
i
t
i
)
∑
j
∈
T
e
x
p
(
u
j
t
j
)
L_{softmax}=-log\frac{exp(u_i t_i)}{\sum_{j \in T} exp(u_j t_j)}
Lsoftmax=−log∑j∈Texp(ujtj)exp(uiti)
这里的
T
T
T 是整个物料候选集(可达百万级),直接计算是不可接受的。
所以才有 NCE 做分母的简化,将“超大规模多分类”简化为“区分样本是否来自噪声”, NCE 公式如下:
L
N
C
E
=
−
l
o
g
(
σ
(
G
(
u
i
,
t
i
)
)
)
−
∑
j
l
o
g
(
1
−
σ
(
G
(
u
i
,
t
j
)
)
)
L_{NCE}=-log(\sigma (G(u_i,t_i)))-\sum_j log(1-\sigma(G(u_i,t_j)))
LNCE=−log(σ(G(ui,ti)))−j∑log(1−σ(G(ui,tj)))
G
(
u
i
,
t
)
G(u_i,t)
G(ui,t) 即样本的 logits,其中
t
i
t_i
ti 为正样本,
t
j
t_j
tj 为随机采样的负样本(即噪声,通常会采样多条),模型目标就是要区分 正样本 vs. 噪声;
σ
\sigma
σ 是 sigmoid 函数
1
1
+
e
−
x
\frac{1}{1+e^{-x}}
1+e−x1 可展开简化为:
L
N
C
E
=
l
o
g
(
1
+
e
x
p
(
−
G
(
u
i
,
t
i
)
)
)
+
∑
j
l
o
g
(
1
+
e
x
p
(
G
(
u
i
,
t
j
)
)
)
L_{NCE}=log(1+exp(-G(u_i,t_i)))+\sum_j log(1+exp(G(u_i,t_j)))
LNCE=log(1+exp(−G(ui,ti)))+j∑log(1+exp(G(ui,tj)))
更重要的是,
G
(
u
i
,
t
)
G(u_i, t)
G(ui,t) 作为 logits,即 log-odds,本身odds几率的设计上考虑了 Contrastive 对比噪声:
G
(
u
,
t
)
=
l
o
g
P
(
t
∣
u
)
Q
(
t
∣
u
)
=
l
o
g
(
P
(
t
∣
u
)
)
−
l
o
g
(
Q
(
t
∣
u
)
)
=
u
t
−
l
o
g
Q
(
t
∣
u
)
G(u, t) = log\frac{P(t|u)}{Q(t|u)}=log(P(t|u))-log(Q(t|u))=ut-logQ(t|u)
G(u,t)=logQ(t∣u)P(t∣u)=log(P(t∣u))−log(Q(t∣u))=ut−logQ(t∣u)
- P ( t ∣ u ) P(t|u) P(t∣u) 表示用户 u u u 确实喜欢 t t t 的概率,这是建模的目标分布;最终通过建模 u , t u,t u,t 的 Embedding 来实现,内积 u , t u,t u,t 即 P ( t ∣ u ) P(t|u) P(t∣u),线上 serving 时只使用这部分(离线训练时使用 G ( u , t ) G(u,t) G(u,t))
- Q ( t ∣ u ) Q(t|u) Q(t∣u) 表示用户 u u u 不喜欢 t t t 的概率(这是 odds 的设定),实际上定义为 t t t 来自噪声分布的概率;可以使用负样本采样频率表示,从效果上是对负采样频率高的物料做补偿(负采样频率越高, Q ( t ∣ u ) Q(t|u) Q(t∣u)越大,迫使模型 P ( t ∣ u ) P(t|u) P(t∣u)预估越大,即最终内积 u , t u,t u,t 打分越高)
NEG Loss(Negative Sampling Loss)
相对 NCE(Noise Contrastive Estimation)简化,去掉对比项,即
G
(
u
,
t
)
G(u, t)
G(u,t) 简化为
P
(
t
∣
u
)
P(t|u)
P(t∣u) 即
u
t
ut
ut:
L
N
E
G
=
l
o
g
(
1
+
e
x
p
(
−
u
i
t
i
)
)
+
∑
j
l
o
g
(
1
+
e
x
p
(
u
i
t
j
)
)
L_{NEG}=log(1+exp(-u_it_i))+\sum_j log(1+exp(u_it_j))
LNEG=log(1+exp(−uiti))+j∑log(1+exp(uitj))
NCE 本身有理论保证(足够的负样本下梯度趋同与超大规模Softmax),NEG 没有保证,但是简单。
Sampled Softmax Loss
整体上类似 NCE,推导过程有差异,形式上最终loss改为 softmax 而非 sigmoid:
L
S
a
m
p
l
e
d
S
o
f
t
m
a
x
=
−
l
o
g
e
x
p
(
G
(
u
i
,
t
i
)
)
e
x
p
(
G
(
u
i
,
t
i
)
)
+
∑
j
e
x
p
G
(
u
i
,
t
j
)
)
L_{SampledSoftmax}=-log\frac{exp(G(u_i,t_i))}{exp(G(u_i,t_i)) + \sum_jexpG(u_i,t_j))}
LSampledSoftmax=−logexp(G(ui,ti))+∑jexpG(ui,tj))exp(G(ui,ti))
同样引入修正项:
G
(
u
,
t
)
=
l
o
g
P
(
t
∣
u
,
C
)
=
u
t
−
l
o
g
Q
(
t
∣
u
)
G(u,t)=logP(t|u,C)=ut-logQ(t|u)
G(u,t)=logP(t∣u,C)=ut−logQ(t∣u)
同样最终建模
u
t
ut
ut,
−
l
o
g
Q
(
t
∣
u
)
-logQ(t|u)
−logQ(t∣u) 在训练过程中纠偏
Pairwise Loss
实际上是 Learning-to-Rank(pointwise、pairwise、listwise)的一种实现。
对于正样本&随机采样负样本的三元组
(
u
i
,
t
i
+
,
t
i
−
)
(u_i,t_{i+},t_{i-})
(ui,ti+,ti−) 建模优化
s
i
m
i
l
a
r
i
t
y
(
u
i
,
t
i
+
)
>
>
s
i
m
i
l
a
r
i
t
y
(
u
i
,
t
i
−
)
similarity(u_i,t_{i+})>>similarity(u_i,t_{i-})
similarity(ui,ti+)>>similarity(ui,ti−),一种表示 “远远高于” 的方法是使用 Marginal Hinge Loss:
L
H
i
n
g
e
=
m
a
x
(
0
,
m
−
(
u
i
t
i
+
−
u
i
t
i
−
)
)
L_{Hinge}=max(0,m-(u_it_{i+}-u_it_{i-}))
LHinge=max(0,m−(uiti+−uiti−))
即要求建模后的 正例pair 相对 负例pair 的度量差异需要大于超参数 m m m(即 Margin),整个 loss 函数呈 Hinge 造型。
如果想避免超参
m
m
m 调节,可以使用 Bayesian Personalized Ranking Loss (BPR Loss)。
BPR 建模排序正确概率(
t
i
+
t_{i+}
ti+排序在
t
i
−
t_{i-}
ti−之前的概率)
P
C
o
r
r
e
c
t
O
r
d
e
r
=
s
i
g
m
o
i
d
(
u
i
t
i
+
−
u
i
t
i
−
)
P_{CorrectOrder}=sigmoid(u_it_{i+}-u_it_{i-})
PCorrectOrder=sigmoid(uiti+−uiti−),目标是将这一概率最大化,label 全部都是 1,最终公式即:
L
B
P
R
=
−
l
o
g
(
P
C
o
r
r
e
c
t
O
r
d
e
r
)
=
l
o
g
(
1
+
e
x
p
(
u
i
t
i
−
−
u
i
t
i
+
)
)
L_{BPR}=-log(P_{CorrectOrder})=log(1+exp(u_it_{i-}-u_it_{i+ }))
LBPR=−log(PCorrectOrder)=log(1+exp(uiti−−uiti+))
5.3 向量召回建模案例
向量召回和 NLP 最经典的 Word2Vec(学习 Word Embedding)有很多相似之处。
Word2Vec 建模设计如下:
- 正样本:在语料corpus原始数据中,给定一个中心词 w w w,其上下文的词 c c c 做正样本。模型的目标是分辨出其上下文的词
- 负样本:随机负采样一些词作上下文词 c c c
- Embedding:直接过一层 Embedding, w w w、 c c c 分别使用不同的 Embedding,实际产出给下游环节使用的是 w w w
- 优化目标:使用 NEG Loss(Negative Sampling Loss)完成,如下(
c
i
c_i
ci是正样本、
c
j
c_j
cj是负样本)
L w o r d v e c = l o g ( 1 + e x p ( − w i c i ) ) + ∑ j l o g ( 1 + e x p ( w i c j ) ) L_{wordvec}=log(1+exp(-w_ic_i))+\sum_j log(1+exp(w_ic_j)) Lwordvec=log(1+exp(−wici))+j∑log(1+exp(wicj))
5.3.1 Item2Vec
将 Word2Vec 最直接应用到推荐领域就是 Item2Vec,将语料corpus替换成用户历史序列(例如某用户某session内有过点击的一系列物料,也类似与语料中的一个句子,每个物料ID当作一个单词word)。模型训练&预测 Item Embedding 后,可以用于推荐系统 I2I 召回。
同样从向量召回角度来看:
- 正样本:session序列内相近的ItemId应该相似
- 负样本:类似Word2Vec,随机负采样一些物料
- Embedding:不做模型,只使用一个待学习的 Item Embedding
- 优化目标:类似Word2Vec,使用 NEG Loss
5.3.2 Airbnb召回算法实践
主要是结合了业务sense来有效落地了向量化召回。
Airbnb I2I召回实践
类似 Item2Vec,Airbnb有用户筛选租住房屋session过程中的点击序列可使用,具体如下:
- 正样本:session序列内相近的用户点击ItemId应该相似(例如某个房屋 l l l 和其序列内临近的 c c c 相似);另外,最终用户预定的房屋(例如 l b l_b lb)和所有ItemId应该相似,这属于结合业务sense的强信号
- 负样本:随机负采样构成主力负样本(easy negative);另外,对正样本同城负采样一些房屋作为 hard negative,这也属于结合业务sense,非同城的房屋在租房场景下相关性低,同城的才更有区分度
- Embedding:只使用一个 Embedding
- 优化目标:NEG Loss,但是要额外补充 强相关正例 & hard negative
Airbnb U2I召回实践
用户&房屋 预订粒度上数据太稀疏,所以方案上通过用户/房屋的属性&人工规则,分别做分群(聚类),建模聚类之间的关系。具体如下:
- 正样本:某用户 u u u 预定过 l l l 则有分别的所属类别 U U U 和 L L L 在向量空间上应该接近
- 负样本:对于某用户类别 U U U,随机负采样房屋类别;另外,如果有 u u u 被房东拒绝,相关房屋的类别 L L L 作 hard negative,也属于结合业务sense
- Embedding:分别定义 用户类型 & 房屋类型 2个 Embedding(维度一致,毕竟还要内积)
- 优化目标:NEG Loss,但是要额外补充 hard negative
5.3.3 阿里 EGES召回(2018)
全称是 EGES(Enhanced Graph Embedding with Side Information),相比于上述召回算法主要差异是:
- Graph Embedding:是一种图算法,会构建物料关系图(也是基于用户session内的交互序列)来提取关系
- Side Information:在 itemID 之外,引入 item属性特征(例如商品类目/品牌信息)共同建模
从向量召回框架的视角上看:
- 正样本:图算法体现在这里,又细分3个步骤
- 建图:将所有用户session内的交互序列(例如30分钟内的点击序列)整合到一个 物料关系的有向图 中,图中Node是物料,图有向边Edge是 M i j M_{ij} Mij 定义序列中“先点击 物料 i i i 后点击 物料 j j j 的次数”
- 随机游走:假设当前处于节点 i i i,该节点指向的节点集合是 N + ( v i ) N_+(v_i) N+(vi),则随机游走下一步转移概率是 P ( v j ∣ v i ) = M i j ∑ j ∈ N + ( v i ) M i j P(v_j|v_i)=\frac{M_{ij}}{\sum_{j\in N_+(v_i)} M_{ij}} P(vj∣vi)=∑j∈N+(vi)MijMij
- 构造样本:通过随机游走新生成的序列生成正样本;类似Word2Vec把序列当作句子,滑窗中临近的物料应该相似,作正样本
- 负样本:随机负采样
- Embedding:Side Information 体现在这里
- 定义:不同于 Item2Vec 只是用1个Embedding,假设有 n n n个Side Information特征字段,则总计采用 n + 1 n+1 n+1 个Embedding
- 合并:具体使用上对
n
+
1
n+1
n+1 个Embedding
W
0
W
1
.
.
.
W
n
W^0W^1...W^n
W0W1...Wn 做合并到一个
H
H
H
- 简单方式:直接 Average Pooling,对于物料 i i i 有表征 H i = 1 n + 1 ∑ j = 0 n W i j H_i=\frac{1}{n+1}\sum_{j=0}^nW_i^j Hi=n+11∑j=0nWij
- 复杂方式:每个物料有不同的合并方式,新引入合并权重矩阵 A ∈ R ∣ I ∣ × ( 1 + n ) A\in R^{|I|\times (1+n)} A∈R∣I∣×(1+n),对于所有物料 ∣ I ∣ |I| ∣I∣ 都有独立的学习的表征合并权重,其中 a i j a_i^j aij 表示第 i i i 个物料中第 j j j个Embedding的合并权重。物料 i i i 的表征定义成 H i = ∑ j = 0 n e x p ( a i j ) W i j ∑ j = 0 n e x p ( a i j ) H_i=\frac{\sum_{j=0}^n exp(a_i^j)W_i^j}{\sum_{j=0}^n exp(a_i^j)} Hi=∑j=0nexp(aij)∑j=0nexp(aij)Wij
- 优化目标:NEG Loss
5.3.4 FM召回
用于 U2I 召回场景,可以在召回环节做到二阶特征交叉;相对经典的双塔模型来说要简单一些。
- 正样本:有过点击的 user、item 组合 & 对人们物料降采样
- 负样本:随机负采样 & 对热门物料过采样
- Embedding:user 和 item 特征都使用一个Embedding(但是需要适配Infer),包括二阶交叉Embedding和一阶特征权重
- Train:就是常规的FM模型,输入user & item 特征( I I I 是特征全集), l o g i t F M = b + ∑ i ∈ I w i + 1 2 r e d u c e s u m [ ( ∑ i ∈ I v i ) 2 − ∑ i ∈ I v i 2 ] logit_{FM}=b+\sum_{i\in I}w_i+\frac{1}{2}reducesum[(\sum_{i\in I}v_i)^2-\sum_{i\in I}v_i^2] logitFM=b+∑i∈Iwi+21reducesum[(∑i∈Ivi)2−∑i∈Ivi2]
- Infer:在向量召回场景下需要做解耦适配,我们把模型参数拆分成
F
M
(
u
,
t
)
=
b
+
[
W
u
+
W
t
]
+
[
V
u
u
+
V
t
t
+
V
u
t
]
FM(u, t) = b+[W_u+W_t]+[V_{uu}+V_{tt}+V_{ut}]
FM(u,t)=b+[Wu+Wt]+[Vuu+Vtt+Vut]
- 先省略排序无关项:infer时和item无关的部分可以干掉,包括偏置 b b b、用户特征一阶权重 W u W_u Wu、用户特征二阶权重 V u u V_{uu} Vuu
- Emb中物料偏置部分: E u = 1 E_u=1 Eu=1 用户侧保留一位做点积用, E t = W t + V t t E_t=W_t+V_{tt} Et=Wt+Vtt 物料侧一阶、二阶权重整合到一位scalar上,其中 V t t = 1 2 r e d u c e s u m [ ( ∑ i ∈ I t v i ) 2 − ∑ i ∈ I t v i 2 ] V_{tt}=\frac{1}{2}reducesum[(\sum_{i\in I_t}v_i)^2-\sum_{i\in I_t}v_i^2] Vtt=21reducesum[(∑i∈Itvi)2−∑i∈Itvi2](只针对 I t I_t It 这部分 item特征)
- Emb中物料交叉部分: E u = ∑ i ∈ I u v i E_u=\sum_{i\in I_u}v_i Eu=∑i∈Iuvi 用户侧相关向量 sum pooling, E u = ∑ i ∈ I t v i E_u=\sum_{i\in I_t}v_i Eu=∑i∈Itvi 物料侧相关向量 sum pooling;这背后是 V u t = ∑ i ∈ I u ∑ j ∈ I t v i v j = ( ∑ i ∈ I u v i ) ( ∑ j ∈ I t v j ) V_{ut} = \sum_{i\in I_u} \sum_{j\in I_t} v_i v_j = (\sum_{i\in I_u}v_i) (\sum_{j\in I_t}v_j) Vut=∑i∈Iu∑j∈Itvivj=(∑i∈Iuvi)(∑j∈Itvj)
- Concat整合上述Emb中“物料偏置部分”&“物料交叉部分”即可将FM应用到向量召回中
- 优化目标:可以使用 BPR Loss,构造三元组样本来优化如下目标(注意,Train使用标准FM就行,上述的trick都是for infer的)
L B P R = − l o g ( P C o r r e c t O r d e r ) = l o g ( 1 + e x p ( F M ( u i , t i − , ) − F M ( u i , t i + ) ) ) L_{BPR}=-log(P_{CorrectOrder})=log(1+exp(FM(u_i,t_{i-},)-FM(u_i,t_{i+}))) LBPR=−log(PCorrectOrder)=log(1+exp(FM(ui,ti−,)−FM(ui,ti+)))
5.3.4 大厂主力: 双塔模型
双塔模型是当前大厂的主力召回算法,可以应用到不同召回场景中(U2I、I2I、U2U),最常见的还是U2I。
双塔的特点是解耦,塔顶就只是一个向量,最终召回时只做这一次向量(模糊)匹配计算;塔身可以非常复杂,可以使用很多特征&很复杂的结构,在线运行时单次请求中塔身只会计算一次,没有性能负担。
为了弥补双塔在交叉上的缺陷(相比之下,精排上DIN在DNN前就做了充分交叉),业界有一些替代方案:
- 对于搜索场景:Query能够描述用户当前意图,可以用来做特征交叉
- 阿里巴巴的 SDM 召回:使用用户画像做Query
- 微信的 CDR 模型中,认为用户最后点击的物料最能反应最新的兴趣偏好,用来做Query
双塔模型几个技巧
- L2正则化:用户向量 u u u 和 物料向量 t t t 在计算交叉点击之前,先各自过一个 L Norm 转成长度=1的向量。这样做向量内积就等价于 c o s i n e ( u , t ) cosine(u,t) cosine(u,t) 度量;相似度建模就专注在向量夹角建模上,没必要再调整向量长度。
- 温度调整:上述 InBatch采样 中已经有用到, τ ∈ ( 0 , 1 ) \tau \in (0, 1) τ∈(0,1) 是超参数温度系数,这一系数越小越放大模型预估 s s s,使得模型对于错误预估项更加敏感;即系数越小模型越远离负样本、越保守。
- 采样概率修正:上述 InBatch采样 & NCE(Noise Contrastive Estimation)中都已经有用到,在 logits 层面增加 − l o g Q ( t ) -logQ(t) −logQ(t) 来纠偏(仅在train生效,infer时不考虑),缓解对于热门物料的过度打压(热门采样成负样本的概率高)
5.4 图卷积网络 GCN(Graph Convolutional Network)召回应用
推荐系统构图:
- Node 顶点:各种实体,例如 用户、商品、店铺、品牌 等
- Edge 边:各种互动关系,例如 点击、购买、收藏 等
在完成构图之后做图上的边预测。按照向量化召回框架看:
- 正样本:图上有边的两端节点
- 负样本:随机负采样
- 优化目标:NEG Loss、Sampled Softmax Loss、Marginal Hinge Loss、BPR Loss 都可以
- Embedding:最大的特点还是 Embedding 定义的时候,会利用图结构信息整合关联节点,正如 Graph Convolution
5.4.1 GraphSAGE(Graph SAmple and aggreGatE)
是传统GCN的一种工业界实现,关键的差异是:
- 传统GCN
- 基于频域(frequency domain):类比图片做傅里叶变化后再卷积
- 直推式(Transductive):Infer 和 Train 只能使用相同的图,未出现过的新节点无法给出向量表示
- GraphSAGE
- 基于空间域(spatial domain):类比图片直接在像素点上卷积
- 归纳式(Inductive):未出现过的新节点可获得向量表示
GraphSAGE(Graph SAmple and aggreGatE)的实现方式正如其名:
- SAmple:样本中每个节点都需要通过Sample去准备相关图邻接节点(不采样的化单点要汇集全图的信息),GraphSage简化只采样单点最近的 k k k 层邻接节点(并且每层都要严格采样数量,否则最终汇集合并数量不可控)
- aggreGatE:将采样得到的
k
k
k 层节点,由远及近逐层卷积合并,并最终和该单节点的上一轮表征合并,相关公式如下
h v 0 = x v h v k = σ ( W k ∑ u ∈ N ( v ) h u k − 1 ∣ N ( v ) ∣ + B k h v k − 1 ) , f o r k > 0 z v = h v l a s t \begin{aligned} h_v^0 &= x_v \\ h_v^k &= \sigma (W_k\sum_{u\in N(v)}\frac{h_u^{k-1}}{|N(v)|}+B_kh_v^{k-1}),\ for\ k>0 \\ z_v &= h_v^{last} \end{aligned} hv0hvkzv=xv=σ(Wku∈N(v)∑∣N(v)∣huk−1+Bkhvk−1), for k>0=hvlast- h v 0 h_v^0 hv0 是输入层,输入的是各个节点 v v v 的原始表征
- z v z_v zv 是输出层,即节点 v v v 的最终表征
- h v k h_v^k hvk 是卷积层,对 v v v 的邻居节点的上一层表征 h u k − 1 , u ∈ N ( v ) h_u^{k-1},\ u\in N(v) huk−1, u∈N(v) 做 Sum Pooling 并合并上自己上一层表征 h v k − 1 h_v^{k-1} hvk−1 做线性映射合并(即卷积参数 W k W_k Wk 和 B k B_k Bk),最后再过非线性的sigmoid激活函数 σ \sigma σ。实际计算上,这是一个由远及近的汇集过程,从 k = 0 k=0 k=0 的原始表征逐层向中心节点 v v v 汇集,随 k k k 增大逐渐靠近乃至最终汇集到一点(公式中 h v k h_v^k hvk 中的 v v v 不太准确,实际上只有最后一层是中心节点 v v v,之前的层是递归的邻接节点)。最终的效果是,GCN扩大了单个几点的感受野(Receptive Field),卷积层数越多感受野越大。
5.4.2 一些落地实践
PinSage
Pinterest 团队开发的 I2I 召回系统,被誉为 GCN 在互联网大型推荐系统中的首次实战。
具体建模上,构建 Pin(可以理解成图片/网站)和 Board(可以理解成某用户的收藏夹)的二部图,通过 Board 将相似的 Pin 串联起来(这是非常好的打标,Pinterest 的业务形态上正好丰富的用户UGC输入),学习到健壮的 Pin Embedding,从而提供精准的相似 Pin 推荐。
- 正样本:Board 也被当做 Pin,有连接的节点对做正样本
- 负样本:随机负采样
实现上大致 GraphSage 一致,做了很多性能上的优化设计。
异构图上的 GCN
图的区分:
- 同构图:只有一种节点&一种边
- 异构图:不只一种节点/不只一种边
推荐算法信息输入很繁杂,可以尝试异构图建模,例如:
- MultiBisage(PinSage 升级版):除了“Pin-Board收藏”之外,再构建“Pin-Keyword搜索”等K个二部图,最终的Pin表征使用这K个表征喂入Transformer做Self-Attention融合后输出。
- GraphTR(微信):异构图上同时引入“用户、视频、视频标签、视频来源”4类节点,卷积合并时邻接的4类节点分别合并(合并方式上用中心节点上一层表征对邻接节点做 “Target-Attention” 或 “Transformer & Avg Pooling”),最后4个表征再通过FM两两交叉得到最终融合表征。
6. 粗排
粗排是速度于精度的又一次折中,是精排的简化版本:
- 相对 召回:候选集大幅减少,百万量级 to 万量级;可以比召回复杂
- 相对 精排:候选集还是太大,万量级 to 千量级;还是不能像精排那样复杂
6.1 粗排主力还是 双塔
本章讨论的各类双塔优化方案,一部分是 召回双塔 & 粗排双塔 通用的。
6.1.1 粗排双塔 和 召回双塔
一致的特性如下
- 用户塔、物料塔解耦(这是双塔模型的本质设计):无论塔身设计如何复杂,只在塔顶的最终向量表征上交叉。还是用不了Target-Attention
- 在线请求时只需要计算一次 用户塔:物料塔向量全部都提前计算了
差异的特性如下
- 模型训练时:
- 样本选择:业界主力做法上,召回双塔负样本使用随机负采样&InBatch负采样,而粗排负样本使用曝光未点击(和精排拉齐)。当然,这里粗排和召回上一致地都有样本选择偏差(SSB),有离线在线偏差,这部分的纠偏是粗排的重要优化方向
- 优化目标:召回常用NEG Loss、Sample Softmax Loss等,而粗排常用 BCE(Binary Cross-Entropy)和精排拉齐
- 在线请求时:
- 物料向量 缓存机制不同:召回候选量级大,需要存储到向量索引中;粗排候选量级小,直接缓存到内存即可
- 相似度计算方式不同:召回是通过最近邻ANN算法近似搜索(向量索引);粗排直接精确计算点积。实际上这也给了粗排更多进一步改进空间,例如增加其他交互信息、增加后置DNN层
6.1.2 双塔结构改进
- SENET(之前精排 FiBiNet 有用到):在双塔特征输入层(原始 Embedding) 后叠加 SENET 用于筛选特征,insight 是把无关噪声抑制在塔底,保障最终塔尖表征质量
- 漏风塔(各层表征抄近路):双塔塔顶的最后一层表征,由之前的所有隐层结果拼接后训练(相当于不同抽象层次的表征漏到最后了)
- 补水塔(原始特征抄近路,选择信息量大、区分度大的特征):原始 Embedding 中筛选出一部分关键特征,这部分关键特征会作为所有隐层的输入(给所有层次都补水)
- 多通道聚合:Meta公式使用 Attention Fusion,就是把多个双塔模型融合到一个上面有两个细节:
- N N N个通道的 Embedding 通过 Simple Attention Fusion 融合成一个 Embedding
- 设置辅助loss,双塔中 “融合后 Embedding” + “对侧任意原始 Embedding” 也做内积一同优化,防止有冗余通道滥竽充数
6.2 粗排&精排一致性
即通过 “知识蒸馏”(Knowledge Distillation)将精排模型吸收的信息传递到粗排。
蒸馏使用:MSE(Mean Squared Error)、BCE(Binary Cross-Entropy)
6.3 粗排其他改进
6.3.1 SSB 样本纠偏
粗排模型和召回模型一样地会遇到样本选择偏差SSB(Sample Selection Bias)问题。这里作者给的一个思路是,负样本在使用曝光未点击样本(和精排拉齐)之余,再补充 “进精排未曝光样本”(被之前精排模型淘汰的样本),本质上和知识蒸馏一样也是去模仿&迎合精排,但这里模仿的是排序而不是精确的打分。
具体来说模型负样本拆成 2部分 构成最终训练目标
L
o
s
s
=
L
C
T
R
+
λ
L
L
T
R
Loss=L_{CTR}+\lambda L_{LTR}
Loss=LCTR+λLLTR (
λ
\lambda
λ 调控权重):
- 曝光未点击:搭配上正样本,做 BCE Loss,和精排训练方式一致,记作 L C T R L_{CTR} LCTR
- 进精排未曝光:搭配上正样本组成 Pair,做 LTR(Learning-To-Rank)中的 Pairwise Loss,例如使用 BPR(Bayesian Personalized Rank)Loss,记作 L L T R L_{LTR} LLTR
6.3.2 模型轻量全连接
对传统双塔做改进,UE(User Embedding)和 IE(Item Embedding)不直接做内积,而是拼接之后再经过一个小型DNN网络。毕竟粗排候选已经大幅减少,并且 IE 部分可以提前计算大幅减少实时服务压力。进一步优化性能可以考虑TensorFlow某些步骤中用Float16取代Float32。