Press "Enter" to skip to content

使用大型语言模型(LLM)和潜在狄利克雷分配(LDA)算法进行文档主题提取

使用大型语言模型(LLM)和潜在狄利克雷分配(LDA)算法高效从大型文档中提取主题的指南。

Henry Be 在 Unsplash 上的照片

介绍

我正在开发一个用于与PDF文件聊天的Web应用程序,能够处理超过1000页的大型文档。但在开始与文档进行交互之前,我希望应用程序能给用户一个主要主题的简要摘要,这样就更容易开始交互。

一种方法是使用LangChain对文档进行总结,如其文档所示。然而,问题在于高计算成本和由此带来的经济成本。一个千页文档大约包含25万个单词,每个单词都需要输入到LLM中。而且,结果还必须进一步处理,如使用映射-归约方法。使用gpt-3.5 Turbo和4k上下文的成本估计超过1美元,仅用于摘要。即使使用免费资源,如非官方的HuggingChat API,所需的API调用数量也会是一种滥用。因此,我需要采用不同的方法。

LDA拯救

潜在狄利克雷分配(LDA)算法是这个任务的自然选择。该算法接受一组“文档”(在此上下文中,“文档”指的是一段文本)并返回每个“文档”的主题列表以及与每个主题相关联的单词列表。对于我们的情况,与每个主题相关联的单词列表是重要的。这些单词列表编码了文件的内容,因此可以将它们输入到LLM中以提示进行摘要。我推荐这篇文章详细解释了该算法。

在获得高质量结果之前,有两个关键问题需要解决:选择LDA算法的超参数和确定输出的格式。最重要的超参数是主题数量,因为它对最终结果有最重要的影响。至于输出格式,一个效果不错的格式是嵌套的项目符号列表。在这种格式中,每个主题都表示为一个带有进一步描述主题的子条目的项目符号列表。至于为什么这样做有效,我认为通过使用这种格式,模型可以专注于从列表中提取内容,而不必处理连接词和关系的复杂性。

实现

我在Google Colab中实现了代码。所需的库包括gensim用于LDA,pypdf用于PDF处理,nltk用于单词处理,以及LangChain用于其提示模板和与OpenAI API的接口。

import gensimimport nltkfrom gensim import corporafrom gensim.models import LdaModelfrom gensim.utils import simple_preprocessfrom nltk.corpus import stopwordsfrom pypdf import PdfReaderfrom langchain.chains import LLMChainfrom langchain.prompts import ChatPromptTemplatefrom langchain.llms import OpenAI

接下来,我定义了一个实用函数preprocess,用于辅助处理输入文本。它会删除停用词和短标记。

def preprocess(text, stop_words):    """    对输入文本进行标记化和预处理,删除停用词和短标记。    参数:        text(str):要预处理的输入文本。        stop_words(set):要从文本中删除的停用词集合。    返回:        list:预处理标记的列表。    """    result = []    for token in simple_preprocess(text, deacc=True):        if token not in stop_words and len(token) > 3:            result.append(token)    return result

第二个函数get_topic_lists_from_pdf实现了代码的LDA部分。它接受PDF文件的路径、主题数量和每个主题的单词数量,并返回一个列表。该列表中的每个元素都包含与每个主题相关联的单词列表。在这里,我们将PDF文件的每一页都视为一个“文档”。

def get_topic_lists_from_pdf(file, num_topics, words_per_topic):    """    使用潜在狄利克雷分配(Latent Dirichlet Allocation,LDA)算法从PDF文档中提取主题及其相关词语。    参数:        file(str):用于主题提取的PDF文件路径。        num_topics(int):要发现的主题数量。        words_per_topic(int):每个主题包含的词语数量。    返回:        list:包含num_topics个子列表的列表,每个子列表包含一个主题的相关词语。    """    # 加载pdf文件    loader = PdfReader(file)    # 将每个页面的文本提取到列表中,每个页面被视为一个文档    documents = []    for page in loader.pages:        documents.append(page.extract_text())    # 预处理文档    nltk.download('stopwords')    stop_words = set(stopwords.words(['english', 'spanish']))    processed_documents = [preprocess(doc, stop_words) for doc in documents]    # 创建字典和语料库    dictionary = corpora.Dictionary(processed_documents)    corpus = [dictionary.doc2bow(doc) for doc in processed_documents]    # 构建LDA模型    lda_model = LdaModel(        corpus,         num_topics=num_topics,         id2word=dictionary,         passes=15        )    # 提取主题及其对应的词语    topics = lda_model.print_topics(num_words=words_per_topic)    # 将每个主题的词语列表存储到列表中    topics_ls = []    for topic in topics:        words = topic[1].split("+")        topic_words = [word.split("*")[1].replace('"', '').strip() for word in words]        topics_ls.append(topic_words)    return topics_ls

下一个函数topics_from_pdf调用LLM模型。如前所述,模型被要求将输出格式化为嵌套的项目列表。

def topics_from_pdf(llm, file, num_topics, words_per_topic):    """    根据从PDF文档中提取的主题词语,为LLM生成描述性提示。    该函数接受`get_topic_lists_from_pdf`函数的输出,该输出由每个主题相关的词语列表组成,并生成以目录格式的输出字符串。    参数:        llm(LLM):用于生成响应的大型语言模型(LLM)的实例。        file(str):用于提取主题相关词语的PDF文件路径。        num_topics(int):要考虑的主题数量。        words_per_topic(int):每个主题包含的词语数量。    返回:        str:基于提供的主题词语生成的语言模型生成的响应。    """    # 提取主题并转换为字符串    list_of_topicwords = get_topic_lists_from_pdf(file, num_topics,                                                   words_per_topic)    string_lda = ""    for lst in list_of_topicwords:        string_lda += str(lst) + "\n"    # 创建模板    template_string = '''以简单的句子描述每个双引号包裹的列表的主题,同时写下三个可能的不同子主题。这些列表是主题发现算法的结果。    不要提供引言或结论,仅描述主题时不要提及“主题”一词。    使用以下模板进行响应。    1: <<<(描述主题的句子)>>>    - <<<(描述第一个子主题的短语)>>>    - <<<(描述第二个子主题的短语)>>>    - <<<(描述第三个子主题的短语)>>>    2: <<<(描述主题的句子)>>>    - <<<(描述第一个子主题的短语)>>>    - <<<(描述第二个子主题的短语)>>>    - <<<(描述第三个子主题的短语)>>>    ...    n: <<<(描述主题的句子)>>>    - <<<(描述第一个子主题的短语)>>>    - <<<(描述第二个子主题的短语)>>>    - <<<(描述第三个子主题的短语)>>>    列表: """{string_lda}""" '''    # LLM调用    prompt_template = ChatPromptTemplate.from_template(template_string)    chain = LLMChain(llm=llm, prompt=prompt_template)    response = chain.run({        "string_lda" : string_lda,        "num_topics" : num_topics        })    return response

在上一个函数中,单词列表被转换为字符串。然后,使用来自LangChain的ChatPromptTemplate对象创建了一个提示;请注意,提示定义了响应的结构。最后,函数调用chatgpt-3.5 Turbo模型。返回值是LLM模型给出的响应。

现在,是时候调用这些函数了。我们首先设置API密钥。 这篇文章提供了如何获得API密钥的说明。

openai_key = "sk-p..."llm = OpenAI(openai_api_key=openai_key, max_tokens=-1)

接下来,我们调用topics_from_pdf函数。我选择了主题数量和每个主题的单词数量的值。此外,我选择了一本公共领域的书籍,弗朗茨·卡夫卡的《变形记》进行测试。该文档存储在我的个人驱动器上,并通过使用gdown库进行下载。

!gdown https://drive.google.com/uc?id=1mpXUmuLGzkVEqsTicQvBPcpPJW0aPqdLfile = "./the-metamorphosis.pdf"num_topics = 6words_per_topic = 30summary = topics_from_pdf(llm, file, num_topics, words_per_topic)

结果如下所示:

1: 探索格雷戈尔·桑萨的转变及其对他的家人和房客的影响- 理解格雷戈尔的变形- 分析格雷戈尔的家人和房客的反应- 分析格雷戈尔的转变对他的家庭的影响2: 调查发现格雷戈尔变化的事件- 调查格雷戈尔的家人和房客的最初反应- 分析格雷戈尔的家人和房客的行为- 探索格雷戈尔环境的物理变化3: 分析由于格雷戈尔的转变对他的家人施加的压力- 调查格雷戈尔家庭的经济负担- 调查格雷戈尔家庭的情感和心理影响- 分析由于格雷戈尔的变形而引起的家庭动态的变化4: 调查格雷戈尔的转变的后果- 调查格雷戈尔环境的物理变化- 分析格雷戈尔的家人和房客的反应- 调查格雷戈尔家庭的情感和心理影响5: 探索格雷戈尔的转变对他的家人的影响- 分析格雷戈尔家庭的经济负担- 调查由于格雷戈尔的变形而引起的家庭动态的变化- 调查格雷戈尔家庭的情感和心理影响6: 调查格雷戈尔环境的物理变化- 分析格雷戈尔的家人和房客的反应- 调查格雷戈尔的转变的后果- 探索格雷戈尔的转变对他的家人的影响

输出结果非常不错,而且只需要几秒钟!它正确地提取了这本书的主要思想。

这种方法也适用于技术书籍。例如,大卫·希尔伯特(1899年)的《几何学基础》(也在公共领域中):

1: 分析几何形状及其关系的属性- 探索几何学的公理- 分析角和线段的相等- 调查几何学定理2: 研究有理函数和代数方程的行为- 考察问题的直线和点- 调查函数的系数- 探索定义积分3: 调查数系统的属性- 探索真实群的定义域- 分析等分线段的定理- 考察任意位移的圆4: 考察几何形状的面积- 分析平行线和点- 调查三角形的内容- 考察多边形的度量5: 考察代数几何学的定理- 探索线段的相等- 分析乘法系统- 调查合法定理的有效性6: 调查图形的属性- 考察三角形的平行线- 分析连接边的方程- 考察线段的交点

结论

将LDA算法与LLM结合用于大型文档主题提取可产生良好的结果,同时显著降低成本和处理时间。我们从数百次API调用减少到只有一次,从几分钟减少到几秒钟。

输出的质量在很大程度上取决于其格式。在这种情况下,嵌套的项目列表效果很好。此外,主题数量和每个主题的单词数量对结果的质量也很重要。建议尝试不同的提示、主题数量和每个主题的单词数量,找到适合给定文档的最佳组合。

代码可以在这个链接找到。

感谢阅读。请告诉我您的文档的结果如何。我希望很快能够写关于我在开头提到的应用程序实现的内容。

LinkedIn:Antonio Jimenez Caballero

GitHub:a-jimenezc

Leave a Reply

Your email address will not be published. Required fields are marked *