2.3.1. FM(因子分解机):双塔模型的雏形¶
虽然因子分解机(Factorization Machine, FM) (Rendle, 2010) 诞生于深度学习兴起之前,但它在思想上可以说是双塔模型的雏形。FM的核心贡献在于,它首次将用户-物品的复杂交互,优雅地分解为两个低维向量的内积操作。
2.3.1.1. 从交互矩阵到向量内积¶
FM模型的完整数学表达式为:
这个公式看起来复杂,但其核心思想简单而深刻:每个特征\(i\)都对应一个\(k\)维的隐向量\(\mathbf{v}_i\),特征间的交互通过这些隐向量的内积\(\langle\mathbf{v}_{i}, \mathbf{v}_{j}\rangle = \sum_{f=1}^{k} v_{i,f} \cdot v_{j,f}\)来建模。
FM的真正巧妙之处在于一个数学变换技巧。原本\(O(n^2)\)复杂度的二阶交互项,可以通过代数运算重写为:
这一变换将计算复杂度从\(O(kn^2)\)降低到\(O(kn)\),使得FM能够处理大规模稀疏数据。
2.3.1.2. 分解为双塔结构¶
虽然FM通过数学变换解决了计算复杂度问题,但在召回任务中,我们还面临另一个挑战:如何高效地为用户从海量候选物品中筛选出最相关的推荐结果?这时就需要考虑将FM分解为双塔结构。
在召回场景下,我们可以将所有特征自然地分为两类:用户侧特征集\(U\)(如用户年龄、性别、历史偏好等)和物品侧特征集\(I\)(如物品类别、价格、品牌等)。
这里有一个关键的发现:当我们为同一个用户推荐不同物品时,用户特征是固定不变的。因此,用户特征内部的交互得分(无论是一阶还是二阶)对所有候选物品都是相同的。既然这部分得分相同,在排序时就可以忽略,我们只需要关注: 1. 物品特征内部的交互得分 2. 用户特征与物品特征之间的交互得分
基于这个思路,我们可以将FM的二阶交互项重新组织:
基于前面的分析,我们发现用户特征内部的交互项对所有候选物品都相同,因此可以在召回阶段忽略。这样就可以将FM重新组织,只保留对排序有影响的部分:
观察上面的公式,我们发现一个重要的数学结构:最后一项\(\sum_{f=1}^{k}\left( {\sum_{u \in U} v_{u, f} x_{u}}{\sum_{t \in I} v_{t, f} x_{t}} \right)\)实际上是两个向量\(\sum_{u \in U} v_{u} x_{u}\)和\(\sum_{t \in I} v_{t} x_{t}\)的内积。这启发我们将整个匹配分数重新组织为双塔结构:
通过这种重新组织,我们得到了FM的双塔表示:
用户向量:\(V_{user} = [1; \sum_{u \in U} v_{u} x_{u}]\)
物品向量:\(V_{item} = [\sum_{t \in I} w_{t} x_{t} + \frac{1}{2} \sum_{f=1}^{k}((\sum_{t \in I} v_{t, f} x_{t})^{2} - \sum_{t \in I} v_{t, f}^{2} x_{t}^{2}); \sum_{t \in I} v_{t} x_{t}]\)
这里的设计很巧妙:用户向量包含一个常数项1和用户特征的聚合表示,物品向量则包含物品的内部交互信息和物品特征的聚合表示。两个向量通过内积运算,既能捕捉用户-物品之间的交互,又保留了物品内部特征的复杂关系。
这样的分解揭示了一个重要原理:即使是复杂的特征交互模式,也可以通过合适的向量表示和简单的内积运算来实现。
2.3.1.3. 代码实践¶
FM召回的双塔实现关键在于如何将数学推导转化为实际的向量表示。用户塔构建了包含常数项和特征聚合的向量:
# 用户塔:V_user = [1; ∑(v_u * x_u)]
user_concat = Concatenate(axis=1)(user_embeddings) # [batch_size, num_user_features, embedding_dim]
user_embedding_sum = SumPooling()(user_concat) # [batch_size, embedding_dim]
# 构建用户向量:[1; ∑(v_u * x_u)]
ones_vector = OnesLayer()(user_embedding_sum) # [batch_size, 1]
user_vector = Concatenate(axis=1)([ones_vector, user_embedding_sum])
物品塔则更为复杂,需要计算一阶线性项和FM交互项:
# 物品塔:V_item = [∑w_t*x_t + FM_interaction; ∑(v_t * x_t)]
item_concat = Concatenate(axis=1)(item_embeddings) # [batch_size, num_item_features, embedding_dim]
item_embedding_sum = SumPooling()(item_concat) # [batch_size, embedding_dim]
# 计算一阶线性项:∑(w_t * x_t)
item_linear_weights = Dense(1, use_bias=False)(item_embedding_sum)
# 计算FM二阶交互项:0.5 * ((∑v_t*x_t)² - ∑(v_t²*x_t²))
sum_squared = SquareLayer()(item_embedding_sum)
item_squared = SquareLayer()(item_concat)
squared_sum = SumPooling()(item_squared)
fm_interaction_vector = Subtract()([sum_squared, squared_sum])
fm_interaction_scalar = SumScalarLayer()(ScaleLayer(0.5)(fm_interaction_vector))
# 组合为物品向量
first_term = Add()([item_linear_weights, fm_interaction_scalar])
item_vector = Concatenate(axis=1)([first_term, item_embedding_sum])
最终通过内积计算匹配分数:fm_score = Dot(axes=1)([item_vector, user_vector])。这种设计使得物品向量可以离线预计算,用户向量实时计算,从而支持高效的召回检索。
下面训练FM召回模型并评估效果。
from funrec import run_experiment
run_experiment('fm_recall')