📝

【LangChain】PDFをメタデータとともに読み込む(作成中)

2024/05/19に公開

前回の記事で、chatGPTを使ってPDFファイルを読み込んで、要約を試みました。
内容については4.oを使うと比較的満足できる回答が得られるのですが、ページ数が読み取れなかったり、章や節の構成が不十分といった問題が残りました。

そこで、このような問題を解決したPDF書類読み取りアプリケーションを開発したいと思います。

PDF読み込みライブラリ

langchainのこちらのページにはいくつかのPDF読み込みのためのライブラリが紹介されています。
https://python.langchain.com/v0.1/docs/modules/data_connection/document_loaders/pdf/

今回はシンプルで使いやすそうだったPyPDFLoaderと、非構造データの読み込みに強そうなUnstructuredPDFLoaderを試しました

PyPDFLoader

基本的には公式チュートリアルの通りにやっていればできます。

loader = PyPDFLoader("000213033.pdf") # PDFファイルを指定
pages = loader.load_and_split()

pagesはDocumentのリストであり、pdfファイルの1ページが1Documentに該当します。
(テキストの読み込みミスや、途切れることなく、一字一句正確に読み込まれていました)

pages[1]

Document(page_content='1 \n  \n \nはじめに  \n \n○ 生物多様性 COP15にて採択された「昆明・モントリオール生物多様性枠組」 では、・・・途中省略・・・  出所:国際統合報告< IR>フレームワーク (2021年1月)', metadata={'source': '000213033.pdf', 'page': 1})

このようにmetadataとしてソース元ファイル名とページが入っていることがわかります

FAISSにぶち込んで近傍検索は以下の通りです。

faiss_index = FAISS.from_documents(pages, OpenAIEmbeddings(model="text-embedding-3-small"))

docs = faiss_index.similarity_search("気候変動への取り組みと生物多様性への取り組みの違いは?", k=3)
for doc in docs:
    print(str(doc.metadata["page"]) + ":", doc.page_content[:150])
---
22: 22 6. 今後の取組  ~自然と共生する 世界の実現に向けて~  
 
(1)  今後の課題 
○ 本戦略は、 2030年を目標年度とする生物多様性国家戦略の基本戦略3「ネイチャーポ
ジティブ経済の実現」について企業が押えておくべき要素やそれを支える国の施策を
具体化したものであり、すなわち 20
---
18: 18 <施策の方向性>  
○ DXの進展は、価値創造プロセスの全般にわたって鍵となる。例えば、リスクの認識・
特定に際しては場所に紐付いた分析に必要な一次情報データベースが 必要であり、 バ
リューチェーンの把握にはデジタル技術を用いたトレーサビリティの確保が有効であ
る。取組の効果の見える化のた
---
4: 4 創造にもつながっている現状に鑑みれば、自然 資本の保全・回復に関しても、企業に
よるソリューションの提供がネイチャーポジティブ実現の 推進力となることが十分に
期待できる。  
○ その際、 地球規模生物多様性概況第5版( GBO5)14において描写されているように、
ネイチャーポジティブ の実

metadataにページが入っているので、近傍検索で探し出した時に何ページ目にその文があるかを抽出することができました。
(ただ近傍検索結果は、そこ抜き出す?という箇所を返してきているので、このへんはチューニング必要かもしれません)

次にchromadbを使って質問の回答を返してやるようにしましょう。

vectorstore = Chroma.from_documents(documents=pages, embedding=OpenAIEmbeddings())

retriever = vectorstore.as_retriever()
prompt = hub.pull("rlm/rag-prompt")
llm = ChatOpenAI(model="gpt-4o", temperature=0)

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)
rag_chain.invoke("研究開発・技術実証支援として、どういった施策がありますか")

#->
研究開発・技術実証支援としての施策には、生物多様性保全に貢献する技術・サービスに対する支援や、遺伝資源の利用に伴うABSの実施が含まれます。また、化学農薬や化学肥料の使用量を低減し、有機農業を推進する「みどりの食料システム戦略」もあります。これらの施策は、持続可能な環境保全型の農林水産業の拡大を目指しています。

今回はとりあえずチュートリアル通りの簡単な書き方にしました。

この質問自体は19ページ目に該当箇所があり、回答すべき箇所は20ページ目にありますが、AIは23ページを抽出してきました。

このあたり、最新の4.oでもまだまだ国語力をサポートしないといけなさそうです。

Unstructured

使い方はPyPDFLoaderとほぼ同じですが、いくつか注意が必要です。

from langchain_community.document_loaders import UnstructuredPDFLoader

loader = UnstructuredPDFLoader("000213033.pdf", mode="paged", languages=['ja'])

pages = loader.load()

modeはデフォルトでは'single'となっており、これだとpdfファイルのページを無視して単一ページとして読み込まれてしまいます。'paged'に指定することで、1ページ1Documentとして読み込みます。

languages(配列ですので注意)はデフォルトでは'eng'になっています。それでも日本語として読み込めましたが、気持ち悪いので'ja'を指定しておきましょう

pages[1]

Document(
    page_content='はじめに\n\n生物多様性 COP15 にて採択された「昆明・モントリオール生物多様性枠組」では、・・・出所:国際統合報告<IR>フレームワーク(2021 年1月)\n\n1\n\n',
    metadata={
        'source': '000213033.pdf',
        'coordinates': {'points': ((294.77, 768.9552), (294.77, 779.5151999999999), (303.58567999999997, 779.5151999999999), (303.58567999999997, 768.9552)),
        'system': 'PixelSpace',
        'layout_width': 595.32,
        'layout_height': 841.92},
        'filename': '000213033.pdf',
        'languages': ['ja'],
        'last_modified': '2024-05-19T03:44:47',
        'page_number': 2,
        'filetype': 'application/pdf',
        'parent_id': 'f4041535dda232505ce51e779e300aa8'})

見ての通り、PyPDFLoaderを使った時と比べて多くのメタ情報を格納しています。

ただ、このメタ情報のせいで、chromedbにぶち込めません

vectorstore = Chroma.from_documents(
    documents=pages,
    embedding=OpenAIEmbeddings(model="text-embedding-3-small")
)

#->
ValueError: Expected metadata value to be a str, int, float or bool, got {'points': ((252.65, 693.1179999999999), (252.65, 711.1179999999999), (351.67, 711.1179999999999), (351.67, 693.1179999999999)), 'system': 'PixelSpace', 'layout_width': 595.32, 'layout_height': 841.92} which is a dict

During handling of the above exception, another exception occurred:

Try filtering complex metadata from the document using langchain_community.vectorstores.utils.filter_complex_metadata.

Chroma.from_documentsはmetadataにdictは受け付けていないので、dict型のデータは消す必要があります。

メッセージにあるようにfilter_complex_metadataを使って、使えないmetadataを消しましょう

from langchain_community.vectorstores.utils import filter_complex_metadata

pages2 = filter_complex_metadata(pages)

これでぶちこめるようになります。

vectorstore = Chroma.from_documents(
    documents=pages2,
    embedding=OpenAIEmbeddings(model="text-embedding-3-small")
)

今後の課題

metadata付きでPDF書類をベクトルストアに変換することはできましたが、精度の高い回答を得たり、AIがその答えを返した理由を知るためには、エージェントを作成するなどもう一工夫必要そうです。

Discussion