推荐系统召回模型之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科技园”及添加小编进群交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值