Day2主要聚焦该框架的数据预处理阶段,详细深扒数据是如何输入,以及输入后的数据是如何被切片分割的。
- 读取文档
- 代码解读
论文主代码为RAKG.py,先来看输入数据部分的代码:
def process_all_topics(json_path, output_dir):
#1. 文档级知识图谱构建,该函数负责遍历每个主题,加载文本,处理并生成知识图谱,
# 直接对应于文档级知识图谱构建的过程。
# Load JSON file
with open(json_path, 'r', encoding='utf-8') as file:
topics = json.load(file)
- 调试理解与输出结果
在这段代码后添加以下监控代码,就可以查看加载数据的结果。
# 监控数据加载
print(f"\n--- 1. 文档加载完成 ---")
print(f"加载的数据类型是: {type(topics)}")
print(f"总共加载了 {len(topics)} 个主题文档")
print("--- 查看第一个主题的内容片段 ---")
first_topic = topics[0]
print(f"主题名称: {first_topic['topic']}")
print(f"内容前200个字符: {first_topic['content'][:200]}")
输出结果为: --- 1. 文档加载完成 ---
加载的数据类型是: <class 'list'>
总共加载了 105 个主题文档
--- 查看第一个主题的内容片段 ---
主题名称: The Life Cycle of a Butterfly
内容前200个字符: ```The Life Cycle of a Butterfly
Butterflies are fascinating creatures that undergo a remarkable transformation throughout their life cycle. From egg to larva to pupa to adult butterfly, this process
- 文本分割
- 代码解读 这是RAKG.py中,process_all_topics函数中,对文本的预处理代码,仅有两行。
# Split text实例化TextProcessor以处理文本,进行分割和格式化。
#TextProcessor类处理文本分割,确保在处理长文本时,能够保留关键信息,从而减轻上下文遗忘的问题。
processor = TextProcessor(text, topic)
text_split = processor.process()
TextProcesser方法来源于开头的导入操作:
from src.textPrcess import TextProcessor
因此,我们跳转到src目录下的textPrcess进行学习分析,下面是textPrcess.py:
import re
import os
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
from src.llm_provider import LLMProvider
class TextProcessor:
def __init__(self, text, name): # 初始化方法,当创建类的实例时自动调用
self.text = text # 存储待处理的文本
self.base_name = name # 存储基础名称,用于生成ID
self.sentence_to_id = {} # 初始化句子到ID的映射字典
self.id_to_sentence = {} # 初始化ID到句子的映射字典
self.llm_provider = LLMProvider()# 创建LLM提供者实例
self.embeddings = self.llm_provider.get_embedding_model()# 获取嵌入模型
def split_sentences(self, text):
"""Support Chinese and English sentence segmentation (handling common abbreviations)"""
"""支持中英文句子分割(处理常见缩写)"""
# Added Chinese punctuation (references 4, 5)
# 使用正则表达式分割句子
# (?<!\b[A-Za-z].) 表示前面不是单字母缩写(如U.S.)
# (?<=[.!?。!?]) 表示在这些标点符号后面
# \s+ 表示一个或多个空白字符
pattern = re.compile(r'(?<!\b[A-Za-z].)(?<=[.!?。!?])\s+')
sentences = [s.strip() for s in pattern.split(text) if s.strip()]
return sentences
def generate_id(self, index):
"""根据要求生成ID"""
"""Generate ID according to requirements"""
# 从1开始编号
return f"{self.base_name}{index+1}" # Start numbering from 1
def process(self):
# Step 1: Convert PDF to text
# 步骤1:获取文本(传入时已经处理好)
text = self.text
# Step 2: Sentence segmentation and ID mapping
# 步骤2:句子分割和ID映射
sentences = self.split_sentences(text)
for idx, sent in enumerate(sentences):
sent_id = self.generate_id(idx) # 生成ID
self.sentence_to_id[sent] = sent_id # 句子→ID映射
self.id_to_sentence[sent_id] = sent # ID→句子映射
# Step 3: Vector storage
# 步骤3:向量存储 - 使用嵌入模型将所有句子转换为向量
vectors = self.embeddings.embed_documents(sentences)
# 返回包含所有处理结果的字典
return {
"sentences": sentences, # 分割后的句子列表
"vectors": vectors,# 句子的向量表示
"sentence_to_id": self.sentence_to_id,# 句子到ID的映射
"id_to_sentence": self.id_to_sentence# ID到句子的映射
}
- 第一部分:import操作,这部分就是纯导入包的操作,注意目录问题,★主函数调用src中模块时,必须与scr是并列目录的关系。
- 第二部分:class类,这里需要普及一下python中类的知识点:
一句话理解:class就是一系列函数的组合,组合起来的函数能够完成某个综合的功能,就是模块化的一种操作。
class具有以下特征:
- 封装:将相关函数和数据组合在一起
- 实例化:可以创建类的多个实例(对象),每个实例独立
- 继承:可以从其他类继承特性
- 方法(★):类中的函数成为方法,第一个参数为self,指向实例本身
def init(self, text, name):的作用:
初始化实例的“初始状态”,初始化self的属性,其实就是给输入对象添加一系列属性,在本代码中,就是给输入文本添加text,base_name,sentence_to_id,id_to_sentence,llm_provider,embeddings这些属性。
class TextProcessor:
def __init__(self, text, name): # 初始化方法,当创建类的实例时自动调用
self.text = text # 存储待处理的文本
self.base_name = name # 存储基础名称,用于生成ID
self.sentence_to_id = {} # 初始化句子到ID的映射字典
self.id_to_sentence = {} # 初始化ID到句子的映射字典
self.llm_provider = LLMProvider()# 创建LLM提供者实例
self.embeddings = self.llm_provider.get_embedding_model()# 获取嵌入模型
关于self的解释:
__init__ 方法的核心作用就是给 self(即当前创建的实例)添加一系列属性,让实例在创建时就具备必要的数据和资源。
在你提供的代码中,self.text、self.base_name、self.sentence_to_id 等都是通过 __init__ 方法为实例绑定的属性。这些属性就像实例的 “自带数据” 或 “初始配置”,让实例一创建就拥有明确的状态,能够直接参与后续的操作。
简单说,self 代表实例本身,__init__ 方法通过 self.xxx = xxx 的形式,给这个实例 “装上” 各种需要的 “零件”,让它成为一个功能完整的对象。
- 第三部分:def split_sentences(self, text):
def split_sentences(self, text):
"""Support Chinese and English sentence segmentation (handling common abbreviations)"""
"""支持中英文句子分割(处理常见缩写)"""
# Added Chinese punctuation (references 4, 5)
# 使用正则表达式分割句子
# (?<!\b[A-Za-z].) 表示前面不是单字母缩写(如U.S.)
# (?<=[.!?。!?]) 表示在这些标点符号后面
# \s+ 表示一个或多个空白字符
pattern = re.compile(r'(?<!\b[A-Za-z].)(?<=[.!?。!?])\s+')
sentences = [s.strip() for s in pattern.split(text) if s.strip()]
return sentences
这是核心的文本分割处理逻辑,
pattern = re.compile(r'(?<!\b[A-Za-z].)(?<=[.!?。!?])\s+')
- (?<!\b[A-Za-z].)这是否定后顾断言,\b表示单词边界,[A-Za-z]匹配任何一个字母,.匹配一个点号
整体含义:不匹配前面是单个字母后跟点号的位置(如:A.)
目的:避免将缩写,如"U.S.", "Dr."等分开
- (?<=[.!?。!?])这是肯定后顾断言:
整体含义:匹配前面有句号,感叹号,问号的位置
目的 :确保在句子结束标点后进行分割
- \s+:匹配一个或多个空白字符,包括(空格,制表符,换行符)
三种模式混合,就实现了段落颗粒级的分割。
完整工作流程: pattern.split(text):按照模式分割文本,返回分割后的句子列表 只在满足"前面是句子结束标点且不是单字母缩写"的条件下,遇到空白字符时才分割 [s.strip() for s in ... if s.strip()]: 对每个分割后的片段去除前后空白字符(strip()) 只保留非空的句子(if s.strip())
举例理解: 对于文本:"Dr. Smith visited U.S.A. He said hello! Then went home." 正则表达式不会在"Dr."和"U.S.A."后分割(因为否定后顾断言阻止了这种分割) 会在"hello!"后分割(因为满足肯定后顾断言且后面有空格) 结果:["Dr. Smith visited U.S.A.", "He said hello!", "Then went home."]
- 第四部分:生成ID
def generate_id(self, index):
"""根据要求生成ID"""
"""Generate ID according to requirements"""
# 从1开始编号
return f"{self.base_name}{index+1}" # Start numbering from 1
- 第五部分:进行句子和ID的映射,并返回一个包含句子,句子向量,句子toID,IDto句子四个值的字典
def process(self):
# Step 1: Convert PDF to text
# 步骤1:获取文本(传入时已经处理好)
text = self.text
# Step 2: Sentence segmentation and ID mapping
# 步骤2:句子分割和ID映射
sentences = self.split_sentences(text)
for idx, sent in enumerate(sentences):
sent_id = self.generate_id(idx) # 生成ID
self.sentence_to_id[sent] = sent_id # 句子→ID映射
self.id_to_sentence[sent_id] = sent # ID→句子映射
# Step 3: Vector storage
# 步骤3:向量存储 - 使用嵌入模型将所有句子转换为向量
vectors = self.embeddings.embed_documents(sentences)
# 返回包含所有处理结果的字典
return {
"sentences": sentences, # 分割后的句子列表
"vectors": vectors,# 句子的向量表示
"sentence_to_id": self.sentence_to_id,# 句子到ID的映射
"id_to_sentence": self.id_to_sentence# ID到句子的映射
}
split_sentences是第二部分的文本分割函数,将其结果赋给sentences,然后进行ID映射循环。enumerate(sentences)返回 索引-值对的迭代器,例如 [(0, "第一句"), (1, "第二句"), ...]
sent_id = self.generate_id(idx)调用类的generate_id方法,将索引转换为格式化ID,如果self.base_name是"文章A",则生成"文章A1"、"文章A2"等ID
self.sentence_to_id[sent] = sent_id在字典中建立句子到ID的映射,例如:{"这是第一句。": "文章A1", ...}
self.id_to_sentence[sent_id] = sent在字典中建立ID到句子的映射,例如:{"文章A1": "这是第一句。", ...}
为什么需要句子ID映射?
句子ID映射是知识图谱和检索系统中的关键技术,提供了多种重要功能: 唯一标识符: 为每个句子提供简短、唯一的引用方式 避免处理完整的、可能很长的句子文本 双向查找: sentence_to_id允许从句子内容快速找到ID id_to_sentence允许从ID快速恢复原始句子 知识图谱构建: 在知识图谱中,可以用句子ID作为节点或边的属性 例如:实体A和实体B之间的关系可以引用句子ID作为来源证据
总的来说,文本预处理的过程包含了四步:文本/句子分割————ID映射————向量化————返回
- 自行测试
# test_text_processor.py
import json
import os
from src.textPrcess import TextProcessor
# 测试文本(小段落)
test_text = """RAKG是一个结合了知识图谱的RAG系统。它能够有效处理文档,并通过语义分析提取关系。
这个系统分为多个模块。文本处理模块负责分割和向量化。知识图谱模块负责实体和关系提取。"""
def main():
# 1. 基本测试
processor = TextProcessor(test_text, "TEST")
results = processor.process()
print("=== 文本处理结果 ===")
print(f"分割后的句子数量: {len(results['sentences'])}")
print("\n前2个句子:")
for i, sentence in enumerate(results['sentences'][:2]):
print(f"{i + 1}. {sentence}")
print("\n句子ID示例:")
for sentence, sent_id in list(results['sentence_to_id'].items())[:2]:
print(f"ID: {sent_id} -> {sentence[:30]}...")
print(f"\n向量维度: {len(results['vectors'][0])}")
# 添加这一行来调用main函数
if __name__ == "__main__":
main()
输出结果
分割后的句子数量: 2
前2个句子:
1. RAKG是一个结合了知识图谱的RAG系统。它能够有效处理文档,并通过语义分析提取关系。
2. 这个系统分为多个模块。文本处理模块负责分割和向量化。知识图谱模块负责实体和关系提取。
句子ID示例:
ID: TEST1 -> RAKG是一个结合了知识图谱的RAG系统。它能够有效处理文档...
ID: TEST2 -> 这个系统分为多个模块。文本处理模块负责分割和向量化。知识图谱...
向量维度: 1024
这个模式只会在句号、问号或感叹号后面有空白字符的地方分割文本。正则表达式只在满足"句号+空白字符"条件的地方分割,即:
- 第一个段落结束的"提取关系。"后面有换行符(\n是空白字符)
- 没有其他地方满足"句号+空白字符"的条件
如何修改以获得更细粒度的分割?
如果你想在每个句号处分割(无论后面是否有空白字符),可以修改正则表达式:
# 原始版本 - 只在句号后有空白字符处分割
pattern = re.compile(r'(?<!\b[A-Za-z]\.)(?<=[.!?。!?])\s+')
# 修改版本 - 在句号处分割,不管后面是否有空白字符
pattern = re.compile(r'(?<!\b[A-Za-z]\.)([.!?。!?])')
sentences = [s.strip() for s in pattern.split(text) if s.strip() and not s in ['.', '!', '?', '。', '!', '?']]
此次学习需要注意的知识点
- 环境验证
# 在RAKG_example.py顶部添加以下代码进行测试运行
print("Python version:", sys.version)
print("Project root:", project_root)
print("Working directory:", os.getcwd())
2. class类,方法,作用 3. 文本预处理的四个步骤 4. __init__方法的作用 5. 后顾断言分割方法 6. src目录问题 7. 修改分割颗粒度的进一步试验
学习结束后,仍旧需要对项目内部这部分的代码进行默写重构,加深理解。
Gemini-2.5-pro对这篇报告的评价: