CCCMKホールディングス TECH Labの Tech Blog

TECH Labスタッフによる格闘記録やマーケティング界隈についての記事など

Large Language Model(LLM)をもっと活用したい!"LangChain"を使ってみました。

こんにちは、CCCMKホールディングス TECH LABの三浦です。

"シャドーイング"という英語の学習方法があり、最近試してみています。これは英語の音声を聞きながら、それを追いかけるように発音する、という方法で、ヒアリングやスピーキング力の改善に効果があるそうです。英語を発音しようとするとなかなか思ったように口が回らないのですが、英語を話すための口周りの筋肉が整っていない、とったことも要因としてあるようです。動画を見ながら発声練習を始めてみたので、今後改善されるといいな、と期待しています。

最近はLarge Language Model(LLM)について、毎日のように新しい情報がインターネットなどで見つかります。本当にホットな話題なんだな、と感じています。このブログでも最近LLMによりよい指示を与えるためのPrompt Engineeringのテクニックについて、最近発表された論文などを中心にまとめていました。

LLMを中心とした新しい研究が活発に行われている一方で、エンジニアリングの観点でLLMをもっと効率的に活用するためのフレームワークなどの開発も行われていることを知りました。そのうちの一つに今回取り上げる"LangChain"というフレームワークがあります。今回はこのLangChainを使ってどんなことが出来るのか、簡単にまとめてみましたのでご紹介したいと思います。

LangChain

LangChainはLLMを利用したアプリケーションの開発をスムーズにしてくれるフレームワークです。コンセプトとして、LLMで外部のデータを活用出来るようにすること、LLMとインタラクティブにやり取りが出来るようにすることでよりパワフルでユニークなアプリケーションの開発をサポートしてくれます。

python.langchain.com

今回使ってみて特に有難いと思った点は外部の様々なデータをLLMに取り込める機能が整っている点です。テキストファイルはもちろん、PDFやWeb上のコンテンツなど、あらゆるデータをロードしてLLMに入力し、その情報を元にしたQ&Aアプリケーションを作ることが出来ます。特にGPT-3などは一度に入力できるテキストの長さに制限がありますが、テキストデータを一度埋め込みベクトルに変換し、それを利用することでその制限を超えたテキストも扱うことが出来るようになっています。

セットアップ

LangChainはpipコマンドでインストールすることが出来ます。バックグラウンドでopenaiのライブラリも利用するため、もしインストールしていない場合はこちらもインストールしておく必要があります。

pip install openai langchain

私は今回、Azure OpenAIでGPT-3を利用しました。Azure OpenAIに接続するための各種設定はOSの環境変数にセットしました。

OPENAI_API_KEY=********
OPENAI_API_BASE=https://*********/
OPENAI_API_VERSION=****-**-**
OPENAI_API_TYPE=azure

とりあえず動かしてみる!

まずはlangchainを使ってLLMにテキストを入力し、それに続くテキストを取得する処理を書いてみます。以下の様な簡単なコードで実現することが出来ます。

from langchain.llms import AzureOpenAI
llm = AzureOpenAI(deployment_name='text-davinci-003',temperature=0.7)
print(llm('こんにちは、今日の天気は'))

AzureOpenAIのパラメータdeployment_nameには使用するModelの名称ではなく、Deploymentの名称を指定します。temperaturemax_tokensといったパラメータも指定することが出来ます。実行結果は以下の様になります。

晴れですね。

はい、今日はとても穏やかな天気ですね。最高気温も上がるので、散歩や外出などを楽しめそうですね。

LLMのAPIを利用していると気になるのがその実行にかかるコストだと思いますが、それを表示することも可能です。

from langchain.callbacks import get_openai_callback

with get_openai_callback() as cb:
  print(llm('こんにちは、今日の天気は'))
  print(cb)

出力結果です。

晴れですね。

はい、本当にいいお天気ですね!気温も上がってきて、良い季節になってきました。今日は散歩などで外に出て楽しみたいと思います!
Tokens Used: 116
    Prompt Tokens: 16
    Completion Tokens: 100
Successful Requests: 1
Total Cost (USD): $0.00232

USDではあるものの、常にコストが把握できるのは有難いです!

TemplateによるPromptの生成

LLMに入力するPromptはある程度の型が決まっていることが多いです。その型をlangchainではPromptTemplateで表現することが出来ます。PromptTemplateを使って、ユーザーからあるカテゴリと商品名を受け取るとそれにぴったりのキャッチコピーを作成してくれる処理を書いてみます。以下のようになります。

from langchain import PromptTemplate
template = """
あなたは新しい商品のキャッチコピーを提案するAIです。

{category}の{item}という新商品にはどんなキャッチコピーがいいですか?
"""

prompt_template = PromptTemplate(
  input_variables=['category','item'],
  template=template
)

print(
  llm(
    prompt_template.format(
      category='飲み物',
      item='さっぱりしソーダ'
    )
  )
)

上のコードでは、GUIなどを通じてユーザーが入力したカテゴリや商品名が変数categoryitemにそれぞれセットされた後、それを展開したPromptを生成し、LLMに入力する・・・といったことを行っています。この結果は以下の様になります。

「さっぱりとする新しい味!ソーダで、毎日を楽しくしよう!」

Chatアプリケーションの作成

LLMと交互にメッセージをやり取りするような、Chat機能を持ったアプリケーションを作ることが出来ます。まず、必要なモジュールをimportします。

from langchain.chains import LLMChain

from langchain.prompts import (
  ChatPromptTemplate,
  SystemMessagePromptTemplate,
  HumanMessagePromptTemplate
)

LangChainには"Chain"という概念があります。複数の処理をまとめたもので、Chainを利用することでユーザーからの入力を受け取り、Promptを作成し、そのPromptをLLMに入力する、といった複数の処理をまとめて実行出来るようになります。

以下では、Chatに必要になるAIの基本的な動作を定めるsystem_message_promptと、ユーザーの入力を受け付けるhuman_message_promptを作成しています。この2つのpromptによって、Chatアプリ全体のPromptであるchat_promptを定義しています。

system_template = """"
あなたは料理のレシピを答えるAIアシスタントです。ユーザーが入力した料理のレシピを答えてください。
"""
system_message_prompt = SystemMessagePromptTemplate.from_template(system_template)
human_template = "{text}"
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)

chat_prompt = ChatPromptTemplate.from_messages(
  [
    system_message_prompt,
    human_message_prompt
  ]
)

そして使用するllmpromptを指定することでChainを作成します。

chain = LLMChain(
  llm=AzureOpenAI(
    deployment_name='text-davinci-003',
    temperature=0.7,
    max_tokens=1024
    ),
    prompt=chat_prompt
)

あとはこのchainをしてChatアプリを利用することが出来ます。一度に複数の入力を指定することも出来ます。

response = chain.run(text=['サバの味噌煮','鶏のから揚げ'])
print(response)

レシピの内容はちょっと怪しいですが、以下の様に出力されます。

System: 
サバの味噌煮のレシピ:
材料:サバ1尾、酒、砂糖、醤油、味噌

1.サバを水洗いし、穴あけをします。

2.鍋にサバを入れ、酒、砂糖、醤油を加えて中火にかけます。

3.表面に膜ができたら、醤油と味噌を加えて中火にかけ、煮詰めます。

4.サバを皿に盛り付け、召し上がれ。

鶏のから揚げのレシピ:
材料:鶏もも肉、卵、パン粉、オイスターソース、塩、胡椒

1.鶏もも肉を切り分け、塩と胡椒を加えて混ぜます。

2.卵を一つ一つつぶして、鶏もも肉に入れます。

3.パン粉を鶏もも肉にまぶします。

4.フライパンに油を入れ、鶏もも肉を揚げます。

5.オイスターソースをかけ、召し上がれ。

過去のやり取りを記憶したChatアプリケーションの作成

これまでの会話のやり取りを記憶したChatアプリケーションを作成することも出来ます。ConversationBufferMemoryを利用します。

from langchain.memory import ConversationBufferMemory

template = """
あなたは明るくて楽しいチャットAIです。ユーザーと楽しい会話をしてください。

{chat_history}
Human:{human_input}
AI:
"""

chat_prompt = PromptTemplate(
  input_variables=['chat_history','human_input'],
  template=template
)

memory = ConversationBufferMemory(memory_key='chat_history')

chain = LLMChain(
  llm=AzureOpenAI(
    deployment_name='text-davinci-003',
    temperature=0.7,
    max_tokens=1024
  ),
  prompt=chat_prompt,
  memory=memory
)

以下、やりとりの内容です。

Human:

chain.run('こんにちは!')

AI:

'こんにちは!今日は何をしましたか?'

Human:

chain.run('今日はずっと仕事をしています・・・、あなたはどうですか?')

AI:

'あなたの仕事は大変ですね!私は今日はお昼に歩いていました。その後、友達と会って映画を見ました。今日は一日楽しかったです!あなたはどんな日を過ごしましたか?'

友達と映画に行ったみたいです!いいなー。最近映画見に行っていなかったので、久しぶりに行きたくなりました。それにしても何の映画を見に行ったのでしょうか?気になったので聞いてみます。

Human:

chain.run('LLMについて調査していました。あなたはどんな映画を見に行ったんですか?')

AI:

'今日見た映画は「ジョーズ」でした!私たちは本当に楽しくて、笑いもありました!あなたの調査はどんなものでしたか?'

まさかの「ジョーズ」!楽しめたみたいで良かったです。ではここで過去のやり取りの情報が必要になる質問を投げてみます。

Human:

chain.run('私は今日、何をしてたって言いましたっけ?')

AI:

'今日は仕事をしていましたね!どんな仕事をしていましたか?'

ちゃんと覚えていてくれました!これはConversationBufferMemoryによって過去のやり取りを含んだPromptがLLMに入力されているためです。それにしてもグイグイ尋ねて来ますね。ちょっと戸惑ってしまいました。

外部のデータソースを元にしたQ&Aアプリケーションの作成

最後に外部のデータをソースにしたQ&Aアプリケーションをlangchainで作ってみました。データソースの形式は幅広く対応しているのですが、今回はMarkdownファイル(mdファイル)を使ってみました。

私はこのブログを書くとき、最初にMarkdownファイルで下書きを書いています。以下の2つの記事を書いた時の下書き用のMarkdownをデータソースとして使用してみました。

techblog.cccmk.co.jp techblog.cccmk.co.jp

まず追加で必要になるライブラリをインストールします。

pip install chromadb tiktoken unstructured

chromadbはデータソースのテキストを埋め込み表現化した後に格納するために利用し、tiktokenはOpenAIのモデルで利用されるトークナイザでunstructuredは様々な非構造化データを読み取るために利用します。

LangChainには"Document Loaders"という様々なデータをロードする機能が用意されています。まずそれを使ってMarkdownファイルをロードしてみます。

from langchain.document_loaders import UnstructuredMarkdownLoader
loader = UnstructuredMarkdownLoader(
  '/path/to/file/blog1.md'
)
print(loader.load())

Markdownの内容が表示されます。Documentというオブジェクトにデータが格納されていることが分かります。

[Document(page_content='Contrastive Learningで学習したモデルを画像検索に応用してみました。\n\nこんにちは、技術開発の三浦です。\n\n似ている表現を持つ画像同士を近づけて、似ていない表現を持つ画像同士は遠ざける。この手続きを通して画像が持つ潜在的な表現を抽出することが出来るモデルを学習させる手法が「Contrastive Learning」です。最近このContrastive Learningに興味を持っていて、色々と調べています。\n\n今回はContrastive Learningで学習したモデルを使い、与えられた画像(クエリ画像)に対して似ている画像を検索することが出来るかを試してみました。\n\n動機\n\n最近、画像分類モデルを学習するために必要なラベル付きデータを効率的に作る方法はないかな、と考えていました。ラベルがついていない画像データに対し、1つ1つ確認しながらラベルを付けていたのですが、とても大変な作業です。\n\n時と場合、そして人に依ると思いますが、...

読み込みたいMarkdownファイルは2つあるので、2つとも読み込みます。

mkdws = [
  UnstructuredMarkdownLoader(
    '/path/to/file/blog1.md'
  ).load(),
  UnstructuredMarkdownLoader(
    '/path/to/file/blog2.md'
  ).load(),
]

読み込めたのはいいのですが、このままだとテキストの長さが長すぎてAPIに入力することが出来ません。そこで次にこのテキストを細かく分割します。RecursiveCharacterTextSplitterを使うことで指定のサイズ(chunk_sizeパラメータで指定)ごとにテキストを分割することが出来ます。chunk_overlapはテキストを分割する際に1つ前の細分化されたテキストの末尾の部分を、その次の細分化されたテキストの先頭にどれくらい加えるかを指定するパラメータです。とりあえず20を指定してみました。

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 1000,
    chunk_overlap  = 20,
    length_function = len,
)

docs_CL = text_splitter.split_documents(mkdws[0])
docs_LLM = text_splitter.split_documents(mkdws[1])

docs = docs_CL + docs_LLM

これでデータソースを加工することが出来たのですが、次に何をするのかというと、このデータソースに含まれるテキストに対する埋め込み表現をLLMに生成させます。埋め込み表現はそのテキストの特徴を表現するベクトルです。

埋め込み表現を生成するLLMはEmbeddingに分類されるモデルで、Azur OpenAIだと"text-embedding-ada-002"がそれにあたります。以下の処理でdocsに含まれるテキストに対する埋め込みを取得し、それをdbに格納します。

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma

embeddings = OpenAIEmbeddings(model="text-embedding-ada-002",chunk_size=1)
db = Chroma.from_documents(docs, embeddings)

ここで注意点があるのですが、OpenAIEmbeddingsmodelパラメータに指定するのはAzure OpenAIの場合Deploymentの名称であることに変わりはないのですが、その名称は"text-embedding-ada-002"の様にModelの名称と同じにする必要があります。たとえば"text-embedding-ada-002"というModelを"embedding-model"というDeploymentにdeployし、OpenAIEmbeddingsmodelパラメータに指定するとKeyError: 'Could not automatically map embedding-ada to a tokeniser.というエラーが発生します。

また、OpenAIEmbeddingschunk_size=1もマストな設定なようで、これが抜けているとエラーが発生します。

最後にChainを作ります。データソースから質問に該当するテキストを抽出し、それを元に回答を返す一連の処理はRetrievalQAWithSourcesChainを使って実装することが出来ます。 (パラメータchain_typeには色々指定することが出来る様ですが、まだあまり試すことが出来ていません。)

from langchain.chains import RetrievalQAWithSourcesChain

chain = RetrievalQAWithSourcesChain.from_chain_type(
  AzureOpenAI(
    deployment_name='text-davinci-003',
    temperature=0.7,
    max_tokens=1024
  ),
  chain_type="refine", 
  retriever=db.as_retriever(search_kwargs={"k": 1})
)

ではこのChainを使っていくつか記事に関連する質問を投げてみます。

chain(
  {"question":"Contrastive Learningについて教えてください。"},
  return_only_outputs=True
)

これに対して、以下の様に回答が得られました。

{'answer': '\nContrastive Learningとは、似ている表現を持つ画像同士を近づけて、似ていない表現を持つ画像同士は遠ざけるような手法です。この手法を用いることで、画像が持つ潜在的な表現を抽出するモデルを学習させることが出来ます。また、ラベル付きの画像データを効率的に作る手段としても使える可能性があります。',
 'sources': ''}

これは私が書いた表現にかなり沿った回答になっています!ではこんな質問はどうでしょうか?

chain(
  {"question":"三浦さんが最近興味を持っていることについて教えてください。"},
  return_only_outputs=True
)

これはこのデータソースを見ない限り、知りえない情報ですが、回答は以下の様になりました。

{'answer': '\n三浦さんは最近、プロンプトエンジニアリング(LLM)に関する調査に関心を持っているようです。LLMは三浦さんにとって新しい分野であるため、インプットに割く時間が多くなっているようです。また、今まで得た知識を使ってアウトプットするための時間も取るつもりだと述べています。',
 'sources': ''}

記事を読まない限り、出せない回答になっています!こんな感じでたくさんのデータソースを元にしたQ&Aアプリケーションを作ることが出来ました。

まとめ

ということで、今回はLangChainを使ってLLMをより実践的な用途で試してみました。ここまで色々なことがLLMを使うと出来るのか・・・と、とても驚きました。LangChainを通して、これまで知らなかったLLMの魅力を知ることが出来ました。LLM周辺は本当に面白い技術領域で、引き続き、色々調べてみたいと思います!