基于LLM的文档提取和分析

文档解析是分析文档内容(非结构化或半结构化)以提取特定信息或将内容转换为更结构化格式的过程。文档解析的目标是将文档分解为其组成部分并解释这些部分。文档解析对于处理大量各种格式的数据且需要自动提取数据的组织非常有用。文档解析在业务中可能有许多有用的用例,例如发票处理、法律合同分析、来自多个来源的客户反馈分析和财务报表分析等。

在大型语言模型 (LLM) 出现之前,文档解析是使用预定义规则(例如正则表达式 (Regex))完成的。但是,这些规则缺乏灵活性,并且仅限于预定义的结构。现实世界中的文档通常存在不一致之处,并且不遵循固定的结构或格式。这就是 LLM 可能具有巨大潜力的地方,可以从半结构化或非结构化文档中提取特定信息以供进一步分析。

在本文中,我将通过一个实际示例来解释如何使用 LLM 自动从半结构化和非结构化文档中提取所需信息,然后分析这些信息。本次实验中使用的文档包括我们 FAIR(芬兰 AI 区域)项目中对公司的 AI 咨询反馈。这些文档包含有关公司 AI 成熟度、当前解决方案、AI 集成需求、AI 集成的未来计划、技术专长、寻求 AI 咨询的服务以及 AI 专家的详细建议的数据。从这些文档中提取关键信息并进行后续分析可以提供有关各个行业采用 AI 的最新趋势、公司在实施 AI 解决方案时面临的具体需求和挑战以及当前需求的 AI 技术类型的有用见解。

下图显示了使用 LLM 进行文档解析和后续分析的整个工作流程。

使用 LLM 和数据分析进行文档解析的工作流程(

实现整个工作流程的代码可在 GitHub 上找到。

让我们逐一介绍这些步骤。

1、文本提取

本例中使用的文档包括我们在咨询会议后向公司提供的 AI 咨询反馈。这些公司包括初创公司和成熟公司,他们希望将 AI 融入其业务或希望改进其现有的 AI 解决方案。反馈文档是半结构化文档,其格式如下所示。由于隐私限制,本文档中的名称和其他信息已更改。

我们的 AI 咨询反馈示例文档

AI 专家针对每个领域提供分析。然而,面对数百份这样的文档,从数据中提取见解成为一项艰巨的任务。为了深入了解这些数据,需要将其转换为简洁、结构化的格式,以便使用现有的统计或机器学习方法进行分析。手动执行此转换不仅费力且耗时,而且容易出错。

除了文档中显而易见的信息(例如公司名称、咨询日期和所涉及的专家)之外,我还旨在提取具体细节以供进一步分析。这些包括每家公司所处的主要行业或领域、当前提供的解决方案的简要描述、AI 主题、公司类型、AI 成熟度级别、目标以及建议的简要摘要。需要对与每个字段相关的详细文本执行此提取。此外,反馈模板随着时间的推移而发展,导致文档格式不一致。

在我们讨论从文档中提取文本之前,请注意,需要安装以下库才能运行本文使用的完整代码。

# Install the required libraries
!pip install tqdm  # For displaying a progress bar for document processing
!pip install requests  # For making HTTP requests
!pip install pandas  # For data manipulation and analysis
!pip install python-docx  # For processing Word documents
!pip install plotly  # For creating interactive visualizations
!pip install numpy  # For numerical computations
!pip install scikit-learn  # For machine learning algorithms and tools
!pip install matplotlib  # For creating static, animated, and interactive plots
!pip install openai  # For interacting with the OpenAI API
!pip install seaborn  # For statistical data visualization

以下代码使用 python-docx 库从文档(.docx 格式)中提取文本。从所有格式(包括段落、表格、页眉和页脚)中提取文本非常重要。

def extract_text_from_docx(docx_path: str):
    """
    Extract text content from a Word (.docx) file.
    """
    doc = docx.Document(docx_path)
    full_text = []

    # Extract text from paragraphs
    for para in doc.paragraphs:
        full_text.append(para.text)

    # Extract text from tables
    for table in doc.tables:
        for row in table.rows:
            for cell in row.cells:
                full_text.append(cell.text)

    # Extract text from headers and footers 
    for section in doc.sections:
        header = section.header
        footer = section.footer
        for para in header.paragraphs:
            full_text.append(para.text)
        for para in footer.paragraphs:
            full_text.append(para.text)

    return '\n'.join(full_text).strip()

2、设置 LLM 提示

我们需要指导 LLM 如何从文档中提取所需信息。此外,我们需要解释要提取的每个感兴趣字段的含义,以便它可以从文档中提取语义匹配的信息。这尤其重要,因为由一个或多个单词组成的必填字段可以以多种方式解释。例如,我们需要解释“目标”的含义,它基本上是指公司对 AI 集成的计划或它希望如何推进其当前解决方案。因此,为此目的制定正确的提示非常重要。

我在系统提示中设置了说明来指导 LLM 的行为。输入提示包含 LLM 要处理的数据。系统提示如下所示。

# System prompt with extraction instructions
system_message = """
You are an expert in analyzing and extracting information from the feedback forms written by AI experts after AI advisory sessions with companies.  
Please carefully read the provided feedback form and extract the following 15 key information. Make sure that the key names are exactly the same as 
given below. Do not create any additional key names other than these 15. 
Key names and their descriptions:
1. Company name: name of the company seeking AI advisory
2. Country: Company's country [output 'N/A' if not available]
3. Consultation Date [output 'N/A' if not available]
4. Experts: persons providing AI consultancy [output 'N/A' if not available]
5. Consultation type: Regular or pop-up [output 'N/A' if not available]
6. Area/domain: Field of the company’s operations. Some examples: healthcare, industrial manufacturing, business development, education, etc. 
7. Current Solution: description of the current solution offered by the company. The company could be currently in ideation phase. Some examples of ‘Current Solution’ field include i) Recommendation system for cars, houses, and other items, ii) Professional guidance system, iii) AI-based matchmaking service for educational peer-to-peer support. [Be very specific and concise]
8. AI field: AI's sub-field in use or required. Some examples: image processing, large language models, computer vision, natural language processing, predictive modeling, speech recognition, etc. [This field is not explicitly available in the document. Extract it by the semantic understanding of the overall document.]
9. AI maturity level: low, moderate, high [output 'N/A' if not available].
10. Company type: ‘startup’ or ‘established company’
11. Aim: The AI tasks the company is looking for. Some examples: i) Enhance AI-driven systems for diagnosing heart diseases, ii) to automate identification of key variable combinations in customer surveys, iii) to develop AI-based system for automatic quotation generation from engineering drawings, iv) to building and managing enterprise-grade LLM applications. [Be very specific and concise]
12. Identified target market: The targeted customers. Some examples: healthcare professionals, construction firms, hospitality, educational institutions, etc. 
13. Data Requirement Assessment: The type of data required for the intended AI integration? Some examples: Transcripts of therapy sessions, patient data, textual data, image data, videos, etc. 
14. FAIR Services Sought: The services expected from FAIR. For instance, technical advice, proof of concept. 
15. Recommendations: A brief summary of the recommendations in the form of key words or phrase list. Some examples: i) Focus on data balance, monitor for bias, prioritize transparency, ii) Explore machine learning algorithms, implement decision trees, gradient boosting. [Be very specific and concise] 
Guidelines:
- Very important: do not make up anything. If the information of a required field is not available, output ‘N/A’ for it.
- Output in JSON format. The JSON should contain the above 15 keys.
"""

强调 LLM 应该关注什么很重要。例如,要提取的关键元素的数量,使用与指定完全相同的字段名称,并且如果不可用则不要编造任何信息。每个字段的解释和所需信息的一些示例(如果可能)也很重要。值得一提的是,第一次尝试可能无法制作出最佳提示。

3、处理文档

处理文档是指将数据发送到 LLM 进行解析。我使用 OpenAI 的 gpt-4o-mini 模型进行文档解析,这是一种经济实惠且智能的小型模型,适用于快速、轻量级的任务。GPT-4o mini 比 GPT-3.5 Turbo 更便宜、更强大。但是,也可以为此目的测试 LlamaMistral Phi-3 等开放式 LLM 的轻量级版本。

以下代码遍历目录及其子目录以查找 AI 咨询文档(.docx 格式),从每个文档中提取文本,并通过 API 调用将文档发送给 gpt-4o-mini。

def process_files(directory_path: str, api_key: str, system_message: str):
    """
    Process all .docx files in the given directory and its subdirectories,
    send their content to the LLM, and store the JSON responses.
    """
    json_outputs = []
    docx_files = []

    # Walk through the directory and its subdirectories to find .docx files
    for root, dirs, files in os.walk(directory_path):
        for file in files:
            if file.endswith(".docx"):
                docx_files.append(os.path.join(root, file))

    if not docx_files:
        print("No .docx files found in the specified directory or sub-directories.")
        return json_outputs

    # Iterate through all .docx files in the directory with a progress bar
    for file_path in tqdm(docx_files, desc="Processing files...", unit="file"):
        filename = os.path.basename(file_path)
        extracted_text = extract_text_from_docx(file_path)
        # Prepare the user message with the extracted text
        input_message = extracted_text

        # Prepare the API request payload
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {api_key}"
        }
        payload = {
            "model": "gpt-4o-mini",
            "messages": [
                {"role": "system", "content": system_message},
                {"role": "user", "content": input_message}
            ],
            "max_tokens": 2000,
            "temperature": 0.2
        }

        # Send the request to the LLM API
        response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload)

        # Extract the JSON response
        json_response = response.json()
        content = json_response['choices'][0]['message']['content'].strip("```json\n").strip("```")
        parsed_json = json.loads(content)

        # Normalize the parsed JSON output
        normalized_json = normalize_json_output(parsed_json)

        # Append the normalized JSON output to the list
        json_outputs.append(normalized_json)

    return json_outputs

在调用的有效负载中,我将最大令牌数 ( max_tokens) 设置为 2000,以容纳输入/输出令牌。我设置了一个相对较低的温度 (0.2),这样 LLM 就不会具有这项任务不需要的高创造力。高温可能会导致幻觉,LLM 可能会发明新的信息。

LLM 的响应在 JSON 对象中接收,并进一步解析和规范化,如下一节所述。

4、解析 LLM 输出

如上面的代码所示,API 的响应在 JSON 对象 ( parsed_json) 中接收,并使用以下函数进一步规范化。

def normalize_json_output(json_output):
    """
    Normalize the keys and convert list values to comma-separated strings.
    """
    normalized_output = {}
    for key, value in json_output.items():
        normalized_key = key.lower().replace(" ", "_")
        if isinstance(value, list):
            normalized_output[normalized_key] = ', '.join(value)
        else:
            normalized_output[normalized_key] = value
    return normalized_output

此函数通过将 JSON 对象的键转换为小写并用下划线替换空格来标准化 JSON 对象的键。此外,它还将任何列表值转换为逗号分隔的字符串,使数据更易于使用和分析。

规范化的 JSON 对象 ( json_outputs) 包含从所有文档中提取的关键信息,最终保存到 Excel 文件中。

def save_json_to_excel(json_outputs, output_file_path: str):
    """
    Save the list of JSON objects to an Excel file with a SNO. column.
    """
    # Convert the list of JSON objects to a DataFrame
    df = pd.DataFrame(json_outputs)

    # Add a Serial Number (SNO.) column
    df.insert(0, 'SNO.', range(1, len(df) + 1))

    # Ensure all columns are consistent and save the DataFrame to an Excel file
    df.to_excel(output_file_path, index=False)

Excel 文件的快照如下所示。LLM 驱动的解析生成了与必填字段相关的精确信息。快照中的“N/A”表示文档中不可用的数据(旧反馈模板缺少此信息)。

包含从所有 AI 咨询反馈文档中提取的信息的 Excel 文件

最后,下面给出了调用所有上述函数的代码。请注意,运行此代码需要 OpenAI 的 API 密钥。

# Directory containing files
directory_path = 'Documents'
# API key for GPT-4-mini
api_key = 'YOUR_OPENAI_API_KEY'
# Process files and get the JSON outputs
json_outputs = process_files(directory_path, api_key, system_message)

if json_outputs:
    # Save the JSON outputs to an Excel file
    output_file_path = 'processed-gpt-o-mini.xlsx'
    save_json_to_excel(json_outputs, output_file_path)
    print(f"Processed data has been saved to {output_file_path}")
else:
    print("No .docx file found.")

我还解析了相同文档的一些非结构化版本。这是相同 AI 反馈的非结构化版本的快照。由于隐私限制,此版本中的名称和重要细节已更改。

AI 咨询反馈的非结构化版本快照

解析提供了同样精确和准确的信息。解析结果的快照如下所示。

显示 AI 咨询反馈非结构化版本解析的快照

5、执行数据分析

现在我们有了一个结构化的文档,我们可以对这些数据进行几项分析。甚至可以进一步使用 LLM 来建议几项分析,甚至可以使用给定的数据进行分析和/或帮助编写分析代码。例如,我快速进行了以下两项分析,以找到公司的 AI 成熟度水平分布和公司类型分布。以下代码生成了对这些分布的视觉洞察。

import pandas as pd
import plotly.express as px

# Load the dataset
file_path = 'processed-gpt-o-mini.xlsx' # Update this to match your file path
data = pd.read_excel(file_path)

# Convert fields to lowercase
data['ai_maturity_level'] = data['ai_maturity_level'].str.lower()
data['company_type'] = data['company_type'].str.lower()

# Plot for AI Maturity Level
fig_ai_maturity = px.bar(data, 
                         x='ai_maturity_level', 
                         title="AI Maturity Level Distribution",
                         labels={'ai_maturity_level': 'AI Maturity Level', 'count': 'Number of Companies'})

# Update layout for AI Maturity Level plot
fig_ai_maturity.update_layout(
    xaxis_title="AI Maturity Level",
    yaxis_title="Number of Companies",
    xaxis={'categoryorder':'total descending'},  # Order bars by descending number of companies
    yaxis=dict(type='linear'),
    showlegend=False
)

# Display the AI Maturity Level figure
fig_ai_maturity.show()

# Plot for Company Type
fig_company_type = px.bar(data, 
                          x='company_type', 
                          title="Company Type Distribution",
                          labels={'company_type': 'Company Type', 'count': 'Number of Companies'})

# Update layout for Company Type plot
fig_company_type.update_layout(
    xaxis_title="Company Type",
    yaxis_title="Number of Companies",
    xaxis={'categoryorder':'total descending'},  # Order bars by descending number of companies
    yaxis=dict(type='linear'),
    showlegend=False
)

# Display the Company Type figure
fig_company_type.show()

以下是此代码生成的图表。

公司按其类型分布
寻求 AI 咨询的公司的 AI 成熟度水平分布

可以对领域/领域、当前解决方案、AI 领域、目标市场和专家建议进行进一步分析,以找到这些领域的主要主题或集群。 为了演示,我仅对领域/领域进行了聚类,以找到这些公司运营的主要行业。

为此,我执行了以下步骤。

  • 使用 OpenAI 的text-embedding-3-small模型计算“领域/领域”字段中文本的嵌入。 或者,也可以使用开放嵌入模型,例如 all-MiniLM-L12-v2
  • 将 K-means 聚类算法应用于嵌入,并尝试使用不同数量的聚类以找到最佳聚类。这是通过计算每个聚类结果的 Silhouette 分数来评估聚类的质量来完成的。选择 Silhouette 分数最高的聚类号作为最佳数量。
  • 使用最佳数量的聚类对数据进行聚类。
  • 将聚类发送到 gpt-4o-mini 模型,以根据聚类内所有数据点的语义相似性为每个聚类分配标签。
  • 使用标记的聚类来表示寻求 AI 咨询的公司所属的主要行业。

以下代码使用 OpenAI 的 text-embedding-3-smalle 嵌入模型计算嵌入。

def fetch_embeddings(texts, filename):
    # Check if the embeddings file already exists
    if os.path.exists(filename):
        print(f"Loading embeddings from {filename}...")
        with open(filename, 'rb') as f:
            embeddings = pickle.load(f)
    else:
        print("Computing embeddings...")
        embeddings = []
        for text in texts:
            embedding = client.embeddings.create(input=[text], model="text-embedding-3-small").data[0].embedding
            embeddings.append(embedding)
        embeddings = np.array(embeddings)
        # Save the embeddings to a file for future use
        print(f"Saving embeddings to {filename}...")
        with open(filename, 'wb') as f:
            pickle.dump(embeddings, f)
    return embeddings

计算嵌入后,以下代码片段使用计算出的嵌入进行 k 均值聚类,找到最佳聚类数。在计算嵌入之前,先计算区域/域字段中的唯一名称;但是,跟踪原始索引以供后续分析也很重要。区域/域字段中的唯一名称包含在 deduplicated_domains 列表中。

# Load the dataset
file_path = 'C:/Users/h02317/Downloads/processed-gpt-o-mini.xlsx'
data = pd.read_excel(file_path)

# Extract the "area/domain" field and process the data
area_domain_data = data['area/domain'].dropna().tolist()

# Deduplicate the data while keeping track of the original indices
deduplicated_domains = []
original_to_dedup = []
for item in area_domain_data:
    item_lower = item.strip().lower()
    if item_lower not in deduplicated_domains:
        deduplicated_domains.append(item_lower)
    original_to_dedup.append(deduplicated_domains.index(item_lower))
# Fetch embeddings for all deduplicated domain data points
embeddings = fetch_embeddings(deduplicated_domains, filename="all_domains_embeddings_2.pkl")

# Determine the optimal number of clusters using the silhouette score
silhouette_scores = []
K_range = list(range(2, len(deduplicated_domains)))  # Testing between 2 and the total number of unique domains
print('Finding optimal number of clusters')
for k in K_range:
    kmeans = KMeans(n_clusters=k, random_state=42)
    kmeans.fit(embeddings)
    score = silhouette_score(embeddings, kmeans.labels_)
    silhouette_scores.append(score)

# Find the optimal number of clusters
optimal_k = K_range[np.argmax(silhouette_scores)]
print(f'Optimal number of clusters: {optimal_k}')
# Plot the silhouette scores
plot_silhouette_scores(K_range, silhouette_scores, optimal_k)

下图显示了所有簇的 Silhouette 分数和最佳簇数。

Silhouette 分数,通过使用不同数量的簇的 k 均值聚类计算得出。最高 Silhouette 分数代表最佳簇数 (n=11)

使用以下代码片段,使用最佳簇数 ( optimal_k) 进行聚类。具有唯一数据点的簇存储在 dedup_clusters 中。这些簇也映射到原始数据点,并存储在 original_clusters 中以供以后使用。

# Perform k-means clustering with the optimal number of clusters
kmeans = KMeans(n_clusters=optimal_k, random_state=42)
kmeans.fit(embeddings)
dedup_clusters = kmeans.labels_

# Map clusters back to the original data points
original_clusters = [dedup_clusters[idx] for idx in original_to_dedup]

original_clusters 被发送到 gpt-4o-mini 模型以分配标签。以下代码片段显示了随有效负载发送的系统和输入提示。输出以 JSON 格式接收。对于​​此任务,选择较高的温度 (0.7),以便模型可以利用一些创造力来分配合适的标签。

# Function to label clusters using GPT-4-o-mini
def label_clusters_with_gpt(clusters, api_key):
    # Prepare the input for GPT-4
    cluster_descriptions = []
    for cluster_id, data_points in clusters.items():
        cluster_descriptions.append(f"Cluster {cluster_id}: {', '.join(data_points)}")

    # Prepare the system and input messages
    system_message = "You are a helpful assistant"
    input_message = (
        "Please label each of the following clusters with a concise, specific label based on the semantic similarity "
        "of the data points within each cluster."
        "Output in a JSON format where the keys are cluster numbers and the values are cluster labels."
        "\n\n" + "\n".join(cluster_descriptions)
    )
    
    # Set up the request payload
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {api_key}"
    }
    payload = {
        "model": "gpt-4o-mini",  # Model name
        "messages": [
            {"role": "system", "content": system_message},
            {"role": "user", "content": input_message}
        ],
        "max_tokens": 2000,
        "temperature": 0.7
    }
    
    # Send the request to the API
    response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload)
    
    # Extract and parse the response
    if response.status_code == 200:
        response_data = response.json()
        response_text = response_data['choices'][0]['message']['content'].strip()
        try:
            # Ensure that the JSON is correctly formatted
            response_text = response_text.replace("```json", "").replace("```", "").strip()
            cluster_labels = json.loads(response_text)
        except json.JSONDecodeError as e:
            print("Failed to parse JSON:", e)
            cluster_labels = {}
        
        return cluster_labels
    else:
        print(f"Request failed with status code {response.status_code}")
        print("Response Body:", response.text)
        return None

以下函数 clusters_to_dataframe 将 gpt-4o-mini 的 JSON 输出转换为结构化的 pandas DataFrame。它通过将每个集群的标签、相关数据点和原始数据点的数量转换为表格格式来组织标记集群。该函数确保每个集群都通过其编号、标签和内容清晰地​​标识,从而更轻松地分析和可视化聚类结果。生成的数据框按集群编号排序,提供干净、有序的数据视图。

# Function to convert the JSON labeled clusters into a DataFrame
def clusters_to_dataframe(cluster_labels, clusters, original_clustered_data):
    data = {"Cluster Number": [], "Label": [], "Data Points": [], "Original Data Points": []}
    
    # Iterate through the cluster labels
    for cluster_num, label in cluster_labels.items():
        cluster_num = int(cluster_num)  # Convert cluster number to integer
        data["Cluster Number"].append(cluster_num)
        data["Label"].append(label)
        data["Data Points"].append(repr(clusters[cluster_num]))  # Use repr to retain the original list format
        data["Original Data Points"].append(len(original_clustered_data[cluster_num]))  # Count original data points
    
    # Convert to DataFrame and sort by "Cluster Number"
    df = pd.DataFrame(data)
    df = df.sort_values(by='Cluster Number').reset_index(drop=True)
    return df

该函数返回的最终数据框如下所示。

gpt-4o-mini 为每个集群分配的标签。这些标签代表公司所属的主要行业。“原始数据点”代表每个行业的公司数量

以下代码片段绘制了每个行业的公司数量的可视化图。

'''draw visualization'''
import plotly.express as px

# Use the "Original Data Points" column for the number of companies
df_labeled_clusters['No. of companies'] = df_labeled_clusters['Original Data Points']

# Create the Plotly bar chart
fig = px.bar(df_labeled_clusters, 
             x='Label', 
             y='No. of companies', 
             title="Number of Companies per Sector", 
             labels={'Label': 'Sectors', 'No. of companies': 'No. of Companies'})

# Update layout for better visibility
fig.update_layout(
    xaxis_title="Sectors",
    yaxis_title="No. of Companies",
    xaxis={'categoryorder':'total descending'},  # Order bars by descending number of companies
    yaxis=dict(type='linear'),
    showlegend=False
)

# Display the figure
fig.show()
各行业/集群内企业数量

6、结束语

在本文中,我演示了如何使用 LLM 将数据从半结构化和非结构化文档转换为结构化格式。随后,可以使用传统的机器学习和统计方法进一步分析结构化数据。

为了便于说明,我仅关注对“区域/领域”字段进行聚类,以确定公司运营的主要行业。但是,这种方法可以扩展到各种其他数据领域,例如确定公司当前提供的解决方案类型、分析正在使用或正在考虑的 AI 技术、对“目标”字段进行聚类以发现业务中的主要 AI 趋势、检查“数据要求评估”字段以了解现有数据需求,以及对“公平服务寻求”字段进行聚类以查找公司的关键咨询兴趣。此外,还可以使用此技术分析包含跨不同领域的丰富咨询数据的“建议”字段。

由于隐私限制,我无法共享原始数据集。但是,我在 GitHub 上提供了可用于运行和测试整个代码的示例文档。

该方法和代码并不局限于本演示中使用的特定文档。代码和 LLM 参数(尤其是提示)可以适用于其他类型的文档。代码还可以修改为解析来自图像(例如扫描的发票、财务和医疗保健文件等)的数据并进行后续分析。


原文链接:LLM-Powered Parsing and Analysis of Semi-Structured & Unstructured Documents

汇智网翻译整理,转载请标明出处