使用自定义Spacy管道从PDF中提取任何命名实体

2023年12月27日 由 alex 发表 211 0

便携式文档格式(PDF)作为分发内容的主导文件格式,尤其在学术、科学、企业和法律界。其强大之处在于保持文档的原始布局和结构。然而,当涉及到数据的检索、分析或操控时,PDF文件带来了一定的挑战。这正是文本提取和结构化的重要性变得明显的地方。在本文中,我们将深入探索构建一个专门设计用于从PDF文件中提取文本的流程,并使用自然语言处理(NLP)技术进行后续处理。关键的是,这个流程纳入了命名实体识别(NER),这是一个专注于识别和分类文本中关键信息元素的NLP子领域,比如人名、组织名称、地点、时间表达、数量、货币价值等。NER在将非结构化文本转换为结构化数据中发挥了关键作用,使得信息提取和分析更加有效。我们将探索这个流程在提取和识别这些命名实体的真实复杂情景中的表现,从而证明它在实际应用中的实用性和有效性。


需求


在开始这个项目之前,你应该确保你的机器上安装了Python 3.8或更高版本。在这项工作中,我们将需要两个Python库:


  • Pdfminer — 用于执行布局分析和数据解析的库;
  • Spacy — 是一个用于工业级自然语言处理(NLP)应用的库,包括命名实体识别(NER)。


在安装所需库之前,请确保你的pip、setuptools和wheel是最新的。为此,运行以下命令:


pip install -U pip setuptools wheel


要安装所述库,请运行以下命令:


pip install pdfminer.six spacy


此外,你需要下载一个Spacy模型:


python -m spacy download en_core_web_sm


文本和实体提取


首先,我们需要导入项目所需的所有必要库。


from pdfminer.high_level import extract_pages
from pdfminer.layout import LTTextContainer
from tqdm import tqdm
import re


我们有一个顶级函数process_document,它需要一个PDF文档的路径,一个具体的页码,我们将要处理它来提取文本。


def process_document(pdf_path, page_ids=None):
   extracted_pages = extract_pages(pdf_path, page_numbers=page_ids)

   page2content = {}

   # Process each extracted page
   for extracted_page in tqdm(extracted_pages):
       page_id = extracted_page.pageid
       content = process_page(extracted_page)
       page2content[page_id] = content

   return page2content


如果你需要解析所有页面,可以将 page_ids 设置为 None。该函数返回一个字典,字典中的键表示页面编号,值是一个字符串值,包含了从页面中以正确的顺序提取的文本信息。


process_page 函数解析整个 PDF 页面,提取文本内容;它通过迭代一个按位置排序的页面高级元素的列表,并将其转换成字符串格式。


def process_page(extracted_page):
   content = []

   # Get a sorted list of elements based on 
                # their Y-coordinate in reverse order
   elements = [element for element in extracted_page._objs]
   elements.sort(key=lambda a: a.y1, reverse=True)

   for i, element in enumerate(elements):
       # Extract text if the element is a text container 
       # and text extraction is enabled
       if isinstance(element, LTTextContainer):
           line_text = extract_text_and_normalize(element)
           content.append(line_text)
   # Combine and clean up the extracted content
   content = re.sub('\n+', '\n', ''.join(content))
   return content


为了让从PDFs中提取的文本对机器和人类更易读,我们应该恰当地处理缩进。实际上,我们需要确保没有换行符切割未完成的句子。这是非常关键的,否则我们可能会给语言模型添加一些偏差,那么最终结果将会更差。


def extract_text_and_normalize(element):
   # Extract text from line and split it with new lines
   line_texts = element.get_text().split('\n')
   norm_text = ''
   for line_text in line_texts:
       line_text=line_text.strip()
       # empty strings after striping convert to newline character
       if not line_text:
           line_text = '\n'
       else:
           line_text = re.sub('\s+', ' ', line_text)
           # if the last character is not a letter or number,
                                # add newline character to a line
           if not re.search('[\w\d\,\-]', line_text[-1]):
               line_text+='\n'
           else:
               line_text+=' '
       # concatenate into single string
       norm_text+=line_text
   return norm_text


实体提取


命名实体识别(NER)已成为许多自然语言处理(NLP)应用的基础任务。它涉及到复杂的过程,即在文本中检测特定实体,并将它们归类到预定义的类别中。当你需要分析大量文本并挑选出你最感兴趣的特定信息时,NER可以很有用。它是信息提取管道的第一阶段之一,旨在从原始文本构建知识图谱。此外,NER还可以用于内容的筛选和分类。与将整个文档或文本块分类到广泛主题的标准文本分类不同,NER更深入地提取和分类个人、组织、地点以及其他预定义类别的单个实体。所以,让我们使用Spacy模型,在真实数据上实施NER管道。


在我们的案例中,我们将处理制药公司的财务报告,我们的任务是提取有关该公司顶级产品的财务信息。要下载报告,请运行以下命令:


!curl "https://s28.q4cdn.com/781576035/files/doc_financials/2022/ar/PFE-2022-Form-10K-FINAL-(without-Exhibits).pdf" > pfizer-report.pdf"https://s28.q4cdn.com/781576035/files/doc_financials/2022/ar/PFE-2022-Form-10K-FINAL-(without-Exhibits).pdf" > pfizer-report.pdf


为了简化流程,我们将放置此类信息所在的页面:


pdf_path = 'pfizer-report.pdf'
page2content = process_document(pdf_path, page_ids=[9])


首先,我们需要初始化NLP对象并排除所有不必要的管道,这将显著加快管道的工作速度:


import spacy
nlp = spacy.load("en_core_web_sm", disable=["tok2vec", "tagger", "parser", "attribute_ruler", "lemmatizer"])


让我们使用displacy来可视化Spacy模型的工作:


from spacy import displacy
text = page2content[1]
doc = nlp(text)
displacy.serve(doc, style="ent")


这是Spacy管道结果:


10


对于这样一个小型模型来说,这是一个相当不错的结果,不过,模型错误地将药品名称匹配为了人名。遗憾的是,我们当前使用的模型不支持识别药物;然而,这对于所有训练在固定实体类集合上的监督模型而言都是一个问题。在现实生活案例中,我们处理着大量不同的实体,能够在运行时指定它们是至关重要的。


让我们创建一个使用零样本 NER API 的自定义 Spacy 管道。


import requests
from spacy.language import Language
from spacy.tokens import Span
@Language.factory("knowledgator_ner")
class KnowledgatorNERComponent:
    def __init__(self, nlp, name, API_KEY, labels_to_add, labels_to_exclude):
        self.url = "https://zero-shot-ner.p.rapidapi.com/token_searcher/ner"
        self.headers = {
            "content-type": "application/json",
            "X-RapidAPI-Key":API_KEY,
            "X-RapidAPI-Host": "zero-shot-ner.p.rapidapi.com"
        }
        self.labels_to_add = labels_to_add
        self.labels_to_exclude = set(labels_to_exclude)
    def get_positions(self, doc):
        start2token_id = {}
        end2token_id = {}
        for tok in doc:
            start = tok.idx
            start2token_id[start] = tok.i
            end = len(tok.text) + tok.idx
            end2token_id[end] = tok.i+1
        return start2token_id, end2token_id
    
    def filter_predefined_ents(self, custom_ents, predefined_ents):
        filtered_ents = []
        custom_ents_pos = [(ent.start, ent.end) for ent in custom_ents]
        for ent in predefined_ents:
            if ent.label_ in self.labels_to_exclude:
                continue
            overlap = False
            for cust_ent_start, cust_ent_end in custom_ents_pos:
                if (ent.start < cust_ent_end and ent.end > cust_ent_start):
                    overlap = True
                    break
            if not overlap:
                filtered_ents.append(ent)
        return filtered_ents
    def __call__(self, doc):
        try:
            payload = {"text": doc.text, "labels": self.labels_to_add}
            entities = requests.post(self.url, json=payload, headers=self.headers).json()['entities']
        except:
            return doc
        start2token_id, end2token_id = self.get_positions(doc)
        spans = []  # keep the spans for later so we can merge them afterwards
        for ent in entities:
            # Generate Span representing the entity & set label
            ent_start = ent['start']
            ent_end = ent['end']
            if ent_start in start2token_id:
                start = start2token_id[ent_start]
            else:
                continue
            if ent_end in end2token_id:
                end = end2token_id[ent_end]
            else:
                continue
            entity = Span(doc, start, end, label=ent['entity'].upper())
            spans.append(entity)
        # Replacing overlaping entities
        filtered_ents = self.filter_predefined_ents(spans, doc.ents)
        doc.ents = spans+filtered_ents
        return doc  # don't forget to return the Doc!
labels_to_add = ['drug', 'pharma company', 'disease']
labels_to_exclude = ['WORK_OF_ART', 'NORP', 'PERSON', 'ORG']
API_KEY = "your_api_key"
nlp.add_pipe("knowledgator_ner", config={"API_KEY": API_KEY, "labels_to_add": labels_to_add, "labels_to_exclude": labels_to_exclude})


你需要输入你的 API 密钥,你可以通过在 RapidAPI 上注册并订阅我们的 API 来获得。此外,你需要设置一个自定义标签列表,你需要添加的标签越多,系统处理所需的时间就越长。还可以指定你想排除的原始标签。如果原始标签可能干扰我们的零射击命名实体识别(NER)的结果,或者如果 Spacy 模型表现不佳,这会很有帮助。


让我们可视化自定义 NER 管道的结果。


11


文章来源:https://medium.com/@knowledgrator/extract-any-named-entities-from-pdf-using-custom-spacy-pipeline-9fd0af2c3e13
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
写评论取消
回复取消