推薦系統召回模型之YouTubeNet

YouTube Net 是推薦系統步入深度DNN時代的開山之作,文中所提到的推薦系統框架非常經典,基本上奠定了後來推薦系統的主要步驟:召回和排序。至今在工業界有着廣泛的應用。

1. 系統概況

YouTube Net 推薦系統主要包含兩部分內容:召回和排序。

(1)召回的主要工作是從全體視頻庫中篩選出用戶感興趣的視頻,此過程要求檢索速度快,並且所檢索出的視頻與用戶的歷史行爲和偏好相關。所以召回模型和特徵都較爲簡單。

(2)排序的主要工作是對召回的視頻進行精粒度的打分排序,模型和特徵較召回環節更加複雜化。

目前 YouTube Net 在召回環節仍然有着廣泛的應用,因此本文只介紹在召回環節的理論和實踐。

2. YouTube Net 召回模型

1)構建模型

在召回階段,YouTube Net 簡單粗暴的將推薦問題轉化爲了一個 Softmax 多分類問題。即定義一個後驗概率,基於用戶 和其上下文 ,在 時刻將視頻庫 中指定的視頻 分爲第 類的概率。公式如下:

其中: 表示用戶和上下文的Embedding, 表示每個視頻的Embedding。

2)訓練模型技巧:負採樣技術

與 Word2vec 算法一樣,爲了高效的訓練模型,YouTube Net 算法也通過負採樣的方式在全體候選集分佈中抽取負類,然後通過重要性加權對抽樣進行校正。對於每個(正)樣本,對真實標籤和採樣得到的負類,最小化其交叉熵損失函數。

(3)召回模型網絡結構

召回模型的結構如圖1所示。

圖1 YouTube推薦系統召回模型結構圖

輸入層:特徵全部都是用戶相關特徵,分別爲:

(i)用戶歷史觀看的視頻ID Embedding;

(ii)用戶搜索視頻ID Embedding;

(iii)用戶的地理屬性及設備信息的Embedding;

(iv)用戶畫像特徵(年齡、性別等);

MLP層:

(i)使用三層ReLU全連接層;

輸出Softmax層:

(i)輸入:經過三層ReLU全連接層生成的用戶Embedding;

(ii)輸出:用戶觀看每一個視頻的概率分佈;

該模型主要是應用有監督的學習方式去學習用戶歷史和上下文信息的Embedding,然後應用Softmax分類器區分視頻,從而獲得視頻Embedding。其中:Softmax前一層的輸出作爲User的Embedding,而Softmax層中的權重矩陣的每一行向量作爲視頻的Embedding。

圖2 YouTube召回模型Softmax層輸出權重Embedding圖示

圖2中權重矩陣 即爲視頻Embedding矩陣, 代表視頻的格式, 代表視頻Embedding的維度,即每一行代表一個視頻的Embedding;右側的 即爲用戶的Embedding。兩個Embedding矩陣做內積,然後經過Softmax函數,再進行歸一化得到每個視頻對應的概率值,從一定程度上反映了預測概率的大小。

線上通過 檢索的方式(faiss或annoy),對於每個用戶向量,對視頻庫中的所有視頻向量做最近鄰算法,得到 的視頻作爲召回結果。

3. 實現 YouTube Net 召回模型

1)數據情況

下面使用一個簡單的數據集,實踐一下YouTube Net召回模型。數據集是《movielens-1M數據》,數據下載地址:

http://files.grouplens.org/datasets/movielens/ml-1m.zip

將數據集加工成如下格式:

5030  2  2  2  2247  3412,2899,2776,2203,2309,2385,743,2958,2512,2485,2404,2675,2568,2555,217,2491,2566,2481,2503,2786,1051,2502,803,3030,1789,2,424,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0  27  2558  1112,1284,1174,3100,1049,2137,2273,2651,340,2163
279  2  3  15  2831  2210,1456,453,1293,3210,2235,2284,1095,1487,3511,738,886,1926,3501,1023,150,1198,3413,156,909,1019,2848,260,2737,1096,2684,1887,107,1143,347,1107,1111,1151,1133,3113,3592,1119,3287,1203,1181,1121,852,1915,1247,3038,240,0,0,0,0  46  2212  820,1009,2076,529,3032,2503,2742,2345,965,366
1300  2  1  11  3282  692,3041,1234,519,1554,1258,3452,1509,1170,1252,2804,754,2866,1987,2416,596,1250,1824,1225,2323,2542,2647,2355,2267,1248,2543,1818,2512,1815,1167,1289,1241,1803,2974,3252,3127,3320,3061,3278,3075,3249,3322,2945,3179,65,1109,3091,1245,2311,3357  165  1880  1545,332,2754,2254,267,1532,1062,1450,1440,2467
323  2  5  13  1799  580,864,1060,2098,2824,1203,1213,1088,1185,2,1925,309,2427,1994,1176,1486,853,1161,29,254,1259,528,1179,1107,1567,4,427,3567,3130,1174,2129,575,347,1415,2786,2204,2487,21,1223,3032,2652,67,2198,1737,45,51,218,2400,1225,467  117  1295  1114,2758,435,318,2251,2111,3650,2510,3705,1111
695  1  2  2  233  2161,2235,700,2962,444,2489,2375,1849,3662,3582,3650,3225,3128,3060,3127,3581,3252,3510,3556,3076,3281,3302,3050,3384,3702,2969,3303,3551,3543,3178,3249,3670,3342,3652,3665,3378,3322,3073,3376,3075,3584,3179,3504,3511,3278,1289,2,467,107,994  190  2945  2456,2716,2635,990,3657,3403,2210,1602,3251,143

說明:

第 1 列
user_id用戶id
第 2 列gender用戶性別
第 3 列age用戶年齡
第 4 列occupation用戶工作
第 5 列zip用戶郵編
第 6 列hist_movie_id用戶歷史觀看電影序列
第 7 列hist_len用戶歷史觀看電影長度
第 8 列pos_movie_id用戶下一步觀看的電影(正樣本)
第 9 列neg_movie_id用戶下一步未觀看的電影(抽樣作爲負樣本)

數據加工邏輯見:https://github.com/wziji/deep_ctr/blob/master/youtubeNet/data.py

2)行爲序列數據處理

由於本示例中有歷史的行爲序列數據,需要對歷史數據進行pooling處理,方式一般爲:mean, max, sum等方式。

另外由於用戶歷史的行爲數據的個數不一致,需要預先對數據進行對齊操作,本文的 hist_movie_id字段 保留50條歷史數據,不足50條的以0填充。所以在進行pooling操作的時候,需要先進行mask操作,操作步驟見:tf.sequence_mask後做max操作或avg操作

pooling功能見:

https://github.com/wziji/deep_ctr/blob/master/youtubeNet/SequencePoolingLayer.py

3)構建 YouTube Net 模型

#-*- coding:utf-8 -*-

import tensorflow as tf
from tensorflow.keras.layers import Input, Embedding, concatenate, Dense, Dropout

from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping
from SequencePoolingLayer import SequencePoolingLayer


def YouTubeNet(
    sparse_input_length=1,
    dense_input_length=1,
    sparse_seq_input_length=50,
    
    embedding_dim = 64,
    neg_sample_num = 10,
    user_hidden_unit_list = [128, 64]
    ):

    # 1. Input layer
    user_id_input_layer = Input(shape=(sparse_input_length, ), name="user_id_input_layer")
    gender_input_layer = Input(shape=(sparse_input_length, ), name="gender_input_layer")
    age_input_layer = Input(shape=(sparse_input_length, ), name="age_input_layer")
    occupation_input_layer = Input(shape=(sparse_input_length, ), name="occupation_input_layer")
    zip_input_layer = Input(shape=(sparse_input_length, ), name="zip_input_layer")
    
    
    user_click_item_seq_input_layer = Input(shape=(sparse_seq_input_length, ), name="user_click_item_seq_input_layer")
    user_click_item_seq_length_input_layer = Input(shape=(sparse_input_length, ), name="user_click_item_seq_length_input_layer")
    
    
    pos_item_sample_input_layer = Input(shape=(sparse_input_length, ), name="pos_item_sample_input_layer")
    neg_item_sample_input_layer = Input(shape=(neg_sample_num, ), name="neg_item_sample_input_layer")


    
    # 2. Embedding layer
    user_id_embedding_layer = Embedding(6040+1, embedding_dim, mask_zero=True, name='user_id_embedding_layer')(user_id_input_layer)
    gender_embedding_layer = Embedding(2+1, embedding_dim, mask_zero=True, name='gender_embedding_layer')(gender_input_layer)
    age_embedding_layer = Embedding(7+1, embedding_dim, mask_zero=True, name='age_embedding_layer')(age_input_layer)
    occupation_embedding_layer = Embedding(21+1, embedding_dim, mask_zero=True, name='occupation_embedding_layer')(occupation_input_layer)
    zip_embedding_layer = Embedding(3439+1, embedding_dim, mask_zero=True, name='zip_embedding_layer')(zip_input_layer)
    
    item_id_embedding_layer = Embedding(3706+1, embedding_dim, mask_zero=True, name='item_id_embedding_layer')
    pos_item_sample_embedding_layer = item_id_embedding_layer(pos_item_sample_input_layer)
    neg_item_sample_embedding_layer = item_id_embedding_layer(neg_item_sample_input_layer)
    
    user_click_item_seq_embedding_layer = item_id_embedding_layer(user_click_item_seq_input_layer)
    user_click_item_seq_embedding_layer = SequencePoolingLayer(sequence_mask_length=sparse_seq_input_length)\
        ([user_click_item_seq_embedding_layer, user_click_item_seq_length_input_layer])

    

    ### ********** ###
    # user part
    ### ********** ###

    # 3. Concat "sparse" embedding & "sparse_seq" embedding
    user_embedding_layer = concatenate([user_id_embedding_layer, gender_embedding_layer, age_embedding_layer,
                      occupation_embedding_layer, zip_embedding_layer, user_click_item_seq_embedding_layer],
                      axis=-1)


    for i, u in enumerate(user_hidden_unit_list):
        user_embedding_layer = Dense(u, activation="relu", name="FC_{0}".format(i+1))(user_embedding_layer)

        
    
    ### ********** ###
    # item part
    ### ********** ###

    item_embedding_layer = concatenate([pos_item_sample_embedding_layer, neg_item_sample_embedding_layer], \
                      axis=1)
    
    item_embedding_layer = tf.transpose(item_embedding_layer, [0,2,1])
    


    # Output
    dot_output = tf.matmul(user_embedding_layer, item_embedding_layer)
    dot_output = tf.nn.softmax(dot_output) # 輸出11個值,index爲0的值是正樣本,負樣本的索引位置爲[1-10]
    
    user_inputs_list = [user_id_input_layer, gender_input_layer, age_input_layer, \
              occupation_input_layer, zip_input_layer, \
              user_click_item_seq_input_layer, user_click_item_seq_length_input_layer]
    
    item_inputs_list = [pos_item_sample_input_layer, neg_item_sample_input_layer]

    model = Model(inputs = user_inputs_list + item_inputs_list,
           outputs = dot_output)
    

    model.__setattr__("user_input", user_inputs_list)
    model.__setattr__("user_embedding", user_embedding_layer)
    
    model.__setattr__("item_input", pos_item_sample_input_layer)
    model.__setattr__("item_embedding", pos_item_sample_embedding_layer)
    
    return model

模型結構圖見:

輸入:7個特徵數據,和2組label數據(包含1個正樣本數據,抽樣的10個負樣本數據);

輸出:11個樣本的 Softmax 概率分佈;

4)訓練 YouTube Net 模型

# Train model

early_stopping_cb = tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True)
callbacks = [early_stopping_cb]


model = YouTubeNet()

model.compile(loss='sparse_categorical_crossentropy', \
    optimizer=Adam(lr=1e-3), \
    metrics=['sparse_categorical_accuracy'])


history = model.fit(train_generator, \
           epochs=2, \
           steps_per_epoch = steps_per_epoch, \
           callbacks = callbacks,
           validation_data = val_generator, \
           validation_steps = validation_steps, \
           shuffle=True
           )


model.save_weights('YouTubeNet_model.h5')

使用的 loss 函數爲:sparse_categorical_crossentropy,請參考tf.nn.sparse_softmax_cross_entropy_with_logits 函數簡介 溫習一下。

5)加載 YouTube Net 模型,得到最終的用戶和電影Embedding

# Generate user features for testing and full item features for retrieval

test_user_model_input = [user_id, gender, age, occupation, zip, hist_movie_id, hist_len]
all_item_model_input = list(range(0, 3706+1))

user_embedding_model = Model(inputs=re_model.user_input, outputs=re_model.user_embedding)
item_embedding_model = Model(inputs=re_model.item_input, outputs=re_model.item_embedding)

user_embs = user_embedding_model.predict(test_user_model_input)
item_embs = item_embedding_model.predict(all_item_model_input, batch_size=2 ** 12)

user_embs = np.reshape(user_embs, (-1, 64))
item_embs = np.reshape(item_embs, (-1, 64))

print(user_embs.shape)
print(item_embs.shape)
# (6040, 64)
# (3707, 64)

6)基於最終的用戶和電影Embedding,使用faiss或annoy檢索用戶感興趣的候選池

(暫時未實現)

本文的代碼請見,歡迎star:https://github.com/wziji/deep_ctr/tree/master/youtubeNet

歡迎關注“python科技園”及添加小編進羣交流。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章