はじめに
LangChain入門ついでに何かシンプルなアプリケーションを作れないかと思い、PDFを要約してかんたんな日本語に変換するWebアプリを作ってみました。
上記は令和4年版情報通信白書の第4章第7節「ICT技術政策の推進」を要約したものです。
難しい言い回しもある程度やさしく変換されるので、内容が堅くとっつきにくい文章をざっくり把握したいときに使えそうです。
この記事では、主にLangChainを使用して実装した部分を解説します。
LangChainを使用した実装
前提として、モデルはgpt-3.5-turbo
を使用しています。
この場合、モデルのインスタンス生成にはChatOpenAI
を使う必要があります。
llm = ChatOpenAI(model_name=MODEL_NAME, temperature=TEMPERATURE)
LangChainのissueでも案内があります。
https://github.com/hwchase17/langchain/issues/1643#issuecomment-1468520580
PDFの読み込み
日本語を安定して読むため、@shimajiroxyz さん作のCJKPDFReaderを使用させていただきました。
今回はPDFをページ単位で分割したかったため、gpt-indexで日本語PDFを読み込む【Python】に記載いただいている情報を参考にconcat_pages = False
としてインスタンスを作成しています。
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を使用して要約しています。
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
要約用のプロンプトテンプレートは以下の通りで、とてもシンプルです。
summarize_prompt_template = """以下の文章を簡潔に要約してください。:
{text}
要約:"""
PDFを設定したテンプレートを用いてページ毎に要約し、そのページ毎の要約を結合して再度要約することにより、PDF全体の要約が出力されます。
LangChainを使えばたったこれだけのコードで実装できてしまうのでとても楽ですね。
かんたんな日本語への変換
LangChainのChat Modelsのはじめにを参考に、SystemMessage
とHumanMessage
をメッセージとして送信します。
それぞれ、OpenAI APIのChat Completionにおけるsystemロールとuserロールに相当するものでしょう。
SystemMessage
に設定するプロンプトは以下です。
simpify_system_message = "あなたは文章を子ども向けのかんたんな日本語に変換するのに役立つアシスタントです。"
HumanMessage
には、FewShotPromptTemplate
を設定しています。
かんたんな日本語に変換する例をいくつか提示することにより、変換精度を上げることを意図しています。
プロンプトの元となるものは以下です。
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はサンプル生成にも有用ですよね。
これらを組み合わせ、変換結果を取得するための一連のコードは以下の通り。
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にもありますので、他の例はこちらを参照してください。
実行
後はこれらを順番に実行するのみです。
今回はPdfSimplySummarizer
というクラスのrun
メソッドにまとめる形にしました。
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という優れたサービスが既に存在し、かつ私が認識していないだけで類似サービスがあるであろうことと、バックエンドの処理が長くFaaSでのホストが難しい(=インスタンス費用がそこそこかかる)ことから断念しました。
サービスとしての価値は提供できませんでしたが、この記事がどなたかの参考になれば幸いです。