RAG增強之路:增強PDF解析并結(jié)構(gòu)化技術(shù)路線方案及思路 原創(chuàng)
前言
現(xiàn)階段,盡管大模型在生成式問答上取得了很大的成功,但由于大部分的數(shù)據(jù)都是私有數(shù)據(jù),大模型的訓(xùn)練及微調(diào)成本非常高,RAG的方式逐漸成為落地應(yīng)用的一種重要的選擇方式。然而,如何準(zhǔn)確的對文檔進(jìn)行劃分chunks,成為一種挑戰(zhàn),在現(xiàn)實中,大部分的專業(yè)文檔都是以 PDF 格式存儲,低精度的 PDF 解析會顯著影響專業(yè)知識問答的效果。因此,本文將介紹針對pdf,介紹一些pdf結(jié)構(gòu)化技術(shù)鏈路供參考。
一、可編輯文檔
1.1 語義分段
經(jīng)pdf解析工具后,原始文檔的段落信息全部丟失,需要進(jìn)行段落的劃分和重組。下面介紹一種語義分段模型的訓(xùn)練思路和一種開源的分段模型。
- 語義分段訓(xùn)練思路
- 開源的模型
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='移動端語音喚醒模型,檢測關(guān)鍵詞為“小云小云”。模型主體為4層FSMN結(jié)構(gòu),使用CTC訓(xùn)練準(zhǔn)則,參數(shù)量750K,適用于移動端設(shè)備運行。模型輸入為Fbank特征,輸出為基于char建模的中文全集token預(yù)測,測試工具根據(jù)每一幀的預(yù)測數(shù)據(jù)進(jìn)行后處理得到輸入音頻的實時檢測結(jié)果。模型訓(xùn)練采用“basetrain + finetune”的模式,basetrain過程使用大量內(nèi)部移動端數(shù)據(jù),在此基礎(chǔ)上,使用1萬條設(shè)備端錄制安靜場景“小云小云”數(shù)據(jù)進(jìn)行微調(diào),得到最終面向業(yè)務(wù)的模型。后續(xù)用戶可在basetrain模型基礎(chǔ)上,使用其他關(guān)鍵詞數(shù)據(jù)進(jìn)行微調(diào),得到新的語音喚醒模型,但暫時未開放模型finetune功能。')
print(result[OutputKeys.TEXT])
# 輸出
'''
移動端語音喚醒模型,檢測關(guān)鍵詞為“小云小云”。模型主體為4層FSMN結(jié)構(gòu),使用CTC訓(xùn)練準(zhǔn)則,參數(shù)量750K,適用于移動端設(shè)備運行。模型輸入為Fbank特征,輸出為基于char建模的中文全集token預(yù)測,測試工具根據(jù)每一幀的預(yù)測數(shù)據(jù)進(jìn)行后處理得到輸入音頻的實時檢測結(jié)果。
模型訓(xùn)練采用“basetrain + finetune”的模式,basetrain過程使用大量內(nèi)部移動端數(shù)據(jù),在此基礎(chǔ)上,使用1萬條設(shè)備端錄制安靜場景“小云小云”數(shù)據(jù)進(jìn)行微調(diào),得到最終面向業(yè)務(wù)的模型。后續(xù)用戶可在basetrain模型基礎(chǔ)上,使用其他關(guān)鍵詞數(shù)據(jù)進(jìn)行微調(diào),得到新的語音喚醒模型,但暫時未開放模型finetune功能。
'''
二、可編輯文檔(掃描件)
2.1 版面分析
版面分析指的是對圖片形式的文檔(掃描件)進(jìn)行區(qū)域劃分,通過bounding box定位其中的關(guān)鍵區(qū)域,如:文字、標(biāo)題、表格、圖片等,通常采用一些CV目標(biāo)檢測模型進(jìn)行版式分析,如:參數(shù)量大的有:DINO等基于transformer的目標(biāo)檢測模型;參數(shù)量小的有MaskRCNN、YOLO系列等。
版式分析的優(yōu)勢,通過大量標(biāo)注的數(shù)據(jù),準(zhǔn)確的劃分出文檔關(guān)鍵區(qū)域,如下:
- 文本區(qū)域:頁眉、頁腳、標(biāo)題、段落、頁碼、腳注、圖片標(biāo)題、表格標(biāo)題等
- 表格
- 公式
- 圖片
2.2 文本識別
對于經(jīng)由版式分析劃分出來的文本區(qū)域,通常采用OCR進(jìn)行相應(yīng)區(qū)域的文字識別,常見的開源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]) # 輸出識別結(jié)果
然而,像paddleOCR等開源ocr方式,在實際應(yīng)用中還是存在很多的問題,如:
- 漏識別:開源的一些ocr模型通常有檢測和識別兩階段構(gòu)成,如果檢測模型檢測不準(zhǔn),將會錯誤累積,ocr識別時也不準(zhǔn)確。
- 識別文字錯誤:開源模型畢竟免費,沒有在特定的領(lǐng)域場景上進(jìn)行特定的訓(xùn)練,因此在識別時難免出現(xiàn)錯誤。
2.3 表格解析
對于經(jīng)由版式分析劃分出來的表格區(qū)域,通常采用表格解析模型進(jìn)行解析,并轉(zhuǎn)化為特定的格式,如: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)
在實際使用過程中,常見的開源方法經(jīng)常遇到的問題就是,無法準(zhǔn)確的對表格進(jìn)行解析,這種問題常見與復(fù)雜表格,尤其是表格合并單元格時,容易解析錯誤,行列不對齊等。
2.4 公式解析
對于經(jīng)由版式分析劃分出來的公式區(qū)域,通常采用公式解析模型進(jìn)行解析,并轉(zhuǎn)化為特定的格式,如:tex等。
下面是一個使用LatexOCR進(jìn)行公式解析的例子:
from PIL import Image
from pix2tex.cli import LatexOCR
model = LatexOCR()
img = Image.open('1.jpg')
print(model(img))
三、閱讀順序
經(jīng)上述解析后,需要根據(jù)boungding box進(jìn)行排序,以便恢復(fù)文檔的格式信息。下面將介紹一種基于規(guī)則的方法和一種基于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及非官方權(quán)重開源??》
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]
總結(jié)
本文詳細(xì)介紹了可編輯pdf和不可編輯pdf(掃描件)的一些開源技術(shù)方案和路線,整個技術(shù)鏈路是一個pipline的路線,每一個步驟都需要精細(xì)的優(yōu)化。在RAG中,準(zhǔn)確的劃分chunks,需要依賴文檔的版式分析的精準(zhǔn)性。因此,尤其是在對文檔進(jìn)行版面分析時,目標(biāo)檢測的粒度及標(biāo)簽需要對落地場景進(jìn)行特定的分析,不要妄想著存在一個通用的版式分析模型解決一切文檔版式分析問題。
參考文獻(xiàn)
- 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
本文轉(zhuǎn)載自公眾號大模型自然語言處理 作者:余俊暉
原文鏈接:??https://mp.weixin.qq.com/s/UOPDuv9hi2MJEhOHM_TZ8A??
