[阿里DIN] 深度興趣網路原始碼分析 之 如何建模使用者序列
0x00 摘要
Deep Interest Network(DIN)是阿里媽媽精準定向檢索及基礎演算法團隊在2017年6月提出的。其針對電子商務領域(e-commerce industry)的CTR預估,重點在於充分利用/挖掘使用者歷史行為資料中的資訊。
本系列文章通過解讀論文以及原始碼,順便梳理一些深度學習相關概念和TensorFlow的實現。本文是第二篇,將分析如何產生訓練資料,建模使用者序列。
0x01 DIN 需要什麼資料
我們先總述下 DIN 的行為:
- CTR預估一般是將使用者的行為序列抽象出一個特徵,這裡稱之為行為emb。
- 之前的預估模型對使用者的一組行為序列,都是平等對待,比如同權pooling,或者加時間衰減。
- DIN 則深刻分析了使用者行為意圖,即使用者的每個行為和候選商品的相關性是不同的,以此為契機,利用一個計算相關性的模組(後來也叫attention),對序列行為加權pooling,得到想要的embedding。
可見使用者序列是輸入核心資料,圍繞此資料,又需要使用者,商品,商品屬性等一系列資料。所以 DIN 需要如下資料:
- 使用者字典,使用者名稱對應的id;
- movie字典,item對應的id;
- 種類字典,category對應的id;
- item對應的category資訊;
- 訓練資料,格式為:label、使用者名稱、目標item、 目標item類別、歷史item、歷史item對應類別;
- 測試資料,格式同訓練資料;
0x02 如何產生資料
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
我們可以看到這些處理檔案的作用如下:
- process_data.py : 生成後設資料檔案,構建負樣本,樣本分離;
- local_aggretor.py : 生成使用者行為序列;
- split_by_user.py : 分割成資料集;
- generate_voc.py : 對使用者,電影,種類分別生成三個資料字典;
2.1 基礎資料
論文中用的是Amazon Product Data資料,包含兩個檔案:reviews_Electronics_5.json, meta_Electronics.json。
其中:
- reviews主要是使用者買了相關商品產生的上下文資訊,包括商品id, 時間,評論等。
- meta檔案是關於商品本身的資訊,包括商品id, 名稱,類別,買了還買等資訊。
具體格式如下:
reviews_Electronics資料 | |
---|---|
reviewerID | 評論者id,例如[A2SUAM1J3GNN3B] |
asin | 產品的id,例如[0000013714] |
reviewerName | 評論者暱稱 |
helpful | 評論的有用性評級,例如2/3 |
reviewText | 評論文字 |
overall | 產品的評級 |
summary | 評論摘要 |
unixReviewTime | 稽核時間(unix時間) |
reviewTime | 稽核時間(原始) |
meta_Electronics 資料 | |
---|---|
asin | 產品的ID |
title | 產品名稱 |
imUrl | 產品圖片地址 |
categories | 產品所屬的類別列表 |
description | 產品描述 |
此資料集中的使用者行為很豐富,每個使用者和商品都有超過5條評論。 特徵包括goods_id,cate_id,使用者評論 goods_id_list 和 cate_id_list。使用者的所有行為都是(b1,b2,...,bk,... ,bn)。
任務是通過利用前 k 個評論商品來預測第(k + 1)個評論的商品。 訓練資料集是用每個使用者的 k = 1,2,...,n-2 生成的。
2.2 處理資料
2.2.1 生成後設資料
通過處理這兩個json檔案,我們可以生成兩個後設資料檔案:item-info,reviews-info。
python script/process_data.py meta_Books.json reviews_Books_5.json
具體程式碼如下,就是簡單提取:
def process_meta(file):
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
def process_reviews(file):
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)
生成檔案如下。
reviews-info格式為:userID,itemID,評分,時間戳
A2S166WSCFIFP5 000100039X 5.0 1071100800
A1BM81XB4QHOA3 000100039X 5.0 1390003200
A1MOSTXNIO5MPJ 000100039X 5.0 1317081600
A2XQ5LZHTD4AFT 000100039X 5.0 1033948800
A3V1MKC2BVWY48 000100039X 5.0 1390780800
A12387207U8U24 000100039X 5.0 1206662400
item-info格式為:產品的id,產品所屬的類別列表,這裡就相當於一個對映表。即 0001048791 這個產品對應 Books這個種類。
0001048791 Books
0001048775 Books
0001048236 Books
0000401048 Books
0001019880 Books
0001048813 Books
2.2.2 構建樣本列表
通過 manual_join 函式構建了負樣本,具體邏輯如下:
- 得到所有商品id列表item_list;
- 得到所有使用者的行為序列。每一個使用者有一個執行序列,每一個序列item的內容是一個tuple2 (userid + item id + rank + timestamp, timestamp);
- 遍歷每一個使用者
- 對於該使用者的行為序列,按照timestamp排序。
- 對於排序後的每一個使用者行為,構建兩個樣本:
- 一個負樣本。即把使用者行為的item id替換成一個隨機選擇的item id,click設定為0。
- 一個正樣本,就是使用者行為,然後click設定為1。
- 分別把樣本寫入檔案。
比如:
商品列表是:
item_list =
0000000 = {str} '000100039X'
0000001 = {str} '000100039X'
0000002 = {str} '000100039X'
0000003 = {str} '000100039X'
0000004 = {str} '000100039X'
0000005 = {str} '000100039X'
使用者的行為序列是:
user_map = {dict: 603668}
'A1BM81XB4QHOA3' = {list: 6}
0 = {tuple: 2} ('A1BM81XB4QHOA3\t000100039X\t5.0\t1390003200', 1390003200.0)
1 = {tuple: 2} ('A1BM81XB4QHOA3\t0060838582\t5.0\t1190851200', 1190851200.0)
2 = {tuple: 2} ('A1BM81XB4QHOA3\t0743241924\t4.0\t1143158400', 1143158400.0)
3 = {tuple: 2} ('A1BM81XB4QHOA3\t0848732391\t2.0\t1300060800', 1300060800.0)
4 = {tuple: 2} ('A1BM81XB4QHOA3\t0884271781\t5.0\t1403308800', 1403308800.0)
5 = {tuple: 2} ('A1BM81XB4QHOA3\t1885535104\t5.0\t1390003200', 1390003200.0)
'A1MOSTXNIO5MPJ' = {list: 9}
0 = {tuple: 2} ('A1MOSTXNIO5MPJ\t000100039X\t5.0\t1317081600', 1317081600.0)
1 = {tuple: 2} ('A1MOSTXNIO5MPJ\t0143142941\t4.0\t1211760000', 1211760000.0)
2 = {tuple: 2} ('A1MOSTXNIO5MPJ\t0310325366\t1.0\t1259712000', 1259712000.0)
3 = {tuple: 2} ('A1MOSTXNIO5MPJ\t0393062112\t5.0\t1179964800', 1179964800.0)
4 = {tuple: 2} ('A1MOSTXNIO5MPJ\t0872203247\t3.0\t1211760000', 1211760000.0)
5 = {tuple: 2} ('A1MOSTXNIO5MPJ\t1455504181\t5.0\t1398297600', 1398297600.0)
6 = {tuple: 2} ('A1MOSTXNIO5MPJ\t1596917024\t5.0\t1369440000', 1369440000.0)
7 = {tuple: 2} ('A1MOSTXNIO5MPJ\t1600610676\t5.0\t1276128000', 1276128000.0)
8 = {tuple: 2} ('A1MOSTXNIO5MPJ\t9380340141\t3.0\t1369440000', 1369440000.0)
具體程式碼如下:
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")
if items[0] not in user_map:
user_map[items[0]]= []
user_map[items[0]].append(("\t".join(items), float(items[-1])))
item_list.append(items[1])
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]
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) #獲取隨機item id index
asin_neg = item_list[asin_neg_index] #獲取隨機item id
if asin_neg == asin: #如果恰好是那個item id,則繼續選擇
continue
items[1] = asin_neg
# 寫入負樣本
print>>fo, "0" + "\t" + "\t".join(items) + "\t" + meta_map[asin_neg]
j += 1
if j == 1: #negative sampling frequency
break
# 寫入正樣本
if asin in meta_map:
print>>fo, "1" + "\t" + line + "\t" + meta_map[asin]
else:
print>>fo, "1" + "\t" + line + "\t" + "default_cat"
最後檔案摘錄如下,生成了一系列正負樣本。
0 A10000012B7CGYKOMPQ4L 140004314X 5.0 1355616000 Books
1 A10000012B7CGYKOMPQ4L 000100039X 5.0 1355616000 Books
0 A10000012B7CGYKOMPQ4L 1477817603 5.0 1355616000 Books
1 A10000012B7CGYKOMPQ4L 0393967972 5.0 1355616000 Books
0 A10000012B7CGYKOMPQ4L 0778329933 5.0 1355616000 Books
1 A10000012B7CGYKOMPQ4L 0446691437 5.0 1355616000 Books
0 A10000012B7CGYKOMPQ4L B006P5CH1O 4.0 1355616000 Collections & Anthologies
2.2.3 分離樣本
這步驟把樣本分離,目的是確定時間線上最後兩個樣本。
- 讀取上一步生成的 jointed-new;
- 用 user_count 計算每個使用者的記錄數;
- 再次遍歷 jointed-new。
- 如果是該使用者記錄的最後兩行,則在行前面寫入 20190119;
- 如果是該使用者記錄的前面若干行,則在行前面寫入 20180118;
- 新記錄寫入到 jointed-new-split-info;
所以,jointed-new-split-info 檔案中,字首為 20190119 的兩條記錄就是使用者行為的最後兩條記錄,正好是一個正樣本,一個負樣本,時間上也是最後兩個。
程式碼如下:
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
else:
print>>fo, "20190119" + "\t" + line
else:
last_user = user
i = 0
if i < user_count[user] - 2:
print>> fo, "20180118" + "\t" + line
else:
print>>fo, "20190119" + "\t" + line
i += 1
最後檔案如下:
20180118 0 A10000012B7CGYKOMPQ4L 140004314X 5.0 1355616000 Books
20180118 1 A10000012B7CGYKOMPQ4L 000100039X 5.0 1355616000 Books
20180118 0 A10000012B7CGYKOMPQ4L 1477817603 5.0 1355616000 Books
20180118 1 A10000012B7CGYKOMPQ4L 0393967972 5.0 1355616000 Books
20180118 0 A10000012B7CGYKOMPQ4L 0778329933 5.0 1355616000 Books
20180118 1 A10000012B7CGYKOMPQ4L 0446691437 5.0 1355616000 Books
20180118 0 A10000012B7CGYKOMPQ4L B006P5CH1O 4.0 1355616000 Collections & Anthologies
20180118 1 A10000012B7CGYKOMPQ4L 0486227081 4.0 1355616000 Books
20180118 0 A10000012B7CGYKOMPQ4L B00HWI5OP4 4.0 1355616000 United States
20180118 1 A10000012B7CGYKOMPQ4L 048622709X 4.0 1355616000 Books
20180118 0 A10000012B7CGYKOMPQ4L 1475005873 4.0 1355616000 Books
20180118 1 A10000012B7CGYKOMPQ4L 0486274268 4.0 1355616000 Books
20180118 0 A10000012B7CGYKOMPQ4L 098960571X 4.0 1355616000 Books
20180118 1 A10000012B7CGYKOMPQ4L 0486404730 4.0 1355616000 Books
20190119 0 A10000012B7CGYKOMPQ4L 1495459225 4.0 1355616000 Books
20190119 1 A10000012B7CGYKOMPQ4L 0830604790 4.0 1355616000 Books
2.2.4 生成行為序列
local_aggretor.py 用來生成使用者行為序列。
例如對於 reviewerID=0 的使用者,他的pos_list為[13179, 17993, 28326, 29247, 62275],生成的訓練集格式為(reviewerID, hist, pos_item, 1), (reviewerID, hist, neg_item, 0)。
這裡需要注意hist並不包含pos_item或者neg_item,hist只包含在pos_item之前點選過的item,因為DIN採用類似attention的機制,只有歷史行為的attention才對後續的有影響,所以hist只包含pos_item之前點選的item才有意義。
具體邏輯是:
- 遍歷 "jointed-new-split-info" 的所有行
- 不停累計 click 狀態的 item id 和 cat id。
- 如果是 20180118 開頭,則寫入 local_train。
- 如果是 20190119 開頭,則寫入 local_test。
- 不停累計 click 狀態的 item id 和 cat id。
因為 20190119 是時間上排最後的兩個序列,所以最終 local_test 檔案中,得到的是每個使用者的兩個累積行為序列,即這個行為序列從時間上包括從頭到尾所有時間。
這裡檔案命名比較奇怪,因為實際訓練測試使用的是 local_test 檔案中的資料。
一個正樣本,一個負樣本。兩個序列只有最後一個item id 和 click 與否不同,其餘都相同。
具體程式碼如下:
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]
movie_id = items[3]
dt = items[5]
cat1 = items[6]
if ds=="20180118":
fo = ftrain
else:
fo = ftest
if user != last_user:
movie_id_list = []
cate1_list = []
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
last_user = user
if clk: #如果是click狀態
movie_id_list.append(movie_id) # 累積對應的movie id
cate1_list.append(cat1) # 累積對應的cat id
line_idx += 1
最後local_test資料摘錄如下:
0 A10000012B7CGYKOMPQ4L 1495459225 Books 000100039X039396797204466914370486227081048622709X04862742680486404730 BooksBooksBooksBooksBooksBooksBooks
1 A10000012B7CGYKOMPQ4L 0830604790 Books 000100039X039396797204466914370486227081048622709X04862742680486404730 BooksBooksBooksBooksBooksBooksBooks
2.2.5 分成訓練集和測試集
split_by_user.py 的作用是分割成資料集。
是隨機從1~10中選取整數,如果恰好是2,就作為驗證資料集。
fi = open("local_test", "r")
ftrain = open("local_train_splitByUser", "w")
ftest = open("local_test_splitByUser", "w")
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 >> ftest, clk_line
else:
print >> ftrain, noclk_line
print >> ftrain, clk_line
舉例如下:
格式為:label, 使用者id,候選item id,候選item 種類,行為序列,種類序列
0 A3BI7R43VUZ1TY B00JNHU0T2 Literature & Fiction 0989464105B00B01691C14778097321608442845 BooksLiterature & FictionBooksBooks
1 A3BI7R43VUZ1TY 0989464121 Books 0989464105B00B01691C14778097321608442845 BooksLiterature & FictionBooksBooks
2.2.6 生成資料字典
generate_voc.py 的作用是對使用者,電影,種類分別生成三個資料字典。三個字典分別包括所有使用者id,所有電影id,所有種類id。這裡就是簡單的把三種元素從1 開始排序。
可以理解為 用movie id,categories,reviewerID分別生產三個 map(movie_map, cate_map, uid_map),key為對應的原始資訊,value為按key排序後的index(從0開始順序排序),然後將原資料的對應列原始資料轉換成key對應的index。
import cPickle
f_train = open("local_train_splitByUser", "r")
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]
mid = arr[2]
cat = arr[3]
mid_list = arr[4]
cat_list = arr[5]
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
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.iteritems(), key=lambda x:x[1], reverse=True)
sorted_mid_dict = sorted(mid_dict.iteritems(), key=lambda x:x[1], reverse=True)
sorted_cat_dict = sorted(cat_dict.iteritems(), key=lambda x:x[1], reverse=True)
uid_voc = {}
index = 0
for key, value in sorted_uid_dict:
uid_voc[key] = index
index += 1
mid_voc = {}
mid_voc["default_mid"] = 0
index = 1
for key, value in sorted_mid_dict:
mid_voc[key] = index
index += 1
cat_voc = {}
cat_voc["default_cat"] = 0
index = 1
for key, value in sorted_cat_dict:
cat_voc[key] = index
index += 1
cPickle.dump(uid_voc, open("uid_voc.pkl", "w"))
cPickle.dump(mid_voc, open("mid_voc.pkl", "w"))
cPickle.dump(cat_voc, open("cat_voc.pkl", "w"))
最終,得到DIN模型處理的幾個檔案:
- uid_voc.pkl:使用者字典,使用者名稱對應的id;
- mid_voc.pkl:movie字典,item對應的id;
- cat_voc.pkl:種類字典,category對應的id;
- item-info:item對應的category資訊;
- reviews-info:review 後設資料,格式為:userID,itemID,評分,時間戳,用於進行負取樣的資料;
- local_train_splitByUser:訓練資料,一行格式為:label、使用者名稱、目標item、 目標item類別、歷史item、歷史item對應類別;
- local_test_splitByUser:測試資料,格式同訓練資料;
0x03 如何使用資料
3.1 訓練資料
train.py部分,做的事情就是先用初始模型評估一遍測試集,然後按照batch訓練,每1000次評估測試集。
精簡版程式碼如下:
def train(
train_file = "local_train_splitByUser",
test_file = "local_test_splitByUser",
uid_voc = "uid_voc.pkl",
mid_voc = "mid_voc.pkl",
cat_voc = "cat_voc.pkl",
batch_size = 128,
maxlen = 100,
test_iter = 100,
save_iter = 100,
model_type = 'DNN',
seed = 2,
):
with tf.Session(config=tf.ConfigProto(gpu_options=gpu_options)) as sess:
# 獲取訓練資料和測試資料
train_data = DataIterator(train_file, uid_voc, mid_voc, cat_voc, batch_size, maxlen, shuffle_each_epoch=False)
test_data = DataIterator(test_file, uid_voc, mid_voc, cat_voc, batch_size, maxlen)
n_uid, n_mid, n_cat = train_data.get_n()
# 建立模型
model = Model_DIN(n_uid, n_mid, n_cat, EMBEDDING_DIM, HIDDEN_SIZE, ATTENTION_SIZE)
iter = 0
lr = 0.001
for itr in range(3):
loss_sum = 0.0
accuracy_sum = 0.
aux_loss_sum = 0.
for src, tgt in train_data:
# 準備資料
uids, mids, cats, mid_his, cat_his, mid_mask, target, sl, noclk_mids, noclk_cats = prepare_data(src, tgt, maxlen, return_neg=True)
# 訓練
loss, acc, aux_loss = model.train(sess, [uids, mids, cats, mid_his, cat_his, mid_mask, target, sl, lr, noclk_mids, noclk_cats])
loss_sum += loss
accuracy_sum += acc
aux_loss_sum += aux_loss
iter += 1
if (iter % test_iter) == 0:
eval(sess, test_data, model, best_model_path)
loss_sum = 0.0
accuracy_sum = 0.0
aux_loss_sum = 0.0
if (iter % save_iter) == 0:
model.save(sess, model_path+"--"+str(iter))
lr *= 0.5
3.2 迭代讀入
DataInput 是一個迭代器,作用就是每次呼叫返回下一個batch的資料。這段程式碼涉及到資料如何按照batch劃分,以及如何構造一個迭代器。
前面提到,訓練資料集格式為:label, 使用者id,候選item id,候選item 種類,行為序列,種類序列
3.2.1 初始化
基本邏輯是:
__init__
函式中:
- 從三個pkl檔案中讀取,生成三個字典,分別放在 self.source_dicts 裡面,對應 [uid_voc, mid_voc, cat_voc];
- 從 "item-info" 讀取,生成對映關係,最後 self.meta_id_map 中的就是每個movie id 對應的 cateory id,即構建 movie id 和 category id 之間的對映關係。關鍵程式碼是:self.meta_id_map[mid_idx] = cat_idx ;
- 從 "reviews-info" 讀取,生成負取樣所需要的id list;
- 得倒各種基礎資料,比如使用者列表長度,movie列表長度等等;
程式碼如下:
class DataIterator:
def __init__(self, source,
uid_voc,
mid_voc,
cat_voc,
batch_size=128,
maxlen=100,
skip_empty=False,
shuffle_each_epoch=False,
sort_by_length=True,
max_batch_size=20,
minlen=None):
if shuffle_each_epoch:
self.source_orig = source
self.source = shuffle.main(self.source_orig, temporary=True)
else:
self.source = fopen(source, 'r')
self.source_dicts = []
# 從三個pkl檔案中讀取,生成三個字典,分別放在 self.source_dicts 裡面,對應 [uid_voc, mid_voc, cat_voc]
for source_dict in [uid_voc, mid_voc, cat_voc]:
self.source_dicts.append(load_dict(source_dict))
# 從 "item-info" 讀取,生成對映關係,最後 self.meta_id_map 中的就是每個movie id 對應的 cateory id,關鍵程式碼是: self.meta_id_map[mid_idx] = cat_idx ;
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]
self.meta_id_map ={}
for key in meta_map:
val = meta_map[key]
if key in self.source_dicts[1]:
mid_idx = self.source_dicts[1][key]
else:
mid_idx = 0
if val in self.source_dicts[2]:
cat_idx = self.source_dicts[2][val]
else:
cat_idx = 0
self.meta_id_map[mid_idx] = cat_idx
# 從 "reviews-info" 讀取,生成負取樣所需要的id list;
f_review = open("reviews-info", "r")
self.mid_list_for_random = []
for line in f_review:
arr = line.strip().split("\t")
tmp_idx = 0
if arr[1] in self.source_dicts[1]:
tmp_idx = self.source_dicts[1][arr[1]]
self.mid_list_for_random.append(tmp_idx)
# 得倒各種基礎資料,比如使用者列表長度,movie列表長度等等;
self.batch_size = batch_size
self.maxlen = maxlen
self.minlen = minlen
self.skip_empty = skip_empty
self.n_uid = len(self.source_dicts[0])
self.n_mid = len(self.source_dicts[1])
self.n_cat = len(self.source_dicts[2])
self.shuffle = shuffle_each_epoch
self.sort_by_length = sort_by_length
self.source_buffer = []
self.k = batch_size * max_batch_size
self.end_of_data = False
最後資料如下:
self = {DataIterator} <data_iterator.DataIterator object at 0x000001F56CB44BA8>
batch_size = {int} 128
k = {int} 2560
maxlen = {int} 100
meta_id_map = {dict: 367983} {0: 1572, 115840: 1, 282448: 1, 198250: 1, 4275: 1, 260890: 1, 260584: 1, 110331: 1, 116224: 1, 2704: 1, 298259: 1, 47792: 1, 186701: 1, 121548: 1, 147230: 1, 238085: 1, 367828: 1, 270505: 1, 354813: 1...
mid_list_for_random = {list: 8898041} [4275, 4275, 4275, 4275, 4275, 4275, 4275, 4275...
minlen = {NoneType} None
n_cat = {int} 1601
n_mid = {int} 367983
n_uid = {int} 543060
shuffle = {bool} False
skip_empty = {bool} False
sort_by_length = {bool} True
source = {TextIOWrapper} <_io.TextIOWrapper name='local_train_splitByUser' mode='r' encoding='cp936'>
source_buffer = {list: 0} []
source_dicts = {list: 3}
0 = {dict: 543060} {'ASEARD9XL1EWO': 449136, 'AZPJ9LUT0FEPY': 0, 'A2NRV79GKAU726': 16, 'A2GEQVDX2LL4V3': 266686, 'A3R04FKEYE19T6': 354817, 'A3VGDQOR56W6KZ': 4...
1 = {dict: 367983} {'1594483752': 47396, '0738700797': 159716, '1439110239': 193476...
2 = {dict: 1601} {'Residential': 1281, 'Poetry': 250, 'Winter Sports': 1390...
3.2.2 迭代讀取
當迭代讀取時候,邏輯如下:
- 如果
self.source_buffer
沒有資料,則讀取總數為 k 的檔案行數。可以理解為一次性讀取最大buffer; - 如果設定,則按照使用者歷史行為長度排序;
- 內部迭代開始,從
self.source_buffer
取出一條資料:- 取出使用者這次歷史行為 movie id list 到 mid_list;
- 取出使用者這次歷史行為 cat id list 到 cat_list;
- 針對mid_list中的每一個pos_mid,製造5個負取樣歷史行為資料;具體就是從 mid_list_for_random 中隨機獲取5個id(如果與pos_mid相同則再次獲取新的);即對於每一個使用者的歷史行為,程式碼中選取了5個樣本作為負樣本;
- 把 [uid, mid, cat, mid_list, cat_list, noclk_mid_list, noclk_cat_list] 放入souce之中,作為訓練資料;
- 把 [float(ss[0]), 1-float(ss[0])] 放入target之中,作為label;
- 如果達到了batch_size,則跳出內部迭代,返回本batch資料,即一個最大長度為128的列表;
具體程式碼見下文:
def __next__(self):
if self.end_of_data:
self.end_of_data = False
self.reset()
raise StopIteration
source = []
target = []
# 如果 self.source_buffer沒有資料,則讀取總數為 k 的檔案行數。可以理解為一次性讀取最大buffer
if len(self.source_buffer) == 0:
#for k_ in xrange(self.k):
for k_ in range(self.k):
ss = self.source.readline()
if ss == "":
break
self.source_buffer.append(ss.strip("\n").split("\t"))
# sort by history behavior length
# 如果設定,則按照使用者歷史行為長度排序;
if self.sort_by_length:
his_length = numpy.array([len(s[4].split("")) for s in self.source_buffer])
tidx = his_length.argsort()
_sbuf = [self.source_buffer[i] for i in tidx]
self.source_buffer = _sbuf
else:
self.source_buffer.reverse()
if len(self.source_buffer) == 0:
self.end_of_data = False
self.reset()
raise StopIteration
try:
# actual work here,內部迭代開始
while True:
# read from source file and map to word index
try:
ss = self.source_buffer.pop()
except IndexError:
break
uid = self.source_dicts[0][ss[1]] if ss[1] in self.source_dicts[0] else 0
mid = self.source_dicts[1][ss[2]] if ss[2] in self.source_dicts[1] else 0
cat = self.source_dicts[2][ss[3]] if ss[3] in self.source_dicts[2] else 0
# 取出使用者一個歷史行為 movie id 列表 到 mid_list;
tmp = []
for fea in ss[4].split(""):
m = self.source_dicts[1][fea] if fea in self.source_dicts[1] else 0
tmp.append(m)
mid_list = tmp
# 取出使用者一個歷史行為 cat id 列表 到 cat_list;
tmp1 = []
for fea in ss[5].split(""):
c = self.source_dicts[2][fea] if fea in self.source_dicts[2] else 0
tmp1.append(c)
cat_list = tmp1
# read from source file and map to word index
#if len(mid_list) > self.maxlen:
# continue
if self.minlen != None:
if len(mid_list) <= self.minlen:
continue
if self.skip_empty and (not mid_list):
continue
# 針對mid_list中的每一個pos_mid,製造5個負取樣歷史行為資料;具體就是從 mid_list_for_random 中隨機獲取5個id(如果與pos_mid相同則再次獲取新的);
noclk_mid_list = []
noclk_cat_list = []
for pos_mid in mid_list:
noclk_tmp_mid = []
noclk_tmp_cat = []
noclk_index = 0
while True:
noclk_mid_indx = random.randint(0, len(self.mid_list_for_random)-1)
noclk_mid = self.mid_list_for_random[noclk_mid_indx]
if noclk_mid == pos_mid:
continue
noclk_tmp_mid.append(noclk_mid)
noclk_tmp_cat.append(self.meta_id_map[noclk_mid])
noclk_index += 1
if noclk_index >= 5:
break
noclk_mid_list.append(noclk_tmp_mid)
noclk_cat_list.append(noclk_tmp_cat)
source.append([uid, mid, cat, mid_list, cat_list, noclk_mid_list, noclk_cat_list])
target.append([float(ss[0]), 1-float(ss[0])])
if len(source) >= self.batch_size or len(target) >= self.batch_size:
break
except IOError:
self.end_of_data = True
# all sentence pairs in maxibatch filtered out because of length
if len(source) == 0 or len(target) == 0:
source, target = self.next()
return source, target
3.2.3 處理資料
在獲取迭代資料之後,還需要進一步處理。
uids, mids, cats, mid_his, cat_his, mid_mask, target, sl, noclk_mids, noclk_cats = prepare_data(src, tgt, return_neg=True)
可以理解為把這個batch的資料(假設是128條)分類整合起來。比如把這128條的uids, mids, cats,歷史序列 分別聚合起來,最後統一發給模型進行訓練。
這裡重要的一點是生成了mask。其意義是:
mask 表示掩碼,它對某些值進行掩蓋,使其在引數更新時不產生效果。padding mask 是掩碼的一種,
- 什麼是 padding mask 呢?因為每個批次輸入序列長度是不一樣的。也就是說,我們要對輸入序列進行對齊。具體來說,就是給在較短的序列後面填充 0。但是如果輸入的序列太長,則是擷取左邊的內容,把多餘的直接捨棄。因為這些填充的位置,其實是沒什麼意義的,所以attention機制不應該把注意力放在這些位置上,需要進行一些處理。
- 具體的做法是,把這些位置的值加上一個非常大的負數(負無窮),這樣的話,經過 softmax,這些位置的概率就會接近0!而我們的 padding mask 實際上是一個張量,每個值都是一個Boolean,值為 false 的地方就是我們要進行處理的地方。
DIN這裡,由於一個 Batch 中的使用者行為序列不一定都相同,其真實長度儲存在 keys_length 中,所以這裡要產生 masks 來選擇真正的歷史行為。
- 首先把mask都設定為0;
- 然後如果該條資料有意義,則把mask設定為1;
具體程式碼如下:
def prepare_data(input, target, maxlen = None, return_neg = False):
# x: a list of sentences
#s[4]是mid_list, input的每個item中,mid_list長度不同
lengths_x = [len(s[4]) for s in input]
seqs_mid = [inp[3] for inp in input]
seqs_cat = [inp[4] for inp in input]
noclk_seqs_mid = [inp[5] for inp in input]
noclk_seqs_cat = [inp[6] for inp in input]
if maxlen is not None:
new_seqs_mid = []
new_seqs_cat = []
new_noclk_seqs_mid = []
new_noclk_seqs_cat = []
new_lengths_x = []
for l_x, inp in zip(lengths_x, input):
if l_x > maxlen:
new_seqs_mid.append(inp[3][l_x - maxlen:])
new_seqs_cat.append(inp[4][l_x - maxlen:])
new_noclk_seqs_mid.append(inp[5][l_x - maxlen:])
new_noclk_seqs_cat.append(inp[6][l_x - maxlen:])
new_lengths_x.append(maxlen)
else:
new_seqs_mid.append(inp[3])
new_seqs_cat.append(inp[4])
new_noclk_seqs_mid.append(inp[5])
new_noclk_seqs_cat.append(inp[6])
new_lengths_x.append(l_x)
lengths_x = new_lengths_x
seqs_mid = new_seqs_mid
seqs_cat = new_seqs_cat
noclk_seqs_mid = new_noclk_seqs_mid
noclk_seqs_cat = new_noclk_seqs_cat
if len(lengths_x) < 1:
return None, None, None, None
# lengths_x 儲存使用者歷史行為序列的真實長度,maxlen_x 表示序列中的最大長度;
n_samples = len(seqs_mid)
maxlen_x = numpy.max(lengths_x) #選取mid_list長度中最大的,本例中是583
neg_samples = len(noclk_seqs_mid[0][0])
# 由於使用者歷史序列的長度是不固定的, 因此引入 mid_his 等矩陣, 將序列長度固定為 maxlen_x. 對於長度不足 maxlen_x 的序列, 使用 0 來進行填充 (注意 mid_his 等矩陣 使用 zero 矩陣來進行初始化的)
mid_his = numpy.zeros((n_samples, maxlen_x)).astype('int64') #tuple<128, 583>
cat_his = numpy.zeros((n_samples, maxlen_x)).astype('int64')
noclk_mid_his = numpy.zeros((n_samples, maxlen_x, neg_samples)).astype('int64') #tuple<128, 583, 5>
noclk_cat_his = numpy.zeros((n_samples, maxlen_x, neg_samples)).astype('int64') #tuple<128, 583, 5>
mid_mask = numpy.zeros((n_samples, maxlen_x)).astype('float32')
# zip函式用於將可迭代的物件作為引數,將物件中對應的元素打包成一個個元組,然後返回由這些元組組成的列表
for idx, [s_x, s_y, no_sx, no_sy] in enumerate(zip(seqs_mid, seqs_cat, noclk_seqs_mid, noclk_seqs_cat)):
mid_mask[idx, :lengths_x[idx]] = 1.
mid_his[idx, :lengths_x[idx]] = s_x
cat_his[idx, :lengths_x[idx]] = s_y
# noclk_mid_his 和 noclk_cat_his 都是 (128, 583, 5)
noclk_mid_his[idx, :lengths_x[idx], :] = no_sx # 就是直接賦值
noclk_cat_his[idx, :lengths_x[idx], :] = no_sy # 就是直接賦值
uids = numpy.array([inp[0] for inp in input])
mids = numpy.array([inp[1] for inp in input])
cats = numpy.array([inp[2] for inp in input])
# 把input(128長的list)中的每個UID,mid, cat ... 都提出來,聚合,返回
if return_neg:
return uids, mids, cats, mid_his, cat_his, mid_mask, numpy.array(target), numpy.array(lengths_x), noclk_mid_his, noclk_cat_his
else:
return uids, mids, cats, mid_his, cat_his, mid_mask, numpy.array(target), numpy.array(lengths_x)
3.2.4 送入模型
最後,送入模型訓練,也就是train.py中的這一步:
loss, acc, aux_loss = model.train(sess, [uids, mids, cats, mid_his, cat_his, mid_mask, target, sl, lr, noclk_mids, noclk_cats])