0x00 сводка
Сеть Deep Interest Network (DIN) была предложена группой точного направленного поиска и базового алгоритма Alimama в июне 2017 года. Его оценка CTR для индустрии электронной коммерции сосредоточена на полном использовании/извлечении информации из исторических данных о поведении пользователей.
Интерпретируя документы и исходный код, эта серия статей объединяет некоторые концепции, связанные с глубоким обучением, и, кстати, реализацию TensorFlow. Эта статья является второй и будет анализировать, как генерировать обучающие данные для моделирования пользовательских последовательностей.
0x01 Какие данные нужны DIN
Сначала мы резюмируем поведение DIN следующим образом:
- Оценка CTR обычно абстрагирует последовательность поведения пользователя в характеристику, которая здесь называется встраиванием поведения.
- Предыдущая модель оценки одинаково обрабатывает набор последовательностей поведения пользователя, например объединение с равным весом или добавление временного затухания.
- DIN глубоко анализирует поведенческие намерения пользователя, то есть корреляция между поведением каждого пользователя и продуктами-кандидатами различна.Используя это как возможность, модуль для расчета корреляции (позже также названный вниманием) используется для взвешивания поведения последовательности. Получите желаемое вложение.
Можно видеть, что последовательность пользователей является входными базовыми данными, и вокруг этих данных требуется ряд данных, таких как пользователи, товары и атрибуты товаров. Итак, DIN нужны следующие данные:
- Пользовательский словарь, идентификатор, соответствующий имени пользователя;
- словарь фильмов, идентификатор, соответствующий элементу;
- Словарь категорий, идентификатор, соответствующий категории;
- Информация о категории, соответствующая элементу;
- Данные обучения, формат: метка, имя пользователя, целевой элемент, категория целевого элемента, исторический элемент, исторический элемент соответствующей категории;
- Тестовые данные, формат такой же, как у обучающих данных;
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_aggregor.py : генерировать последовательность поведения пользователя;
- split_by_user.py : разделить на наборы данных;
- generate_voc.py : создать три словаря данных для пользователей, фильмов и жанров;
2.1 Основные данные
используется в газетеAmazon Product DataДанные, включая два файла: review_Electronics_5.json, meta_Electronics.json.
в:
- Обзоры — это в основном контекстная информация, созданная пользователем, покупающим сопутствующие товары, включая идентификатор продукта, время, комментарии и т. д.
- Метафайл представляет собой информацию о самом продукте, включая идентификатор продукта, имя, категорию, купленные и купленные и другую информацию.
Конкретный формат выглядит следующим образом:
обзоры_Электронные данные | |
---|---|
reviewerID | Идентификатор комментатора, например [A2SUAM1J3GNN3B] |
asin | Идентификатор продукта, например [0000013714] |
reviewerName | Псевдоним рецензента |
helpful | Рейтинг полезности обзора, например 2/3 |
reviewText | Текст комментария |
overall | рейтинг продукта |
summary | Сводка комментариев |
unixReviewTime | Время аудита (время unix) |
reviewTime | Время проверки (исходное) |
данные meta_Electronics | |
---|---|
asin | идантификационный номер продукта |
title | наименование товара |
imUrl | Адрес изображения продукта |
categories | Список категорий, к которым относится товар |
description | Описание продукта |
Поведение пользователей в этом наборе данных богато, более 5 отзывов для каждого пользователя и элемента. Особенности включают goods_id, cate_id, отзывы пользователей goods_id_list и cate_id_list. Все действия пользователя (b1, b2, ..., bk, ..., bn).
Задача состоит в том, чтобы предсказать (k + 1)-й проверенный элемент, используя k лучших просмотренных элементов. Набор обучающих данных генерируется с k = 1,2,...,n-2 для каждого пользователя.
2.2 Обработка данных
2.2.1 Создание метаданных
Обрабатывая эти два json-файла, мы можем сгенерировать два файла метаданных: item-info, review-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)
Сгенерированные файлы выглядят следующим образом.
формат отзывов-информациикак: 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
формат информации об элементеДля: идентификатор продукта, список категорий, к которым относится продукт, что эквивалентно таблице сопоставления. То есть товар 0001048791 соответствует категории Книги.
0001048791 Books
0001048775 Books
0001048236 Books
0000401048 Books
0001019880 Books
0001048813 Books
2.2.2 Составление списка образцов
Отрицательные выборки создаются с помощью функции manual_join, и конкретная логика выглядит следующим образом:
- Получить весь список идентификаторов элементов item_list;
- Получите последовательность поведения всех пользователей. У каждого пользователя есть последовательность выполнения, и содержимое каждого элемента последовательности представляет собой кортеж2 (идентификатор пользователя + идентификатор элемента + ранг + отметка времени, отметка времени);
- перебирать каждого пользователя
- Для последовательности поведения пользователя отсортируйте по отметке времени.
- Для каждого поведения пользователя после ранжирования создайте две выборки:
- отрицательный образец. То есть замените идентификатор элемента поведения пользователя случайно выбранным идентификатором элемента и установите значение click равным 0.
- Положительным образцом является поведение пользователя, а для клика установлено значение 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 Разделение проб
На этом шаге образцы разделяются, чтобы определить последние два образца на временной шкале.
- Прочитайте объединенный новый, сгенерированный на предыдущем шаге;
- Используйте user_count для подсчета количества записей на пользователя;
- Итерация по объединенному новому снова.
- Если это последние две строки записи пользователя, напишите 20190119 перед строкой;
- Если это первые несколько строк записи пользователя, напишите 20180118 перед строкой;
- Новые записи записываются в joined-new-split-info;
Поэтому в файлеjoined-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_aggregor.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, потому что DIN использует механизм, аналогичный вниманию, только внимание исторического поведения влияет на последующие, поэтому hist содержит только элементы, на которые нажали до элемента pos_item, имеют смысл.
Конкретная логика такова:
- Перебрать все строки «jointed-new-split-info»
- Продолжайте накапливать идентификатор элемента и идентификатор состояния клика.
- Если он начинается с 20180118, напишите local_train.
- Если он начинается с 20190119, напишите local_test.
- Продолжайте накапливать идентификатор элемента и идентификатор состояния клика.
Поскольку 20190119 — это две последние последовательности во времени, в итоговом файле local_test мы получаем две совокупные поведенческие последовательности для каждого пользователя, то есть эта поведенческая последовательность включает все время от начала до конца во времени.
Имя файла здесь странное, потому что фактический тренировочный тест используетlocal_testданные в файле.
Один положительный образец и один отрицательный образец. В двух последовательностях различаются только последний идентификатор элемента и щелчок, остальные одинаковы.
Конкретный код выглядит следующим образом:
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
Пример выглядит следующим образом:
Формат: метка, идентификатор пользователя, идентификатор элемента-кандидата, категория элемента-кандидата, последовательность поведения, последовательность категорий.
0 A3BI7R43VUZ1TY B00JNHU0T2 Literature & Fiction 0989464105B00B01691C14778097321608442845 BooksLiterature & FictionBooksBooks
1 A3BI7R43VUZ1TY 0989464121 Books 0989464105B00B01691C14778097321608442845 BooksLiterature & FictionBooksBooks
2.2.6 Создание словаря данных
Роль generate_voc.py заключается в создании трех словарей данных для пользователей, фильмов и жанров. Три словаря включают в себя все идентификаторы пользователей, все идентификаторы фильмов и все идентификаторы жанров соответственно. Здесь просто сортировка трех элементов, начиная с 1.
Можно понять, что создаются три карты (movie_map, cate_map, uid_map) с идентификатором фильма, категориями и ReviewerID соответственно, ключом является соответствующая исходная информация, значением является индекс, отсортированный по ключу (отсортированный от 0), а затем исходные данные Исходные данные соответствующего столбца преобразуются в индекс, соответствующий ключу.
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: словарь пользователя, идентификатор, соответствующий имени пользователя;
- mid_voc.pkl: словарь фильмов, идентификатор, соответствующий элементу;
- cat_voc.pkl: Словарь типов, идентификатор соответствует категории;
- item-info: информация о категории, соответствующая элементу;
- reviews-info: просмотреть метаданные в формате: userID, itemID, рейтинг, отметка времени, данные, используемые для отрицательной выборки;
- local_train_splitByUser: обучающие данные, формат одной строки: метка, имя пользователя, целевой элемент, категория целевого элемента, исторический элемент, соответствующая категория исторического элемента;
- local_test_splitByUser: тестовые данные, формат такой же, как у обучающих данных;
0x03 Как использовать данные
3.1 Тренировочные данные
В части train.py он сначала оценивает тестовый набор с исходной моделью, а затем тренируется в соответствии с пакетом и оценивает тестовый набор каждые 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 — это итератор, который возвращает следующий пакет данных для каждого вызова. Этот код включает в себя то, как данные делятся на пакеты и как построить итератор.
Как упоминалось ранее, формат набора данных для обучения: метка, идентификатор пользователя, идентификатор элемента-кандидата, категория элемента-кандидата, последовательность поведения, последовательность категорий.
3.2.1 Инициализация
Основная логика такова:
__init__
В функции:
- Прочитать из трех файлов pkl, сгенерировать три словаря, поместить их в self.source_dicts, соответствующие [uid_voc, mid_voc, cat_voc];
- Считайте из «item-info», чтобы сгенерировать отношение сопоставления, и, наконец, self.meta_id_map — это идентификатор категории, соответствующий каждому идентификатору фильма, то есть построено отношение сопоставления между идентификатором фильма и идентификатором категории. Код ключа: self.meta_id_map[mid_idx] = cat_idx;
- Прочитайте из «reviews-info», чтобы создать список идентификаторов, необходимых для отрицательной выборки;
- Получить различные базовые данные, такие как длина списка пользователей, длина списка фильмов и т. д.;
код показывает, как показано ниже:
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 строк файла. Это можно понимать как одновременное чтение максимального буфера; - Если установлено, он будет отсортирован в соответствии с продолжительностью исторического поведения пользователя;
- Внутренняя итерация начинается, начиная с
self.source_buffer
Выньте часть данных:- Выньте список идентификаторов фильмов с историческим поведением пользователя в mid_list;
- Выньте список идентификаторов кошек с историческим поведением пользователя в cat_list;
- Для каждого pos_mid в mid_list создается 5 данных об истории поведения с отрицательной выборкой, а именно из mid_list_for_random случайным образом берутся 5 id (если он совпадает с pos_mid, то снова получаются новые), то есть для каждого исторического поведения пользователя, в образцы кода 5 были выбраны как отрицательные образцы;
- Поместите [uid, mid, cat, mid_list, cat_list, noclk_mid_list, noclk_cat_list] в источник в качестве обучающих данных;
- Поместите [float(ss[0]), 1-float(ss[0])] в цель как метку;
- При достижении batch_size он выскочит из внутренней итерации и вернет пакетные данные, то есть список максимальной длины 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)
Это можно понимать как классификацию и интеграцию данных этой партии (при условии, что 128 элементов). Например, поставить эти 128 юдов, мидов, котов, исторических последовательностейсобраны отдельно, и, наконец, отправил модель на обучение.
Важным моментом здесь является то, что маска генерируется. Его значение таково:
mask представляет собой маску, которая маскирует определенные значения, чтобы они не влияли на обновления параметров. padding mask - это тип маски,
- Что такое подкладочная маска? Потому что каждая партия длины входной последовательности отличается. То есть мы хотим выровнять входную последовательность. В частности, заполнение нулями после более короткой последовательности. Но если входная последовательность слишком длинная, содержимое слева перехватывается, а лишнее сразу отбрасывается. Поскольку эти заполненные позиции на самом деле бессмысленны, механизм внимания не должен фокусироваться на этих позициях, и требуется некоторая обработка.
- Конкретный метод заключается в том, чтобы добавить очень большое отрицательное число (отрицательная бесконечность) к значению этих позиций, чтобы после softmax вероятность этих позиций была близка к 0! И наша маска заполнения на самом деле является тензором, каждое значение является логическим значением, а место, где значение равно false, — это то место, где мы хотим обработать.
DIN Здесь, поскольку последовательности поведения пользователей в пакете не обязательно одинаковы, реальная длина сохраняется в keys_length, поэтому здесь генерируются маски для выбора реального исторического поведения.
- Сначала установите маску на 0;
- Затем, если данные имеют смысл, установите маску на 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])
0xEE Личная информация
★★★★★★Думая о жизни и технологиях★★★★★★
Публичный аккаунт WeChat:мысли Росси
Если вы хотите получать своевременные новости о статьях, написанных отдельными лицами, или хотите видеть технические материалы, рекомендованные отдельными лицами, обратите внимание.
ссылка 0xFF
Интерпретация сети Deep Interest
Сеть глубокого интереса (DIN, сеть глубокого интереса)
Анализ официальной реализации документа DIN
Как смоделировать последовательность пользователей в исходном коде Ali DIN (1): базовая схема