HLLM (Hierarchical Large Language Model)

  • HLLM是一种分层大规模语言模型架构,主要用于处理具有层次结构的数据(如用户-物品交互、文档-段落、对话历史等)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
HLLM Architecture:
┌─────────────────────────────────────┐
│ Task-specific Layer │
├─────────────────────────────────────┤
│ Cross-Level Attention │
├─────────────────────────────────────┤
│ Level 3: 全局语义表示层 │
├─────────────────────────────────────┤
│ Level 2: 会话/序列建模层 │
├─────────────────────────────────────┤
│ Level 1: 基础元素编码层 │
├─────────────────────────────────────┤
│ Token Embedding Layer │
└─────────────────────────────────────┘
  • 下面我们基于核心思想,实现了一个简化版的:
1
2
3
4
5
第一阶段(离线):                           第二阶段(在线训练):
┌──────────────┐ ┌─────────────┐ ┌─────────────┐
│ Item LLM │ --预计算--> │ 冻结item嵌入 │ --输入--> │ User LLM │
│ (外部LLM) │ item嵌入 └─────────────┘ │ (可训练) │
└──────────────┘ └─────────────┘

实验说明

整体流程

1
2
3
4
5
6
7
8
9
10
11
preprocess_ml_hstu.py          generate_item_embeddings_hllm.py
↓ ↓
原始数据预处理 使用LLM生成item embeddings
↓ ↓
train/val/test数据 item_embeddings_{model}.pt
↓ ↓
run_hllm_movielens.py

HLLM模型训练

评估(loss + ranking metrics)

数据预处理1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
原始数据 (ratings.dat, movies.dat)

数据加载与过滤

词表构建 (vocab.pkl)

用户序列构建

滑动窗口采样 ← 关键的数据增强步骤

时间差计算 ← 时间感知建模的核心

用户级数据分割

输出: train/val/test_data.pkl
  1. 用户序列构建(滑动窗口采样)
1
2
3
4
5
6
7
8
9
用户A的原始序列: [item1, item2, item3, item4, item5]  (长度5)

滑动窗口生成的样本:
1. 样本1: 历史=[item1], 目标=item2
2. 样本2: 历史=[item1, item2], 目标=item3
3. 样本3: 历史=[item1, item2, item3], 目标=item4
4. 样本4: 历史=[item1, item2, item3, item4], 目标=item5

数据增强倍数: 4倍
  1. 时间差计算
1
2
3
4
5
6
7
8
9
10
11
原始时间戳: [1000, 1200, 1500, 1800, 2000]  (秒)

历史序列: [1000, 1200, 1500, 1800] (长度4)
查询时间: 1800 (最后一个时间戳)

时间差: [800, 600, 300, 0] (秒)
解释:
- 第一个事件在800秒前
- 第二个事件在600秒前
- 第三个事件在300秒前
- 当前事件在0秒前
  1. 序列截断和填充
1
2
3
原始序列: [item1, item2, item3]  (长度3)
填充后: [0, 0, item1, item2, item3] (长度5)
时间差: [0, 0, 800, 300, 0]
  1. 用户级数据分割
1
2
3
4
5
6
7
8
❌ 错误方式:随机分割样本
用户A的样本: [样本1(训练), 样本2(训练), 样本3(测试)]
问题:模型在训练时已经见过用户A的模式,测试时评估有偏差

✅ 正确方式:按用户分割
用户A的所有样本 → 训练集
用户B的所有样本 → 测试集
好处:模型必须泛化到未见过的用户

数据预处理2

1
2
3
4
5
6
7
8
9
10
11
movies.dat (原始电影数据)

提取文本信息

movie_text_map.pkl (电影ID→文本描述)

加载LLM模型

生成embeddings (使用最后一个token的hidden state)

item_embeddings_{model}.pt (V, D)
  1. 文本提示构建
1
2
3
4
5
6
7
8
9
10
11
ITEM_PROMPT = "Compress the following sentence into embedding: "

def extract_movie_text(data_dir, output_dir):
# 读取movies.dat
movies = pd.read_csv(movies_file, sep='::',
names=['movie_id', 'title', 'genres'])

# 构建官方HLLM格式的文本
for row in movies:
text = f"{ITEM_PROMPT}title: {title}genres: {genres}"
movie_text_map[movie_id] = text
1
2
3
4
5
原始电影数据:
movie_id=1, title="Toy Story (1995)", genres="Animation|Children's|Comedy"

构建的prompt:
"Compress the following sentence into embedding: title: Toy Story (1995) genres: Animation|Children's|Comedy"
  1. 模型加载配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def generate_item_embeddings(model_type, movie_text_map, output_dir, device):
# 模型路径和维度配置
if model_type == 'tinyllama':
model_name = '/data1/wangxincheng/HuggingFace/TinyLlama-1.1B-Chat-v1.0'
d_model = 2048 # TinyLlama的隐藏维度
elif model_type == 'llama2-7b':
model_name = '/data1/wangxincheng/HuggingFace/Llama-2-7b-hf'
d_model = 4096 # Llama2-7b的隐藏维度
elif model_type == 'baichuan2':
model_name = 'baichuan-inc/Baichuan2-7B-Chat'
d_model = 4096 # Baichuan2的隐藏维度

# 加载模型(使用8bit量化节省显存)
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16 if device == 'cuda' else torch.float32,
device_map=device,
load_in_8bit=True, # 关键:使用8bit量化
trust_remote_code=True
)
  1. embedding 生成策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
with torch.no_grad():
for movie_id in tqdm.tqdm(sorted(movie_text_map.keys())):
text = movie_text_map[movie_id]

# 1. 分词
inputs = tokenizer(text, return_tensors='pt',
truncation=True, max_length=512)
inputs = {k: v.to(device) for k, v in inputs.items()}

# 2. 前向传播,获取所有层的hidden states
outputs = model(**inputs, output_hidden_states=True)
hidden_states = outputs.hidden_states[-1] # 最后一层

# 3. 使用最后一个token的hidden state作为item embedding
# (item_emb_token_n=1 在官方实现中)
item_emb = hidden_states[0, -1, :].cpu().numpy()

embeddings_array[movie_id] = item_emb
1
2
3
4
5
输入序列: [token1, token2, token3, ..., tokenN]

每个token的hidden state: [h1, h2, h3, ..., hN]

使用hN作为item embedding(包含了整个序列的信息)

为什么用最后一个token?
两种可能的策略:

  1. 策略1: 使用[CLS] token(BERT风格)
  • 需要特殊的[CLS] token,且模型需要预训练支持
  1. 策略2: 使用最后一个token(GPT风格,官方HLLM采用)
  • 简单直接,最后一个token能看到整个序列
  • 符合因果语言模型的特性

实验运行阶段

整个实验遵循两阶段训练策略:

  • 阶段1:离线预计算(一次性)
    • item embedding生成:使用LLM为每个item生成静态表示
    • 数据处理:将用户行为转化为序列格式
  • 阶段2:在线训练(可重复)
    • 序列建模:使用Transformer学习用户行为模式
    • 时间编码:融入时间间隔信息
    • NCE训练:高效处理大规模item空间

这种架构的优势:

  • 计算效率:避免端到端训练大模型
  • 灵活性:可以更换不同的LLM生成item embeddings
  • 可扩展性:适用于大规模item推荐场景

模型架构

Item Embeddings(冻结的静态表示)

1
2
# 关键代码分析
self.register_buffer('item_embeddings', item_embeddings.float())
  • 使用register_buffer注册,不是可训练参数
  • 预计算方式:使用外部LLM对每个item生成embedding

核心Transformer层

1
2
3
4
5
6
7
8
9
10
11
12
class HLLMTransformerBlock(nn.Module):
def forward(self, x, rel_pos_bias=None):
# 1. 自注意力机制
scores = torch.matmul(Q, K.transpose(-2, -1)) * self.scale

# 2. 因果掩码(确保只关注历史信息)
causal_mask = torch.tril(torch.ones(seq_len, seq_len, ...))
scores = scores.masked_fill(~causal_mask, float('-inf'))

# 3. 相对位置偏置(可选)
if rel_pos_bias is not None:
scores = scores + rel_pos_bias

关键设计:

  • 因果掩码:保证序列建模的方向性,只使用历史信息预测未来
  • 相对位置偏置:捕获序列中item之间的相对位置关系
  • 残差连接:每个子层后都有残差连接,防止梯度消失

另外,还有位置和时间编码:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 位置编码
self.position_embedding = nn.Embedding(max_seq_len, d_model)
positions = torch.arange(seq_len, dtype=torch.long, device=seq_tokens.device)
pos_emb = self.position_embedding(positions)

# 时间编码
def _time_diff_to_bucket(self, time_diffs):
# 将时间差映射到桶(支持sqrt/log变换)
time_diffs = time_diffs.float() / 60.0 # 秒转分钟
if self.time_bucket_fn == 'sqrt':
buckets = torch.sqrt(time_diffs).long()
elif self.time_bucket_fn == 'log':
buckets = torch.log(time_diffs).long()

时间编码策略:

  • 将连续的时间差离散化为桶(buckets)
  • 使用sqrt或log变换处理长尾分布的时间间隔
  • 学习每个时间桶的embedding表示

前向传播流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def forward(self, seq_tokens, time_diffs=None):
# Step 1: 查找预计算的item embeddings
item_emb = self.item_embeddings[seq_tokens] # (B, L, D)

# Step 2: 添加位置编码
positions = torch.arange(seq_len, ...)
pos_emb = self.position_embedding(positions)
embeddings = item_emb + pos_emb.unsqueeze(0)

# Step 3: 添加时间编码(如果提供)
if self.use_time_embedding and time_diffs is not None:
time_buckets = self._time_diff_to_bucket(time_diffs)
time_emb = self.time_embedding(time_buckets)
embeddings = embeddings + time_emb

# Step 4: 通过Transformer层
x = embeddings
for block in self.transformer_blocks:
x = block(x, rel_pos_bias=rel_pos_bias)

# Step 5: 计算logits(与所有item的相似度)
logits = torch.matmul(x, self.item_embeddings.t()) / self.temperature
# (B, L, V) - 每个位置对全量item的预测分数

与官方HLLM区别

  1. 整体流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────┐
│ End-to-End Training │
├─────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Item LLM │←→│ User LLM │ │
│ │ (可训练) │ │ (可训练) │ │
│ └──────────────┘ └──────────────┘ │
│ ↑ ↑ │
│ item texts user sequences│
└─────────────────────────────────────┘

┌─────────────────────────────────────┐
│ Two-stage Pipeline │
├─────────────────────────────────────┤
│ Stage 1 (离线) │ Stage 2 (训练) │
│ ┌──────────────┐ │ ┌──────────────┐
│ │ Item LLM │ │ │ User LLM │
│ │ (冻结) │──│→│ (可训练) │
│ └──────────────┘ │ └──────────────┘
│ item texts │ user sequences |
└─────────────────────────────────────┘
对比维度 官方HLLM Rechub实现 差异分析
训练方式 端到端联合训练 两阶段训练(预计算+微调) 根本差异:官方版同时优化item和user表示,Rechub分离两者
Item Embeddings 可训练,随训练更新 预计算并冻结 Rechub牺牲了item表示的动态性,换取了计算效率
User LLM 完整的LLM架构 简化版Transformer Rechub专注于序列建模,去掉了LLM的复杂组件
Item文本处理 在线处理,有特殊[ITEM] token 离线处理,无特殊token 官方版更灵活,Rechub更简单
时间编码 复杂的时间位置编码 可学习的时间bucket embeddings Rechub的实现更轻量
损失函数 对比学习(NCE) 支持NCE和交叉熵 两者都支持NCE,Rechub提供更多选择
模型规模 7B+参数量 百万级参数量 Rechub大幅简化,适合资源受限场景
训练显存 需要24GB+ 6-8GB即可 Rechub的实用性强
推理速度 较慢(需同时运行两个模型) 快速(只需User LLM) Rechub更适合实际部署
可解释性 较低(端到端黑盒) 较高(层次分明) Rechub的架构更清晰

实验结果

数据预处理1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
python preprocess_ml_hstu.py 
数据目录: /data1/wangxincheng/torch-rechub/examples/generative/data/ml-1m
输出目录: /data1/wangxincheng/torch-rechub/examples/generative/data/ml-1m/processed
================================================================================
MovieLens-1M HSTU data preprocessing - sliding window with timestamps
================================================================================
加载MovieLens-1M数据...
原始数据: 1000209 条评分, 6040 用户, 3706 电影

数据过滤...
过滤前: 1000209 条评分
评分过滤后 (>=3): 836478 条评分
Item过滤后 (>=5次): 835790 条评分
User过滤后 (>=5次): 835789 条评分
过滤后: 6038 用户, 3307 电影

创建词表...
词表大小: 3308 (包含PAD)

构建用户序列...
用户数: 6038
序列长度统计: 平均=138.4, 最小=7, 最大=1924

使用滑动窗口生成训练样本(支持时间戳)...
时间差统计(秒):
平均: 2596220.3
中位数: 1902.0
最小: 1
最大: 89224014
平均(小时): 721.2
平均(天): 30.0
生成样本数: 829751
数据增强倍数: 137.4x

按用户分割数据集...
用户分割: Train=4226, Val=603, Test=1209
样本分割: Train=577594, Val=83977, Test=168180

保存数据...
保存 train 数据到 /data1/wangxincheng/torch-rechub/examples/generative/data/ml-1m/processed/train_data.pkl
- seq_tokens: (577594, 200)
- seq_time_diffs: (577594, 200)
- targets: (577594,)
保存 val 数据到 /data1/wangxincheng/torch-rechub/examples/generative/data/ml-1m/processed/val_data.pkl
- seq_tokens: (83977, 200)
- seq_time_diffs: (83977, 200)
- targets: (83977,)
保存 test 数据到 /data1/wangxincheng/torch-rechub/examples/generative/data/ml-1m/processed/test_data.pkl
- seq_tokens: (168180, 200)
- seq_time_diffs: (168180, 200)
- targets: (168180,)
保存词表到 /data1/wangxincheng/torch-rechub/examples/generative/data/ml-1m/processed/vocab.pkl
================================================================================
预处理完成!
================================================================================

关键改进:
✅ 采用滑动窗口策略,大幅提升数据量
✅ 添加冷启动过滤,提高数据质量
✅ 按用户分割数据,避免数据泄露
✅ 序列长度统计,便于调优
✅ 支持时间戳处理,计算时间差用于时间感知建模

数据预处理2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
python preprocess_hllm_data.py --model_type llama2-7b

📂 数据目录: /data1/wangxincheng/torch-rechub/examples/generative/data/ml-1m
📂 输出目录: /data1/wangxincheng/torch-rechub/examples/generative/data/ml-1m/processed


================================================================================
环境检查
================================================================================
✅ 使用 CPU 进行计算(速度会较慢)
✅ 环境检查通过


================================================================================
步骤 1: 提取电影文本信息
================================================================================
加载电影元数据: /data1/wangxincheng/torch-rechub/examples/generative/data/ml-1m/movies.dat
✅ 加载了 3883 部电影
已处理 1000 / 3883 部电影
已处理 2000 / 3883 部电影
已处理 3000 / 3883 部电影
✅ 生成了 3883 条文本描述
✅ 文本映射已保存到: /data1/wangxincheng/torch-rechub/examples/generative/data/ml-1m/processed/movie_text_map.pkl

================================================================================
步骤 2: 生成 Item Embeddings (llama2-7b)
================================================================================
加载模型: /data1/wangxincheng/HuggingFace/Llama-2-7b-hf
设备: cuda:4
`torch_dtype` is deprecated! Use `dtype` instead!
The `load_in_4bit` and `load_in_8bit` arguments are deprecated and will be removed in the future versions. Please, pass a `BitsAndBytesConfig` object in `quantization_config` argument instead.
Loading checkpoint shards: 100%|████████████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:28<00:00, 14.25s/it]
✅ 使用官方 ByteDance HLLM 格式(最后一个 token 的隐藏状态)

生成 3883 个 item embeddings...
Generating embeddings: 0%| | 0/3883 [00:00<?, ?it/s]/shared/miniconda3/envs/wxc_RecHub/lib/python3.11/site-packages/bitsandbytes/autograd/_functions.py:181: UserWarning: MatMul8bitLt: inputs will be cast from torch.float32 to float16 during quantization
warnings.warn(f"MatMul8bitLt: inputs will be cast from {A.dtype} to float16 during quantization")
Generating embeddings: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 3883/3883 [09:27<00:00, 6.84it/s]

✅ Item embeddings已保存到: /data1/wangxincheng/torch-rechub/examples/generative/data/ml-1m/processed/item_embeddings_llama2-7b.pt
形状: torch.Size([3953, 4096])

================================================================================
✅ HLLM 数据预处理完成!
================================================================================

输出文件:
- /data1/wangxincheng/torch-rechub/examples/generative/data/ml-1m/processed/movie_text_map.pkl
- /data1/wangxincheng/torch-rechub/examples/generative/data/ml-1m/processed/item_embeddings_llama2-7b.pt

运行测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
加载item embeddings: torch.Size([3953, 2048])
自动检测到 embedding 维度: 2048
Device: cuda:5
Hidden dim: 2048
Num heads: 16
Num layers: 2
Max sequence length: 200

Building data loaders...
Train size: 577594
Val size: 83977
Test size: 168180

Creating model...
Number of parameters: 105,323,008
1
2
3
4
5
6
Hit@10: 0.0399
Hit@50: 0.0871
Hit@200: 0.1932
NDCG@10: 0.0235
NDCG@50: 0.0337
NDCG@200: 0.0493

结果其实比较差!!!