从"特征索引"到"特征向量"的数据流
整个数据处理流程分为离线训练和在线服务两个阶段,下图清晰地展示了从原始请求到模型最终输出的完整过程:
flowchart LR
A["线上请求
(原始特征字典)"] --> B["在线特征拼接服务
Feature Serving"] subgraph C [离线训练与模型定义] C1["训练数据
(包含特征索引)"] --> C2["模型定义
(包含Embedding层)"] end B --> D["模型预测服务
Model Serving"] C2 -- "模型加载" --> D subgraph E [模型内部Forward过程] D --> F["接收: 特征索引/值
(如 user_id=12345)"] F --> G["Embedding层查表
(转换索引为向量)"] G --> H["拼接、交叉等深度计算"] H --> I["输出: 点击概率"] end I --> J[返回推荐结果]
(原始特征字典)"] --> B["在线特征拼接服务
Feature Serving"] subgraph C [离线训练与模型定义] C1["训练数据
(包含特征索引)"] --> C2["模型定义
(包含Embedding层)"] end B --> D["模型预测服务
Model Serving"] C2 -- "模型加载" --> D subgraph E [模型内部Forward过程] D --> F["接收: 特征索引/值
(如 user_id=12345)"] F --> G["Embedding层查表
(转换索引为向量)"] G --> H["拼接、交叉等深度计算"] H --> I["输出: 点击概率"] end I --> J[返回推荐结果]
下面我们拆解上图中的关键环节:
-
离线训练阶段:准备索引和映射
- 原始日志(如
用户ID=12345,物品ID=678,城市=北京)会被处理成训练样本。 - 每个类别特征(称为一个
field或slot)会建立一张从原始值到连续整数索引的映射表(称为vocabulary或feat_map)。 - 训练时 ,
forward函数接收的输入是一个形如[batch_size, num_fields]的张量,其中每个值都是对应特征的整数索引。模型内部的nn.Embedding层会根据这些索引,查找对应的向量权重。
- 原始日志(如
-
在线服务阶段:从请求到预测
- 当用户刷新信息流时,推荐引擎(如Rank服务)会收到一个包含大量原始特征的请求。
- 特征服务 会将这些原始特征(字符串或数字)实时转换为对应的整数索引。
- 然后,这个由索引构成的数组,被送入部署好的模型进行
forward计算。
1. 一个简化的代码示例对比
python
# 1. 模型定义(包含Embedding层)
class RankingModel(nn.Module):
def __init__(self, vocab_sizes):
super().__init__()
# 为每个特征域定义一个嵌入层
self.user_emb = nn.Embedding(vocab_sizes['user'], 16) # 假设用户ID有10万个,嵌入为16维
self.item_emb = nn.Embedding(vocab_sizes['item'], 16)
# ... 其他层(如全连接层)
def forward(self, inputs):
# inputs 是一个特征索引的字典
user_id_idx = inputs['user_id'] # 形状: [batch_size, 1]
item_id_idx = inputs['item_id'] # 形状: [batch_size, 1]
# ★ 关键步骤:在forward内部,将索引转换为向量
user_emb_vec = self.user_emb(user_id_idx) # 形状变为: [batch_size, 1, 16]
item_emb_vec = self.item_emb(item_id_idx)
# 将向量展平并拼接,输入后续网络
concat_feat = torch.cat([user_emb_vec.squeeze(), item_emb_vec.squeeze()], dim=1)
# ... 后续计算
return prediction
# 2. 在线服务时的调用(伪代码)
# 假设一次请求,特征服务返回的原始特征已转为索引:
raw_request = {'user_id': 12345, 'item_id': 678, 'city': 1}
# 预处理为模型输入格式
model_input = {
'user_id': torch.tensor([raw_request['user_id']]),
'item_id': torch.tensor([raw_request['item_id']]),
# ...
}
# 调用模型
output = model_serving_instance.forward(model_input)
2. 为什么这样设计?关键原因
- 效率与解耦 :模型只关心"计算",不关心"特征工程"。特征的处理(缺失值填充、归一化、哈希、索引化)由上游的特征平台统一负责,保证线上线下一致。
- 灵活性 :当需要新增一个特征时,只需在特征平台配置,并在模型定义中添加一个新的
Embedding层或数值处理层即可,无需改动模型核心架构。 - 性能 :直接传输整数索引比传输高维浮点向量网络开销小得多。模型在GPU/CPU上通过查表(Embedding)将索引转为向量,这个操作高度优化,效率极高。
- 一致性 :这是最重要的原因。必须保证训练 和推理时,特征进入模型的方式完全一致。使用同一套索引映射和模型权重,是保证效果不衰减的基石。
这是一个非常关键的问题。多值和序列特征(例如用户的兴趣标签列表、历史行为序列)是信息流推荐系统的核心,它们的处理方式与单值特征有显著不同。
核心原则依然是:forward函数接收的是经过映射的索引,而不是原始字符串或向量 。但针对多值和序列,其索引的组织形式 和模型内部的后续处理有专门的设计。
3. 两类特征的处理流程对比
| 特征类型 | 示例 | 特征处理后的索引形式 (输入forward前) | 模型内部 (forward函数内)的关键操作 |
|---|---|---|---|
| 多值特征 | 用户兴趣标签 ["体育", "科技"] |
Multi-hot 向量 或 索引列表 如 [0, 0, 1, 0, 1] (长度=总标签数) 或 [23, 45] |
通过一个共享的Embedding层 ,将每个激活的索引转换为向量,然后进行聚合(求和、平均等)。 |
| 序列特征 | 用户点击历史 [item_id_101, item_id_302, ...] |
定长/变长的索引数组 如 [101, 302, 0, 0] (填充后) 或 [101, 302] (带长度信息) |
1. 嵌入查找 :将每个索引变为向量,得到向量序列。 2. 序列建模 :通过Pooling、RNN、Transformer 等结构,将序列聚合成一个固定长度的兴趣表示向量。 |
4. 具体实现与forward输入示例
结合单值、多值、序列特征,一个真实的信息流排序模型在训练/推理时,forward函数接收的输入字典大致如下:
python
{
# --- 单值特征 (直接索引) ---
"user_id": torch.tensor([12894]), # 用户ID索引
"item_id": torch.tensor([356]), # 候选物品ID索引
"city_id": torch.tensor([12]), # 城市索引
# --- 多值特征 (以用户兴趣标签为例) ---
# 形式1: Multi-hot稀疏向量 (知道总类别数时)
"user_tags": torch.tensor([[0, 1, 0, 1, 0, 1]]), # 形状: [batch_size, total_num_tags]
# 形式2: 变长索引列表 + 长度 (更灵活)
"user_tag_list": torch.tensor([23, 45, 67]), # 所有标签索引展平
"user_tag_len": torch.tensor([3]), # 该样本的实际标签数量
# --- 序列特征 (以用户最近点击的10个物品ID为例) ---
# 被处理成固定长度的索引序列,未满部分用0填充
"hist_item_seq": torch.tensor([[101, 302, 454, 0, 0, ..., 0]]), # 形状: [batch_size, seq_len]
# 通常还会传入序列的实际有效长度,供RNN/Attention使用
"hist_seq_len": torch.tensor([3]), # 表示该序列只有前3个是真实的
}
在模型的forward函数内部,会这样处理这些输入:
python
def forward(self, inputs):
# 1. 处理单值特征:查表得到向量
user_emb = self.emb_user(inputs['user_id']) # (batch, 1, emb_dim)
item_emb = self.emb_item(inputs['item_id']) # (batch, 1, emb_dim)
# 2. 处理多值特征(以Multi-hot形式为例)
tag_emb_table = self.emb_tag # 共享的标签嵌入表
# 利用矩阵乘法实现高效的多值查找与聚合(求和)
user_tag_emb = torch.matmul(inputs['user_tags'].float(), tag_emb_table) # (batch, emb_dim)
# 3. 处理序列特征(关键步骤)
hist_item_seq = inputs['hist_item_seq'] # (batch, seq_len)
seq_emb = self.emb_item(hist_item_seq) # (batch, seq_len, emb_dim)
# 使用注意力机制(如DIN)或Transformer,利用候选物品动态加权聚合序列
interest_emb = self.attention_net(seq_emb, item_emb, inputs['hist_seq_len']) # (batch, emb_dim)
# 4. 拼接所有特征向量,输入全连接层
concat_feat = torch.cat([user_emb, item_emb, user_tag_emb, interest_emb, ...], dim=1)
output = self.dnn_layers(concat_feat)
return output
5. 核心要点与工程意义
-
为什么不像单值特征那样直接Embedding?
- 对于多值特征 ,直接Embedding会丢失"这是一个集合"的信息。通过先查表后聚合(通常是求和或平均),模型能学到这个集合的整体表征。
- 对于序列特征 ,其价值在于顺序和结构 。通过专门的序列模型层 (如注意力机制),模型可以捕捉用户兴趣的动态演变,并实现与候选物品的动态交互(例如,用户历史中与当前物品相似的部分会获得更高注意力权重),这是信息流推荐效果提升的关键。
-
工程实现的一致性:
- 无论特征多么复杂,最终都需在特征工程阶段 转化为数值型索引,以保证线上服务的高效和一致性。
- 特征的复杂语义(如序列的时序关系、多值的集合关系)主要由模型结构 (
forward函数内的计算图)来刻画和解释。
总而言之,forward函数输入的是一组结构化、索引化的数字 ,而模型内部的计算路径 (嵌入、聚合、序列建模)才是将这些索引转化为具有丰富语义的向量表示,并最终做出预测的关键。这种设计完美地分离了数据预处理 和模型计算,是大规模系统得以稳定高效运行的基础。
如果你在考虑具体实现,选择多值特征的表示方式 (Multi-hot vs. 列表)和序列模型的复杂度(Pooling vs. Transformer)时,需要在效果和推理延迟之间进行权衡。