TFRS之特征预处理

常用的特征处理策略:

  1. 用户id和物品id必须转换成嵌入向量
  2. 原始文本需要tokenized,并翻译成嵌入文本
  3. 数值特征需要标准化

通过使用TensorFlow,我们可以将这种预处理作为模型的一部分,而不是单独的预处理步骤。这不仅方便,也确保了我们的预处理在培训和服务期间是完全相同的。这使得部署甚至包括非常复杂的预处理的模型既安全又容易。

数据集

import pprint

import tensorflow_datasets as tfds

ratings = tfds.load("movielens/100k-ratings", split="train")

for x in ratings.take(1).as_numpy_iterator():
  pprint.pprint(x)

There are a couple of key features here:

  • Movie title is useful as a movie identifier.
  • User id is useful as a user identifier.
  • Timestamps will allow us to model the effect of time.

The first two are categorical features; timestamps are a continuous feature.

类别特征进行embedding
假设我们的目标是预测哪个用户将观看哪部电影。为此,我们用一个嵌入向量来表示每个用户和每个电影。最初,这些嵌入值将采用随机值,但在训练期间,我们将调整它们,以便用户的嵌入和他们观看的电影最终更接近。

Taking raw categorical features and turning them into embeddings is normally a two-step process:

  1. Firstly, we need to translate the raw values into a range of
    contiguous integers, normally by building a mapping (called a
    “vocabulary”) that maps raw values (“Star Wars”) to integers (say,
    15).
  2. Secondly, we need to take these integers and turn them into
    embeddings.

通过Keras preprocessing layers 定义词汇表

import numpy as np
import tensorflow as tf

movie_title_lookup = tf.keras.layers.StringLookup()

该层本身还没有词汇表,但我们可以使用数据构建它。

movie_title_lookup.adapt(ratings.map(lambda x: x["movie_title"]))

print(f"Vocabulary: {movie_title_lookup.get_vocabulary()[:3]}")

一旦我们有了这个,我们可以使用层来转换原始的标记到嵌入的id:

movie_title_lookup(["Star Wars (1977)", "One Flew Over the Cuckoo's Nest (1975)"])

注: 该层的词汇表包括一个(或多个!)未知的(或“词汇表之外的”,OOV)标记。这非常方便:这意味着层可以处理词汇表中没有的类别值。

特征hash

事实上,StringLookup层允许我们配置多个OOV索引。如果我们这样做,词汇表中没有的任何原始值都将确定性地散列到OOV索引之一。这样的索引越多,两个不同的原始特征值被散列到相同OOV索引的可能性就越小。因此,如果我们有足够的这样的索引,那么模型应该能够训练得和具有显式词汇表的模型一样好,而不必维护令牌列表。
我们可以将依赖于特性哈希,而完全不使用任何词汇表。这是在tf.keras.layers. hash层中实现的。

# We set up a large number of bins to reduce the chance of hash collisions.
num_hashing_bins = 200_000

movie_title_hashing = tf.keras.layers.Hashing(
    num_bins=num_hashing_bins
)

我们可以像以前一样进行查找,而不需要构建词汇表:

movie_title_hashing(["Star Wars (1977)", "One Flew Over the Cuckoo's Nest (1975)"])

定义embedding
在我们有了整数id,我们可以使用Embedding layer 将它们转换为嵌入向量。

在为电影标题创建嵌入层时,我们将把第一个值设置为标题词汇表的大小(或哈希箱的数量)。第二个问题取决于我们:它越大,模型的容量就越大,但适应和服务的速度就越慢。

movie_title_embedding = tf.keras.layers.Embedding(
    # Let's use the explicit vocabulary lookup.
    input_dim=movie_title_lookup.vocab_size(),
    output_dim=32
)

我们可以把这两个放到一个图层中,它可以获取原始文本并生成嵌入。

movie_title_model = tf.keras.Sequential([movie_title_lookup, movie_title_embedding])

就像这样,我们可以直接获得电影标题的嵌入:

movie_title_model(["Star Wars (1977)"])

对用户侧,采用相同的操作

user_id_lookup = tf.keras.layers.StringLookup()
user_id_lookup.adapt(ratings.map(lambda x: x["user_id"]))

user_id_embedding = tf.keras.layers.Embedding(user_id_lookup.vocab_size(), 32)

user_id_model = tf.keras.Sequential([user_id_lookup, user_id_embedding])

连续特征标准化
连续的特征也需要规范化。例如,时间戳特性太大,不能直接用于深层模型,常用的两种处理方法:离散化和标准化

for x in ratings.take(3).as_numpy_iterator():
  print(f"Timestamp: {x['timestamp']}.")

Standardization

timestamp_normalization = tf.keras.layers.Normalization(
    axis=None
)
timestamp_normalization.adapt(ratings.map(lambda x: x["timestamp"]).batch(1024))

for x in ratings.take(3).as_numpy_iterator():
  print(f"Normalized timestamp: {timestamp_normalization(x['timestamp'])}.")

Discretization (离散化)
当我们怀疑某个特征是非连续的时候,可以进行离散化处理:

等宽

max_timestamp = ratings.map(lambda x: x["timestamp"]).reduce(
    tf.cast(0, tf.int64), tf.maximum).numpy().max()
min_timestamp = ratings.map(lambda x: x["timestamp"]).reduce(
    np.int64(1e9), tf.minimum).numpy().min()

timestamp_buckets = np.linspace(
    min_timestamp, max_timestamp, num=1000)

print(f"Buckets: {timestamp_buckets[:3]}")

给定桶边界,我们可以将时间戳转换为嵌入:

timestamp_embedding_model = tf.keras.Sequential([
  tf.keras.layers.Discretization(timestamp_buckets.tolist()),
  tf.keras.layers.Embedding(len(timestamp_buckets) + 1, 32)
])

for timestamp in ratings.take(1).map(lambda x: x["timestamp"]).batch(1).as_numpy_iterator():
  print(f"Timestamp embedding: {timestamp_embedding_model(timestamp)}.")

文本特征的处理

title_text = tf.keras.layers.TextVectorization()
title_text.adapt(ratings.map(lambda x: x["movie_title"]))

举例:

for row in ratings.batch(1).map(lambda x: x["movie_title"]).take(1):
  print(title_text(row))

我们可以检查学习的词汇表,以验证该层使用了正确的标记化:

title_text.get_vocabulary()[40:45]

构建预处理模型

用户侧模型

class UserModel(tf.keras.Model):

  def __init__(self):
    super().__init__()

    self.user_embedding = tf.keras.Sequential([
        user_id_lookup,
        tf.keras.layers.Embedding(user_id_lookup.vocab_size(), 32),
    ])
    self.timestamp_embedding = tf.keras.Sequential([
      tf.keras.layers.Discretization(timestamp_buckets.tolist()),
      tf.keras.layers.Embedding(len(timestamp_buckets) + 2, 32)
    ])
    self.normalized_timestamp = tf.keras.layers.Normalization(
        axis=None
    )

  def call(self, inputs):

    # Take the input dictionary, pass it through each input layer,
    # and concatenate the result.
    return tf.concat([
        self.user_embedding(inputs["user_id"]),
        self.timestamp_embedding(inputs["timestamp"]),
        tf.reshape(self.normalized_timestamp(inputs["timestamp"]), (-1, 1))
    ], axis=1)

测试:

user_model = UserModel()

user_model.normalized_timestamp.adapt(
    ratings.map(lambda x: x["timestamp"]).batch(128))

for row in ratings.batch(1).take(1):
  print(f"Computed representations: {user_model(row)[0, :3]}")

电影侧模型

class MovieModel(tf.keras.Model):

  def __init__(self):
    super().__init__()

    max_tokens = 10_000

    self.title_embedding = tf.keras.Sequential([
      movie_title_lookup,
      tf.keras.layers.Embedding(movie_title_lookup.vocab_size(), 32)
    ])
    self.title_text_embedding = tf.keras.Sequential([
      tf.keras.layers.TextVectorization(max_tokens=max_tokens),
      tf.keras.layers.Embedding(max_tokens, 32, mask_zero=True),
      # We average the embedding of individual words to get one embedding vector
      # per title.
      tf.keras.layers.GlobalAveragePooling1D(),
    ])

  def call(self, inputs):
    return tf.concat([
        self.title_embedding(inputs["movie_title"]),
        self.title_text_embedding(inputs["movie_title"]),
    ], axis=1)

测试

movie_model = MovieModel()

movie_model.title_text_embedding.layers[0].adapt(
    ratings.map(lambda x: x["movie_title"]))

for row in ratings.batch(1).take(1):
  print(f"Computed representations: {movie_model(row)[0, :3]}")
上一篇:大数据学习教程SD版第九篇【Flume】


下一篇:TFRS之信息检索