""" CrossEncoder 文档处理详解 解答:Document 是作为整体还是拆分成 sentences? """ print("=" * 80) print("CrossEncoder 如何处理 Document?") print("=" * 80) # ============================================================================ # Part 1: Document 的实际处理方式 # ============================================================================ print("\n" + "=" * 80) print("📝 Part 1: Document 的实际处理方式") print("=" * 80) query = "什么是人工智能?" document = """人工智能是计算机科学的一个分支。它致力于创建智能系统。 这些系统可以执行需要人类智能的任务。人工智能包括机器学习等子领域。""" print(f"\n原始输入:") print(f"Query: {query}") print(f"\nDocument (包含多个句子):") print(f"{document}") print("\n" + "-" * 80) print("关键问题:Document 有多个句子,CrossEncoder 如何处理?") print("-" * 80) print(""" 答案:CrossEncoder 把整个 Document 作为一个整体处理! 具体过程: 1. 输入拼接:[CLS] Query [SEP] Document [SEP] └─ Document 的所有句子都拼接在一起 2. 分词:整个序列被切分成 tokens └─ 不是按句子分,而是整个 Document 一起分词 3. 生成 embeddings: └─ 每个 token 一个向量(不是每个句子一个向量!) └─ Document 可能有 100 个 tokens = 100 个向量 """) # ============================================================================ # Part 2: 详细的 Token 级别处理 # ============================================================================ print("\n" + "=" * 80) print("🔤 Part 2: Token 级别的处理(实际发生的事情)") print("=" * 80) # 模拟真实的处理过程 concatenated = f"[CLS] {query} [SEP] {document} [SEP]" print(f"\n步骤1:拼接成单一序列") print(f"{'─' * 40}") print(f"{concatenated[:100]}...") # 简化的分词(实际 BERT tokenizer 会用 WordPiece) def tokenize_chinese(text): """简化的中文分词""" tokens = [] i = 0 while i < len(text): if text[i:i+5] == '[CLS]': tokens.append('[CLS]') i += 5 elif text[i:i+5] == '[SEP]': tokens.append('[SEP]') i += 5 elif text[i] == ' ': i += 1 continue else: tokens.append(text[i]) i += 1 return tokens tokens = tokenize_chinese(concatenated) print(f"\n步骤2:分词(每个字/词变成 token)") print(f"{'─' * 40}") print(f"总共 {len(tokens)} 个 tokens") print(f"前 30 个 tokens: {tokens[:30]}") print(f"\n步骤3:每个 token 生成一个向量") print(f"{'─' * 40}") print(f""" Token 序列 (长度={len(tokens)}): tokens[0] = '[CLS]' → embedding[0] (768维向量) tokens[1] = '什' → embedding[1] (768维向量) tokens[2] = '么' → embedding[2] (768维向量) ... tokens[10] = '[SEP]' → embedding[10] (768维向量) tokens[11] = '人' → embedding[11] (768维向量) ← Document 开始 tokens[12] = '工' → embedding[12] (768维向量) tokens[13] = '智' → embedding[13] (768维向量) tokens[14] = '能' → embedding[14] (768维向量) ... tokens[{len(tokens)-1}] = '[SEP]' → embedding[{len(tokens)-1}] (768维向量) 关键点: ✅ Document 不是一个向量! ✅ Document 的每个字/词都是一个向量! ✅ 即使 Document 有多个句子,也是连续的 token 序列 """) # ============================================================================ # Part 3: 注意力如何跨句子工作 # ============================================================================ print("\n" + "=" * 80) print("🌟 Part 3: 注意力机制跨句子工作") print("=" * 80) print(""" Document 有多个句子时的注意力计算: 假设 Document = "句子1。句子2。句子3。" Token序列: [CLS] Query词1 Query词2 [SEP] 句子1词1 句子1词2 。 句子2词1 句子2词2 。 句子3词1 [SEP] ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ t[0] t[1] t[2] t[3] t[4] t[5] t[6] t[7] t[8] t[9] t[10] t[11] Self-Attention 计算: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Query词1 (t[1]) 的注意力: - 可以关注 句子1词1 (t[4]) ✓ - 可以关注 句子2词1 (t[7]) ✓ - 可以关注 句子3词1 (t[10]) ✓ → Query 的词可以看到 Document 所有句子的所有词! 句子1词1 (t[4]) 的注意力: - 可以关注 Query词1 (t[1]) ✓ - 可以关注 句子2词1 (t[7]) ✓ (跨句子!) - 可以关注 句子3词1 (t[10]) ✓ (跨句子!) → Document 内的不同句子也能互相看到! 这就是"全局注意力"(Global Attention): 每个 token 都能看到整个序列的所有 token! """) # ============================================================================ # Part 4: 为什么不拆分成句子? # ============================================================================ print("\n" + "=" * 80) print("❓ Part 4: 为什么不把 Document 拆成多个句子?") print("=" * 80) print(""" 方案A:把 Document 当整体(CrossEncoder 实际做法)✅ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 输入:[CLS] Query [SEP] 句子1+句子2+句子3 [SEP] ↓ 单次推理,得到一个分数: 8.5 优点: ✅ 一次计算,速度快 ✅ 句子之间可以互相关注,理解上下文 ✅ 整体语义理解更好 缺点: ⚠️ 有长度限制(通常 512 tokens) 如果 Document 太长会被截断 方案B:拆成多个句子分别计算(不推荐)❌ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 输入1:[CLS] Query [SEP] 句子1 [SEP] → 分数: 7.2 输入2:[CLS] Query [SEP] 句子2 [SEP] → 分数: 8.1 输入3:[CLS] Query [SEP] 句子3 [SEP] → 分数: 6.5 然后取平均或最大值? 缺点: ❌ 需要计算 3 次,速度慢 3 倍 ❌ 句子之间无法互相理解 ❌ 丢失了上下文信息 ❌ 如何聚合分数?平均?最大?都不完美 """) # ============================================================================ # Part 5: 实际代码示例 # ============================================================================ print("\n" + "=" * 80) print("💻 Part 5: 实际代码示例") print("=" * 80) print(""" 使用 CrossEncoder 的真实代码: ```python from sentence_transformers import CrossEncoder model = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2') query = "什么是人工智能?" # Document 有多个句子 document = \"\"\" 人工智能是计算机科学的一个分支。 它致力于创建智能系统。 这些系统可以执行需要人类智能的任务。 \"\"\" # 直接传入整个 Document! pairs = [[query, document]] # ← 注意:整个 document 作为一个字符串 # 模型内部会自动: # 1. 拼接:[CLS] query [SEP] document [SEP] # 2. 分词:切分成 tokens(可能有 50-100 个) # 3. 编码:每个 token 一个向量 # 4. 注意力:所有 tokens 互相关注 # 5. 输出:一个分数 scores = model.predict(pairs) print(f"相关性分数: {scores[0]}") # 输出: 8.26 ``` 关键理解: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Document 不会被拆分! Document 的每个字/词都会变成一个向量! 所有向量通过注意力机制互相连接! 最终输出一个整体的相关性分数! """) # ============================================================================ # Part 6: Token 限制问题 # ============================================================================ print("\n" + "=" * 80) print("⚠️ Part 6: Document 太长怎么办?") print("=" * 80) print(""" CrossEncoder 有长度限制(通常 512 tokens) 如果 Document 太长(比如 1000 个字): 解决方案1:截断(最常用) ━━━━━━━━━━━━━━━━━━━━━━━ 只保留前 512 tokens: [CLS] Query [SEP] Document前400个字 [SEP] 优点:简单快速 缺点:可能丢失重要信息 解决方案2:滑动窗口 ━━━━━━━━━━━━━━━━━ 分成多个窗口,每个窗口单独计算: 窗口1: [CLS] Query [SEP] Document[0:400] [SEP] → 分数: 7.2 窗口2: [CLS] Query [SEP] Document[200:600] [SEP] → 分数: 8.5 窗口3: [CLS] Query [SEP] Document[400:800] [SEP] → 分数: 6.8 取最高分: 8.5 优点:不会丢失信息 缺点:计算量增加 解决方案3:先用 Bi-Encoder 粗排 ━━━━━━━━━━━━━━━━━━━━━━━━━━ 1. 把长 Document 拆成段落 2. 用 Bi-Encoder 快速找到最相关的 1-2 个段落 3. 只对这些段落用 CrossEncoder 重排 优点:速度快,准确率高 缺点:两阶段处理 你的项目使用的是方案1(截断): ━━━━━━━━━━━━━━━━━━━━━━━━━ 在 reranker.py 中: CrossEncoderReranker(max_length=512) ← 超过 512 会自动截断 """) # ============================================================================ # Part 7: 可视化总结 # ============================================================================ print("\n" + "=" * 80) print("📊 Part 7: 可视化总结") print("=" * 80) print(""" Document 处理的完整流程: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 输入 Document (多句子): ┌────────────────────────────────────────────────────────────┐ │ "人工智能是计算机科学的一个分支。它致力于创建智能系统。" │ │ 句子1 句子2 │ └────────────────────────────────────────────────────────────┘ ↓ 拼接成单一序列 ↓ ┌────────────────────────────────────────────────────────────┐ │ [CLS] 什么是人工智能? [SEP] 人工智能是...智能系统。 [SEP] │ │ 特殊 Query tokens 分隔 Document tokens 结束 │ └────────────────────────────────────────────────────────────┘ ↓ 分词 (Tokenization) ↓ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │[CLS]│ 什 │ 么 │[SEP]│ 人 │ 工 │ ...│ 统 │ 。 │[SEP]│ └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ ↓ 每个 token → 一个 768维向量 ↓ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ V₀ │ V₁ │ V₂ │ V₃ │ V₄ │ V₅ │ ... │ Vₙ₋₂│ Vₙ₋₁│ Vₙ │ │768维│768维│768维│768维│768维│768维│ ... │768维│768维│768维│ └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ ↓ Self-Attention (12 层) 每个向量都能"看到"所有其他向量 ↓ ┌────────────────────────────────────────────────────────────┐ │ V₀' (更新后的 [CLS] 向量) │ │ 包含了整个序列的信息 │ └────────────────────────────────────────────────────────────┘ ↓ 全连接层 (分类头) ↓ 相关性分数 8.26 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 关键点总结: 1. Document 整体处理 ✓ └─ 不是一个向量,是很多向量的序列 2. 每个字/词一个向量 ✓ └─ 不是每个句子一个向量 3. 全局注意力 ✓ └─ Query 的词能看到 Document 所有句子的所有词 4. 最终一个分数 ✓ └─ 从 [CLS] 向量提取出来 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ """) # ============================================================================ # Part 8: 对比 Bi-Encoder 的处理方式 # ============================================================================ print("\n" + "=" * 80) print("🔄 Part 8: 对比 Bi-Encoder 的处理方式") print("=" * 80) print(""" Bi-Encoder (向量检索): ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Document: "句子1。句子2。句子3。" ↓ Encoder (BERT) ↓ 取 [CLS] 向量 ↓ 单个向量 (768维) ← Document 被压缩成一个向量! ↓ 与 Query 向量做余弦相似度 ↓ 相关性分数 CrossEncoder (深度重排): ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Query + Document: "[CLS] Query [SEP] 句子1。句子2。句子3。 [SEP]" ↓ Encoder (BERT) ↓ 保留所有 token 的向量 ↓ 向量序列 (n × 768) ← 保留了所有细节! ↓ Self-Attention 让所有词互相理解 ↓ 相关性分数 区别: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Bi-Encoder: Document → 1 个向量 (信息压缩) CrossEncoder: Document → n 个向量 (信息保留) Bi-Encoder: Query 和 Document 分开处理 CrossEncoder: Query 和 Document 一起处理 Bi-Encoder: 快速但不够准确 CrossEncoder: 慢但非常准确 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ """) print("\n" + "=" * 80) print("✅ 总结答案") print("=" * 80) print(""" 你的问题:Document 是做成一个 embedding,还是每个 sentence 做成一堆向量? 答案:都不是! 😊 正确理解: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ✅ Document 整体作为输入(不拆分句子) ✅ 但 Document 的每个字/词都会生成一个向量 ✅ 不是"一个 embedding",而是"一个向量序列" ✅ 不是"按句子分",而是"按字/词分" Document (50个字) → 50 个向量 (每个 768 维) 不是 1 个向量 也不是 3 个向量(如果有3个句子) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 这就是为什么 CrossEncoder 能理解细粒度的语义关系! """) print("\n💡 现在你理解了吗?如有疑问,请继续提问!\n")