在前面几篇关于推荐模型的文章中,笔者均给出了示例代码,有读者反馈——想知道在 TensorFlow 中用户特征和商品特征是如何 Embedding 的?因此,笔者特意写作此文加以解答。

1. 何为 Embedding ?

关于 Embedding,笔者很久之前写过一篇文章《推荐系统(十一):推荐系统中的 Embedding》,现在看来,差强人意,不过,对 Embedding 的概念解读还是不错的,只是缺乏代码案例解读。在本文中,笔者将基于 TensorFlow 来做解读,让读者加深理解。

如下图所示,为一个极简(CTR 和 CVR 共享了交互层) “双塔模型”(详见文章《推荐系统(十五):基于双塔模型的多目标商品召回/推荐系统》),简单解读一下:

  1. User Feature 和 Item Feature 先经过 Embedding Layer 处理,得到特征的 Embedding;
  2. User Feature Embedding 和 Item Feature Embedding 经过 Concat Layer 连接后输入到 DNN 网络;这样直接 Concat 得到的 Embedding 结果被称为 User 和 Item 的 “表示(Representation)”,显然,这种 “表示” 比较粗糙;
  3. 经过 MLP 处理,得到 User Vector 和 Item Vector,相较于上一步的 “表示形式”,User Vector 和 Item Vector 要 “精细” 得多,是真正意义上的 User Embedding 和 Item Embedding。
  4. User Embedding 和 Item Embedding 计算内积后经过 Sigmoid 函数处理(即图中的 Prediction),即可得到一个 0~1 之间的数值,即概率。
  5. 对于商品点击(1-点击,0-未点击)和商品转化(1-转化,0-未转化)这种二分类问题,结合模型预测的概率和样本 Label,很容易计算出损失(二分类问题一般采用交叉墒损失)。
  6. 对于 CTR 和 CVR 这种多任务场景,需要将 CTR Loss 和 CVR Loss 加权融合作为最终的损失,进而指导训练模型。
    在这里插入图片描述

2.ID 类特征 Embedding(方案一)

在 User Feature 和 Item Feature 中,User ID 和 Item ID 是最为重点的特征之一,是典型的 “高维稀疏” 特征。直接以原始数据形式输入模型是不行的,必须经过 Embedding Layer 的处理。在此,以 Item ID(规模较小) 为例,Embedding 处理的代码如下:

# 模拟生成商品特征,其中 item_id 取值[1, 10000]
num_items = 10000
item_data = {
    'item_id': np.arange(1, num_items + 1),
    'item_category': np.random.choice(['electronics', 'books', 'clothing'], size=num_items),
    'item_brand': np.random.choice(['brandA', 'brandB', 'brandC'], size=num_items),
    'item_price': np.random.randint(1, 199, size=num_items)
}

# 基于 TensorFlow 对原始的 item_id 进行 Embedding 处理,分为两步
item_id = feature_column.categorical_column_with_identity('item_id', num_buckets=num_items)
item_id_emb = feature_column.embedding_column(item_id, dimension=8)

1. 分类列的创建:categorical_column_with_identity

item_id = feature_column.categorical_column_with_identity('item_id', num_buckets=num_items)
  • 功能:将输入的整数 item_id 直接映射为分类标识。例如,若 num_items=1000,则输入的 item_id 必须是 [0,
    1, 2, …, 999] 范围内的整数。
  • 本质:这类似于对 item_id 做 One-Hot 编码(但底层实现更高效,不显式生成稀疏矩阵)。

2.嵌入列的创建:embedding_column

item_id_emb = feature_column.embedding_column(item_id, dimension=8)
  • 功能:将高维稀疏的分类 ID(如 num_items=1000 维的 One-Hot 向量)映射到低维稠密的连续向量空间(维度为 8)。
  • 关键点:嵌入矩阵的维度是 [num_items, 8],即每个 item_id 对应一个 8 维向量。这个嵌入矩阵是一个可训练参数,初始值随机(如 Glorot 初始化),通过神经网络的反向传播逐步优化。

3.嵌入向量的训练过程

  • 何时生成:嵌入矩阵的值并非预先计算,而是在模型训练时动态学习。
  • 如何学习
    1-输入数据中的 item_id 会触发嵌入层查找对应的 8 维向量。
    2-在反向传播时,优化器(如Adam)根据损失函数的梯度调整嵌入矩阵的值。
    3-模型通过最小化损失函数,迫使相似的 item_id 在嵌入空间中靠近,从而捕捉潜在语义关系(如用户行为中的物品相似性)。

4. 嵌入层的底层实现

当你在模型中调用 item_id_emb 时,TensorFlow 会隐式完成以下操作:

# 伪代码解释
# 隐式创建可训练参数矩阵
embedding_matrix = tf.Variable(  
    initial_value=tf.random.uniform([num_items, 8]), 
    name="item_id_embedding"
)
# 通过tf.nn.embedding_lookup动态检索
# 每次前向传播时,根据输入的ID索引查表获取对应向量
item_id_emb = tf.nn.embedding_lookup(embedding_matrix, input_item_ids)

5. 嵌入的优势

  • 降维:将高维稀疏特征压缩为低维稠密向量(例如从1000 维的 One-Hot 降到 8 维)。
  • 语义学习:模型自动学习嵌入空间中的几何关系(如相似物品的向量距离更近)。
  • 泛化性:即使某些 item_id 在训练数据中出现次数少,其嵌入向量仍可通过相似物品的梯度更新得到合理表示。

6. 完整流程示例

假设你的模型是一个推荐系统,处理流程如下:

  • 输入层:接收原始特征(如 {‘item_id’: 5})。
  • 特征转换:通过 item_id_emb 将 item_id=5 转换为一个 8 维向量。
  • 神经网络:将嵌入向量输入全连接层(如 DNN)、激活函数等后续结构。
  • 训练:通过损失函数(如点击率预测的交叉熵)反向传播,更新嵌入矩阵和其他权重。

3.ID 类特征 Embedding(方案二)

相较于 item ID,User ID 的数据规模可能要大得多,若按照方案一,则需要为每一个 ID 映射一个 embedding 向量,那么,User ID 对应的 Embedding 矩阵大小为 num_users x embedding_dimension,如果用户有 10 亿,embedding_dimension = 16,则单就 User ID 对应的 embedding 矩阵参数就有 16 B(160亿),这个数量相当巨大!!!
与此同时,对于任何一个平台而言,虽然总用户规模巨大(如 10亿),但是活跃用户通常只占据较小的比例,比如 1亿(甚至千万级),鉴于此,对所有用户 ID 单独映射 embedding 是没有必要的,存在巨大的空间浪费。此外,User ID 是随着时间增加的,因此还需要 embedding 方案具备动态处理机制。

3.1 Embedding 步骤

embedding 的步骤与方案一相同,这里简单介绍一下。

1.创建分类特征列

使用 categorical_column_with_identity 将用户 ID 直接映射为整数索引,索引范围:[0, hash_bucket_size)。

# 设置 2000W 个分桶,即预估活跃用户为 1000W 左右
hash_bucket_size = 20000000
user_id = tf.feature_column.categorical_column_with_identity(
	    'user_id', num_buckets=hash_bucket_size)

2.生成 Embedding 列

通过 embedding_column 将高维稀疏 ID 映射为低维稠密向量:

user_id_emb = tf.feature_column.embedding_column(
	    user_id, dimension=16, initializer=tf.keras.initializers.GlorotUniform())

3.2 关键设计考量

很明显,如果哈希分桶的数量少于 User ID 数量,则存在哈希冲突;如果哈希分桶的数量等于 User ID 数量,则与方案一并无差异。

在实践中,哈希分桶的数量是远远少于 User ID 的数量的,哈希分桶在用户 ID Embedding 中的应用看似存在参数量的矛盾,实则蕴含重要的工程权衡。以下从技术原理、实际场景、优化策略三个维度详细解析其意义:

一、哈希分桶的核心价值

1.动态扩展能力
  • 无需预定义词汇表:当用户ID无法提前枚举(如社交平台每日新增用户)时,哈希分桶可自动处理新ID;
  • 示例:短视频平台每天新增百万用户,若使用原始 ID 编码需每天重建模型,而哈希分桶无需调整结构;
2.内存效率的辩证关系
  • 原始编码:10亿用户,16维 embedding,参数量 10亿*16 = 16B 参数;
  • 哈希分桶:0.5亿分桶,16维 embedding,参数量 0.5*16 = 0.8B 参数;
  • 实际优势场景:当活跃用户(真实用户)量远小于 User ID 数量时,具备绝对的优势。事实上,在很多平台都存在类似的现象,即注册用户 ID 多,但真实的活跃用户少。
3.系统稳定性保障
  • 内存上限可控:通过固定hash_bucket_size可严格限制内存消耗,避免用户激增导致OOM;
  • 线上服务优势:在Kubernetes等容器化部署中,可基于固定内存预算进行资源分配;

二、参数增大的合理性场景分析

当建议设置 hash_bucket_size=2-5 倍用户数导致参数多于原始 ID 时,仍需采用哈希分桶的典型情况:

1.长尾分布场景
  • 案例:电商平台有10 亿注册用户,但月活用户仅 1 亿,用户ID embedding 维数 16;
  • 策略:设置 hash_bucket_size= 2亿,用 2亿16 参数服务活跃用户,而非维护全量 10亿16 参数表;
2.动态ID体系
  • 场景特征:用户ID包含时间戳、设备指纹等组合信息,导致唯一ID数量爆炸式增长;
  • 示例:IoT设备ID格式为"型号_区域_时间戳",每日新增千万级ID,哈希分桶可保持固定内存消耗;
3.模型泛化需求
  • 冷启动增强:通过强制哈希冲突使新用户共享已有用户表征,提升冷启动效果;
  • 示例:某电商 APP 采用 5 倍哈希桶后,新用户 CTR 提升;

三、哈希分桶的不可替代性

1.在线学习兼容性
  • 哈希分桶天然支持新增ID的实时处理,而原始ID编码需停机更新词汇表;
  • AB测试结果:在实时推荐场景下,哈希分桶方案相比原始ID编码,吞吐量提升,延迟降低;
2.多模态数据统一处理
  • 可对用户ID、设备ID、地理位置等异构标识符统一进行哈希分桶;
  • 架构优势:简化特征处理流水线,降低系统复杂度
3.联邦学习适配

在隐私计算场景下,哈希分桶可对加密后的用户标识进行统一映射,避免原始ID暴露;

3.3 常见问题解决方案

1.哈希冲突严重

  • 升级到 categorical_column_with_vocabulary_list(需维护用户 ID 列表);
  • 采用混合编码:高频用户精确编码,长尾用户哈希编码;

2.内存占用过高

  • 使用参数服务器分布式训练;
  • 量化压缩:将 float32 转为 float16;

通过上述实现方案,可在保持模型灵活性的同时,有效处理亿级用户规模的推荐场景。具体实施时建议监控哈希冲突率和嵌入向量相似度分布,持续优化参数配置。


4. 类别特征 Embedding

以用户性别为例:

# 模拟生成用户特征,其中用户性别是可以枚举的类别特征:male,female
user_data = {
    'user_id': np.arange(1, num_users + 1),
    'user_age': np.random.randint(18, 65, size=num_users),
    'user_gender': np.random.choice(['male', 'female'], size=num_users),
    'user_occupation': np.random.choice(['student', 'worker', 'teacher'], size=num_users),
    'city_code': np.random.randint(1, 2856, size=num_users),  # 城市编码,中国有 2856 个城市
    'device_type': np.random.randint(0, 5, size=num_users)  # 设备类型(0=Android,1=iOS等)
}
# 对性别特征进行 Embedding 处理
user_gender = feature_column.categorical_column_with_vocabulary_list(
    'user_gender', ['male', 'female'])
user_gender_emb = feature_column.embedding_column(user_gender, dimension=2)

1. 定义分类特征列

代码如下:

user_gender = feature_column.categorical_column_with_vocabulary_list(
    'user_gender', ['male', 'female'])
  • 作用:将字符串类型的性别特征(如 ‘male’ 或 ‘female’)映射为整数索引。
  • 细节: 输入特征名为 ‘user_gender’,词汇表为 [‘male’, ‘female’]。 模型会根据词汇表将 ‘male’ 编码为
    0,‘female’ 编码为 1。 如果输入的值不在词汇表中(如 ‘unknown’),默认会被映射为 -1(可通过
    num_oov_buckets 参数调整)。

2. 创建嵌入列(Embedding Column)

如下代码:

user_gender_emb = feature_column.embedding_column(user_gender, dimension=2)
  • 作用:将稀疏的整数索引转换为密集的低维向量(嵌入向量)。
  • 细节
    1.嵌入矩阵的维度:嵌入矩阵的形状为 (vocab_size, embedding_dimension),即 (2, 2)。 行数 2:对应词汇表中的两个类别(male 和 female);列数 2:指定的嵌入维度 dimension=2。
    2.嵌入初始化:嵌入向量的初始值默认通过随机均匀分布生成(可通过 initializer 参数自定义)。
    3.训练过程:嵌入向量会在模型训练时通过反向传播自动优化,学习与任务相关的语义表示。

5.数值特征 Embedding

数值特征是一种简单的特征,按照常理,可以直接用原始数据进行模型训练和预测,然而,由于不同类型的数值特征存在 “量纲差异”,从而使得不同类型的数值特征 “不可比较”(如年龄数值区间(0~150),价格区间(0~10000000)),因此,数值特征也需要处理,比如标准化/归一化、离散分箱等。

5.1 方案一:标准化后直接输入

标准化处理的好处如下:

  • 统一特征尺度,避免梯度下降因不同特征量纲而震荡。
  • 所有特征在相同尺度下,模型权重更新更均衡。
  • L1/L2正则化对所有特征施加相似强度的惩罚。

以用户年龄为例:

scaler_age = StandardScaler()
df['user_age'] = scaler_age.fit_transform(df[['user_age']])
user_age = feature_column.numeric_column('user_age')

1. 数据标准化处理

代码如下:

scaler_age = StandardScaler()
  • 作用:创建一个标准化处理器,用于对数值型特征(如年龄)进行均值方差标准化(Z-Score标准化)。
  • 细节:StandardScaler 是 scikit-learn 库中的标准化工具,核心操作为:标准化值 =(原始值−均值)/ 标准差 标准化后,数据分布均值为 0,标准差为 1,消除量纲差异。适用于数值范围大、分布不均衡的特征(如年龄范围可能从 0 到 100)。

2.应用标准化到年龄列

代码如下:

df['user_age'] = scaler_age.fit_transform(df[['user_age']])
  • 作用:对 DataFrame 中的 user_age 列进行拟合和转换,实现标准化。
  • 细节
    1. fit_transform 两步合并。fit:计算 user_age 列的均值(μ)和标准差(σ)。transform:使用公式:(X−μ)/ σ,对所有样本进行标准化。
    2. 示例:假设原始年龄数据为 [20, 30, 40],均值为 30,标准差为 8.16,标准化后为 [-1.22, 0, 1.22]。
    3. 存储参数:scaler_age 对象会保存计算出的 μ 和 σ,便于后续对新数据(如测试集)使用 transform 而非重新拟合。

3.创建数值特征列

代码如下:

user_age = feature_column.numeric_column('user_age')
  • 作用:定义 TensorFlow 模型可接收的数值型特征列,将标准化后的年龄值直接输入模型。
  • 细节
    1. 输入数据类型:该列接收的是连续数值(如标准化后的 -1.22、0、1.22)。
    2. 模型中的处理:在训练时,每个样本的 user_age 值会以浮点数形式直接传递给神经网络,无需进一步编码。
    3. 参数扩展性: 可结合其他参数增强特征(例如 normalizer_fn 可添加自定义归一化,但此处已提前标准化,通常不再需要)。

5.2 方案二:分箱后转为类别特征

分箱处理的优势:捕捉年龄段的非线性关系,避免直接输入连续值带来的量纲问题。

1.分箱离散化

将连续年龄分段为桶(如 0-18, 19-30, 31-45 等),代码如下:

age_bucket = tf.feature_column.bucketized_column(
	    tf.feature_column.numeric_column('age'),
	    boundaries=[18, 30, 45, 60])

2.生成 Embedding 列

对分箱后的类别进行 Embedding,代码如下:

age_emb = tf.feature_column.embedding_column(
	    age_bucket, dimension=4)

6. Embedding 层是如何学习的?

在商品推荐系统中,Embedding 层的反向传播优化机制通过梯度链式法则实现,其核心在于 Embedding 矩阵的梯度计算与参数更新。以下结合TensorFlow实现机制,从数学原理、梯度传播路径和参数更新逻辑三个维度展开说明。

6.1 数学原理与梯度计算

在这里插入图片描述

6.2 TensorFlow实现机制

1.梯度计算优化(以用户ID为例)

# 正向传播:嵌入查找(实际为矩阵乘法的高效实现)
user_emb = tf.nn.embedding_lookup(embedding_matrix, user_ids)  # shape: [batch, d]
	
# 反向传播:自动微分机制
with tf.GradientTape() as tape:
	loss = compute_loss(model(user_emb))
	grads = tape.gradient(loss, [embedding_matrix])  # 仅更新出现过的ID对应行

稀疏梯度更新:TensorFlow 通过 tf.IndexedSlices 数据结构,仅对当前 batch 中出现过的特征 ID 对应的矩阵行进行梯度累积.

2.多特征梯度融合

当同时存在用户ID、性别、年龄等特征时,梯度传播路径为:

损失函数 → DNN层梯度 → Embedding 拼接层梯度 → { 用户ID嵌入矩阵梯度, 性别嵌入矩阵梯度, 年龄分箱嵌入矩阵梯度}

各特征 Embedding 矩阵独立接收梯度信号,通过优化器同步更新。

6.3 参数更新特性

1.稀疏性感知更新

  • 仅当前 batch 中出现的特征 ID 对应的矩阵行会被更新(如用户ID=123在本次训练未出现,则其嵌入向量保持不变);
  • 性别、年龄分箱等低基数特征在每轮训练中均有更新机会;

2.优化器作用机制

以 Adam 优化器为例,更新过程包含动量累积:

# 伪代码示意
for row in active_embedding_rows:  # 仅处理出现过的特征ID
	m[row] = beta1 * m[row] + (1-beta1) * grad[row]  # 一阶动量
	v[row] = beta2 * v[row] + (1-beta2) * grad[row]^2  # 二阶动量
	W[row] -= lr * m[row] / (sqrt(v[row]) + eps)

3.冷启动处理

新用户 ID 首次出现时,其嵌入向量通过随机初始化参与训练,后续随曝光次数增加逐步优化。

6.4 工程实践建议

1.梯度裁剪

对 Embedding 矩阵梯度施加范数约束,防止长尾 ID 的稀疏更新引发数值不稳定:

grads, _ = tf.clip_by_global_norm(grads, clip_norm=5.0)

2.异步更新策略

在分布式训练中,采用参数服务器架构对Embedding矩阵进行异步更新,提升训练效率。

3.冻结微调

当上层 DNN 需要高频更新时,可冻结 Embedding 层参数仅微调上层网络。

通过上述机制,Embedding 层在反向传播中实现了特征语义的分布式表示学习,其优化过程兼具数学严谨性与工程高效性。在实际推荐系统中,这种"稀疏更新+稠密表达"的特性,使其能有效处理十亿级用户ID的同时保持模型收敛稳定性。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐