第 7 章 双线性模型

从本章开始,我们介绍参数化模型中的非线性模型。在前几章中,我们介绍了线性回归与逻辑斯谛回归模型。这两个模型都有一个共同的特征:包含线性预测因子。将该因子看作的函数,如果输入变为原来的倍,那么输出为,也变成原来的 倍。在第 6 章的扩展阅读中,我们将这一类模型都归为广义线性模型。然而,此类模型所做的线性假设在许多任务上并不适用,我们需要其他参数假设来导出更合适的模型。本章首先讲解在推荐系统领域很常用的双线性模型(bilinear model)。

双线性模型虽然名称中包含“线性模型”,但并不属于线性模型或广义线性模型,其正确的理解应当是“双线性”模型。在数学中,双线性的含义为,二元函数固定任意一个自变量时,函数关于另一个自变量线性。具体来说,二元函数是双线性函数,当且仅当对任意都有:

最简单的双线性函数的例子是向量内积,我们按定义验证前两条性质:

后两条性质由对称性,显然也是成立的。而向量的加法就不是双线性函数。虽然加法满足第 1、3 条性质,但对第 2 条,如果,则有:

与线性模型类似,双线性模型并非指模型整体具有双线性性质,而是指其包含双线性因子。该特性赋予模型拟合一些非线性数据模式的能力,从而得到更加精准预测性能。接下来,我们以推荐系统场景为例,介绍两个基础的双线性模型:矩阵分解模型和因子分解机。

7.1 矩阵分解

矩阵分解(matrix factorization,MF)是推荐系统中评分预测(rating prediction)的常用模型,其任务为根据用户和商品已有的评分来预测用户对其他商品的评分。为了更清晰地解释 MF 模型的任务场景,我们以用户对电影的评分为例进行详细说明。如图 7-1 所示,设想有 个用户和 部电影,每个用户对一些电影按自己的喜好给出了评分。现在,我们的目标是需要为用户从他没有看过的电影中,向他推荐几部他最有可能喜欢看的电影。理想情况下,如果这个用户对所有电影都给出了评分,那么这个任务就变为从已有评分的电影中进行推荐——直接按照用户打分的高低排序。但实际情况下,在浩如烟海的电影中,用户一般只对很小一部分电影做了评价。因此,我们需要从用户已经做出的评价中推测用户为其他电影的打分,再将电影按推测的打分排序,从中选出最高的几部推荐给该用户。

user_item
图 7-1 用户对电影的评分矩阵

我们继续从生活经验出发来思考这一问题。假设某用户为一部电影打了高分,那么可以合理猜测,该用户喜欢这部电影的某些特征。例如,电影的类型是悬疑、爱情、战争或是其他种类;演员、导演和出品方分别是哪些;叙述的故事发生在什么年代;时长是多少,等等。假如我们有一个电影特征库,可以将每部电影用一个特征向量表示。向量的每一维代表一种特征,值代表电影具有这一特征的程度。同时,我们还可以构建一个用户画像库,包含每个用户更偏好哪些类型的特征,以及偏好的程度。假设特征的个数是,那么所有电影的特征构成的矩阵是,用户喜好构成的矩阵是。图 7-2 给出了两个矩阵的示例。

user_item_feat
图 7-2 电影和用户的隐变量矩阵

需要说明的是,我们实际上分解出的矩阵只是某种交互结果背后的隐变量,并不一定对应真实的特征。这样,我们就把一个用户与电影交互的矩阵拆分成了用户、电影两个矩阵,并且这两个矩阵中包含了更多的信息。最后,用这两个矩阵的乘积可以还原出用户对电影的评分。即使用户对某部电影并没有打分,我们也能通过矩阵乘积,根据用户喜欢的特征和该电影具有的特征,预测出用户对电影的喜好程度。


小故事:矩阵分解和下面要介绍的因子分解机都属于推荐系统(recommender system)领域的算法。我们在日常使用软件、浏览网站的时候,软件或网站会记录下来我们感兴趣的内容,并在接下来更多地为我们推送同类型的内容。例如,如果我们在购物网站上浏览过牙刷,它就可能再给我们推荐牙刷、毛巾、脸盆等等相关性比较大的商品,这就是推荐系统的作用。推荐系统希望根据用户的特征、商品的特征、用户和商品的交互历史,为用户做出更符合个人喜好的个性化推荐,提高用户的浏览体验,同时为公司带来更高的经济效益。

机器学习界开始大量关注推荐系统任务源自美国奈飞电影公司(Netflix)于 2006 年举办的世界范围的推荐系统算法大赛。该比赛旨在探寻一种算法能更加精确地预测 48 万名用户对 1.7 万部电影的打分,如果某个参赛队伍给出的评分预测精度超过了基线算法 10%,就可以获得 100 万美元的奖金。该竞赛在 1 年来吸引了来自全球 186 个国家的超过 4 万支队伍的参加,经过 3 年的“马拉松”竞赛,最终由一支名为 BellKor's Pragmatic Chaos 的联合团队摘得桂冠。而团队中时任雅虎研究员的耶胡达·科伦(Yehuda Koren)则在后来成为了推荐系统领域最为著名的科学家之一,他使用的基于矩阵分解的双线性模型则成为了那个时代推荐系统的主流模型[1]。


实际上,我们通常能获取到的并不是,而是打分的结果。并且由于一个用户只会对极其有限的一部分电影打分,矩阵是非常稀疏的,绝大多数元素都是空白。因此,我们需要从有限的元素中推测出用户的喜好和电影的特征。MF 模型利用矩阵分解的技巧完成了这一任务。设第个用户的偏好向量是,第部电影的特征向量是,其维度都是特征数。MF 假设用户对电影的评分是用户偏好与电影特征的内积,即。在本章开始已经讲过,向量内积是双线性函数,这也是 MF 模型属于双线性模型的原因。

既然 MF 的目标是通过特征还原评分矩阵,我们就以还原结果和中已知部分的差距作为损失函数。记,即当用户为电影打过分时,否则为。那么损失函数可以写为:

式中,是模型预测和真实值之间的损失。一般情况下,我们就选用最简单的 MSE 作为损失,那么优化目标为:

再加入对正则化约束,就得到总的优化目标:

需要注意,这里的约束并非对整个矩阵或者而言。我们知道,正则化的目的是通过限制参数的规模来约束模型的复杂度,使模型的复杂度与数据中包含的信息相匹配。以用户为例,假设不同用户直接的评分是独立的。如果用户甲给 10 部电影打了分,用户乙给 2 部电影打了分,那么数据中关于甲的信息就比乙多。反映到正则化上,对甲的参数的约束强度也应当比乙大。因此,总损失函数中的正则化系数是,在的基础上又乘以用户评分的数量。对电影向量也是同理。上式对的梯度分别为:

可以发现,上面梯度中含有,而的梯度中含有,两者互相包含,这是由双线性函数的性质决定的,也是双线性模型的一个重要特点。

7.2 动手实现矩阵分解

下面,我们来动手实现矩阵分解模型。我们选用的数据集是推荐系统中的常用数据集 MovieLens,其包含从电影评价网站 MovieLens 中收集的真实用户对电影的打分信息。简单起见,我们采用其包含来自 943 个用户对 1682 部电影的 10 万条样本的版本 MovieLens-100k。我们对原始的数据进行了一些处理,现在数据集的每一行有 3 个数,依次表示用户编号、电影编号、用户对电影的打分,其中且三者都是整数。表 7-1 展示了数据集中的 3 个样本,读者也可以从网站上下载更大的数据集,测试模型的预测效果。

表 7-1 MovieLens 数据集示例

用户编号电影编号评分
1962423
1863023
223771
import numpy as np
import matplotlib.pyplot as plt
from tqdm import tqdm # 进度条工具
data = np.loadtxt('movielens_100k.csv', delimiter=',', dtype=int)
print('数据集大小:', len(data))
# 用户和电影都是从1开始编号的,我们将其转化为从0开始
data[:, :2] = data[:, :2] - 1
# 计算用户和电影数量
users = set()
items = set()
for i, j, k in data:
users.add(i)
items.add(j)
user_num = len(users)
item_num = len(items)
print(f'用户数:{user_num},电影数:{item_num}')
# 设置随机种子,划分训练集与测试集
np.random.seed(0)
ratio = 0.8
split = int(len(data) * ratio)
np.random.shuffle(data)
train = data[:split]
test = data[split:]
# 统计训练集中每个用户和电影出现的数量,作为正则化的权重
user_cnt = np.bincount(train[:, 0], minlength=user_num)
item_cnt = np.bincount(train[:, 1], minlength=item_num)
print(user_cnt[:10])
print(item_cnt[:10])
# 用户和电影的编号要作为下标,必须保存为整数
user_train, user_test = train[:, 0], test[:, 0]
item_train, item_test = train[:, 1], test[:, 1]
y_train, y_test = train[:, 2], test[:, 2]
数据集大小: 100000
用户数:943,电影数:1682
[215 47 42 19 139 170 320 47 18 156]
[371 109 70 172 70 21 308 158 240 68]

接下来,我们将 MF 模型定义成类,在其中实现梯度计算方法。根据上面的推导,模型的参数是用户喜好和电影特征,其中特征数是我们自己指定的超参数。在参数初始化部分,考虑到最终电影的得分都是正数,我们将参数都初始化为 1。

class MF:
def __init__(self, N, M, d):
# N是用户数量,M是电影数量,d是特征维度
# 定义模型参数
self.user_params = np.ones((N, d))
self.item_params = np.ones((M, d))
def pred(self, user_id, item_id):
# 预测用户user_id对电影item_id的打分
# 获得用户偏好和电影特征
user_param = self.user_params[user_id]
item_param = self.item_params[item_id]
# 返回预测的评分
rating_pred = np.sum(user_param * item_param, axis=1)
return rating_pred
def update(self, user_grad, item_grad, lr):
# 根据参数的梯度更新参数
self.user_params -= lr * user_grad
self.item_params -= lr * item_grad

下面定义训练函数,以 SGD 算法对 MF 模型的参数进行优化。对于回归任务来说,我们仍然以 MSE 作为损失函数,RMSE 作为的评价指标。在训练的同时,我们将其记录下来,供最终绘制训练曲线使用。

def train(model, learning_rate, lbd, max_training_step, batch_size):
train_losses = []
test_losses = []
batch_num = int(np.ceil(len(user_train) / batch_size))
with tqdm(range(max_training_step * batch_num)) as pbar:
for epoch in range(max_training_step):
# 随机梯度下降
train_rmse = 0
for i in range(batch_num):
# 获取当前批量
st = i * batch_size
ed = min(len(user_train), st + batch_size)
user_batch = user_train[st: ed]
item_batch = item_train[st: ed]
y_batch = y_train[st: ed]
# 计算模型预测
y_pred = model.pred(user_batch, item_batch)
# 计算梯度
P = model.user_params
Q = model.item_params
errs = y_batch - y_pred
P_grad = np.zeros_like(P)
Q_grad = np.zeros_like(Q)
for user, item, err in zip(user_batch, item_batch, errs):
P_grad[user] = P_grad[user] - err * Q[item] + lbd * P[user]
Q_grad[item] = Q_grad[item] - err * P[user] + lbd * Q[item]
model.update(P_grad / len(user_batch), Q_grad / len(user_batch), learning_rate)
train_rmse += np.mean(errs ** 2)
# 更新进度条
pbar.set_postfix({
'Epoch': epoch,
'Train RMSE': f'{np.sqrt(train_rmse / (i + 1)):.4f}',
'Test RMSE': f'{test_losses[-1]:.4f}' if test_losses else None
})
pbar.update(1)
# 计算测试集上的RMSE
train_rmse = np.sqrt(train_rmse / len(user_train))
train_losses.append(train_rmse)
y_test_pred = model.pred(user_test, item_test)
test_rmse = np.sqrt(np.mean((y_test - y_test_pred) ** 2))
test_losses.append(test_rmse)
return train_losses, test_losses

最后,我们定义超参数,并实现 MF 模型的训练部分,并将损失随训练的变化曲线绘制出来。

# 超参数
feature_num = 16 # 特征数
learning_rate = 0.1 # 学习率
lbd = 1e-4 # 正则化强度
max_training_step = 30
batch_size = 64 # 批量大小
# 建立模型
model = MF(user_num, item_num, feature_num)
# 训练部分
train_losses, test_losses = train(model, learning_rate, lbd,
max_training_step, batch_size)
plt.figure()
x = np.arange(max_training_step) + 1
plt.plot(x, train_losses, color='blue', label='train loss')
plt.plot(x, test_losses, color='red', ls='--', label='test loss')
plt.xlabel('Epoch')
plt.ylabel('RMSE')
plt.legend()
plt.show()
88%|██████████████▉ | 32883/37500 [01:16<00:09, 488.84it/s, Epoch=26, Train RMSE=0.9871, Test RMSE=1.0160]

为了直观地展示模型效果,我们输出一些模型在测试集中的预测结果与真实结果进行对比。上面我们训练得到的模型在测试集上的 RMSE 大概是 1 左右,所以这里模型预测的评分与真实评分大致也差 1。

y_test_pred = model.pred(user_test, item_test)
print(y_test_pred[:10]) # 把张量转换为numpy数组
print(y_test[:10])

7.3 因子分解机

本节我们介绍推荐系统中用户行为预估的另一个常用模型:因子分解机(factorization machines,FM)。FM 的应用场景与 MF 有一些区别,MF 的目标是从交互的结果中计算出用户和物品的特征;而 FM 则正好相反,希望通过物品的特征和某个用户点击这些物品的历史记录,预测该用户点击其他物品的概率,即点击率(click through rate,CTR)。由于被点击和未被点击是一个二分类问题,CTR 预估可以用逻辑斯谛回归模型来解决。在逻辑斯谛回归中,线性预测子为数据中的每一个特征赋予权重,由此来判断数据的分类。然而,这样的线性参数化假设中,输入的不同特征之间并没有运算,相当于假设不同特征之间是独立的。而在现实中,输入数据的不同特征之间有可能存在关联。例如,假设我们将一张照片中包含的物品作为其特征,那么“红灯笼”与“对联”这两个特征就很可能不是独立的,因为它们都是与春节相关联的意象。因此,作为对线性的逻辑斯谛回归模型的改进,我们进一步引入双线性部分,将输入的不同特征之间的联系也考虑进来。改进后的预测函数为:

其中,是常数项,是权重。上式的第二项将所有不同特征相乘,从而可以通过权重调整特征组合对预测结果的影响。将上式改写为向量形式,为:

式中,矩阵是对称的,即。此外,由于我们已经考虑了单独特征的影响,所以不需要将特征与其自身进行交叉,引入项,从而的对角线上元素都为。读者可以自行验证,形如的函数是双线性函数。双线性模型由于考虑了不同特征之间的关系,理论上比线性模型要更准确。然而,在实际应用中,该方法面临着稀疏特征的挑战。

在用向量表示某一事物的离散特征时,一种常用的方法是独热编码(one-hot encoding)。这一方法中,向量的每一维都对应特征的一种取值,样本所具有的特征所在的维度值为 1,其他维度为 0。如图 7-3 所示,某物品的产地是北京、上海、广州、深圳其中之一,为了表示该物品的产地,我们将其编码为 4 维向量,4 个维度依次对应产地北京、上海、广州、深圳。当物品产地为北京时,其特征向量就是;物品产地为上海时,其特征向量就是。如果物品有多个特征,就把每个特征编码成的向量依次拼接起来,形成多域独热编码(multi-field one-hot encoding)。假如某种食品产地是上海、生产日期在 2 月份、食品种类是乳制品,那么它的编码就如图 7-3 所示。

mfoh
图 7-3 多域独热编码示意

像这样的独热特征向量往往维度非常高,但只有少数几个位置是 1,其他位置都是 0,稀疏程度很高。当我们训练上述的模型时,需要对参数求导,结果为。由于特征向量的稀疏性,大多数情况下都有,无法对参数进行更新。为了解决这一问题,Steffen Rendle 提出了因子分解机模型。该方法将权重矩阵分解成,其中。根据矩阵分解的相关理论,当满足某些性质且足够大时,我们总可以找到分解矩阵。即使条件不满足,我们也可以用近似分解来代替。设的行向量是,也即是对每个特征配一个维实数向量,用矩阵乘法直接计算可以得到,这样模型的预测函数可以写为:

这时,再对参数求梯度的结果为:

上面的计算过程中,为了简洁,我们采用了不太严谨的写法,当时会出现求和下界大于上界的情况。此时我们规定求和的结果为零。如果要完全展开,只需要做类似于变为的裂项操作即可。从该结果中可以看出,只要,参数的梯度就不为零,可以用梯度相关的算法对其更新。因此,即使特征向量非常稀疏,FM 模型也可以正常进行训练。

至此,我们的模型还存在一个问题。双线性模型考虑不同特征之间乘积的做法,虽然提升了模型的能力,但也引入了额外的计算开销。对一个样本来说,线性模型需要计算,时间复杂度为;而我们的模型需要计算每一对特征的乘积,以及参数的内积,时间复杂度为。上面已经讲过,多热编码的特征向量维度常常特别高,因此这一时间开销是相当巨大的。但是,我们可以对上面的公式做一些变形,改变计算顺序来降低时间复杂度。变形方式如下:

在变形的第二步和第三步,我们利用了向量内积的双线性性质,将标量以及求和都移到内积中去。最后的结果中只含有两重求和,外层为次,内层为次,因此整体的时间复杂度为。这样,FM 的时间复杂度关于特征规模的增长从平方变为线性,得到了大幅优化。至此,FM 的预测公式为:

如果要做分类任务,只需要再加上 softmax 函数即可。

在上面的模型中,我们只考虑了两个特征之间的组合,因此该 FM 也被称为二阶 FM。如果进一步考虑多个特征的组合,如,就可以得到高阶的 FM 模型。由于高阶 FM 较为复杂,并且也不再是双线性模型,本书在此略去,感兴趣的读者可以自行查阅相关资料。

7.4 动手实现因子分解机

下面,我们来动手实现二阶 FM 模型。本节采用的数据集是为 FM 制作的示例数据集 fm_dataset.csv,包含了某个用户浏览过的商品的特征,以及用户是否点击过这个商品。数据集的每一行包含一个商品,前 24 列是其特征,最后一列是 0 或 1,分别表示用户没有或有点击该商品。我们的目标是根据输入特征预测用户在测试集上的行为,是一个二分类问题。我们先导入必要的模块和数据集并处理数据,将其划分为训练集和测试集。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import metrics # sklearn中的评价指标函数库
from tqdm import tqdm
# 导入数据集
data = np.loadtxt('fm_dataset.csv', delimiter=',')
# 划分数据集
np.random.seed(0)
ratio = 0.8
split = int(ratio * len(data))
x_train = data[:split, :-1]
y_train = data[:split, -1]
x_test = data[split:, :-1]
y_test = data[split:, -1]
# 特征数
feature_num = x_train.shape[1]
print('训练集大小:', len(x_train))
print('测试集大小:', len(x_test))
print('特征数:', feature_num)
训练集大小: 800
测试集大小: 200
特征数: 24

接下来,我们将 FM 模型定义成类。与 MF 相同,我们在类中实现预测和梯度更新方法。

class FM:
def __init__(self, feature_num, vector_dim):
# vector_dim代表公式中的k,为向量v的维度
self.theta0 = 0.0 # 常数项
self.theta = np.zeros(feature_num) # 线性参数
self.v = np.random.normal(size=(feature_num, vector_dim)) # 双线性参数
self.eps = 1e-6 # 精度参数
def _logistic(self, x):
# 工具函数,用于将预测转化为概率
return 1 / (1 + np.exp(-x))
def pred(self, x):
# 线性部分
linear_term = self.theta0 + x @ self.theta
# 双线性部分
square_of_sum = np.square(x @ self.v)
sum_of_square = np.square(x) @ np.square(self.v)
# 最终预测
y_pred = self._logistic(linear_term \
+ 0.5 * np.sum(square_of_sum - sum_of_square, axis=1))
# 为了防止后续梯度过大,对预测值进行裁剪,将其限制在某一范围内
y_pred = np.clip(y_pred, self.eps, 1 - self.eps)
return y_pred
def update(self, grad0, grad_theta, grad_v, lr):
self.theta0 -= lr * grad0
self.theta -= lr * grad_theta
self.v -= lr * grad_v

对于分类任务,我们仍用 MLE 作为训练时的损失函数。在测试集上,我们采用 AUC 作为评价指标。由于我们在第 6 章中已经动手实现过 AUC,简单起见,这里我们就直接使用 sklearn 中的函数直接计算 AUC。我们用 SGD 进行参数更新,训练完成后,我们把训练过程中的准确率和 AUC 绘制出来。

# 超参数设置,包括学习率、训练轮数等
vector_dim = 16
learning_rate = 0.01
lbd = 0.05
max_training_step = 200
batch_size = 32
# 初始化模型
np.random.seed(0)
model = FM(feature_num, vector_dim)
train_acc = []
test_acc = []
train_auc = []
test_auc = []
with tqdm(range(max_training_step)) as pbar:
for epoch in pbar:
st = 0
while st < len(x_train):
ed = min(st + batch_size, len(x_train))
X = x_train[st: ed]
Y = y_train[st: ed]
st += batch_size
# 计算模型预测
y_pred = model.pred(X)
# 计算交叉熵损失
cross_entropy = -Y * np.log(y_pred) \
- (1 - Y) * np.log(1 - y_pred)
loss = np.sum(cross_entropy)
# 计算损失函数对y的梯度,再根据链式法则得到总梯度
grad_y = (y_pred - Y).reshape(-1, 1)
# 计算y对参数的梯度
# 常数项
grad0 = np.sum(grad_y * (1 / len(X) + lbd))
# 线性项
grad_theta = np.sum(grad_y * (X / len(X) \
+ lbd * model.theta), axis=0)
# 双线性项
grad_v = np.zeros((feature_num, vector_dim))
for i, x in enumerate(X):
# 先计算sum(x_i * v_i)
xv = x @ model.v
grad_vi = np.zeros((feature_num, vector_dim))
for s in range(feature_num):
grad_vi[s] += x[s] * xv - (x[s] ** 2) * model.v[s]
grad_v += grad_y[i] * grad_vi
grad_v = grad_v / len(X) + lbd * model.v
model.update(grad0, grad_theta, grad_v, learning_rate)
pbar.set_postfix({
'训练轮数': epoch,
'训练损失': f'{loss:.4f}',
'训练集准确率': train_acc[-1] if train_acc else None,
'测试集准确率': test_acc[-1] if test_acc else None
})
# 计算模型预测的准确率和AUC
# 预测准确率,阈值设置为0.5
y_train_pred = (model.pred(x_train) >= 0.5)
acc = np.mean(y_train_pred == y_train)
train_acc.append(acc)
auc = metrics.roc_auc_score(y_train, y_train_pred) # sklearn中的AUC函数
train_auc.append(auc)
y_test_pred = (model.pred(x_test) >= 0.5)
acc = np.mean(y_test_pred == y_test)
test_acc.append(acc)
auc = metrics.roc_auc_score(y_test, y_test_pred)
test_auc.append(auc)
print(f'测试集准确率:{test_acc[-1]},\t测试集AUC:{test_auc[-1]}')
100%|█| 200/200 [00:43<00:00, 4.61it/s, 训练轮数=199, 训练损失=11.3006, 训练集准确率=0.816, 测试集准确率=0.200 [00:00<?, ?it/s]
测试集准确率:0.79, 测试集AUC:0.7201320910484726

最后,我们把训练过程中在训练集和测试集上的精确率和 AUC 绘制出来,观察训练效果。

# 绘制训练曲线
plt.figure(figsize=(13, 5))
x_plot = np.arange(len(train_acc)) + 1
plt.subplot(121)
plt.plot(x_plot, train_acc, color='blue', label='train acc')
plt.plot(x_plot, test_acc, color='red', ls='--', label='test acc')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
plt.subplot(122)
plt.plot(x_plot, train_auc, color='blue', label='train AUC')
plt.plot(x_plot, test_auc, color='red', ls='--', label='test AUC')
plt.xlabel('Epoch')
plt.ylabel('AUC')
plt.legend()
plt.show()

png

7.6 本章小结

本章介绍了双线性模型的来源和特点,以及与线性模型的区别。双线性模型通过引入满足双线性性质的函数,相比于线性模型提升了对特征间关系建模的能力,从而达到更好的预测效果。本章以因子分解机和概率矩阵分解两个推荐系统中的常用模型为例,具体讲解了双线性模型的应用,并动手实现了两个模型。MF 模型和 FM 模型的应用场景不同。在 FM 中,用户的特征已知,我们希望挖掘特征与输出、特征与特征之间的关系;而在 MF 中,用户和物品的特征都是未知的,需要从模型训练得到。这两个模型都是目前推荐系统所用模型的基础,从它们改进和衍生的模型仍然有广泛应用。

习题

  1. 以下关于双线性模型的说法,不正确的是: A. 双线性模型考虑了特征之间的关联,比线性模型建模能力更强。 B. 在因子分解机中,因为引入了特征的乘积,只有特征都不为零时才能更新参数。 C. 可以通过重新设置参数,把因子分解机中的常数项和一次项都合并到二次项里,得到更一般的表达式。 D. 在矩阵分解中,最优的特征数量是超参数,不能通过公式推导出来。

  2. 以下哪一个模型不是关于双线性模型: A. 。 B. 。 C. 。 D.

  3. 关于多域独热编码,思考其相比于如下编码方式的优势:针对每一个域,依次把其中的离散取值以自然数(以 0 开始)作为编码,在编码后每个域就对应一个自然数。例如图 7-3 中产地上海对应为 1,深圳对应为 3,生产月份 2 月对应为 1,12 月对应 11,食品种类乳制品对应为 0,图中的整个编码向量为

  4. 试修改 MF 的pred(self, user_id, item_id)函数,在模型预测中加入全局打分偏置、用户打分偏置和物品打分偏置,类似 FM 模型中的常数项部分,观察模型拟合性能指标的变化。

  5. 试基于本章的 MF 代码,调试不同的超参数,包括,关注训练集和测试集的性能指标的改变,根据训练和测试的性能曲线,判定哪些超参数导致过拟合。

  6. 试通过代码实验来验证双线性模型 FM 做回归或分类任务时,其优化目标相对参数是非凸的,也即是,设置不同的参数初始值,使用同样的 SGD 学习算法,最后参数会收敛到不同的位置。

拓展阅读:概率矩阵分解

概率矩阵分解(probabilistic matrix factorization,PMF)是另一种常用的双线性模型。与矩阵分解模型不同,它对用户给电影的评分的分布进行了先验假设,认为其满足正态分布:

其中是正态分布的方差,与用户和电影无关。注意,都是未知的。记,即当用户对电影打过分时,否则。再假设不同的评分采样之间互相独立,那么,我们观测到的出现的概率是:

这里,我们用表示正态分布的概率密度函数,其完整表达式为:

对于那些空缺的,由于,对连乘没有贡献,最终的概率只由已知部分计算得出。接下来,我们进一步假设用户的喜好和电影的特征都满足均值为的正态分布,协方差矩阵分别为,即:

根据全概率公式,并注意到无关,我们可以计算出的后验概率为:

其中是常数。为了简化这一表达式,我们利用与 MLE 中相同的技巧,将上式取对数,从而把连乘变为求和:

再代入取对数后的表达式

计算得到

其中,是与参数无关的常数。根据最大似然的思想,我们应当最大化上面计算出的对数概率。因此,定义损失函数为:

于是,最大化对数概率就等价于最小化损失函数。并且,这一损失函数恰好为目标值与参数内积之间的平方损失,再加上正则化的形式。由于向量内积是双线性函数,PMF 模型也属于双线性模型的一种。

将损失函数对求导,得到:

令梯度为零,解得:

在正则化约束一节中我们讲过,根据矩阵相关的理论,只要足够大,上式的第一项逆矩阵就总是存在。同理,对也有类似的结果。因此,我们可以通过如上形式的来求解参数。在参数的高斯分布假设下,我们自然导出了带有正则化的 MF 模型,这并不是偶然。我们会在第 16 章概率图模型中进一步阐释其中的原理。

参考文献

[1] 矩阵分解模型论文:Koren Y, Bell R, Volinsky C. Matrix factorization techniques for recommender systems[J]. Computer, 2009, 42(8): 30-37.

[2] 因子分解机论文:Rendle S. Factorization machines[C]//2010 IEEE International conference on data mining. IEEE, 2010: 995-1000.

[3] 概率矩阵分解论文:Mnih A, Salakhutdinov R R. Probabilistic matrix factorization[J]. Advances in neural information processing systems, 2007, 20.