ESMM模型实现详解
ESMM模型实现详解
下面我将详细介绍如何使用mlgb库实现ESMM(Entire Space Multi-task Model)模型,用于同时处理点击预测和转化预测两个任务。我们将不使用GPU,并生成测试数据进行模型训练和评估。
1. ESMM模型原理
ESMM模型是阿里巴巴提出的一种多任务学习模型,主要解决了CVR(转化率)预测中的样本选择偏差和数据稀疏问题。其核心思想是:
- 同时建模CTR(点击率)和CTCVR(点击并转化率)两个任务
- 利用 pCVR = pCTCVR / pCTR 的关系间接学习CVR
- 在整个样本空间上进行学习,而不仅仅是点击样本
2. 完整代码实现
# coding=utf-8
# 导入必要的库
import os
import numpy as np
import pandas as pd
import tensorflow as tf
from sklearn.metrics import roc_auc_score, precision_score, recall_score
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split
# 导入mlgb库
from mlgb import get_model
from mlgb.data import get_multitask_label_data
from mlgb.utils import check_filepath
# 设置随机种子,确保结果可复现
seed = 42
np.random.seed(seed)
tf.random.set_seed(seed)
# 禁用GPU
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
print("使用CPU进行训练")
# 设置模型名称和相关目录
model_name = 'ESMM'
print(f'模型名称: {model_name}')
# 创建保存模型和日志的目录
tmp_dir = '.tmp'
model_dir = f'{tmp_dir}/{model_name}_tf'
log_dir = f'{model_dir}/log_dir'
save_model_dir = f'{model_dir}/save_model'
check_filepath(tmp_dir, model_dir, log_dir, save_model_dir)
# 生成多任务学习的样本数据
print("正在生成测试数据...")
feature_names, (x_train, y_train), (x_test, y_test) = get_multitask_label_data(
n_samples=10000, # 生成10000条样本
negative_class_weight=0.9, # 负样本权重为0.9
multitask_cvr=0.3, # 多任务转化率为0.3
test_size=0.2, # 测试集占比20%
seed=seed, # 随机种子
)
# 将标签数据分离为两个任务的标签
y_train_ctr, y_train_cvr = y_train[:, 0], y_train[:, 1]
y_test_ctr, y_test_cvr = y_test[:, 0], y_test[:, 1]
# 打印数据信息
print(f'特征结构: {[len(names) for names in feature_names]}')
print(f'训练集样本数: {len(y_train_ctr)}')
print(f'测试集样本数: {len(y_test_ctr)}')
print(f'训练集点击率: {np.mean(y_train_ctr):.4f}')
print(f'训练集转化率: {np.sum(y_train_cvr) / np.sum(y_train_ctr):.4f}')
print(f'训练集点击并转化率: {np.mean(y_train_cvr):.4f}')
# 使用get_model函数创建ESMM模型
print("正在构建ESMM模型...")
model = get_model(
feature_names=feature_names,
model_name=model_name,
task=('binary', 'binary'), # 两个二分类任务
aim='mtl', # 多任务学习
lang='tf', # 使用TensorFlow
seed=seed,
# ESMM特有参数
dnn_hidden_units=(128, 64, 32), # DNN隐藏层单元数
dnn_activation='relu', # DNN激活函数
dnn_dropout=0.2, # DNN的dropout率
dnn_if_bn=True, # 是否使用批归一化
dnn_if_ln=False, # 是否使用层归一化
embed_dim=16, # 嵌入维度
embed_initializer='glorot_uniform', # 嵌入层初始化方法
model_l1=0.0, # L1正则化系数
model_l2=0.0001, # L2正则化系数
)
# 编译模型
print("正在编译模型...")
model.compile(
loss=[tf.losses.BinaryCrossentropy(), tf.losses.BinaryCrossentropy()], # 两个任务都使用二元交叉熵损失
optimizer=tf.optimizers.Adam(learning_rate=0.001), # 使用Adam优化器
metrics=[
tf.metrics.AUC(), # 使用AUC作为评估指标
tf.metrics.BinaryAccuracy(), # 使用准确率作为评估指标
],
)
# 打印模型摘要
model.summary()
# 训练模型
print("开始训练模型...")
history = model.fit(
x=x_train, # 训练数据
y=[y_train_ctr, y_train_cvr], # 训练标签
batch_size=256, # 批次大小
epochs=10, # 训练轮数
validation_split=0.1, # 验证集比例
callbacks=[
tf.keras.callbacks.TensorBoard(log_dir=log_dir), # TensorBoard回调
tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=3, min_delta=0.001), # 早停回调
tf.keras.callbacks.ModelCheckpoint(
filepath=os.path.join(save_model_dir, 'best_model'),
monitor='val_loss',
save_best_only=True,
save_weights_only=False,
), # 模型检查点回调
]
)
# 打印训练历史
print("训练历史:")
history_df = pd.DataFrame(history.history)
print(history_df.tail())
# 在测试集上评估模型
print("在测试集上评估模型...")
test_results = model.evaluate(x=x_test, y=[y_test_ctr, y_test_cvr], batch_size=256, return_dict=True)
print(f"测试集评估结果: {test_results}")
# 在测试集上进行预测
print("在测试集上进行预测...")
y_pred = model.predict(x_test)
y_pred_ctr = y_pred[0].flatten() # CTR预测结果
y_pred_ctcvr = y_pred[1].flatten() # CTCVR预测结果
# 计算CVR预测结果
# 注意:只对点击样本计算CVR
clicked_indices = y_test_ctr == 1
y_pred_cvr = np.zeros_like(y_pred_ctr)
y_pred_cvr[clicked_indices] = y_pred_ctcvr[clicked_indices] / np.maximum(y_pred_ctr[clicked_indices], 1e-8)
# 计算评估指标
ctr_auc = roc_auc_score(y_test_ctr, y_pred_ctr)
ctcvr_auc = roc_auc_score(y_test_cvr, y_pred_ctcvr)
cvr_auc = roc_auc_score(y_test_cvr[clicked_indices], y_pred_cvr[clicked_indices])
print(f"CTR AUC: {ctr_auc:.4f}")
print(f"CTCVR AUC: {ctcvr_auc:.4f}")
print(f"CVR AUC: {cvr_auc:.4f}")
# 排序任务:找出最有可能被点击的10个物品
print("\n执行排序任务:找出最有可能被点击的10个物品")
# 假设每个样本代表一个物品
top_k = 10
top_indices = np.argsort(y_pred_ctr)[-top_k:][::-1] # 获取点击概率最高的10个索引
print(f"点击概率最高的{top_k}个物品索引: {top_indices}")
print(f"对应的点击概率: {y_pred_ctr[top_indices]}")
print(f"对应的真实点击标签: {y_test_ctr[top_indices]}")
print(f"对应的转化概率: {y_pred_cvr[top_indices]}")
print(f"对应的真实转化标签: {y_test_cvr[top_indices]}")
# 计算Top-K的准确率和召回率
top_k_precision = precision_score(y_test_ctr[top_indices], np.ones_like(top_indices))
top_k_recall = np.sum(y_test_ctr[top_indices]) / np.sum(y_test_ctr)
print(f"Top-{top_k}准确率: {top_k_precision:.4f}")
print(f"Top-{top_k}召回率: {top_k_recall:.4f}")
# 保存模型
model.save(filepath=save_model_dir, save_format='tf')
print(f"模型已保存到: {save_model_dir}")
# 加载模型并验证
print("\n加载保存的模型并验证...")
loaded_model = tf.keras.models.load_model(filepath=save_model_dir)
loaded_pred = loaded_model.predict(x_test)
# 验证加载的模型预测结果与原模型一致
ctr_match = np.allclose(loaded_pred[0], y_pred[0])
ctcvr_match = np.allclose(loaded_pred[1], y_pred[1])
print(f"加载模型CTR预测与原模型一致: {ctr_match}")
print(f"加载模型CTCVR预测与原模型一致: {ctcvr_match}")
print("\nESMM模型实现完成!")
3. 代码详解
3.1 数据准备
# 生成多任务学习的样本数据
feature_names, (x_train, y_train), (x_test, y_test) = get_multitask_label_data(
n_samples=10000, # 生成10000条样本
negative_class_weight=0.9, # 负样本权重为0.9
multitask_cvr=0.3, # 多任务转化率为0.3
test_size=0.2, # 测试集占比20%
seed=seed, # 随机种子
)
# 将标签数据分离为两个任务的标签
y_train_ctr, y_train_cvr = y_train[:, 0], y_train[:, 1]
y_test_ctr, y_test_cvr = y_test[:, 0], y_test[:, 1]
这部分代码使用get_multitask_label_data函数生成多任务学习的样本数据:
n_samples:生成的样本数量negative_class_weight:负样本的权重,这里设为0.9表示90%的样本为负样本multitask_cvr:多任务转化率,这里设为0.3表示30%的点击样本会转化test_size:测试集占比,这里设为0.2表示20%的样本用于测试seed:随机种子,确保结果可复现
生成的数据包括:
feature_names:特征名称列表x_train、x_test:训练集和测试集的特征数据y_train、y_test:训练集和测试集的标签数据,其中第一列是CTR标签,第二列是CTCVR标签
3.2 模型构建
# 使用get_model函数创建ESMM模型
model = get_model(
feature_names=feature_names,
model_name=model_name,
task=('binary', 'binary'), # 两个二分类任务
aim='mtl', # 多任务学习
lang='tf', # 使用TensorFlow
seed=seed,
# ESMM特有参数
dnn_hidden_units=(128, 64, 32), # DNN隐藏层单元数
dnn_activation='relu', # DNN激活函数
dnn_dropout=0.2, # DNN的dropout率
dnn_if_bn=True, # 是否使用批归一化
dnn_if_ln=False, # 是否使用层归一化
embed_dim=16, # 嵌入维度
embed_initializer='glorot_uniform', # 嵌入层初始化方法
model_l1=0.0, # L1正则化系数
model_l2=0.0001, # L2正则化系数
)
这部分代码使用get_model函数创建ESMM模型:
feature_names:特征名称列表model_name:模型名称,这里是'ESMM'task:任务类型,这里是两个二分类任务aim:目标,这里是多任务学习'mtl'lang:使用的框架,这里是'tf'(TensorFlow)
ESMM特有参数:
dnn_hidden_units:DNN隐藏层单元数,这里是(128, 64, 32)dnn_activation:DNN激活函数,这里是'relu'dnn_dropout:DNN的dropout率,这里是0.2dnn_if_bn:是否使用批归一化,这里是Truednn_if_ln:是否使用层归一化,这里是Falseembed_dim:嵌入维度,这里是16embed_initializer:嵌入层初始化方法,这里是'glorot_uniform'model_l1、model_l2:L1和L2正则化系数
3.3 模型训练
# 编译模型
model.compile(
loss=[tf.losses.BinaryCrossentropy(), tf.losses.BinaryCrossentropy()], # 两个任务都使用二元交叉熵损失
optimizer=tf.optimizers.Adam(learning_rate=0.001), # 使用Adam优化器
metrics=[
tf.metrics.AUC(), # 使用AUC作为评估指标
tf.metrics.BinaryAccuracy(), # 使用准确率作为评估指标
],
)
# 训练模型
history = model.fit(
x=x_train, # 训练数据
y=[y_train_ctr, y_train_cvr], # 训练标签
batch_size=256, # 批次大小
epochs=10, # 训练轮数
validation_split=0.1, # 验证集比例
callbacks=[
tf.keras.callbacks.TensorBoard(log_dir=log_dir), # TensorBoard回调
tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=3, min_delta=0.001), # 早停回调
tf.keras.callbacks.ModelCheckpoint(
filepath=os.path.join(save_model_dir, 'best_model'),
monitor='val_loss',
save_best_only=True,
save_weights_only=False,
), # 模型检查点回调
]
)
这部分代码编译并训练ESMM模型:
- 编译模型时,为两个任务都使用二元交叉熵损失函数,使用Adam优化器,并设置AUC和准确率作为评估指标
- 训练模型时,设置批次大小为256,训练轮数为10,验证集比例为0.1
- 使用TensorBoard回调记录训练过程,使用早停回调防止过拟合,使用模型检查点回调保存最佳模型
3.4 模型评估与预测
# 在测试集上评估模型
test_results = model.evaluate(x=x_test, y=[y_test_ctr, y_test_cvr], batch_size=256, return_dict=True)
# 在测试集上进行预测
y_pred = model.predict(x_test)
y_pred_ctr = y_pred[0].flatten() # CTR预测结果
y_pred_ctcvr = y_pred[1].flatten() # CTCVR预测结果
# 计算CVR预测结果
# 注意:只对点击样本计算CVR
clicked_indices = y_test_ctr == 1
y_pred_cvr = np.zeros_like(y_pred_ctr)
y_pred_cvr[clicked_indices] = y_pred_ctcvr[clicked_indices] / np.maximum(y_pred_ctr[clicked_indices], 1e-8)
# 计算评估指标
ctr_auc = roc_auc_score(y_test_ctr, y_pred_ctr)
ctcvr_auc = roc_auc_score(y_test_cvr, y_pred_ctcvr)
cvr_auc = roc_auc_score(y_test_cvr[clicked_indices], y_pred_cvr[clicked_indices])
这部分代码在测试集上评估模型并进行预测:
- 使用
model.evaluate在测试集上评估模型性能 - 使用
model.predict在测试集上进行预测,得到CTR和CTCVR的预测结果 - 计算CVR预测结果:CVR = CTCVR / CTR,但只对点击样本计算CVR
- 计算CTR、CTCVR和CVR的AUC评估指标
3.5 排序任务
# 排序任务:找出最有可能被点击的10个物品
top_k = 10
top_indices = np.argsort(y_pred_ctr)[-top_k:][::-1] # 获取点击概率最高的10个索引
# 计算Top-K的准确率和召回率
top_k_precision = precision_score(y_test_ctr[top_indices], np.ones_like(top_indices))
top_k_recall = np.sum(y_test_ctr[top_indices]) / np.sum(y_test_ctr)
这部分代码执行排序任务,找出最有可能被点击的10个物品:
- 使用
np.argsort获取点击概率最高的10个索引 - 计算Top-K的准确率和召回率,评估排序效果
4. ESMM模型的优势
-
解决样本选择偏差:
- 传统CVR模型只在点击样本上训练,导致样本选择偏差
- ESMM在整个样本空间上训练,避免了样本选择偏差
-
缓解数据稀疏问题:
- 转化样本通常比点击样本少得多,导致数据稀疏
- ESMM通过引入CTR任务,增加了模型的训练信号
-
特征表示共享:
- CTR和CVR任务共享底层特征表示
- 有助于模型学习更通用的特征表示
-
端到端训练:
- 同时优化CTR和CTCVR两个目标
- 不需要分阶段训练,简化了模型训练流程
5. 使用建议
-
数据预处理:
- 确保数据中的CTR和CVR标签符合逻辑关系(只有点击样本才可能转化)
- 对特征进行适当的归一化和标准化处理
-
超参数调优:
- 调整
dnn_hidden_units的大小和层数 - 调整
dnn_dropout的值,防止过拟合 - 调整
embed_dim的值,影响嵌入表示的丰富程度
- 调整
-
任务权重平衡:
- 可以为CTR和CTCVR任务设置不同的损失权重
- 根据业务需求调整两个任务的重要性
-
模型评估:
- 分别评估CTR、CTCVR和CVR三个指标
- 对于排序任务,重点关注Top-K的准确率和召回率
通过以上代码和说明,你应该能够使用mlgb库成功实现ESMM模型,并将其应用到实际的多任务学习场景中,同时完成排序和转化预测两个任务。

浙公网安备 33010602011771号