RAG增強之路:增強PDF解析并結構化技術路線方案及思路 原創
前言
現階段,盡管大模型在生成式問答上取得了很大的成功,但由于大部分的數據都是私有數據,大模型的訓練及微調成本非常高,RAG的方式逐漸成為落地應用的一種重要的選擇方式。然而,如何準確的對文檔進行劃分chunks,成為一種挑戰,在現實中,大部分的專業文檔都是以 PDF 格式存儲,低精度的 PDF 解析會顯著影響專業知識問答的效果。因此,本文將介紹針對pdf,介紹一些pdf結構化技術鏈路供參考。
一、可編輯文檔
1.1 語義分段
經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功能。
'''
二、可編輯文檔(掃描件)
2.1 版面分析
版面分析指的是對圖片形式的文檔(掃描件)進行區域劃分,通過bounding box定位其中的關鍵區域,如:文字、標題、表格、圖片等,通常采用一些CV目標檢測模型進行版式分析,如:參數量大的有:DINO等基于transformer的目標檢測模型;參數量小的有MaskRCNN、YOLO系列等。
版式分析的優勢,通過大量標注的數據,準確的劃分出文檔關鍵區域,如下:
- 文本區域:頁眉、頁腳、標題、段落、頁碼、腳注、圖片標題、表格標題等
- 表格
- 公式
- 圖片
2.2 文本識別
對于經由版式分析劃分出來的文本區域,通常采用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識別時也不準確。
- 識別文字錯誤:開源模型畢竟免費,沒有在特定的領域場景上進行特定的訓練,因此在識別時難免出現錯誤。
2.3 表格解析
對于經由版式分析劃分出來的表格區域,通常采用表格解析模型進行解析,并轉化為特定的格式,如: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)
在實際使用過程中,常見的開源方法經常遇到的問題就是,無法準確的對表格進行解析,這種問題常見與復雜表格,尤其是表格合并單元格時,容易解析錯誤,行列不對齊等。
2.4 公式解析
對于經由版式分析劃分出來的公式區域,通常采用公式解析模型進行解析,并轉化為特定的格式,如:tex等。
下面是一個使用LatexOCR進行公式解析的例子:
from PIL import Image
from pix2tex.cli import LatexOCR
model = LatexOCR()
img = Image.open('1.jpg')
print(model(img))
三、閱讀順序
經上述解析后,需要根據boungding box進行排序,以便恢復文檔的格式信息。下面將介紹一種基于規則的方法和一種基于Layoutreader模型的方法。
- xy-cut
import numpy as np
def xy_cut(bboxes, directinotallow="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,
directinotallow="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, directinotallow=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, directinotallow="y")
print(res_idx)
# res_idx, res_bboxes = augment_xy_cut(bboxes, directinotallow="x")
# print(res_idx)
new_boxs = []
for i in res_idx:
# print(i)
new_boxs.append(bboxes[i])
print(new_boxs)
- Layoutreader
該模型及其介紹可以查閱往期文章《???文檔智能】符合人類閱讀順序的文檔模型-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(掃描件)的一些開源技術方案和路線,整個技術鏈路是一個pipline的路線,每一個步驟都需要精細的優化。在RAG中,準確的劃分chunks,需要依賴文檔的版式分析的精準性。因此,尤其是在對文檔進行版面分析時,目標檢測的粒度及標簽需要對落地場景進行特定的分析,不要妄想著存在一個通用的版式分析模型解決一切文檔版式分析問題。
參考文獻
- LaTeX-OCR:https://github.com/lukas-blecher/LaTeX-OCR
- PaddleOCR:https://github.com/PaddlePaddle/PaddleOCR
- 語義分段模型,https://modelscope.cn/models/iic/nlp_bert_document-segmentation_chinese-base/summary
本文轉載自公眾號大模型自然語言處理 作者:余俊暉
原文鏈接:??https://mp.weixin.qq.com/s/UOPDuv9hi2MJEhOHM_TZ8A??
