打造高精度推荐系统的 TensorFlow 特征工程实战
你有没有遇到过这种情况:模型结构堆得再深,AUC 就是卡在 0.7 几上不去?训练跑得慢不说,上线后效果还“翻车”——线下指标亮眼,线上 AB 测试却毫无提升。
别急,问题很可能出在特征工程上。
在推荐系统领域,我们常说:“数据决定上限,模型只是逼近这个上限。” 而连接原始数据与模型能力的桥梁,正是特征工程。尤其是当使用 TensorFlow 构建深度推荐模型时,如何科学地处理用户 ID、行为序列、价格、点击次数这些五花八门的数据,直接决定了你的 DNN、DeepFM 或 YouTube DNN 能不能真正“看懂”用户意图。
本文不讲空泛理论,带你用TensorFlow 原生组件,从零搭建一套工业级推荐系统的特征处理流水线。我们将聚焦四个核心实战问题:
- 高基数用户/物品 ID 怎么高效嵌入?
- 年龄、价格这类数值特征该怎么归一化才不会让梯度爆炸?
- 用户最近点击的 100 个商品,怎么变成一个固定长度的“兴趣向量”?
- 数据太大,GPU 经常等 CPU 解码,怎么破?
一步步来,全是能直接用的硬货。
类别型特征:别再 Pandas + LabelEncoder 了,试试 TF 的嵌入之道
推荐系统里最多的就是“标签类”数据:用户 ID、城市、设备型号、类目标签……它们不是数字,没法直接喂给神经网络。传统做法是用 Pandas 做LabelEncoder再 one-hot,但面对百万级用户 ID,one-hot 向量会稀疏到内存都扛不住。
TensorFlow 提供了一套更优雅的解决方案:哈希分桶 + 嵌入查表(Hashing + Embedding)。
为什么选择categorical_column_with_hash_bucket?
想象一下,你有 500 万用户,不可能枚举所有 ID 建立映射表。这时候就可以用哈希函数把字符串 ID 映射到一个固定大小的桶中,比如 10 万个桶。然后每个桶对应一个可学习的 64 维向量——这就是嵌入。
import tensorflow as tf # 用户ID:高基数类别特征 user_id = tf.feature_column.categorical_column_with_hash_bucket( 'user_id', hash_bucket_size=100000) # 哈希到10万桶 # 转换为64维稠密向量 user_id_embedded = tf.feature_column.embedding_column(user_id, dimension=64) # 构造成 Keras 可用的特征层 feature_layer = tf.keras.layers.DenseFeatures([user_id_embedded]) # 模拟输入 example_batch = {'user_id': tf.constant(['u123', 'u456', 'u789'])} embedded_output = feature_layer(example_batch) print(embedded_output.shape) # (3, 64)看到没?三行代码搞定高基数 ID 的嵌入转换。而且这个DenseFeatures层可以无缝接入 Keras 模型,还能随 SavedModel 一起导出,保证线上服务和训练阶段特征处理完全一致。
💡小贴士:哈希会有冲突风险(不同 ID 被分到同一个桶),如果业务对精度要求极高,可以在原始 ID 后加盐(salt)缓解,例如
'u123' + '_user'。
更进一步:多值类别特征怎么处理?
有些特征本身就是一个列表,比如“用户兴趣标签”可能是['运动', '科技', '旅行']。这种多值特征可以用indicator_column或结合embedding_column做平均池化:
interests = tf.feature_column.categorical_column_with_vocabulary_list( 'interests', vocabulary_list=['运动', '科技', '旅行', '美食'], num_oov_buckets=1) # 多值需设置 multi_hot=True interests_onehot = tf.feature_column.indicator_column(interests, max_length=4) feature_layer_multi = tf.keras.layers.DenseFeatures([interests_onehot]) inputs = {'interests': tf.ragged.constant([['运动', '科技'], ['旅行'], ['美食', '运动', '旅行']])} output = feature_layer_multi(inputs) print(output.shape) # (3, 5) —— 包含 OOV 桶这种方式适合标签数量有限且语义明确的场景。如果是超大规模多值输入(如历史点击 item_id),更适合走嵌入 + 序列聚合路线,后面我们会详细展开。
数值型特征:别让尺度差异毁了你的梯度下降
年龄 18~80,商品价格从几块到上万,观看时长从几秒到几十分钟——这些数值特征如果不做处理,放进神经网络就像让小学生和博士生同场竞技,结果只会是“强者恒强”,小尺度特征被彻底淹没。
标准做法是归一化(Normalization),而 TensorFlow 提供了一个神器:tf.keras.layers.Normalization。
在线统计 vs 离线预估?Keras 层全都要!
过去我们常在训练前用 Scikit-learn 的StandardScaler先算好均值和标准差,保存下来供线上使用。但这样容易引入偏差——万一线上来了个“999岁”的用户怎么办?
更好的方式是让归一化层自己“学会”统计数据,并随着模型一起保存:
# 创建归一化层 normalizer = tf.keras.layers.Normalization(axis=-1) # 用训练数据“适应”(adapt),自动计算 mean 和 variance price_data = tf.constant([[100.], [200.], [300.], [150.], [250.]]) normalizer.adapt(price_data) # 构建模型片段 model = tf.keras.Sequential([ normalizer, tf.keras.layers.Dense(64, activation='relu') ]) # 推理时自动应用相同变换 output = model(tf.constant([[180.]])) print(output.shape) # (1, 64)这个normalizer不仅记录了均值和方差,还能在分布式训练中通过sync_on_read实现跨设备同步统计,非常适合 TFX 这类生产级 pipeline。
⚠️避坑提醒:
adapt()必须只在训练集上调用!千万别把验证集或未来数据混进去,否则就是“数据泄露”。
对偏态分布更友好的选择:分位数缩放
有些数值特征严重右偏,比如“用户总消费金额”,大多数人几百块,少数人几万。这时 Z-Score 标准化会让大多数样本挤在一起。
你可以考虑先做对数变换,或者使用自定义分位数归一化:
# 示例:对数缩放 + 归一化 log_price = tf.math.log(price_data + 1) # 加1避免 log(0) normalizer_log = tf.keras.layers.Normalization() normalizer_log.adapt(log_price)这类技巧虽然简单,但在实际项目中往往能带来 AUC 提升 0.5% 以上。
序列型特征:让用户的行为历史“活”起来
如果说静态特征描述的是“你是谁”,那行为序列回答的是“你现在想要什么”。用户的最近点击、搜索、加购列表,是捕捉短期兴趣的关键。
但问题是:每个人的历史长度不一样,怎么变成固定维度的输入?
TensorFlow 的RaggedTensor是为此而生的。
使用 RaggedTensor 处理变长序列
假设我们要处理用户最近点击的商品 ID 列表:
# 变长输入:用户A点了2个商品,用户B点了4个,用户C点了1个 item_ids = tf.ragged.constant([ [101, 102], [201, 202, 203, 204], [301] ])接下来,我们需要把这些 ID 映射成向量,并聚合出一个“用户当前兴趣”的表征。最简单的办法是平均池化,但更聪明的做法是引入注意力机制(Attention)。
自定义注意力池化:让模型自己决定“谁更重要”
原理很简单:最新点击的商品通常比三天前的更重要。我们可以设计一个查询向量query,让它与每个商品嵌入计算相似度,得到注意力权重。
# 商品嵌入表(1000种商品,32维) embedding_table = tf.Variable(tf.random.normal((1000, 32))) # 查找嵌入,保留 ragged 结构 embedded_sequence = tf.nn.embedding_lookup(embedding_table, item_ids) # shape: (3, None, 32) # 查询向量(代表当前上下文,如正在浏览的页面) query = tf.Variable(tf.random.normal((32,)), name="attention_query") # 计算注意力分数:点积相似度 attn_scores = tf.reduce_sum(embedded_sequence * query, axis=-1, keepdims=True) # (3, N, 1) # softmax 归一化权重 attn_weights = tf.nn.softmax(attn_scores, axis=1) # 沿序列维度归一 # 加权求和 → 固定长度输出 user_context = tf.reduce_sum(embedded_sequence * attn_weights, axis=1) # (3, 32) print(user_context.shape) # (3, 32)最终输出的user_context就是一个融合了“行为重要性”的用户动态表征,可以直接拼接到主模型的输入中。
💡进阶建议:可以把
query设计成由当前候选 item 的嵌入生成,实现“候选感知注意力”(Candidate-aware Attention),这正是 DIN 模型的核心思想。
高效数据 Pipeline:别让 I/O 拖慢你的 GPU
你有没有算过一笔账?假设你有 1TB 的 TFRecord 数据,每秒只能读取 100MB,那一个 epoch 就要 100 秒。而 GPU 训练可能只要 20 秒——这意味着 GPU 80% 的时间在“干等”。
解决之道只有一个:构建高性能 tf.data pipeline。
推荐系统的标准流水线架构
TFRecord 文件 → 解析 (parse) → 特征变换 (transform) → 批量化 (batch) → 预取 (prefetch) → GPU 训练每一环都能优化。
实战代码:榨干 CPU 和磁盘 IO
def parse_fn(record): features = { 'user_id': tf.io.FixedLenFeature([], tf.string), 'age': tf.io.FixedLenFeature([], tf.int64), 'clicked_items': tf.io.VarLenFeature(tf.int64), # 变长序列 'label': tf.io.FixedLenFeature([], tf.float32) } parsed = tf.io.parse_single_example(record, features) # 处理稀疏张量为密集或 ragged 张量 parsed['clicked_items'] = tf.sparse.to_dense(parsed['clicked_items']) return parsed # 构建 pipeline dataset = tf.data.TFRecordDataset('data/train.tfrecord') dataset = dataset.map(parse_fn, num_parallel_calls=tf.data.AUTOTUNE) dataset = dataset.shuffle(buffer_size=10000) # 打乱样本顺序 dataset = dataset.batch(1024) # 批量化 dataset = dataset.prefetch(tf.data.AUTOTUNE) # 预取下一批 # 直接用于训练 model.fit(dataset, epochs=5)几个关键优化点:
num_parallel_calls=tf.data.AUTOTUNE:让 TensorFlow 自动选择最优并发数,充分利用多核 CPU。prefetch:实现流水线并行,当前 batch 在 GPU 上训练时,CPU 已经在准备下一个 batch。- 使用
TFRecord格式:二进制存储,支持压缩,读取速度快于 CSV 百倍不止。
生产环境加分项
- 缓存小数据集:若数据能全载入内存,加一句
.cache(),第二个 epoch 开始几乎无延迟。 - 文件级并行读取:大数据集可用
interleave实现多个文件同时读取:python dataset = tf.data.Dataset.list_files("data/*.tfrecord") dataset = dataset.interleave( tf.data.TFRecordDataset, cycle_length=4, num_parallel_calls=tf.data.AUTOTUNE ) - 分布训练兼容:配合
strategy.experimental_distribute_dataset实现多 GPU/TPU 自动负载均衡。
最后说两句:特征工程的本质是什么?
很多人觉得特征工程是“脏活累活”,不如搞个新模型酷炫。但真相是:再牛的模型也救不了垃圾特征。
你在特征处理上的每一个细节——是否合理归一化、有没有考虑哈希冲突、序列建模是否加入注意力——都会在 AUC 曲线上留下痕迹。
而 TensorFlow 正好提供了一套端到端的工具链:
tf.feature_column和 Keras Preprocessing Layers:统一线上线下特征逻辑;tf.RaggedTensor和tf.sparse:灵活表达复杂结构;tf.data:打造高性能数据引擎,不让 I/O 成短板。
掌握这些,你就不只是“调参侠”,而是真正能构建稳定、高效、可落地推荐系统的工程师。
如果你正在从传统机器学习转向深度推荐,不妨试着把原来的 Pandas 预处理脚本,一步步迁移到tf.data+Keras Layers的声明式流程中。你会发现,不仅代码更干净,模型表现也会悄然提升。
欢迎在评论区分享你的特征工程踩坑经历,我们一起打怪升级。