こんにちは!逆瀬川( https://twitter.com/gyakuse )です!
今日はLangChainの使い方について書いていこうと思います。
ChatGPT API の欠点について
LangChainについて書く前に、ChatGPT APIの使いづらい部分をまとめていきたいと思います。
これを考えておくと、なぜLangChainが必要であるかということがわかり、さらに今後どのような機能が搭載されうるか/されるべきかということがわかります。
ChatGPT APIを使う際の難しい部分は一般的に以下のようにまとめられます。
- プロンプトの共通化や管理が面倒くさい
- 最近の事実をベースとした質問-応答が難しい
- 最大の入出力合計が4096トークン(約3000字)であるため、長い情報を持たせることがしづらい
- ExcelやCSV、PDF等を直接読み込ませることができない
- 出力の処理のチェーンの実装がたいへん
これらの課題は、以下のようなサービスを作る際に問題となります。
- 人間らしいふるまいをするChatbotの開発
- 任意の書類を処理するシステムの開発
- Make/IFTTTのような言語モデルが途中に入った使ったツール連携の開発
LangChainはそれらの課題を克服するためのツールです。
実際に、わたしが実装した議事録書き出し・サマリ作成やCSVデータからのレポート自動作成など、いくつかのシステムはLangChainで置き換えることが可能です。
LangChainが実装するもの
上記を踏まえて、以下のようなものが必要であるということがわかります。
- プロンプト管理機構
- 任意の形式のファイル入力への対応
- ローカルの文章検索
- 会話記憶
- 情報要約
- botとしてのインターフェイス
こうしたてんこもり系ツールは設計が難しいため、今後破壊的な変更などもある可能性がありますが、
少なくとも利便性を加味して変更を追っておく必要はあるでしょう。
LangChainを使わないという選択
ちなみにわたしはLangChainは中身は追っていますが、ほとんど使っていません。
理由はいくつかありますが、単純に破壊的変更があった場合に動かない/古いコードとなってしまうことと、
機序を理解して実装しておきたいというモチベーションがあるためです。
また、シンプルな実装であればほとんど同じ機能を作ることはコストがあまりかかりません。
それと、個人的には、データの入力部分と記憶の部分は独立していると嬉しい気持ちがあります。
そうした意味でLlamaIndexも便利なのですが、これもデカくなっているので注意が必要と考えます。
とはいえサクッと実装できるうれしみが強いので、とりあえず実装して公開するというときには積極的に使っていくべきでしょう。ちなみにmicrosoftから出た画像対話(画像に関するQAや編集等)を行えるシステムの論文でもLangChainが内部的に使われています。
LangChainの各モジュール
それでは、LangChainに実際に用意されている各モジュールを見ていきます。
なお、副読用としてcolabを用意しました。動かしながら遊びましょう。
Prompt Templates: プロンプトテンプレート
これはプロンプトの管理を行うための機構です。
プロンプトテンプレートでは独自のプロンプトを管理したり、LangChainHubからプロンプトテンプレートを呼び出すなどができます。
独自のプロンプトを作成する場合は以下のような実装になります。
PromptTemplateクラスを用いて作成し、そのインスタンスに対してformat関数を実行することで作成されたプロンプトを使うことができます。
ここでは以前書いたプレゼン作成自動化の任意の概念についての説明出力用プロンプトを使用します。
from langchain import PromptTemplate
template = """{item}とは何ですか?
説明してください。説明においては以下のルールに従ってください。
・リスト形式で出力する (先頭は ・ を使う)
・わかりやすい文章で丁寧に説明する
A:"""
prompt = PromptTemplate(
input_variables=["item"],
template=template,
)
作るうえでは、以下に注意してください。
- templateに f-strings 形式と同様に変数を
{}
で囲って定義する - templateを f-strings 形式にしない (
f"""~~~"""
と書かない) - input_valiablesにリスト形式で入れる
これをプロンプトとして出力させるときは以下のように使います。
prompt.format(item="りんご")
これはprintすると、以下のように出力されます。
りんごとは何ですか?
説明してください。説明においては以下のルールに従ってください。
・リスト形式で出力する (先頭は ・ を使う)
・わかりやすい文章で丁寧に説明する
A:
変数がないときは prompt.format()
で大丈夫です。
LLMs: 言語モデルラッパー
言語モデルのラッパーです。OpenAI GPT-3 (text-davinci-003) などを使う場合は以下のように呼び出します。
from langchain.llms import OpenAI
gpt = OpenAI(model_name="text-davinci-003", openai_api_key=openai_key)
gpt("織田信長について教えて下さい")
max tokens(出力可能なトークン数) やtemperatureを変えるときは以下のようにします。
from langchain.llms import OpenAI
gpt = OpenAI(
model_name="text-davinci-003",
max_tokens=512,
temperature=1,
frequency_penalty=0.02,
openai_api_key=openai_key
)
gpt("織田信長について教えて下さい")
こちらについてはnpakaさんの記事に詳しいです。
Document Loaders: ドキュメントの読み取り
任意のドキュメント(PDF, csv, 画像など)を読み取る機構です。
Images(画像), PDF, CSV, Markdown, Email, Word, Notion, Evernote, YouTube, Google Driveなどに対応しています。
多くの部分がunstructuredに依存しています。
たとえばPDFの読み取りは以下のクラスが使えます。
- OnlinePDFLoader
- 内部的にUnstructuredPDFLoaderが呼び出されています
- UnstructuredPDFLoader
- unstructuredを使ったローダーです
- PDFMinerLoader
- pdfminer.sixを使ったローダーです
- PyMuPDFLoader
- PyMuPDFを使ったローダーです
- PagedPDFSplitter
- ページ単位で取得できるローダーです
- pypdfを使っています
なお、PyMuPDFはAGPL-3.0ライセンスであるため、これを使ったソフトウェアを提供する場合AGPLが伝播する気がします。
たとえば PagedPDFSplitter を使うと以下のように実装できます。
from langchain.document_loaders import PagedPDFSplitter
loader = PagedPDFSplitter(target_pdf_file_path)
pages = loader.load_and_split()
print(pages[0].page_content[:200])
Utils
検索APIのラッパーなどが入っています。とりあえず突っ込んだ感があるのであとで整理されることでしょう。
Indexes: インデックス
大規模文書等からQAするというモチベーションを解決するために必要な機構です。
テキストデータを任意文字列等で分割したり、埋め込みにしたり、
Vector Storeに入れたり、近傍探索する処理を統括しています。
このあたりの機構がなぜ必要なのかや埋め込みがどう便利なのか等は以下にまとめています。
テキストの分割
テキストの分割は以下のように実装できます。
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 100,
chunk_overlap = 20,
length_function = len,
)
texts = text_splitter.create_documents([sample_text])
単純にn文字ずつチャンクにするのとは異なり、overlapで重複部分を作ることができるため、文章が突然切れて言語モデルが困ることがなさそうです。
埋め込み作成
今回はOpenAIのEmbeddings APIを用います。
Embeddings APIは非英語は推奨されないと以前は説明されていたのですが、最近なくなったので何らかの対応がされたのだと思われます。
参考: https://twitter.com/OfficialLoganK/status/1621942366914461699
from langchain.embeddings import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
query_result = embeddings.embed_query(text)
一括して行う場合は以下を実行します
doc_result = embeddings.embed_documents([text])
これにより、1536次元の埋め込み表現が取得できます
Vector Storeへの保存
ここではFaissを使って保存します。
# Vector Store
from langchain.vectorstores import FAISS
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 500,
chunk_overlap = 20,
length_function = len,
)
docs = text_splitter.create_documents([sample_text])
embeddings = OpenAIEmbeddings(openai_api_key=openai_key)
db = FAISS.from_documents(docs, embeddings)
探索は以下のように行います。
query = "埋め込みのつらみ"
results = db.similarity_search(query)
print(results[0].page_content)
References