常用的特征处理策略:
- 用户id和物品id必须转换成嵌入向量
- 原始文本需要tokenized,并翻译成嵌入文本
- 数值特征需要标准化
通过使用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:
- 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). - 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]}")