クックパッドは RubyKaigi 2024 に参加していました!イベントレポート

はじめに

こんにちは。レシピ事業部プロダクト開発グループの堀内 (@Sota_Horiuchi)です。普段はバックエンドの開発を行っている新卒2年目のエンジニアです。

RubyKaigi 2024が 2024 年 5 月 15 日から 17 日に沖縄県那覇市で開催され、クックパッドからは総勢 24 名が参加しました。参加したメンバーのうち 13 名が新卒 3 年目までのエンジニアであり、社内の若手バックエンドエンジニアがほぼ参加していました。 また、クックパッドは Wi-Fi スポンサーとして協賛しており、更に 16 日の夜には Cookpad Drinkup at RubyKaigi 2024 と称して懇親会を開きました。 ドリンクアップの話はまた後日記事として公開されると思います。

RubyKaigi 2024 について

セッションは全部で 53(去年 51 )で、スポンサー数は 97(去年 89 )でした。スポンサー数は個人スポンサーが存在していた 2011 年を抜けば過去最大の値でした。 今回最も大きく変わっていたのはイベントの数であり、今年は 23(去年 9 )のイベント(パーティ)が設定されていました。夜な夜な那覇市のどこかで Rubyist の集まりが開かれており、自分も連日さまざまなところで飲みいろいろな方と交流ができました。 また、夜だけでなく昼には他社のエンジニア同士で集まりランチに向かうという取り組みがちらほら起こっており、他社の Rubyist の方と交流することができました。

RubyKaigi 2024の全体感

今回の RubyKaigi のセッションをテーマ別にざっくり分類してみると、パフォーマンスに関するセッションが多く見られました。

パフォーマンスについては Matz さんの Keynote でも第1にパフォーマンス、第2にパフォーマンス...とにかくこれからは(も)パフォーマンス改善に注力していきたいと述べられていました。具体的には VM の高速化、メモリ効率の改善、スレッド=パフォーマンス改善という需要への対応、ソフトウェア面での高速化等で、今後どれだけ改善していくか楽しみです。

また、パフォーマンス以外では、型やパーサー、 wasm の話も多くありました。型については day 3 の Ruby Committers and the World でも白熱した議論が起こり面白かったです。Ruby の型についてはまだまだ方が付きそうにありませんね。 パーサーの話で言えば、Matz さんからシンタックス・モラトリアムという、少なくとも今年いっぱいは文法に手を加えないようにし、パーサ周りの改善に注力していこうという提案がなされました。手書きパーサの Prism と文法定義から生成する Lrama が今後それぞれどのように発展し、互いに影響を与えていくのか注目です。世はまさに大パーサ時代。

パフォーマンス

先ほどRubyKaigi 2024ではパフォーマンスの話が多かったと述べましたが、ここでは特に印象に残ったパフォーマンスのトークについて紹介します。

取り上げるのはRubyそのものの実行速度の向上を目的とした、Speeding up Instance Variables with Red-Black Trees です。

本トークは Ruby のインスタンス変数へのアクセスを平衡二分探索木である赤黒木で高速化したという内容です。既にこの内容は Ruby 3.4にマージされており、Pull Request は右です (https://github.com/ruby/ruby/pull/8744 )。 この高速化においては、Ruby 3.2 で導入された、インスタンス変数の管理方法である Object Shape が前提となっています。( RubyKaigi 2022でCRubyに実装された時の発表 https://rubykaigi.org/2022/presentations/jemmaissroff.html

このトークを取り上げた理由としては、私自身がデータ構造やアルゴリズムを使った高速化について興味があったのと、思ったより前提となるこの Object Shape の日本語解説が少なく、Object Shape を理解する上で誰かの参考になればと思い取り上げました。

そもそも Ruby (CRuby) ではインスタンス変数を配列で保存しているため1、インスタンス変数にアクセスするには、当該配列においてインスタンス変数と対応している要素のindexを知る必要があります。Ruby VM ではインラインキャッシュと呼ばれるしくみにより、index をキャッシュすることで高速化を行なっています。しかし、同じインスタンス変数名があるクラスが複数登場するとキャッシュがうまく機能しないという問題がありました。そんな時、Ruby 3.2で導入された Object Shape がこれを解決しました。 Object Shape により「別のインスタンスだが、同名インスタンス変数のindexが同じである」ということを認識することができるようになり、index が同じなので使いまわせるとして、キャッシュヒット率を向上させることができるようになりました。

それでもキャッシュヒットしない(キャッシュミス)が起こる事例はかなりあるようで、例えば下のようなメモ化は Object Shape と相性が悪いようです。

class Sample
  def initialize(x)
    @hoge = x
  end

  def foo(x)
    @_foo ||= x
  end

  def bar(x)
    @_bar ||= x
  end
end

Object Shape ではインスタンス変数の初呼び出しにより Shape が決定されます。そのため、インスタンスにおける foo と bar の最初の呼び出しの順番が異なるだけで違う Shape になってしまい、キャッシュを使うことができません。結果としてキャッシュミスが起こります。キャッシュミスが起こるとインスタンス変数の index を獲得する必要があります。Object Shape を使った場合、そのクラスが複雑な Shape でない場合はインスタンス変数の index を獲得するために O(|インスタンス変数|) の計算処理が必要でした。 そのため、キャッシュミス時の index の高速取得が求められていました。

Object Shape 自体はそもそも木構造でそれぞれの Shape を管理していて且つそれぞれのインスタンス変数の index が各ノードに格納されています。本トークのアイディアはこの木構造をできるだけ均一の高さにすることで index の探索を早くするというものです。

個人的な感想として、赤黒木は同じ平衡二分探索木であるAVL木に比べて木の形が歪になりやすいことが知られているので、ノードの検索は AVL 木の方が速いはずです。そのため、AVL 木で実装した場合とどちらが速いのか気になりました。 また、本トークを聴くまでは Object Shape についてあまり詳しく知らなかったので、本セッションにより Object Shape の気持ちも多少感じることができました。 加えて、Ruby の高速化のため Object Shape に優しいコード(インスタンス変数の代入は同じ順序で行う方が良い)を書こうという気になれました。 ただ一方で社内の人と議論をした際、「Ruby のユーザーとしてはどう書いてもある程度速くなってほしい」という話題になり、普段から Object Shape を考えながらコードを書くことは難しいよねという話になりました。 もちろん普段から常に意識することは難しいですが、例えばメモ化をできるだけ控えるとか、コンストラクタにおける変数初期化の際に変数の順番を一定にするツールを使う(あるのか?)とかである程度 Object Shape フレンドリーなコードを書くことができるのかなとも思いました。参考2

今後もどのような高速化手法が出てくるか楽しみです。

終わりに

次回の RubyKaigi 2025 は愛媛県松山市にて開催予定です。 愛媛県には千と千尋の神隠しの舞台になった道後温泉やゆるキャラのバリィさん3などがいるのでとても楽しみです。

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 アプリケーションを作る際の知見をためていきたいです。

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