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_trainx_test:训练集和测试集的特征数据
  • y_trainy_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.2
  • dnn_if_bn:是否使用批归一化,这里是True
  • dnn_if_ln:是否使用层归一化,这里是False
  • embed_dim:嵌入维度,这里是16
  • embed_initializer:嵌入层初始化方法,这里是'glorot_uniform'
  • model_l1model_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模型的优势

  1. 解决样本选择偏差

    • 传统CVR模型只在点击样本上训练,导致样本选择偏差
    • ESMM在整个样本空间上训练,避免了样本选择偏差
  2. 缓解数据稀疏问题

    • 转化样本通常比点击样本少得多,导致数据稀疏
    • ESMM通过引入CTR任务,增加了模型的训练信号
  3. 特征表示共享

    • CTR和CVR任务共享底层特征表示
    • 有助于模型学习更通用的特征表示
  4. 端到端训练

    • 同时优化CTR和CTCVR两个目标
    • 不需要分阶段训练,简化了模型训练流程

5. 使用建议

  1. 数据预处理

    • 确保数据中的CTR和CVR标签符合逻辑关系(只有点击样本才可能转化)
    • 对特征进行适当的归一化和标准化处理
  2. 超参数调优

    • 调整dnn_hidden_units的大小和层数
    • 调整dnn_dropout的值,防止过拟合
    • 调整embed_dim的值,影响嵌入表示的丰富程度
  3. 任务权重平衡

    • 可以为CTR和CTCVR任务设置不同的损失权重
    • 根据业务需求调整两个任务的重要性
  4. 模型评估

    • 分别评估CTR、CTCVR和CVR三个指标
    • 对于排序任务,重点关注Top-K的准确率和召回率

通过以上代码和说明,你应该能够使用mlgb库成功实现ESMM模型,并将其应用到实际的多任务学习场景中,同时完成排序和转化预测两个任务。

posted @ 2025-03-06 17:05  zedliu  阅读(341)  评论(0)    收藏  举报