手扒Github项目文档级知识图谱构建框架RAKG(保姆级)Day2

159 阅读10分钟

Day2主要聚焦该框架的数据预处理阶段,详细深扒数据是如何输入,以及输入后的数据是如何被切片分割的。

  1. 读取文档
  • 代码解读

论文主代码为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

  1. 文本分割
  • 代码解读 这是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具有以下特征:

  1. 封装:将相关函数和数据组合在一起
  2. 实例化:可以创建类的多个实例(对象),每个实例独立
  3. 继承:可以从其他类继承特性
  4. 方法(★):类中的函数成为方法,第一个参数为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.textself.base_nameself.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+')
  1. (?<!\b[A-Za-z].)这是否定后顾断言,\b表示单词边界,[A-Za-z]匹配任何一个字母,.匹配一个点号

整体含义:不匹配前面是单个字母后跟点号的位置(如:A.)

目的:避免将缩写,如"U.S.", "Dr."等分开

  1. (?<=[.!?。!?])这是肯定后顾断言:

整体含义:匹配前面有句号,感叹号,问号的位置

目的 :确保在句子结束标点后进行分割

  1. \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

这个模式只会在句号、问号或感叹号后面有空白字符的地方分割文本。正则表达式只在满足"句号+空白字符"条件的地方分割,即:

  1. 第一个段落结束的"提取关系。"后面有换行符(\n是空白字符)
  2. 没有其他地方满足"句号+空白字符"的条件

如何修改以获得更细粒度的分割?

如果你想在每个句号处分割(无论后面是否有空白字符),可以修改正则表达式:

# 原始版本 - 只在句号后有空白字符处分割
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 ['.', '!', '?', '。', '!', '?']]

此次学习需要注意的知识点

  1. 环境验证
# 在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对这篇报告的评价:

image.png

image.png

image.png