Softmax DNN推荐模型为何仅向CFModel传入电影嵌入?
矩阵分解与Softmax推荐模型实现差异的核心原因
你在谷歌推荐系统课程实践里看到的实现差异,不是代码编写的风格区别,本质是两类模型的建模逻辑完全不同。
矩阵分解模型的实现逻辑
矩阵分解是最经典的静态协同过滤方案,核心思路是把高维的用户-物品评分矩阵拆解成两个低维嵌入矩阵的乘积:
- 整个模型不依赖用户属性、物品属性这类额外特征,训练数据只有用户ID、物品ID和对应的交互评分
- 每个用户、每个物品的嵌入向量都是独立的可训练静态参数,训练时直接通过评分预测的误差迭代更新所有参数值
- 推理阶段不需要复杂计算,直接根据ID查表拿到对应嵌入,算个内积就能出预测评分
对应模型构建代码:
def build_model(ratings, embedding_dim=3, init_stddev=1.): """ Args: ratings: a DataFrame of the ratings embedding_dim: the dimension of the embedding vectors. init_stddev: float, the standard deviation of the random initial embeddings. Returns: model: a CFModel. """ # Split the ratings DataFrame into train and test. train_ratings, test_ratings = split_dataframe(ratings) # SparseTensor representation of the train and test datasets. A_train = build_rating_sparse_tensor(train_ratings) A_test = build_rating_sparse_tensor(test_ratings) # Initialize the embeddings using a normal distribution. U = tf.Variable(tf.random_normal( [A_train.dense_shape[0], embedding_dim], stddev=init_stddev)) V = tf.Variable(tf.random_normal( [A_train.dense_shape[1], embedding_dim], stddev=init_stddev)) train_loss = sparse_mean_square_error(A_train, U, V) test_loss = sparse_mean_square_error(A_test, U, V) metrics = { 'train_error': train_loss, 'test_error': test_loss } embeddings = { "user_id": U, "movie_id": V } return CFModel(embeddings, train_loss, [metrics])
因为用户嵌入和电影嵌入都是可以持久化存储的静态参数矩阵,所以代码里会同时初始化U(用户嵌入矩阵)、V(电影嵌入矩阵),一起传入CFModel类保存。
Softmax深度推荐模型的实现逻辑
课程里的Softmax模型是特征驱动的深度召回方案,用了不对称的建模设计:
- 物品(电影)侧还是静态嵌入设计:每个电影对应一个固定维度的可训练嵌入向量,这部分向量同时充当Softmax输出层的权重参数,最终和用户嵌入计算内积,得到每个电影被用户交互的概率
- 用户侧完全放弃了静态ID嵌入的设计,搭了一套前馈神经网络:输入用户的历史观影记录、属性等稀疏特征,经过网络层前向传播动态算出当前用户的嵌入向量。这种设计最大的好处是能处理训练集里没出现过的新用户,只要能拿到对应的用户特征就能生成有效表征,比纯矩阵分解的冷启动能力强很多
- 用户嵌入是依赖输入特征动态生成的中间张量,不是可以全局存储的固定参数矩阵,既不需要提前初始化,也没法作为静态嵌入保存
对应模型构建代码:
def build_softmax_model(rated_movies, embedding_cols, hidden_dims): """Builds a Softmax model for MovieLens. Args: rated_movies: DataFrame of traing examples. embedding_cols: A dictionary mapping feature names (string) to embedding column objects. This will be used in tf.feature_column.input_layer() to create the input layer. hidden_dims: int list of the dimensions of the hidden layers. Returns: A CFModel object. """ def create_network(features): """Maps input features dictionary to user embeddings. Args: features: A dictionary of input string tensors. Returns: outputs: A tensor of shape [batch_size, embedding_dim]. """ # Create a bag-of-words embedding for each sparse feature. inputs = tf.feature_column.input_layer(features, embedding_cols) # Hidden layers. input_dim = inputs.shape[1].value for i, output_dim in enumerate(hidden_dims): w = tf.get_variable( "hidden%d_w_" % i, shape=[input_dim, output_dim], initializer=tf.truncated_normal_initializer( stddev=1./np.sqrt(output_dim))) / 10. outputs = tf.matmul(inputs, w) input_dim = output_dim inputs = outputs return outputs train_rated_movies, test_rated_movies = split_dataframe(rated_movies) train_batch = make_batch(train_rated_movies, 200) test_batch = make_batch(test_rated_movies, 100) with tf.variable_scope("model", reuse=False): # Train train_user_embeddings = create_network(train_batch) train_labels = select_random(train_batch["label"]) with tf.variable_scope("model", reuse=True): # Test test_user_embeddings = create_network(test_batch) test_labels = select_random(test_batch["label"]) movie_embeddings = tf.get_variable( "input_layer/movie_id_embedding/embedding_weights") test_loss = softmax_loss( test_user_embeddings, movie_embeddings, test_labels) train_loss = softmax_loss( train_user_embeddings, movie_embeddings, train_labels) _, test_precision_at_10 = tf.metrics.precision_at_k( labels=test_labels, predictions=tf.matmul(test_user_embeddings, movie_embeddings, transpose_b=True), k=10) metrics = ( {"train_loss": train_loss, "test_loss": test_loss}, {"test_precision_at_10": test_precision_at_10} ) embeddings = {"movie_id": movie_embeddings} return CFModel(embeddings, train_loss, metrics)
所以代码里只会把静态的电影嵌入提取出来传入CFModel,用户嵌入靠网络根据输入实时生成,根本不需要提前初始化一个全局的用户嵌入矩阵。
核心差异总结
说白了,这俩实现的差别根上是建模路线不一样:矩阵分解是纯ID查表的协同过滤,用户、物品全是静态参数;Softmax深度模型算是双塔结构的早期雏形,用户侧走特征动态生成表征的路线,物品侧保留静态嵌入,自然不需要全局静态的用户嵌入矩阵。
内容的提问来源于stack exchange,提问作者heisenberg
相关产品推荐
相关产品推荐

