LangChainでPDFを要約してかんたんな日本語に変換するWebアプリを作ってみた

# はじめに
LangChain入門ついでに何かシンプルなアプリケーションを作れないかと思い、PDFを要約してかんたんな日本語に変換するWebアプリを作ってみました。

![スクリーンショット 2023-05-04 235359.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/59654f57-43b4-7ead-f3e3-0f1a60677f7b.png)


上記は[令和4年版情報通信白書](https://www.soumu.go.jp/johotsusintokei/whitepaper/ja/r04/pdf/index.html)の第4章第7節「ICT技術政策の推進」を要約したものです。
難しい言い回しもある程度やさしく変換されるので、内容が堅くとっつきにくい文章をざっくり把握したいときに使えそうです。

この記事では、主にLangChainを使用して実装した部分を解説します。

# LangChainを使用した実装
前提として、モデルは`gpt-3.5-turbo`を使用しています。
この場合、モデルのインスタンス生成には`ChatOpenAI`を使う必要があります。
```python
llm = ChatOpenAI(model_name=MODEL_NAME, temperature=TEMPERATURE)
```
LangChainのissueでも案内があります。
https://github.com/hwchase17/langchain/issues/1643#issuecomment-1468520580

## PDFの読み込み
日本語を安定して読むため、@shimajiroxyz さん作の[CJKPDFReader](https://llamahub.ai/l/file-cjk_pdf)を使用させていただきました。
今回はPDFをページ単位で分割したかったため、[gpt-indexで日本語PDFを読み込む【Python】](https://qiita.com/shimajiroxyz/items/aa1e22e19d690a3944ba)に記載いただいている情報を参考に`concat_pages = False`としてインスタンスを作成しています。
```python
def _load_documents(self, file_path: str) -> list:
        CJKPDFReader = download_loader("CJKPDFReader")
        loader = CJKPDFReader(concat_pages=False)

        documents = loader.load_data(file=file_path)
        langchain_documents = [d.to_langchain_format() for d in documents]
        return langchain_documents
```

## 要約
LangChainの[load_summarize_chain](https://python.langchain.com/en/latest/use_cases/summarization.html)を使用して要約しています。

```python
def _summarize(self, langchain_documents: list) -> str:
        summarize_template = PromptTemplate(
            template=summarize_prompt_template, input_variables=["text"])

        chain = load_summarize_chain(
            self.llm,
            chain_type="map_reduce",
            map_prompt=summarize_template,
            combine_prompt=summarize_template
        )

        summary = chain.run(langchain_documents)
        return summary
```
要約用のプロンプトテンプレートは以下の通りで、とてもシンプルです。

```python
summarize_prompt_template = """以下の文章を簡潔に要約してください。:

{text}

要約:"""
```
PDFを設定したテンプレートを用いてページ毎に要約し、そのページ毎の要約を結合して再度要約することにより、PDF全体の要約が出力されます。

LangChainを使えばたったこれだけのコードで実装できてしまうのでとても楽ですね。

## かんたんな日本語への変換
LangChainの[Chat Modelsのはじめに](https://python.langchain.com/en/latest/modules/models/chat/getting_started.html)を参考に、`SystemMessage`と`HumanMessage`をメッセージとして送信します。
それぞれ、[OpenAI APIのChat Completion](https://platform.openai.com/docs/guides/chat/introduction)におけるsystemロールとuserロールに相当するものでしょう。
`SystemMessage`に設定するプロンプトは以下です。
```python
simpify_system_message = "あなたは文章を子ども向けのかんたんな日本語に変換するのに役立つアシスタントです。"
```
`HumanMessage`には、`FewShotPromptTemplate`を設定しています。
かんたんな日本語に変換する例をいくつか提示することにより、変換精度を上げることを意図しています。

プロンプトの元となるものは以下です。
```python
simplify_prompt_prefix = "元の文章の難しい表現を、元の意味を損なわないように注意して子ども向けのかんたんな表現に変換してください。語尾は「です」「ます」で統一してください。"

simplify_examples = [
    {
        "元の文章": "綿密な計画のもと、彼は革命を起こし、王朝の支配に終止符を打った。",
                "かんたんな文章": "よく考えられた計画で、彼は国の政治や社会の仕組みを大きく変えました。そして、王様の家族がずっと支配していた時代が終わりました。"
    },
    {
        "元の文章": "彼は無類の読書家であり、その博識ぶりは同僚からも一目置かれる存在だった。",
                "かんたんな文章": "彼はたくさんの本を読むのが大好きで、たくさんのことを知っています。友達も彼の知識を尊敬しています。"
    },
    {
        "元の文章": "彼女は劇団に所属し、舞台で熱演を繰り広げ、観客を魅了していた。",
                "かんたんな文章": "彼女は劇のグループに入っていて、舞台でとても上手に演じて、見ている人たちを楽しませています。"
    },
    {

        "元の文章": "宇宙の膨張は、エドウィン・ハッブルによって観測された銀河の運動から発見されました。",
                "かんたんな文章": "宇宙がどんどん広がっていることは、エドウィン・ハッブルさんがたくさんの星が集まった大きなものが動いていることを見つけることでわかりました。"
    }
]

simplify_example_formatter_template = """
元の文章: {元の文章}
かんたんな文章: {かんたんな文章}\n
"""

simplify_prompt_suffix = "元の文章: {input}\nかんたんな文章:"
```
これらを`PromptTemplate`および`FewShotPromptTemplate`に設定することで、プロンプトとして組み合わされます。

プロンプトの頭（=simplify_prompt_prefix）に「難しい漢字はひらがなにしてください」「難しい言葉には文末に解説を追加してください」「漢字にふりがなをふってください」といった指示を加えることも試してみたのですが、出力が安定せず、
「元の文章の難しい表現を、元の意味を損なわないように注意して子ども向けのかんたんな表現に変換してください。語尾は「です」「ます」で統一してください。」
に落ち着いています。
gpt-4を使えば追加できる制約の数や複雑さも変わってくると思うので、モデルによってプロンプトを使い分けるというのもよさそうです。

また、few shotで与える例文はGPT-4に考えてもらっています。
ChatGPTはサンプル生成にも有用ですよね。




これらを組み合わせ、変換結果を取得するための一連のコードは以下の通り。
```python
def _simplify(self, summary: str) -> str:
        simplify_example_prompt = PromptTemplate(
            input_variables=["元の文章", "かんたんな文章"],
            template=simplify_example_formatter_template,
        )

        simplify_few_shot_prompt_template = FewShotPromptTemplate(
            examples=simplify_examples,
            example_prompt=simplify_example_prompt,
            prefix=simplify_prompt_prefix,
            suffix=simplify_prompt_suffix,
            input_variables=["input"],
            example_separator="\n\n",
        )

        simplify_few_shot_prompt = simplify_few_shot_prompt_template.format(
            input=summary)

        messages = [
            SystemMessage(content=simpify_system_message),
            HumanMessage(content=simplify_few_shot_prompt),
        ]
        result = self.llm(messages)

        return result.content
```
`FewShotTemplate`を使用した例はLangChainのドキュメント[How to create a prompt template that uses few shot examples](https://python.langchain.com/en/latest/modules/prompts/prompt_templates/examples/few_shot_examples.html)にもありますので、他の例はこちらを参照してください。

## 実行
後はこれらを順番に実行するのみです。
今回は`PdfSimplySummarizer`というクラスの`run`メソッドにまとめる形にしました。
```python
class PdfSimplySummarizer():
    def __init__(self, llm: ChatOpenAI):
        self.llm = llm
    
    def _load_documents(self, file_path: str) -> list:
    # 省略

    def _summarize(self, langchain_documents: list) -> str:
    # 省略

    def _simplify(self, summary: str) -> str:
    # 省略

    def run(self, file_path: str):
        langchain_documents = self._load_documents(file_path)
        summary = self._summarize(langchain_documents)
        result = self._simplify(summary)
        return result
    
```


# さいごに
アプリケーションの全体はGitHubで公開しているので、もしよければ参考にしてみてください。
https://github.com/keitomatsuri/pdf-simply-summarizer-jp

本当はこれを改良してWebサービスとして世に出したかったのですが、[ChatPDF](https://www.chatpdf.com/)という優れたサービスが既に存在し、かつ私が認識していないだけで類似サービスがあるであろうことと、バックエンドの処理が長くFaaSでのホストが難しい（=インスタンス費用がそこそこかかる）ことから断念しました。

サービスとしての価値は提供できませんでしたが、この記事がどなたかの参考になれば幸いです。