[Alibin] Как смоделировать последовательность пользователей в анализе исходного кода Deep Interest Network

глубокое обучение

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): базовая схема