当前位置:   article > 正文

动手学机器学习双线性模型+习题

动手学机器学习双线性模型+习题

在数学中,双线性的含义为,二元函数固定任意一个自变量时,函数关于另一个自变量线性

矩阵分解

设想有N个用户和M部电影,构建一个用户画像库,包含每个用户更偏好哪些类型的特征,以及偏好的程度。假设特征的个数是d,那么所有电影的特征构成的矩阵是P∈R^Mxd,用户喜好构成的矩阵是Q∈R^Nxd

 最后,用这两个矩阵的乘积 R = P.T * Q 可以还原出用户对电影的评分。即使用户对某部电影并没有打分,我们也能通过矩阵乘积,根据用户喜欢的特征和该电影具有的特征,预测出用户对电影的喜好程度

实际上,我们通常能获取到的并不是P和Q,而是打分的结果R。并且由于一个用户只会对极其有限的一部分电影打分,矩阵R是非常稀疏的,绝大多数元素都是空白。因此,我们需要从R有限的元素中推测出用户的喜好P和电影的特征Q

 变成MSE形式+正则化得到:

这里的正则化不是针对整个矩阵,而是每一行,因为电影之间、用户之间是相互独立的

对p和q的梯度:

 

动手实现矩阵分解

  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. from tqdm import tqdm # 进度条工具
  4. data = np.loadtxt('movielens_100k.csv', delimiter=',', dtype=int)
  5. print('数据集大小:', len(data))
  6. # 用户和电影都是从1开始编号的,我们将其转化为从0开始
  7. data[:, :2] = data[:, :2] - 1
  8. # 计算用户和电影数量
  9. users = set()
  10. items = set()
  11. for i, j, k in data:
  12. users.add(i)
  13. items.add(j)
  14. user_num = len(users)
  15. item_num = len(items)
  16. print(f'用户数:{user_num},电影数:{item_num}')
  17. # 设置随机种子,划分训练集与测试集
  18. np.random.seed(0)
  19. ratio = 0.8
  20. split = int(len(data) * ratio)
  21. np.random.shuffle(data)
  22. train = data[:split]
  23. test = data[split:]
  24. # 统计训练集中每个用户和电影出现的数量,作为正则化的权重
  25. user_cnt = np.bincount(train[:, 0], minlength=user_num)
  26. item_cnt = np.bincount(train[:, 1], minlength=item_num)
  27. print(user_cnt[:10])
  28. print(item_cnt[:10])
  29. # 用户和电影的编号要作为下标,必须保存为整数
  30. user_train, user_test = train[:, 0], test[:, 0]
  31. item_train, item_test = train[:, 1], test[:, 1]
  32. y_train, y_test = train[:, 2], test[:, 2]

用set去重

user_cnt用np.bincount计算数组 train 中第一列中每个元素出现的次数

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

给定用户 ID 和电影 ID,计算用户参数和电影参数的乘积,并返回预测的评分

  1. def train(model, learning_rate, lbd, max_training_step, batch_size):
  2. train_losses = []
  3. test_losses = []
  4. batch_num = int(np.ceil(len(user_train) / batch_size))
  5. with tqdm(range(max_training_step * batch_num)) as pbar:
  6. for epoch in range(max_training_step):
  7. # 随机梯度下降
  8. train_rmse = 0
  9. for i in range(batch_num):
  10. # 获取当前批量
  11. st = i * batch_size
  12. ed = min(len(user_train), st + batch_size)
  13. user_batch = user_train[st: ed]
  14. item_batch = item_train[st: ed]
  15. y_batch = y_train[st: ed]
  16. # 计算模型预测
  17. y_pred = model.pred(user_batch, item_batch)
  18. # 计算梯度
  19. P = model.user_params
  20. Q = model.item_params
  21. errs = y_batch - y_pred
  22. P_grad = np.zeros_like(P)
  23. Q_grad = np.zeros_like(Q)
  24. for user, item, err in zip(user_batch, item_batch, errs):
  25. P_grad[user] = P_grad[user] - err * Q[item] + lbd * P[user]
  26. Q_grad[item] = Q_grad[item] - err * P[user] + lbd * Q[item]
  27. model.update(P_grad / len(user_batch), Q_grad / len(user_batch), learning_rate)
  28. train_rmse += np.mean(errs ** 2)
  29. # 更新进度条
  30. pbar.set_postfix({
  31. 'Epoch': epoch,
  32. 'Train RMSE': f'{np.sqrt(train_rmse / (i + 1)):.4f}',
  33. 'Test RMSE': f'{test_losses[-1]:.4f}' if test_losses else None
  34. })
  35. pbar.update(1)
  36. # 计算测试集上的RMSE
  37. train_rmse = np.sqrt(train_rmse / len(user_train))
  38. train_losses.append(train_rmse)
  39. y_test_pred = model.pred(user_test, item_test)
  40. test_rmse = np.sqrt(np.mean((y_test - y_test_pred) ** 2))
  41. test_losses.append(test_rmse)
  42. return train_losses, test_losses

np.ceil 用于计算数组中每个元素的向上取整值

遍历训练集中的每个批量,进行随机梯度下降训练

根据梯度公式求梯度,并根据批大小调整

通过 set_postfix 方法设置进度条的附加信息,在更新完附加信息后,使用 pbar.update(1) 更新进度条,使其前进一步

pred的结果:也差不多相差1

因子分解机

FM 的应用场景与 MF 有一些区别,MF 的目标是从交互的结果中计算出用户和物品的特征;而 FM 则正好相反,希望通过物品的特征和某个用户点击这些物品的历史记录,预测该用户点击其他物品的概率,即点击率(click through rate,CTR)

由于被点击和未被点击是一个二分类问题,CTR 预估可以用逻辑斯谛回归模型来解决,然而逻辑回归的线性参数化假设中不同的特征xi与xj之间并没有运算,因此需要进一步引入双线性部分:

写成向量形式:

物体的特征向量one-hot后会过于稀疏,y(x)对w求导后得到的xixj大多地方都是0,所以难以对wij更新

从该结果中可以看出,只要xs≠0,参数的梯度vs就不为零,可以用梯度相关的算法对其更新。因此,即使特征向量非常稀疏,FM 模型也可以正常进行训练

模型还存在一个问题。双线性模型考虑不同特征之间乘积的做法,虽然提升了模型的能力,但也引入了额外的计算开销。可以对上面的公式做一些变形,改变计算顺序来降低时间复杂度

 至此,FM 的预测公式为:

 动手实现因子分解机

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

np.clip将预测值限制在 [self.eps, 1 - self.eps] 的范围内,如果预测值超出了这个范围,就将其设置为边界值(因为用了sigmoid)

习题

1.B。只要有不为0的特征就能训练

2.C。C不涉及θ1和θ2之间的乘积或内积,因此不是一个双线性模型;D不是标准的双线性形式,但它可以被视为双线性的,它涉及到了θ1和θ2的乘积

3.题目中这种编码方式叫作label encoder

避免顺序假设: Label Encoder 将类别按照它们出现的顺序进行编码,这可能会给模型引入错误的假设,认为类别之间存在顺序关系

避免偏好性: Label Encoder 可能会给编码后的类别赋予不同的数值,这可能导致模型在训练过程中对数值较大的类别产生偏好

适用性广泛: One-Hot Encoder 适用于大多数机器学习模型,包括线性模型、树模型等

4.

  1. class MF:
  2. def __init__(self, N, M, d):
  3. # N是用户数量,M是电影数量,d是特征维度
  4. # 定义模型参数
  5. self.user_params = np.ones((N, d))
  6. self.item_params = np.ones((M, d))
  7. self.global_bias = 0.0 # 全局打分偏置
  8. self.user_bias = np.zeros(N) # 用户打分偏置
  9. self.item_bias = np.zeros(M) # 物品打分偏置
  10. def pred(self, user_id, item_id):
  11. # 预测用户user_id对电影item_id的打分
  12. # 获得用户偏好和电影特征
  13. user_param = self.user_params[user_id]
  14. item_param = self.item_params[item_id]
  15. # 计算预测分数
  16. pred_score = np.sum(user_param * item_param, axis=1)
  17. pred_score += self.global_bias # 添加全局打分偏置
  18. pred_score += self.user_bias[user_id] # 添加用户打分偏置
  19. pred_score += self.item_bias[item_id] # 添加物品打分偏置
  20. return pred_score
  21. def update(self, user_grad, item_grad, global_bias_grad, user_bias_grad, item_bias_grad, lr):
  22. # 根据参数的梯度更新参数
  23. self.user_params -= lr * user_grad
  24. self.item_params -= lr * item_grad
  25. self.global_bias -= lr * global_bias_grad
  26. self.user_bias -= lr * user_bias_grad
  27. self.item_bias -= lr * item_bias_grad
  28. def train(model, learning_rate, lbd, max_training_step, batch_size):
  29. train_losses = []
  30. test_losses = []
  31. batch_num = int(np.ceil(len(user_train) / batch_size))
  32. with tqdm(range(max_training_step * batch_num)) as pbar:
  33. for epoch in range(max_training_step):
  34. # 随机梯度下降
  35. train_rmse = 0
  36. for i in range(batch_num):
  37. # 获取当前批量
  38. st = i * batch_size
  39. ed = min(len(user_train), st + batch_size)
  40. user_batch = user_train[st: ed]
  41. item_batch = item_train[st: ed]
  42. y_batch = y_train[st: ed]
  43. # 计算模型预测
  44. y_pred = model.pred(user_batch, item_batch)
  45. # 计算梯度
  46. P = model.user_params
  47. Q = model.item_params
  48. errs = y_batch - y_pred
  49. P_grad = np.zeros_like(P)
  50. Q_grad = np.zeros_like(Q)
  51. # 计算全局打分偏置、用户打分偏置和物品打分偏置的梯度
  52. global_bias_grad = -np.mean(errs) # 全局打分偏置梯度
  53. user_bias_grad = np.zeros_like(model.user_bias)
  54. item_bias_grad = np.zeros_like(model.item_bias)
  55. for user, item, err in zip(user_batch, item_batch, errs):
  56. user_bias_grad[user] += -err
  57. item_bias_grad[item] += -err
  58. P_grad[user] = P_grad[user] - err * Q[item] + lbd * P[user]
  59. Q_grad[item] = Q_grad[item] - err * P[user] + lbd * Q[item]
  60. model.update(P_grad / len(user_batch), Q_grad / len(user_batch),
  61. global_bias_grad, user_bias_grad / len(user_batch),
  62. item_bias_grad / len(user_batch), learning_rate)
  63. train_rmse += np.mean(errs ** 2)
  64. # 更新进度条
  65. pbar.set_postfix({
  66. 'Epoch': epoch,
  67. 'Train RMSE': f'{np.sqrt(train_rmse / (i + 1)):.4f}',
  68. 'Test RMSE': f'{test_losses[-1]:.4f}' if test_losses else None
  69. })
  70. pbar.update(1)
  71. # 计算 RMSE 损失
  72. train_rmse = np.sqrt(train_rmse / len(user_train))
  73. train_losses.append(train_rmse)
  74. y_test_pred = model.pred(user_test, item_test)
  75. test_rmse = np.sqrt(np.mean((y_test - y_test_pred) ** 2))
  76. test_losses.append(test_rmse)
  77. return train_losses, test_losses

之前

加了之后可以看到loss低了

5.略

6.对不起,做不到>_<

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/359151
推荐阅读
相关标签
  

闽ICP备14008679号