NLP2024 に参加しました

こんにちは! 技術部機械学習グループの山口 (@altescy) です。

先月、神戸にて開催された言語処理学会第30回年次大会 (NLP2024)に同じく機械学習グループの深澤 (@fufufukakaka)と共に参加してきました。 昨年に引き続き今年も過去最多の参加者数となり、言語処理研究の盛り上がりを実感しました。

特に去年の年次大会 (NLP2023) のタイミングで GPT-4 が発表されて以降、自然言語処理の研究は大きな転換期を迎えていると感じます。 大規模言語モデル (LLM) が研究の主流となる中、どんな課題や発見があるのか、期待をもって参加する大会となりました。

この記事では NLP2024 にてクックパッドから発表した 2 つの研究と、その他の興味深かった研究についていくつか紹介します。

発表内容の紹介

クックパッドからは以下 2 つの研究を発表しました。

P2-11: Sequential Recommendation におけるテキスト情報を活用した未知アイテムへの対処法に関する分析

推薦モデルの一種として、Sequential Recommendation Modelがあります。これはアイテムIDの系列情報を基に次にアクションすべきアイテムを推薦するモデルです。このモデルは未知のユーザーであっても、既知のアイテムで構成された履歴があれば推薦が可能です。しかし、未知のアイテムが入力として与えられた場合、そのアイテムはモデルにとってout-of-vocabularyであり、入力に使用することはできません。実際のサービスで推薦モデルを運用する上では、学習時に存在しなかった新着アイテムを無視せざるを得ないですが、仮にそうしたアイテムだけを見ているユーザがいたとすると、そのユーザには推薦を表出できません。 この研究では、未知のアイテムが入力された際に最も効果的にそれを取り扱う方法を検討しました。

本研究ではクックパッドにおける実際の閲覧履歴データ、及びNII IDRで公開しているつくれぽデータセットを使用しました。Sequential Recommendation Modelとしては、2021年SIGIRで発表されたCOREを使用します。

今回、未知アイテムを取り扱う方法として3つの手法を提案しました。

  1. テキスト情報を用いた未知アイテムのID埋め込み推測(Embedding Mapping)
    • 未知アイテムが持つ情報としてテキスト情報(本研究ではレシピタイトル)を使用します。
    • 予め学習された推薦モデルのID埋め込みとアイテムのテキスト情報を対にして、テキスト情報からID埋め込みを推定するモデルを学習します。
    • 未知アイテムが入力された際には、そのアイテムのテキスト情報を用いてID埋め込みを推定し、そのID埋め込みを用いて推薦を行います。
    • この手法の構築にはBERT及びLSTMを使用しました。
  2. 類似度が高いアイテムによる置換(Replace Similar Item)
    • 未知アイテムが入力された際に、そのアイテムとテキストの類似度が高い既知アイテムを入力履歴から探し出し、そのアイテムを未知アイテムの代わりに入力として使用します。
  3. 未知アイテムを入力履歴から除外(Ignore)
    • 未知アイテムが入力された際に、そのアイテムを入力履歴から除外し、残りのアイテムのみを使用して推薦を行います。

履歴の先頭または末尾から最大5つが未知アイテムであると仮定します。未知アイテムとなったものについては、推薦モデルから対象のID埋め込みを削除して入力に利用できないようにします。

以下が実験結果となります。それぞれのデータセットに対して、各手法を適用した際のNDCG@50の結果です。

NDCG@50 の結果一覧

まず分かる基本的なこととして、履歴の先頭側よりも末尾側(より最近のアイテム)が未知である場合の方が精度影響が大きいことが見て取れます。各手法に関する結果としては、今回提案した手法である Embedding Mapping が他手法と比べて十分な精度を出すには至らず、多くの場合でシンプルな別手法が上回る結果となりました。今回対象とした系列が10要素以上のもののみを対象としていたこともあり、単純に未知アイテムを除外する Ignore がほとんどのケースで最も良い性能を示しました。ただ、閲覧データ・つくれぽデータのどちらを対象としていたかによって若干結果は異なっており、類似アイテムが並びやすい閲覧データに対しては Replace Similar Item が有効に働く場面もありました。

この結果に関連して以下のような分析を行いました。 事前にレシピテキストで学習されたfastTextベクトルでコサイン類似度が0.8以上となるタイトル類似ペアを100組抽出します。その後、Embedding Mapping(LSTM)とCOREのID埋め込みベクトルを使ってペアのコサイン類似度を計算し、その平均を算出しました。

類似ペアの調査

この結果を見ると、本来近づけたかった Embedding Mapping(LSTM) と CORE との間で全く異なる傾向が見られています。fastTextベクトルの類似度が高いレシピ同士であっても、CORE のID埋め込みの類似度は比較的低めであることがわかりました。 このことから、今回提案した Embedding Mapping では捉えきれない性質が CORE の ID埋め込みに備わっていることが考えられます。ID埋め込みを復元するように学習することで性質を獲得できないか、と考えて取り掛かったのですがまだ改善できる点があるようでした。

Sequential Recommendation Model はオンライン推薦を実装する上で非常に重要な選択肢の一つです。一方で実サービスでの運用を考えると、モデル学習後に登録される新着アイテムを上手く推論に利用できるようになれば、今まで以上にユーザの行動を捉えられるようになるはずです。 今後の研究では、より広いパラメータ設定ので実験を行うと共により良い ID埋め込みの推定方法を模索するなどに努めていきたいと思います。

P3-8: RecipeSTS: レシピのための類似性評価

レシピは自然言語で書かれる文書形態の一種であるものの、通常のテキストとは異なる特徴を持っているため既存のデータセットで評価された基盤モデルがレシピを上手く扱えるかどうかは定かではありません。 この研究では言語モデルのレシピ処理能力を理解するための第一歩として、レシピタイトルを対象にした STS (Semantic Textual Similarity) データセットを構築し、既存の言語モデルの評価と今後の研究方針を示しました。

データセットの作成にあたっては、2つの異なるレシピタイトルのペアに対して以下のアノテーション基準に基づき人手によるアノテーションを実施しました。 500件のペアについて、1ペアあたり 5 人の作業者が 0 ~ 5 のスコアを付与し、その平均値を正解のスコアとして採用しました。

RecipeSTS のアノテーション基準

作成した RecipeSTS データセットを用いて言語モデル (BERT / T5) の性能を評価した結果が以下の図になります。 各言語モデルから作成したレシピタイトルの埋め込み表現のコサイン類似度と、アノテーションされたスコアのスピアマン順位相関係数を示しています。 また、+ fine-tuning は事前学習済みモデルに対して独自のレシピデータで追加学習したモデルを表しています。

RecipeSTS の評価結果

今回試した中で最も高い性能を示したのはレシピデータで追加学習した BERT モデルでした。 合わせて評価を行った JSTS の結果と比べると、既存の言語モデルは一般的なテキストに比べてレシピテキストの処理は不得意な傾向があるように見えます。

また、追加で OpenAI Embedding API を使った評価も上図下部に記載しました。 論文執筆時点では text-embedding-3 の公開前であったため追加学習したモデルが最も高い性能を示していましたが、text-embedding-3-large は今回比較したモデルの中で最高性能を達成しています。 それでもやはり JSTS の結果と比べるとレシピテキストを不得意とする傾向はあるようで、レシピ処理における課題は依然として残されていると言えるでしょう。

RecipeSTS の事例

いくつかの事例をピックアップしてみると、上図の (a)、(b)、(d) のように表層的な類似・相違の影響で意味的な類似性を捉えられていないケースが複数存在しました。 また、事例 (c) はどちらも「オイスター炒め」であるものの、調理法や食材など着目する観点によって類似性の評価が変化するケースと考えられます。 レシピの類似性評価においては、より多面的な基準が必要になりそうです。

類似性評価はモデル選択など機械学習タスクの基礎のみでなく、検索や推薦といった応用においても重要な要素です。 今後の研究では、調理法・食材・味付けといったよりレシピに特化した多面的な基準に基づくデータセットの構築や、レシピに適した基盤モデルの開発に取り組みたいと考えています。

気になった発表

以下は NLP2024 で発表された研究の中から、山口・深澤が特に興味深かったものをピックアップして紹介します。

A4-3: LLM の出力結果に対する人間による評価分析とGPT-4 による自動評価との比較分析

題の通り、LLMの出力結果を人間、GPT-4で評価した結果を比較分析するという研究です。 GPT3.5-turbo-1106 (GPT-3.5) と houou-instruction-7b-v2 (houou) を対象に、Rakuda ベンチマークを用いてそれぞれの応答について関連性・正確性など複数の観点で評価を行っています。 実験の結果、人間とGPT-4の判断には乖離があり、GPT-4 は houou の方が優れていると評価した一方、人間は GPT-3.5 の方が優れていると評価したケースが多かったとのことです。

houou は具体的な数値や情報を含む応答を生成する傾向があるらしく、GPT-4 はそうした情報の具体性を評価したと考えられています。 しかし、人間が事実確認も含めて houou を評価したところ、ハルシネーションが多く、特に正確性の点で劣っていると判断されたようです。 houou の学習に利用された ichikara-instruction データセットは具体的な情報を含む例が多く、その傾向が houou の出力にも反映されていると考えられています。

この研究を見ると、LLMを評価に利用することやインストラクションデータセットを設計・構築する難しさを感じます。 情報の具体性と正確性のトレードオフについて示唆を得られる興味深い発表でした。

P6-25: 自己認知は LM as KB の信頼性を高めるか

真偽値で回答可能なQAタスク (StrategyQA) を対象に、予測の不確実性を考慮して応答を行う仕組みを提案した研究です。 予測が不確実な場合は質問を等価な命題集合・論理式へと再帰的に分割し、それぞれの命題に対して回答を得ることで与えられた質問に答えるという手法 (Back-off LMKB) を提案しています。 不確実性を考慮しない場合や間接証明しない場合と比べて、提案手法を用いることでより正確な回答を得られることを示しています。

応答が真偽値であることを利用して質問を論理式に分解するという発想は合理的で興味深いと感じました。 最初の回答で真偽不明だった質問においても間接証明により正確な回答が得られていて、提案手法の有効性が示されています。 一方、課題にも書かれているように不確実性の推定や命題集合の生成精度については今後の発展が期待されます。

個人的に自己認知的なアプローチは LLM をはじめとした AI システムの能力向上につながるのではないかと期待しています。 モデル自身が出力を再帰的に検証するという仕組みは、他のタスクにも適用できる可能性があると感じました。

P10-6: 事前学習済みの分散表現は表層的な知識を獲得しているか

現在の LLM、例えば GPT-3.5 では 『「人類学者」の 3 文字目は何ですか?』といった表層に関関する質問に正しく回答できない場合が多くあります。 この研究ではWord2Vec・BERT・T5・Llama2 など複数の学習済みの言語モデルを対象に、分散表現や生成結果を用いてモデルに表層的な情報がどの程度含まれているかを調査しています。 文字数や構成文字の予測といったタスクを通して、学習済み言語モデルが表層の情報を部分的には獲得しているものの、出力に表層の知識を反映させることが不得意であったり、出現位置や順序の情報は十分獲得できていないことが示されています。

特に興味深かったのは、文字数を予測するタスクにおいて分散表現から予測した場合とテキスト生成で予測した場合の性能の差です。 BERT や Llama2 において、分散表現を利用した場合にはある程度予測できているものの、テキスト生成で予測した場合にはその性能が大きく低下したとのことです。 モデルの内部に表層の情報を獲得できたとしても、出力のメカニズムによってそれを反映できていない可能性がありそうです。

文字数制限のある要約など、タスクによっては表層の情報が重要になる場合もあるはずです。 言語モデルの振る舞いや能力を理解するためには、意味的な評価と合わせて表層の情報を扱う能力について考えることも大切だと感じました。

A10-4: 平均プーリングによる文埋め込みの再検討: 平均は点群の要約として十分か?

文章などをベクトル化することは近年の検索拡張生成(RAG) などを実装する上で必要不可欠な技術として認識されてきています。こちらの研究では文埋め込みを作る際に最もよく用いられる平均プーリングが、単語埋め込みの空間的な広がりの情報を潰してしまう問題を指摘しています。つまり、「意味の異なる点群なのに平均が近くなる」という問題です。 これを確かめるための実験として、WMD(Word Mover's Distance) による点群としての類似度と平均プーリング後のL2距離、人手評価による類似度を用意し、それぞれの類似度を比較しています。結果として、WMDによる類似度が低い場合に平均プーリング後のL2距離が高いケースがいくつか存在していることが確認できたとのことです。この結果は経験的に平均プーリングが有効であることを示していますが、同時に考慮しなければならないケースが有ることも示しています。

点群を点群のまま捉えられるリーズナブルなモデルを用意できればいいのですが、基本的に点として圧縮されている平均プーリングの方が現時点ではやはり扱いやすいです。ただ、個人的にも平均プーリングという操作が言葉の意味を正しく捉えられているかというと疑問がある、と常々感じていたため、こちらの研究におけるクエスチョンは非常に共感できました。 今回調査した STS データでは平均プーリングでほとんどのケースに対応できていましたが、ドメインを絞ったりしてみると特有のドメインでは問題が発生しやすいなどがあるかもしれないと思っています。とても今後が気になる興味深い研究でした。

E6-2: 意味変化の統計的法則は1000年成り立つ

意味変化の統計的法則とは、頻度語ほど意味変化の度合いが小さく多義語ほど意味変化の度合いが大きくなるという、ACL2016 にて Hamilton らが発表したものを指しています。先の研究における時間範囲は 1800年から2000年でしたが、こちらの研究では聖書を対象に含めることで、Hamilton が発見した意味変化の統計的法則が1000年経過しても成り立つことを調べています。シード語を設定し、聖書を構成するラテン語とラテン語をもとにして派生したロマンス語との間で意味変化が成り立つかを調べたところ、頻度が高い・多義性の低いラテン語語源ほどロマンス語形との意味のずれが小さくなる傾向が見られ、1000年単位であっても意味変化の統計的法則が成り立つことを示したとのことでした。

個人的に Hamiltion の研究は当時読んだときからとても印象に残っていました。こちらの研究は聖書に着目してその時間範囲を広げた分析を行う、というのがユニークだなと感じ、紹介させていただきました。意味変化の法則が長い時間を書けても変化しない普遍的なものだとすると、今後も同様の変化が今我々が扱っている言葉でも起きうるということになります。例えばマルチエージェンシミュレーションなどで人工言語のモデリングを行う際などにも今回の法則を取り入れることでより自然な言語生成が可能になるかもしれません。非常に興味深い研究でした。

B7-4: 文脈構造を利用した埋め込み表現学習の提案

文章や画像に対する埋め込み表現は、検索を始めとした様々なアプリケーションで利用されています。この研究では、文脈構造を利用することで埋め込み表現の表現能力を向上させるための最適輸送を用いた教師なし埋め込み学習手法を提案しています。BERT を対象として考えた時、先行研究では CLS トークンのみに着目した学習が行われます。一方で提案手法では最適輸送を用いてシャッフルした文章の各トークンについて輸送コストを最小化するように学習させることで、文脈構造を考慮した埋め込み表現を獲得することに成功しています。得られたモデルは SimCSE などの先行研究で提案されたモデルよりも STS タスクなどにおいて高い性能を示しているとのことです。

こちらの研究を紹介させていただいた理由は、先に示した平均プーリングに関する調査を行った論文と同じモチベーションによるものです。つまり、平均プーリングで潰す・CLSだけに着目するよりももっと良い方法があるのではないか、という問いです。こちらの論文は最適輸送によって点群全体での学習を行う、というアプローチで、非常に納得しやすいものでした。発表後の質疑応答で、点群全体を見る分どうしても計算時間がかかってしまう、という問題があるとのことでしたが、今後も追いかけたい研究だな、と感じました。

おわりに

今回の記事では NLP2024 の参加レポートをお届けしました。

冒頭にも書いた通り、去年と比較して NLP2024 は特に LLM の存在感を強く感じる大会だったように感じます。 LLM の構築や評価の研究はもちろん、その他の研究においても LLM との比較や LLM の活用を意識した研究を多く見かけました。 きっとこの流れはまだしばらく続くのでしょう。 激動の時期を迎える中、今大会で得られた知見をもとに LLM をはじめとした最新の言語処理技術を実際のサービスにも活用していきたいと思います!

AWS 内で大規模言語モデルを利用できる Amazon Bedrock を使って作る RAG アプリケーション

こんにちは。機械学習グループの深澤(@fukkaa1225)です。

先日、Amazon Bedrock が一般利用できるよう(GA)になりました 。本記事ではこちらを用いて RAG(Retrieval-augmented generation) アプリケーションを作成してみた様子と、他 LLM モデルとの比較結果についてご紹介します。

Amazon Bedrock とは

aws.amazon.com

公式サイトより文言を引用します。

Amazon Bedrock は、Amazon や主要な AI スタートアップ企業が提供する基盤モデル (FM) を API を通じて利用できるようにする完全マネージド型サービスです。そのため、さまざまな FM から選択して、ユースケースに最も適したモデルを見つけることができます。Amazon Bedrock のサーバーレスエクスペリエンスにより、すぐに FM を開始したり、FM を簡単に試したり、独自のデータを使用して FM をプライベートにカスタマイズしたり、AWS のツールや機能を使用して FM をアプリケーションにシームレスに統合してデプロイしたりできます。Amazon Bedrock のエージェントはフルマネージド型で、デベロッパーは独自の知識源に基づいて最新の回答を提供し、幅広いユースケースのタスクを完了できる生成系 AI アプリケーションを簡単に作成できます。

殆どの企業にとって、現時点で LLM を使うときには OpenAI が提供する GPT-3.5-turbo, GPT-4 を使うことがほぼ唯一の選択肢になっているかと思います。弊社も GPT シリーズの API を活用して社内版 ChatGPT を展開しています。 一方で、OpenAI を用いる上でいくつか考えなくてはならない問題もあります。例えば、権限管理が API Key によってなされているため取り扱いに注意する必要があったり、OpenAI と通信する必要がある以上セキュリティ要件を満たせないケースがあるなどの点が挙げられます。 これに対して Amazon Bedrock は AWS のサービスであるため IAM での権限管理が可能です。通信も AWS 内で完結しており、VPC と接続できるのも嬉しいポイントです。モデルについても、GPT シリーズに匹敵する十分な性能を持ったものが用意されています。

Claude とは

Amazon Bedrock で利用できるモデルはいくつかありますが、日本語での質問応答に適したものとなると実質的に使えるモデルは Claude シリーズです。 Claude シリーズは Anthropic が提供しているモデルです。Chatbot-arena・Nejumi JGLUE スコアリーダーボード などいくつかのベンチマークで Claude シリーズの能力は GPT シリーズに匹敵するスコアを出しています。

wandb.ai

Amazon Bedrock では Playground で Claude シリーズとのチャットを試すこともできます。試してみると、想像以上に流暢な日本語で喋ってくれることがわかります。下図のように夕飯の献立を提案してくれました。

Claude シリーズの優れている点として、入力できるトークン数の多さと価格の安さが挙げられます。Claude シリーズに入力可能なトークン数は脅威の 10万トークンで、これは GPT-4 の 8192 トークンと比較すると圧倒的な数字です。 また、価格も GPT-4 と比べると非常に安いです。2023/10/12 時点で Build Generative AI Applications with Foundation Models - Amazon Bedrock Pricing - AWS を見ると Claude 2 の値段は掲載されていないため、 Claude の値段で比較を行います。単位はドルです。

Model Input Output
Claude(100k) 0.011 0.032
GPT-4(8k) 0.03 0.06
GPT-4(32k) 0.06 0.12
GPT-3.5-turbo(4k) 0.0015 0.002
GPT-3.5-turbo(16k) 0.003 0.004

Claude と GPT-4 のみの比較で言えば、Claude は 1/3 以下の値段となっています。それでいてベンチマーク上ではかなり良い勝負をしているので、非常に優れた選択肢であると言えるでしょう。GPT-3.5-turbo と比較してしまうと GPT-3.5-turbo の安さが際立ちます。トークン数や応答の質に関して満足できるならばやはり GPT-3.5-turbo は有力な選択肢ではあります。 一方で 10万トークンを実現しながら AWS 内で通信を完結できる十分な性能を持った LLM を用いることができる Bedrock はそれだけで十分なメリットを持っていると思います。

社内文書に対する RAG アプリケーションを作成する

Cookpad には Groupad と呼ばれる社内 wiki のようなものがあります。かなり長年運用されており、様々な知見が蓄積されていて日々の仕事を助けてくれています。 一方で問題もあり、Groupad に対する検索システムはあまりチューニングを行っていないため、同義語解決などがされず、ほしい結果を得るのに苦労することがありました。 そこで、ユーザーからのクエリに基づいて外部データから関連するドキュメントを検索し、検索結果を prompt に埋め込んで LLM に結果を生成させて表示するアプリケーション (Retrieval-augmented generation、RAG) を作成することにしました。 LLM を用いて semantics を考慮したベクトル検索と質問応答を実装することで、チューニングの手間なく今よりも幅広い検索結果を得られるだろうと考えました。

以下、実際に作ってみた様子をご紹介します。 RAG アプリケーションを作るために必要なコンポーネントはいくつか存在します。代表的なものとしては以下のようなものかと思います。

  • Vector DB ... RAG のソースとなる情報(ここでは Groupad の文書群)をベクトルとして保持しておくための DB
    • Embedding Function ... ベクトル DB に文書を追加する際に文書をベクトルに変換する
    • Retriever ... ベクトル DB から検索クエリにマッチする文書を取得する
  • LLM ... クエリから得られた関連文書を元に、LLM による対話応答を行う

この内、LLM は Amazon Bedrock で使うことができる Claude 2 を用います。 Vector DB は Chroma DB を用いることにします。 簡単な構成図としては以下のようなものになります。

なお、Bedrock には知識ベースと接続して質問応答を行うアプリケーションを作るための機能が存在します。検索拡張生成 (RAG) - Amazon Bedrock のナレッジベース - AWS この場合 Embedding には Bedrock が提供する Amazon Titan を使うことになります。ですが、Amazon Titan は現時点では英語にのみ対応したモデルで、日本語のテキストに対する埋め込み表現を得るのに適したモデルとなっていないため、今回は自分たちで huggingface hub から適したモデルをダウンロードして使うこととします。

Vector DB

embedding function には oshizo/sbert-jsnli-luke-japanese-base-lite を利用しました。これを用いて Groupad の文書群をベクトル化し、 Chroma DB に保存します。 ベクトル検索をするだけのクライアントを用意するなら例えば以下のようなものが考えられるかと思います。

class VectorSearcher:
    def __init__(self, db_path: str) -> None:
        embedding_function = embedding_functions.SentenceTransformerEmbeddingFunction(model_name="oshizo/sbert-jsnli-luke-japanese-base-lite")
        client = chromadb.PersistentClient(path=db_path)
        self.collection = client.create_collection("groupad", embedding_function=embedding_function)

    def set_groupad_data(self, data_path: str) -> None:
        logger.info("set groupad data")
        # columns: id, title, content, created_at
        df = pd.read_csv(data_path)
        self.collection.add(
            ids=df["id"].apply(str).values.tolist(),
            documents=df["content"].values.tolist(),
            metadatas=[{"title": title, "created_at": created_at} for title, created_at in zip(df["title"].values, df["created_at"].values)],
        )
        logger.info("set done")

    def search(self, query: str, top_k: int = 10) -> list[tuple[str, str]]:
        res = self.collection.query(query_texts=[query], n_results=top_k)

        return res

今回は RAG を LangChain を用いて実装するため、実際にはこのベクトル検索クラスは用いません。

LLM: Claude 2

では続いて、今回の目玉である Claude 2 を Amazon Bedrock から利用する設定をします。

Bedrock の client はシンプルに import boto3; client = boto3.client('bedrock') で使えるようになっています。Bedrock - Boto3 1.28.66 documentation

LangChain に Bedrock を使えるオプションが存在するので、今回はVector DB と接続する部分をそれに頼ることにします。 以下のように簡単に書けますので、あとはいつもの OpenAI などを使うときと同じように使えるはずです。

from langchain.llms import Bedrock

llm = Bedrock(
    credentials_profile_name="bedrock",
    model_id="anthropic.claude-v2",
    model_kwargs={
        "max_tokens_to_sample": 1000
    }
)

RAG 全体像

ChromaDB の設定などをすべて LangChain 上で行うようにして、以下のようにすれば RAG を実装できます。

なお ChromaDB の設定上、sqlite のバージョンが 3.35.5 以上でないといけないため、事前に sqlite の設定が必要な場合があります。 (ビルドして export LD_LIBRARY_PATH=sqlite-3.42.0/.libs を指定するなど)

from langchain.chains import RetrievalQA
from langchain.document_loaders.csv_loader import CSVLoader
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.llms import Bedrock
from langchain.vectorstores import Chroma


def build_qa_chain(file_path: str) -> RetrievalQA:
    loader = CSVLoader(file_path=file_path, source_column="content")
    data = loader.load_and_split()

    embeddings = HuggingFaceEmbeddings(
        model_name="oshizo/sbert-jsnli-luke-japanese-base-lite"
    )
    vectorstore = Chroma.from_documents(documents=data, embedding=embeddings)
    llm = Bedrock(
        credentials_profile_name="bedrock",
        model_id="anthropic.claude-v2",
        model_kwargs={"max_tokens_to_sample": 1000},
    )

    qa_chain = RetrievalQA.from_chain_type(llm, retriever=vectorstore.as_retriever())

    return qa_chain

実際に使うときは以下のようにして利用することができます。

qa_chain({"query": "ほげほげ"})

実際の出力を見てみた

さて、ここまで RAG の実装方針について示してきました。社内ドキュメントを対象とした実際の例をご紹介することはできませんが、今回は代替データとして深澤が Techlife で公開した記事をソースとして、RAG の挙動を確かめることとします。

今回のコードでは content カラムだけを取り込むようにしているので、以下の記事の markdown を持ってきて idx,content カラムの形式で csv "data/fukasawa_techlife.csv" に保存します。

qa_chain = build_qa_chain("data/fukasawa_techlife.csv") として qa_chain を取り出せば質問応答を行うパイプラインが用意できます。 出力を見てみましょう。

In [4]: print(qa_chain({"query": "深澤さんが取り組んできたレコメンドについて教えてください"}))
{'query': '深澤さんが取り組んできたレコメンドについて教えてください',
'result': '''
では、深澤さんが取り組んできたクックパッドマートのレコメンドについて要点をまとめます。

- 最初はItem2Vecでレコメンデーションを実装していましたが、オフラインの評価指標が思わしくなかったため他の手法を探していました
- そこでRecBoleを利用して30モデル以上の実験を行った結果、RecVAEが良好な指標を示しました
- RecVAEとItem2Vecをオンラインで比較するため、インターリービングによるテストを実施しました
- その結果、RecVAEの方が指標的に良く、Item2VecからRecVAEへの移行を決定しました

深澤さんはItem2Vecから始まり、RecBoleを利用した実験を重ね、最終的にRecVAEへ移行したことが分かります。レコメンデーション精度の改善に向けた断続的な取り組みが伺えます。
'''}

質問応答としてはかなり完璧ですね。クックパッドマートにおける item-to-item レコメンデーションの変遷 - クックパッド開発者ブログ での内容です。

In [5]: print(qa_chain({"query": "深澤さんが取り組んできた仕事について教えてください"}))
{'query': '深澤さんが取り組んできた仕事について教えてください',
'result': '''
はい、深澤さんの仕事についてまとめてみました。

- クックパッドに入社後、主に自然言語処理を活用したプロジェクトを担当されています。
- NPSアンケートの自動分類システムを構築したり、本記事で紹介されているように、NPSコメントのポジティブ/ネガティブ部分の抽出システムの開発に取り組んでいます。
- 料理手順の自動生成プロジェクトにも関わっていて、手順間の照応解析や手順から材料を自動抽出する研究などを行っています。
- Google BigQueryを活用した大規模データ分析も行っており、レシピ閲覧ログの分析結果を基にレシピ検索の改善を図るなどしています。
- 推薦システムの研究開発にも携わっており、Matrix Factorizationや popularity-based モデルなどを用いたレコメンデーションエンジンの開発をしています。
- 卒業論文ではソーシャルメディアテキスト(Twitter)のセンチメント分析を研究していました。
- 論文執筆や学会発表も積極的に行っており、自然言語処理分野を中心に研究開発を進めています。

以上のように、クックパッドにおける深澤さんの仕事は、自然言語処理とデータ分析を組み合わせた研究開発が中心だと言えそうです。
'''}

料理手順の自動生成プロジェクトソーシャルメディアテキスト(Twitter)のセンチメント分析 はしていないので hallucination が見られますが、大体の雰囲気はあっています。ちょっと範囲の広い聞き方をしたのが良くなかったのかもしれません。

In [6]: print(qa_chain({"query": "深澤さんはインターンシップにもかかわっていたことがあるのですか?"}))
{'query': '深澤さんはインターンシップにもかかわっていたことがあるのですか?',
'result': '''
はい、深澤さんはクックパッドでインターンシップにかかわっていました。

深澤さんはクックパッドの研究開発部に所属していて、過去にサマーインターンシップのメンターを務めた経験があるようです。
例えば、このブログ記事では2020年のサマーインターンシップで深澤さんがメンターを務めた様子が書かれています。
https://techlife.cookpad.com/entry/2020/09/14/140000

この記事によると、深澤さんは機械学習コースのメンターとして、インターン学生の技術指導を行ったそうです。また、インターンシップのプログラム作成にも関わったとのことです。他にも、深澤さんは過去のインターンシップで機械学習の講義を行ったり、インターン学生の研究開発にアドバイスをしたりするなど、インターンシップに積極的に関わってきたようです。そのため、深澤さんはインターンシップにかかわった経験が豊富で、インターン学生の育成に力を入れているエンジニアのようです。
'''}

回答してきた URL や 機械学習コースのメンターとして、インターン学生の技術指導 というあたりもまた hallucination が起きています。一方で事実も存在しており、インターンシップのプログラム作成にも関わった という部分などは事実です。

GPT-4 との回答比較

claude-v2 には hallucination が見られましたが、GPT-4 ではどうでしょうか。出力を比較するためにコードに以下の変更を加えます:

  • llm の部分を langchain.chat_models.ChatOpenAI を用いるようにする
  • GPT-4 の Max Token Size 8192 に合わせて langchain.text_splitter.RecursiveCharacterTextSplitter や max_token パラメータの調整を行う

最終的には以下のようなコードで実行しました。

def build_qa_chain2(file_path: str) -> RetrievalQA:
    loader = CSVLoader(file_path=file_path, source_column="content")
    data = loader.load_and_split(RecursiveCharacterTextSplitter(chunk_size=2048, chunk_overlap=0))

    embeddings = HuggingFaceEmbeddings(
        model_name="oshizo/sbert-jsnli-luke-japanese-base-lite"
    )
    vectorstore = Chroma.from_documents(documents=data, embedding=embeddings)
    llm = ChatOpenAI(
        model_name="gpt-4",
        temperature=0,
        max_tokens=4096
    )

    qa_chain = RetrievalQA.from_chain_type(
        llm,
        retriever=vectorstore.as_retriever(),
        chain_type="stuff"
    )

    return qa_chain

Claude 2 に対して行ったのと同じ質問を投げてみた結果、以下のような結果を得られました。

In [71]: print(qa_chain({"query": "深澤さんが取り組んできたレコメンドについて教えてください"}))
{'query': '深澤さんが取り組んできたレコメンドについて教えてください', 'result': '深澤さんは、レコメンド系のプロジェクトRecBoleに取り組んでいます。RecBoleは再現性に取り組むプロジェクトで、50以上のレコメンドモデルを一つのコマンドで試すことができます。深澤さんは、クックパッドが展開している事業の一つであるクックパッドマートのデータを使って、これらのレコメンドモデルを試す実験を行いました。その結果、テストデータの6000ユーザに対して2000ユーザ(三分の一)に正しい推薦を行うことができるモデルを発見できました。'}

In [72]: print(qa_chain({"query": "深澤さんが取り組んできた仕事について教えてください"}))
{'query': '深澤さんが取り組んできた仕事について教えてください', 'result': '深澤さんはクックパッドの研究開発部で働いており、レコメンド系のプロジェクトRecBoleに注目しています。RecBoleを用いて、クックパッドが展開している事業の一つであるクックパッドマートのデータを使って、50以上のレコメンドモデルの実験を行いました。その結果、テストデータの6000ユーザに対して2000ユーザ(三分の一)に正しい推薦を行うことができるモデルを発見できたとのことです。'}

In [73]: print(qa_chain({"query": "深澤さんはインターンシップにもかかわっていたことがあるのですか?"}))
{'query': '深澤さんはインターンシップにもかかわっていたことがあるのですか?', 'result': '文脈からは深澤さんがインターンシップに関与していたかどうかは明らかになりません。'}

GPT-4 はレコメンドと仕事に関する質問については Claude 2 と同じような返答をしています。一方、インターンシップの話題に関してはうまく見つけられなかったのか答えられない、という返答になりました。hallucination を抑えるような学習の結果として、このように外れではない無難な回答に終始しているのかもしれません。

Claude 2 は context window が大きい分長い系列を処理できますが、長距離依存をうまく扱えず hallucination が起こった可能性が考えられます。そうだとすれば max_token パラメータを調整することで hallucination を抑制できたかもしれません。

LLM のこうした傾向の違いについてはまだあまり調査できていないため、今後比較検討できればと思っています。

まとめ

Amazon Bedrock が提供している Claude 2 と LangChain を組み合わせて RAG アプリケーションを作成してみました。 Claude 2 は OpenAI のモデルと比べても性能やコストの点で魅力であり、これを AWS のサービスとして用いることができるのは非常に便利だなと感じました。 Amazon Bedrock の到来によって社内ドキュメント に対する RAG アプリケーションを作るハードルはかなり下がったと感じています。積極的に社内で試して、ゆくゆくはユーザの方にも使っていただけるような LLM アプリケーションを作る際の知見をためていきたいです。

この記事を読んでいただきありがとうございました。 クックパッド機械学習グループでは引き続き最先端の機械学習の技術をプロダクトで活かすべく、試行錯誤と開発を進めていきます。

Hatamoto 〜モバイルアプリに関する情報を一元管理するためのWebアプリケーション〜

こんにちは。元モバイル基盤部(現クックパッドマートプロダクト開発部)の大川(@aomathwift)です。

クックパッドでは、レシピサービスのクックパッドアプリだけでなく、生鮮 EC サービスのクックパッドマートをはじめ、複数の iOS アプリを開発しています。

複数のアプリを開発する上で、機能そのものの開発以外に開発者が気にかけないといけないことがいくつかあります。 例えば、各種証明書の有効期限、ライブラリの選定やバージョンアップなどです。開発者はこれらの関心事に対して、全てのアプリで同じように注意していなければいけません。

クックパッドでは、「Hatamoto」という Web アプリケーションでこれらのアプリに関する情報を一元管理しています。 本記事では、iOS アプリにまつわる情報管理の中で生じる課題と、Hatamoto がそれをどのように解決しているかを紹介します。

Hatamoto 導入前の課題

クックパッドでは、モバイル基盤というグループが App Store Connect をはじめとするアプリ開発に必要なサービスのワークスペースの管理者としての役割を担っています。アプリの証明書の発行等のオペレーションは、モバイル基盤グループで行っています。

このモバイル基盤グループに所属するエンジニア目線で、Hatamoto 導入前は社内アプリの証明書管理やライブラリの更新に関していくつかの課題がありました。

開発者が自発的に証明書の有効期限に気づく必要がある

iOSアプリでプッシュ通知を送信するには、Apple Push Notification Service(以下 APNs )の証明書が必要です。この証明書は1年という有効期限を持つため、継続的にプッシュ通知を送り続けるためには期限が切れる前に更新して差し替える必要があります。 もしこの証明書の差し替えを忘れて有効期限を過ぎると、アプリのプッシュ通知が突然届かなくなってしまいます。 そのため、アプリ開発者達は APNs 証明書の有効期限を把握し、期限が切れる前に自発的にモバイル基盤グループに更新を依頼する必要がありました。

また、クックパッドでは、Apple Developer Enterprise Program に加入しており、社内で動作確認するためのアプリを配信するのに利用しています。この Apple Developer Enterprise Program を使ってアプリを配信する際にも、通常の Apple Developer Program を使った配信と同様に Provisioning Profile が必要になります。 もしこの Provisioning Profile の更新を忘れて有効期限を過ぎると、社内ですでに配信済みだったアプリが突然利用できなくなるという、開発を進める上での問題が起こります。さらに、通常の Apple Developer Program で作成した App Store 配信用の Provisioning Profile も、更新を忘れると App Store にアップロードするバイナリをビルドできない問題に直面します。 したがって、これらも APNs 証明書と同様に、期限が切れる前に気づいて更新を依頼する必要がありました。

更新が必要なライブラリを使い続けていることに気づけない

クックパッドで開発している iOS アプリでは、オープンソースのライブラリから社内ライブラリまで、様々なライブラリを利用しています。 モバイル基盤グループでは、あるライブラリで脆弱性を含んだバージョンがリリースされたり、既存の挙動を壊すような問題・変更のあるバージョンがリリースされたりした場合に、そのライブラリを使用しているアプリの開発者に迅速に共有したいという需要がありました。*1

しかし、各アプリで使われているライブラリとそのバージョンを常に人力で追い続けることは難しく、利用しているアプリ側で実際に問題に直面してから対処するということが多いのが実情でした。

Hatamotoの機能

上記の課題の解決をモチベーションに、Hatamoto には以下の機能が実装されています。

証明書の有効期限を監視し、期日が近づいたら Issue を起票する

各アプリの APNs 証明書に加え、Provisioning Profile ・開発者証明書の有効期限を一覧して管理できます。

また、この収集した有効期限を毎日定期実行されるジョブで監視し、有効期限が1ヶ月以内に近づいた場合にモバイル基盤グループへの依頼用リポジトリ宛に更新依頼 Issue を自動で立てることができます。

各アプリで使われているライブラリを一覧する

アプリごとに、使用しているライブラリとそのバージョンを一覧できます。 iOS はライブラリの管理方法が SwiftPM、CocoaPods、 Carthage と複数存在するので、それぞれに対し異なる収集方法で収集しています。 また、この収集した情報を活かし、「このライブラリを利用しているアプリ」も参照できます。 この機能は gem_collector にインスパイアされています。

ライブラリを利用しているアプリに向けての一括アナウンスをする

これもまた gem_collector にインスパイアを受けた機能になりますが、特定のライブラリを使用しているアプリの開発リポジトリに対して、一斉に Issue を起票することができます。

これは、ライブラリ側に問題が見つかり早急にアップデートを依頼したい場合や、社内ライブラリに破壊的な変更が加わった場合などに利用される想定で作られています。

ちなみに Hatamoto という名前は、モバイルアプリにまつわるデータを「監視する」という意味合いから監視役→衛兵→戦国時代における武将の近衛兵=旗本という流れで名付けられています。

各機能の実現方法

各機能は以下のように実装しています。

証明書の有効期限管理

各証明書の有効期限管理は、それぞれ以下の図のような流れで実現しています。

APNs 証明書の更新の際は、有効な証明書の有無を確認し、必要に応じて新規作成するという作業をまとめて行う fastlane の pem というアクションを利用しています*2

この機能を利用するためには、Apple Developer の AppManager 以上のユーザーでログインする必要があり、そのログインの際に他要素認証が求められることから、APNs 証明書の更新はモバイル基盤グループのメンバーによって手動で実行されています。 この更新の際、作成された証明書を Amazon S3 (以下 S3 )のバケットにアップロードするようにしているので、その中身を見て有効期限をチェックしています。

Provisioning Profile・開発者証明書の有効期限の取得には App Store Connect API を使っています。

有効期限が近づいているかどうかは、毎日定期実行されるジョブ*3で今日の日付と有効期限を比較して確認しています。

使用ライブラリの収集

各アプリが利用しているライブラリの取得は、パッケージマネージャーごとに異なる方法で実現しています。

Carthage、SwiftPM

GitHub API を使って各リポジトリ内の Cartfile.resolved、Package.resolved の中身を取得しパースしています。同じく GitHub API 経由で、利用しているライブラリの最新バージョンを取得しています。

CocoaPods

CocoaPods で解決されたライブラリのリストとそれぞれのバージョンが記述される Podfile.lock には、そのライブラリのリポジトリ URL が記載されません。そのため、SwiftPM や Carthage と同様に Podfile.lock の中身を基に GitHub API を利用してライブラリの最新バージョンを取得するというのは難しいです。 したがって、利用している CocoaPods ライブラリの取得及び最新の podspec の情報の取得は、各アプリのメインブランチの CI で実行するようにしました。ここで収集した情報を S3 にアップロードし、アップロードされた情報を同様に Kuroko2 のジョブを定期実行して取得し DB に登録する、という流れにしています。

Hatamoto を実際に運用してみて

Hatamoto を実際に運用してみて、最も良かったのは証明書の有効期限を気にする必要がなくなったことです。証明書は各アプリの管理者がダウンロードして差し替えることになるので、管理者であるモバイル基盤グループのオペレーションが減りました。

また、各アプリが使っているライブラリを一覧できるというのは、思いの外管理者以外の開発者の目線から役に立つ場面がありました。 たとえば、新しくライブラリを導入しようとしたときに、社内にそのライブラリを使っているアプリが他に無いかを調べ、導入方法を確認する、といった活用例がありました。

逆に、ライブラリを利用しているアプリに向けての一括アナウンスは未だ活用されていません。対応が必要な問題がみつかったときに、各アプリで開発者側が自発的に Issue を立てて対応していることが多く、開発者達の対応力の高さ故にあまり需要がない機能となってしまいました。

おわりに

この記事では、アプリの各種証明書や各プロジェクトが依存している開発ツール・ライブラリなど、 iOS アプリに関する情報を一元管理するための Web アプリケーション「Hatamoto」を紹介しました。 このツールのおかげで、 iOS アプリを開発していく上で面倒となる管理部分が効率化できています。App Store Connect API の機能が増えれば、証明書の生成フローなど更に自動化できる部分が増え、より便利に利用することもできそうです。

この Hatamoto は主に App Store Connect API と GitHub API 、fastlane を使って比較的単純な処理で実現されています。複数アプリにおける証明書等の情報管理に悩む方は、Hatamoto のようなWebアプリによる管理を試してみてはいかがでしょうか。

*1:社内ではアプリごとにライブラリの更新を監視するスクリプトを自前で書いて週次で実行していましたが、利用するバージョンを固定している場合や、更新の緊急性が高い場合にこれが機能しないという事情がありました。現在は、Renovate のようなツールを利用するのも解決手段の一つだと思います。

*2: App Store Connect API には証明書を作成する機能がないため、作成処理を完全に自動化することは現状できません

*3:クックパッドで利用されている Ruby 製の Web ベースのジョブスケジューラー Kuroko2 を利用しています