【Python】unstructuredを使って、 PDFファイル内の非構造データを抽出する

# はじめに
RAGの仕組みを利用して、文書内のテキストに特化した情報を取得する操作って非常に便利ですよね。  
一方で、PDFファイルに限らず、テキスト以外の画像やテーブルが含まれているファイルはたくさんあると思います。  
こういった画像やテーブルといった非構造データは、文書内のテキストとしてはノイズになることがあります。  
それを解決するべく、`unstructured`と呼ばれるライブラリを使って、非構造データを抽出してみます。これができると、マルチモーダルRAGの幅が結構広がると思っています。  

※基本的には、ipynbの拡張子でnotebookを使って作業していたので、notebookベースで以降のスクリプトは実行しています。

## 準備
以下のライブラリをインストールします。  
```
$ pip install unstructured[all-docs]
```
たったこれだけ。  

## 素直に実装してみる
langchainのgithub上にあった[Semi_structured_and_multi_modal_RAG.ipynb](https://github.com/langchain-ai/langchain/blob/master/cookbook/Semi_structured_and_multi_modal_RAG.ipynb)を参考に、まずは素直に実装してみます。  
なお、今回使用したPDFファイルは[ITパスポートの過去問](https://www3.jitec.ipa.go.jp/JitesCbt/html/openinfo/questions.html)のうち、令和6年度分の問題冊子です。  
```use_unstructured.py
import os

from unstructured.partition.pdf import partition_pdf


# 環境変数
DATA_PAR_PATH = os.path.join('..','..','data')
DATASET_PATH = os.path.join(DATA_PAR_PATH,'2024r06_ip_qs.pdf')
OUTPUT_PATH = os.path.join(DATA_PAR_PATH,'images')


# PDFファイル内のデータを分割する
raw_pdf_elements = partition_pdf(
    filename=DATASET_PATH,
    chunking_strategy='by_title',
    infer_table_structure=True,
    extract_images_in_pdf=True,
    extract_image_block_output_dir=OUTPUT_PATH
)
```
このとき、非構造データは画像として分割され、`OUTPUT_PATH`で指定したディレクトリに入っています。  
この非構造データを確認してみた結果を、以下にまとめます。  
* 画像しか検出していない
    * テーブルも画像として認識してほしい
* 画像を出力する必要がある
    * わざわざ出力せずに、byte型で非構造データを保持したい
* 画像の端っこが見切れている
    * 画像によって、見切れ具合は様々

ちなみに、ここで画像やテーブルといった非構造データは、ライブラリ内でOCRによって読み取られた文字が、テキストとして保持されています。OCRすらもライブラリがまとめてやってくれるのは、非常に便利ですね。  
ですが、マルチモーダルRAGを使える現代において、非構造データをOCRした情報よりも、画像としてマルチモーダルに対応しているLLMに投げたいな〜、と思います。  

また、精度を上げるには、`unstructured`ライブラリが用意するAPIを使うと良さそうですね（[公式サイト](https://unstructured.io/)）。

## 非構造データの抽出を工夫してみる
上記の結果を踏まえて、僕なりに解決した結果が次になります。
```use_unstructured_2.py
import base64
import io
import os

from PIL import Image
from unstructured.partition.pdf import partition_pdf


# 環境変数
DATA_PAR_PATH = os.path.join('..','..','data')
DATASET_PATH = os.path.join(DATA_PAR_PATH,'2024r06_ip_qs.pdf')


# PDFファイル内のデータを分割する
raw_pdf_elements = partition_pdf(
    filename=DATASET_PATH,
    infer_table_structure=True,
    strategy='hi_res',
    extract_images_in_pdf=True,
    extract_image_block_types=['Image', 'Table'],
    extract_image_block_to_payload=True
)

# 画像として保持されている非構造データを確認する
for elem in raw_pdf_elements:
    if elem.category in ['Image', 'Table']:
        image_base64 = elem.metadata.image_base64
        decoded_image = base64.b64decode(image_base64)
        image = Image.open(io.BytesIO(decoded_image))
        print(elem.metadata.page_number)
        display(image)
```
改良したスクリプトで、再び非構造データを確認してみると以下の結果がわかりました。  
* テーブルが画像として抽出できた
* 2つくらいテーブルの検出漏れがあった
    * 検出漏れしたテーブルは、いずれも文書の左側に寄っていたため、文書の中央あたりにないテーブルは見落としてしまう可能性がありそう
    * PDFファイルの周囲にパディングを設けて軽減できたりするかな？
* 非構造データを.jpg形式で出力することなく、byte型で取得できた
* 検出した非構造データ（=画像とテーブル）で、端っこが見切れてる画像があった
    * 情報の質がやや落ちることは懸念しておく必要あり

---

ここで`partition_pdf`を使用するにあたって、いくつか気を付ける点があったので、下にまとめます。
* `strategy='hi_res'`を指定する
    * 他のパラメータのうち、`extract`から始まるパラメータを使用するために指定する必要あり
* `chunking_strategy='by_title'`は**指定しない**
    * このパラメータを指定すると、タイトル単位で文書内の情報が束ねられてしまい、画像やテーブルの情報を抽出することができない
* `extract_image_block_types=['Image', 'Table']`を指定
    * このパラメータによって、文書内の非構造データである画像やテーブルの情報を、画像として認識してくれる
* `extract_image_block_to_payload=True`を指定
    * このパラメータによって、画像を出力することなく、メタデータにbyte型の形式で保持してくれる

## 少し比較してみる
また、PDFファイルのデータ抽出の結果は、前後で以下のようになりました。  
```use_unstructured.py
defaultdict(int, {'CompositeElement': 72, 'Table': 25})
```
```use_unstructured_2.py
defaultdict(int,
            {'Title': 111,
             'Image': 5,
             'NarrativeText': 40,
             'ListItem': 340,
             'UncategorizedText': 51,
             'Formula': 30,
             'Table': 16,
             'Footer': 6,
             'FigureCaption': 2})
```
こうしてみると、`chunking_strategy='by_title'`というパラメータを指定した前者は、文書内の関連した情報を1つのかたまりとして、まとめてくれるという点では便利ですね。  
しかし、非構造データを抽出したり、細かく文書内の情報を取得する場合は、このパラメータは指定しない方がカスタマイズの幅が広がりそうです。  
また、前者後者問わず、抽出されたデータは、ページ番号が若い順(=先頭)でかつ、ページの上から順に保持されているので、文書の脈略がおかしくなることはないです。非常に助かります。

# おわりに
冒頭で参考にしたnotebookを見ていると、3パターンのやり方でマルチモーダルRAGを実装する手法が紹介されています。  
その手法のいずれも画像情報を要約したり、あるいはそのまま画像データとしてLLMに投げる仕組みのようです。今回、僕が工夫したこととしては、テーブルも画像として扱うこと、そして画像をbyte型で保持することの2つだったので、option3の手法をやるにはベストな気がしています。  

もし、もっと違うやり方を知っていたり、誤った情報を記述している場合は、気軽にご指摘ください。