源代码git地址:
https://github.com/mouna99/dien
整个数据处理可以通过执行脚本prepare_data.sh来实现,代码如下:
export PATH="~/anaconda4/bin:$PATH"
wget http://snap.stanford.edu/data/amazon/productGraph/categoryFiles/reviews_Books.json.gz
wget http://snap.stanford.edu/data/amazon/productGraph/categoryFiles/meta_Books.json.gz
gunzip reviews_Books.json.gz
gunzip meta_Books.json.gz
python script/process_data.py meta_Books.json reviews_Books_5.json
python script/local_aggretor.py
python script/split_by_user.py
python script/generate_voc.py
两个json文件,用户评价、点击相关的文件reviews_Books.json和具体商品信息相关的文件:meta_Books.json,具体json字段可参考DIN数据处理中的介绍。下面依次介绍3个python文件的功能,部分代码作了修改(适合python3执行并且修改一些问题)。
import sys
import random
import time
def process_meta(file): # meta_Books.json
fi = open(file, "r")
fo = open("item-info", "w")
for line in fi:
obj = eval(line)
cat = obj["categories"][0][-1]
#print>>fo, obj["asin"] + "\t" + cat
print(obj["asin"] + "\t" + cat, file=fo)
# (文件 item-info 保存字段): 商品item id, 商品分类 cat(某个名词,例如:Cables & Accessories)
fo.close()
def process_reviews(file): # reviews_Books_5.json
fi = open(file, "r")
user_map = {}
fo = open("reviews-info", "w")
for line in fi:
obj = eval(line)
userID = obj["reviewerID"]
itemID = obj["asin"]
rating = obj["overall"]
time = obj["unixReviewTime"]
#print>>fo, userID + "\t" + itemID + "\t" + str(rating) + "\t" + str(time)
print(userID + "\t" + itemID + "\t" + str(rating) + "\t" + str(time), file=fo)
# (文件reviews-info保存字段):user id, 商品item id, rating of the product(商品等级,浮点数), 时间戳
fo.close()
def manual_join():
f_rev = open("reviews-info", "r")
user_map = {}
item_list = []
for line in f_rev:
line = line.strip()
items = line.split("\t")
#loctime = time.localtime(float(items[-1]))
#items[-1] = time.strftime('%Y-%m-%d', loctime)
if items[0] not in user_map:
user_map[items[0]]= []
user_map[items[0]].append(("\t".join(items), float(items[-1])))
# user_map: key -> user id; list:每个元素为 ("user id \t 商品item id \t rating of the product(商品等级)\t 时间戳", 时间戳)
item_list.append(items[1]) # 所有点击商品id,按顺序存入 item_list
f_meta = open("item-info", "r")
meta_map = {}
for line in f_meta:
arr = line.strip().split("\t")
if arr[0] not in meta_map:
meta_map[arr[0]] = arr[1] # meta_map: key -> item id; item category 一一对应
arr = line.strip().split("\t")
fo = open("jointed-new", "w")
for key in user_map:
sorted_user_bh = sorted(user_map[key], key=lambda x:x[1]) # 一个用户点击过的商品按时间戳排序
for line, t in sorted_user_bh:
items = line.split("\t")
asin = items[1]
j = 0
while True:
asin_neg_index = random.randint(0, len(item_list) - 1)
asin_neg = item_list[asin_neg_index]
if asin_neg == asin:
continue
items[1] = asin_neg
#print>>fo, "0" + "\t" + "\t".join(items) + "\t" + meta_map[asin_neg]
print("0" + "\t" + "\t".join(items) + "\t" + meta_map[asin_neg], file=fo)
# 负样本,字段为:0(label) \t \t user id \t item id (负样本item) \t rating of the product(商品等级)\t 时间戳 \t 真实item 分类
j += 1
if j == 1: #negative sampling frequency
break
if asin in meta_map:
#print>>fo, "1" + "\t" + line + "\t" + meta_map[asin]
print("1" + "\t" + line + "\t" + meta_map[asin], file=fo)
# 正样本,字段为:1(label) \t \t user id \t item id (正样本item) \t rating of the product(商品等级)\t 时间戳 \t 真实item 分类
else:
#print>>fo, "1" + "\t" + line + "\t" + "default_cat"
print("1" + "\t" + line + "\t" + "default_cat", file=fo)
# 正样本,字段为:1(label) \t \t user id \t item id (正样本item) \t rating of the product(商品等级)\t 时间戳 \t 真实item 分类
def split_test():
fi = open("jointed-new", "r")
fo = open("jointed-new-split-info", "w")
user_count = {}
for line in fi:
line = line.strip()
user = line.split("\t")[1]
if user not in user_count:
user_count[user] = 0
user_count[user] += 1
fi.seek(0)
i = 0
last_user = "A26ZDKC53OP6JD"
for line in fi:
line = line.strip()
user = line.split("\t")[1]
if user == last_user:
if i < user_count[user] - 2: # 1 + negative samples
#print>> fo, "20180118" + "\t" + line
print("20180118" + "\t" + line, file=fo)
else:
#print>>fo, "20190119" + "\t" + line
print("20190119" + "\t" + line, file=fo)
else:
last_user = user
i = 0
if i < user_count[user] - 2:
#print>> fo, "20180118" + "\t" + line
print("20180118" + "\t" + line, file=fo)
else:
#print>>fo, "20190119" + "\t" + line
print("20190119" + "\t" + line, file=fo)
i += 1
#(一个用户的最后一对正负样本:20190119,除最后一个外的其他所有对正负样本:20180118 ) \t label \t \t user id \t item id \t rating of the product(商品等级)\t 时间戳 \t 真实item 分类
process_meta(sys.argv[1]) # meta_Books.json
process_reviews(sys.argv[2]) # reviews_Books_5.json
manual_join()
split_test()
process_meta函数,读取meta_Books.json文件,保存为item-info文件,文件字段为:
商品item id, 商品分类 cat(某个名词,例如:Cables & Accessories)
process_reviews函数,读取reviews_Books_5.json文件,保存为reviews-info文件,文件字段为:
user id, 商品item id, rating of the product(商品等级,浮点数), 时间戳
manual_join函数,提取每一个点击行为的样本数据(正样本),且对于每一个正样本,都会对应随机选择一个其他的item商品作为负样本,保存为jointed-new文件,并且同一个用户多次点击的正负样本对会按照时间戳排序连续存储,文件字段为:
label \t \t user id \t item id \t rating of the product(商品等级)\t 时间戳 \t 真实item 分类
split_test函数,写入文件jointed-new-split-info,字段如下:
(一个用户的最后一对正负样本:20190119,除最后一个外的其他所有对正负样本:20180118 ) \t label \t \t user id \t item id \t rating of the product(商品等级)\t 时间戳 \t 真实item 分类
每个用户的样本分为正负样本对,且按时间顺序排序,依次存下来,且第一列加上一列前缀,20180118表示该用户的样本中最后一次之前点击的样本,20190119标识最后一次点击样本。
import sys
import hashlib
import random
fin = open("jointed-new-split-info", "r")
ftrain = open("local_train", "w")
ftest = open("local_test", "w")
last_user = "0"
common_fea = ""
line_idx = 0
for line in fin:
items = line.strip().split("\t")
ds = items[0]
clk = int(items[1]) # 是否有点击
user = items[2] # 用户id
movie_id = items[3] # 商品item id
dt = items[5] # 时间戳
cat1 = items[6] # item分类
if ds=="20180118": # 如果不是最后一个正负样本对
fo = ftrain
else: # 如果是最后一个正负样本对
fo = ftest
if user != last_user: # 用户的第一个样本对
movie_id_list = []
cate1_list = []
#print >> fo, items[1] + "\t" + user + "\t" + movie_id + "\t" + cat1 +"\t" + "" + "\t" + ""
else:
history_clk_num = len(movie_id_list)
cat_str = ""
mid_str = ""
for c1 in cate1_list:
cat_str += c1 + ""
for mid in movie_id_list:
mid_str += mid + ""
if len(cat_str) > 0: cat_str = cat_str[:-1]
if len(mid_str) > 0: mid_str = mid_str[:-1]
if history_clk_num >= 1: # 8 is the average length of user behavior
#print >> fo, items[1] + "\t" + user + "\t" + movie_id + "\t" + cat1 +"\t" + mid_str + "\t" + cat_str
print(items[1] + "\t" + user + "\t" + movie_id + "\t" + cat1 +"\t" + mid_str + "\t" + cat_str, file = fo)
last_user = user
if clk:
movie_id_list.append(movie_id) #之前点过的商品id
cate1_list.append(cat1) #之前点过的商品分类
line_idx += 1
#local test 存储内容:
#每个用户两行数据:
#第一行:label 0, 用户id, 商品id(未点击,负样本), 商品分类, 之前点击过所有商品id,之前点击过所有商品分类
#第二行:label 1, 用户id, 商品id(点击过,正样本), 商品分类, 之前点击过所有商品id,之前点击过所有商品分类
打开上一步写入的文件jointed-new-split-info,由上面分析可知,用户的每一次点击样本都会生成一个正负样本对,然后按照时间戳依次存储,在此文件的处理中,会生成所有在此次点击之前有点击行为的样本(非用户的第一次点击都会生成样本),并且会添加之前点击商品id和之前商品分类这两个特征,同样按时间顺序保存正负样本对,根据上面文件中的前缀来区分是否是最后一次点击,最后一次点击的样本保存在local_test文件(每个用户只保存最后一次点击对应的一对样本),非最后一次点击保存在local_train文件(可能会有多对样本),保存字段如下:
#第一行:label 0, 用户id, 商品id(未点击,负样本), 商品分类, 之前点击过所有商品id,之前点击过所有商品分类
#第二行:label 1, 用户id, 商品id(点击过,正样本), 商品分类, 之前点击过所有商品id,之前点击过所有商品分类
代码如下:
import random
fi = open("local_test", "r")
#local test 存储内容:
#每个用户两行数据:
#第一行:label 0, 用户id, 商品id(未点击,负样本), 商品分类, 之前点击过所有商品id,之前点击过所有商品分类
#第二行:label 1, 用户id, 商品id(点击过,正样本), 商品分类, 之前点击过所有商品id,之前点击过所有商品分类
ftrain = open("local_train_splitByUser", "w")
ftest = open("local_test_splitByUser", "w")
#格式相同:
#第一行:label 0, 用户id, 商品id(未点击,负样本), 商品分类, 之前点击过所有商品id,之前点击过所有商品分类
#第二行:label 1, 用户id, 商品id(点击过,正样本), 商品分类, 之前点击过所有商品id,之前点击过所有商品分类
while True:
rand_int = random.randint(1, 10)
noclk_line = fi.readline().strip()
clk_line = fi.readline().strip() # 点击和未点击 成对出现
if noclk_line == "" or clk_line == "":
break
if rand_int == 2:
#print >> ftest, noclk_line
print(noclk_line, file = ftest)
#print >> ftest, clk_line
print(clk_line, file = ftest)
else:
#print >> ftrain, noclk_line
print(noclk_line, file = ftrain)
#print >> ftrain, clk_line
print(clk_line, file = ftrain)
这里代码比较简单,就是将local_test即用户最后一次点击,按照1:9分成train和test,分别存入local_train_splitByUser文件和local_test_splitByUser文件,保存字段同上一步存储:
#第一行:label 0, 用户id, 商品id(未点击,负样本), 商品分类, 之前点击过所有商品id,之前点击过所有商品分类
#第二行:label 1, 用户id, 商品id(点击过,正样本), 商品分类, 之前点击过所有商品id,之前点击过所有商品分类
import _pickle as cPickle
f_train = open("local_test", "r")
#第一行:label 0, 用户id, 商品id(未点击,负样本), 商品分类, 之前点击过所有商品id,之前点击过所有商品分类
#第二行:label 1, 用户id, 商品id(点击过,正样本), 商品分类, 之前点击过所有商品id,之前点击过所有商品分类
uid_dict = {}
mid_dict = {}
cat_dict = {}
iddd = 0
for line in f_train:
arr = line.strip("\n").split("\t")
clk = arr[0] # 是否有点击
uid = arr[1] # 用户id
mid = arr[2] # 商品item id
cat = arr[3] # item分类
mid_list = arr[4] #之前点过的item id
cat_list = arr[5] #之前点过的item的分类
if uid not in uid_dict:
uid_dict[uid] = 0
uid_dict[uid] += 1
if mid not in mid_dict:
mid_dict[mid] = 0
mid_dict[mid] += 1
if cat not in cat_dict:
cat_dict[cat] = 0
cat_dict[cat] += 1
if len(mid_list) == 0:
continue
for m in mid_list.split(""):
if m not in mid_dict:
mid_dict[m] = 0
mid_dict[m] += 1
#print iddd
iddd+=1
for c in cat_list.split(""):
if c not in cat_dict:
cat_dict[c] = 0
cat_dict[c] += 1
sorted_uid_dict = sorted(uid_dict.items(), key=lambda x:x[1], reverse=True) # items将 key和value转换成 组元(key, value),按照value排序,降序排序。每个user样本个数都为2.
sorted_mid_dict = sorted(mid_dict.items(), key=lambda x:x[1], reverse=True) # 每个商品item对应的被点击次数。
sorted_cat_dict = sorted(cat_dict.items(), key=lambda x:x[1], reverse=True) # 每个商品分类对应的被点击次数。
uid_voc = {}
index = 0
for key, value in sorted_uid_dict:
uid_voc[key] = index
index += 1
# 所有 uid 依次编号为 0,1,2,3,4,5 ......
mid_voc = {}
mid_voc["default_mid"] = 0
index = 1
for key, value in sorted_mid_dict:
mid_voc[key] = index
index += 1
# 所有的 item id 依次编号为0,1,2,3,4,5 .......
cat_voc = {}
cat_voc["default_cat"] = 0
index = 1
for key, value in sorted_cat_dict:
cat_voc[key] = index
index += 1
# 所有的 cate id 依次编号为0,1,2,3,4,5 .......
cPickle.dump(uid_voc, open("uid_voc.pkl", "wb"))
cPickle.dump(mid_voc, open("mid_voc.pkl", "wb"))
cPickle.dump(cat_voc, open("cat_voc.pkl", "wb"))
这部分代码也比较简单,将所有用户id(uid)、商品item id(mid)和item分类id(cat)按照出现次数从大到小排序,并且每一个id都会编号(从0开始依次编号),uid_voc、mid_voc和cat_voc分别表示三类特征id映射的编号(0,1,2,3,4........)。
先讲到这里,下一节开始介绍具体模型的训练过程。