机器学习30:《推荐系统-III》使用 TensorFlow 构建电影推荐系统

news2024/11/15 8:45:57

本文将介绍基于 MovieLens 数据集创建一个电影推荐系统的方法。具体而言,包括探索电影数据,训练矩阵分解模型,检查嵌入,矩阵分解中的正则化,Softmax 模型训练等内容。

目录

1.准备工作

1.1 导入依赖模块

1.2 加载数据

1.3 探索电影镜头数据

1.3.1 User 数据

 1.3.2 电影

2.评分矩阵表示和误差计算

2.1 评分矩阵的稀疏表示

2.2 计算误差

3.训练矩阵分解模型

3.1 CFModel 定义

3.2 建立矩阵分解模型并对其进行训练

3.3 训练模型

4.检查嵌入件

4.1 编写一个计算候选电影评分的函数

4.2 用户推荐和最近的邻居(相似 Item 和 User)

5.矩阵分解中的正则化

 5.1 建立正则化矩阵分解模型并对其进行训练

6.Softmax模型

6.1 损失函数

6.2 为softmax模型编写一个损失函数

6.3 建立一个softmax模型,对其进行训练,并检查其嵌入

6.4 训练Softmax模型

7.参考文献


1.准备工作

关于环境搭建,请前往《机器学习6:使用 TensorFlow 的训练线性回归模型》,本文不再赘述。

1.1 导入依赖模块

from __future__ import print_function

import numpy as np
import pandas as pd
import collections
from mpl_toolkits.mplot3d import Axes3D
from IPython import display
from matplotlib import pyplot as plt
import sklearn
import sklearn.manifold
import tensorflow.compat.v1 as tf

tf.disable_v2_behavior()
tf.logging.set_verbosity(tf.logging.ERROR)
import altair as alt
from google.colab import auth
import gspread
from oauth2client.client import GoogleCredentials
from urllib.request import urlretrieve
import zipfile

# Add some convenience functions to Pandas DataFrame.
pd.options.display.max_rows = 10
pd.options.display.float_format = '{:.3f}'.format


def mask(df, key, function):
    """Returns a filtered dataframe, by applying function to key"""
    return df[function(df[key])]


def flatten_cols(df):
    df.columns = [' '.join(col).strip() for col in df.columns.values]
    return df


pd.DataFrame.mask = mask
pd.DataFrame.flatten_cols = flatten_cols

# Install Altair and activate its colab renderer.
print("Installing Altair...")

alt.data_transformers.enable('default', max_rows=None)
alt.renderers.enable('colab')
print("Done installing Altair.")

USER_RATINGS = False

1.2 加载数据

# 加载电影数据
print("Downloading movielens data...")
urlretrieve("/Users/shulan/Downloads/ml-100k.zip", "movielens.zip")
zip_ref = zipfile.ZipFile('movielens.zip', "r")
zip_ref.extractall()
print("Done. Dataset contains:")
print(zip_ref.read('ml-100k/u.info'))

# Load each data set (users, movies, and ratings).
users_cols = ['user_id', 'age', 'sex', 'occupation', 'zip_code']
users = pd.read_csv(
    'ml-100k/u.user', sep='|', names=users_cols, encoding='latin-1')

ratings_cols = ['user_id', 'movie_id', 'rating', 'unix_timestamp']
ratings = pd.read_csv(
    'ml-100k/u.data', sep='\t', names=ratings_cols, encoding='latin-1')

# 电影文件包含每种电影题材(类型)的二进制特征
genre_cols = [
    "genre_unknown", "Action", "Adventure", "Animation", "Children", "Comedy",
    "Crime", "Documentary", "Drama", "Fantasy", "Film-Noir", "Horror",
    "Musical", "Mystery", "Romance", "Sci-Fi", "Thriller", "War", "Western"
]
movies_cols = [
                  'movie_id', 'title', 'release_date', "video_release_date", "imdb_url"
              ] + genre_cols
movies = pd.read_csv(
    'ml-100k/u.item', sep='|', names=movies_cols, encoding='latin-1')

# 由于ID从1开始,我们将它们改为从0开始
users["user_id"] = users["user_id"].apply(lambda x: str(x - 1))
movies["movie_id"] = movies["movie_id"].apply(lambda x: str(x - 1))
movies["year"] = movies['release_date'].apply(lambda x: str(x).split('-')[-1])
ratings["movie_id"] = ratings["movie_id"].apply(lambda x: str(x - 1))
ratings["user_id"] = ratings["user_id"].apply(lambda x: str(x - 1))
ratings["rating"] = ratings["rating"].apply(lambda x: float(x))

# 计算指定类型的电影数量
genre_occurences = movies[genre_cols].sum().to_dict()


# 由于有些电影可以属于不止一种类型,我们创造了不同的“流派”列如下:
# all_genres:电影中所有活跃的类型。
# genre:从活跃的流派中随机抽取。
def mark_genres(movies, genres):
    def get_random_genre(gs):
        active = [genre for genre, g in zip(genres, gs) if g == 1]
        if len(active) == 0:
            return 'Other'
        return np.random.choice(active)

    def get_all_genres(gs):
        active = [genre for genre, g in zip(genres, gs) if g == 1]
        if len(active) == 0:
            return 'Other'
        return '-'.join(active)

    movies['genre'] = [
        get_random_genre(gs) for gs in zip(*[movies[genre] for genre in genres])]
    movies['all_genres'] = [
        get_all_genres(gs) for gs in zip(*[movies[genre] for genre in genres])]


mark_genres(movies, genre_cols)

# 创建一个包含所有 movielens 数据的合并 DataFrame
movielens = ratings.merge(movies, on='movie_id').merge(users, on='user_id')


# 将数据拆分为训练集和测试集
def split_dataframe(df, holdout_fraction=0.1):
    test = df.sample(frac=holdout_fraction, replace=False)
    train = df[~df.index.isin(test.index)]
    return train, test

1.3 探索电影镜头数据

在我们深入模型构建之前,让我们检查一下 MovieLens 的数据集。了解数据集的统计数据通常是有帮助的。

1.3.1 User 数据

首先打印一些描述用户特征的基本统计数据。在 Python Console 中执行"users.describe()",结果如下:可以看出,User 数据共计 943 条。

>>> users.describe()

	    age
count	943.000
mean	34.052
std	    12.193
min	    7.000
25%	    25.000
50%	    31.000
75%	    43.000
max	    73.000

此外,我们还可以打印一些描述用户特征的基本统计数据,如下所示:

>>> users.describe(include=[np.object])

      user_id	sex	occupation	zip_code
count	943	    943	  943	      943
unique	943	     2	  21	      795
top	     0	     M    student	  55414
freq	 1	    670	  196	      9

我们还可以创建直方图来进一步了解用户的分布情况。我们使用 Altair 创建一个交互式图表。
 

# 创建用于对数据进行切片的过滤器
occupation_filter = alt.selection_multi(fields=["occupation"])
occupation_chart = alt.Chart().mark_bar().encode(
    x="count()",
    y=alt.Y("occupation:N"),
    color=alt.condition(
        occupation_filter,
        alt.Color("occupation:N", scale=alt.Scale(scheme='category20')),
        alt.value("lightgray")),
).properties(width=300, height=300, selection=occupation_filter)


# 定义生成直方图的函数.
def filtered_hist(field, label, filter):
    """第一层(浅灰色)包含完整数据的直方图,第二层包含过滤数据的直方图。
    Args:
      field: 生成直方图的属性
      label: 直方图标签.
      filter: 过滤器
    """
    base = alt.Chart().mark_bar().encode(
        x=alt.X(field, bin=alt.Bin(maxbins=10), title=label),
        y="count()",
    ).properties(
        width=300,
    )
    return alt.layer(
        base.transform_filter(filter),
        base.encode(color=alt.value('lightgray'), opacity=alt.value(.7)),
    ).resolve_scale(y='independent')

接下来,我们看一下每个用户的评分分布。

users_ratings = (
    ratings
    .groupby('user_id', as_index=False)
    .agg({'rating': ['count', 'mean']})
    .flatten_cols()
    .merge(users, on='user_id')
)

# Create a chart for the count, and one for the mean.
alt.hconcat(
    filtered_hist('rating count', '# ratings / user', occupation_filter),
    filtered_hist('rating mean', 'mean user rating', occupation_filter),
    occupation_chart,
    data=users_ratings)

 1.3.2 电影

用类似的方法,我们可以查看有关电影的数据。

movies_ratings = movies.merge(
    ratings
    .groupby('movie_id', as_index=False)
    .agg({'rating': ['count', 'mean']})
    .flatten_cols(),
    on='movie_id')

genre_filter = alt.selection_multi(fields=['genre'])
genre_chart = alt.Chart().mark_bar().encode(
    x="count()",
    y=alt.Y('genre'),
    color=alt.condition(
        genre_filter,
        alt.Color("genre:N"),
        alt.value('lightgray'))
).properties(height=300, selection=genre_filter)


(movies_ratings[['title', 'rating count', 'rating mean']]
 .sort_values('rating count', ascending=False)
 .head(10))

输出结果如下:

	title	                    rating count	rating mean
49	Star Wars (1977)	            583	          4.358
257	Contact (1997)	                509	          3.804
99	Fargo (1996)	                508	          4.156
180	Return of the Jedi (1983)	    507	          4.008
293	Liar Liar (1997)	            485	          3.157
285	English Patient, The (1996)    	481	          3.657
287	Scream (1996)	                478	          3.441
0	Toy Story (1995)	            452	          3.878
299	Air Force One (1997)	        431	          3.631
120	Independence Day (ID4) (1996)	429	          3.438

当然,我们也可以绘制更加直观的直方图,如下所示:


2.评分矩阵表示和误差计算

我们的目标是将评级矩阵 𝐴 分解转化为用户嵌入矩阵 𝑈 和电影嵌入矩阵 𝑉  的乘积, 使得 

 其中,

𝑁 是用户的数量,
𝑀 是电影的数量,
𝐴𝑖𝑗 是第 i 个用户对第 j 部电影的评分,
每一行𝑈𝑖 是一个𝑑-维用户的维向量(嵌入)𝑖,
每一行𝑉𝑗 是一个𝑑-维电影的维向量(嵌入)𝑗,
模型的预测是 (𝑖,𝑗) 对的点积〈𝑈𝑖,𝑉𝑗〉.

2.1 评分矩阵的稀疏表示

评分矩阵可能非常大,而且通常情况下,大多数条目(本案例中是电影评分)都没有被观察到,因为给定的用户只会对电影的一小部分进行评分。为了有效地表示,我们将使用 tf.SparseTensor。稀疏张量使用三个张量来表示矩阵:tf.SparseTensor(index,values,dense_shape) 表示张量,其中值 𝐴𝑖𝑗=𝑎 通过设置 indices[k]=[i,j] 和 values[k]=a 来编码。最后一个张量 dense_shape 用于指定整个底层矩阵的形状。

举个例子,假设我们有 2 个用户和 4 部电影,这 2 个用户对 4 部电影打了 3 个评分,如下所示:

user_id	   movie_id	   rating
   0	      0	         5.0
   0	      1	         3.0
   1	      3	         1.0

那么,相应的矩阵如下所示:

 

接下来,我们来构建评分矩阵的 tf.SparseTensor 表示。通过编写一个函数,将评级 DataFrame 映射到 tf.SparseTensor。提示:可以使用 df['column_name].values 来选择数据帧 df 的给定列的值。代码如下:

def build_rating_sparse_tensor(ratings_df):
  """
  Args:
    ratings_df: a pd.DataFrame with `user_id`, `movie_id` and `rating` columns.
  Returns:
    a tf.SparseTensor representing the ratings matrix.
  """
  indices = ratings_df[['user_id', 'movie_id']].values
  values = ratings_df['rating'].values
  return tf.SparseTensor(
      indices=indices,
      values=values,
      dense_shape=[users.shape[0], movies.shape[0]])

2.2 计算误差

我们需要一种测量近似误差的方法。我们将从仅使用观测条目的均方误差开始(稍后我们将重新讨论)。它被定义为:

 其中,Ω 是观测评分的集合; |Ω| 是 Ω 的基数.

相应地,实现代码如下:

# 定义计算均方根误差的函数
def sparse_mean_square_error(sparse_ratings, user_embeddings, movie_embeddings):
  """
  Args:
    sparse_ratings: 密度为[N,M]的稀疏张量评分矩阵
    user_embeddings: 形状为[N,k]的稠密张量U,其中k是嵌入维度,使得U_i是用户i的嵌入。
    movie_embeddings: 形状为[M,k]的稠密张量V,其中k是嵌入维度,使得V_j是电影j的嵌入。
  Returns:标量张量,表示真实评级和模型预测之间的 MSE。
  """
  predictions = tf.gather_nd(
      tf.matmul(user_embeddings, movie_embeddings, transpose_b=True),
      sparse_ratings.indices)
  loss = tf.losses.mean_squared_error(sparse_ratings.values, predictions)
  return loss


3.训练矩阵分解模型

我们需要用到 CFModel(协作过滤模型)帮助类(helper class),它是一个使用随机梯度下降训练矩阵分解模型的简单类。类构造函数参数如下:

  • 用户嵌入U(一个 tf.Variable)
  • 电影嵌入 V(一个 tf.Variable)
  • 要优化的损失(tf.Tensor)
  • 度量字典的可选列表,每个字典将一个字符串(度量的名称)映射到一个张量。这些都是在训练过程中评估和绘制的(例如训练误差和测试误差)。

经过训练后,可以使用 model.embeddings 字典访问经过训练的嵌入。示例用法如下:

U_var = ...
V_var = ...
loss = ...
model = CFModel(U_var, V_var, loss)
model.train(iterations=100, learning_rate=1.0)
user_embeddings = model.embeddings['user_id']
movie_embeddings = model.embeddings['movie_id']

3.1 CFModel 定义

# CFModel 帮助类
class CFModel(object):
  """简单定义一个协同过滤模型"""
  def __init__(self, embedding_vars, loss, metrics=None):
    """初始化 CFModel.
    Args:
      embedding_vars: tf.Variables 字典.
      loss: 一个浮动张量,代表要优化的损失。
      metrics: 张量词典的可选列表。在训练过程中,每个字典中的指标将绘制在一个单独的图中。

    """
    self._embedding_vars = embedding_vars
    self._loss = loss
    self._metrics = metrics
    self._embeddings = {k: None for k in embedding_vars}
    self._session = None

  @property
  def embeddings(self):
    """The embeddings dictionary."""
    return self._embeddings

  def train(self, num_iterations=100, learning_rate=1.0, plot_results=True,
            optimizer=tf.train.GradientDescentOptimizer):
    """训练模型.
    Args:
      iterations: 迭代次数.
      learning_rate: 学习率
      plot_results: 是否打印训练结果
      optimizer: 使用的优化器,默认为GradientDescentOptimizer。
    Returns:
      上次迭代时评估所用的度量字典
    """
    with self._loss.graph.as_default():
      opt = optimizer(learning_rate)
      train_op = opt.minimize(self._loss)
      local_init_op = tf.group(
          tf.variables_initializer(opt.variables()),
          tf.local_variables_initializer())
      if self._session is None:
        self._session = tf.Session()
        with self._session.as_default():
          self._session.run(tf.global_variables_initializer())
          self._session.run(tf.tables_initializer())
          tf.train.start_queue_runners()

    with self._session.as_default():
      local_init_op.run()
      iterations = []
      metrics = self._metrics or ({},)
      metrics_vals = [collections.defaultdict(list) for _ in self._metrics]

      # Train and append results.
      for i in range(num_iterations + 1):
        _, results = self._session.run((train_op, metrics))
        if (i % 10 == 0) or i == num_iterations:
          print("\r iteration %d: " % i + ", ".join(
                ["%s=%f" % (k, v) for r in results for k, v in r.items()]),
                end='')
          iterations.append(i)
          for metric_val, result in zip(metrics_vals, results):
            for k, v in result.items():
              metric_val[k].append(v)

      for k, v in self._embedding_vars.items():
        self._embeddings[k] = v.eval()

      if plot_results:
        # Plot the metrics.
        num_subplots = len(metrics)+1
        fig = plt.figure()
        fig.set_size_inches(num_subplots*10, 8)
        for i, metric_vals in enumerate(metrics_vals):
          ax = fig.add_subplot(1, num_subplots, i+1)
          for k, v in metric_vals.items():
            ax.plot(iterations, v, label=k)
          ax.set_xlim([1, num_iterations])
          ax.legend()
      return results

3.2 建立矩阵分解模型并对其进行训练

使用 spare_mean_square_error 函数,编写一个函数,通过创建嵌入变量以及训练和测试损失来构建 CFModel。代码如下:

# 定义模型函数
def build_model(ratings, embedding_dim=3, init_stddev=1.):
  """
  Args:
    ratings: 评分 DataFrame
    embedding_dim: 嵌入向量的维数
    init_stddev: float,随机初始化嵌入的标准偏差
  Returns:
    model: a CFModel.
  """
  # 将评分 DataFrame 切分为训练集和测试集.
  train_ratings, test_ratings = split_dataframe(ratings)
  # 训练和测试数据集的稀疏张量表示
  A_train = build_rating_sparse_tensor(train_ratings)
  A_test = build_rating_sparse_tensor(test_ratings)
  # 使用正态分布初始化嵌入
  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])

3.3 训练模型

执行如下代码,尝试不同的参数(嵌入维度、学习率、迭代)。训练和测试误差在训练结束时绘制。同时,我们可以检查这些值以验证超参数。

# 创建 CF model 并执行训练
model = build_model(ratings, embedding_dim=30, init_stddev=0.5)
model.train(num_iterations=1000, learning_rate=10.)

运行结果如下:

 


4.检查嵌入件

在本节中,我们会检查嵌入,具体而言,包括:

  • 计算你的推荐
  • 查看一些电影最近的邻居(相似的电影)

4.1 编写一个计算候选电影评分的函数

我们首先编写一个函数,在给定查询嵌入  u\epsilon \mathbb{R}^{d} 和 Item嵌入 V\epsilon \mathbb{R}^{N\times d} 的情况下, 计算 Item 得分。正如之前文章所介绍的,我们可以使用不同的相似性度量,这些度量可以产生不同的结果。在此,我们将比较以下内容:

  • 点积: Item j 的评分为 \left \langle u,V_{j}\right \rangle
  • 余弦: Item j 的评分为 \frac{\left \langle u,V_{j}\right \rangle}{\left \| u \right \|\left \| V_{j}\right \|}

相关代码如下所示:

DOT = 'dot'
COSINE = 'cosine'
def compute_scores(query_embedding, item_embeddings, measure=DOT):
  """Computes the scores of the candidates given a query.
  Args:
    query_embedding: a vector of shape [k], representing the query embedding.
    item_embeddings: a matrix of shape [N, k], such that row i is the embedding
      of item i.
    measure: a string specifying the similarity measure to be used. Can be
      either DOT or COSINE.
  Returns:
    scores: a vector of shape [N], such that scores[i] is the score of item i.
  """
  u = query_embedding
  V = item_embeddings
  if measure == COSINE:
    V = V / np.linalg.norm(V, axis=1, keepdims=True)
    u = u / np.linalg.norm(u)
  scores = u.dot(V.T)
  return scores

4.2 用户推荐和最近的邻居(相似 Item 和 User)

# 定义推荐函数
def user_recommendations(model, measure=DOT, exclude_rated=False, k=6):
  if USER_RATINGS:
    scores = compute_scores(
        model.embeddings["user_id"][943], model.embeddings["movie_id"], measure)
    score_key = measure + ' score'
    df = pd.DataFrame({
        score_key: list(scores),
        'movie_id': movies['movie_id'],
        'titles': movies['title'],
        'genres': movies['all_genres'],
    })
    if exclude_rated:
      # remove movies that are already rated
      rated_movies = ratings[ratings.user_id == "943"]["movie_id"].values
      df = df[df.movie_id.apply(lambda movie_id: movie_id not in rated_movies)]
    display.display(df.sort_values([score_key], ascending=False).head(k))  

# 定义寻找指定电影“邻居(相似电影)”的函数
def movie_neighbors(model, title_substring, measure=DOT, k=6):
  # 根据指定字符串 title_substring 搜索电影.
  ids =  movies[movies['title'].str.contains(title_substring)].index.values
  titles = movies.iloc[ids]['title'].values
  if len(titles) == 0:
    raise ValueError("Found no movies with title %s" % title_substring)
  print("Nearest neighbors of : %s." % titles[0])
  if len(titles) > 1:
    print("[Found more than one matching movie. Other candidates: {}]".format(
        ", ".join(titles[1:])))
  movie_id = ids[0]
  scores = compute_scores(
      model.embeddings["movie_id"][movie_id], model.embeddings["movie_id"],
      measure)
  score_key = measure + ' score'
  df = pd.DataFrame({
      score_key: list(scores),
      'titles': movies['title'],
      'genres': movies['all_genres']
  })
  display.display(df.sort_values([score_key], ascending=False).head(k))

执行以下代码,可以查看与电影《Little Princess》相似电影。替换“Little Princess”,可以查看与其他指定电影相似的电影。

movie_neighbors(model, "Little Princess", DOT)
movie_neighbors(model, "Little Princess", COSINE)

执行结果如下:


5.矩阵分解中的正则化

在上一节中,我们的损失被定义为评级矩阵中观察部分的均方误差。正如讲座中所讨论的,这可能会有问题,因为模型没有学会如何处理无关电影(没有观察到的电影)的嵌入。这种现象被称为折叠。在本节,我们将添加正则化术语来解决这个问题。我们将使用两种类型的正则化:

 5.1 建立正则化矩阵分解模型并对其进行训练

编写一个函数来构建正则化(规范化)模型。给定一个函数 gravity(U,V),该函数计算给定两个嵌入矩阵的重力项 𝑈 和 𝑉 .

# 定义函数 gravity
def gravity(U, V):
  """基于给定两个嵌入矩阵,创建重力损失."""
  return 1. / (U.shape[0].value*V.shape[0].value) * tf.reduce_sum(
      tf.matmul(U, U, transpose_a=True) * tf.matmul(V, V, transpose_a=True))

# 定义规范化模型构建函数
def build_regularized_model(
    ratings, embedding_dim=3, regularization_coeff=.1, gravity_coeff=1.,
    init_stddev=0.1):
  """
  Args:
    ratings: the DataFrame of movie ratings.
    embedding_dim: The dimension of the embedding space.
    regularization_coeff: The regularization coefficient lambda.
    gravity_coeff: The gravity regularization coefficient lambda_g.
  Returns:
    A CFModel object that uses a regularized loss.
  """
  # 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)
  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))

  error_train = sparse_mean_square_error(A_train, U, V)
  error_test = sparse_mean_square_error(A_test, U, V)
  gravity_loss = gravity_coeff * gravity(U, V)
  regularization_loss = regularization_coeff * (
      tf.reduce_sum(U*U)/U.shape[0].value + tf.reduce_sum(V*V)/V.shape[0].value)
  total_loss = error_train + regularization_loss + gravity_loss
  losses = {
      'train_error_observed': error_train,
      'test_error_observed': error_test,
  }
  loss_components = {
      'observed_loss': error_train,
      'regularization_loss': regularization_loss,
      'gravity_loss': gravity_loss,
  }
  embeddings = {"user_id": U, "movie_id": V}

  return CFModel(embeddings, total_loss, [losses, loss_components])

基于上面的代码,可以训练正则化模型了!可以尝试不同的正则化系数值和不同的嵌入维度。示例代码如下:

reg_model = build_regularized_model(
    ratings, regularization_coeff=0.1, gravity_coeff=1.0, embedding_dim=35,
    init_stddev=.05)
reg_model.train(num_iterations=2000, learning_rate=20.)

可以观察到,在训练集和测试集上,添加正则化项会导致更高的 MSE。然而,正如我们将看到的那样,模型预测的质量有所提高。这突出了拟合观测数据和最小化正则化项之间的紧张关系。拟合观察到的数据通常强调学习高相似度(在具有许多交互的 Item 之间),但良好的嵌入表示也需要学习低相似度(在很少或没有交互的 Item 间)。

进一步,我们可以查看指定电影(Item)的相似电影。这里以 “Aladdin” 为例,示例代码如下:

movie_neighbors(reg_model, "Aladdin", DOT)
movie_neighbors(reg_model, "Aladdin", COSINE)


6.Softmax模型

在本节中,我们将训练一个简单的 softmax 模型,该模型预测给定用户是否对电影进行了评分。该模型将采用一个特征向量 𝑥 作为输入表示用户已经评定的电影的列表。我们从评分 DataFrame 开始,我们根据 user_id 对其进行分组。

rated_movies = (ratings[["user_id", "movie_id"]]
                .groupby("user_id", as_index=False)
                .aggregate(lambda x: list(x)))
rated_movies.head()

然后,我们创建一个函数来生成一个示例批,这样每个示例都包含以下特征:

  • movie_id:用户评分的电影 id 字符串的张量
  • genre:这些电影流派的一个张量
  • year:发行年份字符串的张量
years_dict = {
    movie: year for movie, year in zip(movies["movie_id"], movies["year"])
}
genres_dict = {
    movie: genres.split('-')
    for movie, genres in zip(movies["movie_id"], movies["all_genres"])
}

def make_batch(ratings, batch_size):
  """Creates a batch of examples.
  Args:
    ratings: A DataFrame of ratings such that examples["movie_id"] is a list of
      movies rated by a user.
    batch_size: The batch size.
  """
  def pad(x, fill):
    return pd.DataFrame.from_dict(x).fillna(fill).values

  movie = []
  year = []
  genre = []
  label = []
  for movie_ids in ratings["movie_id"].values:
    movie.append(movie_ids)
    genre.append([x for movie_id in movie_ids for x in genres_dict[movie_id]])
    year.append([years_dict[movie_id] for movie_id in movie_ids])
    label.append([int(movie_id) for movie_id in movie_ids])
  features = {
      "movie_id": pad(movie, ""),
      "year": pad(year, ""),
      "genre": pad(genre, ""),
      "label": pad(label, -1)
  }
  batch = (
      tf.data.Dataset.from_tensor_slices(features)
      .shuffle(1000)
      .repeat()
      .batch(batch_size)
      .make_one_shot_iterator()
      .get_next())
  return batch

def select_random(x):
  """Selectes a random elements from each row of x."""
  def to_float(x):
    return tf.cast(x, tf.float32)
  def to_int(x):
    return tf.cast(x, tf.int64)
  batch_size = tf.shape(x)[0]
  rn = tf.range(batch_size)
  nnz = to_float(tf.count_nonzero(x >= 0, axis=1))
  rnd = tf.random_uniform([batch_size])
  ids = tf.stack([to_int(rn), to_int(nnz * rnd)], axis=1)
  return to_int(tf.gather_nd(x, ids))

6.1 损失函数

回想一下,softmax 模型映射了输入特征 𝑥 到用户嵌入 𝜓(𝑥)∈ℝ𝑑 。其中,𝑑 是嵌入维度。然后将该向量乘以电影嵌入矩阵 𝑉∈ℝ𝑚×𝑑 (其中 𝑚 是电影的数量),模型的最终输出是产品的 softmax

\hat{p}=softmax(\psi (x)V^{T}))

给定目标标签 y , 如果我们用表示 p = 1_{y} 对这个目标标签进行一次热编码,则损失是之间的交叉熵\hat{p(x))}p .

6.2 为softmax模型编写一个损失函数

# 定义损失函数
def softmax_loss(user_embeddings, movie_embeddings, labels):
  """Returns the cross-entropy loss of the softmax model.
  Args:
    user_embeddings: A tensor of shape [batch_size, embedding_dim].
    movie_embeddings: A tensor of shape [num_movies, embedding_dim].
    labels: A tensor of [batch_size], such that labels[i] is the target label
      for example i.
  Returns:
    The mean cross-entropy loss.
  """
  # Verify that the embddings have compatible dimensions
  user_emb_dim = user_embeddings.shape[1].value
  movie_emb_dim = movie_embeddings.shape[1].value
  if user_emb_dim != movie_emb_dim:
    raise ValueError(
        "The user embedding dimension %d should match the movie embedding "
        "dimension % d" % (user_emb_dim, movie_emb_dim))

  logits = tf.matmul(user_embeddings, movie_embeddings, transpose_b=True)
  loss = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(
      logits=logits, labels=labels))
  return loss

6.3 建立一个softmax模型,对其进行训练,并检查其嵌入

本节将构建一个 softmax CFModel。实现 build_softmax_model 函数。该模型的体系结构在函数create_user_embeddings 中定义,如下图所示。输入嵌入(movie_id, genre and year)被连接起来形成输入层,然后我们有隐藏层,其维度由 hidden_dims 参数指定。最后,将最后一个隐藏层乘以电影嵌入,以获得 logits 层。对于目标标签,我们将使用用户评分的电影列表中随机采样的movie_id。

# 定义softmax模型构建函数
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)

6.4 训练Softmax模型

基于前面的准备,本节开始训练softmax模型。我们可以设置以下超参数:

  • 学习率
  • 迭代次数
  • 输入嵌入维度(input_dims 参数)
  • 隐藏层的数量和每个层的大小(hidden_dims参数)
# 定义特征嵌入函数
def make_embedding_col(key, embedding_dim):
  categorical_col = tf.feature_column.categorical_column_with_vocabulary_list(
      key=key, vocabulary_list=list(set(movies[key].values)), num_oov_buckets=0)
  return tf.feature_column.embedding_column(
      categorical_column=categorical_col, dimension=embedding_dim,
      # default initializer: trancated normal with stddev=1/sqrt(dimension)
      combiner='mean')

with tf.Graph().as_default():
  softmax_model = build_softmax_model(
      rated_movies,
      embedding_cols=[
          make_embedding_col("movie_id", 35),
          make_embedding_col("genre", 3),
          make_embedding_col("year", 2),
      ],
      hidden_dims=[35])

# 训练模型
softmax_model.train(
    learning_rate=8., num_iterations=3000, optimizer=tf.train.AdagradOptimizer)

此外,我们还可以通过执行如下代码来可视化嵌入和查看与指定电影的最邻近(最相似)的电影:

movie_neighbors(softmax_model, "Aladdin", DOT)
movie_neighbors(softmax_model, "Aladdin", COSINE)

movie_embedding_norm([reg_model, softmax_model])

tsne_movie_embeddings(softmax_model)

7.参考文献 

链接-https://developers.google.cn/machine-learning/recommendation/labs/movie-rec-programming-exercise

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/738821.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LeetCode 1107.每日新用户统计

数据准备 Create table If Not Exists Traffic (user_id int, activity ENUM(login, logout, jobs, groups, homepage), activity_date date); Truncate table Traffic; insert into Traffic (user_id, activity, activity_date) values (1, login, 2019-05-01); insert into …

Charles抓包map local后出现“failed: unacceptable content-type: text/plain“

Charles 抓包map local 修改 映射到本地数据 出现如下报错 返回ErrorUrlhttps://xxxx 返回ErrorError Domaincom.alamofire.error.serialization.response Code-1016 "Request failed: unacceptable content-type: text/plain" UserInfo{NSLocalizedDescriptionRequ…

邮票面值-2022年全国青少年信息素养大赛Python国赛第5题

[导读]:超平老师计划推出《全国青少年信息素养大赛Python编程真题解析》50讲,这是超平老师解读Python编程挑战赛真题系列的第7讲。 全国青少年信息素养大赛(原全国青少年电子信息智能创新大赛)是“世界机器人大会青少年机器人设计…

适配理想全系车型,OPPO首创手表无感蓝牙车钥匙系统

OPPO 宣布与理想汽车深度合作,首家推出系统级手表无感蓝牙车钥匙功能,适配理想全系列车型。此功能早已适配 OPPO 手机。仅支持 OPPO Watch 2、OPPO Watch 3 和 OPPO Watch SE 系列,可独立使用,无需手机即可控制车辆。 OPPO首发数字…

Python:创建一个满足高斯分布的立方体

算法说明: (1)首先定义一个中心点坐标 center,标准差 sigma 和峰值 amplitude。 (2)然后通过计算每个点到中心点的欧氏距离,并将欧氏距离应用于高斯分布的公式 amplitude * exp(-distances**2 /…

tcp转发服务桥(windows)

目的 目的是为了在网关上转发udp数据和tcp数据。对于网络里面隔离的内网来说,有一台可以上网的服务器,那么通过两块网卡就可以转发出去,在服务器上进行数据的转发,有tcp和udp两种,udp已经写过了,这次使用了…

MySQL 导出库和表信息导出成Excel

最近在写文档需要将数据库的表和对应的表信息做成EXCEL。 我不能一个一个表一个一个字段的敲下去吧!!! 那有没有一个SQL搞定呢? 这个可以有有! 数据库里有那些表(包含表名和表介绍) SELECT…

路径规划算法:基于野马优化的路径规划算法- 附代码

路径规划算法:基于野马优化的路径规划算法- 附代码 文章目录 路径规划算法:基于野马优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要:本文主要介绍利用智能优化算法野马…

关于CEPH的简单畅谈

CEPH是什么 CEPH是一个先进的分布式存储系统,它具有高度可靠性、可扩展性和性能。CEPH旨在解决传统存储系统中存在的诸多挑战,如单点故障、难以扩展、数据丢失风险等。 CEPH的设计理念是将数据分布到一个由多个节点组成的集群中,并利用冗余…

ERROR: ORA-12560: TNS: 协议适配器错误

之前在Windows安装了Oracle,遇到了ORA-12560 TNS: protocol adapter error的错误。这个问题的原因很简单,就是没有配Oracle的环境变量。由于是去年遇到的问题,我现在已经忘了具体配置什么变量,但可以肯定的是这个问题就是环境变量…

24 | MySQL是怎么保证主备一致的?

以下内容出自《MySQL 实战 45 讲》 https://time.geekbang.org/column/article/76446 24 | MySQL是怎么保证主备一致的? MySQL 主备的基本原理 如图所示就是基本的主备切换流程。(M-S结构) 节点 A 到 B 这条线的内部流程是什么样的 &#x…

DEVICENET转ETHERNET/IP网关devicenet怎么读

远创智控YC-EIP-DNT,你听说过吗?这是一款自主研发的ETHERNET/IP从站功能的通讯网关,它能够连接DEVICENET总线和ETHERNET/IP网络,从而解决生产管理系统中协议不同造成的数据交换互通问题。 这款产品在工业自动化领域可谓是一大利…

微调预训练的 NLP 模型

动动发财的小手,点个赞吧! 针对任何领域微调预训练 NLP 模型的分步指南 简介 在当今世界,预训练 NLP 模型的可用性极大地简化了使用深度学习技术对文本数据的解释。然而,虽然这些模型在一般任务中表现出色,但它们往往缺…

vue进阶----路由

目录 前端路由的概念与原理 什么是路由 SPA 与前端路由 前端路由 前端路由的工作方式 实现简易的前端路由 vue-router 的基本用法 vue-router vue-router 安装和配置的步骤 声明路由的匹配规则 vue-router 的常见用法 1、路由重定向 2、嵌套路由 3、动态路由匹配 …

Stable Diffusion高阶技能(1)-掌握这些,你也能绘出惊艳画作

开篇 初踏入AI作画的世界,你可能会对如何制造出惊艳的艺术作品而困惑。作为一个前沿技术的探索者,我在这一篇文章中,会和你一同揭秘如何用正确的提示词操控AI的“透视”,将最美的画面展现在你眼前。 技能一、提高图片质量的高阶手法 在数量众多的元素中,我们如何做出最…

Vue组件库Element-常见组件-Form表单

Form表单 Form 表单&#xff1a;由输入框、选择器、单选框、多选框等控件组成&#xff0c;用以收集、检验、提交数据 具体关键代码如下&#xff1a; <template><div><el-row><!-- button 按钮 --><el-button>默认按钮</el-button><e…

DDPM 知识点

Generative Modeling by Estimating Gradients of the Data Distribution | Yang Song Score Matching 系列 (一) Non-normalized 模型估計 | 棒棒生

基于单片机智能饮水机加热系统的设计与实现

功能介绍 以51单片机作为主控系统&#xff1b;LCD1602液晶显示当前水温&#xff0c;定时提醒&#xff0c;水量变化DS18B20检测当前水体温度&#xff1b;水位传感器检测当前水位&#xff1b;继电器驱动加热片进行水温加热&#xff1b;定时提醒喝水&#xff0c;蜂鸣器报警&#x…

一键报警终端怎么样

一键报警终端是一种便携式设备&#xff0c;用于紧急情况下的一键求救。通过一键报警终端&#xff0c;用户可以发送紧急求助信号给预设的联系人或报警中心&#xff0c;以便及时获得救援。一键报警终端的主要功能和特点如下&#xff1a;1. 便携式设计&#xff1a;一键报警终端通常…

【Android studio】学号及姓名的输入保存页面

一、设计需求 设计一个页面有两个编辑框&#xff0c;分别输入学号和姓名。有两个按钮&#xff0c;一个是修改按钮&#xff0c;当按下修改按钮&#xff0c;编辑框可以进行编辑&#xff1b;一个是保存按钮&#xff0c;当按下保存按钮&#xff0c;使编辑框显示当前的内容并且编辑…