尽管大型模型在生成式问答领域已经取得了显著的成果,但因为大量数据是私有的,这些模型的训练和微调成本依旧高得惊人。因此,使用RAG(Retrieval-Augmented Generation)方法逐渐成为实现应用的一种关键途径。然而,对文档进行精确的chunk划分却是一项挑战。现实中,多数专业文档以PDF格式保存,若解析精度不高,将严重影响专业问答系统的性能。
本文旨在深入探讨PDF文档的智能解析与结构化技术,助您在采用RAG策略的旅途中破浪前行。
一、PDF结构化技术链路供参考
二、可编辑PDF文档的结构化技术方案
PDF解析工具:
可编辑PDF文档可以使用PDF解析工具进行解析,优劣势对比如下:
PDF解析后得到的文本数据丢失了段落信息,需要重新划分和重组段落。下面我们将探讨训练语义段落划分模型的策略,并介绍一款现成的开源分段模型工具。
语义分段训练策略:
经过pdf解析工具处理后,原始文档的段落信息会丢失,需要重新划分和重组段落。接下来将介绍一种语义分段模型的训练方法和一个开源分段模型。
语义分段训练策略的步骤流程如下:
开源模型示例:
from modelscope.outputs import OutputKeys``from modelscope.pipelines import pipeline``from modelscope.utils.constant import Tasks`` ``p = pipeline(` `task=Tasks.document_segmentation,` `model='damo/nlp_bert_document-segmentation_chinese-base')`` ``result = p(documents='移动端语音唤醒模型,检测关键词为“小云小云”。模型主体为4层FSMN结构,使用CTC训练准则,参数量750K,适用于移动端设备运行。模型输入为Fbank特征,输出为基于char建模的中文全集token预测,测试工具根据每一帧的预测数据进行后处理得到输入音频的实时检测结果。模型训练采用“basetrain + finetune”的模式,basetrain过程使用大量内部移动端数据,在此基础上,使用1万条设备端录制安静场景“小云小云”数据进行微调,得到最终面向业务的模型。后续用户可在basetrain模型基础上,使用其他关键词数据进行微调,得到新的语音唤醒模型,但暂时未开放模型finetune功能。')`` ``print(result[OutputKeys.TEXT])``# 输出``'''` `移动端语音唤醒模型,检测关键词为“小云小云”。模型主体为4层FSMN结构,使用CTC训练准则,参数量750K,适用于移动端设备运行。模型输入为Fbank特征,输出为基于char建模的中文全集token预测,测试工具根据每一帧的预测数据进行后处理得到输入音频的实时检测结果。` `模型训练采用“basetrain + finetune”的模式,basetrain过程使用大量内部移动端数据,在此基础上,使用1万条设备端录制安静场景“小云小云”数据进行微调,得到最终面向业务的模型。后续用户可在basetrain模型基础上,使用其他关键词数据进行微调,得到新的语音唤醒模型,但暂时未开放模型finetune功能。``'''
三、不可编辑PDF文档的结构化技术方案
版面分析:
文档版面分析通过边界框界定图片文档的不同内容区域。该过程使用计算机视觉的目标检测模型,包括基于Transformer的DINO、Mask R-CNN和YOLO系列算法。
利用版式分析的优势,我们得以通过大量精确的标注数据,细致地识别和区分文档中的每个关键部分。
-
文本区域:精确划分页眉、页脚、主标题、正文段落、页码、脚注、图片说明文字及表格顶部的标题等,为后续信息提取与理解奠定基础。
-
表格:单独识别并框选出表格区域,便于结构化抽取表格内容。
-
数学公式:准确识别文档内的数学公式,保持科学文献与教育资料的严谨性。
-
图片:有效区分并标注文档内嵌的图像内容,为图像单独处理或内容关联分析创造条件。
文本识别(OCR):
-
当涉及到通过版式分析确定的文本区域时,通常采用OCR技术进行文字识别。常见的开源OCR工具包括读光OCR和PaddleOCR。
以下是PaddleOCR的使用例子:
import cv2``from paddleocr import PaddleOCR``paddleocr = PaddleOCR(lang='ch', show_log=False, enable_mkldnn=True)` `img = cv2.imread('1.jpg')` `result = paddleocr.ocr(img)``for i in range(len(result[0])):` `print(result[0][i][1][0]) # 输出识别结果
然而,像paddleOCR等开源ocr方式在实际应用中存在多个问题。
如:
-
漏识别:开源OCR模型由检测和识别两阶段构成,若检测不准确,会导致错误的累积和OCR识别的不精确。
-
识别文字错误:开源模型免费且未经特定场景训练,因此识别时可能出错。
表格解析:
-
对版式分析的表格区域,用表格解析模型转换为特定格式,如:csv、html、markdown等。常见开源模型有ppstructure。
如下:
import os``import cv2``from paddleocr import PPStructure,save_structure_res``table_engine = PPStructure(layout=False, show_log=True)` `save_folder = './output'``img_path = 'table.jpg'``img = cv2.imread(img_path)``result = table_engine(img)``save_structure_res(result, save_folder, os.path.basename(img_path).split('.')[0])``for line in result:` `line.pop('img')` `print(line)
开源方法常遇问题包括无法准确解析复杂表格,尤其是合并单元格时容易发生解析错误和行列对齐问题。
公式解析:
对版式分析划分的公式区域,用解析模型转化为特定格式。
下面使用LatexOCR进行公式解析的示例:
from PIL import Image``from pix2tex.cli import LatexOCR`` ``model = LatexOCR()``img = Image.open('1.jpg')``print(model(img))
四、阅读顺序恢复
在完成文本、表格、公式等区域的解析之后,为了重构文档原有的结构和格式,重新排序已识别区域,依据原页面布局,通常使用到边界框位置信息。
一般有两种方法:传统的明确规则和现代的LayoutReader模型技术。
- xy-cut方法:通过边界框的位置信息重新排序文档内容,就像整理散落的拼图,恢复它们原本的图案。
import numpy as np`` ``def xy_cut(bboxes, direction="x"):` `result = []` `K = len(bboxes)` `indexes = range(K)` `if len(bboxes) <= 0:` `return result` `if direction == "x":` `# x first` `sorted_ids = sorted(indexes, key=lambda k: (bboxes[k][0], bboxes[k][1]))` `sorted_boxes = sorted(bboxes, key=lambda x: (x[0], x[1]))` `next_dir = "y"` `else:` `sorted_ids = sorted(indexes, key=lambda k: (bboxes[k][1], bboxes[k][0]))` `sorted_boxes = sorted(bboxes, key=lambda x: (x[1], x[0]))` `next_dir = "x"`` ` `curr = 0` `np_bboxes = np.array(sorted_boxes)` `for idx in range(len(sorted_boxes)):` `if direction == "x":` `# a new seg path` `if idx != K - 1 and sorted_boxes[idx][2] < sorted_boxes[idx + 1][0]:` `rel_res = xy_cut(sorted_boxes[curr:idx + 1], next_dir)` `result += [sorted_ids[i + curr] for i in rel_res]` `curr = idx + 1` `else:` `# a new seg path` `if idx != K - 1 and sorted_boxes[idx][3] < sorted_boxes[idx + 1][1]:` `rel_res = xy_cut(sorted_boxes[curr:idx + 1], next_dir)` `result += [sorted_ids[i + curr] for i in rel_res]` `curr = idx + 1`` ` `result += sorted_ids[curr:idx + 1]` `return result`` `` ``def augment_xy_cut(bboxes,` `direction="x",` `lambda_x=0.5,` `lambda_y=0.5,` `theta=5,` `aug=False):` `if aug is True:` `for idx in range(len(bboxes)):` `vx = np.random.normal(loc=0, scale=1)` `vy = np.random.normal(loc=0, scale=1)` `if np.abs(vx) >= lambda_x:` `bboxes[idx][0] += round(theta * vx)` `bboxes[idx][2] += round(theta * vx)` `if np.abs(vy) >= lambda_y:` `bboxes[idx][1] += round(theta * vy)` `bboxes[idx][3] += round(theta * vy)` `bboxes[idx] = [max(0, i) for i in bboxes[idx]]` `res_idx = xy_cut(bboxes, direction=direction)` `res_bboxes = [bboxes[idx] for idx in res_idx]` `return res_idx, res_bboxes`` `` ``bboxes = [[58.54924774169922, 1379.6373291015625, 1112.8863525390625, 1640.0870361328125],` `[60.1091423034668, 483.88677978515625, 1117.4927978515625, 586.197021484375],` `[57.687435150146484, 1098.1053466796875, 387.9796142578125, 1216.916015625],` `[63.158992767333984, 311.2080993652344, 1116.2508544921875, 365.2145080566406],` `[138.85513305664062, 144.44039916992188, 845.18017578125, 198.04937744140625],` `[996.1032104492188, 1053.6279296875, 1126.1046142578125, 1071.3463134765625],` `[58.743492126464844, 634.3077392578125, 898.405029296875, 700.9544677734375],` `[61.35755920410156, 750.6771240234375, 1051.1060791015625, 850.3980712890625],` `[426.77691650390625, 70.69780731201172, 556.0884399414062, 109.58145141601562],` `[997.040283203125, 903.5933227539062, 1129.2984619140625, 921.10595703125],` `[59.40523910522461, 1335.1563720703125, 329.7382507324219, 1357.46533203125],` `[568.9025268554688, 14.365530967712402, 1087.898193359375, 32.60292434692383],` `[998.1250610351562, 752.936279296875, 1128.435546875, 770.4116821289062],` `[59.6968879699707, 947.9129638671875, 601.4513549804688, 999.4548950195312],` `[58.91489028930664, 1049.8773193359375, 487.3372497558594, 1072.2935791015625],` `[60.49456024169922, 902.8802490234375, 600.7571411132812, 1000.3502197265625],` `[60.188941955566406, 247.99755859375, 155.72970581054688, 272.1385192871094],` `[996.873291015625, 637.3861694335938, 1128.3558349609375, 655.1572875976562],` `[59.74936294555664, 1272.98828125, 154.8768310546875, 1295.870361328125],` `[58.835716247558594, 1050.5926513671875, 481.59027099609375, 1071.966796875],` `[60.60163116455078, 750.1132202148438, 376.1781921386719, 771.8764038085938],` `[57.982513427734375, 419.16058349609375, 155.35882568359375, 444.25115966796875],` `[1017.0194091796875, 1336.21826171875, 1128.002197265625, 1355.67724609375],` `[1019.8740844726562, 486.90814208984375, 1127.482421875, 504.61767578125]]`` ``res_idx, res_bboxes = augment_xy_cut(bboxes, direction="y")``print(res_idx)``# res_idx, res_bboxes = augment_xy_cut(bboxes, direction="x")``# print(res_idx)`` ``new_boxs = []``for i in res_idx:` `# print(i)`` ` `new_boxs.append(bboxes[i])`` ``print(new_boxs)
- **LayoutReader****方法:**采用先进的模型技术,让文档内容的排序更加智能和准确。
import torch``from model import LayoutLMv3ForBboxClassification``from collections import defaultdict`` ``CLS_TOKEN_ID = 0``UNK_TOKEN_ID = 3``EOS_TOKEN_ID = 2`` `` ``def BboxesMasks(boxes):` `bbox = [[0, 0, 0, 0]] + boxes + [[0, 0, 0, 0]]` `input_ids = [CLS_TOKEN_ID] + [UNK_TOKEN_ID] * len(boxes) + [EOS_TOKEN_ID]` `attention_mask = [1] + [1] * len(boxes) + [1]` `return {` `"bbox": torch.tensor([bbox]),` `"attention_mask": torch.tensor([attention_mask]),` `"input_ids": torch.tensor([input_ids]),` `}`` `` ``def decode(logits, length):` `logits = logits[1: length + 1, :length]` `orders = logits.argsort(descending=False).tolist()` `ret = [o.pop() for o in orders]` `while True:` `order_to_idxes = defaultdict(list)` `for idx, order in enumerate(ret):` `order_to_idxes[order].append(idx)` `order_to_idxes = {k: v for k, v in order_to_idxes.items() if len(v) > 1}` `if not order_to_idxes:` `break` `for order, idxes in order_to_idxes.items():` `idxes_to_logit = {}` `for idx in idxes:` `idxes_to_logit[idx] = logits[idx, order]` `idxes_to_logit = sorted(` `idxes_to_logit.items(), key=lambda x: x[1], reverse=True` `)` `for idx, _ in idxes_to_logit[1:]:` `ret[idx] = orders[idx].pop()` `return ret`` `` ``def layoutreader(bboxes):` `inputs = BboxesMasks(bboxes)` `logits = model(**inputs).logits.cpu().squeeze(0)` `orders = decode(logits, len(bboxes))` `return orders`` `` ``if __name__ == '__main__':` `bboxes = [[584, 0, 595, 1], [35, 120, 89, 133],` `[35, 140, 75, 152]]` `model_path = ""` `model = LayoutLMv3ForBboxClassification.from_pretrained()`` ` `print(layoutreader(bboxes))``# [1, 2, 0]
总结:
在深入探讨可编辑PDF与不可编辑PDF(通常为扫描件)的开源技术栈及其精妙之处时,我们构建了一个系统性的管道(pipeline),这一管道的每一环节都蕴含着精细化的优化潜力。在检索增强生成(RAG,Retrieval-Augmented Generation)的语境下,为了实现对文档内容碎片(chunks)的精确划分,文档版式分析的准确性显得至关重要。这一过程要求我们在目标检测阶段达到极高的粒度,并且所使用的标签需紧密贴合具体应用场景的需求,而非盲目追求一个“万能钥匙”式的版式分析模型,因为后者往往难以兼顾所有文档的多样化布局特性。
一、大模型全套的学习路线
学习大型人工智能模型,如GPT-3、BERT或任何其他先进的神经网络模型,需要系统的方法和持续的努力。既然要系统的学习大模型,那么学习路线是必不可少的,下面的这份路线能帮助你快速梳理知识,形成自己的体系。
L1级别:AI大模型时代的华丽登场
L2级别:AI大模型API应用开发工程
L3级别:大模型应用架构进阶实践
L4级别:大模型微调与私有化部署
一般掌握到第四个级别,市场上大多数岗位都是可以胜任,但要还不是天花板,天花板级别要求更加严格,对于算法和实战是非常苛刻的。建议普通人掌握到L4级别即可。
以上的AI大模型学习路线,不知道为什么发出来就有点糊,高清版可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费
】
二、640套AI大模型报告合集
这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。
三、大模型经典PDF籍
随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。
四、AI大模型商业化落地方案
作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量。