推荐系统(二十五):基于阿里DIN(Deep Interest Network)的CTR模型实现
阿里巴巴的DIN(Deep Interest Network)模型是一种创新的深度学习推荐系统模型,主要用于解决电商场景中的个性化推荐问题。它于2017年由阿里巴巴团队提出(论文:《Deep Interest Network for Click-Through Rate Prediction》),核心目标是捕捉用户动态变化的兴趣,特别是在用户历史行为中与当前候选商品相关的兴趣。DIN 通过注意力机制实现了用户兴趣的动态捕捉,成为推荐系统领域的里程碑式工作。其设计思想(如局部激活、数据自适应)对后续模型(如Transformer)也有深远影响。
一、DIN模型概述
1. 背景与核心问题
在电商推荐中,用户的历史行为(如点击、购买)是重要的兴趣信号。传统模型(如DNN、YouTube DNN)通常将用户行为序列简单聚合(如平均池化),忽略了两个关键点:
- 兴趣多样性:用户历史行为可能包含多个不相关的兴趣(如同时浏览服装和电子产品)。
- 局部激活:用户对当前商品的兴趣可能仅由部分历史行为触发(如浏览“手机”时,只有“手机配件”相关行为有用)。
DIN 通过注意力机制动态学习用户兴趣的权重,解决了上述两个关键问题。
2. 模型结构
DIN 的核心改进在于用户兴趣建模部分,整体结构如下:
(1) 输入层
- 用户特征:用户ID、画像(性别、年龄等)。
- 商品特征:候选商品ID、类别、价格等。
- 用户行为序列:用户最近点击/购买的商品列表(每个商品附带特征)。
(2) 嵌入层(Embedding Layer)
将稀疏特征(如商品ID)转换为低维稠密向量。
(3) 兴趣激活层(关键部分)
- 注意力机制:计算用户行为序列中每个商品与候选商品的相关性权重。
- 权重动态化:不同候选商品会激活不同的历史行为权重(如推荐“连衣裙”时,“高跟鞋”的权重高于“游戏机”)。
- 加权求和:根据注意力权重对行为序列的嵌入向量加权求和,得到用户兴趣表示。
(4) 多层感知机(MLP)
将用户兴趣表示、候选商品特征及其他特征拼接后输入MLP,输出点击率(CTR)预测。
3. 关键技术点
(1) 局部激活单元(Local Activation Unit)
注意力机制仅依赖当前候选商品与历史行为的交互,无需全局参数,灵活捕捉动态兴趣。示例:用户历史行为包含“手机、衬衫、耳机”,当推荐“手机壳”时,“手机”和“耳机”的权重较高,而“衬衫”权重低。
(2) 自适应正则化(Data Adaptive Activation Function)
使用 Dice 激活函数替代 ReLU,根据输入数据分布自适应调整阈值,缓解稀疏特征下的过拟合问题。
(3) 兴趣强度归一化(无需Softmax)
注意力权重不强制求和为1(即不用softmax),保留用户对多个行为的绝对兴趣强度(如同时高权重“手机”和“耳机”)。
4. 优势与创新
- 动态兴趣建模:相比静态池化,DIN 能更精准反映用户当前兴趣。
- 可解释性:注意力权重可解释(如高权重行为商品与候选商品类别相似)。
- 工程友好:适用于阿里巴巴超大规模场景(亿级用户/商品)。
5. 后续演进与应用场景
DIN 的改进版本包括:
- DIEN(Deep Interest Evolution Network):引入序列模型(GRU)建模兴趣演化过程。
- DSIN(Deep Session Interest Network):将行为按会话分割,捕捉会话内和会话间兴趣。
典型应用场景
- 电商推荐(如淘宝“猜你喜欢”)。
- 广告 CTR 预估(如阿里妈妈广告系统)。
- 任何需要建模用户动态兴趣的推荐场景。
二、DIN 模型实现
本节将基于 TensorFlow 实现一个 DIN 模型的 Demo,供读者学习、参考。
1.模拟数据生成
相较于之前介绍的推荐模型,DIN 模型需要用到用户历史行为序列数据,因此模拟数据生成要相对复杂一些。
# ====================
# 1. 模拟数据生成
# 特别说明:DIN 模型需要使用用户行为序列,模型中行为序列长度设置为 SEQUENCE_LENGTH,但不同用户的实际行为序列长度是不一致的
# 因此,对于行为序列长度不足 SEQUENCE_LENGTH 的部分,需要用 0 填充,即索引 0 会被视为填充值,为了区分,特征(如性别、职业、品牌、类目等)对应词表
# 的索引都从 1 开始,而不是传统的从 0 开始。与此同时,特征词表的大小需为 "特征词表大小+1",如 user_gender 是1~2,则 vocab_size=2+1
# ====================
SEQUENCE_LENGTH = 30 # 用户行为序列长度
EMBEDDING_DIM = 8 # 特征 embedding 维数
num_users = 100 # 模拟数据用户数量
num_items = 200 # 模拟数据商品数量# 生成模拟数据,真实场景中,这些数据由特征工程产出
def generate_mock_data(num_users=100, num_items=200, num_interactions=10000):"""生成模拟用户、商品及交互数据"""# 设置随机种子保证可复现性np.random.seed(42)tf.random.set_seed(42)# 用户特征user_data = {'user_id': np.arange(1, num_users + 1),'user_age': np.random.randint(18, 65, size=num_users),# 1-male,2-female'user_gender': np.random.choice([1, 2], size=num_users),# 1-student,2-worker,3-teacher'user_occupation': np.random.choice([1, 2, 3], size=num_users),'city_code': np.random.randint(1, 2856, size=num_users),'device_type': np.random.randint(1, 5, size=num_users),'click_sequence': np.full((num_users, SEQUENCE_LENGTH), 0, dtype=np.int32), # 初始化为固定长度(SEQUENCE_LENGTH)填充 0'click_category_sequence': np.full((num_users, SEQUENCE_LENGTH), 0, dtype=np.int32), # 初始化为固定长度填充 0'click_brand_sequence': np.full((num_users, SEQUENCE_LENGTH), 0, dtype=np.int32), # 初始化为固定长度填充 0'seq_mask': np.zeros((num_users, SEQUENCE_LENGTH), dtype=bool) # 初始化为False}# 商品特征item_data = {'item_id': np.arange(1, num_items + 1),# 1-electronics,2-books,3-clothing'item_category': np.random.choice([1, 2, 3], size=num_items),# 1-brandA,2-brandB,3-brandC'item_brand': np.random.choice([1, 2, 3], size=num_items),'item_price': np.random.randint(1, 199, size=num_items)}# 创建 DataFrame 并设置索引,便于填充序列时根据 item_id 查询对应的类目和品牌df = pd.DataFrame(item_data).set_index('item_id')# 交互数据interactions = []for _ in range(num_interactions):user_idx = np.random.randint(0, num_users)user_id = user_idx + 1item_id = np.random.randint(1, num_items + 1)# 点击标签。0: 未点击, 1: 点击。在真实场景中可通过客户端埋点上报获得用户的点击行为数据click_label = np.random.randint(0, 2)interactions.append([user_id, item_id, click_label])# 查询对应的行,从而获取item id 对应的类目和品牌row = df.loc[item_id]# 如果点击,则行为序列(点击ID、点击品牌、点击类目)中需要将预填充占位的 0 替换为实际的数据:id、类目、品牌if click_label == 1:# 遍历,找到当前序列中第一个填充位(值为0),而后填充点击的 item_id、item_category、item_brandcurrent_seq = user_data['click_sequence'][user_idx]current_category_seq = user_data['click_category_sequence'][user_idx]current_brand_seq = user_data['click_brand_sequence'][user_idx]empty_slot = np.where(current_seq == 0)[0]if len(empty_slot) > 0:pos = empty_slot[0]current_seq[pos] = item_idcurrent_category_seq[pos] = row['item_category']current_brand_seq[pos] = row['item_brand']user_data['seq_mask'][user_idx, pos] = Truereturn user_data, item_data, interactions
2.合并、划分数据集
将模拟生成的样本数据划分为训练集和验证集。
# ====================
# 2. 合并、划分数据集
# ====================
# 生成数据
user_data, item_data, interactions = generate_mock_data(num_users, num_items, 1000)
# 合并用户特征、商品特征和交互数据
interaction_df = pd.DataFrame(interactions, columns=['user_id', 'item_id', 'click_label'])
user_df = pd.DataFrame({'user_id': user_data['user_id'],'user_age': user_data['user_age'],'user_gender': user_data['user_gender'],'user_occupation': user_data['user_occupation'],'city_code': user_data['city_code'],'device_type': user_data['device_type'],'click_sequence': list(user_data['click_sequence']), # 转换为列表的数组'click_category_sequence': list(user_data['click_category_sequence']), # 转换为列表的数组'click_brand_sequence': list(user_data['click_brand_sequence']), # 转换为列表的数组'seq_mask': list(user_data['seq_mask'])
})# 预览数据
pd.set_option('display.max_columns', None)
print("\nuser_df:\n", user_df[0:5])item_df = pd.DataFrame(item_data)
df = interaction_df.merge(user_df, on='user_id').merge(item_df, on='item_id')
# 划分数据集:训练集、测试集
labels = df[['click_label']]
features = df.drop(['click_label'], axis=1)
train_features, test_features, train_labels, test_labels = train_test_split(features, labels, test_size=0.2,random_state=42)
3.Local Activation Unit 实现
本部分通过候选商品与历史行为的交互特征(concat、减法、点积)生成注意力权重,实现局部兴趣激活。
# ====================
# 3.自定义注意力单元层(Local Activation Unit)
# 通过候选商品与历史行为的交互特征(concat、减法、点积)生成注意力权重,实现局部兴趣激活
# ====================
class LocalActivationUnit(Layer):def __init__(self, hidden_units=(64, 32), activation='dice'):"""注意力计算单元Args:hidden_units: 隐藏层维度,论文中使用两层全连接activation: 激活函数,论文提出Dice函数"""super().__init__()# 隐藏层self.dense_layers = [Dense(unit, activation=None) for unit in hidden_units]# 激活函数控制,原论文中激活函数可选,默认采用论文中提出的 dice 激活函数self.activation = activation# DNN部分(使用Dice激活),提前定义激活函数,数量与 hidden_units 层数保持一致self.dice = [Dice(), Dice()]# dense_layers、activation、dice 三个部分可以合并为下面的代码,更为简洁# self.dnn = tf.keras.Sequential([# Dense(64, activation=None),# Dice(),# Dense(32, activation=None),# Dice()# ])# 最终输出标量权重,即注意力分数self.final_dense = Dense(1, activation=None)def call(self, query, keys, mask=None):"""核心计算逻辑Args:query: 候选广告特征 [batch_size, embed_dim]keys: 用户历史行为序列 [batch_size, seq_len, embed_dim]mask: 序列padding掩码"""# 扩展query维度用于广播计算# 在论文中,query(候选item)需要与行为序列(keys)分别计算 attention 分数,因此需扩展成和 keys 相同的形状query = tf.expand_dims(query, axis=1) # [batch, 1, embed]query = tf.tile(query, [1, tf.shape(keys)[1], 1]) # [batch, seq, embed]# 构建交互特征(论文提出的外积+拼接)att_input = tf.concat([query, keys, # 原始特征query - keys, # 特征差值query * keys # 外积交互], axis=-1) # [batch, seq, 4*embed]print("att_input形状:", att_input.shape)# 通过多层感知机计算注意力得分for i in range(len(self.dense_layers)):att_input = self.dense_layers[i](att_input)if self.activation == 'dice':att_input = self.dice[i](att_input) # 论文提出的自适应激活函数else:att_input = tf.nn.relu(att_input)scores = self.final_dense(att_input) # [batch, seq, 1]# 从 scores 张量中移除最后一个维度(即 axis=-1)scores = tf.squeeze(scores, axis=-1) # [batch, seq]# 应用mask处理paddingif mask is not None:# 创建一个与 scores 形状相同的张量,并将其值设置为一个非常小的负数(-2 ** 32 + 1)paddings = tf.ones_like(scores) * (-2 ** 32 + 1)# 由于序列中存在人工填充占位(实际序列不足模型定义的序列长度),用选择函数 tf.where 来重塑 scores:# 如果 mask 中的某个位置的值是 True,则选择 scores 中对应位置的值;# 如果 mask 中的某个位置的值是 False,则选择 paddings 中对应位置的值scores = tf.where(mask, scores, paddings)# axis=1: 沿着第二个轴(即轴1)应用 softmax 函数,就是对每一个 seq 计算出的一组scores应用softmaxreturn tf.nn.softmax(scores, axis=1) # 归一化权重
4.Dice 激活函数实现
# ====================
# 4.自定义激活函数(Dice)
# 引入批量归一化和数据自适应门控机制,相比 PReLU 更适合稀疏数据分布
# ====================
class Dice(Layer):"""Dice激活函数实现(论文核心创新)论文:《Deep Interest Network for Click-Through Rate Prediction》"""def __init__(self, axis=-1, epsilon=1e-9):super(Dice, self).__init__()self.axis = axisself.epsilon = epsilon# 批归一化通过标准化每一层的输入来稳定和加速神经网络的训练过程。具体来说,它会对每个批次的数据进行标准化处理,# 使其均值接近0,方差接近1。这样可以减少内部协变量偏移(internal covariate shift),使得每一层的输入分# 布更加稳定,从而加速训练并提高模型的泛化能力self.bn = BatchNormalization(axis=axis, epsilon=epsilon)self.alpha = self.add_weight(name='dice_alpha', shape=(), initializer='zeros')def call(self, inputs):# 数据自适应归一化# 由于是全连接层,通常在最后一个轴(axis=-1)上进行归一化bn_inputs = self.bn(inputs)p = tf.sigmoid(bn_inputs)return self.alpha * (1.0 - p) * inputs + p * inputs
5.DIN 模型(Deep Interest Network)主体结构实现
# ====================
# 5.DIN 模型(Deep Interest Network)
# 特征处理:
# 用户行为序列使用变长mask处理
# 多值离散特征通过加权池化转为定长向量
# 用户/上下文特征直接拼接
# ====================
class DIN(Model):def __init__(self, feature_columns, behavior_feature_list, embedding_dim=8):"""注意力计算单元Args:feature_columns: 特征列配置behavior_feature_list:行为序列特征列表embedding_dim: 特征 embedding 维数,默认8维"""super().__init__()# 特征处理组件self.embed_dict = self._build_embed_layers(feature_columns, behavior_feature_list, embedding_dim)# 注意力计算单元self.attention = LocalActivationUnit(hidden_units=(64, 32))# DNN部分(使用Dice激活)self.dnn = tf.keras.Sequential([Dense(64, activation=None),Dice(),Dense(32, activation=None),Dice()])# 输出层,预估 CTR 分数self.final = Dense(1, activation='sigmoid')def _build_embed_layers(self, feature_columns, behavior_feature_list, embedding_dim):"""根据给定的特征列配置和行为序列特征列表,为每个稀疏特征创建一个嵌入层,并将其存储在一个字典中。对于行为序列特征,会显式指定序列长度。这对于处理推荐系统中的用户行为序列特别有用Args:feature_columns: 特征列配置behavior_feature_list:行为序列特征列表embedding_dim: 特征 embedding 维数,默认8维"""embed_dict = {}for feat in feature_columns:# 根据特征配置,检查当前特征是否为稀疏特征。稀疏特征通常是指那些取值范围较大但实际出现的值较少的特征,例如用户ID、商品ID等if feat['type'] == 'sparse':# 处理行为序列特征if feat['name'] in behavior_feature_list:embed_dict[feat['name']] = Embedding(feat['vocab_size'],embedding_dim,# mask_zero: 是否将输入中的0视为填充值并忽略它们,默认值为 False,对于序列特征而言,0 视为填充mask_zero=feat.get('mask_zero', False),input_length=SEQUENCE_LENGTH # 序列特征显式指定序列长度)else:embed_dict[feat['name']] = Embedding(feat['vocab_size'],embedding_dim,mask_zero=feat.get('mask_zero', False))return embed_dictdef call(self, inputs):# 用户特征user_id = inputs['user_id']user_gender = inputs['user_gender']user_occupation = inputs['user_occupation']city_code = inputs['city_code']device_type = inputs['device_type']# 候选商品特征item_id = inputs['item_id']item_category = inputs['item_category']item_brand = inputs['item_brand']# 行为序列特征,其中点击序列就是点击商品 ID 序列behavior_seq = inputs['click_sequence']behavior_category_seq = inputs['click_category_sequence']behavior_brand_seq = inputs['click_brand_sequence']seq_mask = inputs['seq_mask'] # 序列掩码print("behavior_seq 原始输入序列形状:", behavior_seq.shape) # 应为 (batch_size, 10)# 特征嵌入user_id_embed = self.embed_dict['user_id'](user_id)user_gender_embed = self.embed_dict['user_gender'](user_gender)user_occupation_embed = self.embed_dict['user_occupation'](user_occupation)city_code_embed = self.embed_dict['city_code'](city_code)device_type_embed = self.embed_dict['device_type'](device_type)item_id_embed = self.embed_dict['item_id'](item_id)item_category_embed = self.embed_dict['item_category'](item_category)item_brand_embed = self.embed_dict['item_brand'](item_brand)id_seq_embed = self.embed_dict['click_sequence'](behavior_seq)category_seq_embed = self.embed_dict['click_category_sequence'](behavior_category_seq)brand_seq_embed = self.embed_dict['click_brand_sequence'](behavior_brand_seq)print("behavior_seq 嵌入后序列形状:", id_seq_embed.shape) # 应为 (batch_size, 10, 8)# 特征拼接user_embed = tf.concat([user_id_embed,user_gender_embed,user_occupation_embed,city_code_embed,device_type_embed], axis=1)ad_embed = tf.concat([item_id_embed,item_category_embed,item_brand_embed], axis=1)# 修正为保持序列维度seq_embed = tf.concat([id_seq_embed, # (batch,10,8)category_seq_embed, # (batch,10,8)brand_seq_embed # (batch,10,8)], axis=-1) # → (batch,10,24)print("序列特征 seq_embed shape:", seq_embed.shape)# 注意力加权池化:DIN 模型的核心结构之一attention_weights = self.attention(ad_embed, seq_embed, seq_mask)behavior_embed = tf.reduce_sum(seq_embed * tf.expand_dims(attention_weights, -1), axis=1)# 特征拼接all_features = tf.concat([user_embed,behavior_embed,ad_embed], axis=1)# 深度网络预测dnn = self.dnn(all_features)return self.final(dnn)
6.模型训练与评估
# ====================
# 6. 模型训练与评估
# ====================
# 创建输入函数
def df_to_dataset(features, labels, shuffle=True, batch_size=32):# 将序列字段转换为NumPy数组并调整形状,确保序列字段转换为三维数组(batch_size, sequence_length)click_sequence = np.stack(features['click_sequence'].values).reshape(-1, SEQUENCE_LENGTH)click_category_sequence = np.stack(features['click_category_sequence'].values).reshape(-1, SEQUENCE_LENGTH)click_brand_sequence = np.stack(features['click_brand_sequence'].values).reshape(-1, SEQUENCE_LENGTH)seq_mask = np.stack(features['seq_mask'].values).reshape(-1, SEQUENCE_LENGTH)# 查询特征print("\nclick_sequence数据:\n", click_sequence)print("\nclick_sequence形状:\n", click_sequence.shape)# 检查形状应为 (batch_size, SEQUENCE_LENGTH)assert click_sequence.shape[1] == SEQUENCE_LENGTH# 构建特征字典,直接使用处理后的数组feature_dict = {'user_id': features['user_id'].values,'user_age': features['user_age'].values,'user_gender': features['user_gender'].values,'user_occupation': features['user_occupation'].values,'city_code': features['city_code'].values,'device_type': features['device_type'].values,'click_sequence': click_sequence,'click_category_sequence': click_category_sequence,'click_brand_sequence': click_brand_sequence,'seq_mask': seq_mask,'item_id': features['item_id'].values,'item_category': features['item_category'].values,'item_brand': features['item_brand'].values,'item_price': features['item_price'].values}# 使用 TensorFlow 的 tf.data.Dataset API 创建了一个数据管道,主要用于处理特征和标签数据,并为训练模型做准备ds = tf.data.Dataset.from_tensor_slices((# 每个样本是一个元组 (features, labels)# features: 来自 feature_dict 的字典;labels: 字典 {'output_1': click_label},表示模型需要学习的标签dict(feature_dict),{'output_1': labels['click_label']}))if shuffle:# 每次从数据集中随机抽取 1000 个样本并打乱顺序ds = ds.shuffle(1000)ds = ds.batch(batch_size)return ds# 转换数据集
train_ds = df_to_dataset(train_features, train_labels)
test_ds = df_to_dataset(test_features, test_labels, shuffle=False)# 定义特征配置
feature_columns1 = [{'name': 'user_id', 'type': 'sparse', 'vocab_size': num_users + 1, 'mask_zero': True},{'name': 'user_gender', 'type': 'sparse', 'vocab_size': 2 + 1, 'mask_zero': True},{'name': 'user_occupation', 'type': 'sparse', 'vocab_size': 3 + 1, 'mask_zero': True},{'name': 'city_code', 'type': 'sparse', 'vocab_size': 2856 + 1, 'mask_zero': True},{'name': 'device_type', 'type': 'sparse', 'vocab_size': 5 + 1, 'mask_zero': True},{'name': 'item_id', 'type': 'sparse', 'vocab_size': num_items + 1, 'mask_zero': True},{'name': 'item_category', 'type': 'sparse', 'vocab_size': 3 + 1, 'mask_zero': True},{'name': 'item_brand', 'type': 'sparse', 'vocab_size': 3 + 1, 'mask_zero': True},{'name': 'click_sequence', 'type': 'sparse', 'vocab_size': num_items + 1, 'mask_zero': True},{'name': 'click_category_sequence', 'type': 'sparse', 'vocab_size': 3 + 1, 'mask_zero': True},{'name': 'click_brand_sequence', 'type': 'sparse', 'vocab_size': 3 + 1, 'mask_zero': True}
]# 初始化模型
model = DIN(feature_columns1, ['click_sequence', 'click_category_sequence', 'click_brand_sequence'])
# 编译
# loss='binary_crossentropy':使用二元交叉熵损失函数,因为 CTR 预估这是一个二分类问题
# metrics=['AUC', 'accuracy']:在训练和评估时跟踪两个指标
model.compile(optimizer='adam',loss='binary_crossentropy',metrics=['AUC', 'accuracy'])# 训练
history = model.fit(train_ds,validation_data=test_ds,epochs=10,batch_size=32,verbose=1)# 评估
test_loss, test_auc, test_acc = model.evaluate(test_ds)
print(f"\n测试集评估结果:AUC={test_auc:.3f}, 准确率={test_acc:.3f}")
7.训练过程可视化与模型保存
# ====================
# 7. 绘制训练曲线
# ====================
import matplotlib.pyplot as pltplt.plot(history.history['accuracy'], label='Train Accuracy')
# plt.plot(history.history['val_accuracy'], label='Validation Accuracy')
plt.plot(history.history['loss'], label='Loss')
plt.legend()
plt.show()# ====================
# 8. 模型保存
# ====================
model.save('DIN_Recommender')
print("模型已保存到 DIN_Recommender 目录")
三、完整 DIN 模型代码
import tensorflow as tf# MAC M1 芯片不支持部分命令,因此禁用GPU设备
tf.config.set_visible_devices([], 'GPU') # 禁用GPU设备
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from tensorflow.keras.layers import Layer, Dense, Embedding, BatchNormalization
from tensorflow.keras.models import Model# ====================
# 1. 模拟数据生成
# 特别说明:DIN 模型需要使用用户行为序列,模型中行为序列长度设置为 SEQUENCE_LENGTH,但不同用户的实际行为序列长度是不一致的
# 因此,对于行为序列长度不足 SEQUENCE_LENGTH 的部分,需要用 0 填充,即索引 0 会被视为填充值,为了区分,特征(如性别、职业、品牌、类目等)对应词表
# 的索引都从 1 开始,而不是传统的从 0 开始。与此同时,特征词表的大小需为 "特征词表大小+1",如 user_gender 是1~2,则 vocab_size=2+1
# ====================
SEQUENCE_LENGTH = 30 # 用户行为序列长度
EMBEDDING_DIM = 8 # 特征 embedding 维数
num_users = 100 # 模拟数据用户数量
num_items = 200 # 模拟数据商品数量# 生成模拟数据,真实场景中,这些数据由特征工程产出
def generate_mock_data(num_users=100, num_items=200, num_interactions=10000):"""生成模拟用户、商品及交互数据"""# 设置随机种子保证可复现性np.random.seed(42)tf.random.set_seed(42)# 用户特征user_data = {'user_id': np.arange(1, num_users + 1),'user_age': np.random.randint(18, 65, size=num_users),# 1-male,2-female'user_gender': np.random.choice([1, 2], size=num_users),# 1-student,2-worker,3-teacher'user_occupation': np.random.choice([1, 2, 3], size=num_users),'city_code': np.random.randint(1, 2856, size=num_users),'device_type': np.random.randint(1, 5, size=num_users),'click_sequence': np.full((num_users, SEQUENCE_LENGTH), 0, dtype=np.int32), # 初始化为固定长度(SEQUENCE_LENGTH)填充 0'click_category_sequence': np.full((num_users, SEQUENCE_LENGTH), 0, dtype=np.int32), # 初始化为固定长度填充 0'click_brand_sequence': np.full((num_users, SEQUENCE_LENGTH), 0, dtype=np.int32), # 初始化为固定长度填充 0'seq_mask': np.zeros((num_users, SEQUENCE_LENGTH), dtype=bool) # 初始化为False}# 商品特征item_data = {'item_id': np.arange(1, num_items + 1),# 1-electronics,2-books,3-clothing'item_category': np.random.choice([1, 2, 3], size=num_items),# 1-brandA,2-brandB,3-brandC'item_brand': np.random.choice([1, 2, 3], size=num_items),'item_price': np.random.randint(1, 199, size=num_items)}# 创建 DataFrame 并设置索引,便于填充序列时根据 item_id 查询对应的类目和品牌df = pd.DataFrame(item_data).set_index('item_id')# 交互数据interactions = []for _ in range(num_interactions):user_idx = np.random.randint(0, num_users)user_id = user_idx + 1item_id = np.random.randint(1, num_items + 1)# 点击标签。0: 未点击, 1: 点击。在真实场景中可通过客户端埋点上报获得用户的点击行为数据click_label = np.random.randint(0, 2)interactions.append([user_id, item_id, click_label])# 查询对应的行,从而获取item id 对应的类目和品牌row = df.loc[item_id]# 如果点击,则行为序列(点击ID、点击品牌、点击类目)中需要将预填充占位的 0 替换为实际的数据:id、类目、品牌if click_label == 1:# 遍历,找到当前序列中第一个填充位(值为0),而后填充点击的 item_id、item_category、item_brandcurrent_seq = user_data['click_sequence'][user_idx]current_category_seq = user_data['click_category_sequence'][user_idx]current_brand_seq = user_data['click_brand_sequence'][user_idx]empty_slot = np.where(current_seq == 0)[0]if len(empty_slot) > 0:pos = empty_slot[0]current_seq[pos] = item_idcurrent_category_seq[pos] = row['item_category']current_brand_seq[pos] = row['item_brand']user_data['seq_mask'][user_idx, pos] = Truereturn user_data, item_data, interactions# ====================
# 2. 合并、划分数据集
# ====================
# 生成数据
user_data, item_data, interactions = generate_mock_data(num_users, num_items, 1000)
# 合并用户特征、商品特征和交互数据
interaction_df = pd.DataFrame(interactions, columns=['user_id', 'item_id', 'click_label'])
user_df = pd.DataFrame({'user_id': user_data['user_id'],'user_age': user_data['user_age'],'user_gender': user_data['user_gender'],'user_occupation': user_data['user_occupation'],'city_code': user_data['city_code'],'device_type': user_data['device_type'],'click_sequence': list(user_data['click_sequence']), # 转换为列表的数组'click_category_sequence': list(user_data['click_category_sequence']), # 转换为列表的数组'click_brand_sequence': list(user_data['click_brand_sequence']), # 转换为列表的数组'seq_mask': list(user_data['seq_mask'])
})# 预览数据
pd.set_option('display.max_columns', None)
print("\nuser_df:\n", user_df[0:5])item_df = pd.DataFrame(item_data)
df = interaction_df.merge(user_df, on='user_id').merge(item_df, on='item_id')
# 划分数据集:训练集、测试集
labels = df[['click_label']]
features = df.drop(['click_label'], axis=1)
train_features, test_features, train_labels, test_labels = train_test_split(features, labels, test_size=0.2,random_state=42)# ====================
# 3.自定义注意力单元层(Local Activation Unit)
# 通过候选商品与历史行为的交互特征(concat、减法、点积)生成注意力权重,实现局部兴趣激活
# ====================
class LocalActivationUnit(Layer):def __init__(self, hidden_units=(64, 32), activation='dice'):"""注意力计算单元Args:hidden_units: 隐藏层维度,论文中使用两层全连接activation: 激活函数,论文提出Dice函数"""super().__init__()# 隐藏层self.dense_layers = [Dense(unit, activation=None) for unit in hidden_units]# 激活函数控制,原论文中激活函数可选,默认采用论文中提出的 dice 激活函数self.activation = activation# DNN部分(使用Dice激活),提前定义激活函数,数量与 hidden_units 层数保持一致self.dice = [Dice(), Dice()]# dense_layers、activation、dice 三个部分可以合并为下面的代码,更为简洁# self.dnn = tf.keras.Sequential([# Dense(64, activation=None),# Dice(),# Dense(32, activation=None),# Dice()# ])# 最终输出标量权重,即注意力分数self.final_dense = Dense(1, activation=None)def call(self, query, keys, mask=None):"""核心计算逻辑Args:query: 候选广告特征 [batch_size, embed_dim]keys: 用户历史行为序列 [batch_size, seq_len, embed_dim]mask: 序列padding掩码"""# 扩展query维度用于广播计算# 在论文中,query(候选item)需要与行为序列(keys)分别计算 attention 分数,因此需扩展成和 keys 相同的形状query = tf.expand_dims(query, axis=1) # [batch, 1, embed]query = tf.tile(query, [1, tf.shape(keys)[1], 1]) # [batch, seq, embed]# 构建交互特征(论文提出的外积+拼接)att_input = tf.concat([query, keys, # 原始特征query - keys, # 特征差值query * keys # 外积交互], axis=-1) # [batch, seq, 4*embed]print("att_input形状:", att_input.shape)# 通过多层感知机计算注意力得分for i in range(len(self.dense_layers)):att_input = self.dense_layers[i](att_input)if self.activation == 'dice':att_input = self.dice[i](att_input) # 论文提出的自适应激活函数else:att_input = tf.nn.relu(att_input)scores = self.final_dense(att_input) # [batch, seq, 1]# 从 scores 张量中移除最后一个维度(即 axis=-1)scores = tf.squeeze(scores, axis=-1) # [batch, seq]# 应用mask处理paddingif mask is not None:# 创建一个与 scores 形状相同的张量,并将其值设置为一个非常小的负数(-2 ** 32 + 1)paddings = tf.ones_like(scores) * (-2 ** 32 + 1)# 由于序列中存在人工填充占位(实际序列不足模型定义的序列长度),用选择函数 tf.where 来重塑 scores:# 如果 mask 中的某个位置的值是 True,则选择 scores 中对应位置的值;# 如果 mask 中的某个位置的值是 False,则选择 paddings 中对应位置的值scores = tf.where(mask, scores, paddings)# axis=1: 沿着第二个轴(即轴1)应用 softmax 函数,就是对每一个 seq 计算出的一组scores应用softmaxreturn tf.nn.softmax(scores, axis=1) # 归一化权重# ====================
# 4.自定义激活函数(Dice)
# 引入批量归一化和数据自适应门控机制,相比 PReLU 更适合稀疏数据分布
# ====================
class Dice(Layer):"""Dice激活函数实现(论文核心创新)论文:《Deep Interest Network for Click-Through Rate Prediction》"""def __init__(self, axis=-1, epsilon=1e-9):super(Dice, self).__init__()self.axis = axisself.epsilon = epsilon# 批归一化通过标准化每一层的输入来稳定和加速神经网络的训练过程。具体来说,它会对每个批次的数据进行标准化处理,# 使其均值接近0,方差接近1。这样可以减少内部协变量偏移(internal covariate shift),使得每一层的输入分# 布更加稳定,从而加速训练并提高模型的泛化能力self.bn = BatchNormalization(axis=axis, epsilon=epsilon)self.alpha = self.add_weight(name='dice_alpha', shape=(), initializer='zeros')def call(self, inputs):# 数据自适应归一化# 由于是全连接层,通常在最后一个轴(axis=-1)上进行归一化bn_inputs = self.bn(inputs)p = tf.sigmoid(bn_inputs)return self.alpha * (1.0 - p) * inputs + p * inputs# ====================
# 5.DIN 模型(Deep Interest Network)
# 特征处理:
# 用户行为序列使用变长mask处理
# 多值离散特征通过加权池化转为定长向量
# 用户/上下文特征直接拼接
# ====================
class DIN(Model):def __init__(self, feature_columns, behavior_feature_list, embedding_dim=8):"""注意力计算单元Args:feature_columns: 特征列配置behavior_feature_list:行为序列特征列表embedding_dim: 特征 embedding 维数,默认8维"""super().__init__()# 特征处理组件self.embed_dict = self._build_embed_layers(feature_columns, behavior_feature_list, embedding_dim)# 注意力计算单元self.attention = LocalActivationUnit(hidden_units=(64, 32))# DNN部分(使用Dice激活)self.dnn = tf.keras.Sequential([Dense(64, activation=None),Dice(),Dense(32, activation=None),Dice()])# 输出层,预估 CTR 分数self.final = Dense(1, activation='sigmoid')def _build_embed_layers(self, feature_columns, behavior_feature_list, embedding_dim):"""根据给定的特征列配置和行为序列特征列表,为每个稀疏特征创建一个嵌入层,并将其存储在一个字典中。对于行为序列特征,会显式指定序列长度。这对于处理推荐系统中的用户行为序列特别有用Args:feature_columns: 特征列配置behavior_feature_list:行为序列特征列表embedding_dim: 特征 embedding 维数,默认8维"""embed_dict = {}for feat in feature_columns:# 根据特征配置,检查当前特征是否为稀疏特征。稀疏特征通常是指那些取值范围较大但实际出现的值较少的特征,例如用户ID、商品ID等if feat['type'] == 'sparse':# 处理行为序列特征if feat['name'] in behavior_feature_list:embed_dict[feat['name']] = Embedding(feat['vocab_size'],embedding_dim,# mask_zero: 是否将输入中的0视为填充值并忽略它们,默认值为 False,对于序列特征而言,0 视为填充mask_zero=feat.get('mask_zero', False),input_length=SEQUENCE_LENGTH # 序列特征显式指定序列长度)else:embed_dict[feat['name']] = Embedding(feat['vocab_size'],embedding_dim,mask_zero=feat.get('mask_zero', False))return embed_dictdef call(self, inputs):# 用户特征user_id = inputs['user_id']user_gender = inputs['user_gender']user_occupation = inputs['user_occupation']city_code = inputs['city_code']device_type = inputs['device_type']# 候选商品特征item_id = inputs['item_id']item_category = inputs['item_category']item_brand = inputs['item_brand']# 行为序列特征,其中点击序列就是点击商品 ID 序列behavior_seq = inputs['click_sequence']behavior_category_seq = inputs['click_category_sequence']behavior_brand_seq = inputs['click_brand_sequence']seq_mask = inputs['seq_mask'] # 序列掩码print("behavior_seq 原始输入序列形状:", behavior_seq.shape) # 应为 (batch_size, 10)# 特征嵌入user_id_embed = self.embed_dict['user_id'](user_id)user_gender_embed = self.embed_dict['user_gender'](user_gender)user_occupation_embed = self.embed_dict['user_occupation'](user_occupation)city_code_embed = self.embed_dict['city_code'](city_code)device_type_embed = self.embed_dict['device_type'](device_type)item_id_embed = self.embed_dict['item_id'](item_id)item_category_embed = self.embed_dict['item_category'](item_category)item_brand_embed = self.embed_dict['item_brand'](item_brand)id_seq_embed = self.embed_dict['click_sequence'](behavior_seq)category_seq_embed = self.embed_dict['click_category_sequence'](behavior_category_seq)brand_seq_embed = self.embed_dict['click_brand_sequence'](behavior_brand_seq)print("behavior_seq 嵌入后序列形状:", id_seq_embed.shape) # 应为 (batch_size, 10, 8)# 特征拼接user_embed = tf.concat([user_id_embed,user_gender_embed,user_occupation_embed,city_code_embed,device_type_embed], axis=1)ad_embed = tf.concat([item_id_embed,item_category_embed,item_brand_embed], axis=1)# 修正为保持序列维度seq_embed = tf.concat([id_seq_embed, # (batch,10,8)category_seq_embed, # (batch,10,8)brand_seq_embed # (batch,10,8)], axis=-1) # → (batch,10,24)print("序列特征 seq_embed shape:", seq_embed.shape)# 注意力加权池化:DIN 模型的核心结构之一attention_weights = self.attention(ad_embed, seq_embed, seq_mask)behavior_embed = tf.reduce_sum(seq_embed * tf.expand_dims(attention_weights, -1), axis=1)# 特征拼接all_features = tf.concat([user_embed,behavior_embed,ad_embed], axis=1)# 深度网络预测dnn = self.dnn(all_features)return self.final(dnn)# ====================
# 6. 模型训练与评估
# ====================
# 创建输入函数
def df_to_dataset(features, labels, shuffle=True, batch_size=32):# 将序列字段转换为NumPy数组并调整形状,确保序列字段转换为三维数组(batch_size, sequence_length)click_sequence = np.stack(features['click_sequence'].values).reshape(-1, SEQUENCE_LENGTH)click_category_sequence = np.stack(features['click_category_sequence'].values).reshape(-1, SEQUENCE_LENGTH)click_brand_sequence = np.stack(features['click_brand_sequence'].values).reshape(-1, SEQUENCE_LENGTH)seq_mask = np.stack(features['seq_mask'].values).reshape(-1, SEQUENCE_LENGTH)# 查询特征print("\nclick_sequence数据:\n", click_sequence)print("\nclick_sequence形状:\n", click_sequence.shape)# 检查形状应为 (batch_size, SEQUENCE_LENGTH)assert click_sequence.shape[1] == SEQUENCE_LENGTH# 构建特征字典,直接使用处理后的数组feature_dict = {'user_id': features['user_id'].values,'user_age': features['user_age'].values,'user_gender': features['user_gender'].values,'user_occupation': features['user_occupation'].values,'city_code': features['city_code'].values,'device_type': features['device_type'].values,'click_sequence': click_sequence,'click_category_sequence': click_category_sequence,'click_brand_sequence': click_brand_sequence,'seq_mask': seq_mask,'item_id': features['item_id'].values,'item_category': features['item_category'].values,'item_brand': features['item_brand'].values,'item_price': features['item_price'].values}# 使用 TensorFlow 的 tf.data.Dataset API 创建了一个数据管道,主要用于处理特征和标签数据,并为训练模型做准备ds = tf.data.Dataset.from_tensor_slices((# 每个样本是一个元组 (features, labels)# features: 来自 feature_dict 的字典;labels: 字典 {'output_1': click_label},表示模型需要学习的标签dict(feature_dict),{'output_1': labels['click_label']}))if shuffle:# 每次从数据集中随机抽取 1000 个样本并打乱顺序ds = ds.shuffle(1000)ds = ds.batch(batch_size)return ds# 转换数据集
train_ds = df_to_dataset(train_features, train_labels)
test_ds = df_to_dataset(test_features, test_labels, shuffle=False)# 定义特征配置
feature_columns1 = [{'name': 'user_id', 'type': 'sparse', 'vocab_size': num_users + 1, 'mask_zero': True},{'name': 'user_gender', 'type': 'sparse', 'vocab_size': 2 + 1, 'mask_zero': True},{'name': 'user_occupation', 'type': 'sparse', 'vocab_size': 3 + 1, 'mask_zero': True},{'name': 'city_code', 'type': 'sparse', 'vocab_size': 2856 + 1, 'mask_zero': True},{'name': 'device_type', 'type': 'sparse', 'vocab_size': 5 + 1, 'mask_zero': True},{'name': 'item_id', 'type': 'sparse', 'vocab_size': num_items + 1, 'mask_zero': True},{'name': 'item_category', 'type': 'sparse', 'vocab_size': 3 + 1, 'mask_zero': True},{'name': 'item_brand', 'type': 'sparse', 'vocab_size': 3 + 1, 'mask_zero': True},{'name': 'click_sequence', 'type': 'sparse', 'vocab_size': num_items + 1, 'mask_zero': True},{'name': 'click_category_sequence', 'type': 'sparse', 'vocab_size': 3 + 1, 'mask_zero': True},{'name': 'click_brand_sequence', 'type': 'sparse', 'vocab_size': 3 + 1, 'mask_zero': True}
]# 初始化模型
model = DIN(feature_columns1, ['click_sequence', 'click_category_sequence', 'click_brand_sequence'])
# 编译
# loss='binary_crossentropy':使用二元交叉熵损失函数,因为 CTR 预估这是一个二分类问题
# metrics=['AUC', 'accuracy']:在训练和评估时跟踪两个指标
model.compile(optimizer='adam',loss='binary_crossentropy',metrics=['AUC', 'accuracy'])# 训练
history = model.fit(train_ds,validation_data=test_ds,epochs=10,batch_size=32,verbose=1)# 评估
test_loss, test_auc, test_acc = model.evaluate(test_ds)
print(f"\n测试集评估结果:AUC={test_auc:.3f}, 准确率={test_acc:.3f}")# ====================
# 7. 绘制训练曲线
# ====================
import matplotlib.pyplot as pltplt.plot(history.history['accuracy'], label='Train Accuracy')
# plt.plot(history.history['val_accuracy'], label='Validation Accuracy')
plt.plot(history.history['loss'], label='Loss')
plt.legend()
plt.show()# ====================
# 8. 模型保存
# ====================
model.save('DIN_Recommender')
print("模型已保存到 DIN_Recommender 目录")
1.运行过程日志
user_df:user_id user_age user_gender user_occupation city_code device_type \
0 1 56 2 1 1184 4
1 2 46 2 3 2057 3
2 3 32 1 2 1683 1
3 4 60 1 1 2256 2
4 5 25 1 1 1155 3 click_sequence \
0 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...
1 [13, 60, 93, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...
2 [32, 120, 1, 148, 18, 73, 169, 200, 27, 97, 14...
3 [93, 59, 134, 20, 120, 148, 198, 0, 0, 0, 0, 0...
4 [5, 176, 20, 104, 162, 2, 189, 0, 0, 0, 0, 0, ... click_category_sequence \
0 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...
1 [1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...
2 [2, 1, 1, 3, 2, 3, 2, 3, 1, 1, 1, 0, 0, 0, 0, ...
3 [1, 3, 1, 1, 1, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, ...
4 [2, 2, 1, 1, 1, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, ... click_brand_sequence \
0 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...
1 [1, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...
2 [1, 3, 3, 2, 3, 1, 2, 1, 1, 3, 2, 0, 0, 0, 0, ...
3 [1, 1, 2, 2, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, ...
4 [3, 2, 2, 2, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, ... seq_mask
0 [False, False, False, False, False, False, Fal...
1 [True, True, True, False, False, False, False,...
2 [True, True, True, True, True, True, True, Tru...
3 [True, True, True, True, True, True, True, Fal...
4 [True, True, True, True, True, True, True, Fal... click_sequence数据:[[ 69 148 121 ... 0 0 0][173 8 49 ... 0 0 0][105 33 64 ... 0 0 0]...[ 16 153 66 ... 0 0 0][ 0 0 0 ... 0 0 0][ 96 131 178 ... 0 0 0]]click_sequence形状:(800, 30)click_sequence数据:[[111 106 130 ... 0 0 0][ 92 69 0 ... 0 0 0][ 97 104 110 ... 0 0 0]...[146 41 198 ... 0 0 0][146 41 198 ... 0 0 0][166 62 89 ... 0 0 0]]click_sequence形状:(200, 30)
Epoch 1/10
behavior_seq 原始输入序列形状: (None, 30)
behavior_seq 嵌入后序列形状: (None, 30, 8)
序列特征 seq_embed shape: (None, 30, 24)
att_input形状: (None, 30, 96)
behavior_seq 原始输入序列形状: (None, 30)
behavior_seq 嵌入后序列形状: (None, 30, 8)
序列特征 seq_embed shape: (None, 30, 24)
att_input形状: (None, 30, 96)
2025-05-09 17:47:21.773772: W tensorflow/tsl/platform/profile_utils/cpu_utils.cc:128] Failed to get CPU frequency: 0 Hz
21/25 [========================>.....] - ETA: 0s - loss: 0.6928 - auc: 0.5349 - accuracy: 0.5357 behavior_seq 原始输入序列形状: (None, 30)
behavior_seq 嵌入后序列形状: (None, 30, 8)
序列特征 seq_embed shape: (None, 30, 24)
att_input形状: (None, 30, 96)
25/25 [==============================] - 2s 10ms/step - loss: 0.6932 - auc: 0.5131 - accuracy: 0.5213 - val_loss: 0.6937 - val_auc: 0.4397 - val_accuracy: 0.4500
Epoch 2/10
25/25 [==============================] - 0s 4ms/step - loss: 0.6872 - auc: 0.6595 - accuracy: 0.6263 - val_loss: 0.6949 - val_auc: 0.4481 - val_accuracy: 0.4400
Epoch 3/10
25/25 [==============================] - 0s 3ms/step - loss: 0.6764 - auc: 0.6927 - accuracy: 0.6600 - val_loss: 0.6968 - val_auc: 0.4493 - val_accuracy: 0.4500
Epoch 4/10
25/25 [==============================] - 0s 4ms/step - loss: 0.6460 - auc: 0.7252 - accuracy: 0.6687 - val_loss: 0.7048 - val_auc: 0.4424 - val_accuracy: 0.4900
Epoch 5/10
25/25 [==============================] - 0s 4ms/step - loss: 0.5959 - auc: 0.7633 - accuracy: 0.7025 - val_loss: 0.7183 - val_auc: 0.4490 - val_accuracy: 0.4650
Epoch 6/10
25/25 [==============================] - 0s 4ms/step - loss: 0.5533 - auc: 0.7935 - accuracy: 0.7225 - val_loss: 0.7437 - val_auc: 0.4660 - val_accuracy: 0.4350
Epoch 7/10
25/25 [==============================] - 0s 4ms/step - loss: 0.5266 - auc: 0.8156 - accuracy: 0.7362 - val_loss: 0.7378 - val_auc: 0.4779 - val_accuracy: 0.5050
Epoch 8/10
25/25 [==============================] - 0s 4ms/step - loss: 0.5036 - auc: 0.8337 - accuracy: 0.7475 - val_loss: 0.7481 - val_auc: 0.4923 - val_accuracy: 0.5050
Epoch 9/10
25/25 [==============================] - 0s 4ms/step - loss: 0.4751 - auc: 0.8558 - accuracy: 0.7713 - val_loss: 0.7804 - val_auc: 0.4859 - val_accuracy: 0.4850
Epoch 10/10
25/25 [==============================] - 0s 4ms/step - loss: 0.4384 - auc: 0.8805 - accuracy: 0.7875 - val_loss: 0.7938 - val_auc: 0.4960 - val_accuracy: 0.5150测试集评估结果:AUC=0.496, 准确率=0.515
略...
WARNING:absl:Found untraced functions such as _update_step_xla, dense_2_layer_call_fn, dense_2_layer_call_and_return_conditional_losses, dense_layer_call_fn, dense_layer_call_and_return_conditional_losses while saving (showing 5 of 11). These functions will not be directly callable after loading.
模型已保存到 DIN_Recommender 目录Process finished with exit code 0