因为推荐系统中各个子领域的数据处理过程各不相同, 为防止遗忘, 作此笔记(本文应该会不停更新)
纯CTR预估
我的模型的数据处理过程, 根据 AutoInt 修改
首先原始数据大概是长这样的, 以下数据是我编造用来讲解的
1 | 1 a1b paq 1 2 |
原始数据集会告诉你每列所代表的意思以及该列是否为实值数据
以上述数据为例, 数据集提供方会告知第一列代表用户是否点击了, 0代表未点击, 1代表点击了, 其中第二三列为稀疏特征 ( sparse features ) , 值为特征 hash 后的值, 第四列为实值特征 ( dense feature )
在 AutoInt 中, 稀疏特征和实值特征最后都会变成向量表示, 即 embedding, 这是最通用的做法, 如果你不需要实值特征的 embedding, 那么你可以使用下面讲解的方法进行数据处理但最后不使用实值特征的 embedding 即可
对于实值特征, 每一列对于一个 embedding
对于稀疏特征, 因为每一列是独立不相关的, 所以需要先对每一列进行 hash 到 index 的映射
对于第二列, 我们可以得到以下映射 ( 用 python 的 dict 实现 )
1 | None:0 |
第三列我们可以得到以下映射
1 | None:0 |
于是原始数据变成了下面这样
1 | 1 1 1 1 2 |
为了将第二列和第三列都存储在同一个 embedding 中, 我们可以在第三列的映射上加上一个偏移量offset, 第三列前面所有的列 ( field ) 共有 3 个特征, 于是 offset 为 3
1 | 1 1 4 1 2 |
这样处理后, 我们就可以将不同列 ( field ) 的数据存储在同一个field中了 ( 因为不同列的数据通过 offset 实现了隔离 )
对于实值特征, 你可以将它们的 embedding 的 index 放在最前面, 也可以放在最后面, 下面用放最前面来讨论
此时对于两个实值特征的 embedding, 我们需要让它们对应列的 index 分别为 0, 1, 于是就需要处理后面 embedding 的 index, 即 offset 全部加2
即第二列的 index 从 2 开始, 第三列的 index 从 5 ( 2 + 3 ) 开始, 于是数据变成了以下这样
1 | 1 3 6 1 2 |
如果将实值特征的 embedding 放在最后面, 那么就不需要添加相应的 offset
自此数据就处理完毕, 直接输入模型使用即可
模型最后的激活函数使用 sigmoid
划分数据集
打乱后 train : valid : test = 8 : 1 : 1
训练阶段
损失函数使用 logloss ( tf.compat.v1.losses.log_loss ) 进行训练
测试阶段
使用 logloss 和 AUC 作为评测标准
召回
ComiRec
生成用户点击数据
下面用 amazon book 的数据进行讨论, 使用的具体文件为 reviews_Books_5.json
原始数据每行长这样
{"reviewerID": "A2WVHIRDMLM82E", "asin": "000100039X", "reviewerName": "Amazon Customer", "helpful": [0, 0], "reviewText": "This book has so much you can take out of it to use in your real life. Amazing, and one of my favorite reads of all time.", "overall": 5.0, "summary": "Amazing", "unixReviewTime": 1394928000, "reviewTime": "03 16, 2014"}
格式化后长这样
1 | { |
这里附上官方对各个字段的解释
1 | reviewerID - ID of the reviewer, e.g. A2SUAM1J3GNN3B |
主要用到 reviewerID, asin, unixReviewTime 这三个字段
首先收集所有物品 id , 然后删除所有冷门物品 ( 至少出现过 filter_size
次 ) , 并进行重映射到数字 id
然后收集所有用户 id , 只要那些至少浏览过 ( 去冷门 ) 物品集 filter_size
个的用户 , 并进行重映射到数字 id
将每个用户序列按时间排序后以 <user_id>,<item_id>,<time_stamp>
格式写入文件即可
处理完有五个文件
1 | book_user_map.txt |
前两个文件保留了原始 id 到新 id 的对应关系, 训练过程中用不到
后三个文件包含 <user_id>,<item_id>,<time_stamp>
格式数据
对于每个用户来说, 可能被分到 train, valid, test中的任意一个
生成物品类别数据
下面依旧用 amazon book 的数据进行讨论, 使用的具体文件为 meta_Books.json
以及上一节生成的 book_item_map.txt
每行数据格式以后长这样
1 | { |
生成 asin
和 categories[0][-1]
间的对应关系, 将结果保存到book_item_cate.txt
其中 book_item_cate.txt
包含 <item_id>,<cate_id>
格式数据
划分数据集
所有用户中的 80% 当作 train 用户
所有用户中的 10% 当作 valid 用户
所有用户中的 10% 当作 test 用户
train阶段
以下讨论只针对采样中的某个用户讨论
为讨论方便, 我们假设某个用户按时间顺序访问过的序列为 item_0, item_1, ..., item_n
一次采样会在 k = range(4, len(item_list))
中采样得到需要预测的物品, 即第 k 个物品是需要预测的物品
同时模型能看到的历史记录长度是有限制的
当 k < self.maxlen
时, 就需要用 0 进行填充, 考虑到 0 又的确代表某样商品, 于是 0 填充的地方和真实历史物品序列需要分别用一个 mask 数组用 0 和 1 进行遮挡 ( mask 为 1 的下标是模型能看到的, 感觉作者 mask 定义搞反了)
当 k >= self.maxlen
时, 则只取 k 前面的 maxlen 的物品当作用户历史
具体到代码中
user_id_list
代表用户 id 列表, 数据结构为 list[int]
item_id_list
代表每个用户需要预测的物品 id, 数据结构为 list[int]
hist_item_list
代表每个用户需要预测的物品之前的历史点击物品序列, 数据结构为 list[list[int]]
hist_mask_list
代表 hist_item_list
中的历史点击物品序列是否为真实物品还是 0 填充, 数据结构为 list[list[int]]
每个模型最终会输出一个代表用户兴趣的向量 ( 有些模型有多个用户兴趣向量, 则输出与要预测物品内积最大的向量 ), 然后使用负采样进行训练 ( tf.nn.sampled_softmax_loss ), 希望最大化要预测物品的概率, 目标函数为最小化负似然函数
valid和test阶段
与 train 阶段的不同, k稳定取 int(len(item_list) * 0.8)
item_id_list
不再代表需要预测的物品, 而是代表测试集物品集 (即第 k 后所有物品)hist_item_list
代表第 k 前的 maxlen 个物品
指标介绍
对于点击率, 需要统计有多少比例用户的 TopN 集合和 item_id_list
有相同的物品
对于召回率, 模型最终会输出 TopN 集合, 只需要比对 TopN 集合和 item_id_list
有多少物品是一样的, 然后将结果除以 item_id_list
的大小, 最后对所有用户取平均
对于 NDCG, 详见公式和公式, 伪代码如下
对每一个用户:
recall = 0
dcg = 0.0
true_item_set = set(iid_list)
for no, iid in enumerate(I[i]):
if iid in true_item_set:
recall += 1
dcg += 1.0 / math.log(no+2, 2) # 加2是因为代码从0开始数, 公式是从1开始数
idcg = 0.0
for no in range(recall):
idcg += 1.0 / math.log(no+2, 2)
total_recall += recall * 1.0 / len(iid_list)
if recall > 0:
total_ndcg += dcg / idcg # recall 为 0 时, dcg, idcg 都是 0
total_hitrate += 1
ndcg = total_ndcg / total # 对所有用户求平均
知识图谱CTR
具体数据是怎么预处理的我不清楚, 但是处理完是什么样的网上有很详细的数据集
KGAT
在上面这个页面中有很多文件, 但是在真正训练过程中只需要使用 3 个文件, 分别是 train.txt
, test.txt
, kg_final.txt
train.txt
, test.txt
每行开头数字为重映射后的用户 id , 后面的数字为用户访问的重映射后的物品 id
kg_final.txt
每行为三元组, 第一个和第三个数字代表重映射后的图谱结点 id, 第二个数字为关系重映射后的 id
划分数据集
TODO
训练过程
TODO
测试过程
TODO
序列预测CTR
DIN
预处理数据集
在官方代码的 utils
文件夹下有三个文件, 依次执行后可以得到包含后续操作所需的数据文件 remap.pkl
remap.pkl
文件中包含了四部分数据, 分别如下
1 | reviews_df : 按 reviewerID, unixReviewTime 排序, 用户点击了什么物品在什么时候 |
生成训练所需数据集
1 | 为每个用户的每个历史正样本序列生成一个对应的负样本序列, 负样本序列中的样本不得出现在正样本序列中 |
训练数据第一个数字代表用户 id, 第二个列表代表采样的历史, 第三个数据代表下一个物品, 第四个数字决定是点击还是未点击 ( label )
测试数据前两个意义与训练数据相同, 第三个元组的第一个为正样本, 第二个为负样本
最后会将数据保存到 dataset.pkl
1 | with open('dataset.pkl', 'wb') as f: |
划分数据集
训练数据所采用的是从长度 1 到 len(pos_list) - 2 (含) 的历史数据, 比如上述例子中, 用户历史数据长度为 4, 那么训练数据长度为从 1 到 2 (4-2)
历史数据的最后一个物品永远被当作测试集
训练阶段
对数据进行 padding 确保长度一致
计算训练样本的点击率, 损失函数使用 logloss
因为没有验证集, 所以通过计算测试集上的指标来保存最好的模型
测试阶段
对数据进行 padding 确保长度一致
有两个指标, 一个是 gauc, 另一个是 auc
论文和代码中只用到 gauc, auc在代码中只是程序输出看一下而已
AUC 计算过程
对每个用户来说, 有一个正测试样本和负测试样本, 比如上面例子中 10 号用户的正样本为 4, 负样本为 6
于是让模型分别计算正负样本的点击概率, 然后用所有用户的 predictions 和 labels 计算总 AUC 即可
GAUC 计算过程
先介绍一下 GAUC
推荐模型目前比较成熟的模式是训练分类模型, 这个分类模型的任务是预测用户是否会点击给定的商品, 因此, 推荐系统的核心, 仍然是一个二分类问题, 但是是更细力度的二分类
传统的AUC有时表现不好, AUC 反映的是整体样本间的一个排序能力, 而在计算广告领域, 我们实际要衡量的是不同用户对不同广告之间的排序能力, 因此实际应该更关注的是同一个用户对不同广告间的排序能力
GAUC(group auc)实际是计算每个用户的 AUC, 然后加权平均, 最后得到 group auc, 这样就能减少不同用户间的排序结果不太好比较这一影响
因为每个用户的测试数据只有一条, 所以用 Wilcoxon-Mann-Whitney U-statistic
来代替 AUC 计算
举例来说, 比如 100 个测试用户数据输入模型, 模型预测其中 72 个用户的正样本点击概率大于负样本点击概率, 于是此时 GAUC = 72 / 100 = 0.72
注: 虽然在 DIN 的论文中对每个用户有一个 impression 的权重, 但是在实际的代码里每个用户权重都是 1