1. 画像层
2. 召回/粗排
3. 精排
电子书-FunRec ↗
纸质书-推荐系统技术原理与实践 ↗
多路召回
召回技术的演进图谱
协同过滤(Collaborative Filtering):基于用户行为数据,找到相似用户或物品, 重点是计算用户/物品之间的相似度。
记用户为,物品为,用户集合为。
1. Jaccard相似度
from sklearn.metrics.pairwise import jaccard_similarity_score2. 余弦相似度
from sklearn.metrics.pairwise import cosine_similarity3. Pearson相似度
from scipy.stats import pearsonr, np.corrcoef()4. Tanimoto
1. 计算方法
根据已有的用户向量计算用户与其他用户的相似度,得到与用户最相似的个用户。根据这 个用户对物品的评分情况和与的相似程度猜测出对的评分。如果评分比较高的话, 就把推荐给, 否则不推荐。
| 用户 | 物品 | 物品 | 物品 | ··· | 物品 |
|---|---|---|---|---|---|
| 用户 | 1 | 0 | 1 | ... | ? |
| 用户 | 0 | 1 | 1 | ... | 0 |
| 用户 | 1 | 0 | 0 | ... | 1 |
2. 代码实现
import numpy as np
import pandas as pd
# 用户-物品评分数据,因为是稀疏矩阵,所以使用字典存储,后面也使用字典来计算用户相似度
user_data = {'y': {'A': 5, 'B': 3, 'C': 4, 'D': 4},
'X1': {'A': 3, 'B': 1, 'C': 2, 'D': 3, 'k': 3},
'X2': {'A': 4, 'B': 3, 'C': 4, 'D': 3, 'k': 5},
'X3': {'A': 3, 'B': 3, 'C': 1, 'D': 5, 'k': 4},
'X4': {'A': 1, 'B': 5, 'C': 5, 'D': 2, 'k': 1}
}
user_names = list(user_data.keys())
# 用户相似度矩阵,行列均为用户名称
similarity_matrix = pd.DataFrame(
np.identity(len(user_data)),
index=user_names,
columns=user_names
)
# 遍历每条用户-物品评分数据
for u1, info1 in user_data.items():
for u2, info2 in user_data.items():
if u1 == u2:
continue
vec1, vec2 = [], [] # 用户u1, u2对应的评分向量
for item1, rating1 in info1.items():
rating2 = info2.get(item1, -1)
if rating2 == -1:
continue
vec1.append(rating1)
vec2.append(rating2)
similarity_matrix[u1][u2] = np.corrcoef(vec1, vec2)[0][1] # 计算不同用户之间的皮尔逊相关系数
print(similarity_matrix)
target_user = 'y'
topN = 2
sim_users = similarity_matrix[target_user].sort_values(ascending=False)[1:topN+1].index.tolist() # 去除自身
print(f'与用户{target_user}最相似的{topN}个用户为:{sim_users}')
weighted_scores = 0.
corr_values_sum = 0.
target_item = 'k'
for user in sim_users:
sim_y_Xi = similarity_matrix[target_user][user]
Xi_mean_rating = np.mean(list(user_data[user].values())) # 用户Xi的平均评分
weighted_scores += sim_y_Xi * (user_data[user][target_item] - Xi_mean_rating) # 分子
corr_values_sum += sim_y_Xi # 分母
y_mean_rating = np.mean(list(user_data[target_user].values()))
target_item_pred = y_mean_rating + weighted_scores / corr_values_sum # 预测用户y对物品k的评分
print(f'用户{target_user}对物品{target_item}的预测评分为:{target_item_pred}')
输出:
y X1 X2 X3 X4
y 1.000000 0.852803 0.707107 0.000000 -0.792118
X1 0.852803 1.000000 0.467707 0.489956 -0.900149
X2 0.707107 0.467707 1.000000 -0.161165 -0.466569
X3 0.000000 0.489956 -0.161165 1.000000 -0.641503
X4 -0.792118 -0.900149 -0.466569 -0.641503 1.000000
与用户y最相似的2个用户为:['X1', 'X2']
用户y对物品k的预测评分为:4.871979899370592
3. 优缺点
4. 算法评估
UserCF和ItemCF结果评估是一致的。
1. 计算方法
ItemCF算法并不利用物品的内容属性计算物品之间的相似度,而是通过分析用户的行为记录来计算物品之间的相似度。该算法认为,物品和物品具有很大的相似度是因为喜欢的用户也可能喜欢。
| 用户 | 物品 | 物品 | 物品 | ··· | 物品 |
|---|---|---|---|---|---|
| 用户 | 1 | 0 | 1 | ... | ? |
| 用户 | 0 | 1 | 1 | ... | 0 |
| 用户 | 1 | 0 | 0 | ... | 1 |
2. 代码实现
import numpy as np
import pandas as pd
# 物品-用户评分数据
item_data = {'A': {'y': 5.0, 'X1': 3.0, 'X2': 4.0, 'X3': 3.0, 'X4': 1.0},
'B': {'y': 3.0, 'X1': 1.0, 'X2': 3.0, 'X3': 3.0, 'X4': 5.0},
'C': {'y': 4.0, 'X1': 2.0, 'X2': 4.0, 'X3': 1.0, 'X4': 5.0},
'D': {'y': 4.0, 'X1': 3.0, 'X2': 3.0, 'X3': 5.0, 'X4': 2.0},
'E': {'X1': 3.0, 'X2': 5.0, 'X3': 4.0, 'X4': 1.0}
}
item_names = list(item_data.keys())
# 物品相似度矩阵,行列均为物品名称
similarity_matrix = pd.DataFrame(
np.identity(len(item_data)),
index=item_names,
columns=item_names
)
# 遍历每条物品-用户评分数据
for i1, info1 in item_data.items():
for i2, info2 in item_data.items():
if i1 == i2:
continue
vec1, vec2 = [], [] # 物品i1, i2对应的评分向量
for user, rating1 in info1.items():
rating2 = info2.get(user, -1)
if rating2 == -1:
continue
vec1.append(rating1)
vec2.append(rating2)
similarity_matrix[i1][i2] = np.corrcoef(vec1, vec2)[0][1] # 计算不同物品之间的皮尔逊相关系数
print(similarity_matrix)
target_user = 'y'
target_item = 'E'
topN = 2
sim_items = []
sim_items_list = similarity_matrix[target_item].sort_values(ascending=False).index.tolist()
for item in sim_items_list:
# 如果target_user对物品item评分过,也就是排除了target_item本身
if target_user in item_data[item]:
sim_items.append(item)
if len(sim_items) == topN:
break
print(f'与物品{target_item}最相似的{topN}个物品为:{sim_items}')
k_mean_rating = np.mean(list(item_data[target_item].values())) # 物品k的平均评分
weighted_scores = 0.
corr_values_sum = 0.
for item in sim_items:
sim_k_Ii = similarity_matrix[target_item][item] # 物品k与物品I_i的相似度
Ii_mean_rating = np.mean(list(item_data[item].values())) # 物品I_i的平均评分
weighted_scores += sim_k_Ii * (item_data[item][target_user] - Ii_mean_rating) # 分子
corr_values_sum += sim_k_Ii # 分母
target_item_pred = k_mean_rating + weighted_scores / corr_values_sum
print(f'用户{target_user}对物品{target_item}的预测评分为:{target_item_pred}')
输出:
A B C D E
A 1.000000 -0.476731 -0.123091 0.532181 0.969458
B -0.476731 1.000000 0.645497 -0.310087 -0.478091
C -0.123091 0.645497 1.000000 -0.720577 -0.427618
D 0.532181 -0.310087 -0.720577 1.000000 0.581675
E 0.969458 -0.478091 -0.427618 0.581675 1.000000
与物品E最相似的2个物品为:['A', 'D']
用户y对物品E的预测评分为:4.6
3. 优缺点
1. 计算方法
若用户和用户之间除了购买过外,还购买过商品,则认为两件商品是具有某种程度上的相似的(准确说是“相关度”或者“互补性”)。
为了衡量的相似性,比较同时购买了物品的用户,如果这两个用户共同购买的物品越少,即这两个用户原始兴趣不相似,但仍同时购买了两个相同的物品,则物品的相似性越高。
其中 是点击过商品的用户集合, 是用户u点击过的商品集合,是平滑系数,, 是用户权重参数,来降低活跃用户的影响。
2. 代码实现
import numpy as np
import pandas as pd
# 物品-用户点击数据
item_data = {
'A': {'y': 1, 'X1': 1, 'X2': 1, 'X3': 1},
'B': {'y': 1, 'X1': 1, 'X4': 1},
'C': {'y': 0, 'X2': 1, 'X3': 1, 'X4': 1},
'D': {'y': 1, 'X1': 1, 'X3': 1, 'X4': 1},
'E': {'X2': 1, 'X3': 1, 'X4': 1}
}
item_names = list(item_data.keys())
# 物品相似度矩阵,行列均为物品名称
similarity_matrix = pd.DataFrame(
np.zeros((len(item_data), len(item_data))),
index=item_names,
columns=item_names
)
# 计算物品相似度矩阵
alpha = 0.5
for i1 in item_data.keys():
for i2 in item_data.keys():
if i1 == i2:
continue
common_users = set(item_data[i1].keys()).intersection(set(item_data[i2].keys())) # 共同点击过i1, i2的用户
if not common_users:
similarity_matrix[i1][i2] = 0
continue
sum_weights = 0
for u in common_users:
for v in common_users:
if u == v:
continue # 跳过同一个用户
I_u = set(item for item, users in item_data.items() if u in users)
I_v = set(item for item, users in item_data.items() if v in users)
I_u_v = I_u.intersection(I_v)
weight = 1 / (np.sqrt(len(I_u)) * np.sqrt(len(I_v)) * (alpha + len(I_u_v)))
sum_weights += weight
similarity_matrix[i1][i2] = sum_weights
print("物品相似度矩阵:")
print(similarity_matrix)
# 计算推荐评分
target_user = 'y'
target_item = 'E'
topN = 2
# 获取与目标物品最相似的topN物品
sim_items = []
sim_items_list = similarity_matrix[target_item].sort_values(ascending=False).index.tolist()
for item in sim_items_list:
# 排除target_item本身
if target_user in item_data[item]:
sim_items.append(item)
if len(sim_items) == topN:
break
print(f'与物品{target_item}最相似的{topN}个物品为:{sim_items}')
k_mean_rating = np.mean([item_data[target_item][user] for user in item_data[target_item]]) # 物品k的平均评分
weighted_scores = 0.
corr_values_sum = 0.
for item in sim_items:
sim_k_Ii = similarity_matrix[target_item][item] # 物品k与物品I_i的相似度
Ii_mean_rating = np.mean([item_data[item][user] for user in item_data[item]]) # 物品I_i的平均评分
weighted_scores += sim_k_Ii * (item_data[item][target_user] - Ii_mean_rating) # 分子
corr_values_sum += sim_k_Ii # 分母
target_item_pred = k_mean_rating + weighted_scores / corr_values_sum if corr_values_sum != 0 else k_mean_rating
print(f'用户{target_user}对物品{target_item}的购买预测为:{target_item_pred}')
输出:
物品相似度矩阵:
A B C D E
A 0.000000 0.164957 0.538754 0.538754 0.164957
B 0.164957 0.000000 0.142857 0.538754 0.000000
C 0.538754 0.142857 0.000000 0.428571 0.538754
D 0.538754 0.538754 0.428571 0.000000 0.142857
E 0.164957 0.000000 0.538754 0.142857 0.000000
与物品E最相似的2个物品为:['C', 'A']
用户y对物品E的购买预测为:0.42580767318794366
FunRec实现(未给出相似度之后的计算):
from itertools import combinations
import pandas as pd
def load_data(train_path):
train_data = pd.read_csv(train_path, sep="\t", engine="python", names=["userid", "itemid", "rate"]) # 提取用户交互记录数据
train_data = train_data.drop(0) # 第一行是列名
train_data = train_data.astype(int) # 转换数据类型
print(train_data.head(3))
return train_data
def get_uitems_iusers(train):
u_items = dict()
i_users = dict()
for index, row in train.iterrows(): # 处理用户交互记录
u_items.setdefault(row["userid"], set()) # 初始化该用户row["userid"]
i_users.setdefault(row["itemid"], set()) # 初始化该物品
u_items[row["userid"]].add(row["itemid"]) # 得到该用户交互过的所有item
i_users[row["itemid"]].add(row["userid"]) # 得到该物品交互过的所有user
print("使用的用户个数为:{}".format(len(u_items)))
print("使用的item个数为:{}".format(len(i_users)))
return u_items, i_users
def swing_model(u_items, i_users, alpha):
# print([i for i in i_users.values()][:5])
# print([i for i in u_items.values()][:5])
item_pairs = list(combinations(i_users.keys(), 2)) # C(n,2) = (n!)/(2!(n-2)!) = (n(n-1))/2 = 50*49/2 = 1225
print("item pairs length:{}".format(len(item_pairs)))
item_sim_dict = dict()
for (i, j) in item_pairs:
user_pairs = list(combinations(i_users[i] & i_users[j], 2)) # 共同交互过i, j的用户对
result = 0
for (u, v) in user_pairs:
result += 1 / (alpha + list(u_items[u] & u_items[v]).__len__()) # \frac1{\alpha+|I_u\cap I_v|},
if result != 0:
item_sim_dict.setdefault(i, dict()) # 初始化物品i
item_sim_dict[i][j] = format(result, '.6f')
return item_sim_dict # 返回item相似度字典, key为item_id, value为与该item相似的item_id及相似度
def save_item_sims(item_sim_dict, top_k, path):
new_item_sim_dict = dict()
try:
writer = open(path, 'w', encoding='utf-8')
for item, sim_items in item_sim_dict.items():
new_item_sim_dict.setdefault(item, dict())
new_item_sim_dict[item] = dict(
sorted(sim_items.items(), key=lambda k: k[1], reverse=True)[:top_k]) # 排序取出 top_k个相似的item
writer.write('item_id:%d\t%s\n' % (item, new_item_sim_dict[item]))
print("SUCCESS: top_{} item saved".format(top_k))
except Exception as e:
print(e.args)
if __name__ == "__main__":
train_data_path = "input/ratings_final.txt"
item_sim_save_path = "output/item_sim_dict.txt"
alpha = 0.5
top_k = 10 # 与item相似的前 k 个item
train = load_data(train_data_path)
u_items, i_users = get_uitems_iusers(train)
item_sim_dict = swing_model(u_items, i_users, alpha)
save_item_sims(item_sim_dict, top_k, item_sim_save_path)
输出:
item_id:10 {33: '9.369308', 31: '7.928368', 47: '5.119530', 29: '4.917776', 26: '4.831854', 5: '4.683046', 44: '4.008186', 34: '3.672097', 39: '3.564048', 35: '3.440806'}
item_id:40 {39: '6.158427', 23: '4.632332', 33: '4.158849', 31: '3.806523', 29: '3.648068', 37: '3.619602', 44: '3.429525', 47: '3.339190', 13: '2.779905', 38: '2.730002'}
item_id:38 {24: '6.053660', 34: '5.870747', 37: '3.573692', 16: '3.442025', 22: '3.350414', 44: '3.142554', 33: '3.035718', 19: '2.920647', 1: '2.838041', 43: '2.728516'}
item_id:41 {24: '6.010128', 35: '5.212512', 13: '4.167028', 33: '4.107981', 42: '3.528947', 31: '3.508580', 46: '3.186477', 5: '3.029635', 12: '2.768056', 16: '2.493831'}
item_id:1 {31: '7.446685', 33: '5.682953', 26: '5.509304', 25: '4.267118', 19: '3.768161', 42: '3.599587', 34: '3.567465', 47: '3.367748', 13: '3.305826', 29: '3.260593'}
item_id:19 {47: '7.296887', 29: '5.516440', 31: '4.385619', 34: '4.169741', 5: '3.913757', 13: '3.611356', 44: '3.491264', 28: '3.002532', 33: '2.969671', 35: '2.744241'}
item_id:50 {33: '8.690459', 27: '5.078108', 29: '4.640582', 47: '4.618278', 46: '4.478693', 35: '3.572142', 26: '3.049875', 13: '3.017851', 16: '2.997565', 44: '2.921754'}
item_id:49 {44: '3.339786', 29: '2.908996', 22: '2.822287', 26: '2.804519', 8: '2.512262', 14: '2.290975', 35: '2.152327', 46: '1.932279', 47: '1.859695', 5: '1.851126'}
item_id:18 {33: '9.920042', 39: '4.963052', 13: '3.424331', 44: '3.095113', 26: '3.067319', 28: '3.026770', 42: '2.541916', 11: '2.273064', 27: '2.131459', 12: '1.968478'}
item_id:17 {35: '4.230042', 44: '3.490393', 33: '2.887144', 16: '2.579394', 34: '2.490362', 29: '1.976302', 39: '1.716705', 27: '1.653835', 46: '1.594548', 15: '1.590245'}
item_id:29 {15: '7.765038', 31: '7.699624', 13: '7.647818', 23: '7.624496', 47: '6.841698', 26: '6.210281', 8: '6.187537', 39: '5.725741', 24: '5.666022', 6: '5.411497'}
item_id:36 {24: '5.823616', 37: '4.367127', 47: '4.106552', 12: '2.964950', 42: '2.911300', 33: '2.708663', 25: '2.404134', 16: '2.350667', 31: '2.273338', 13: '2.137498'}
item_id:4 {31: '5.609852', 44: '4.414842', 33: '4.036755', 24: '4.017716', 39: '2.561788', 47: '2.521624', 13: '2.157043', 26: '1.741391', 35: '1.683751', 15: '1.653737'}
item_id:45 {26: '5.482775', 35: '4.646579', 34: '4.081938', 11: '3.202662', 13: '3.001079', 30: '2.863475', 39: '2.676639', 24: '2.616236', 2: '2.573928', 5: '2.481827'}
item_id:27 {31: '5.421499', 26: '3.988214', 33: '3.633894', 21: '3.493322', 5: '3.339473', 16: '3.085929', 24: '2.921068', 35: '2.631777', 47: '2.299567', 28: '2.220544'}
item_id:44 {33: '7.679608', 35: '4.741540', 31: '4.397215', 37: '4.138681', 34: '4.053771', 39: '4.050300', 46: '3.907530', 48: '3.647042', 26: '3.598477', 15: '3.300210'}
item_id:26 {34: '6.816138', 31: '6.682804', 39: '5.334259', 33: '4.829264', 25: '4.774081', 5: '4.623790', 35: '4.499845', 3: '4.092521', 8: '3.597685', 13: '3.552098'}
item_id:37 {24: '6.843369', 47: '6.510498', 12: '6.430014', 43: '4.463759', 16: '4.037183', 31: '3.804751', 11: '3.745736', 33: '3.483176', 13: '3.426464', 25: '3.370829'}
item_id:21 {24: '5.424326', 33: '4.635283', 16: '3.070736', 31: '3.033045', 5: '3.022268', 34: '2.890594', 12: '2.462445', 2: '2.203518', 25: '1.944303', 47: '1.884469'}
item_id:39 {34: '5.951387', 11: '4.709477', 35: '4.594422', 24: '3.859427', 31: '3.639452', 28: '3.612227', 5: '3.518218', 8: '3.281585', 13: '3.192304', 25: '2.769550'}
item_id:48 {34: '4.169734', 47: '3.352142', 33: '2.325230', 31: '2.073971', 24: '1.853498', 43: '1.662014', 15: '1.615838', 5: '1.573125', 8: '1.377898', 23: '0.962713'}
item_id:33 {47: '9.778090', 31: '8.583368', 24: '7.860965', 42: '7.855050', 13: '7.480130', 35: '7.015999', 5: '6.818251', 12: '6.764609', 11: '6.272421', 43: '6.185646'}
item_id:31 {16: '7.105310', 5: '6.804654', 34: '6.040925', 42: '5.351108', 12: '5.088411', 13: '4.974254', 6: '4.522709', 47: '4.510200', 25: '4.144825', 24: '4.031206'}
item_id:8 {34: '7.980686', 22: '6.623847', 13: '3.555553', 24: '3.101205', 23: '3.059310', 6: '2.255133', 16: '2.133014', 46: '2.065485', 11: '2.017622', 42: '1.994395'}
item_id:42 {13: '7.610054', 47: '6.245807', 46: '4.083513', 24: '3.970524', 28: '2.697660', 16: '2.691497', 32: '2.042380', 22: '1.950787', 2: '1.864192', 11: '1.760448'}
item_id:22 {34: '6.418238', 47: '3.978257', 16: '3.115300', 46: '3.068168', 32: '2.713828', 13: '2.024774', 24: '1.994894', 6: '1.452366', 23: '1.434605', 12: '1.264802'}
item_id:46 {35: '6.741871', 24: '6.465305', 13: '5.543031', 47: '3.959384', 16: '3.904737', 14: '2.100597', 34: '2.009843', 23: '1.665809', 43: '1.365662', 9: '1.318219'}
item_id:24 {13: '9.051286', 12: '8.932852', 16: '7.994105', 47: '7.056703', 25: '5.138607', 34: '4.107616', 5: '3.916675', 23: '3.865081', 35: '3.327251', 11: '2.944826'}
item_id:30 {16: '4.278808', 34: '2.566232', 47: '1.778311', 3: '1.582892', 23: '1.169706', 11: '1.074682', 15: '1.071165', 6: '0.860447', 12: '0.638951', 5: '0.614136'}
item_id:32 {47: '2.562393', 11: '1.757731', 28: '1.635400', 5: '1.569821', 13: '1.463118', 23: '1.066609', 43: '1.011198', 12: '0.930438', 25: '0.924289', 35: '0.776113'}
item_id:14 {35: '2.999675', 16: '2.486331', 6: '2.298938', 12: '2.042664', 34: '1.674629', 25: '1.556820', 13: '1.454499', 28: '1.286698', 2: '1.209419', 7: '1.176068'}
item_id:43 {5: '6.015474', 16: '5.777772', 47: '4.198979', 35: '3.899262', 34: '2.491394', 15: '2.108719', 2: '1.653933', 28: '1.527717', 13: '1.513159', 23: '1.143745'}
item_id:28 {47: '4.465181', 34: '2.392612', 16: '2.009888', 35: '2.006491', 2: '1.871937', 13: '1.650812', 23: '1.515468', 5: '1.227799', 25: '1.181200', 3: '1.157692'}
item_id:7 {13: '1.737218', 35: '1.722211', 6: '1.688831', 16: '1.373115', 11: '1.012005', 25: '0.942200', 9: '0.825241', 12: '0.781583', 20: '0.404827', 34: '0.384314'}
item_id:2 {25: '2.863793', 13: '2.409164', 35: '2.256263', 34: '2.223412', 5: '1.959987', 3: '1.756192', 23: '1.414545', 16: '1.289138', 15: '1.263231', 12: '0.748327'}
item_id:11 {12: '6.191016', 13: '3.205327', 35: '2.463550', 47: '2.132076', 16: '1.844691', 5: '1.691581', 34: '1.441119', 6: '0.977622', 9: '0.929768', 23: '0.866365'}
item_id:47 {34: '7.172651', 13: '6.306686', 16: '5.733066', 23: '5.680964', 5: '4.475031', 20: '3.314331', 12: '2.738435', 3: '1.958704', 15: '1.652260', 9: '1.638983'}
item_id:5 {12: '6.600787', 13: '5.907053', 34: '4.839141', 25: '4.743027', 35: '3.908568', 15: '2.732167', 6: '1.742200', 23: '1.735456', 16: '1.714101', 3: '1.681806'}
item_id:35 {12: '4.986266', 13: '4.623114', 16: '3.369654', 34: '2.442132', 9: '2.008932', 25: '1.866088', 15: '1.683837', 6: '0.799530', 3: '0.778766', 20: '0.153846'}
item_id:34 {16: '6.049411', 15: '5.894930', 6: '5.486742', 23: '5.281373', 13: '3.893850', 25: '3.873124', 12: '2.577946', 3: '2.081190', 20: '1.876311', 9: '0.382418'}
item_id:23 {13: '3.898035', 6: '2.681844', 3: '1.643481', 16: '1.295438', 15: '1.281638', 12: '0.598291', 25: '0.117647', 20: '0.117647', 9: '0.095238'}
item_id:25 {13: '5.461985', 12: '4.039666', 20: '1.690132', 6: '1.649728', 16: '1.481843', 9: '0.694180', 15: '0.559179', 3: '0.556820'}
item_id:12 {13: '6.455876', 6: '3.627968', 20: '2.449737', 16: '2.022631', 9: '1.332711', 3: '0.295739'}
item_id:15 {13: '2.303888', 3: '1.099235', 16: '1.020237', 6: '0.783844', 9: '0.692284', 20: '0.286959'}
item_id:20 {6: '2.109553', 16: '1.629160', 13: '0.849959', 3: '0.153846', 9: '0.133333'}
item_id:16 {6: '2.801347', 13: '1.961959', 3: '0.964732', 9: '0.420513'}
item_id:3 {13: '0.619418', 6: '0.133333'}
item_id:9 {13: '1.410797', 6: '0.133333'}
item_id:6 {13: '0.389140'}
1. 类别层面
2. 商品层面
3. 聚类层面
4. 线性组合
Surprise算法通过利用类别信息和标签传播技术解决了用户共同购买图上的稀疏性问题。
1. 预测打分
假设个用户,部电影有个特征。我们预测
例如:
P矩阵:
| 特征 | 特征 | 特征 | |
|---|---|---|---|
| 用户 | 0.1 | 0.9 | 0.6 |
| 用户 | 0.8 | 0.5 | 0.4 |
Q矩阵:
| 电影 | 电影 | 电影 | 电影 | |
|---|---|---|---|---|
| 特征 | 0.7 | 0.2 | 0.3 | 0.4 |
| 特征 | 0.1 | 0.6 | 0.9 | 0.2 |
| 特征 | 0.5 | 0.8 | 0.4 | 0.1 |
因此:
即:
| 电影 | 电影 | 电影 | 电影 | |
|---|---|---|---|---|
| 用户 | 0.46 | 1.04 | 1.08 | 0.28 |
| 用户 | 0.81 | 0.78 | 0.85 | 0.46 |
2. 反推用户、物品矩阵
2.1 FunkSVD(Latent Factor Model, LFM)
因为实际获取到的一般是稀疏矩阵,我们需要反推,。因此损失函数就是预测的与真实的之间的误差,可选MSE。注意正则化的时候需要考虑每个用户对电影打分的数量不同,因此损失函数是:
2.2 BiasSVD(Bias Latent Factor Model)
在FunkSVD的基础上,加入用户偏好和物品偏好,即:
import random
import math
class BiasSVD:
def __init__(self, rating_data, F=5, alpha=0.1, lmbda=0.1, max_iter=1000):
self.F = F # 这个表示隐向量的维度
self.P = dict() # 用户矩阵P 大小是[users_num, F]
self.Q = dict() # 物品矩阵Q 大小是[item_nums, F]
self.bu = dict() # 用户偏置系数
self.bi = dict() # 物品偏置系数
self.mu = 0 # 全局偏置系数
self.alpha = alpha # 学习率
self.lmbda = lmbda # 正则项系数
self.max_iter = max_iter # 最大迭代次数
self.rating_data = rating_data # 评分矩阵
for user, items in self.rating_data.items():
# 初始化矩阵P和Q。随机数根据经验,和1/sqrt(F)成正比
self.P[user] = [random.random() / math.sqrt(self.F) for x in range(0, F)]
self.bu[user] = 0
for item, rating in items.items():
if item not in self.Q:
self.Q[item] = [random.random() / math.sqrt(self.F) for x in range(0, F)]
self.bi[item] = 0
# 采用随机梯度下降的方式训练模型参数
def train(self):
cnt, mu_sum = 0, 0
for user, items in self.rating_data.items():
for item, rui in items.items():
mu_sum, cnt = mu_sum + rui, cnt + 1
self.mu = mu_sum / cnt
for step in range(self.max_iter):
# 遍历所有的用户及历史交互物品
for user, items in self.rating_data.items():
# 遍历历史交互物品
for item, rui in items.items():
rhat_ui = self.predict(user, item) # 评分预测
e_ui = rui - rhat_ui # 评分预测偏差
# 参数更新
# $$b_u=b_u+\alpha(e_{ui}-\lambda b_u)$$
# $$b_i=b_i+\alpha(e_{ui}-\lambda b_i)$$
self.bu[user] += self.alpha * (e_ui - self.lmbda * self.bu[user])
self.bi[item] += self.alpha * (e_ui - self.lmbda * self.bi[item])
for k in range(0, self.F):
# p_{u,k}=p_{u,k}+\alpha e_{ui}q_{i,k}-\lambda p_{u,k}
# q_{i,k}=q_{i,k}+\alpha e_{ui}p_{u,k}-\lambda q_{i,k}
self.P[user][k] += self.alpha * (e_ui * self.Q[item][k] - self.lmbda * self.P[user][k])
self.Q[item][k] += self.alpha * (e_ui * self.P[user][k] - self.lmbda * self.Q[item][k])
# 逐步降低学习率
self.alpha *= 0.9
# 评分预测
def predict(self, user, item):
# $$r_{ij}^{\prime}=\mu+b_u+b_i+\boldsymbol{p}^{(i)}\boldsymbol{q}_j$$
return sum(self.P[user][f] * self.Q[item][f] for f in range(0, self.F))\
+ self.bu[user] + self.bi[item] + self.mu
# 加载数据
rating_data = {'y': {'A': 5, 'B': 3, 'C': 4, 'D': 4},
'X1': {'A': 3, 'B': 1, 'C': 2, 'D': 3, 'k': 3},
'X2': {'A': 4, 'B': 3, 'C': 4, 'D': 3, 'k': 5},
'X3': {'A': 3, 'B': 3, 'C': 1, 'D': 5, 'k': 4},
'X4': {'A': 1, 'B': 5, 'C': 5, 'D': 2, 'k': 1}
}
biassvd = BiasSVD(rating_data, F=10)
biassvd.train()
# 预测用户y对物品k的评分
for item in ['k']:
print(item, biassvd.predict('y', item))
输出(不固定):
k 4.998386122960065
2.3 MF小结
用户和物品都用隐向量的形式存放,空间复杂度从降到,其中是隐向量的维度。MF模型的优点是可以发现用户和物品的隐含特征,但是也有一些缺点:
MF的目标是从交互的结果中计算出用户和物品的特征。而FM则正好相反,希望通过物品的特征和某个用户点击这些物品的历史记录,预测该用户点击其他物品的概率,即点击率(click through rate,CTR)。
因为物品之间可能存在关联,所以也要引入双线性模型,记为物品的特征向量,为第个特征,为第个特征和第个特征的权重。FM模型的公式为:
即,其中对应偏置项。时间复杂度。
如果将物品离散编码,可以使用多域独热编码multi-field one-hot encoding,将物品的多个特征各自独热编码成的向量依次拼接起来。比如:
在此编码方式下,在大多数时候等于0,无法更新参数。矩阵分解后能解决这一问题,其中,是隐向量的维度。此时算法的时间复杂度为。
算法优化后时间复杂度:
其中,是第个特征的第个隐向量。由于本文章需要将 FM 模型用在召回,故将二阶特征交互项拆分为用户和物品项。记表示用户相关特征集合,表示物品相关特征集合。有:
为什么不直接将FM中学习到的User Embedding:和Item Embedding:text的内积做召回呢?
因为用户喜欢的未必一定是与自身最匹配的,也包括一些自身性质极佳的item(e.g.热门item),所以,非常有必要将所有Item特征一阶权重之和和所有Item特征隐向量两两点积之和考虑进去。
化简过程见hml第7.3节以及(更推荐)FunRec第2.1.2节
1. UserCF和ItemCF的区别
UserCF:适用于用户少,物品多,时效性较强的场合,比较的是人与人之间的相似度,具备更强的社交属性。可以发现用户潜在的尚未发现的兴趣。例如新闻等。
ItemCF:适用于物品少,用户多,物品相对稳定,用户兴趣稳定的场合,比较的是物品与物品之间的相似度,具备更强的个性化推荐属性。例如电影、音乐等。
2. Swing与MF相较于传统CF的优势
Swing:通过用户共同购买的事件计算物品之间的相似度。
MF:通过矩阵分解的方式,将用户-物品交互矩阵分解为两个低维矩阵的乘积,得到用户和物品的Embedding。
3. 协同过滤算法的权重改进
base 公式
对热门物品进行惩罚
控制对热门物品的惩罚力度
对活跃用户的惩罚
3. 协同过滤算法的问题
Embedding就是用一个低维、稠密的向量表示一个对象,相当于对One-hot编码做了平滑处理。它将用户和物品映射到低维空间,通过内积计算相似度。
主要分为i2i和u2u两种召回,i2i得到的是物品的embedding,u2u得到的是用户和物品的embedding。
"You shall know a word by the company it keeps" (J. R. Firth 1957: 11)
分为Skip-gram和CBOW两种模型。
计算过程对比:
左图是根据中心词believe预测上下文的过程,右图是根据上下文预测中心词believe的过程。
中心词维度变化是:,式中是的列数。第一步是通过将中心词的one-hot编码转换为低维向量,第二步是通过上下文矩阵与softmax将低维向量转换为上下文词的概率分布。
设独热编码矩阵为,中心词向量为,上下文词向量为,,分别是隐编码和输出概率的权重矩阵。独热编码里的第行的低维编码就是的第行,因此维度从降到,其中是的列数。
是文本长度,是上下文窗口大小,是中心词,是上下文词,具体而言是。
使用中心词和上下文词的相似性来计算概率,最后得到损失函数:
1. Skip-gram
Skip-gram的损失函数为:
其中,,是上下文词的向量, 是中心词的向量, 是词汇表的大小。
2. CBOW
CBOW的损失函数为:
其中,,,是上下文词的向量, 是中心词的向量,是上下文词的平均向量,是上下文词的个数。
3. 代码实现
注:FastText与Word2Vec的训练代码几乎一致,因此这里不给出FastText的代码。
from gensim.models import Word2Vec
# 示例语料
sentences = [
["I", "love", "natural", "language", "processing"],
["I", "enjoy", "machine", "learning"],
["I", "love", "programming"],
["natural", "language", "processing", "is", "fun"],
["machine", "learning", "is", "fun"],
["programming", "is", "fun"],
["machine", "learning", "is", "amazing"],
["deep", "learning", "is", "a", "subset", "of", "machine", "learning"],
["artificial", "intelligence", "is", "the", "future"]
]
# 参数vector_size表示词向量的维度,window表示上下文窗口大小,min_count表示最小词频(设置为1,表示所有出现过的词都将被考虑),sg表示模型选择(0表示CBOW,1表示Skip-gram)
# 使用 Skip-gram 模型训练词向量
skipgram_model = Word2Vec(sentences, vector_size=50, window=3, min_count=1, sg=1, seed=42)
# 使用 CBOW 模型训练词向量
cbow_model = Word2Vec(sentences, vector_size=50, window=3, min_count=1, sg=0, seed=42)
SG_word_vectors = skipgram_model.wv # 获取 Skip-gram 模型的词向量
CBOW_word_vectors = cbow_model.wv # 获取 CBOW 模型的词向量
print(SG_word_vectors['natural']) # 查看'natural'的词向量
print(SG_word_vectors.similarity('love', 'enjoy')) # 计算'love'和'enjoy'的相似度
print(CBOW_word_vectors['natural'])
print(CBOW_word_vectors.similarity('love', 'enjoy'))
输出(因为设置了随机种子为42,所以输出不变):
[ 1.99211426e-02 1.10961189e-02 -7.18320208e-03 1.88729670e-02
-2.77304462e-05 2.97273145e-05 -2.65200273e-03 -1.42441131e-02
1.73749384e-02 -1.94424614e-02 -1.30288973e-02 -1.08137988e-02
-8.82801600e-03 -1.47270644e-02 -1.92814860e-02 7.10630929e-03
1.87723450e-02 -1.51267508e-02 -8.13869014e-03 2.53136037e-04
1.71001330e-02 7.77058257e-03 1.06474981e-02 3.24456976e-03
-3.31867533e-03 -1.20089147e-02 1.45726334e-02 1.21650817e-02
-1.89958680e-02 8.61627050e-03 -1.66244879e-02 9.55938827e-03
1.30381957e-02 -1.47577636e-02 -2.64655659e-03 -1.50499176e-02
2.69118510e-03 1.71025433e-02 -1.67651549e-02 -4.09697322e-03
1.89247914e-02 -7.96200614e-03 -1.47113595e-02 -4.56641690e-04
3.34811688e-04 6.51462236e-03 -7.45060900e-03 1.82248317e-02
8.40980443e-04 -8.54207668e-03]
0.09877222
[ 1.99211426e-02 1.10961189e-02 -7.18320208e-03 1.88729670e-02
-2.77304462e-05 2.97273145e-05 -2.65200273e-03 -1.42441131e-02
1.73749384e-02 -1.94424614e-02 -1.30288973e-02 -1.08137988e-02
-8.82801600e-03 -1.47270644e-02 -1.92814860e-02 7.10630929e-03
1.87723450e-02 -1.51267508e-02 -8.13869014e-03 2.53136037e-04
1.71001330e-02 7.77058257e-03 1.06474981e-02 3.24456976e-03
-3.31867533e-03 -1.20089147e-02 1.45726334e-02 1.21650817e-02
-1.89958680e-02 8.61627050e-03 -1.66244879e-02 9.55938827e-03
1.30381957e-02 -1.47577636e-02 -2.64655659e-03 -1.50499176e-02
2.69118510e-03 1.71025433e-02 -1.67651549e-02 -4.09697322e-03
1.89247914e-02 -7.96200614e-03 -1.47113595e-02 -4.56641690e-04
3.34811688e-04 6.51462236e-03 -7.45060900e-03 1.82248317e-02
8.40980443e-04 -8.54207668e-03]
0.098765165
import re
from gensim.models import Word2Vec
from easier_excel import read_data
read_data.set_pd_option()
raw_df = read_data.read_df(path="data/ag_news/dataset/train.csv")
# 对rar_df的第三列(新闻内容)逐行进行处理并添加到列表里
text_list = [row[2] for row in raw_df.values]
# 对text_list的每个元素进行处理:去掉非字母字符,转小写,然后分割为单词列表
for i, text in enumerate(text_list):
text = re.sub('[^A-Za-z]+', ' ', text).strip().lower()
text_list[i] = text.split(" ")
# 使用word2vec训练词向量
path = "../model/word2vec/ag_news_word2vec.model"
model = Word2Vec(sentences=text_list, vector_size=100, window=5, min_count=5, workers=4)
model.save(path) # 保存模型
model = Word2Vec.load(path) # 加载模型
print("词向量矩阵的形状:", model.wv.vectors.shape)
print("nice的相似词:", model.wv.most_similar("nice"))
print("寻找不同词:", model.wv.doesnt_match("good nice terrible".split()))
print("获取相似度:", model.wv.similarity("happy", "excited"))
# # 获取词向量
# print(model.wv['good'])
# # 获取所有词
# print(model.wv.index_to_key)
# # 获取词向量矩阵
# print(model.wv.vectors)
输出:
词向量矩阵的形状: (23777, 100)
nice的相似词: [('happy', 0.8072608709335327), ('pretty', 0.7431333065032959), ('simple', 0.7430432438850403), ('feeling', 0.7355679273605347), ('lucky', 0.7346422672271729), ('always', 0.7252234220504761), ('quite', 0.7218983173370361), ('true', 0.7208051085472107), ('simply', 0.7169813513755798), ('favre', 0.7106463313102722)]
寻找不同词: terrible
获取相似度: 0.6986826
4. Word2Vec的问题
Item2Vec是Word2Vec的一个变种,用于学习物品的Embedding。它假设对于一个集合的物品,它们之间是相似的,与用户购买它们的顺序、时间无关。
损失函数:
在Skip-Gram模型中,每个单词有2个特征表示。Item2Vec同样如此,论文中是将物品的中心词向量作为物品的特征向量。作者还提到了其他两种方式来表示物品向量:add: ,concat: 。
Airbnb的推荐系统是基于Item2Vec的,它描述了两种 Embedding 的构建方法,分别为:
listing表示房源的意思。1. Listing Embeddings
假设用户最近的行为序列为,其中表示房源。使用 Skip-gram 模型,损失函数:
负采样后的损失函数:
改进--正负样本集构建的改进,增加了这两项:
booked listing,是用户在session中最终预定的房源,一般只会出现在结束位置,表示用户的最终选择,因此作为全局的上下文(正样本)。listing位于同一区域的负样本集,因为这些房源可能是用户可能选择的,但是用户没有选择。如果随机采集负样本,可能会有一些不合理的负样本,例如离用户选择的房源位置market太远的房源。改进--冷启动问题的解决:
listing被创建后,房主需要提供如位置、价格、类型等在内的信息。然后利用房主提供的房源信息,为其查找3个相似的listing,并将它们embedding的均值作为新listing的embedding表示。2. User-type & Listing-type Embeddings
Listing Embedding是基于用户的点击sessions学习得到的。同一个session内的点击时间间隔低于30分钟,所以它们更适合短期,session内的个性化需求。
长期兴趣的探索是基于booking session(用户的历史预定序列),Embedding的构建方法与Listing Embeddings类似,只是在构建时,将用户的行为序列替换为用户的类型序列,房源的行为序列替换为房源的类型序列。
问题:
booking sessions 数据量的大小远远小于click sessionssession无法学习到用户的兴趣booking间隔时间较长listing被预定的次数低于5-10次。因此,Airbnb提出了基于booking session来学习用户和房源的Type Embedding。给定一个booking sessions集合 ,其中包含了个用户的booking session:
booking session表示为:,这里表示listing, 学习到Embedding记作Listing Embedding,与相应的lisitng是一一对应的,只包含了listing的信息。
而Type Embedding包含了房源和用户的类型信息(User-type Embedding和Listing-type Embedding)。
对于不同的listing,它们的Type Embedding可能是相同的(User 同样如此)。比如:listing可能是apartment_5stars_villa,User可能是西藏人_男_学生_21岁_工作山东_南翔_挖掘机。这样避免过于稀疏的问题,使得冷启动、动态变化的问题得到解决。
User-type的目标函数:
Listing-type的目标函数:
listing和context的内积,而User-type Embedding是对user和context的内积,Listing-type Embedding是对user和listing的内积。reject样本,User-type Embedding是对user和listing的内积,Listing-type Embedding是对listing和user的内积。3. Airbnb检索策略
在给定学习到的Listing Embedding,通过计算其向量和来自同一区域的所有listing的向量之间的余弦相似度,可以找到给定房源的相似房源。
YouTubeDNN是一个深度学习模型,用于生成用户和视频的Embedding,目标是最大化观看时间,它的核心是一个深度神经网络,用于预测用户对视频的点击率。
召回模型的目的是在大量YouTube视频中检索出数百个和用户相关的视频来,也就是一个多分类的问题,即用户在某一个时刻点击了某个视频,可以建模成输入一个用户向量,从海量视频中预测出被点击的那个视频的概率。
1. YoutubeDNN的网络结构(召回模型)
图中模型的输出(ReLU上面)是就是用户u的embedding,图中箭头指向不是很好。。另外用户embedding的列维度等于视频embedding的行维度,视频embedding的列向量代表一个视频的的embedding。
输入主要是用户侧的特征,包括用户观看的历史video序列,用户搜索的历史tokens,用户的人文特征,如位置、性别、年龄。通过embedding层,将这些特征映射到低维空间,然后将这些特征拼接在一起,输入到DNN中进行降维,最后输出一个用户向量,而视频向量是模型本身的参数。
item_1,item_2,...,item_n,通过embedding层映射到低维空间,然后使用average pooling融合(每一维求平均得到一个最终向量来表示用户的历史兴趣或搜索兴趣)。论文中是每个item事先通过w2v方式算好了的embedding,直接作为了输入,然后进行pooling融合。2. "Example Age"特征
The example age is expressed as where is the maximum observed time in the training data.代表的是用户访问(或交互)这个视频的时间戳。
At serving time, this feature is set to zero (or slightly negative) to reflect that the model is making predictions at the very end of the training window.
效果图
其实就是现在常用的位置上的除偏,传统的机器学习往往是对整个时间求平均,而事实上新视频的点击率可能会比较高。而在用户点击视频时也往往选择上方的。这些都需要给模型加上位置的feature,这样模型就能学到这些特征。
为什么有用?可能用户喜欢的是新颖,而不是这个item。
为什么不定义成?虽然二者等价(因为其和为constant),但是因为example age在线上预测时是常量。
加上example age后的图
在线预估时满足低延时的要求,但是无法考虑到user和item特之间的特征交叉。
令表示user侧特征,表示item侧特征,双塔模型相当于假设, 也即假设A、B之间是独立的。相对单塔模型,双塔模型的精度损失应该源于上述假设的不完全成立。
1. 经典双塔模型DSSM
Deep Structured Semantic Model深度语义模型,是微软提出的一种用于文本检索的模型,用于计算query和doc之间的相似度。在推荐系统中,可以用于计算user和item之间的相似度。
user侧塔和item侧塔分别是一个DNN结构。输入特征,经过DNN得到embedding,然后计算两者之间的相似度(常用内积或者余弦值),因此对于user和item两侧最终得到的embedding维度需要保持一致,即最后一层全连接层隐藏单元个数相同。
这种结构是静态的,即在整个网络中,user侧和item侧的特征交叉是在最后一层全连接层发生的,而在前面的层级,user侧和item侧的特征是独立的。
实际应用时,离线计算item集合,因为item的会相对稳定,不会频繁的变动。在线服务需要实时的通过拼接用户特征,输入到user侧的DNN当中,进而得到user embedding,在通过user embedding去 faiss中进行ANN检索,召回最相似的个item embedding。
2. SENet
从上图可以看出SENET主要分为三个步骤Squeeze,Excitation, Re-weight,对应在推荐系统里是:
Squeeze阶段:对每个特征的Embedding向量进行数据压缩与信息汇总,即在Embedding维度计算均值:
Excitation阶段:这阶段是根据上一阶段得到的向量进行缩放,即将上阶段的得到的 的向量先压缩成 长度,然后在放回到 的维度,其中表示压缩的程度。这个过程的具体操作就是经过两层DNN。
Re-weight阶段:是将Excitation阶段得到的每个特征对应的权重再乘回到特征对应的Embedding里,就完成了对特征重要性的加权操作。
这种结构可以通过对特征embedding先压缩,再交互,再选择,进而实现特征选择的效果。即:提前把没用的特征降权,重要的升权。并且这是动态模型,对于某个特征,在某个输入组合里可能是没用的,但是换一个输入组合,有可能是重要特征。
相对FM模型,双塔DNN的优点是引入了非线性,但是因为这种非线性是在User侧特征之间/Item侧特征之间做的,而忽视了User侧和Item侧之间的特征交互。而单侧特征的多层非线性操作,可能反而会带来上面说的两侧特征交互太晚,细节信息损失的问题。而FM模型,特性和DNN双塔正好相反,它在User侧和Item侧交互方面比较有优势,因为没有深层,也没有非线性对单侧特征的深度融合,只在两侧特征Embedding层级发生交互作用,所以在特征Embedding层级能够更好地表达User侧和Item侧特征之间的交叉作用,当然,缺点是缺乏非线性。
SENet引入双塔模型可以凸显那些对高层User Embedding和Item Embedding的特征交叉起重要作用的特征,更有利于表达两侧的特征交互,避免单侧无效特征经过DNN双塔非线性融合时带来的噪声。
另外,双塔DNN很适合做多任务目标:
如上图所示,在user侧和item侧分别通过多个通道(DNN结构)为每个任务得到一个user embedding和item embedding,然后针对不同的目标分别计算user和item的相似度,并计算各个目标的损失,最后的优化目标可以是多个任务损失之和,或者使用多任务学习中的动态损失权重。
3. 双塔模型细节
3.1 归一化与温度系数
在Google的双塔召回模型中,重点介绍了两个trick,将user和item侧输出的embedding进行归一化以及对于內积值除以温度系数:
归一化:对user侧和item侧的输入embedding,进行L2归一化
温度系数:对归一化之后的向量计算内积之后,除以一个固定的超参 ,论文中命名为温度系数。
3.2 负样本
训练精排模型的时候(假设是优化点击目标),一般会用用户点击做为正例,曝光未点击做为负例,来训练模型。但在召回阶段,这样会出现:Sample Selection Bias(样本选择偏差)。对于召回模型来说,它的输入数据是所有物料库里的物品,如果仍然用曝光未点击做为召回和粗排的负例训练数据,那这个训练集合只是全局物料库的一小部分,与实际输入的分布差异比较大。可能使得模型泛化性差、忽略冷门物品。
各种负采样的对比表格如下:
| 方法 | 优点 | 缺点 |
|---|---|---|
| 曝光未点击数据 | 较好地模拟用户兴趣分布 | 负例不够随机,Sample Selection Bias |
| 全局随机选择负例 | 简单 | 负例质量不高,难以区分用户兴趣,模型能力不足 |
| Batch内随机选择负例 | 增强了负例的多样性,有利于模型训练 | 这个方法是在一个训练batch里,在所有其它用户的正例里进行随机选择,因此难以保证负例的真实性和代表性 |
| 曝光数据随机选择负例 | 较好地模拟用户兴趣分布 | 需要大量曝光数据 |
| 基于Popularity随机选择负例 | 选取热门物品作为负例,有助于提高模型的区分能力 | 容易导致流行度偏差,影响模型的泛化能力 |
| 基于Hard选择负例 | 选取与正例相似度较高的物品(最难区分的负例),增强模型的判别能力 | 难以保证负例的多样性,计算复杂度高,可能引入噪声数据,影响模型稳定性 |
总之,曝光未点击数据还是需要的,但是要与其他方法混合使用,还要考虑应用场景。
用户行为数据是一个图,物品是图的节点,物品之间的关系是图的边。对于相似结构的两个图,对应的节点和边的相似性也会比较高。避免embedding不相近但是实际上相近的物品,提高召回的准确性。
将所有的用户的行为序列转化成行为图,对于行为图的节点和,其权重是从物品到的转移概率。
随机游走:随机从一个节点开始,以的概率去选择邻接点,最终从图中采样出一些节点序列,然后通过Word2Vec的方式学习节点的embedding。
问题仍然是冷启动、不利于出现次数少的商品。
EGES(Embedding-based Graph Edge Sampling,基于边采样的图嵌入方法)是一种基于图的召回方法,主要用于解决冷启动问题。设物品含有个辅助信息(如店铺名称、物品风格),将稀疏特征编码的辅助信息转为稠密向量,然后使用加权平均,其中是辅助信息的权重,使用softmax函数计算,表示物品自身。一般权重最大的是自身,其次是店铺。
Node2vec是DeepWalk的改进,它考虑DFS与BFS。
同质性:DFS,距离相近,使得一个集合内部的节点具有更高的相似度。比如都是同类新闻。
结构性:BFS,结构相近,使得embedding更能刻画当前节点邻域的结构信息。比如都是热点新闻。
从跳跃到的概率是
其中是边的权重,是到的距离,(返回参数)和(进出参数)是超参数,越小随机游走越倾向于回到上一个节点(BFS),越小随机游走越倾向于探索新的节点(DFS),当时,就是标准的随机游走。
GCN(Graph Convolutional Network,图卷积网络)是一种基于图的神经网络。
定义拉普拉斯矩阵,其中是度矩阵,是邻接矩阵,是归一化的拉普拉斯矩阵,形状都是。
多层图卷积的规则是:
其中,是的度矩阵,是第层的节点特征矩阵,是当前特征维数,是第层的权重矩阵,是激活函数。最后一层的输出就是节点的embedding,可以使用softmax得到类别。
问题:需要一次性载入拉普拉斯矩阵,增加新节点需要重新训练。
GCN将embedding通过拉普拉斯矩阵传递,而GraphSAGE则是通过邻接节点的embedding传递。
使用历史CTR(点击率)等指标,对物品进行排序,还是冷启动与长尾问题。
可以通过威尔逊区间进行修正:
对于CTR指标,是CTR,是曝光次数:
其中是置信度,一般取3,是点击次数,是曝光次数。
排序的时比较的是。
置信区间的作用是:当点击次数较少时,CTR的估计值会有较大的波动,置信区间可以帮助我们更好地估计CTR的真实值。
LR,GBDT。但特征交叉能力有限,无法处理高维稀疏特征。
将用户和物品的embedding进行内积,得到一个分数,然后通过softmax得到概率,最后排序。
这个embedding是通过双塔DNN得到的,因此线上的时候只需要计算内积,但无法处理用户和物品之间的特征交叉、无法实时响应、迭代时需要重新生成embedding、无法处理冷启动问题。
Wide & Deep Learning,结合了LR和DNN,LR用于处理低阶特征(人工交叉特征),DNN用于处理高阶特征。
可以任意特征交叉,还能实时训练,这是因为COLD在特征筛选时使用了SE Block。具体而言,对于个特征,其权重的计算为其中是一个全连接层,是一个维向量,表示每个特征的权重。然后,特征的embedding乘上权重,再进行训练。
为了平衡算力和效果,一般会对特征进行筛选,选择前TopK个权重的特征,然后选择AUC最高的特征进行特征交叉。
1. 知识蒸馏的简介
知识蒸馏(knowledge distillation):通过教师网络来指导学生网络的训练,将复杂、学习能力强的教师网络学到的特征表示“蒸馏”出来,传递给参数小、学习能力弱的学生网络,从而得到一个速度快、表达能力强的学生网络。
和一般的模型蒸馏(model distillation,MD)不同,优势特征蒸馏(privileged features distillation,PFD)的教师学生网络结构相同,但是教师网络的输入特征比学生网络多。如下图:
2. 知识蒸馏的原理
知识蒸馏的目标是学生网络的输出和教师网络的输出尽可能相似,使用下标依次代表学生、蒸馏、教师:
式中,是普通/优势特征,是标签,是交叉熵损失函数,是平衡两个损失函数的超参数。损失函数如下:
当教师网络预先训练好时,不需要这一项,但先训练教师网络会让训练时间更多。而同步更新两个网络会让学生在前期学到欠拟合的教师网络,所以在前步迭代时将设为0,这样在欠拟合阶段只更新教师网络而不更新学生网络。
线上服务时计算用户向量,而商品向量是离线计算好了的。
FM上文已经讲过了,就是对embedding进行特征交叉,虽然是二阶,但是时间复杂度是。
FFM:
FFM和FM的区别在于隐向量由原来的变成了,其中表示特征对应的特征域,这意味着每个特征对应的不是唯一的隐向量,而是一组隐向量。FM可以看作FFM的特例,它把所有特征都归属到一个特征域中。
FMM无法简化为。可参考美团的文章。
通过GBDT生成组合特征向量,然后输入到LR(逻辑回归)模型中,如左图:
GBDT+LR模型结构
GBDT生成组合特征向量的过程
如右图,假设GBDT模型由两棵决策树组成,每棵树有4个叶节点。输入训练样本后:在第一棵树中,样本落到第2个叶节点,对应的特征向量为,第二棵树中对应的特征向量为,生成的组合向量是,然后将组合向量输入到LR里面。原始特征在经过GBDT模型后被转换为叶节点上的编码(这些叶节点可以看作新的特征),不需要直接使用的embedding。
使用(Normalized Entropy)来评估模型性能,它是交叉熵损失的归一化版本:
其中是真实标签,是预测标签,是真实CTR(随着时间和数据的变化而变化)。经验CTR可能会因为数据集的不同而有所不同,如果直接使用未归一化的交叉熵损失,模型的性能度量可能会受到数据集CTR的影响。通过归一化,可以使得不同数据集上的模型性能具有可比性。
MLR利用分片线性方式对数据进行拟合,相比LR模型,能够学习到更高阶的特征交叉。
式中,是特征交叉的个数,和是特征交叉的权重,是聚合函数(聚类,决定空间的划分),是拟合函数(分类,决定空间的预测),保证输出在0-1之间。
在实际中,是softmax函数,是sigmoid函数,。MLR模型的优势在于可以学习到更高阶的特征交叉,但是模型的复杂度也随之增加。
式中,是用户特征,是物品特征,是位置特征(使用乘法加权的影响比加法更显著)。
WDL模型结构
将WDL模型部署在Google Play中
其中,Wide部分是通用的线性模型,对输入特征进行交叉:表示特征维度,表示特征向量,表示第维特征,如果第维特征参与了交叉,则为1,否则为0。
Deep部分是一个多层神经网络,对于高维稀疏特征,首先将其转化为稠密的Embedding(大小一般为10~100),然后将Embedding级联(concat)到一起后输入MLP,每一层的计算方式如上式。
DCN(Deep & Cross Network)将Wide改为Cross,用于增强特征之间的交叉力度。
DCN-v2增加了stacked结构(但与Parallel效果没有显著优劣差异),增加了哈达玛积
DCN
DCN-v2
第层的Cross Network的计算方式如下:
其中,是输入特征,是上一层的输出特征,和是权重和偏置。是哈达玛积,和是权重矩阵,是偏置,是输入特征,是上一层的输出特征,因为较大,所以分解为。
DCN
DCN-v2
除了对原有交叉网络进行改进,DCN-v2还引入了MOE网络结构,进一步提升了特征交叉能力:
阿里巴巴推荐系统发展历程:
DIN模型结构:
其中,是用户向量,是历史行为的向量,是目标物品的向量,是注意力机制,的计算方式是一个神经网络(图右上角)。
DIN虽然能让各个行为之间的权重不同,但无法得知兴趣的转移概率。DIEN让推荐系统从学习“历史兴趣”转变为学习“预测下次”,从而更好地捕捉用户的序列兴趣变化。
DIEN使用的是GRU,考虑到耗时,线上最多只能处理长度为50的序列,DSIN则将序列分为不同主题,认为用户每次的行为组成一个会话,30分钟内没有行为再结束当前会话,一个会话内的行为是相近的,因此可以使用同一个主题。但要注意适用范围,对于带有目标性的购物,DSIN效果会更好,而对于无目标性的新闻浏览,用户不一定每次都是在看同一个主题的新闻。
将用户的行为序列扩展到整个用户的生命周期,使用硬搜索(相同类别)来查询用户的行为。