製造でのトピックモデリング
美容とファッションの専門家による、製造業におけるトピックモデリング
LangChainを活用して、アドホックなJupyter Notebooksから本番のモジュール化されたサービスへの移行
前の記事では、ChatGPTを使用してトピックモデリングを行い、優れた結果を得る方法について説明しました。課題はホテルチェーンの顧客レビューを見て、レビューで言及されている主なトピックを定義することでした。
前回の実装では、標準的なChatGPTの補完APIを使用し、自分自身で生の入力プロンプトを送信しました。このようなアプローチは、アドホックな分析的研究を行う場合にはうまく機能します。
しかし、チームが積極的に顧客レビューを使用し、監視している場合は、自動化を検討する価値があります。良い自動化は、自律的なパイプラインを構築するだけでなく、より便利(LLMやコーディングに慣れていないチームメンバーでもこのデータにアクセスできる)で、より効率的です(すべてのテキストをLLMに送信し、一度だけ支払うことができます)。
持続可能な本番用サービスを構築していると仮定しましょう。その場合、既存のフレームワークを活用してグルーコードの量を削減し、よりモジュラーなソリューションを実現することが価値があります(たとえば、別のLLMに簡単に切り替えることができるように)。
この記事では、LLMアプリケーションの最も人気のあるフレームワークの1つであるLangChainについて詳しく紹介します。また、ビジネスアプリケーションにおいて重要なステップであるモデルのパフォーマンス評価についても詳しく理解します。
本番プロセスのニュアンス
初期アプローチの見直し
まず、ChatGPTによるアドホックなトピックモデリングの前のアプローチを見直しましょう。
ステップ1:代表的なサンプルを取得します。
マークアップに使用するトピックのリストを決定したいです。最も簡単な方法は、すべてのレビューを送信し、LLMによってレビューで言及されている20〜30のトピックのリストを定義してもらうことです。残念ながら、文脈サイズに収まらないため、これを行うことはできません。Map-Reduceアプローチを使用することもできますが、コストがかかる可能性があります。そのため、代表的なサンプルを定義したいと考えています。
このために、BERTopicトピックモデルを構築し、各トピックの最も代表的なレビューを取得しました。
ステップ2:マークアップに使用するトピックのリストを決定します。
次のステップは、選択したテキストをすべてChatGPTに渡し、これらのレビューで言及されているトピックのリストを定義してもらうことです。その後、これらのトピックを後でマークアップに使用できます。
ステップ3:バッチでトピックのマークアップを行います。
最後のステップは最も簡単です-コンテキストサイズに合わせて顧客レビューをバッチで送信し、LLMに各顧客レビューのトピックを返すように依頼することです。
以上の3つのステップで、テキストの関連トピックのリストを決定し、すべてのテキストを分類できます。
これは1回限りの研究には完璧に機能します。ただし、優れた本番用ソリューションにはいくつかの要素が不足しています。
アドホックから本番へ
初期のアドホックアプローチを改善するための改善点について話しましょう。
- 前のアプローチでは固定のトピックリストを持っています。しかし、実際の例では、新しいトピックが時間の経過とともに発生する場合があります。たとえば、新しい機能を導入する場合です。そのため、使用しているトピックのリストを更新するためのフィードバックループが必要です。最も簡単な方法は、トピックの割り当てがないレビューのリストをキャプチャし、定期的にそれらにトピックモデリングを実行することです。
- 1回限りの研究の場合、トピックの割り当て結果を手動で検証できます。しかし、本番で実行されているプロセスでは、継続的な評価について考える必要があります。
- 顧客レビュー分析のためのパイプラインを構築している場合、他の潜在的なユースケースを考慮し、必要な関連情報を保存する必要があります。例えば、Google Translateをいつも使わなくても済むように、顧客レビューの翻訳バージョンを保存すると便利です。また、感情や他の特徴(たとえば、顧客レビューで言及されている製品)は、分析やフィルタリングに役立つ場合があります。
- LLM業界は現在非常に急速に進化しており、すべてが常に変化しています。サービス全体をゼロから書き直すことなく、短期間で素早くイテレーションし、新しいアプローチを試すことができるモジュラーアプローチを検討する価値があります。
私たちはトピックモデリングサービスについてたくさんのアイデアを持っています。しかし、メインの部分に焦点を当ててみましょう。API呼び出しと評価の代わりにモジュラーアプローチを採用しましょう。LangChainフレームワークは、両方のトピックをサポートするために役立ちますので、詳細を学んでみましょう。
LangChainの基本
LangChainは、言語モデルを活用したアプリケーションを構築するためのフレームワークです。LangChainの主要なコンポーネントは以下の通りです:
- スキーマは、ドキュメント、チャットメッセージ、テキストなどの基本的なクラスです。
- モデル。LangChainは、LLMs、チャットモデル、テキスト埋め込みモデルにアクセスすることができます。必要に応じてこれらのモデルを簡単にアプリケーションで使用したり切り替えたりすることができます。ChatGPT、Anthropic、Llamaなどの人気モデルもサポートされています。
- プロンプトは、プロンプトの作業を支援する機能です。プロンプトテンプレートや出力パーサ、Few-shotプロンプトのための例選択子などが含まれます。
- チェインはLangChainの中核です。チェインを使用すると、実行されるブロックのシーケンスを構築できます。複雑なアプリケーションを構築している場合、この機能の効果を実感することができます。
- インデックス:ドキュメントローダー、テキスト分割、ベクトルストア、およびリトリーバー。このモジュールは、LLMがドキュメントと対話するためのツールを提供します。Q&Aのユースケースを構築している場合に有用です。ただし、今日の例ではこの機能をあまり使用しません。
- LangChainは、メモリを管理および制限するための一連のメソッドを提供します。この機能は主にChatBotのシナリオで必要です。
- 最新かつ強力な機能の1つはエージェントです。ChatGPTの重度のユーザーであれば、プラグインについて聞いたことがあるかもしれません。同じアイデアで、LLMにカスタムまたは事前定義されたツールセット(Google検索やWikipediaなど)を活用させることができます。そしてエージェントは、回答する際にそれらを使用できます。この設定では、LLMは推論エージェントのように振る舞い、結果を達成するために何をすべきか、最終的な答えを得るために何を共有できるかを判断します。これは非常にエキサイティングな機能なので、それについては別の議論に値します。
したがって、LangChainを使用すると、モジュラーアプリケーションを構築し、異なるコンポーネント間を切り替えることができます(例えば、ChatGPTからAnthropicへの切り替え、データ入力としてのCSVからSnowflake DBへの切り替えなど)。LangChainには190以上の統合機能があり、時間を節約することができます。
また、ゼロからではなく、いくつかのユースケースに対して既製のチェーンを再利用することもできます。
ChatGPT APIを手動で呼び出す際には、Pythonの結合コードを多く管理する必要があります。小規模で単純なタスクを作業している場合は問題ありませんが、より複雑で煩雑なものを構築する必要がある場合、この結合コードは扱いにくくなる可能性があります。そのような場合、LangChainを使用することで、この結合コードを排除し、メンテナンスしやすいモジュラーコードを作成することができます。
ただし、LangChainには以下の制限もあります:
- 主にOpenAIモデルに焦点を当てているため、オンプレミスのオープンソースモデルとの連携はうまくいかない場合があります。
- 便利さの裏返しとして、支払っているChatGPT APIがどのように実行されるのか、内部で何が起こっているのかを理解するのは容易ではありません。デバッグモードを使用することはできますが、明確なビューを得るために完全なログを参照する必要があります。
- かなり優れたドキュメントがあるにもかかわらず、質問に対する回答を見つけるのに時折苦労します。公式のドキュメント以外の他のチュートリアルやリソースはあまり多くなく、Googleでは公式のページしか見ることができません。
- LangChainライブラリは進化が早く、チームが常に新機能を提供しています。したがって、ライブラリは未熟であり、使用している機能から切り替える必要があるかもしれません。たとえば、
SequentialChain
クラスは現在レガシーと見なされ、将来的に廃止される可能性があるため、後ほどLCELについて詳しく説明します。
LangChainの機能の概要を把握しましたが、実践こそが完璧さを生むのです。LangChainの使用を始めましょう。
トピックの割り当ての向上
通常のプロセスにおいてもっともよく行われる操作であるトピックの割り当てをリファクタリングしましょう。それによって、LangChainの実践的な使い方を理解するのに役立ちます。
まず、パッケージをインストールする必要があります。
!pip install --upgrade langchain
ドキュメントの読み込み
顧客のレビューを扱うためには、まずそれらを読み込む必要があります。そのためにはドキュメントローダーを使用することができます。私たちの場合、顧客のレビューはディレクトリ内の一連の.txtファイルとして保存されていますが、サードパーティのツールからも簡単にドキュメントを読み込むことができます。たとえば、Snowflakeとの統合があります。
ホテルごとに別々のファイルがあるため、DirectoryLoader
を使用してディレクトリ内のすべてのファイルを読み込みます(デフォルトでは非構造化ドキュメント用のローダーが使われます)。ファイルごとに、ローダーとしてTextLoader
を指定します。ファイルはISO-8859-1
でエンコードされているため、デフォルトの呼び出しではエラーが発生します。ただし、LangChainは自動的に使用されているエンコーディングを検出することができます。この設定で正常に動作します。
from langchain.document_loaders import TextLoader, DirectoryLoadertext_loader_kwargs={'autodetect_encoding': True}loader = DirectoryLoader('./hotels/london', show_progress=True, loader_cls=TextLoader, loader_kwargs=text_loader_kwargs)docs = loader.load()len(docs)82
ドキュメントの分割
さて、ドキュメントを分割したいと思います。各ファイルは\n
で区切られた一連の顧客コメントで構成されていることがわかっています。私たちの場合は非常に簡単なので、文字ごとにドキュメントを分割するCharacterTextSplitter
を使用します。実際のドキュメント(個々の短いコメントではなく、長いテキスト全体)を扱う場合は、よりスマートにドキュメントを分割できるRecursive split by characterを使用することが望ましいです。
ただし、LangChainはテキストの曖昧な分割により適しています。したがって、私はそれを私の望む方法で動作するように少しハックしました。
動作原理:
chunk_size
とchunk_overlap
を指定すると、各チャンクがchunk_size
より小さくなるように、最小限の分割を試みます。十分に小さなチャンクを作成できない場合は、Jupyter Notebookの出力にメッセージを表示します。chunk_size
を大きすぎると、すべてのコメントが分離されない場合があります。chunk_size
を小さすぎると、出力の各コメントにプリント文が表示され、Notebookがリロードされることになります。残念ながら、それをオフにするためのパラメータは見つけられませんでした。
この問題を解決するために、length_function
をchunk_size
として定数として指定しました。結果的に、標準的な文字ごとの分割が行われます。LangChainには望み通りの操作を行う十分な柔軟性がありますが、ある程度のハックが必要です。
from langchain.text_splitter import CharacterTextSplittertext_splitter = CharacterTextSplitter( separator = "\n", chunk_size = 1, chunk_overlap = 0, length_function = lambda x: 1, # 通常はlenが使用されます is_separator_regex = False)split_docs = text_splitter.split_documents(docs)len(split_docs) 12890
また、メタデータにドキュメントIDを追加しましょう。後で使用します。
for i in range(len(split_docs)): split_docs[i].metadata['id'] = i
Documentsを使用する利点は、自動的なデータソースの作成とそれによるデータのフィルタリングができることです。たとえば、Travelodge Hotelに関連するコメントのみをフィルタリングすることができます。
list(filter( lambda x: 'travelodge' in x.metadata['source'], split_docs))
次に、モデルが必要です。LangChainで以前に話し合ったように、LLMとChat Modelsがあります。主な違いは、LLMはテキストを受け取りテキストを返すのに対して、Chat Modelsは対話型の使用例に適しており、メッセージのセットを入力として受け取ることができます。今回は、システムメッセージも渡したいので、OpenAIのChatModelを使用します。
from langchain.chat_models import ChatOpenAIchat = ChatOpenAI(temperature=0.0, model="gpt-3.5-turbo", openai_api_key = "your_key")
プロンプト
では、最も重要な部分であるプロンプトに移りましょう。LangChainでは、プロンプトテンプレートという概念があります。これらは、変数によってパラメータ化されたプロンプトを再利用するのに役立ちます。実際のアプリケーションでは、プロンプトは非常に詳細で洗練されたものになる場合があるため、プロンプトテンプレートは効果的にコードを管理するのに役立つ高レベルの抽象化です。
今回はChat Modelを使用するので、ChatPromptTemplateが必要です。
しかし、プロンプトに飛び込む前に、便利な機能である出力パーサーについて簡単に説明しましょう。驚くほど、効果的なプロンプトの作成に役立つことがあります。出力の形を定義し、出力パーサーを生成し、それからパーサーを使用してプロンプトの命令を作成することができます。
出力に表示されるものを定義しましょう。まず、バッチ処理で顧客レビューのリストをプロンプトに渡せるようにしたいので、結果として次のパラメータが含まれるリストを取得したいと思います:
- 文書を識別するためのid
- 事前定義されたリストからのトピックのリスト(前の反復からのリストを使用します)
- 感情(ネガティブ、中立、ポジティブ)
出力パーサーを指定しましょう。非常に複雑なJSON構造が必要なので、最も一般的に使用されるStructured Output Parserの代わりにPydantic Output Parserを使用します。
そのために、BaseModel
から継承したクラスを作成し、名前と説明を含むすべての必要なフィールドを指定する必要があります(LLMが応答で期待しているものを理解できるようにします)。
from langchain.output_parsers import PydanticOutputParserfrom langchain.pydantic_v1 import BaseModel, Fieldfrom typing import Listclass CustomerCommentData(BaseModel): doc_id: int = Field(description="入力パラメータからのdoc_id") topics: List[str] = Field(description="顧客レビューの関連トピックのリスト。提供されたリストからのみトピックを含めてください。") sentiment: str = Field(description="コメントの感情(positive, neutral, negative)")output_parser = PydanticOutputParser(pydantic_object=CustomerCommentData)
そして、このパーサーを使ってプロンプトの形式指示を生成できるようになりました。これは、プロンプトのベストプラクティスを使用し、プロンプトエンジニアリングにかける時間を短縮する素晴らしいケースです。
format_instructions = output_parser.get_format_instructions()print(format_instructions)
それから、プロンプトに進む時が来ました。コメントのバッチを取り、期待される形式に整形しました。次に、topics_descr_list
、format_instructions
、input_data
という多くの変数を持つプロンプトメッセージを作成しました。その後、定数のシステムメッセージとプロンプトメッセージから構成されるチャットプロンプトメッセージを作成しました。最後のステップは、実際の値を使用してチャットプロンプトメッセージを形式化することです。
from langchain.prompts import ChatPromptTemplatedocs_batch_data = []for rec in docs_batch: docs_batch_data.append( { 'id': rec.metadata['id'], 'review': rec.page_content } )topic_assignment_msg = '''以下はJSON形式の顧客レビューのリストで、次のキーが含まれます:1. doc_id - レビューの識別子2. review - 顧客レビューのテキスト提供されたレビューを分析し、主要なトピックと感情を特定してください。以下のリストからトピックのみを含めてください。以下のリストには、次のように説明が含まれます(「:」で区切られています):{topics_descr_list}出力形式:{format_instructions}顧客レビュー:```{input_data}```'''topic_assignment_template = ChatPromptTemplate.from_messages([ ("system", "あなたは役に立つアシスタントです。ホテルのレビューを分析するのがお仕事です。"), ("human", topic_assignment_msg)])topics_list = '\n'.join( map(lambda x: '%s: %s' % (x['topic_name'], x['topic_description']), topics))messages = topic_assignment_template.format_messages( topics_descr_list = topics_list, format_instructions = format_instructions, input_data = json.dumps(docs_batch_data))
今、これらのフォーマットされたメッセージをLLMに渡して応答を確認できます。
response = chat(messages)type(response.content)strprint(response.content)
応答は文字列オブジェクトとして得られましたが、パーサを利用してCustomerCommentData
クラスオブジェクトのリストを結果として取得することもできます。
response_dict = list(map(lambda x: output_parser.parse(x), response.content.split('\n')))response_dict
これにより、LangChainとそのいくつかの機能を利用して、コメントをバッチごとにトピックに割り当て(コストを節約できます)だけでなく、センチメントも定義し始めるなど、少しスマートなソリューションを構築しました。
より多くのロジックを追加
これまでは、関係やシーケンスなしで単一のLLM呼び出しのみを構築しました。しかし実際の現場では、タスクを複数のステップに分割することがよくあります。そのために、Chainsを使用できます。ChainsはLangChainの基本的なビルディングブロックです。
LLMChain
最も基本的なチェーンのタイプはLLMChainです。これはLLMとプロンプトを組み合わせたものです。
ですので、ロジックをチェーンに書き直すことができます。このコードで以前とまったく同じ結果が得られますが、すべてを定義する一つのメソッドを持っていると非常に便利です。
from langchain.chains import LLMChaintopic_assignment_chain = LLMChain(llm=chat, prompt=topic_assignment_template)response = topic_assignment_chain.run( topics_descr_list = topics_list, format_instructions = format_instructions, input_data = json.dumps(docs_batch_data))
シーケンシャルチェーン
LLMチェーンは非常に基本的です。チェーンの力はより複雑なロジックの構築にあります。より高度なものを作成してみましょう。
シーケンシャルチェーンのアイデアは、1つのチェーンの出力を別のチェーンの入力として使用することです。
チェーンを定義するために、LCEL(LangChain Expression Language)を使用します。この新しい言語は数カ月前に導入され、現在はSimpleSequentialChain
やSequentialChain
などの古いアプローチは非推奨とされています。ですので、LCELの概念を理解するために少し時間をかける価値があります。
前のチェーンをLCELで書き直してみましょう。
chain = topic_assignment_template | chatresponse = chain.invoke( { 'topics_descr_list': topics_list, 'format_instructions': format_instructions, 'input_data': json.dumps(docs_batch_data) })
直接学びたい場合は、LangChainチームが提供するこのビデオをご覧ください。
シーケンシャルチェーンの使用
いくつかの場合では、1つのチェーンの出力を他のチェーンで使用することが役立つ場合があります。
この場合、まずレビューを英語に翻訳し、次にトピックモデリングとセンチメント分析を行うことができます。
from langchain.schema import StrOutputParserfrom operator import itemgetter# 翻訳translate_msg = '''Below is a list of customer reviews in JSON format with the following keys:1. doc_id - identifier for the review2. review - text of customer reviewPlease, translate review into English and return the same JSON back. Please, return in the output ONLY valid JSON without any other information.Customer reviews:```{input_data}```'''translate_template = ChatPromptTemplate.from_messages([ ("system", "You're an API, so you return only valid JSON without any comments."), ("human", translate_msg)])# トピックの割り当てとセンチメント分析topic_assignment_msg = '''Below is a list of customer reviews in JSON format with the following keys:1. doc_id - identifier for the review2. review - text of customer reviewPlease, analyse provided reviews and identify the main topics and sentiment. Include only topics from the provided below list.List of topics with descriptions (delimited with ":"):{topics_descr_list}Output format:{format_instructions}Customer reviews:```{translated_data}```'''topic_assignment_template = ChatPromptTemplate.from_messages([ ("system", "You're a helpful assistant. Your task is to analyse hotel reviews."), ("human", topic_assignment_msg)])# チェーンの定義translate_chain = translate_template | chat | StrOutputParser()topic_assignment_chain = {'topics_descr_list': itemgetter('topics_descr_list'), 'translated_data': translate_chain, 'format_instructions': itemgetter('format_instructions')} | topic_assignment_template | chat # 実行response = topic_assignment_chain.invoke( { 'topics_descr_list': topics_list, 'format_instructions': format_instructions, 'input_data': json.dumps(docs_batch_data) })
翻訳とトピックの割り当てのために、プロンプトテンプレートを同様に定義しました。そして、翻訳チェーンを決定しました。新しいことは、StrOutputParser()
の使用です。これはレスポンスオブジェクトを文字列に変換します(ロケット科学ではありません)。
次に、入力パラメータ、プロンプトテンプレート、LLMを指定して、フルチェーンを定義しました。入力パラメータでは、translate_chain
の出力からtranslated_data
を使用し、他のパラメータはitemgetter
関数を使用して呼び出し入力から取得しました。
ただし、私たちの場合、連結されたチェーンでこのようなアプローチを使用すると、最初のチェーンの出力も保存して翻訳された値を持つことが非常に便利ではないかもしれません。
チェーンを使用すると、すべてが少しややこしくなるため、デバッグの能力が必要になる場合があります。デバッグのオプションは2つあります。最初のオプションは、ローカルでデバッグをオンにすることです。
import langchainlangchain.debug = True
もう1つのオプションは、LangChainプラットフォームである「LangSmith」を使用することですが、まだベータテスターモードですので、アクセスするには待つ必要があるかもしれません。
ルーティング
チェーンの最も複雑なケースの1つは、さまざまなユースケースに対して異なるプロンプトを使用したルーティングです。例えば、顧客のレビューに応じて、異なるカスタマーレビューパラメータを保存することができます。
- コメントがネガティブな場合、顧客が言及した問題のリストを保存します。
- それ以外の場合、レビューから良いポイントのリストを取得します。
ルーティングチェーンを使用するには、コメントを一つずつ渡す必要があります(以前とは異なり、バッチ処理は行いません)。
したがって、上位レベルでの私たちのチェーンは次のようになります。
まず、感情(センチメント)を決定する主要なチェーンを定義する必要があります。このチェーンはプロンプト、LLM、そして既に知っているStrOutputParser()
から構成されています。
sentiment_msg = '''顧客のコメントを次のように分類してください:コメントがネガティブの場合、「negative」と返し、そうでない場合は「positive」と返します。1つの単語以上の返答しないでください。顧客のコメント:```{input_data}```'''sentiment_template = ChatPromptTemplate.from_messages([ ("system", "あなたはアシスタントです。ホテルのレビューのセンチメントをマークアップすることがあなたのタスクです。"), ("human", sentiment_msg)])sentiment_chain = sentiment_template | chat | StrOutputParser()
ポジティブなレビューの場合、モデルに良い点を抽出してもらい、ネガティブな場合は問題点を抽出してもらう必要があります。したがって、2つの異なるチェーンが必要です。以前と同じPydanticのアウトプットパーサーを使用して、意図した出力形式を指定し、指示を生成します。
私たちは、ポジティブとネガティブのケースに異なるフォーマット指示を指定するために、一般的なトピック割り当てプロンプトメッセージの上にpartial_variables
を使用しました。
from langchain.prompts import PromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate# positiveとnegativeのケースの構造を定義class PositiveCustomerCommentData(BaseModel): topics: List[str] = Field(description="提供されたリストからの関連トピックのリストを含めてください") advantages: List[str] = Field(description = "顧客が言及した良い点のリスト") sentiment: str = Field(description="コメントのセンチメント(positive、neutral、negative)")class NegativeCustomerCommentData(BaseModel): topics: List[str] = Field(description="提供されたリストからの関連トピックのリストを含めてください") problems: List[str] = Field(description = "顧客が言及した問題のリスト") sentiment: str = Field(description="コメントのセンチメント(positive、neutral、negative)")# アウトプットパーサーの定義と指示の生成positive_output_parser = PydanticOutputParser(pydantic_object=PositiveCustomerCommentData)positive_format_instructions = positive_output_parser.get_format_instructions()negative_output_parser = PydanticOutputParser(pydantic_object=NegativeCustomerCommentData)negative_format_instructions = negative_output_parser.get_format_instructions()general_topic_assignment_msg = '''以下は```.で区切られた顧客のレビューです。提供されたレビューを分析し、主要なトピックと感情を特定してください。提供された下記のリストからトピックのみを含めてください。トピックのリストと説明(「:」で区切られる):{topics_descr_list}出力形式:{format_instructions}顧客のレビュー:```{input_data}```'''# promptテンプレートを定義positive_topic_assignment_template = ChatPromptTemplate( messages=[ SystemMessagePromptTemplate.from_template("あなたは助けになるアシスタントです。ホテルのレビューを分析することがあなたのタスクです。"), HumanMessagePromptTemplate.from_template(general_topic_assignment_msg) ], input_variables=["topics_descr_list", "input_data"], partial_variables={"format_instructions": positive_format_instructions} )negative_topic_assignment_template = ChatPromptTemplate( messages=[ SystemMessagePromptTemplate.from_template("あなたは助けになるアシスタントです。ホテルのレビューを分析することがあなたのタスクです。"), HumanMessagePromptTemplate.from_template(general_topic_assignment_msg) ], input_variables=["topics_descr_list", "input_data"], partial_variables={"format_instructions": negative_format_instructions} )
それでは、完全なチェーンを構築する必要があります。メインのロジックは RunnableBranch
で定義され、感情に基づく条件を使用して、sentiment_chain
の出力に依存します。
from langchain.schema.runnable import RunnableBranchbranch = RunnableBranch( (lambda x: "negative" in x["sentiment"].lower(), negative_chain), positive_chain)full_route_chain = { "sentiment": sentiment_chain, "input_data": lambda x: x["input_data"], "topics_descr_list": lambda x: x["topics_descr_list"]} | branchfull_route_chain.invoke({'input_data': review, 'topics_descr_list': topics_list})
以下はいくつかの例です。これは非常にうまく機能し、感情に応じて異なるオブジェクトを返します。
LangChain を使用したトピックモデリングのモジュールアプローチとより複雑なロジックの詳細について説明しました。次は、モデルのパフォーマンスを評価する方法について説明します。
評価
本番稼働しているシステムの重要な部分は、評価です。本番稼働中の LLM モデルの品質を保証し、長期間にわたって監視する必要があります。
多くの場合、ヒューマンインザループ(人々がモデル結果を小規模なサンプルについて定期的にチェックしてパフォーマンスを管理する方法)だけでなく、このタスクに LLM を活用することもできます。ランタイムチェックにはより複雑なモデルを使用すると良いでしょう。例えば、トピックの割り当てには ChatGPT 3.5 を使用しましたが、評価には GPT 4 を使用できます(実生活の監督の概念に似ており、より上級者の同僚にコードをレビューしてもらうイメージです)。
LangChain は、結果を評価するためのツールも提供してくれるため、このタスクをサポートしてくれます:
- String Evaluators は、モデルの結果を評価するのに役立ちます。フォーマットの検証から、提供されたコンテキストやリファレンスに基づいた正確性の評価まで、幅広いツールがあります。以下ではこれらの方法について詳しく話します。
- もう一つの評価者のクラスは、Comparison evaluators です。2つの異なる LLM モデル(A/B テストのユースケース)のパフォーマンスを評価したい場合に便利です。詳細には触れませんが、詳細はあります。
完全一致
最も直接的なアプローチは、モデルの出力を正解(エキスパートやトレーニングセットから)と比較することです。完全に一致するかどうかを評価するために、ExactMatchStringEvaluator
を使用することができます。この場合、LLM は必要ありません。
from langchain.evaluation import ExactMatchStringEvaluatorevaluator = ExactMatchStringEvaluator( ignore_case=True, ignore_numbers=True, ignore_punctuation=True,)evaluator.evaluate_strings( prediction="positive.", reference="Positive")# {'score': 1}evaluator.evaluate_strings( prediction="negative", reference="Positive")# {'score': 0}
独自のカスタム String Evaluatorを作成したり、出力を正規表現と一致させるためのツールもあります。
また、出力が妥当な JSON であるか、期待される構造を持ち、参照に対して距離が近いかどうかを検証するための便利なツールもあります。詳細については、ドキュメンテーションを参照してください。
埋め込み距離評価
別の便利なアプローチは、埋め込み間の距離を見ることです。結果にスコアが表示されます。スコアが低いほどよく、回答が互いに近いことを示します。たとえば、ユークリッド距離で見つかった良い点を比較できます。
from langchain.evaluation import load_evaluatorfrom langchain.evaluation import EmbeddingDistanceevaluator = load_evaluator( "embedding_distance", distance_metric=EmbeddingDistance.EUCLIDEAN)evaluator.evaluate_strings( prediction="目のいい部屋、きれい、素晴らしい場所", reference="目のいい部屋、きれい、素晴らしい場所、いい雰囲気"){'score': 0.20732719121627757}
0.2の距離を得ました。ただし、このような評価の結果は、データの分布を見て、閾値を定義する必要があるため、解釈が困難な場合があります。さて、LLMに基づく手法に移行しましょう。そうすれば、その結果を簡単に解釈することができます。
基準の評価
LLMの回答をある基準やガイドラインに照らし合わせるために、LangChainを使用することができます。事前定義された基準のリストがありますが、カスタムの基準も作成することができます。
from langchain.evaluation import Criterialist(Criteria)[<Criteria.CONCISENESS: 'conciseness'>, <Criteria.RELEVANCE: 'relevance'>, <Criteria.CORRECTNESS: 'correctness'>, <Criteria.COHERENCE: 'coherence'>, <Criteria.HARMFULNESS: 'harmfulness'>, <Criteria.MALICIOUSNESS: 'maliciousness'>, <Criteria.HELPFULNESS: 'helpfulness'>, <Criteria.CONTROVERSIALITY: 'controversiality'>, <Criteria.MISOGYNY: 'misogyny'>, <Criteria.CRIMINALITY: 'criminality'>, <Criteria.INSENSITIVITY: 'insensitivity'>, <Criteria.DEPTH: 'depth'>, <Criteria.CREATIVITY: 'creativity'>, <Criteria.DETAIL: 'detail'>]
一部の基準は参照を必要としません(たとえば、harmfulness
やconciseness
など)。ただし、correctness
の場合は、正解を知る必要があります。それでは、データに使用してみましょう。
evaluator = load_evaluator("criteria", criteria="conciseness")eval_result = evaluator.evaluate_strings( prediction="目のいい部屋、きれい、素晴らしい場所", input="顧客が挙げた良い点をリストアップしてください",)
結果として、指定された基準に結果がフィットするかどうか(正しいかどうか)と、推論の過程が得られます。これにより、結果の裏にある論理を理解し、プロンプトを調整する可能性があります。
動作原理に興味がある場合、langchain.debug = True
をオンにして、LLMに送られるプロンプトを確認できます。
次に、正確性基準を見てみましょう。これを評価するには、参照(正しい回答)を提供する必要があります。
evaluator = load_evaluator("labeled_criteria", criteria="correctness")eval_result = evaluator.evaluate_strings( prediction="目のいい部屋、きれい、素晴らしい場所", input="顧客が挙げた良い点をリストアップしてください", reference="目のいい部屋、きれい、素晴らしい場所、いい雰囲気",)
自分自身でカスタム基準を作成することもできます。たとえば、回答に複数のポイントが含まれているかどうかなどの基準です。
custom_criterion = {"multiple": "出力に複数のポイントが含まれていますか?"}evaluator = load_evaluator("criteria", criteria=custom_criterion)eval_result = evaluator.evaluate_strings( prediction="目のいい部屋、きれい、素晴らしい場所", input="顧客が挙げた良い点をリストアップしてください",)
スコアリング評価
基準評価では、イエスまたはノーの回答しか得られませんが、多くの場合、それだけでは十分ではありません。例えば、この例では、予測のうち4つのポイントのうち3つが言及されており、それは良い結果ですが、正確性の評価ではNとなっています。したがって、このアプローチを使用すると、「デザインの良い部屋、清潔、素晴らしい場所」と「高速インターネット」の回答は、私たちの評価基準の観点では同じ価値となり、モデルのパフォーマンスを理解するために十分な情報を提供してくれません。
また、LLMに出力でスコアを提供するという、かなり近いテクニックもあります。これにより、より詳細な結果を得ることができます。試してみましょう。
from langchain.chat_models import ChatOpenAI
accuracy_criteria = {
"accuracy": """スコア1:回答は関連するポイントを何も言及していません。
スコア3:回答は一部の関連するポイントを言及していますが、主要な不正確さや数多くの関連のないオプションを含んでいます。
スコア5:回答には中程度の量の関連オプションがありますが、不正確さや誤ったポイントがあるかもしれません。
スコア7:回答は参照と一致し、最も関連するポイントを示し、完全に間違ったオプションは言及されていません。
スコア10:回答は完全に正確で参照と完全に一致しています。"""
}
evaluator = load_evaluator(
"labeled_score_string",
criteria=accuracy_criteria,
llm=ChatOpenAI(model="gpt-4"),
)
eval_result = evaluator.evaluate_strings(
prediction="デザインの良い部屋、清潔、素晴らしい場所",
input="""下記はクライアントのレビューで ``` によって区切られ、クライアントのレビューに記載されている良いポイントのリストを提供します。
カスタマーレビュー:
```
小さいがデザインの良い部屋、清潔、素晴らしい場所、良い雰囲気です。また泊まりたいです。コンチネンタルブレックファーストは弱いですが、大丈夫です。
```
""",
reference="デザインの良い部屋、清潔、素晴らしい場所、良い雰囲気"
)
スコアとして7を得ました、これはかなり妥当です。実際に使用されたプロンプトを見てみましょう。
ただし、LLMからのスコアは慎重に扱う必要があります。回帰関数ではなく、スコアはかなり主観的なものになる可能性があります。
参照と一緒にスコアリングモデルを使用してきましたが、正しい回答が必要ない場合や、得るのが困難な場合もあります。モデルに回答を評価させるためにスコアリング評価者を参照スコアなしで使用することもできます。GPT-4を使用することで、より信頼性の高い結果を得ることができます。
accuracy_criteria = {
"recall": "アシスタントの回答には、質問で言及されたすべての情報が含まれる必要があります。情報が欠落している場合は、スコアが低くなります。",
"precision": "アシスタントの回答には、質問に存在しないポイントを含めてはいけません。"
}
evaluator = load_evaluator("score_string", criteria=accuracy_criteria, llm=ChatOpenAI(model="gpt-4"))
eval_result = evaluator.evaluate_strings(
prediction="デザインの良い部屋、清潔、素晴らしい場所",
input="""下記はクライアントのレビューで ``` によって区切られ、クライアントのレビューに記載されている良いポイントのリストを提供します。
カスタマーレビュー:
```
小さいがデザインの良い部屋、清潔、素晴らしい場所、良い雰囲気です。また泊まりたいです。コンチネンタルブレックファーストは弱いですが、大丈夫です。
```
"""
)
前のスコアにかなり近いスコアが得られました。
さまざまな方法で出力を検証する方法を見てきましたので、自分のモデルの結果をテストする準備ができたことを願っています。
概要
この記事では、製品開発プロセスにLLM(Large Language Models)を使用する場合に考慮すべき微妙な点について説明しました。
- 私たちは、LangChainフレームワークの使用について詳しく見てきました。このフレームワークを使用することで、新しいアプローチ(例えば、異なるLLMへの切り替え)を容易に反復し、採用することができるようになります。また、フレームワークは通常、コードの保守性を高めるのに役立ちます。
- もう1つの主要なトピックは、モデルのパフォーマンスを評価するためのさまざまなツールについて議論しました。LLMを本番環境で使用する場合、サービスの品質を確保するために定期的なモニタリングが必要です。また、LLMや人間を組み合わせた評価パイプラインを作成するためには、時間をかける価値があります。
この記事をお読みいただき、ありがとうございました。お役に立てたことを願っています。ご質問やコメントがありましたら、コメントセクションにお願いします。
データセット
Ganesan, KavitaおよびZhai, ChengXiang(2011)「OpinRank Review Dataset」UCI Machine Learning Repository。 https://doi.org/10.24432/C5QW4W
参考文献
この記事は、DeepLearning.AIおよびLangChainの「LangChain for LLM Application Development」というコースの情報に基づいています。 「LangChain for LLM Application Development」
We will continue to update VoAGI; if you have any questions or suggestions, please contact us!
Was this article helpful?
93 out of 132 found this helpful
Related articles