Featured image of post LangChainの可能性:LLMを特定の情報ソースと組み合わせて利用するアプリの模索

LangChainの可能性:LLMを特定の情報ソースと組み合わせて利用するアプリの模索

最近、ChatGPTを含むLLM(Large Language Model)が注目を集めていますね。

これらのモデルは、人間に近い精度を実現し、自然言語によるコミュニケーションの可能性を広げており、 個人的にはこれら言語モデルを利用した対話システムや情報インターフェースなどへの応用に期待を感じています。

LangChainとは

一方でLLMが本当のことのように嘘を述べてしまう、いわゆるHallunicationが問題になっています。

LLMはあくまでも大量のデータをもとに自動的に学習された言語モデルであるため、専門知識や文脈に関する認識が限定されることがあります。 私は研究者ではないので詳しいことはわかりませんが、使う側からすれば汎用言語モデルに特定の専門知識そのものを求めることに違和感を感じており、 そもそもとしてLLMにそういったアウトプットを求めていません。

それよりは、LLMをツールとして特定の文献や学習コンテンツと連携することで、学習効率を高めるようなアプリを開発できる可能性に注目しています。

例えば、BingやGithub Copilot、ChatGPT Pluginなどは、LLMを活用した特定の領域にフォーカスしたアプリケーションの一部だと思っています。

LangChain は、こういったLLMを活用したアプリケーションの開発を支援するライブラリです。

⚡ Building applications with LLMs through composability ⚡ - GitHub - hwchase17/langchain: ⚡ Building applications with LLMs through composability ⚡
GitHub - hwchase17/langchain: ⚡ Building applications with LLMs through composability ⚡

LLMをツールとして特定の文献や学習コンテンツと連携することで、学習効率を高めるようなアプリについては以下の「Data Augumented Generation」にユースケースとして記載されています。

Data Augmented Generation — 🦜🔗 LangChain 0.0.123

人間の教師と学習していても、別の文献やソースでファクトチェックや考察視点の補完をするものです。

PDF文献をソースとして学習するユースケースを考えてみる

上記のような「LLMをツールとして特定の文献や学習コンテンツと連携することで、学習効率を高めるようなアプリ」として、このようなアプリを考えてみます。

ここでは以下の記事で紹介されている、大きなテキストをvector index化して自然言語で検索する例を応用して、PDFドキュメントを検索してみます。

「LangChain」の「データ拡張生成」の使い方をまとめました。 前回 1. データ拡張生成 「データ生成拡張」は、特定のデータに基づいて言語モデルでテキスト生成する手法です。 言語モデルは、大量の非構造化データで学習しているため、汎用のテキスト生成に最適ですが、特定のデータに基づいてテキスト生成したい場合も多くあります。 データ生成拡張が必要になる場合は、次のとおりです。 ・特定のテキストの要約 (Webサイト、プライベートな文書など) ・特定のテキストに対する質問応答 (Webサイト、プライベートな文書など) ・複数のテキストに対する質問応答&
LLM連携アプリの開発を支援するライブラリ LangChain の使い方 (2) - データ拡張生成|npaka|note

環境の準備

Google Colabで必要なパッケージをインストールします。

1
2
3
4
!pip install langchain
!pip install openai
!pip install faiss-gpu
!pip install pypdf

続いてOpenAIのAPI keyをセットします。

1
2
import os
os.environ["OPENAI_API_KEY"] = "xxx"

OpenAIのAPIは有料です。ChatGPT plusとは別料金体系で、従量課金になっています。

Simple and flexible. Only pay for what you use.
Pricing

2023年3月現在では登録時に3ヶ月間利用できる$5分のクレジットがもらえますので、試しに利用してみる分にはそれで十分かと思います。

ドキュメントの準備

今回PDFドキュメントには、azu (azu) · GitHubさんがCC BY NNで公開されているJavascript Promiseの薄い本を利用させていただきます。

JavaScript Promiseの本. Contribute to azu/promises-book development by creating an account on GitHub.
GitHub - azu/promises-book: JavaScript Promiseの本

ちなみに私がこの本に出会ったのは2014年くらいだったと思います。 現在でもAsync Functionが追記されているなど精力的にメンテされているようで、今でも初学者の方におすすめできる良書かと思います。

検索アプリの用意とテスト

必要なライブラリをインポートします。

1
2
3
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.document_loaders import PyPDFLoader
from langchain.vectorstores.faiss import FAISS

LangChainのドキュメントには様々なドキュメントの利用方法が記載されているので参考になります。 ここでのPDFの読み込みには、以下のPDFセクションで紹介 pypdf を利用する方法を使います。

PDF — 🦜🔗 LangChain 0.0.123

1
2
loader = PyPDFLoader("/content/javascript-promise-book.pdf")
pages = loader.load_and_split()

これで、1ページ単位でPDFを分割できました。これをFAISSに渡して自然言語による検索を実施します。

1
2
3
4
5
6
7
8
docsearch = FAISS.from_documents(pages, OpenAIEmbeddings())
docs = docsearch.similarity_search("Promise.then?の使い方は?", k=2)

print(len(pages))
print(len(docs))
#print(docs[0].page_content)
for doc in docs:
  print(str(doc.metadata["page"]) + ":", doc.page_content)

「Promise.then?の使い方は?」 と質問した場合の検索結果を見てみます。

118

2

ページ分割と結果の提示は意図通りにできているようです。

17: JavaScript Promiseの本 18ThenableについてはPromiseを使ったライブラリを書くとき等には知っておくべきで すが、 通常の利用だとそこまで使う機会がないものかもしれません。 ThenableとPromise.resolveの具体的な例を交えたものは 第4章 のPromise.resolveとThenable にて詳しく解説しています。 Promise.resolve を簡単にまとめると、「渡した値でFulfilledされるpromiseオブ ジェクトを返すメソッド」と考えるのがいいでしょう。 また、Promiseの多くの処理は内部的に Promise.resolve のアルゴリズムを使って 値をpromiseオブジェクトに変換しています。 Promise.reject Promise.reject(error) は Promise.resolve(value) と同じ静的メソッドで new Promise() のショートカットとなるメソッドです。 例えば、 Promise.reject(new Error(“エラー”)) というのは下記のコードのシン タックスシュガーです。 new Promise( function (resolve,reject){ reject( new Error(“エラー” )); }); 返り値のpromiseオブジェクトに対して、thenの onRejected に設定された関数に エラーオブジェクトが渡ります。 Promise.reject( new Error(“BOOM!”)).catch(function (error){ console.error(error); }); Promise.resolve(value) との違いは resolveではなくrejectが呼ばれるという点 で、 テストコードやデバッグ、一貫性を保つために利用する機会などがあるかもし れません。 コラム: Promiseは常に非同期? Promise.resolve(value) 等を使った場合、 promiseオブジェクトがすぐにresolve されるので、 .then に登録した関数も同期的に処理が行われるように錯覚してしま います。

これは「Promise.resolve」に関する説明の節で Thenable を説明しているところのちょうど最後にあたるページをポイントしているので悪くない結果だと思います。

29: JavaScript Promiseの本 30つまり、 Promise#then は単にコールバックとなる関数を登録するだけではなく、 受け取った値を変化させて別のpromiseオブジェクトを生成する という機能も持っ ていることを覚えておくといいでしょう。 Promise#catch 先ほどのPromise#then についてでも Promise#catch は既に使っていましたね。 改めて説明すると Promise#catch は promise.then(undefined, onRejected); のエ イリアスとなるメソッドです。 つまり、promiseオブジェクトがRejectedとなった 時に呼ばれる関数を登録するためのメソッドです。 Promise#then とPromise#catch の使い分けについては、 then or catch?で紹介しています。 IE8以下での問題 このバッジは以下のコードが、 polyfill27 を用いた状態でそれぞれのブラウザで 正しく実行できているかを示したものです。 polyfillとはその機能が実装されていないブラウザでも、その 機能が使えるようにするライブラリのことです。 この例では jakearchibald/es6-promise28 を利用しています。 Promise#catchの実行結果 var promise = Promise.reject( new Error(“message” )); promise. catch(function (error) { 27 https://github.com/jakearchibald/es6-promise 28 https://github.com/jakearchibald/es6-promise

こちらは「Promise.catch」を説明する節ですので、質問の意図からすると外れている気がします。

k=4 として結果を増やしてみると、以下も追加で出力されました。

111: JavaScript Promiseの本 112•thenをその場に並べた書き方 •forループを使った書き方 •reduceを使った書き方 •逐次処理する関数を分けた書き方 しかし、これはJavaScriptで配列を扱うのにforループや forEach 等、色々やり方 があるのと本質的には違いはありません。 そのため、Promiseを扱う場合も処理を まとめられるところは小さく関数に分けて、実装していくのがいいと言えるでしょ う。 まとめ このセクションでは、 Promise.all とは違い、 一つづつ順番に処理したい場合 に、Promiseでどのように実装していくかについて学びました。 手続き的な書き方から、逐次処理を行う関数を定義するところまで見ていき、 Promiseであっても関数に処理を分けるという基本的な事は変わらないことを示しま した。 Promiseで書くとPromise chainを繋げすぎて縦に長い処理を書いてしまうことがあ ります。 そんな時は基本に振り返り、処理を関数に分けることで全体の見通しを良くするこ とは大切です。 また、Promiseのコンストラクタ関数や then 等は高階関数なので、 処理を関数に 分けておくと組み合わせが行い易いという副次的な効果もあるため、意識してみる といいかもしれません。 高階関数とは引数に関数オブジェクトを受け取る関数のこと Promises API Reference Promise#then promise.then(onFulfilled, onRejected); thenコード例

API Reference章の「Promise.then」の解説が出力されました。これも妥当と思われます。

73: JavaScript Promiseの本 74この時、returnしたものがpromiseオブジェクトである場合、そのpromiseオブジェ クトの状態によって、 次の then に登録されたonFulfilledとonRejectedのうち、 どちらが呼ばれるかを決めることが出来ます。 var promise = Promise.resolve(); promise.then( function () { var retPromise = new Promise( function (resolve, reject) { // resolve or reject で onFulfilled or onRejected どちらを呼ぶか決まる }); return retPromise; }).then(onFulfilled, onRejected); 次に呼び出されるthenのコールバックはpromiseオブジェクトの状態によって決 定される つまり、この retPromise がRejectedになった場合は、 onRejected が呼び出され るので、 throw を使わなくても then の中でrejectすることが出来ます。 var onRejected = console.error.bind(console); var promise = Promise.resolve(); promise.then( function () { var retPromise = new Promise( function (resolve, reject) { reject( new Error(“this promise is rejected” )); }); return retPromise; }).catch(onRejected); これは、 the section called “Promise.reject” を使うことでもっと簡潔に書く ことが出来ます。 var onRejected = console.error.bind(console); var promise = Promise.resolve(); promise.then( function () { return Promise.reject( new Error(“this promise is rejected” )); }).catch(onRejected); まとめ このセクションでは、以下のことについて学びました。 •throw ではなくて reject した方が安全 •then の中でもrejectする方法

これは「throwしないでrejectしよう」の説明です。 Advanced章にはひとつ前に「Prmose.resolveとThenable」があるので、どちらかというとこちらを出力してほしかったところですね。

まとめ

一例しか示せていませんが、PDFドキュメントをvector index化して自然言語で検索するアプリの可能性を模索してみました。

まず、このようなアプリケーションをたった数行で構築できるだけでもLangChainの可能性の高さが伺えます。

結果を見てみると、そこそこ利用できそうな印象を受けます。やはり入力が自然言語であるためか不安定な面はありそうです。 質問の仕方によっても結果に感じる精度は変わってくるので、同じことを言い方を変えて質問してみるとよいかもしれません。 また検索結果も絞り込まず、なるべく多めに出力しておく方がよいでしょう。

LangChainにはPDF以外にもソースにできる例が沢山用意されており、WebページやRoam、Evernoteなどもありました。 デジタルガーデンなど自身のノートの検索に利用するしたり、ソースを変えたりマッシュアップすることで様々な応用ができそうです。

ちなみにLangChainにはPythonの他に、JavaScriptによる実装も提供されています。

Contribute to hwchase17/langchainjs development by creating an account on GitHub.
GitHub - hwchase17/langchainjs

今回はドキュメント検索の結果を出すだけの利用でしたが、より対話的に回答を得られるように次は「RetrievalQA」を使ってみたいと思います。

Built with Hugo
テーマ StackJimmy によって設計されています。