在拿下2025腾讯广告算法大赛之前我们可以做些什么——官方数据集和 Baseline 代码解读

数据集

数据集概述

TencentGR_1k/是一个腾讯广告推荐系统的数据集,包含用户行为序列、物品特征、多模态嵌入等多种数据格式。该数据集主要用于推荐系统模型的训练和评估。

目录结构

TencentGR_1k/
├── seq.jsonl              # 用户行为序列数据
├── item_feat_dict.json    # 物品特征字典
├── indexer.pkl            # ID映射索引
├── seq_offsets.pkl        # 序列偏移指针
├── README.md              # 原始说明文档
└── creative_emb/          # 多模态嵌入特征
    ├── emb_81_32/         # 嵌入特征81,维度32
    ├── emb_82_1024/       # 嵌入特征82,维度1024
    ├── emb_83_3584/       # 嵌入特征83,维度3584
    ├── emb_84_4096/       # 嵌入特征84,维度4096
    ├── emb_85_4096/       # 嵌入特征85,维度4096
    └── emb_86_3584/       # 嵌入特征86,维度3584

核心数据文件详解

1. seq.jsonl - 用户行为序列数据

文件格式: JSON Lines格式,每行一个用户的行为序列

数据样例:

[
    # item interaction
    [480, 53961, null, {"112": 19, "117": 103, "118": 125, "119": 87, "120": 126, "100": 2, "101": 40, "102": 8559, "122": 5998, "114": 16, "116": 1, "121": 52176, "111": 5630}, 0, 1746077791], 
    [480, 50654, null, {"112": 15, "117": 285, "118": 737, "119": 1500, "120": 1071, "100": 6, "101": 22, "102": 10420, "122": 2269, "114": 16, "115": 43, "116": 13, "121": 12734, "111": 6737}, 0, 1746094091], 
    [480, 23149, null, {"112": 15, "117": 84, "118": 774, "119": 1668, "120": 348, "100": 6, "101": 32, "102": 6372, "122": 2980, "114": 16, "116": 15, "121": 30438, "111": 34195}, 0, 1746225104],
    ...
]

数据格式说明:

  • 每一行为一个用户的行为序列,按时间排序

  • 用户序列中每一个record的数据格式为:

    [user_id, item_id, user_feature, item_feature, action_type, timestamp]
    

字段详解:

  • user_id: 用户ID(已重新编号)

  • item_id: 物品ID(已重新编号)

  • user_feature: 用户特征(dict格式,key为feature_id,value为feature_value)

  • item_feature: 物品特征(dict格式,key为feature_id,value为feature_value)

  • action_type: 行为类型

    • 0 = 曝光(看到但未点击)

    • 1 = 点击(看到并点击)

  • timestamp: 时间戳

特殊说明:

  • 每一条record为记录user profileitem interaction其中的一个。

  • 若当前record为user profile,则item_iditem_featureaction_type为null

  • 若当前record为item interaction,则user_feature为null

  • 所有ID都从1开始重新编号(re-id),便于模型embedding lookup。

  • 原始值到re-id的映射参考下方的indexer.pkl文件。

2. indexer.pkl - ID映射索引

文件格式: Python pickle格式

说明: 记录了原始wuid(website-uid), creative_id和feature_value从1开始编号后的值(以下记为re-id)

用途: 记录原始ID到重新编号后的映射关系,便于模型训练时的embedding查找。

indexer['u'] # 记录了wuid到re-id, key为wuid, value为re-id
indexer['i'] # 记录了creative_id到re-id, key为creative_id, value为re-id
indexer['f'] # 记录了各feature_id中feature_value到re-id,例如:⬇️
indexer['f']['112'] # 记录了feature_id 112中的feature_value到re-id的映射

数据结构:

indexer = {
    'u': {wuid: re_id},           # 用户ID映射
    'i': {creative_id: re_id},     # 物品ID映射
    'f': {                        # 特征值映射
        '112': {feature_value: re_id},
        '113': {feature_value: re_id},
        # ... 其他特征ID
    }
}

数据样例:

# data.keys()
dict_keys(['f', 'i', 'u', 'a'])

# data['u']
{'user_01060923': 1,
 'user_00300038': 2,
 'user_00122945': 3,
 'user_00311455': 4,
 'user_00427257': 5,
 ...}

# data['i']
{20002650278: 1,
 20002695270: 2,
 20005426854: 3,
 20004710150: 4,
 20004532262: 5,
 ...}
 
# data['f'].keys()
dict_keys(['122', '102', '121', '118', '111', '119', '103', '120', '117', '115', '107', '114', '101', '108', '112', '109', '110', '116', '106', '105', '104', '100'])

# data['f']['122']
{1220085321: 1,
 1220026889: 2,
 1220093353: 3,
 1220088233: 4,
 1220061609: 5,
 ...}
 
 # data['a']
 {1: 1, 0: 2}

auto. item_feat_dict.json - 物品特征字典

文件格式: JSON格式

用途: 记录训练集中所有物品的特征,方便训练时进行负采样

数据结构:

{
    "item_re_id": {
        "feature_id": "feature_value",
        // ... 其他特征
    }
}

数据样例:

{
    "47086": {
        "112": 14,
        "117": 80,
        "118": 249,
        "119": 1174,
        "120": 118,
        "100": 6,
        "101": 18,
        "102": 6779,
        "122": 8307,
        "114": 16,
        "116": 6,
        "121": 28737,
        "111": 42338
      },
     "53920": {
        "112": 14,
        "117": 80,
        "118": 249,
        "119": 305,
        "120": 11,
        "100": 6,
        "101": 28,
        "102": 10123,
        "122": 5741,
        "114": 4,
        "116": 15,
        "121": 1485
      },
      ...
}

4. seq_offsets.pkl - 序列偏移指针

文件格式: Python pickle格式

用途: 记录seq.jsonl中每一行的起始文件指针offset,提升I/O效率,便于快速定位数据位置

数据样例:

[0, 18809, 38087, 50037, 66624, ..., 15044950, 15060592, 15079757, 15097201, 15114487]

5. creative_emb/ - 多模态嵌入特征

说明: 存储不同的多模态特征,多模态特征ID为81-86

目录结构: 包含6个不同维度的嵌入特征文件夹

嵌入特征详情:

  • emb_81_32/: 嵌入特征81,维度32

  • emb_82_1024/: 嵌入特征82,维度1024

  • emb_83_3584/: 嵌入特征83,维度3584

  • emb_84_4096/: 嵌入特征84,维度4096

  • emb_85_4096/: 嵌入特征85,维度4096

  • emb_86_3584/: 嵌入特征86,维度3584

文件格式: 每个文件夹包含多个part文件,采用Spark输出格式

数据样例:

{"anonymous_cid": 20001973219, "emb": [0.05808627977967262, -0.10434802621603012, ...]}

字段说明:

  • anonymous_cid: 匿名化的创意ID(对应item_id)

  • emb: 嵌入向量,维度根据文件夹名称确定

Baseline 代码

项目概述

这是一个基于Transformer架构的序列推荐系统baseline实现,主要用于处理用户行为序列和多模态特征数据。项目结合了注意力机制、多模态特征融合和向量量化技术,为推荐系统提供了一个完整的训练和推理框架。

项目结构

baseline/
├── main.py              # 主训练脚本
├── dataset.py           # 数据集处理模块
├── model.py             # 基础模型实现
├── model_rqxvae.py      # RQ-VAE向量量化模型
├── infer.py             # 推理脚本
└── run.sh               # 运行脚本

Pipeline

1. 数据集处理 (dataset.py)

1.1 数据加载
# 用户序列数据格式: [(user_id, item_id, user_feat, item_feat, action_type, timestamp)]
user_sequence = self._load_user_data(uid)  # 动态加载用户数据

# 扩展用户序列,插入用户token和物品token
ext_user_sequence = []
for record_tuple in user_sequence:
    u, i, user_feat, item_feat, action_type, _ = record_tuple
    if u and user_feat:
        ext_user_sequence.insert(0, (u, user_feat, 2, action_type))  # 用户token
    if i and item_feat:
        ext_user_sequence.append((i, item_feat, 1, action_type))     # 物品token
        
# 输出序列类似 [用户A, 物品1, 物品2, 物品3]
1.2 序列填充和负采样

一些概念:

处理流程: 首先对短序列进行左填充到固定长度,然后从后往前遍历用户序列,为每个位置生成对应的ID、类型和特征信息。当预测下一个物品时,会生成正样本(真实下一个物品)和负样本(随机未交互物品)用于训练。

核心变量:

  • seq/pos/neg: 存储序列ID、正样本ID、负样本ID,每个位置对应一个用户或物品的ID

  • token_type/next_token_type: 标识当前和下一个token的类型(1=物品,2=用户),用于区分序列中的用户token和物品token

  • seq_feat/pos_feat/neg_feat: 存储对应的特征字典,每个位置包含该用户或物品的所有特征信息(稀疏特征、多模态特征等)

训练目标: 模型学习预测用户序列中每个位置的下一个物品,通过对比正负样本提高推荐准确性。

数据示例:

# 假设用户序列: [用户A, 物品1, 物品2, 物品3]

seq =        [0, 用户A,    物品1,    物品2,    物品3]   # 序列ID (0为填充)
pos =        [0, 物品1,    物品2,    物品3,    0]      # 下一个正样本ID
neg =        [0, 随机物品, 随机物品, 随机物品,   0]      # 下一个负样本ID

token_type = [0, 2, 1, 1, 1]      # 1=物品, 2=用户
next_token_type = [0, 1, 1, 1, 0]                    # 下一个token类型

# 特征字典示例 (每个位置存储一个字典)
seq_feat[1] = {'103': 123, '104': 456, '106': [1,2,3]}  # 用户A的特征
seq_feat[2] = {'100': 789, '117': 321, '81': [0.1,0.2]} # 物品1的特征
pos_feat[1] = {'100': 789, '117': 321, '81': [0.1,0.2]} # 物品1特征(正样本)
neg_feat[1] = {'100': 999, '117': 888, '81': [0.3,0.4]} # 随机物品特征(负样本)
1.3 特征处理
feat_types = {
    'user_sparse': ['103', '104', '105', '109'],      # 用户稀疏特征 - 用户ID、性别、年龄等离散特征
    'item_sparse': ['100', '117', '111', '118', '101', '102', '119', '120', '114', '112', '121', '115', '122', '116'], # 物品稀疏特征 - 物品ID、类别、品牌等离散特征
    'user_array': ['106', '107', '108', '110'],       # 用户数组特征 - 用户兴趣标签、行为序列等数组特征
    'item_array': [],                                  # 物品数组特征 - 物品标签、属性列表等数组特征
    'item_emb': self.mm_emb_ids,                      # 物品多模态特征 - 物品内容的预训练embedding向量 (默认['81'], 可选['81', '82', '83', '84', '85', '86'])
    'user_continual': [],                              # 用户连续特征 - 用户活跃度、注册时长等数值特征
    'item_continual': []                               # 物品连续特征 - 物品热度、价格等数值特征
}

特征类型说明:

  • 稀疏特征(sparse): 离散的分类特征,如ID、性别、类别等,需要embedding编码

  • 数组特征(array): 多值的列表特征,如兴趣标签、属性列表等,需要特殊处理

  • 多模态特征(emb): 预训练的embedding向量,如用户画像、物品内容等,直接使用

  • 连续特征(continual): 数值型特征,如活跃度、价格等,直接作为数值使用

2. 模型架构 (model.py)

2.1 模型初始化
class BaselineModel(torch.nn.Module):
    def __init__(self, user_num, item_num, feat_statistics, feat_types, args):
        # 核心组件:
        self.item_emb = nn.Embedding(item_num + 1, hidden_units)     # [item_num+1, hidden_units] - 物品ID的embedding表,将物品ID映射为向量
        self.user_emb = nn.Embedding(user_num + 1, hidden_units)     # [user_num+1, hidden_units] - 用户ID的embedding表,将用户ID映射为向量
        self.pos_emb = nn.Embedding(maxlen + 1, hidden_units)        # [102, hidden_units] - 位置编码,为序列中的每个位置生成位置向量
        self.sparse_emb = {}  # 稀疏特征embedding表 - 存储所有稀疏特征(如性别、年龄、类别等)的embedding层
        self.emb_transform = {}  # 多模态特征线性变换 - 将预训练的多模态embedding转换为模型所需的维度
2.2 特征转Embedding(feat2emb函数)

核心功能: 将用户和物品的各种特征统一转换为向量表示。

关键函数:

def feat2emb(self, seq, feature_array, mask=None, include_user=False):
    """
    将特征转换为embedding
    seq中可能同时包含用户ID和物品ID,feat2emb会根据mask对用户ID和物品ID进行分别处理,这部分最好看具体代码
    
    Args:
        seq: [batch_size, seq_len] - 序列ID (用户ID或物品ID)
        feature_array: [batch_size, seq_len] - 特征数组 (每个位置是一个特征字典)
        mask: [batch_size, seq_len] - token类型掩码(1=item, 2=user)
    
    Returns:
        embs: [batch_size, seq_len, hidden_units] - 特征embedding
    """

关键变量详解:

  • seq: 序列ID,存储用户ID或物品ID,用于获取基础embedding

  • feature_array: 特征字典数组,每个位置包含该用户/物品的所有特征信息

    • 例如: {'103': 123, '104': 456, '81': [0.1,0.2]} (用户ID、性别、多模态特征等)
  • mask: token类型掩码,1=物品token,2=用户token,用于区分处理方式

  • include_user: 是否包含用户特征,推理时设为False

特征类型详解:

  • 稀疏特征: 如用户ID(103)、性别(104)、物品类别(100)等离散特征

  • 数组特征: 如用户兴趣标签(106)、物品属性列表等数组特征

  • 多模态特征: 如物品内容embedding(81)等预训练向量特征

  • 连续特征: 如用户活跃度、物品热度等数值特征

2.3 序列编码(log2feats函数)

核心功能: 将用户序列转换为统一的特征表示,通过Transformer捕获序列中的时序依赖关系。

编码过程说明:

  • 特征转换: 调用 2.2 特征转Embeddingfeat2emb函数将序列中的每个token(用户ID或物品ID)及其特征转换为向量表示

  • 位置编码: 为序列中的每个位置添加位置信息,帮助模型理解token在序列中的位置

  • Transformer编码: 通过多层Transformer结构捕获序列中的长距离依赖关系,学习用户行为模式

  • 输出: 返回每个位置的最终特征表示,用于后续的推荐任务

编码流程详解:

def log2feats(self, log_seqs, mask, seq_feature):
    """
    将用户序列编码为特征表示
    
    Args:
        log_seqs: [batch_size, seq_len] - 用户序列ID (用户ID和物品ID的混合序列)
        mask: [batch_size, seq_len] - token类型掩码(1=item, 2=user)
        seq_feature: [batch_size, seq_len] - 序列特征 (每个位置的特征字典)
    
    Returns:
        log_feats: [batch_size, seq_len, hidden_units] - 序列特征表示
    """
    # 1. 特征转embedding: 将序列中的每个token转换为向量表示
    seqs = self.feat2emb(log_seqs, seq_feature, mask)  # [batch_size, seq_len, hidden_units]
    
    # 2. 位置编码: 为序列中的每个位置添加位置信息
    positions = torch.arange(log_seqs.size(1), device=self.dev)  # [seq_len]
    seqs += self.pos_emb(positions)  # 添加位置编码
    
    # 3. Transformer编码: 通过多层Transformer捕获序列依赖关系
    for i in range(len(self.attention_layers)):
        # Multi-head attention: 计算注意力权重
        mha_outputs, _ = self.attention_layers[i](seqs, seqs, seqs, attn_mask=attention_mask)
        # Layer normalization + residual connection
        seqs = self.attention_layernorms[i](seqs + mha_outputs)
        # Feed forward + Layer normalization + residual connection
        seqs = self.forward_layernorms[i](seqs + self.forward_layers[i](seqs))
    
    # 4. 最终输出: 经过所有Transformer层后的序列表示
    return self.last_layernorm(seqs)

3. 训练过程 (main.py)

3.1 训练循环
for epoch in range(epoch_start_idx, args.num_epochs + 1):
    for step, batch in tqdm(enumerate(train_loader)):
        # batch shapes:
        seq = seq.to(args.device)           # [batch_size, maxlen+1]
        pos = pos.to(args.device)           # [batch_size, maxlen+1] 
        neg = neg.to(args.device)           # [batch_size, maxlen+1]
        token_type = token_type             # [batch_size, maxlen+1]
        next_token_type = next_token_type   # [batch_size, maxlen+1]
        seq_feat = seq_feat                 # [batch_size, maxlen+1]
        pos_feat = pos_feat                 # [batch_size, maxlen+1]
        neg_feat = neg_feat                 # [batch_size, maxlen+1]
        
        # 前向传播
        pos_logits, neg_logits = model(seq, pos, neg, token_type, next_token_type, 
                                     next_action_type, seq_feat, pos_feat, neg_feat)
        # pos_logits: [batch_size, maxlen+1] - 正样本logits
        # neg_logits: [batch_size, maxlen+1] - 负样本logits
        
        # 计算损失 (只对item token计算)
        indices = np.where(next_token_type == 1)
        loss = bce_criterion(pos_logits[indices], pos_labels[indices])
        loss += bce_criterion(neg_logits[indices], neg_labels[indices])
3.2 模型前向传播(forward
def forward(self, user_item, pos_seqs, neg_seqs, mask, next_mask, next_action_type, 
           seq_feature, pos_feature, neg_feature):
    """
    训练时前向传播
    
    Returns:
        pos_logits: [batch_size, maxlen] - 正样本logits
        neg_logits: [batch_size, maxlen] - 负样本logits
    """
    # 1. 序列编码
    log_feats = self.log2feats(user_item, mask, seq_feature)  # [batch_size, seq_len, hidden_units]
    
    # 2. 正负样本embedding
    pos_embs = self.feat2emb(pos_seqs, pos_feature, include_user=False)  # [batch_size, seq_len, hidden_units]
    neg_embs = self.feat2emb(neg_seqs, neg_feature, include_user=False)  # [batch_size, seq_len, hidden_units]
    
    # 3. 计算相似度
    pos_logits = (log_feats * pos_embs).sum(dim=-1)  # [batch_size, seq_len]
    neg_logits = (log_feats * neg_embs).sum(dim=-1)  # [batch_size, seq_len]
    
    # 4. 只对item token计算损失
    loss_mask = (next_mask == 1).to(self.dev)
    pos_logits = pos_logits * loss_mask
    neg_logits = neg_logits * loss_mask

4. 推理过程 (infer.py)

4.1 用户表征生成(predict
def predict(self, log_seqs, seq_feature, mask):
    """
    生成用户序列表征
    
    Args:
        log_seqs: [batch_size, seq_len] - 用户序列
        seq_feature: [batch_size, seq_len] - 序列特征
        mask: [batch_size, seq_len] - token类型掩码
    
    Returns:
        final_feat: [batch_size, hidden_units] - 用户表征
    """
    log_feats = self.log2feats(log_seqs, mask, seq_feature)  # [batch_size, seq_len, hidden_units]
    final_feat = log_feats[:, -1, :]  # 取最后一个token作为用户表征
    return final_feat
4.2 候选物品Embedding生成(get_candidate_emb, save_item_emb, process_cold_start_feat函数)

核心功能: 为推荐系统中的所有候选物品生成embedding向量,用于后续的向量检索和相似度计算。

作用说明:

  • 离线预计算: 在推理阶段之前,预先为所有候选物品生成embedding,避免在线计算延迟

  • 向量检索: 生成的embedding用于ANN(近似最近邻)检索,快速找到与用户query最相似的物品

  • 存储优化: 将embedding保存到文件,支持大规模物品库的高效检索

候选库生成函数:

def get_candidate_emb(indexer, feat_types, feat_default_value, mm_emb_dict, model):
    """
    生成候选库item的id和embedding
    """
    # 1. 读取候选库数据文件
    candidate_path = Path(os.environ.get('EVAL_DATA_PATH'), 'predict_set.jsonl')
    
    # 2. 处理每个候选物品的特征
    for line in f:
        feature = line['features']
        creative_id = line['creative_id']
        retrieval_id = line['retrieval_id']
        
        # 3. 处理冷启动特征和缺失特征
        feature = process_cold_start_feat(feature)
        for feat_id in missing_fields:
            feature[feat_id] = feat_default_value[feat_id]
        
        # 4. 处理多模态特征
        for feat_id in feat_types['item_emb']:
            if creative_id in mm_emb_dict[feat_id]:
                feature[feat_id] = mm_emb_dict[feat_id][creative_id]
            else:
                feature[feat_id] = np.zeros(EMB_SHAPE_DICT[feat_id])
    
    # 5. 调用模型生成embedding
    model.save_item_emb(item_ids, retrieval_ids, features, save_path)
    
    return retrieve_id2creative_id

冷启动特征处理:

def process_cold_start_feat(feature):
    """
    处理冷启动特征。推理时遇到训练集未出现过的特征值,将字符串转换为0。
    
    Args:
        feature: dict - 物品特征字典,格式如 {'100': 123, '117': 'new_brand', '81': [0.1,0.2]}
                  其中key是特征ID,对应feat_types中的特征类型
    """
    processed_feat = {}
    for feat_id, feat_value in feature.items():
        if type(feat_value) == list:
            value_list = []
            for v in feat_value:
                if type(v) == str:
                    value_list.append(0)  # 字符串特征转换为0
                else:
                    value_list.append(v)
            processed_feat[feat_id] = value_list
        elif type(feat_value) == str:
            processed_feat[feat_id] = 0  # 字符串特征转换为0
        else:
            processed_feat[feat_id] = feat_value  # 数值特征保持不变
    return processed_feat

冷启动处理说明:

  • feature含义: 每个物品的特征字典,key是特征ID(如'100', '117', '81'),对应feat_types中定义的特征类型

  • 特征类型对应关系:

    • '100'item_sparse (物品ID、类别等离散特征)

    • '117'item_sparse (品牌等离散特征)

    • '81'item_emb (多模态embedding特征)

  • 问题: 推理时遇到训练集未出现过的特征值(如新品牌'new_brand')

  • 解决方案: 将字符串特征值统一转换为0,数值特征保持不变

  • 应用: 在候选库特征处理阶段自动调用,确保模型输入的一致性

物品Embedding生成函数:

def save_item_emb(self, item_ids, retrieval_ids, feat_dict, save_path, batch_size=1024):
    """
    生成候选库item embedding用于检索
    
    Args:
        item_ids: [num_items] - 候选item ID列表,包含所有需要生成embedding的物品ID
        retrieval_ids: [num_items] - 检索ID列表,用于映射物品ID到检索ID
        feat_dict: [num_items] - item特征字典,每个物品的所有特征信息,包含每个物品的稀疏特征、多模态特征等
        save_path: str - 保存embedding的文件路径
        batch_size: int - 批处理大小,用于内存优化
    
    Returns:
        无返回值,直接保存embedding到文件 save_emb(batch_emb, f"{save_path}/batch_{start_idx}.fbin")
    """

注意: retrieval_ids 是测试集json中的一个参数,可能是从0开始连续编号,便于高效的向量检索

4.3 检索流程
def infer():
    # 1. 基于用户行为序列生成用户表征向量
    for step, batch in enumerate(test_loader):
        seq, token_type, seq_feat, user_id = batch
        logits = model.predict(seq, seq_feat, token_type)  # [batch_size, hidden_units]
        all_embs.append(logits.detach().cpu().numpy())
    
    # 2. 生成候选item embedding
    retrieve_id2creative_id = get_candidate_emb(...)
    
    # 3. 保存文件用于ANN检索
    save_emb(all_embs, 'query.fbin')      # 用户行为序列表征向量
    save_emb(candidate_embs, 'embedding.fbin')  # 候选库embedding
    save_emb(candidate_ids, 'id.u64bin')   # 候选库ID
    
    # 4. 执行ANN检索
    ann_cmd = "faiss_demo --query_vector_file_path=query.fbin --dataset_vector_file_path=embedding.fbin ..."
    os.system(ann_cmd)
    
    # 5. 读取检索结果
    top10s_retrieved = read_result_ids("id100.u64bin")  # [num_users, 10]

5. RQ-VAE模型架构 (model_rqvae.py)

参考资料:https://zhuanlan.zhihu.com/p/716658479

5.1 核心组件

K-means聚类:

def kmeans(data, n_clusters, kmeans_iters):
    """
    标准K-means聚类算法
    
    Args:
        data: [num_samples, num_features] - 输入数据
        n_clusters: 聚类数量
        kmeans_iters: 最大迭代次数
    
    Returns:
        cluster_centers: [n_clusters, num_features] - 聚类中心
        labels: [num_samples] - 聚类标签
    """

平衡K-means:

class BalancedKmeans(torch.nn.Module):
    """
    平衡K-means聚类,确保每个聚类大小相对平衡
    
    Args:
        num_clusters: 聚类数量
        kmeans_iters: 最大迭代次数
        tolerance: 收敛容差
        device: 计算设备
    """
5.2 编码器-解码器架构(代码中Encoder和Decoder只是简单的神经网络实现)

RQ编码器:

class RQEncoder(torch.nn.Module):
    """
    RQ-VAE编码器
    
    Args:
        input_dim: 输入维度 (4096)
        hidden_channels: 隐藏层通道数列表
        latent_dim: 潜在空间维度
    """
    def forward(self, x):
        # x: [batch_size, input_dim] - 输入多模态embedding
        # 返回: [batch_size, latent_dim] - 编码后的潜在表示

RQ解码器:

class RQDecoder(torch.nn.Module):
    """
    RQ-VAE解码器
    
    Args:
        latent_dim: 潜在空间维度
        hidden_channels: 隐藏层通道数列表
        output_dim: 输出维度 (4096)
    """
    def forward(self, x):
        # x: [batch_size, latent_dim] - 潜在表示
        # 返回: [batch_size, output_dim] - 重构的多模态embedding
5.3 向量量化嵌入

VQ嵌入层:

## Generate semantic id
class VQEmbedding(torch.nn.Embedding):
    """
    向量量化嵌入层:将连续向量量化为离散的语义ID
    
    功能:创建codebook → 计算距离 → 生成语义ID → 返回量化嵌入
    """
    def __init__(
        self,
        num_clusters,           # 聚类数量(codebook大小)
        codebook_emb_dim: int,  # codebook嵌入维度
        kmeans_method: str,     # kmeans初始化方法
        kmeans_iters: int,      # kmeans迭代次数
        distances_method: str,   # 距离计算方法
        device: str,            # 设备
    ):
        super(VQEmbedding, self).__init__(num_clusters, codebook_emb_dim)

        # 保存参数
        self.num_clusters = num_clusters
        self.codebook_emb_dim = codebook_emb_dim
        self.kmeans_method = kmeans_method
        self.kmeans_iters = kmeans_iters
        self.distances_method = distances_method
        self.device = device

    def _create_codebook(self, data):
        """
        创建codebook:根据数据初始化量化字典
        
        支持三种初始化方式:
        - kmeans: 标准kmeans聚类
        - bkmeans: 平衡kmeans聚类
        - random: 随机初始化
        """
        if self.kmeans_method == 'kmeans':
            # 标准kmeans聚类
            _codebook, _ = kmeans(data, self.num_clusters, self.kmeans_iters)
        elif self.kmeans_method == 'bkmeans':
            # 平衡kmeans聚类(确保每个聚类大小相近)
            BKmeans = BalancedKmeans(
                num_clusters=self.num_clusters, kmeans_iters=self.kmeans_iters, tolerance=1e-4, device=self.device
            )
            _codebook, _ = BKmeans.fit(data)
        else:
            # 随机初始化
            _codebook = torch.randn(self.num_clusters, self.codebook_emb_dim)
        
        # 移动到指定设备并验证形状
        _codebook = _codebook.to(self.device)
        assert _codebook.shape == (self.num_clusters, self.codebook_emb_dim)
        self.codebook = torch.nn.Parameter(_codebook)

    @torch.no_grad()
    def _compute_distances(self, data):
        """
        计算数据与codebook的距离
        
        支持两种距离计算:
        - cosine: 余弦距离
        - l2: 欧几里得距离
        """
        _codebook_t = self.codebook.t()  # 转置codebook
        assert _codebook_t.shape == (self.codebook_emb_dim, self.num_clusters)
        assert data.shape[-1] == self.codebook_emb_dim

        if self.distances_method == 'cosine':
            # 余弦距离:1 - cos(θ)
            data_norm = F.normalize(data, p=2, dim=-1)  # 数据归一化
            _codebook_t_norm = F.normalize(_codebook_t, p=2, dim=0)  # codebook归一化
            distances = 1 - torch.mm(data_norm, _codebook_t_norm)  # 计算余弦距离
        else:
            # L2距离:||x - c||² = ||x||² + ||c||² - 2<x,c>
            data_norm_sq = data.pow(2).sum(dim=-1, keepdim=True)  # ||x||²
            _codebook_t_norm_sq = _codebook_t.pow(2).sum(dim=0, keepdim=True)  # ||c||²
            distances = torch.addmm(data_norm_sq + _codebook_t_norm_sq, data, _codebook_t, beta=1.0, alpha=-2.0)
        return distances

    @torch.no_grad()
    def _create_semantic_id(self, data):
        """
        生成语义ID:找到最近的codebook向量
        
        Returns:
            _semantic_id: 每个样本对应的codebook索引
        """
        distances = self._compute_distances(data)  # 计算距离
        _semantic_id = torch.argmin(distances, dim=-1)  # 找到最小距离的索引
        return _semantic_id

    def _update_emb(self, _semantic_id):
        """
        根据语义ID获取对应的量化嵌入
        
        Args:
            _semantic_id: 语义ID
            
        Returns:
            update_emb: 对应的量化嵌入
        """
        update_emb = super().forward(_semantic_id)  # 使用父类的embedding查找
        return update_emb

    def forward(self, data):
        """
        前向传播:完整的量化流程
        
        Args:
            data: 输入数据
            
        Returns:
            update_emb: 量化后的嵌入
            _semantic_id: 语义ID
        """
        self._create_codebook(data)  # 创建codebook
        _semantic_id = self._create_semantic_id(data)  # 生成语义ID
        update_emb = self._update_emb(_semantic_id)  # 获取量化嵌入

        return update_emb, _semantic_id
5.4 残差量化器

RQ模块:

## Residual Quantizer
class RQ(torch.nn.Module):
    """
    残差量化器:通过多个量化步骤逐步减少输入数据的残差
    
    Args:
        num_codebooks: 量化器数量
        codebook_size: 每个codebook的大小列表
        codebook_emb_dim: codebook嵌入维度
        shared_codebook: 是否共享codebook
        kmeans_method: kmeans初始化方法
        kmeans_iters: kmeans迭代次数
        distances_method: 距离计算方法
        loss_beta: RQ-VAE损失权重
        device: 设备
    """

    def __init__(
        self,
        num_codebooks: int,
        codebook_size: list,
        codebook_emb_dim,
        shared_codebook: bool,
        kmeans_method,
        kmeans_iters,
        distances_method,
        loss_beta: float,
        device: str,
    ):
        super().__init__()
        # 基本参数设置
        self.num_codebooks = num_codebooks
        self.codebook_size = codebook_size
        assert len(self.codebook_size) == self.num_codebooks  # 确保codebook数量匹配
        self.codebook_emb_dim = codebook_emb_dim
        self.shared_codebook = shared_codebook

        # 量化相关参数
        self.kmeans_method = kmeans_method
        self.kmeans_iters = kmeans_iters
        self.distances_method = distances_method
        self.loss_beta = loss_beta
        self.device = device

        # 创建量化模块列表
        if self.shared_codebook:
            # 共享codebook:所有量化器使用相同的codebook大小
            self.vqmodules = torch.nn.ModuleList(
                [
                    VQEmbedding(
                        self.codebook_size[0],  # 使用第一个codebook大小
                        self.codebook_emb_dim,
                        self.kmeans_method,
                        self.kmeans_iters,
                        self.distances_method,
                        self.device,
                    )
                    for _ in range(self.num_codebooks)
                ]
            )
        else:
            # 独立codebook:每个量化器使用不同的codebook大小
            self.vqmodules = torch.nn.ModuleList(
                [
                    VQEmbedding(
                        self.codebook_size[idx],  # 使用对应索引的codebook大小
                        self.codebook_emb_dim,
                        self.kmeans_method,
                        self.kmeans_iters,
                        self.distances_method,
                        self.device,
                    )
                    for idx in range(self.num_codebooks)
                ]
            )

    def quantize(self, data):
        """
        关键函数:执行残差量化过程
        
        量化流程:
        - 第i步量化:输入[i] = VQ[i] + 残差[i]
        - vq_emb_list: [vq1, vq1+vq2, vq1+vq2+vq3, ...] 累积量化结果
        - res_emb_list: [res1, res2, res3, ...] 每步残差
        - semantic_id_list: [vq1_sid, vq2_sid, vq3_sid, ...] 每步语义ID

        Returns:
            vq_emb_list: 累积的量化嵌入列表
            res_emb_list: 残差嵌入列表  
            semantic_id_list: 语义ID张量 [batch_size, num_codebooks]
        """
        # 初始化残差为输入数据的副本
        res_emb = data.detach().clone()

        vq_emb_list, res_emb_list = [], []
        semantic_id_list = []
        vq_emb_aggre = torch.zeros_like(data)  # 累积量化结果

        # 逐步量化
        for i in range(self.num_codebooks):
            # 使用第i个量化器量化当前残差
            vq_emb, _semantic_id = self.vqmodules[i](res_emb)

            # 更新残差:残差 = 残差 - 量化结果
            res_emb -= vq_emb
            # 累积量化结果
            vq_emb_aggre += vq_emb

            # 保存结果
            res_emb_list.append(res_emb)
            vq_emb_list.append(vq_emb_aggre)
            semantic_id_list.append(_semantic_id.unsqueeze(dim=-1))

        # 将所有语义ID拼接成张量
        semantic_id_list = torch.cat(semantic_id_list, dim=-1)
        return vq_emb_list, res_emb_list, semantic_id_list

    def _rqvae_loss(self, vq_emb_list, res_emb_list):
        """
        计算RQ-VAE损失
        
        RQ-VAE损失包含两部分:
        - loss1: 量化结果与残差的匹配损失(停止残差梯度)
        - loss2: 残差与量化结果的匹配损失(停止量化梯度)
        """
        rqvae_loss_list = []
        for idx, quant in enumerate(vq_emb_list):
            # 计算两个方向的匹配损失
            loss1 = (res_emb_list[idx].detach() - quant).pow(2.0).mean()  # 残差梯度停止
            loss2 = (res_emb_list[idx] - quant.detach()).pow(2.0).mean()  # 量化梯度停止
            partial_loss = loss1 + self.loss_beta * loss2
            rqvae_loss_list.append(partial_loss)

        # 所有步骤损失求和
        rqvae_loss = torch.sum(torch.stack(rqvae_loss_list))
        return rqvae_loss

    def forward(self, data):
        """
        前向传播:执行量化并计算损失
        
        Returns:
            vq_emb_list: 累积量化嵌入
            semantic_id_list: 语义ID
            rqvae_loss: RQ-VAE损失
        """
        # 执行量化
        vq_emb_list, res_emb_list, semantic_id_list = self.quantize(data)
        # 计算损失
        rqvae_loss = self._rqvae_loss(vq_emb_list, res_emb_list)

        return vq_emb_list, semantic_id_list, rqvae_loss
5.5 完整RQ-VAE模型

RQVAE类:

class RQVAE(torch.nn.Module):
    """
    RQ-VAE模型:结合编码器、残差量化器和解码器的完整模型
    
    工作流程:输入 → 编码 → 残差量化 → 解码 → 重构输出
    """
    def __init__(
        self,
        input_dim: int,          # 输入维度
        hidden_channels: list,    # 隐藏层通道数列表
        latent_dim: int,         # 潜在空间维度
        num_codebooks: int,      # 量化器数量
        codebook_size: list,     # 每个codebook的大小
        shared_codebook: bool,   # 是否共享codebook
        kmeans_method,           # kmeans方法
        kmeans_iters,           # kmeans迭代次数
        distances_method,        # 距离计算方法
        loss_beta: float,       # RQ-VAE损失权重
        device: str,            # 设备
    ):
        super().__init__()
        # 编码器:将输入映射到潜在空间
        self.encoder = RQEncoder(input_dim, hidden_channels, latent_dim).to(device)
        # 解码器:将潜在表示重构回原始空间(隐藏层顺序反转)
        self.decoder = RQDecoder(latent_dim, hidden_channels[::-1], input_dim).to(device)
        # 残差量化器:在潜在空间进行量化
        self.rq = RQ(
            num_codebooks,
            codebook_size,
            latent_dim,
            shared_codebook,
            kmeans_method,
            kmeans_iters,
            distances_method,
            loss_beta,
            device,
        ).to(device)

    def encode(self, x):
        """编码:输入 → 潜在表示"""
        return self.encoder(x)

    def decode(self, z_vq):
        """解码:潜在表示 → 重构输出"""
        # 如果输入是列表(量化结果列表),使用最后一个(累积结果)
        if isinstance(z_vq, list):
            z_vq = z_vq[-1]
        return self.decoder(z_vq)

    def compute_loss(self, x_hat, x_gt, rqvae_loss):
        """
        计算总损失
        
        Args:
            x_hat: 重构输出
            x_gt: 原始输入
            rqvae_loss: RQ-VAE损失
            
        Returns:
            recon_loss: 重构损失
            rqvae_loss: RQ-VAE损失  
            total_loss: 总损失
        """
        # 重构损失:重构输出与原始输入的MSE
        recon_loss = F.mse_loss(x_hat, x_gt, reduction="mean")
        # 总损失:重构损失 + RQ-VAE损失
        total_loss = recon_loss + rqvae_loss
        return recon_loss, rqvae_loss, total_loss

    def _get_codebook(self, x_gt):
        """
        获取语义ID(用于推理)
        
        Returns:
            semantic_id_list: 语义ID列表
        """
        z_e = self.encode(x_gt)  # 编码
        vq_emb_list, semantic_id_list, rqvae_loss = self.rq(z_e)  # 量化
        return semantic_id_list  # 只返回语义ID

    def forward(self, x_gt):
        """
        前向传播:完整的RQ-VAE流程
        
        Returns:
            x_hat: 重构输出
            semantic_id_list: 语义ID
            recon_loss: 重构损失
            rqvae_loss: RQ-VAE损失
            total_loss: 总损失
        """
        # 1. 编码:输入 → 潜在表示
        z_e = self.encode(x_gt)
        # 2. 量化:潜在表示 → 量化结果 + 语义ID
        vq_emb_list, semantic_id_list, rqvae_loss = self.rq(z_e)
        # 3. 解码:量化结果 → 重构输出
        x_hat = self.decode(vq_emb_list)
        # 4. 计算损失
        recon_loss, rqvae_loss, total_loss = self.compute_loss(x_hat, x_gt, rqvae_loss)
        return x_hat, semantic_id_list, recon_loss, rqvae_loss, total_loss

6. 关键变量含义

6.1 数据相关
  • seq: 用户交互序列ID,包含用户token和物品token

  • token_type: token类型,1=物品token,2=用户token

  • next_token_type: 下一个token类型

  • next_action_type: 动作类型,0=曝光,1=点击

  • seq_feat/pos_feat/neg_feat: 特征字典,key为特征ID,value为特征值

6.2 模型相关
  • hidden_units: 隐藏层维度,默认32

  • maxlen: 序列最大长度,默认101

  • num_heads: 注意力头数,默认1

  • num_blocks: Transformer层数,默认1

6.3 特征相关
  • sparse_emb: 稀疏特征embedding表

  • emb_transform: 多模态特征线性变换层

  • feat_statistics: 特征统计信息,包含各特征的数量

  • feat_types: 特征类型分类,分为sparse/array/emb/continual

posted @ 2025-12-02 00:23  Orzjh  阅读(0)  评论(0)    收藏  举报