动手学深度学习 英文版 推荐系统部分 pytorch实现

代码实现&&md笔记:

https://github.com/SNIKCHS/d2l_RecSys_pytorch

1.相关概念

1.1协同过滤Collaborative Filtering

协同过滤算法基于一个基础的强预设:在观测到用户消费过条目A之后,有很高的可能性观测到用户会喜欢与A相似的条目B(Item CF)以及相似的用户可能喜欢同一个条目。所以协同过滤的核心在于描述条目和用户的相似度。

相似度有很多种计算方式,最常用的就是欧式距离,和余弦相似度。

1.2显式反馈和隐式反馈

显性反馈行为:用户明确表示对物品喜好的行为。

隐性反馈行为:不能明确反映用户喜好的行为。

2.矩阵分解Matrix Factorization

矩阵分解是一类协同过滤模型。该模型将user-item交互矩阵(如评分矩阵)分解为两个低秩矩阵的乘积,从而捕获user-item交互的低秩结构。

R ∈ R m × n \mathbf{R} \in \mathbb{R}^{m \times n} RRm×n表示 m m m个user和 n n n个item的交互矩阵, R \mathbf{R} R代表明确的评分,用user-item交互矩阵将被分解为用户潜在矩阵 P ∈ R m × k \mathbf{P} \in \mathbb{R}^{m \times k} PRm×k和item潜在矩阵 Q ∈ R n × k \mathbf{Q} \in \mathbb{R}^{n \times k} QRn×k,其中 k ≪ m , n k \ll m, n km,n 是潜在因子大小。 p u \mathbf{p}_u pu P \mathbf{P} P u u u行, q i \mathbf{q}_i qi Q \mathbf{Q} Q i i i行。对一个给定的item i i i q i \mathbf{q}_i qi的元素代表 i i i拥有的特征的程度大小(如电影的类型和语言)。对一个给定的用户 u u u p u \mathbf{p}_u pu的元素代表用户在items相应特征上的兴趣大小。这些潜在因子可能代表显式的特征(如电影的类型和语言),也可能是无法解释的。预测的评分可以估计为:

R ^ = P Q ⊤ \hat{\mathbf{R}} = \mathbf{PQ}^\top R^=PQ

其中 R ^ ∈ R m × n \hat{\mathbf{R}}\in \mathbb{R}^{m \times n} R^Rm×n是预测的评分矩阵,与 R \mathbf{R} R大小相同。该预测规则的一个主要问题是无法对 users/items 偏差进行建模。例如,一些用户倾向于给出更高的评分,或者某些item由于质量较差而总是得到较低的评分。这些偏差在实际应用中很常见。为了捕捉这些偏差,引入了用户特定和item 特定的偏差项。具体来说,用户 u u u对item i i i 的预测评分由下式计算:

R ^ u i = p u q i ⊤ + b u + b i \hat{\mathbf{R}}_{ui} = \mathbf{p}_u\mathbf{q}^\top_i + b_u + b_i R^ui=puqi+bu+bi

然后通过最小化预测评分和实际评分之间的均方误差来训练矩阵分解模型。目标函数定义如下:

a r g m i n P , Q , b ∑ ( u , i ) ∈ K ∥ R u i − R ^ u i ∥ 2 + λ ( ∥ P ∥ F 2 + ∥ Q ∥ F 2 + b u 2 + b i 2 ) \underset{\mathbf{P}, \mathbf{Q}, b}{\mathrm{argmin}} \sum_{(u, i) \in \mathcal{K}} \| \mathbf{R}_{ui} - \hat{\mathbf{R}}_{ui} \|^2 + \lambda (\| \mathbf{P} \|^2_F + \| \mathbf{Q} \|^2_F + b_u^2 + b_i^2 ) P,Q,bargmin(u,i)KRuiR^ui2+λ(PF2+QF2+bu2+bi2)

λ \lambda λ表示正则化率,正则项 λ ( ∥ P ∥ F 2 + ∥ Q ∥ F 2 + b u 2 + b i 2 ) \lambda (\| \mathbf{P} \|^2_F + \| \mathbf{Q} \|^2_F + b_u^2 + b_i^2 ) λ(PF2+QF2+bu2+bi2)通过对参数大小惩罚用来避免过拟合。已知的 R u i \mathbf{R}_{ui} Rui ( u , i ) (u, i) (u,i)对被存在集合 K = { ( u , i ) ∣ R u i  is known } \mathcal{K}=\{(u, i) \mid \mathbf{R}_{ui} \text{ is known}\} K={(u,i)Rui is known}。模型参数可以通过优化算法来学习,例如随机梯度下降和 Adam。 矩阵分解模型的直观说明如下所示:

../_images/rec-mf.svg

Q ⊤ \mathbf{Q}^\top Q实际上就是n部电影的k维embedding, P \mathbf{P} P就是m个用户的k维embedding

2.1模型实现

class MF(nn.Module):
    def __init__(self, num_factors, num_users, num_items):
        super(MF, self).__init__()
        self.num_users = num_users
        self.num_items = num_items
        self.P = nn.Embedding(num_embeddings=num_users, embedding_dim=num_factors)
        self.Q = nn.Embedding(num_embeddings=num_items, embedding_dim=num_factors)
        self.user_bias = nn.Embedding(num_users, 1)
        self.item_bias = nn.Embedding(num_items, 1)

    def forward(self, user_id, item_id):
        P_u = self.P(user_id) #(b,num_factors)
        Q_i = self.Q(item_id) #(b,num_factors)
        b_u = self.user_bias(user_id)
        b_i = self.item_bias(item_id)
        outputs = (P_u * Q_i).sum(axis=1) + b_u.squeeze() + b_i.squeeze()
        return outputs.flatten()

2.2评价方法

RMSE(均方根误差)方法,该方法通常用于衡量模型预测的评分与实际观察到的评分(基本事实)之间的差异。RMSE 定义为:

R M S E = 1 ∣ T ∣ ∑ ( u , i ) ∈ T ( R u i − R ^ u i ) 2 \mathrm{RMSE} = \sqrt{\frac{1}{|\mathcal{T}|}\sum_{(u, i) \in \mathcal{T}}(\mathbf{R}_{ui} -\hat{\mathbf{R}}_{ui})^2} RMSE=T1(u,i)T(RuiR^ui)2

其中 ∣ T ∣ |\mathcal{T}| T是要评估的 ( u , i ) (u, i) (u,i)对的集合大小,

def RMSELoss(yhat,y):
    return torch.sqrt(torch.mean((yhat-y)**2))

3.AutoRec:使用自动编码器进行评分预测

尽管矩阵分解(MF)模型在评分预测任务上取得了不错的表现,但其本质上是一个线性模型,无法捕获复杂非线性关系。

本节介绍一种非线性神经网络协同过滤模型[AutoRec](Sedhain, S., Menon, A. K., Sanner, S., & Xie, L. (2015). Autorec: autoencoders meet collaborative filtering. Proceedings of the 24th International Conference on World Wide Web (pp. 111–112).)。它使用自编码器autoencoder架构,旨在基于显式反馈将非线性变换集成到 CF 中。神经网络已被证明能够逼近任何连续函数,使其适合解决矩阵分解的局限性,丰富矩阵分解的表达能力。

一方面,AutoRec 具有与autoencoder相同的结构,由输入层、隐藏层和重建(输出)层组成。自编码器是一种神经网络,它学习将其输入用中间隐藏状态(通常是低维)表示,并尽可能在输出层根据隐藏状态恢复其原始输入

【全】一文带你了解自编码器(AutoEncoder)

在 AutoRec 中,它不是将user/item显式嵌入到低维空间中,而是使用交互矩阵的列/行作为输入,然后在输出层重构交互矩阵。

另一方面,AutoRec 与传统的自动编码器不同:AutoRec 不是学习隐藏表示,而是专注于学习/重建输出层。它使用部分观察到的交互矩阵作为输入,旨在重建一个完整的评分矩阵。同时,输入的缺失条目通过重构填充到输出层,以达到推荐的目的。

AutoRec 有两种变体:基于user的和基于item的。为简洁起见,这里只介绍基于item的 AutoRec。可以相应地导出基于user的 AutoRec。

3.1模型

R ∗ i \mathbf{R}_{*i} Ri为评分矩阵的 i t h i^\mathrm{th} ith列,未知评分默认置零,神经网络结构定义为:

h ( R ∗ i ) = f ( W ⋅ g ( V R ∗ i + μ ) + b ) h(\mathbf{R}_{*i}) = f(\mathbf{W} \cdot g(\mathbf{V} \mathbf{R}_{*i} + \mu) + b) h(Ri)=f(Wg(VRi+μ)+b)

上式中 f ( ⋅ ) f(\cdot) f() g ( ⋅ ) g(\cdot) g()代表激活函数, W \mathbf{W} W V \mathbf{V} V为权重矩阵, μ \mu μ b b b为偏差。 h ( ⋅ ) h( \cdot ) h()为AutoRec的整个网络。 h ( R ∗ i ) h(\mathbf{R}_{*i}) h(Ri)的输出为评分矩阵 i t h i^\mathrm{th} ith列的重建。

以下目标函数旨在最小化重建误差:

a r g m i n W , V , μ , b ∑ i = 1 M ∥ R ∗ i − h ( R ∗ i ) ∥ O 2 + λ ( ∥ W ∥ F 2 + ∥ V ∥ F 2 ) \underset{\mathbf{W},\mathbf{V},\mu, b}{\mathrm{argmin}} \sum_{i=1}^M{\parallel \mathbf{R}_{*i} - h(\mathbf{R}_{*i})\parallel_{\mathcal{O}}^2} +\lambda(\| \mathbf{W} \|_F^2 + \| \mathbf{V}\|_F^2) W,V,μ,bargmini=1MRih(Ri)O2+λ(WF2+VF2)

上式中 ∥ ⋅ ∥ O \| \cdot \|_{\mathcal{O}} O意为只考虑观察到的评分的贡献,也就是在反向传播期间只更新与观察到的输入相关的权重

class AutoRec(nn.Module):
    def __init__(self, num_hidden, num_users, dropout=0.05,mode = 'train'):
        super(AutoRec, self).__init__()
        self.encoder = nn.Sequential(
            nn.Linear(num_users,num_hidden),
            nn.Sigmoid()
        )
        self.decoder = nn.Linear(num_hidden,num_users)
        self.dropout = nn.Dropout(dropout)
        self.mode = mode

    def forward(self, X):
        hidden = self.dropout(self.encoder(X)) #(b,num_users) ->(b,num_hidden)
        y = self.decoder(hidden) #(b,num_hidden) ->(b,num_users)
        if self.mode=='train':  # Mask the gradient during training
            return y * torch.sign(X) 
        # torch.sign(X):-1 if x < 0, 0 if x==0, 1 if x > 0. 
        # 交互矩阵中未标注的数据为0,用来屏蔽未标注的输入的梯度对模型的影响(否则就会向0靠近)
        else:
            return y

4.推荐系统的个性化排序

前几节只考虑了显式反馈,并根据观察到的评级对模型进行了训练和测试。这种方法有两个缺点:首先,大多数反馈在现实世界的场景中不是显式而是隐式的,而且显式反馈的收集成本可能更高。其次,可以预测用户兴趣的未观察到的user-item对被完全忽略,这使得这些方法不适用于评级不是随机缺失而是因为用户偏好的情况。未观察到的user-item对是真实负反馈(用户对item不感兴趣)和缺失值(用户将来可能与item交互)的混合体。我们简单地忽略了矩阵分解和 AutoRec 中的未观察到的对。显然这些模型无法区分观察到的和未观察到的对,并且通常不适合个性化排名任务。

为此,一类旨在从隐式反馈中生成排名推荐列表的推荐模型得到了普及。一般来说,个性化排名模型可以通过逐点、逐对或逐列表方法进行优化。逐点方法一次考虑单个交互,并训练分类器或回归器来预测个人偏好。矩阵分解和 AutoRec 使用逐点目标进行优化。逐对方法为每个用户考虑一对item ,并旨在近似该对的最佳排序。通常,逐对方法更适合排序任务,因为预测相对顺序让人联想到排序的本质。 Listwise 方法近似于整个item 列表的排序,例如,直接优化归一化折扣累积增益 (NDCG) 等排名措施。但是,listwise 方法比逐点或逐对方法更复杂且计算密集 。 在本节中,我们将介绍两个逐对的目标/损失,贝叶斯个性化排序损失和合页损失,以及它们各自的实现。

4.1贝叶斯个性化排序损失及其实现

贝叶斯个性化排序算法介绍

贝叶斯个性化排序算法和MF类似,同样是找到合适的用户和item的嵌入矩阵 P \mathbf{P} P Q \mathbf{Q} Q,但是使用BPR损失更新参数

贝叶斯个性化排序 (BPR) 是从最大后验估计量导出的成对个性化排序损失。它已广泛用于许多现有的推荐模型中。 BPR 的训练数据由正负对(缺失值)组成。它假设用户更喜欢正面item而不是所有其他未观察到的item。

设训练数据由 ( u , i , j ) (u, i, j) (u,i,j)形式的元组构成,表示用户 u u u更喜欢item i i i而不是item j j j。下面给出了旨在最大化后验概率的 BPR 贝叶斯公式,对每—个用户u而言,后验概率正比于似然概率乘上先验概率::

p ( Θ ∣ > u ) ∝ p ( > u ∣ Θ ) p ( Θ ) p(\Theta \mid >_u ) \propto p(>_u \mid \Theta) p(\Theta) p(Θ>u)p(>uΘ)p(Θ)

其中 Θ \Theta Θ代表任意推荐模型的参数,$>u 表 示 用 户 表示用户 u$的所有item 所需的个性化总排名。我们可以制定最大后验估计量来推导出个性化排名任务的通用优化标准。

KaTeX parse error: No such environment: split at position 7: \begin{̲s̲p̲l̲i̲t̲}̲\begin{aligned}…

式中 D : = { ( u , i , j ) ∣ i ∈ I u + ∧ j ∈ I \ I u + } D := \{(u, i, j) \mid i \in I^+_u \wedge j \in I \backslash I^+_u \} D:={(u,i,j)iIu+jI\Iu+}为训练集, I u + I^+_u Iu+为用户 u u u喜欢的items, I I I为所有的items, I \ I u + I \backslash I^+_u I\Iu+ I I I中除了 I u + I^+_u Iu+以外的items。 y ^ u i \hat{y}_{ui} y^ui y ^ u j \hat{y}_{uj} y^uj为用户 u u u对item i i i j j j的预测分数。 p ( Θ ) p(\Theta) p(Θ)是零均值且协方差矩阵为 Σ Θ \Sigma_\Theta ΣΘ的正态分布。假设 Σ Θ = λ Θ I \Sigma_\Theta = \lambda_\Theta I ΣΘ=λΘI

loss = - np.sum(np.log(npx.sigmoid(positive - negative)), 0, keepdims=True)

4.2 合页损失Hinge Loss

用于排序的Hinge Loss与SVM 等分类器使用的Hinge Loss具有不同的形式。推荐系统中用于排序的损失具有以下形式:

∑ ( u , i , j ∈ D ) max ⁡ ( m − y ^ u i + y ^ u j , 0 ) \sum_{(u, i, j \in D)} \max( m - \hat{y}_{ui} + \hat{y}_{uj}, 0) (u,i,jD)max(my^ui+y^uj,0)

上式中, m m m是safety margin 大小。它旨在将消极item推离积极item。与 BPR 类似,它旨在优化正负样本之间的相关距离,而不是绝对输出,使其非常适合推荐系统。

distances = positive - negative
loss = np.sum(np.maximum(- distances + margin, 0))

5.用于个性化排序的神经协同过滤

本节介绍用于隐式反馈推荐的神经协同过滤 (NCF) 框架。隐式反馈在推荐系统中无处不在。点击、购买和观看等行为是常见的隐式反馈,很容易收集并表明用户的偏好。我们将介绍的模型名为 NeuMF ,是神经矩阵分解的缩写,旨在通过隐式反馈解决个性化排序任务。该模型利用神经网络的灵活性和非线性来代替矩阵分解的点积,旨在增强模型的表达能力。具体来说,该模型由两个子网络构成,包括广义矩阵分解 (GMF) 和 MLP,并对来自两个路径的交互进行建模,而不是简单的点积。这两个网络的输出被连接起来用于最终的预测分数计算。与评分预测不同 在 AutoRec 任务中,该模型根据隐式反馈为每个用户生成一个排序推荐列表。 我们将使用上一节介绍的个性化排序损失来训练这个模型。

5.1 NeuMF 模型

NeuMF 融合了两个子网络,GMF是矩阵分解的通用神经网络版本,其中输入是用户和item潜在因子的元素乘积。它由两个神经层组成:

KaTeX parse error: No such environment: split at position 7: \begin{̲s̲p̲l̲i̲t̲}̲\mathbf{x} = \m…

其中 ⊙ \odot 表示向量的 Hadamard 积(对应元素做乘法)。 P ∈ R m × k \mathbf{P} \in \mathbb{R}^{m \times k} PRm×k Q ∈ R n × k \mathbf{Q} \in \mathbb{R}^{n \times k} QRn×k分别对应于用户和项目的潜在矩阵。 p u ∈ R k \mathbf{p}_u \in \mathbb{R}^{ k} puRk P \mathbf{P} P的第 u 行, q i ∈ R k \mathbf{q}_i \in \mathbb{R}^{ k} qiRk Q \mathbf{Q} Q的第 i 行。 α \alpha α h h h表示输出层的激活函数和权重。 y ^ u i \hat{y}_{ui} y^ui是用户 u 可能给item i 的预测分数。

该模型的另一个组成部分是 MLP。为了丰富模型的灵活性,MLP 子网不与 GMF 共享用户和item嵌入。它使用用户和item嵌入的concat作为输入。通过复杂的连接和非线性变换,它能够估计用户和项目之间复杂的交互。MLP 子网定义为:

KaTeX parse error: No such environment: split at position 7: \begin{̲s̲p̲l̲i̲t̲}̲\begin{aligned}…

其中 W ∗ , b ∗ \mathbf{W}^*, \mathbf{b}^* W,b α ∗ \alpha^* α为权重矩阵,偏差向量和激活函数, ϕ ∗ \phi^* ϕ为相应层的函数, z ∗ \mathbf{z}^* z为相应层的输出

为了融合 GMF 和 MLP 的结果,NeuMF 不进行简单的加法,而是concat两个子网络的倒数第二层,以创建一个特征向量,该向量可以传递给其他层。之后,输出用矩阵 h \mathbf{h} h和 sigmoid 激活函数进行投影。预测层公式为:

y ^ u i = σ ( h ⊤ [ x , ϕ L ( z ( L − 1 ) ) ] ) . \hat{y}_{ui} = \sigma(\mathbf{h}^\top[\mathbf{x}, \phi^L(z^{(L-1)})]). y^ui=σ(h[x,ϕL(z(L1))]).

../_images/rec-neumf.svg
class NeuMF(nn.Module):
    def __init__(self, num_factors, num_users, num_items, nums_hiddens):
        super(NeuMF, self).__init__()
        self.P = nn.Embedding(num_users, num_factors)
        self.Q = nn.Embedding(num_items, num_factors)
        self.U = nn.Embedding(num_users, num_factors)
        self.V = nn.Embedding(num_items, num_factors)
        self.mlp = nn.Sequential()
        pre_num_hiddens = num_factors*2
        for i in range(0,len(nums_hiddens)):
            self.mlp.add_module('mlp'+str(i),
                nn.Sequential(
                    nn.Linear(pre_num_hiddens,nums_hiddens[i]),
                    nn.ReLU()
                )
            )
            pre_num_hiddens = nums_hiddens[i]
        self.prediction_layer = nn.Sequential(
            nn.Linear(pre_num_hiddens+num_factors,1,bias=False),
            nn.Sigmoid()
        )

    def forward(self, user_id, item_id):
        p_mf = self.P(user_id)
        q_mf = self.Q(item_id)
        gmf = p_mf * q_mf
        p_mlp = self.U(user_id) # (b,1)->(b,num_factors)
        q_mlp = self.V(item_id) # (b,1)->(b,num_factors)
        mlp = self.mlp(torch.concat([p_mlp, q_mlp], dim=1)) # (b,num_factors*2)->(b,last_num_hiddens)
        con_res = torch.concat([gmf, mlp], dim=1) # (b,last_num_hiddens+num_factors)
        return self.prediction_layer(con_res) # (b,last_num_hiddens+num_factors)->(b,1)

5.2 评价函数

本节采用按时间分割的策略来构建训练集和测试集。两个评估指标包括给定截断 ℓ \ell 时的命中率 ( Hit @ ℓ ) (\text{Hit}@\ell) (Hit@)和 ROC 曲线下面积 (AUC) 用于评估模型的有效性。每个用户在给定位置 ℓ \ell 的命中率表示推荐项目是否包含在前 ℓ 排名列表中。正式定义如下:

Hit @ ℓ = 1 m ∑ u ∈ U 1 ( r a n k u , g u < = ℓ ) \text{Hit}@\ell = \frac{1}{m} \sum_{u \in \mathcal{U}} \textbf{1}(rank_{u, g_u} <= \ell) Hit@=m1uU1(ranku,gu<=)

其中 1 \textbf{1} 1表示一个指示函数,如果 ground truth item 排在列表的前 ℓ 部分中则等于 1,否则等于 0。 r a n k u , g u rank_{u, g_u} ranku,gu表示用户u的ground truth item g u g_u gu在推荐列表中的排名(理想排名为1)。 m m m是用户数。 U \mathcal{U} U是用户集。

AUC的定义如下:

AUC = 1 m ∑ u ∈ U 1 ∣ I \ S u ∣ ∑ j ∈ I \ S u 1 ( r a n k u , g u < r a n k u , j ) , \text{AUC} = \frac{1}{m} \sum_{u \in \mathcal{U}} \frac{1}{|\mathcal{I} \backslash S_u|} \sum_{j \in I \backslash S_u} \textbf{1}(rank_{u, g_u} < rank_{u, j}), AUC=m1uUI\Su1jI\Su1(ranku,gu<ranku,j),

其中 I \mathcal{I} I是项目集。 S u S_u Su是用户 u u u的候选项目。也可以使用许多其他评价指标,如精度、召回和归一化折扣累积增益 (NDCG)。

6.序列感知推荐系统

在前面的部分中,我们将推荐任务抽象为矩阵补全问题,而不考虑用户的短期行为。在本节中,我们将介绍一种将用户交互日志按顺序排列的推荐模型。它是一个序列感知推荐器,其中输入是过去用户操作的有序且通常带有时间戳的列表。

将介绍的模型Caser是卷积序列嵌入推荐模型的缩写,采用卷积神经网络捕捉用户近期活动的动态模式影响。 Caser 的主要组件由水平卷积网络和垂直卷积网络组成,旨在分别揭示联合级(union-level)和点级序列模式。点级模式表示历史序列中单个项目对目标项目的影响,而联合级模式则表示之前的几个动作对后续目标的影响。例如,同时购买牛奶和黄油会导致购买面粉的可能性高于仅购买其中一种。此外,用户的一般兴趣或长期偏好也在最后的全连接层中建模,从而对用户兴趣进行更全面的建模。

6.1模型架构

在序列感知推荐系统中,每个用户都与项目集中的一些项目序列相关联。以 S u = ( S 1 u , . . . S ∣ S u ∣ u ) S^u = (S_1^u, ... S_{|S_u|}^u) Su=(S1u,...SSuu)表示有序序列。 Caser 的目标是通过考虑用户的长期偏好和短期兴趣来推荐项目。假设我们考虑前面的 L L L项,可以构造一个表示前t个时间步的交互的嵌入矩阵:

E ( u , t ) = [ q S t − L u , . . . , q S t − 2 u , q S t − 1 u ] ⊤ , \mathbf{E}^{(u, t)} = [ \mathbf{q}_{S_{t-L}^u} , ..., \mathbf{q}_{S_{t-2}^u}, \mathbf{q}_{S_{t-1}^u} ]^\top, E(u,t)=[qStLu,...,qSt2u,qSt1u],

其中 Q ∈ R n × k \mathbf{Q} \in \mathbb{R}^{n \times k} QRn×k代表item的嵌入矩阵, q i \mathbf{q}_i qi代表第i行。 E ( u , t ) ∈ R L × k \mathbf{E}^{(u, t)} \in \mathbb{R}^{L \times k} E(u,t)RL×k被用来推断用户u在t时间步时的短期兴趣。我们可以将输入的矩阵 E ( u , t ) \mathbf{E}^{(u, t)} E(u,t)看作一个图片,作为后面两个卷积部分的输入。

水平卷积层有 d d d个水平卷积核 F j ∈ R h × k , 1 ≤ j ≤ d , h = { 1 , . . . , L } \mathbf{F}^j \in \mathbb{R}^{h \times k}, 1 \leq j \leq d, h = \{1, ..., L\} FjRh×k,1jd,h={1,...,L},垂直卷积层有 d ′ d' d个垂直卷积核 G j ∈ R L × 1 , 1 ≤ j ≤ d ′ \mathbf{G}^j \in \mathbb{R}^{ L \times 1}, 1 \leq j \leq d' GjRL×1,1jd,经过一系列卷积和池化操作,得到两个输出:

KaTeX parse error: No such environment: split at position 7: \begin{̲s̲p̲l̲i̲t̲}̲\mathbf{o} = \t…

o ∈ R d \mathbf{o} \in \mathbb{R}^d oRd是水平卷积网络的输出, o ′ ∈ R k d ′ \mathbf{o}' \in \mathbb{R}^{kd'} oRkd是垂直卷积层的输出。它们被连接起来(concat)并送到一个全连接层,以获得更多高级表示。

z = ϕ ( W [ o , o ′ ] ⊤ + b ) , \mathbf{z} = \phi(\mathbf{W}[\mathbf{o}, \mathbf{o}']^\top + \mathbf{b}), z=ϕ(W[o,o]+b),

W ∈ R k × ( d + k d ′ ) \mathbf{W} \in \mathbb{R}^{k \times (d + kd')} WRk×(d+kd)是权重矩阵, b ∈ R k \mathbf{b} \in \mathbb{R}^k bRk为偏差,向量 z ∈ R k \mathbf{z} \in \mathbb{R}^k zRk是用户短期兴趣的表示。

最后,预测函数将用户的短期兴趣和长期偏好结合在一起,定义为:

y ^ u i t = v i ⋅ [ z , p u ] ⊤ + b i ′ , \hat{y}_{uit} = \mathbf{v}_i \cdot [\mathbf{z}, \mathbf{p}_u]^\top + \mathbf{b}'_i, y^uit=vi[z,pu]+bi,

V ∈ R n × 2 k \mathbf{V} \in \mathbb{R}^{n \times 2k} VRn×2k是另一个item的嵌入矩阵, b ′ ∈ R n \mathbf{b}' \in \mathbb{R}^n bRn是这个item的特定偏差, P ∈ R m × k \mathbf{P} \in \mathbb{R}^{m \times k} PRm×k是用户长期偏好的嵌入矩阵。 p u ∈ R k \mathbf{p}_u \in \mathbb{R}^{ k} puRk P P P的第u行, v i ∈ R 2 k \mathbf{v}_i \in \mathbb{R}^{2k} viR2k V \mathbf{V} V的第i行。

该模型可以通过 BPR 或Hinge损失来学习。

../_images/rec-caser.svg

6.2带负采样的序列数据集

为了处理交互数据序列,需要重新实现 Dataset 类。以下代码创建一个名为 SeqDataset 的新数据集类。在每个样本中,它输出用户id,该用户前 L L L个交互项目作为一个序列,该用户下一个交互的项目作为目标。下图演示了一个用户的数据加载过程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KUko24A8-1653204194005)(https://raw.githubusercontent.com/SNIKCHS/MDImage/main/img/Illustration%20of%20the%20data%20generation%20process.svg)]

假设这个用户喜欢 9 部电影,按时间顺序排列这 9 部电影。最新的电影作为测试项目被排除在外。对于剩下的八部电影,我们可以获得三个训练样本,每个样本包含五个(L = 5)电影的序列及其后续项目作为目标项目。负样本也包含在自定义数据集中。

7.Feature-Rich推荐系统

交互数据是用户偏好和兴趣的最基本指示。它在以前引入的模型中起着至关重要的作用。然而,交互数据通常非常稀疏,有时可能有较多噪声。为了解决这个问题,我们可以将诸如项目特征、用户档案,甚至交互发生在哪个上下文中的辅助信息集成到推荐模型中。利用这些特征有助于提出建议,因为这些特征可以有效预测用户兴趣,尤其是在缺乏交互数据时。因此,推荐模型必须具备处理这些特征的能力,并赋予模型一些内容/上下文意识。为了演示这种类型的推荐模型,我们引入了另一个关于在线广告推荐点击率 (CTR) 的任务,并展示了匿名广告数据。针对性的广告服务受到广泛关注,通常被视为推荐引擎。 推荐符合用户个人品味和兴趣的广告对于提高点击率很重要。

营销人员使用在线广告向客户展示广告。点击率是衡量广告客户在每次展示次数的广告上获得的点击次数的指标,它表示为使用以下公式计算的百分比:

$\text{CTR} = \frac{#\text{Clicks}} {#\text{Impressions}} \times 100 % $

点击率是表示预测算法有效性的重要信号。点击率预测是一项预测网站上某些内容被点击的可能性的任务。 CTR 预测模型不仅可以用于有针对性的广告系统,还可以用于一般项目(例如电影、新闻、产品)推荐系统、电子邮件活动甚至搜索引擎。它还与用户满意度、转化率密切相关,有助于设定活动目标,因为它可以帮助广告商设定切合实际的期望。

7.1在线广告数据集

本节使用的数据集是一个在线广告数据集。它由 34 个字段组成,第一列代表目标变量,指示广告是否被点击 (1) 或未点击 (0)。所有其他列都是分类特征。这些列可能代表广告 ID、站点或应用程序 ID、设备 ID、时间、用户配置文件等。由于匿名化和隐私问题,这些特征的真实语义未公开。

该数据集有一个训练集和一个测试集,分别由 15000 和 3000 个样本组成。

#@save
class CTRDataset(Dataset):
    def __init__(self, data_path, feat_mapper=None, defaults=None,
                 min_threshold=4, num_feat=34):
        self.NUM_FEATS, self.count, self.data = num_feat, 0, {}
        feat_cnts = defaultdict(lambda: defaultdict(int)) #记录第i个特征有几种embedding,每个embedding出现次数
        self.feat_mapper, self.defaults = feat_mapper, defaults
        self.field_dims = np.zeros(self.NUM_FEATS, dtype=np.int64)
        with open(data_path) as f:
            for line in f:
                instance = {}
                values = line.rstrip('\n').split('\t')
                if len(values) != self.NUM_FEATS + 1:
                    continue
                label = np.float32([0, 0])
                label[int(values[0])] = 1
                instance['y'] = [np.float32(values[0])]
                for i in range(1, self.NUM_FEATS + 1):
                    feat_cnts[i][values[i]] += 1 #第i种feature的一种embedding的计数+1
                    instance.setdefault('x', []).append(values[i])
                self.data[self.count] = instance #{'y': [1.0], 'x': ['11417225884335159926',...,'631302449310544']}
                self.count = self.count + 1

        if self.feat_mapper is None and self.defaults is None:
            feat_mapper = {i: {feat for feat, c in cnt.items() if c >= min_threshold} for i, cnt in feat_cnts.items()}
            # 对feat_cnts的34种feature的每个embedding保存在feat_mapper,前提是其出现次数不小于min_threshold
            self.feat_mapper = {i: {feat_v: idx for idx, feat_v in enumerate(feat_values)}
                                for i, feat_values in feat_mapper.items()}
            # self.feat_mapper{key:1~34 ;value:DICT},DICT{key:第i种feature的某个embedding;value:其在feat_mapper的第i个dict内的index 范围:[0,len(feat_mapper[i])]}
            self.defaults = {i: len(feat_values) for i, feat_values in feat_mapper.items()}
            #记录第i种feature有多少种embedding
        for i, fm in self.feat_mapper.items():
            self.field_dims[i - 1] = len(fm) +1 #留出空间给默认值
        self.offsets = np.array((0, *np.cumsum(self.field_dims)[:-1]))
        # 偏置 使每个embedding有不同的代码 offsets[i]==len(self.feat_mapper[i-1])+1
        # 第i个feature:[offsets[i],offsets[i]+len(self.feat_mapper[i])-1]
        # 第i-1个feature不存在的embedding:offsets[i-1]+len(self.feat_mapper[i-1])==offsets[i]-1
    def __len__(self):
        return self.count

    def __getitem__(self, idx):
        feat = np.array([self.feat_mapper[i + 1].get(v, self.defaults[i + 1]) #如果不存在则返回self.defaults[i + 1]
                         for i, v in enumerate(self.data[idx]['x'])])
        return feat + self.offsets, self.data[idx]['y']

8.Factorization Machines(FM)

因式分解机 (FM) 由 Steffen Rendle 在 2010 年提出,是一种可用于分类、回归和排名任务的监督算法。它很快引起了人们的注意,并成为一种流行且有影响力的预测和建议方法。特别是,它是线性回归模型和矩阵分解模型MF的推广。此外,它让人想起具有多项式内核的支持向量机。分解机相对于线性回归和矩阵分解的优势在于:(1)它可以对 χ 路变量交互进行建模,其中 χ 是多项式阶数,通常设置为 2。 (2) 与因式分解机相关的快速优化算法可以将多项式计算时间减少到线性复杂度,使其非常有效,特别是对于高维稀疏输入。由于这些原因,因式分解机被广泛应用于现代广告和产品推荐。 技术细节和实现如下所述。

8.1 2路FM

x ∈ R d x \in \mathbb{R}^d xRd表示一个样本的特征向量,y 表示对应的标签,可以是实值标签或类标签,例如二元类“点击/非点击”。二次分解机的模型定义为:

y ^ ( x ) = w 0 + ∑ i = 1 d w i x i + ∑ i = 1 d ∑ j = i + 1 d ⟨ v i , v j ⟩ x i x j \hat{y}(x) = \mathbf{w}_0 + \sum_{i=1}^d \mathbf{w}_i x_i + \sum_{i=1}^d\sum_{j=i+1}^d \langle\mathbf{v}_i, \mathbf{v}_j\rangle x_i x_j y^(x)=w0+i=1dwixi+i=1dj=i+1dvi,vjxixj

其中 w 0 ∈ R \mathbf{w}_0 \in \mathbb{R} w0R是全局偏差; w i ∈ R d \mathbf{w_i} \in \mathbb{R}^d wiRd表示第 i 个变量的权重; V ∈ R d × k \mathbf{V} \in \mathbb{R}^{d\times k} VRd×k表示特征嵌入; v i \mathbf{v}_i vi代表 V \mathbf{V} V的第i行;k 是嵌入向量的维数; ⟨ ⋅ , ⋅ ⟩ \langle\cdot, \cdot \rangle ,是两个向量的点积。 ⟨ v i , v j ⟩ \langle \mathbf{v}_i, \mathbf{v}_j \rangle vi,vj建模了第i个特征和 第j个特征之间的交互 。一些特征交互很容易理解,因此可以由专家设计。然而,大多数其他特征交互都隐藏在数据中,难以识别。因此自动建模特征交互可以大大减少特征工程的工作量。很明显,前两项对应于线性回归模型,最后一项是矩阵分解模型的扩展。如果特征 i 代表一个项目,而特征 j 代表一个用户,则第三项正是用户和项目嵌入之间的点积。值得注意的是,FM 也可以泛化到更高阶(degree > 2)。然而,数值稳定性 可能会削弱泛化。

8.2一个有效的优化准则

以直接的方法优化分解机会导致 O ( k d 2 ) \mathcal{O}(kd^2) O(kd2)的复杂性 ,因为所有成对的交互都需要计算。为了解决这个效率低下的问题,我们可以重新组织FM的第三项,这可以大大降低计算成本,导致线性时间复杂度( O ( k d ) \mathcal{O}(kd) O(kd))。成对交互项的重新表述如下:

KaTeX parse error: No such environment: split at position 7: \begin{̲s̲p̲l̲i̲t̲}̲\begin{aligned}…

通过这种变形,模型的复杂性大大降低。此外,对于稀疏特征,只需要计算非零元素,使得整体复杂度与非零特征的数量成线性关系。 要学习 FM 模型,我们可以使用回归任务的 MSE 损失、分类任务的交叉熵损失和排名任务的 BPR 损失。随机梯度下降和 Adam 等标准优化器可用于优化。

8.3模型实现

class FM(nn.Module):
    def __init__(self, field_dims,in_features, num_factors):
        super(FM, self).__init__()
        num_inputs = int(sum(field_dims)) #34种特征,每种特征各有多少种类别的总和
        self.embedding = nn.Embedding(num_inputs, num_factors)
        self.fc = nn.Embedding(num_inputs, 1)
        self.linear_layer = nn.Linear(in_features,1,bias=True)

    def forward(self, x):
        # x.shape  [b,34]
        # self.embedding(x).shape [b,34, 20]
        # torch.sum(self.embedding(x), dim=1).shape [b,34]
        square_of_sum = torch.sum(self.embedding(x), dim=2) ** 2 #[b,34]
        sum_of_square = torch.sum(self.embedding(x) ** 2, dim=2) #[b,34]
        x = self.linear_layer(self.fc(x).sum(2)) + 0.5 * (square_of_sum - sum_of_square).sum(1,keepdims=True)
        x = torch.sigmoid(x)
        return x

9.Deep FM

学习有效的特征组合对于点击率预测任务的成功至关重要。FM模型以线性范式(例如,双线性交互)对特征交互进行建模。对于固有特征交叉结构通常非常复杂和非线性的实际数据,这通常是不够的。更糟糕的是,二阶特征交互在实践中通常用于FM。用FM对更高程度的特征组合进行建模在理论上是可能的,但由于数值不稳定性和高计算复杂性,通常不采用这种方法。 一种有效的解决方案是使用深度神经网络。深度神经网络在特征表示学习方面非常强大,并且有可能学习复杂的特征交互。因此,将深度神经网络集成到FM是很自然的。向FM添加非线性变换层使其具有 能够对低阶特征组合和高阶特征组合进行建模。 此外,来自输入的非线性固有结构也可以用深度神经网络捕获。 在本节中,我们将介绍一个名为深度分解机 (DeepFM) [Guo et al., 2017] 的代表性模型,它结合了 FM 和深度神经网络。

9.1模型结构

DeepFM 由一个 FM 组件和一个 deep 组件组成,它们集成在一个并行结构中。FM 组件与用于对低阶特征交互进行建模的 2 路分解机相同。深度组件是一个MLP,用于捕获高阶特征交互和非线性。这两个组件共享相同的输入/嵌入,它们的输出总结为最终预测。值得指出的是,DeepFM 的精神类似于 Wide & Deep 架构的精神,既能记忆又能泛化。 DeepFM 相对于 Wide & Deep 模型的优势在于它通过自动识别特征组合来减少手工特征工程的工作量。

为简洁起见,我们省略了 FM 组件的描述,并将输出表示为 y ^ ( F M ) \hat{y}^{(FM)} y^(FM)。令 e i ∈ R k \mathbf{e}_i \in \mathbb{R}^{k} eiRk 表示第 i 个字段的潜在特征向量。深度组件的输入是使用稀疏分类特征输入查找的所有字段的密集嵌入的concat,表示为:

z ( 0 ) = [ e 1 , e 2 , . . . , e f ] , \mathbf{z}^{(0)} = [\mathbf{e}_1, \mathbf{e}_2, ..., \mathbf{e}_f], z(0)=[e1,e2,...,ef],

其中 f f f是字段数。然后将其输入以下神经网络:

z ( l ) = α ( W ( l ) z ( l − 1 ) + b ( l ) ) , \mathbf{z}^{(l)} = \alpha(\mathbf{W}^{(l)}\mathbf{z}^{(l-1)} + \mathbf{b}^{(l)}), z(l)=α(W(l)z(l1)+b(l)),

其中 α \alpha α是激活函数。 W l \mathbf{W}_{l} Wl b l \mathbf{b}_{l} bl 是第 l l l层的权重和偏差。让 y D N N y_{DNN} yDNN表示预测的输出。 DeepFM 的最终预测是 FM 和 DNN 输出的总和。所以我们有:

y ^ = σ ( y ^ ( F M ) + y ^ ( D N N ) ) , \hat{y} = \sigma(\hat{y}^{(FM)} + \hat{y}^{(DNN)}), y^=σ(y^(FM)+y^(DNN)),

其中 σ \sigma σ是 sigmoid 函数。 DeepFM 的架构如下图所示。

Illustration of the DeepFM model
class DeepFM(nn.Module):
    def __init__(self, field_dims, num_factors, mlp_dims, drop_rate=0.1):
        super(DeepFM, self).__init__()
        num_inputs = int(sum(field_dims))
        self.embedding = nn.Embedding(num_inputs, num_factors)
        self.fc = nn.Embedding(num_inputs, 1)
        self.linear_layer = nn.Linear(len(field_dims),1, bias=True)
        self.mlp = nn.Sequential()
        pre_input_dim = self.embed_output_dim =len(field_dims)*num_factors
        for i in range(len(mlp_dims)):
            self.mlp.add_module('mlp'+str(i),nn.Sequential(
                nn.Linear(pre_input_dim,mlp_dims[i]),
                nn.ReLU(),
                nn.Dropout(p=drop_rate)
            ))
            pre_input_dim = mlp_dims[i]

        self.mlp.add_module('mlp'+str(len(mlp_dims)),nn.Linear(pre_input_dim,1))

    def forward(self, x):
        embed_x = self.embedding(x)
        square_of_sum = torch.sum(embed_x, dim=2) ** 2
        sum_of_square = torch.sum(embed_x ** 2, dim=2)
        inputs = torch.reshape(embed_x, (-1, self.embed_output_dim)) # 展平
        x = self.linear_layer(self.fc(x).sum(2)) + 0.5 * (square_of_sum - sum_of_square).sum(1, keepdims=True)
            + self.mlp(inputs)
        x = torch.sigmoid_(x)
        return x
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值