跳转至

推荐算法中Sparse与Dense层在结构、训练及优化上的核心区别解析

原文地址: https://88box.top 生成时间: 2026-05-19 16:32:00


推荐算法模型sparse和dense在结构和训练以及优化上有什么区别 - hey99 知识搜索引擎

精选文章

推荐算法模型sparse和dense在结构和训练以及优化上有什么区别

Sparse层是记忆(存储每个ID的表示),Dense层是推理(学习特征间的交互规律)

更新于 2026-05-19 08:17

算法

机器学习

人工智能

数据挖掘

推荐算法

推荐模型 Dense 层与 Sparse 层深度解析

一、本质区别概览

推荐模型

├── Sparse层:处理离散特征(用户ID/商品ID/类目)

│ └── 核心:Embedding Table查表

└── Dense层:处理连续特征 + 特征交叉 + 预测

└── 核心:矩阵乘法 + 激活函数

最核心的区别一句话:

Sparse层是

记忆

(存储每个ID的表示),Dense层是

推理

(学习特征间的交互规律)

二、结构区别

2.1 Sparse层结构

本质:超大规模查找表

class SparseLayer(nn.Module):

def init(self):

典型规模:

用户ID: 1亿 × 64维 = 6.4GB (float32)

商品ID: 5000万 × 64维 = 3.2GB

类目ID: 10万 × 32维 = 很小

self.user_embedding = nn.EmbeddingBag(

num_embeddings=100_000_000, # 词表大小

embedding_dim=64,

mode='mean',

sparse=True # 关键参数!

)

self.item_embedding = nn.EmbeddingBag(

num_embeddings=50_000_000,

embedding_dim=64,

sparse=True

)

def forward(self, user_ids, item_ids):

user_emb = self.user_embedding(user_ids) # [B, 64]

item_emb = self.item_embedding(item_ids) # [B, 64]

return user_emb, item_emb

关键特性:

参数量:巨大(百亿级别),但每次只激活极少数

激活方式:查表(index select),不做矩阵乘法

梯度:极度稀疏(一个batch只更新极少数行)

内存分布:必须跨机器分布式存储

2.2 Dense层结构

class DenseLayer(nn.Module):

def init(self, input_dim=512, hidden_dims=[256, 128, 64]):

super().init()

典型的MLP结构

layers = []

prev_dim = input_dim

for dim in hidden_dims:

layers.extend([

nn.Linear(prev_dim, dim),

nn.BatchNorm1d(dim),

nn.ReLU(),

nn.Dropout(0.1)

])

prev_dim = dim

self.mlp = nn.Sequential(*layers)

self.output = nn.Linear(prev_dim, 1)

def forward(self, x):

x: 拼接后的所有embedding + 连续特征

hidden = self.mlp(x) # [B, 64]

logit = self.output(hidden) # [B, 1]

return logit

工业界常见结构(以DLRM为例)

class DLRM(nn.Module):

def init(self):

Bottom MLP: 处理连续特征

self.bottom_mlp = DenseMLP([dense_features, 256, 64])

Sparse: 处理离散特征

self.embeddings = SparseLayer()

Interaction: 特征交叉

self.interaction = DotInteraction()

Top MLP: 最终预测

self.top_mlp = DenseMLP([interaction_dim, 256, 1])

关键特性:

参数量:相对小(百万级别),但每次全部激活

激活方式:矩阵乘法(GEMM),GPU高度并行

梯度:稠密(每次更新所有参数)

内存分布:单机即可,GPU显存友好

2.3 结构对比表

维度

Sparse层

Dense层

参数规模

百亿~千亿

百万~千万

每次激活参数

<0.01%

100%

计算类型

Index Select(内存带宽瓶颈)

GEMM(算力瓶颈)

硬件亲和

CPU/大内存

GPU

参数增长方式

随用户/商品量线性增长

相对固定

三、训练区别

3.1 梯度特性差异

Sparse层梯度:极度稀疏

假设batch_size=1024,用户ID词表=1亿

每次只有1024个用户的embedding被更新

梯度矩阵稀疏度 = 1 - 1024/100000000 ≈ 99.999%

Dense层梯度:稠密

所有参数都有梯度,正常反向传播

Sparse梯度问题:

├── 热门ID梯度累积过快 → 过拟合

├── 冷门ID几乎不更新 → 欠拟合

└── 长尾分布导致训练极不均衡

3.2 学习率策略差异

Dense层:标准学习率

optimizer_dense = torch.optim.Adam(

dense_params,

lr=1e-3, # 常规学习率

weight_decay=1e-5 # L2正则有效

)

Sparse层:需要特殊处理

optimizer_sparse = torch.optim.SparseAdam(

sparse_params,

lr=0.01, # 通常比dense大,因为更新频率低

注意:SparseAdam只更新本次出现的embedding

)

工业界常见:Adagrad for Sparse

因为Adagrad天然适合稀疏更新

optimizer_sparse = torch.optim.Adagrad(

sparse_params,

lr=0.05,

initial_accumulator_value=1.0 # 避免初期学习率过大

)

为什么Sparse用Adagrad?

Adagrad: lr_i = lr / sqrt(G_ii + ε)

G_ii = 累积历史梯度平方和

热门ID:G_ii大 → 学习率小 → 防止过拟合

冷门ID:G_ii小 → 学习率大 → 加速学习

天然自适应处理长尾问题!

3.3 正则化策略差异

Dense层正则化:标准方法有效

class DenseRegularization:

def apply(self, model):

L2正则(weight_decay):有效

Dropout:有效

BatchNorm:有效

因为每个参数都被频繁更新

pass

Sparse层正则化:需要特殊设计

class SparseRegularization:

def frequency_based_regularization(self, embedding, freq):

"""

基于频次的差异化正则

高频ID:强正则(防过拟合)

低频ID:弱正则(防欠拟合)

"""

方案1:频次自适应L2

reg_weight = 1.0 / (freq + 1) # 频次越高,正则越弱?

实际更复杂,需要根据业务调整

方案2:范数约束

将embedding L2范数限制在一定范围内

norm = torch.norm(embedding, dim=1, keepdim=True)

embedding = embedding / torch.clamp(norm, min=1.0)

方案3:冷启动特殊处理

低频ID共享一个统一的embedding,

超过阈值后才独立

return embedding

3.4 分布式训练差异

Dense层分布式:数据并行(简单)

┌──────────┐ ┌──────────┐ ┌──────────┐

│ GPU 0 │ │ GPU 1 │ │ GPU 2 │

│ 完整Dense │ │ 完整Dense │ │ 完整Dense │

│ 1/3数据 │ │ 1/3数据 │ │ 1/3数据 │

└──────────┘ └──────────┘ └──────────┘

↓梯度AllReduce同步↑

Sparse层分布式:模型并行(复杂)

┌──────────┐ ┌──────────┐ ┌──────────┐

│ Shard 0 │ │ Shard 1 │ │ Shard 2 │

│用户ID 0-3kw│ │用户ID 3-6kw│ │用户ID 6kw+│

└──────────┘ └──────────┘ └──────────┘

需要All-to-All通信(昂贵!)

四、优化区别

4.1 计算优化

Dense层优化重点:算力

1. 混合精度训练

from torch.cuda.amp import autocast

with autocast():

output = dense_model(input) # FP16计算,FP32存储

2. 算子融合

将Linear + BN + ReLU融合为单个CUDA kernel

减少显存读写次数

3. 梯度检查点(节省显存)

from torch.utils.checkpoint import checkpoint

output = checkpoint(dense_layer, input)

4. Flash Attention(如果有attention结构)

减少HBM读写,提升速度

Sparse层优化重点:内存带宽

1. Embedding压缩

class CompressedEmbedding:

方案A:量化(INT8/FP16存储)

def quantized_embedding(self):

存储节省50%~75%,精度损失很小

pass

方案B:Hash技巧(减小词表)

def hashed_embedding(self, id, num_buckets=1_000_000):

hashed_id = id % num_buckets # 多个ID共享embedding

return self.embedding(hashed_id)

方案C:QR分解(组合embedding)

def qr_embedding(self, id, q_size, r_size):

q_id = id % q_size

r_id = id // r_size

return self.q_emb(q_id) + self.r_emb(r_id)

参数量从n×d降至(q+r)×d

2. EmbeddingBag vs Embedding

EmbeddingBag:查表+聚合一步完成,更快

nn.EmbeddingBag(mode='mean') # 优于 Embedding + mean

3. 缓存热门ID的embedding

LRU Cache,减少跨机器通信

4.2 工程优化

问题:Sparse在CPU/内存服务器,Dense在GPU

每个batch需要跨设备通信,成为瓶颈

优化方案:

方案1:Pipeline并行

┌─────────────────────────────────┐

│ Batch N: [Sparse查表] ────────→ [Dense计算] → Loss │

│ Batch N+1: [Sparse查表] → ... │

│ │

│ Sparse和Dense流水线重叠,掩盖通信延迟 │

└─────────────────────────────────┘

方案2:Prefetch

提前预取下一个batch的embedding

当前batch做Dense计算时,已经在取下一批ID的embedding

方案3:本地化热门Embedding

高频ID的embedding缓存到GPU显存

命中率通常>80%(长尾分布特性)

4.3 更新频率优化

class DifferentialUpdateStrategy:

"""

Dense和Sparse使用不同的更新频率

"""

def init(self):

self.dense_update_freq = 1 # 每个batch更新

self.sparse_update_freq = 1 # 每个batch更新(但稀疏)

工业界实践:

Dense: 小batch + 高频更新(对新数据敏感)

Sparse: 可以积累梯度后批量更新

因为同一个ID在短时间内出现次数有限

def sparse_lazy_update(self, embedding_grad, accumulate_steps=10):

"""

Lazy更新:只在ID出现时更新其embedding

标准Adam需要维护所有ID的m/v,太费内存

SparseAdam只维护出现过的ID的状态

"""

pass

4.4 核心优化对比总结

优化维度

Sparse层

Dense层

主要瓶颈

内存容量+带宽

GPU算力

压缩方式

量化/Hash/分解

剪枝/蒸馏/低秩分解

优化器

Adagrad/SparseAdam

Adam/AdamW

并行策略

模型并行(分片)

数据并行

通信模式

All-to-All

AllReduce

精度策略

FP16存储+FP32更新

AMP混合精度

正则策略

频次自适应

Dropout + L2

五、工业界典型问题与解法

5.1 Sparse特有问题

问题1:新ID冷启动

症状:新用户/新商品没有embedding,随机初始化效果差

解法:

├── 用画像特征(年龄/性别/类目)初始化embedding

├── 共享相似ID的embedding(基于side information)

└── Meta-learning快速适应

问题2:ID爆炸

症状:词表过大,内存放不下

解法:

├── Hash trick:多个ID共享bucket

├── 频次过滤:低于阈值的ID用UNK替代

└── 分层embedding:用类目embedding兜底

问题3:Embedding漂移

症状:长时间训练后,embedding空间变形,相似ID距离变远

解法:

├── 定期重新初始化低频ID

└── 对比学习约束embedding空间

5.2 Dense特有问题

问题1:梯度消失/爆炸

解法:BatchNorm + 残差连接 + 梯度裁剪

问题2:过拟合(特别是特征交叉层)

解法:Dropout + 早停 + 正则化

问题3:特征尺度不均

解法:归一化 + 分桶离散化

六、一句话总结

Sparse层:

"花100GB内存,记住10亿用户/商品的个性化表示,

每次只查1000个,靠频次自适应的优化器处理长尾"

Dense层:

"花1GB显存,用矩阵乘法学会所有特征如何交互,

每次全量更新,靠GPU算力快速收敛"

两者配合:

Sparse负责记住"你是谁",

Dense负责推理"你可能喜欢什么"

查看原文


🏷 标签: 推荐算法, Embedding技术, 稀疏训练, 分布式优化, 特征工程