📖

LangChain(GPT-3.5) + Marpでスライドを自動生成する

2023/04/29に公開

はじめに

Markdown形式でスライドを作成できるMarpなるものが存在することを知り、自然言語ならChatGPTで出力できるのではと思い試してみました。
適当なテーマを与えてスライドに作成する分には、ChatGPTで結果をMarkdown形式でMarpで使えるようにプロンプトを調整することで実現可能です。しかし、大量のテキストを含むドキュメントやWebページ、情報の正確さを担保した内容の文書に基づいたスライドを作成したい場合、トークン数の問題[1]でそもそもChatGPTでは扱いにくい場面があります。
そこで、今回はLangChainを使用して事前に収集した大量のテキストデータの情報をもとに自動でスライドを出力することを目指します。

メリット

  • API経由で入力したデータはモデルの精度向上に利用されない[2]
  • 情報の正確性を担保しやすい
  • アプリケーションとしてカスタマイズしやすい

必要なもの

  • OpenAIのAPIキー
    3ヶ月で失効する$18の無料枠があります。(以降は従量課金製)

  • Pythonのライブラリ

    • langchain
    • openai
    • faiss-cpu
      その他必要に応じて入れてください。
  • pdfファイル
    今回はこちらの記事を参考に米国クラウド法のpdfデータを使用しています。

実装

今回作成したプログラムの全体はこちら
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import FAISS
from langchain.document_loaders import PyPDFLoader
from langchain.chat_models import ChatOpenAI
from langchain.chains import ConversationalRetrievalChain
import time
import os
import openai

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 1000,
    chunk_overlap  = 20,
    length_function = len,
)
loader = PyPDFLoader("./docs/doj_cloud_act_white_paper_2019_04_10.pdf")
pages = loader.load_and_split()

state_of_the_union = "".join([x.page_content for x in pages])
texts = text_splitter.create_documents([state_of_the_union])

os.environ["OPENAI_API_KEY"]="your api key"

embeddings = OpenAIEmbeddings()
filename = "faiss_index"  # チェックするファイル名
if os.path.isdir(filename):
    vectorstore = FAISS.load_local("faiss_index", embeddings)
else:
    vectorstore = FAISS.from_documents(texts, embedding=embeddings)
vectorstore.save_local("faiss_index")

llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo")
retrieval_chain_agent = ConversationalRetrievalChain.from_llm(llm, vectorstore.as_retriever(), return_source_documents=True)

topic = "US Cloud"

query = f'''
{topic}について、重要なテーマを5つまでカンマ区切りで挙げてください。カンマ区切りのテーマのリストのみを出力してください。
'''
chat_history = []

result = retrieval_chain_agent({"question": query, "chat_history": chat_history})
chat_history.append(query)
chat_history.append(result)

results = []
topics = result["answer"].split(",")

if len(topics) < 2:
    raise Exception
for t in topics:
    query = f'{t}について要点を記述してください。'
    time.sleep(10)
    result = retrieval_chain_agent({"question": query, "chat_history": []})
    results.append(result["answer"])

status = '''
あなたは優秀なエバンジェリストです。与えられた文章について簡潔に説明するためのスライドを作成します。
スライドを作成する際は次のようなMarpのデザインテンプレートを使用したマークダウン形式で表現されます。
箇条書きの文はできるだけ短くまとめてください。
"""

---
<!-- スライド n -->
# { タイトル }

- { 本文 }
- { 本文 }
- { 本文 } 

"""
'''
prefix = [
        {"role": "system", "content": status},
]

slides = []
for r in results:
    data = f"""
次に与えられる文章の内容についてMarpによるマークダウン形式でスライドを日本語で作成してください。
必要に応じてスライドは2枚以上に分割してください:
{r}

    """
    comversation = [{"role": "user", "content": data}]
    messages = prefix + comversation
    response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=messages,
    temperature = 0,
    )
    time.sleep(10)
    answer = response["choices"][0]["message"]["content"]
    slides.append(answer)

output_str = """---
marp: true
_theme: gaia
paginate: true
backgroundColor: #f5f5f5

"""
for i in slides:
    output_str += i + "\n"


with open("slide.md", "w") as file:
    file.write(output_str)

テキストデータの読み込み

今回はpdfからデータを読み込んでいますが、他にも様々な形式のテキストデータを読み込むことができるので目的に応じて調整してください。

1. 各ページの文字を連結

意味がつながっている部分で区切るのが理想ですが、全ての文章を解析して区切るのは難しいので、ある程度の文章量を担保できる値として1000chunk、文の前後関係を把握するために20chunkのオーバーラップを設定しています。理想的な動作の実現や文書ごとの特性に応じて調整の余地があります。

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 1000,
    chunk_overlap  = 20,
    length_function = len,
)
state_of_the_union = "".join([x.page_content for x in pages])
texts = text_splitter.create_documents([state_of_the_union])

Vectorstoreの作成

先ほど作成したテキストデータをVectorstoreに保存します。
LangChainでは様々なVectorstoreがサポートされていますが、今回はFAISSを使用します。
OpenAIのEmbeddingsを使用してテキストをベクトル化し、このベクトルをVectorstoreに格納します。

embeddings = OpenAIEmbeddings()
filename = "faiss_index"  # チェックするファイル名
if os.path.isdir(filename):
    vectorstore = FAISS.load_local("faiss_index", embeddings)
else:
    vectorstore = FAISS.from_documents(texts, embedding=embeddings)
    vectorstore.save_local("faiss_index")

作成済みのVectorstoreがある場合は読み込むようにしています。

スライド生成

今回は、米国クラウド法について全体の概要を押さえたスライドを作成するため、米国クラウド法の重要なトピックの抽出→トピックから内容の肉付け→生成した内容をもとにスライドを作成の順に作業を行なっています。

1. エージェントの作成

LangChainで提供されているConversationalRetrievalChainを使用しています。
また、自身でカスタムエージェントを作成することも可能です[3]

llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo")
retrieval_chain_agent = ConversationalRetrievalChain.from_llm(llm, vectorstore.as_retriever(), return_source_documents=True)

2. promptの作成・エージェントの呼び出し

まずは、テーマについて重要なトピックを挙げさせています。コンマ区切りでの出力に失敗した場合はプログラムを終了するようにしています。

topic = "US Cloud"

query = f'''
{topic}について、重要なテーマを5つまでカンマ区切りで挙げてください。カンマ区切りのテーマのリストのみを出力してください。
'''
chat_history = []
result = retrieval_chain_agent({"question": query, "chat_history": chat_history})
topics = result["answer"].split(",")

if len(topics) < 2:
    raise Exception

3. テーマごとの内容の肉付け

先ほど生成した各テーマについて要点を記述させる形でスライドに記載する本文を生成させます。

results = []
for t in topics:
    query = f'{t}について要点を記述してください。'
    time.sleep(10)
    result = retrieval_chain_agent({"question": query, "chat_history": []})
    results.append(result["answer"])

4. スライドの生成

ConversationalRetrievalChainに設定されているプロンプトの影響で、このエージェントに安定してスライドを作成させることができなかったため、この部分のみLangChainを使用せず、直接OpenAIのAPIを使用しています。
また、Few-Shotでスライドの形式を例示しなくてもMarpで使える形式で出力できますが、正確性を担保したいため今回プロンプトに入れています。

status = '''
あなたは優秀なエバンジェリストです。与えられた文章について簡潔に説明するためのスライドを作成します。
スライドを作成する際は次のようなMarpのデザインテンプレートを使用したマークダウン形式で表現されます。
箇条書きの文はできるだけ短くまとめてください。
"""

---
<!-- スライド n -->
# { タイトル }

- { 本文 }
- { 本文 }
- { 本文 } 

"""
'''
prefix = [
        {"role": "system", "content": status},
]

slides = []
for r in results:
    data = f"""
次に与えられる文章の内容についてMarpによるマークダウン形式でスライドを日本語で作成してください。
必要に応じてスライドは2枚以上に分割してください:
{r}

    """
    comversation = [{"role": "user", "content": data}]
    messages = prefix + comversation
    response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=messages,
    temperature = 0,
    )
    time.sleep(10)
    answer = response["choices"][0]["message"]["content"]
    slides.append(answer)

5. 生成したスライドの書き出し

最後に生成したテキストをマークダウン形式のファイルとして出力します。
今回は冒頭のスライド設定を手動で設定しています。

output_str = """---
marp: true
_theme: gaia
paginate: true
backgroundColor: #f5f5f5

"""
for i in slides:
    print(i)
    output_str += i + "\n"

with open("slide.md", "w") as file:
    file.write(output_str)

実行例

最終的な出力は以下の通りです。(gif形式に変換しています)

最後に

大量のテキストデータから、簡潔にまとめたスライドを作成することができました。
スライドのデザインがチープであったり、各トピックごとにスライドを作成しているため論理展開や見出しレベルに統一感がないなど改善点がまだまだあります。
今回使用したプロンプトは時間をかけて練り上げたものではないため、プロンプトの工夫次第で色々改善できる可能性が高いと思います。
これ記事がみなさんの作業効率の向上に貢献できれば幸いです。

参考

https://qiita.com/ydty/items/39d39ad5d5b6448d55fc

https://qiita.com/hiroki_okuhata_int/items/7102bab7d96eb2574e7d

脚注
  1. https://platform.openai.com/docs/models/gpt-4 ↩︎

  2. https://platform.openai.com/docs/guides/chat/do-you-store-the-data-that-is-passed-into-the-api ↩︎

  3. https://python.langchain.com/en/latest/modules/agents/agents/custom_agent.html?highlight=retriever custom ↩︎

Discussion