TECH PLAY

株式会社AI Shift(株式会社サイバーエージェントグループ)

株式会社AI Shift(株式会社サイバーエージェントグループ) の技術ブログ

79

はじめまして!東北大学修士1年の 林崎 由 ( @u_hyszk )と申します!大学では音声信号処理や自然言語処理に関する研究を行っています。 9/6〜9/30の15日間、株式会社AI Shiftで内定者アルバイトとして就業しました。テーマは「 モノラル音声での話者ダイアライゼーション 」であり、技術選定から実際のプロダクトへのリリースまで一貫してやり切ることができました。 本記事では、内定者アルバイトとして取り組んだ内容や検証内容の一部をご紹介いたします。 タスクの背景 AI Shiftが扱うコールセンターの音声には、ステレオ音声とモノラル音声のものがあります。 ステレオ音声 では、オペレーターとカスタマーの音声がそれぞれ異なるオーディオチャンネルに格納されています。各チャンネルの音声に対して音声認識を実行することで、オペレーターとカスタマーのどちらが何を話したのか容易に特定することができます。 一方、 モノラル音声 は、2人の音声が単一のオーディオチャンネルに格納されているため、このままの形式では発話者と発言内容を紐づけることができません。 「誰が、何を話したのか」を考慮した高度な分析を提供する には、 モノラル音声中の2人のの発言を識別し、区別する必要があります。 今回のタスクではこの課題を解決するために、話者ダイアライゼーション機能の導入を行いました。 話者ダイアライゼーションとは 話者ダイアライゼーション (Speaker Diarization)とは、複数話者の音声から「だれが、いつ話したか」を推定するタスクのことを指します。大きく分けて、(1)段階的に処理を行うクラスタリングベースの手法と、(2)単一のEnd-to-Endモデルを使用して一気に処理を行うアプローチ(3)音源分離を用いる手法があります。今回はクラスタリングベースの手法についてご紹介します。 クラスタリングベースの手法では音声区間検出(VAD)・話者特徴量抽出・クラスタリングを段階的に行います。 音声区間検出(VAD) : 音声から発話区間(セグメント)を抽出する処理です。この時点では発話した時間のみを推定し、どちらの話者がどの区間で発話したのかは特定できません。 話者特徴量抽出 : 各セグメントから、声の高さやイントネーションなどの「音声に含まれる話者の特徴」を定量化した話者特徴量を抽出します。x-vectorと呼ばれる事前学習済みのニューラルモデルから得られる特徴量を使用することが一般的です。 クラスタリング : 抽出された話者特徴量を使用して、教師なし学習のクラスタリングを行います。この処理により、どちらの話者がどのセグメントを発話したのか特定できます。   クラスタリングベースの手法は、End-to-Endモデルを使用するアプローチと比較して、処理が軽量・高速であることが多いというメリットがある一方で、複数話者の発話が重複するデータの処理が難しいというデメリットがあります。しかし、コールセンターの音声は 発話区間の重複が少ない 話者が2人(オペレーター・カスタマー)のみであることが多い という特徴があり、発話区間の重複を考慮する・話者がより多い一般的な話者ダイアライゼーションと比較して単純なため、クラスタリングベースの手法でも十分に精度を出すことができる可能性があると考えられます。 クラスタリングベースの手法において ボトルネックとなる処理は発話区間検出(VAD)と話者特徴量抽出 です。今回はさらなる高速化のために、この2つの処理に対して工夫を加えます。 MFCC(メル周波数ケプストラム係数)による高速化 MFCC(メル周波数ケプストラム係数) とは、人の聴覚特性を考慮した音響特徴量の一つで、音声認識や話者識別などのタスクでよく使用されます。 ケプストラムは人の声道特性を捉える音響特徴量ですが、メル周波数ケプストラム係数(MFCC)はここにメル尺度という人の聴覚感度に基づく尺度を取り入れて変換した特徴量です。MFCCの計算はx-vectorなどのように複雑なモデルを使用したものと比べて単純になっているため、高速化が見込まれます。 今回は話者特徴量として、x-vectorの代わりに使用することで処理の高速化を図ります。 ONNXによる高速化 ONNX(Open Neural Network eXchange) とは、機械学習モデルを表現するためのオープンソースのフォーマットです。ONNXモデルで推論を行う際には、同じくオープンソースである ONNX Runtime を使用することで、様々な最適化が施されるため、推論が高速化される可能性があります。 今回は、VADモデルをONNXに変換することで推論の高速化を狙います。 性能・速度検証 先ほど紹介したクラスタリングベースの手法について、話者ダイアライゼーションの性能と速度を検証します。また、(1)話者特徴量をMFCCに変えた場合(2)VADモデルをONNXに変換した場合についても同様に検証を行います。なお、今回の検証はVertex AIのCPU環境(n1-standard-2)で行いいます。 使用技術 話者ダイアライゼーションを行うことができる代表的なライブラリ・API・ツールなどは以下の通りです。 名称 実行環境 料金 Github Star(執筆時) ライセンス pyannote-audio CPU / GPU 無料 6k MIT licence Cloud Speech-to-Text API Google Cloud - - Amazon Transcribe API Amazon Web Services - - Azure Conversation Transcription API API Microsoft Azure - - SpeechBrain CPU / GPU 無料 8.6k Apache-2.0 pyAudioAnalysis CPU 無料 5.8k Apache-2.0 今回はこれらのうち、既存システムとの相性が良い pyannote-audio を使用して検証を行います。使用するモデルや手法は以下の通りです。 VAD: pyannote/segmentation@Interspeech2021 話者特徴: pyannote/wespeaker-voxceleb-resnet34-LM またはMFCC クラスタリング: 凝集的クラスタリング( AgglomerativeClustering ) 評価データ 評価データとして CALLHOME日本語データセット のテストデータを使用します。CALLHOME日本語データセットには、合計120件の電話での雑談対話音声とその書き起こしが含まれており、そのうち20件がテストデータセットとして指定されています。今回はこのテストデータを使用して話者ダイアライゼーションの性能を評価します。 評価指標 評価指標には Diarization Error Rate(DER) を使用します。DERは以下の式で計算することができ、低ければ低いほど性能が高いと評価されます。 False Alarm: モデルが発話区間であると推定したが、正解ラベルは非発話区間である時間の長さ Missed Detection: モデルが非発話区間であると推定したが、正解ラベルは発話区間である時間の長さ Confusion: モデルが推定した話者と正解ラベルの話者が異なる時間の長さ Total: 音声の長さ 実行速度の評価には Real Time Factor(RTF) を使用します。RTFは実行時間÷音声の長さで計算することができます。 結果 結果は以下の表の通りです。VADモデルをONNXに変換すると、 性能を低下させずにVAD単体で1.33〜1.43倍の速度向上が見られます 。また、話者特徴量の抽出をMFCCに変更すると、 DERが0.079低下するものの、従来モデルから約120倍速くなっています 。全体ではRTF 0.058まで実行速度を向上させることができました。 実行環境上では数値が変動しますが、検証環境上ではVADモデルのONNX化・話者特徴量の変更の両者の有効性が確認できる結果となりました。 VADモデルのONNX 話者特徴量 DER RTF - VAD RTF - 話者特徴量抽出 RTF - クラスタリング RTF - 合計 × MFCC 0.453 0.0773 0.0006 0.0000 0.077 × wespeaker-voxceleb-resnet34-LM 0.374 0.0849 0.0732 0.0000 0.158 ⚪︎ MFCC 0.453 0.0581 0.0006 0.0000 0.058 ⚪︎ wespeaker-voxceleb-resnet34-LM 0.374 0.0592 0.0738 0.0000 0.133 プロダクトへの組み込み 本記事では検証内容の一部を紹介しましたが、内定者アルバイトでは追加の検証でノイズへの頑健性や実データでの性能を確認した上で、技術選定を行っています。また、プロダクトへの組み込み・リリースまでやり切ることができました。 学んだこと 今回のタスクでは、計15日間という短期間で技術選定からリリースまでを一貫して行いました。タスクを取り巻く背景情報は無数にあるのですが、 判断材料を集め、集めた情報を元に考える時間すらも「コスト」になること を強く意識させられたところが、普段の研究とのギャップを大きく感じた点でした。限られた情報の中から判断するスピードと質を両方追求していきたいと思いました。 また、今回の内定者アルバイトを通して最も苦戦したのが 既存ツール・ライブラリへの理解 でした。自分の研究領域から近い領域にもベストプラクティスと呼ばれる手法を素早く展開できるよう、自分が技術選定する立場になって考える、実際に手を動かしてみるなど、キャッチアップの質を向上させることが重要であると改めて感じました。 最後に 本記事では、自分が内定者アルバイトで取り組んだ内容や検証内容の一部をご紹介しました。AI Shiftや親会社のサイバーエージェントでのインターンシップ・内定者アルバイトを検討している方の参考になれば幸いです。 AI Shiftはとても活力がある会社です。社員さんも優しくて頼もしい方ばかりで、ソフトスキル・ハードスキルともに学ぶことが非常に多かったと感じています。 最後にトレーナーの大竹さん、メンターの干飯さん、期間中に関わっていただいたすべての皆様、本当にありがとうございました! 投稿 内定者が15日で話者ダイアライゼーション機能をリリースした話 は 株式会社AI Shift に最初に表示されました。
アバター
はじめに こんにちは、AIチームの大竹です。 今回は弊社AIチームが Nishikaにて開催された音声認識コンペ に参加したので、その参加報告をさせていただければと思います。 弊社は AI Messenger Voicebot や AI Messenger Summary など、音声処理技術を活用したプロダクトを提供しています。しかし、弊社が持っている音声処理に関する技術力をアピールできる機会は少なく、現在のAIチームの技術力のアピールとさらなる技術力向上のために非常に良い機会でしたのでコンペに参加しました。 コンペ概要 主催者について 今回参加したコンペは Nishika株式会社 が主催したものになります。NishikaはAIモデルの開発、AI人材マッチング、AI研修サービスを提供するプラットフォームです。今回のようなコンペティションを通して高精度なAIモデルの開発、提供を可能にしています。 コンペの内容について 本コンペティションの内容は文学作品の文字起こし(音声認識)です。 学習・評価に利用されたデータセットは青空文庫の文章とそれを朗読した音声から構成されています。具体的には、「芥川龍之介の蜘蛛の糸」、「夏目漱石の吾輩は猫である」、「太宰治の走れメロス」などの著名な文学作品を朗読した音声と作品の文章が含まれています。 評価指標は CER(Character Error Rate) となっており、どれだけ正確に音声認識ができているかを文字単位で評価します。この値が 低いほど スコア的には良いものになります。 本コンペは7月から8月末までおよそ2ヶ月間開催され、総勢252人が参加し、合計で427件の投稿がありました。 コンペの進行 本コンペには弊社AIチームのうち5人が参加し、協力して一つのアウトプットを出すというよりはそれぞれ違う解法を各自で試すという方針で臨みました。普段の業務と並行しながらの参加ということもあり、チュートリアルまで取り組んで終わってしまったというメンバーもいたため、結局、コンペにアウトプットを提出できたのは5人のうち私を含めて2人でした。 1人はWhisperを用いた音声認識に取り組み、コンペ期間の前半(7月末まで)でベスト3の成績を残していました。しかし、その方が都合により継続できなくなったため、コンペ期間後半からは私がメインで取り組むことになりました。 以下には、私が取り組んだ解法のみを記載したいと思います。 解法 モデル 本コンペで用いた音声認識システムのパイプラインは、まず、VAD (Voice Activity Detection)モデルで有声区間を検出し、抽出した有声区間に対してASR (Automatic Speech Recognition)モデルを用いて文字起こしを行うというものです。 それぞれ使用したモデルは以下のとおりです。 VADモデル: pyannote segmentation-3.0 ASRモデル: reazonspeech-espnet-v2 実行環境 下記の環境でモデルの学習および評価を行いました。 CPU: Google Compute Engineのn1-standard-8インスタンス (8 個の vCPU、30 GB RAM) GPU: NVIDIA T4(メモリ16GB) 4枚 (推論はGPU1枚で十分です) OS: Debian GNU/Linux 11 (bullseye) ESPNetのレシピの実行 今回、ASRモデルの学習は espnet を用いて行いました。学習は基本的には espnetの学習レシピ に従い、以下のような流れで行いました。 コンペデータ のダウンロード データの前処理 データの分割:訓練セット、検証セット、テストセットに分割 話速変化に基づくデータ拡張: 0.9, 1.0, 1.1倍の話速の音声データを生成し、データ量を3倍にする トークンリストの作成:トランスクリプトに含まれる文字集合でトークンリストを作成する 言語モデルの訓練 音声認識モデルのデコーディング時に用いるLSTMベースの言語モデルをスクラッチから訓練します。 バッチサイズ384, エポック数40で訓練し、検証データの損失が最小のモデルを保存するようにしました。 訓練に用いたハイパーパラメータの完全版は こちら に記載しています。 ASRモデルの訓練 reazonspeech-espnet-v2をファインチューニングします。 エポック数15で訓練し、検証データに対する正解率ベスト3のモデルを保存し、それらの移動平均を取るようにしました。バッチサイズは音声長によって動的になるように設定しました。 訓練に用いたハイパーパラメータ完全版は こちら を参照してください。 本来なら、この後に訓練したモデルを分割したテストセットを用いて評価するべきですが、追加の実装が必要になるため、今回はテストセットによる評価を省略しました。 追加データを用いた学習 今回のコンペでは機械学習モデルを学習する用途で外部データの使用が許可されていました。コンペ内でのトピックにおける議論によると 青空文庫のデータ を用いた言語モデルの学習も許可されるとのことでした。テストデータのリークの可能性に関する疑問を持ちながらも、ランキング上位の方々が使用する方針とのことだったので私もこちらのデータの活用を試みました。 追加データを用いることでオープンリーダーボードのスコアが 1%程度改善 しました。 推論時のハイパーパラメータのチューニング ハイパーパラメータ一覧 今回のコンペでチューニングしたハイパーパラメータには以下のようなものがあります。 asr_beam_size: 音声認識のビームサイズ asr_ctc_weight: 音声認識のデコーディングにおけるCTCモジュールの予測の重み asr_lm_weight: 音声認識のデコーディングにおける言語モデルの予測の重み vad_min_duration_off: この値未満の音声区間は無視する vad_min_duration_on: この値未満の無音区間は音声区間として扱う merge_segments: 短い音声セグメントを結合するかどうか 書き起こし結果に対する定性的な分析やオープンリーダーボード上のスコアを手がかりにこれらのハイパーパラメータを調整しました。 具体的には以下のような流れでハイパーパラメータを決めていきました。 リーダーボードのスコアに関係なく決定したハイパラ merge_segments , vad_min_duration_on , vad_min_duration_off merge_segments は短いセグメントを指定したセグメント長になるまで結合するのでASRモデルに与える音声セグメントが長くなります。その結果OOM(GPUメモリが溢れて推論できなくなってしまうこと)になったり、「ハルシネーション」や「繰り返し」が起きてしまったりする問題が発生しました。したがって、スコアに関係なく今回は merge_sgements=False で固定しました。 vad_min_duration_on は短いセグメントを削除するためのもので、この値よりも小さい音声セグメントは無視されます。フィラーなどを無視するためには有効だと思いますが、今回扱うデータは朗読音声なので基本的にフィラーなどは存在しないだろうと考えて vad_min_duration_on=0.0 に設定しました。 一方で vad_min_duration_off は有声セグメント長が短く区切れすぎないようにするパラメータです。この値より小さいものは1つのセグメントとして区切れないようになります。セグメントが短くなりすぎると文脈を考慮した音声認識性能が低下してしまうのではないかと考えて書き起こし結果を観察しつつ、 vad_min_duration_off=0.3 としました。 asr_beam_size asr_beam_size に関してespnetのライブラリのデフォルト値は20でしたが、値を少し減少させても定性的に音声認識結果が変わらない、かつ、値を大きくしすぎるとOOMになってしまうためスコアに関係なく asr_beam_size=5 で固定しました。 asr_ctc_weight ハイブリッドCTC/Attention のASRモデルのアーキテクチャにおいて、どれだけCTCモジュールの予測を考慮するかどうかというパラメータになっています。CTCモジュールの予測確率を用いたビームサーチにより、音声系列とテキスト系列の単調なアライメントを導入でき、繰り返しや早期にend of sequence(系列の終了記号)を予測してしまう問題を防ぐことができます。 今回は直感ですが asr_ctc_weight=0.1 に決め打ちしました。 ちなみに、コンペ終了後に asr_ctc_weight=0 として提出したところスコアが顕著に悪化しました。やはり、Attentionを用いた音声認識において単調なアライメント制約を導入することは重要のようです。 asr_ctc_weight オープンリーダーボードスコア(↓) 0.1 0.105855 0.0 0.184982 リーダーボードのスコアを参照して決めたハイパラ asr_lm_weight デコーディングの際のビームサーチにおいて、言語モデルの予測をどれだけ考慮するかを決めます。これが大きいほど言語モデルの影響が大きくなります。 asr_lm_weight を0.1から増加させていった場合の提出結果を以下に示します。 asr_lm_weight オープンリーダーボードスコア(↓) 0.6 0.106495 0.5 0.105855 0.4 0.106425 0.3 0.110242 0.2 0.113428 0.1 0.118170 やはり、言語モデルの予測がスコアに与える影響は大きく、 asr_lm_weight を大きくしていくとスコアが改善します。しかし、大きければ大きいほど良いわけではなく、ベストな値があり、ASRモデルと言語モデルの予測のバランスを取ることが重要だとわかりました。 最終結果 最終リーダーボードのスコアが 0.107036 で 2位 の結果となりました!🎉 その時のハイパーパラメータの値は以下の通りです。 asr_beam_size : 5 asr_ctc_weight : 0.1 asr_lm_weight : 0.5 merge_segments : false vad_min_duration_off : 0.3 vad_min_duration_on : 0.0 最終結果が出るまで色々ありましたが、最終的に2位になることができてよかったです。 最後に今回のコンペで工夫した点や難しかったと感じた点を私の所感とともに記述します。 工夫した点 ASRモデルに与える音声セグメントの音声に対して無音区間を付与した点 ReazonSpeechのissue で議論されている通り、Transformerベースの音声認識モデルは突然音声が始まったり終わったりするセグメントに対して、ハルシネーションや繰り返しが発生する可能性が高まります。こうした音声セグメントに対して、 ReazonSpeechの実装 に従って、冒頭の1秒間と末尾の0.5秒間に無音区間を挿入しました。このようにすることでVADモデルを用いて切り出したセグメントに対する音声認識のロバスト性が向上したと考えています。 ASRモデルをファインチューニングする際にトークン集合とトークン埋め込み層を初期化した点 文学作品へのドメイン適応性能を高めるため、ReazonSpeechのデコーダーのトークン埋め込み層のみ初期化してファインチューニングすることにしました。このようにすることで、汎用的な音声処理認識能力は維持しつつデコーダーのトークン出力のみを文学作品向けにカスタマイズできたと思います。また、言語モデルでも新たなトークン集合を扱うことができ、より文学作品に特化した音声認識が可能になったと考えられます。 難しかった点 文学作品という特殊な文字の扱い方がされている中での音声認識 普段ならひらがなで表現する文字が漢字になっていたり、見慣れない表現が頻発したり、送り仮名の使い方が普段のものと異なったりしていて、音声的には正解と言えるものでも、文学作品の文字起こしとしては正解とはならないものが多かった点が難しかったです。一方で、このような問題に対して、文学作品特化の言語モデルの予測を音声認識時に組み込むことで、ある程度カバーできることが確認できたのは良い学びでした。 音声認識モデルに対してベストな音声区間検出 音声区間が長すぎるとセグメントのデコーディング時にハルシネーションや繰り返しが発生する可能性が高まり、一方で音声区間が短すぎると文脈を考慮した音声認識による精度向上の可能性が低くなってしまうので、適切な長さで音声区間を検出できると良いのですが、その調整が難しかったです。 テストデータがリークしていると思われる中でどの手法を選択したらよいか 今回のコンペでは、テストデータが明示的に示されていないものの、 青空文庫のデータ に含まれていて、こちらのデータを言語モデルの学習資源として扱っていいという運営の方針がありました。さらに、このデータにはwhisperによる音声認識結果も付与されているため、その結果を入力とし正解テキストを予測するような言語モデルを学習すれば音声認識を工夫することなく、高スコアがとれるのではないかということも考えられました。個人的にそのような方法ではそのような方法では音声認識の新しい知見が得られないと思ったので、解法に示した方法で対策しました。しかし、私が選択した方法でも結局正解がリークしている可能性があるので、本来は避けたほうが良いのですが、言語モデルの予測を音声認識のデコーディング時に考慮することで、ある程度有用なドメイン適応が可能だという知見が得られたので良かったです。専門用語の多い音声認識などに対しては有効な手段だと思います。 最後に 今回は、弊社AIチームとして初めて国内の音声認識コンペに参加した報告をさせていただきました。コンペ終了までにいろいろなことがありましたが、チームの音声認識に関する知識レベルの底上げや今後参考になるような知見、また2位という名誉な結果となり、非常に良い機会だったなと思います!今後もこのような機会があれば積極的に参加し、音声認識技術の向上および対外的なアピールができたらと考えています。 なお、今回のコンペで使用したコードは こちらのリポジトリ に保管してあるのでご参考までにご確認ください。 最後に、本コンペティションを主催していただいたNishika株式会社様には、心より感謝申し上げます。 投稿 Nishika音声認識コンペに参加して2位になりました!🎉 は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは。AIチームの干飯( @hosimesi11_ )です。今回はAI Shiftの新規サービスであるAI Messanger SummaryのMLパイプラインの監視についてまとめたいと思います。 具体的にはExitHandlerで外部ツールとの連携をしつつ、詳細なメトリクスの監視は Google Cloud Managed Service for Prometheus + Cloud Monitoring で構成しました。 事前情報 AI Messenger Summaryとは AI Messenger Summaryはコールセンターにおける、オペレーターと顧客の会話内容の要約サービスになります( プレスリリース )。コールセンターでは、会話内容をまとめるといったアフターコールワークに多くの時間が割かれるという課題があります。この状況を解決するため、AI Messenger Summaryはオペレーターと顧客の会話データや録音データを音声認識し、大規模言語モデル(LLM)で処理して要約し、導入企業に合わせたフォーマットで出力するようなサービスとなっています。 MLパイプラインの構成 MLを扱う音声認識やLLMを使った要約処理をStepとみなし、全ての実行をワークフローツールで制御しています。Argo Workflowsをワークフローツールとして使用し、並列化やリトライを制御しています。1つのワークフローに対する処理のステップ数はワークフローによりますが、10~20ステップほどあります。 Argo Workflowsの構成は以下を参考にしてください。 https://www.ai-shift.co.jp/techblog/4380 インフラ基盤 AI Messenger Summaryのインフラ基盤には Google Cloud を採用しています。アプリケーションはGKE上に構築しています。 AI Messenger Summaryにおける監視 元々の監視体制 立ち上げ期のプロダクトのため、開発を行うエンジニアは数人程度で、監視に関してはソフトウェアエンジニア 1人、MLエンジニア 1人で監視していました。 そのため監視項目はあまりリッチにはせず、最低限のJobの成否とサーバーエラーのみをモニタリングしていました。ここでは通常のCloud LoggingのアラートとArgo WorkflowsのExitHandlerというものを使い、ステータスの連携とSlackへの通知を実行していました。ExitHandlerは成功または失敗に関係なく、ワークフローの最後に常に実行されるテンプレートです。 ExitHandlerの使用例としては以下のようなものがあります。 ワークフロー実行後のリソースの削除 Slackへのワークフローの成否の通知 retry制御や他ワークフローとの連携 具体的には以下のように使用し、Slack通知とPubSubへのステータス通知をしていました。 Slack通知用のWorkflowTemplateです。 apiVersion: argoproj.io/v1alpha1 kind: WorkflowTemplate metadata: name: slack-notification-step namespace: sample-namespace spec: serviceAccountName: sample-sa templates: - name: slack-notification nodeSelector: cloud.google.com/gke-nodepool: sumple-node-pool inputs: parameters: - name: status - name: title - name: env - name: url - name: workflow-id - name: duration - name: config - name: message script: command: - sh image: sample-image source: | FAILURE_MESSAGES=$(echo {{inputs.parameters.message}} | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/'message'\042/){print $(i+1); exit}}}' | tr -d '"') PAYLOAD="{\"title\":\"{{inputs.parameters.title}}\",\"config\":\"{{inputs.parameters.config}}\",\"duration\":\"{{inputs.parameters.duration}}s\",\"id\":\"{{inputs.parameters.workflow-id}}\",\"status\":\"{{inputs.parameters.status}}\",\"env\":\"{{inputs.parameters.env}}\", \"url\":\"{{inputs.parameters.url}}\", \"message\":\"$FAILURE_MESSAGES\"}" curl -X POST -H 'Content-type: application/json' --data "$PAYLOAD" $SLACK_WEBHOOK_URL Slack通知の実際の画面です。 slack_notification 監視に関する課題 しかしSlack通知のステップで落ちた場合、通知が上手くいかないといった課題がありました。ログの場所やアラートの探し方も属人化していたため、チームで運用できる状況になっていませんでした。 ワークフロー自体が動いていない時やArgo Serverの負荷が上がってエラーになった場合にも気付けないため、これらも監視できるようなシステムを作ることに決めました。またエラーではありませんが、特定のステップで時間がかかりすぎてユーザーの操作に影響をきたしている場合なども気付ける必要があります。 監視に必要な項目の洗い出し システム運用についてのゴールを以下と定めました。 明確な指標を持ち、その指標の変動に迅速に対応できる監視システムを構築する。 個々の運用者に依存せず、複数人でシステムを効率的に運用できるようにする。 監視システムを設計するために、まず必要な監視項目を洗い出しました。全てのメトリクスを監視すると、重要なデータが見落とされる可能性があるため、必要なメトリクスを厳選することが求められました。また、少人数での運用を実現するためにも、この選別作業は重要でした。 そこで、ビジネスにおける重要度が高い項目を中心に、必須とオプションの2つに分けてメトリクスをリスト化しました。これにより、監視すべき重要な項目を明確にし、効率的なシステム運用を実現することを目指しました。 マスト Argo Workflows Server自体のエラー Argo Workflows Serverのリソース使用量 Workflowの成否 Workflowの実行数 Workflowの実行時間(平均・中央・最大・最小・95%tile) Workflowのリソース使用量 オプション ステップごとの成否 ステップごとの実行時間 ステップごとのリトライ回数 ステップごとのリソース使用量 ワークフローのリトライ回数 上記を満たすようなツール選定、モニタリング方法を調査しました。 ツール選定 ワークフローの監視ツール選択の基準としては以下の3つの観点で考えました。 上記の監視項目を達成できるか 運用コスト 金銭コスト この観点で調査すると候補は以下になりました。 ログ取得 Self-Hosted Prometheus Google Cloud Managed Service for Prometheus Datadog Agent Dashboard(アラート) Datadog Cloud Monitoring モニタリング基盤自体に運用工数を割きたくなかったため、ログ取得部分はDatadog AgentかManaged Prometheusに絞りました。どちらもやりたいことを達成できますが、結果的にGoogle Cloudで完結してコストが安く、運用がしやすい Google Cloud Managed Service for Prometheus + Cloud Monitoring を使用しました。 GKEではManaged Prometheusは、以下のバージョンでデフォルトで有効になっています。 GKE v1.25以降を実行している GKE Autopilot クラスタ GKE v1.27以降を実行している GKE Standard クラスタ AI Shiftでは、ソフトウェアエンジニアが主にCloud Monitoringをシステム監視に利用しています。このため、MLエンジニアも同様にCloud Monitoringを使用することで、全員が使い慣れたツールで運用できることを目指しました。 Managed Prometheus + Cloud Monitoringでの監視 Cloud Monitoringとは Cloud Monitoring はアプリケーションとインフラストラクチャのパフォーマンス、可用性、健全性の可視化を提供します。取得したメトリクスに対して閾値を設けることでアラートを上げたり、外部ツールとの連携も可能になります。 Google Cloud Managed Service for Prometheusとは Prometheus は、オープンソースのシステム監視とアラートツールです。 Google Cloud Managed Service for PrometheusはフルマネージドなPrometheusでMonarchアーキテクチャ上に構築されています。 料金 も安く、100万サンプルで0.06ドル程度になっています。Argo Workflowsではデフォルトでworkflow-controllerの /metrics からPrometheus形式のメトリクスを吐き出しています。 Argo Workflowsのメトリクス Argo WorkflowsがPrometheus形式で取れるメトリクスには以下の2種類があります。 コントローラメトリクス カスタムメトリクス コントローラメトリクス コントローラメトリクスは コントローラの状態を通知するメトリクスであり、サーバの状態等を自動的に <host>:9090/metrics に吐き出します。 現在取れるメトリクスは以下のとおりです。 cronworkflows_triggered_total CronWorkflowが実行された回数 gauge 各フェーズで現在クラスター内にあるワークフローの数 error_count コントローラによって発生した特定のエラーの数 k8s_request_total Kubernetes API に送信された API リクエストの数 k8s_request_duration 各タイプのリクエストにかかった時間 is_leader コントローラがリーダーかどうか log_messages コントローラによってログレベル別に発行されたログメッセージの数 operation_duration_seconds ワークフローの操作の所要時間 pods_gauge ワークフローによって作成されたポッドの数 pod_missing 実行されなかったポッドの数 pod_pending_count 保留状態にされたポッドの数 pods_total_count コントローラによってスケジューリングされたポッドの数 queue_adds_count コントローラ内のキューへ追加された数 queue_depth_gauge キューに溜まってる数 queue_duration キュー内のイベントを捌くのにかかる時間 queue_latency キュー内のイベントが処理されるまでにかかる時間 queue_longest_running キューで最も長く実行されている秒数 queue_retries キュー内でメッセージが再試行された回数 queue_unfinished_work まだ処理されていないキュー項目の数 total_count 処理されているワークフローの数 version コントローラのメタデータ。属性は こちら workers_busy_count 処理中のキューのワーカー数 workflow_condition それぞれのステータスのワークフローの数 workflowtemplate_runtime 各ワークフローテンプレートの実行時間 workflowtemplate_triggered_total 各ワークフローテンプレートの実行数 Prometheusの他、 OpenTelemetryへの メトリクスの出力が可能です。 カスタムメトリクス カスタムメトリクスはユーザー側で定義できる一連のワークフローの状態を通知するメトリクスです。ステップごとの実行時間などはこちらで定義することになります。 Argo Workflowsの設定 Argo WorkflowsのメトリクスをPrometheusに出力しようと思うと、PodMonitoringと呼ばれるCRDを定義する必要があります。Cluster全体のPodをモニタリングしたい場合はClusterPodMonitoringを使うことができます。 apiVersion: monitoring.googleapis.com/v1 kind: PodMonitoring metadata: name: argo-workflows-monitoring namespace: sample-namespace spec: selector: matchLabels: app: workflow-controller endpoints: - port: metrics interval: 30s timeout: 10s metricRelabeling: - action: keep regex: sample.* sourceLabels: [__name__] 設定としてはmetricsのエンドポイントに30秒ごとにメトリクスを取りに行ってます。ここで重要になるのはmetricRelabelingです。こちらを設定しない場合、全てのメトリクスを取得してPrometheusに送ってしまうので想定以上にコストがかかる可能性があります。こちらで取りたいメトリクスのみに絞って取ることをおすすめします。 そしてWorkflow ControllerのConfigMapにモニタリングに関する設定を追加します。 下記ではmetricsのTTL( Time To Live )を10分としています。 apiVersion: v1 kind: ConfigMap metadata: name: workflow-controller-configmap namespace: sample-namespace data: metricsConfig: | enabled: true metricsTTL: "10m" そして、カスタムメトリクスを取りたいため、各WorkflowTemplateにmetricsプロパティを追加します。こちらではステップごとの実行時間と実行の成否、リトライ数をメトリクスとして送っています。 apiVersion: argoproj.io/v1alpha1 kind: WorkflowTemplate metadata: name: sample-step namespace: sample-namespace spec: serviceAccountName: sample-sa templates: - name: sample-step nodeSelector: cloud.google.com/gke-nodepool: sample-node-pool inputs: parameters: - name: task-config container: image: image_uri command: ["python", "main.py"] metrics: prometheus: - name: step_duration help: duration of current step labels: - key: template_name value: sample-step gauge: value: "{{workflow.duration}}" - name: step_result help: result of step executed lastly labels: - key: template_name value: sample-step - key: status value: "{{workflow.status}}" gauge: value: "1" - name: step_retry_count help: retry count of step labels: - key: template_name value: sample-step gauge: value: "{{retries}}" これらをapplyするだけで自動的にCloud Monitoring上からメトリクスを取れるようになるので、そこからはPromQL等で可視化していくことになります。 作成したダッシュボード 上記を設定後作成したダッシュボードは以下のようになりました。月2ドル前後で必要なメトリクスの監視ができているため、コスト的にも納得できるものになりました。 おわりに 今回はArgo Workflowsで構築したLLMを使った要約サービスのMLパイプラインの監視にManaged PrometheusとCloud Monitoringを使用した事例を紹介しました。少人数でGKE上に構築したサービスを運用する場合、Managed Prometheus + Cloud Monitoringは有力な候補になりうると思いました。低コストで上手くマネージドサービスを使用しつつ、必要なサーバメトリクスを取れるメリットはかなり大きいと思います。今回はどちらかというとソフトウェア側の監視になり、LLMの出力自体の監視やMLの監視はまだまだ発展途上ですが、今後このあたりのML監視にも力を入れていきたいと思います。   投稿 Argo Workflowsで構築したMLパイプラインの監視事例 は 株式会社AI Shift に最初に表示されました。
アバター
はじめに こんにちは。AIチームの栗原健太郎です。 今年度開催の2024年9月4日(水)〜2024年9月6日(金)に梅田スカイビルで行われた第19回YANSシンポジウム (YANS2024) にAIチームから自分を含む4名 (栗原, 長澤, 大竹, 邊土名) が聴講参加しました (邊土名は本所属AI Lab)。 また、僕自身は今年度よりYANS委員として、微力ながら本シンポジウムの運営業務にも参画しておりました。 昨年での東京開催に続きオフラインでの開催となった今回のYANS2024は、参加者数・発表者数共に過去最多人数であったということで、NLP分野への関心がより高まっていることを実感させられるシンポジウムでした。会場の熱気も高く、終始参加者間の議論が絶えない賑やかな空間でした! 本記事では、数多くの発表が行われた今回のYANSの中から聴講参加した我々4名が特に興味を魅かれた研究発表をピックアップする形で、参加報告をさせていただきます。 以降の内容に関しましては、全て著者の方に掲載許諾を頂いた内容になります。 研究発表の紹介 選んだ論文(栗原) [S4-P39] 応答候補の多様性を用いた参照応答集合の評価に向けて 佐藤 魁 (東北大), 吉野 幸一郎 (東工大/理研), 河野 誠也 (理研/NAIST), 赤間 怜奈 (東北大/理研) 研究の概要 雑談対話システムの性能を適切に評価するための第一歩として、ある文脈に対する応答候補の大きさに注目する研究です。本研究では、応答の候補が多様であるほど、応答候補の適切さを評価するための参照応答の大きさもまた大きくなるという仮説を検証するためのロードマップを示しております。さらに、実験では、既存の対話データセットを人手アノテーションによって適切な応答評価ができている事例とそうでない事例とで分類した上で、それぞれの事例に対する言語モデルの応答の多様性を比較した上で、今後の研究の指針を示しておりました。 気になった理由 雑談対話システムの評価が一般的に難しいとされている中で、応答の候補の多様さを可視化するという試みが斬新であると感じました。参照応答の大きさを考慮することで応答候補を可視化しその大きさを予測する糸口になるという示唆は、雑談システムによらずある発話や文脈に対する応答の妥当性を評価する上で参考になるものでした。長期的な目線で「弊社のボイスボットで将来的にLLMによる自由度の高い対話を実現させることができたら」という世界観を考える上での重要な研究になりうると考え、選択しました! 選んだ論文(長澤) [S4-P04] LLMの"衝突回避" : LLMと制御理論の融合 宮岡 佑弥 (慶應大), 井上 正樹 (慶應大) 研究の概要 こちらは慶應大の宮岡さんの研究で、LLM のアライメントに対し制御理論の観点からのアプローチを提案されているものになります。 制御理論では、障害物への衝突回避のための理論が数多く提唱されているという背景があります。このアナロジーとして、LLMアライメントを「不適切なテキストという障害物への衝突回避」と捉えた研究内容になっております。 具体的には、RLHFなどの再学習を必要とせず、LLMの出力に介入するアドオン型の制御ロジックを導入することでアライメントを行うものになっています。 結果として、不適切なトークン出力を抑制できることが報告されています。 気になった理由 AI Shift やその親会社のサイバーエージェントでは、LLM を組み込んだプロダクトをユーザーさんに提供する際に、どう出力を制御するかを日々議論しています。 この研究は着想がとてもユニークであることもさることながら、再学習を必要とせずに LLM の振る舞いを制御するという取り組み内容が非常に興味深く感じられました。 発表の中では、衝突回避の弊害として「ニュアンスの乖離」が挙げられていましたが、この辺りの今後の取り組みも含め、引き続き注目に値する研究だと感じました。 選んだ論文(大竹) [IV-P02] 人間らしい対話とは:非言語情報生成のための基盤モデル 井上 昂治 氏(京都大学) 研究の概要 人間同士の対話では、相槌・フィラーといった非言語的な情報が頻繁に利用されます。この非言語情報が人間らしい対話において重要であるという背景のもと、リアルタイムで自然な対話を実現するための研究が行われています。具体的には、2話者から成るマルチチャネルの音声を入力として受け取り、CNNベースの音声特徴量エンコーダとTransformerを用いて、現在および未来の音声活動を予測します。これにより、1対1の会話において、ターン終了予測や自然なタイミングでの相槌やフィラーの出力が可能となります。研究のゴールとして、対話破綻のないロボットの対話の実現が目指されています。 気になった理由 弊社AI Shiftでは、AI自動音声対話システム「AI Messenger Voicebot」を開発・運用しています。このVoicebotにおいても、柔軟な対話を実現するための自然なターンテイキングが重要な課題となっています。そのため、本研究は弊社にとって非常に有用な示唆を与えてくれると考えています。また、発話の割り込みに対応できたり、マルチリンガルにも対応できるとのことだったので非常にこの分野の発展に期待しています。プロダクトへの応用可能性を考慮すると、この分野の研究は引き続き注目すべきだと考えています。 選んだ論文(邊土名) [S3-P24] チェックリストを利用した生成系タスクの網羅的評価 古橋 萌々香 (東北大/NII), 中山 功太 (NII), 児玉 貴志 (NII), 菅原 朔 (NII), 関根 聡 (NII/理研), 宮尾 祐介 (東大/NII) 研究の概要 近年、生成系タスクの自動評価においてLLMが利用されてきていますが、適切な指示方法や評価基準は未だ確立されていません。 そこで、この研究では、生成系タスクの自動評価を改善するために12種類の評価指標と54項目のチェックリストを提案しています。 この評価指標とチェックリスト自体もLLMを用いて生成されており、その内容を人手で確認、修正したものを採用しているとのことでした。 Ichikara-instructionデータセットと作成したチェックリストを用いた評価実験では、以下の結果が示されていました。 チェックリストとオープンLLMを用いた自動評価と人手評価との一致率は約60% 評価指標によっては評価が揺れる傾向にある項目もあることを確認 気になった理由 AI Shiftやその親会社であるサイバーエージェントでは、様々な研究やプロダクトでLLMを活用していますが、"LLMの生成結果をどのように評価すればよいか"は未だに課題となっています。 この研究では、性能評価指標のリストアップと評価をLLMを用いて行う手法を提案しており、様々なタスクの評価に対応できるフレームワークになる可能性を感じました。 AI ShiftのRAGプロダクトの生成結果の評価や、サイバーエージェントで行なっている広告生成のようなクリエイティブなテキスト生成の評価にも適用できそうな手法で、学術的な価値だけでなく実務的にも有用な研究だと感じました。 ちなみに、こちらの研究はYANSスポンサー賞であるサイバーエージェント賞に選定させていただきました。 最後に 今回もLLMをメインテーマに据えた研究が多いと感じる一方で、取り扱う評価の幅やモダリティは拡大の一途を辿っているとも感じられるシンポジウムでした。今回記事では取り上げられなかった画像領域や、弊社で常に関心を寄せる音声領域にまたがる研究がますます増えていることから、LLMがこれらの領域の研究をより加速させたことを実感しました。 チャットボット・ボイスボットの開発に加え、生成AIコンサル事業を展開する弊社としてより、幅広くアンテナを張っていく必要性を再度強く感じました。 今回、このような良い刺激をもらえる素敵なシンポジウムを企画・運営してくださった委員の皆様、本当にありがとうございました(自分も委員ではありましたが笑)。 私事ではありますが、来年度のYANSでも委員を務めさせていただく予定ですので、運営として、また一参加者として来年も楽しめたら良いなと思っております! 投稿 YANS2024参加報告 は 株式会社AI Shift に最初に表示されました。
アバター
はじめに こんにちは、AI チームの長澤です。 この記事では Google によって開発・提供されている AutoMLOps を使って、充実した CI/CD パイプラインを手軽に構築してみようと思います。 今回は公式によって提供されているチュートリアルの流れに沿って、どのような GCP サービスが展開・構築されるのかを実際に手を動かして試してみます。 AutoMLOps AutoMLOps は Google によって提供されている OSS で、CI/CD 機械学習パイプラインを手軽に構築できるツールになっています。 各パイプライン処理を実施する関数を定義するだけで、必要なサービスを自動で provisioning, deploy してくれます。 基本的な使い方 AutoMLOps での CI/CD パイプライン構築ですが、自分で行うのはコードベースで処理を定義する程度です。 少しだけ記述の仕方に癖がありますが、操作としては単純です。 パイプライン処理の記述で使うのは主に以下の2つのデコレータです。 .custom-list li { margin-bottom: 20px; /* 項目下に20pxの間隔を追加 */ } .custom-list li:last-child { margin-bottom: 0; /* 最後の項目の下には間隔を追加しない */ } @AutoMLOps.component(...) パイプライン処理のそれぞれの処理を定義する際に使います。関数内で使用するライブラリなどのバージョンをここで指定することもできます。 @AutoMLOps.pipeline(...) それぞれのコンポーネントをパイプラインとして組み合わせる際に使用します。各要素をどのような順序で実行するかを制御できます。 この2つを使ってパイプライン処理を定義したら、実際にサービスをデプロイする作業に移ります。これを実現するために6つの関数が提供されています。 AutoMLOps.generate(...) 上の2つのデコレータを使って定義したパイプライン処理のための MLOps サービスを展開するために必要なコードを自動生成します。具体的には各コンポーネントごとに設定や構成情報が記述された YAML ファイルや GCP の各サービスを展開するために必要な CLI コマンドが記述されたスクリプトが自動作成されます。 AutoMLOps.provision(...) 自動作成されたスクリプトをもとに、Artifact Registory の作成やサービスアカウントの設定等を実施するための関数です。 AutoMLOps.deploy(...) ビルドや docker image の push を行い、実際にパイプライン処理をデプロイするために使用します。 AutoMLOps.monitor(...) エンドポイントにデプロイされたモデルに対して、monitoring job を実行する際に使用します。 AutoMLOps.launchAll(...) generate() , provision() , deploy() の3つの関数を一気に実行します。 AutoMLOps.deprovision(...) AutoMLOps によって自動作成された MLOps のインフラを削除するために使用します。 terraform と pulumi をフレームワークとして指定して AutoMLOps を利用した場合のみ使えるようです。(記事公開時点) 今回は generate() で MLOps に必要なコードを自動生成し、 provision() , deploy() でサービスを実際に展開したのち、 monitor() を使ってエンドポイントへ推論リクエストを送るところまで実施してみます。 実際に使ってみた 今回は GitHub ページに公開されている サンプルコード を使用します。 内容としては、BigQuery から学習データを取得し、多クラス分類を行う決定木モデルを学習、最後に推論エンドポイントを立てる流れになります。 コードをまとめたり、リージョンを変更するための編集を行いますが、基本的にはサンプルコードをそのまま利用する形になっています。 今回構築されるCI/CDパイプラインの全容としては以下のような形になっています(画像は GitHub ページ より引用)。 事前準備 ライブラリとして主に必要になるのは google-cloud-automlops ですが、必要に応じて google-cloud-bigquery や google-cloud-aiplatform も pip などでインストールしておきます。 また Python のバージョンとしては Python 3.7 - 3.10 がサポートされているようですので、ご留意頂ければと思います。 また事前準備として、使用するデータセットを BigQuery にアップロードしておきます。こちらも 用意されている csv ファイルと関数 を使えば手軽に準備できます。 以下のように BQ 上に展開されれば事前準備としては完了になります。 用意したプログラム 今回は以下のようなディレクトリ構造で開発をスタートします。 . ├── .env # リージョン指定などに使用する環境変数 ├── data # BQ にアップロードしたデータセット │ └── Dry_Beans_Dataset.csv └── src ├── callable_functions.py # component を用意 ├── main.py # pipeline を組み, AutoMLOps を実行 ├── inference.py # 推論用のコードを記述 └── upload_to_bq.py # 事前準備で使用 主に必要になるのは、構成要素を定義する callable_functions.py とそれらをパイプラインとして組み立てる処理が記述された main.py になります。 inference.py は推論リクエスト処理に関するもので、本記事の後半で使用します。 またリージョンなどの環境変数は .env に定義することとします。 ( Dry_Beans_Dataset.csv および upload_to_bq.py は事前準備のデータアップロードで使用したものになるので、以降は特に使用しません) callable_functions.py ここでは @AutoMLOps.component(...) のデコレータを使用してパイプラインで必要になる各処理を定義します。ここで AutoMLOps 特有の記述方法が出てきます。それはそれぞれの関数で処理に必要なライブラリを関数の内側で import する記述が必要になります。 またこの時、デコレータの引数として packages_to_install が用意されており、ライブラリのバージョン指定が可能となっています。バージョン指定が必要ない場合などは空でも問題ありません。 今回は「データの準備 → モデルの学習 → エンドポイントにデプロイ」するためにそれぞれを関数として用意します。 create_dataset(...) : 学習に必要なファイルを BQ から取得し、前処理を行います。 train_model(...) : 多クラス分類を行う決定木モデルの学習、保存を行います。 deploy_model(...) : 学習済みのモデルをエンドポイントにデプロイし、推論リクエストを受け付けられるようにします。 関数の中に必要な処理を全て盛り込むような書き方になるので、少し煩雑に見えますが、処理としては比較的単純なものばかりとなっています。 from google_cloud_automlops import AutoMLOps @AutoMLOps.component( packages_to_install=[ 'google-cloud-bigquery', 'pandas', 'pyarrow', 'db_dtypes', 'fsspec', 'gcsfs' ] ) def create_dataset( bq_table: str, data_path: str, project_id: str ): from google.cloud import bigquery import pandas as pd from sklearn import preprocessing bq_client = bigquery.Client(project=project_id) def get_query(bq_input_table: str) -> str: # sample code そのままですが、実行時はご注意ください return f''' SELECT * FROM {bq_input_table} ''' def load_bq_data(query: str, client: bigquery.Client) -> pd.DataFrame: df = client.query(query).to_dataframe() return df dataframe = load_bq_data(get_query(bq_table), bq_client) le = preprocessing.LabelEncoder() dataframe['Class'] = le.fit_transform(dataframe['Class']) dataframe.to_csv(data_path, index=False) @AutoMLOps.component( packages_to_install=[ 'scikit-learn==1.2.2', 'pandas', 'joblib', 'tensorflow' ] ) def train_model( data_path: str, model_directory: str ): from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split import pandas as pd import tensorflow as tf import pickle import os def save_model(model, uri): """Saves a model to uri.""" with tf.io.gfile.GFile(uri, 'w') as f: pickle.dump(model, f) df = pd.read_csv(data_path) labels = df.pop('Class').tolist() data = df.values.tolist() x_train, x_test, y_train, y_test = train_test_split(data, labels) skmodel = DecisionTreeClassifier() skmodel.fit(x_train,y_train) score = skmodel.score(x_test,y_test) print('accuracy is:',score) output_uri = os.path.join(model_directory, 'model.pkl') save_model(skmodel, output_uri) @AutoMLOps.component( packages_to_install=[ 'google-cloud-aiplatform' ] ) def deploy_model( model_directory: str, project_id: str, region: str ): import pprint as pp import random from google.cloud import aiplatform aiplatform.init(project=project_id, location=region) # Check if model exists models = aiplatform.Model.list() model_name = 'beans-model' if 'beans-model' in (m.name for m in models): parent_model = model_name model_id = None is_default_version=False version_aliases=['experimental', 'challenger', 'custom-training', 'decision-tree'] version_description='challenger version' else: parent_model = None model_id = model_name is_default_version=True version_aliases=['champion', 'custom-training', 'decision-tree'] version_description='first version' serving_container = 'us-docker.pkg.dev/vertex-ai/prediction/sklearn-cpu.1-2:latest' uploaded_model = aiplatform.Model.upload( artifact_uri=model_directory, model_id=model_id, display_name=model_name, parent_model=parent_model, is_default_version=is_default_version, version_aliases=version_aliases, version_description=version_description, serving_container_image_uri=serving_container, serving_container_ports=[8080], labels={'created_by': 'automlops-team'}, ) endpoint = uploaded_model.deploy( machine_type='n1-standard-4', deployed_model_display_name='deployed-beans-model') sample_input = [[random.uniform(0, 300) for x in range(16)]] # Test endpoint predictions print('running prediction test...') try: resp = endpoint.predict(instances=sample_input) pp.pprint(resp) except Exception as ex: print('prediction request failed', ex) main.py ここでは @AutoMLOps.pipeline(...) を使って機械学習パイプラインを構築します。 pipeline() 関数を定義し、各コンポーネントを実行します。 この時それぞれの呼び出しのタイミングで .after(another_component) としてどの処理の後に実行するかを明示的に指定します。 これで AutoMLOps を利用する準備は完了です! それでは main() 関数として、サンプルコードに対して monitoring などの処理を少し書き加えて動かしてみようと思います。 import os import sys import datetime import argparse from dotenv import load_dotenv from google.cloud import aiplatform from google_cloud_automlops import AutoMLOps from callable_functions import create_dataset, train_model, deploy_model from inference import run_inference load_dotenv() def get_args(): parser = argparse.ArgumentParser() parser.add_argument('--operation', type=str, required=True, choices=['provision', 'deploy', 'generate', 'monitor']) return parser.parse_args() @AutoMLOps.pipeline def pipeline( bq_table: str, model_directory: str, data_path: str, project_id: str, region: str): create_dataset_task = create_dataset( bq_table=bq_table, data_path=data_path, project_id=project_id) train_model_task = train_model( model_directory=model_directory, data_path=data_path).after(create_dataset_task) deploy_model_task = deploy_model( model_directory=model_directory, project_id=project_id, region=region).after(train_model_task) pipeline_params = { 'bq_table': os.getenv('TRAINING_DATASET'), 'model_directory': f'gs://{os.getenv("PROJECT_ID")}-{os.getenv("MODEL_ID")}-bucket/trained_models/{datetime.datetime.now()}', 'data_path': f'gs://{os.getenv("PROJECT_ID")}-{os.getenv("MODEL_ID")}-bucket/data.csv', 'project_id': os.getenv('PROJECT_ID'), 'region': os.getenv('REGION'), } if __name__ == '__main__': args = get_args() if args.operation == 'generate': AutoMLOps.generate( project_id=os.getenv('PROJECT_ID'), pipeline_params=pipeline_params, use_ci=True, naming_prefix=os.getenv('MODEL_ID'), schedule_pattern='59 11 * * 0', # retrain every Sunday at Midnight setup_model_monitoring=True, # use this if you would like to use Vertex Model Mointoring artifact_repo_location=os.getenv('REGION'), build_trigger_location=os.getenv('REGION'), pipeline_job_submission_service_location=os.getenv('REGION'), schedule_location=os.getenv('REGION'), storage_bucket_location=os.getenv('REGION'), ) if args.operation == 'provision': AutoMLOps.provision(hide_warnings=False) if args.operation == 'deploy': AutoMLOps.deploy(precheck=True, hide_warnings=False) # optional if args.operation == 'monitor': aiplatform.init(project=os.getenv("PROJECT_ID"), location=os.getenv("REGION")) endpoints = aiplatform.Endpoint.list(filter='display_name="beans-model_endpoint"') print(aiplatform.Endpoint.list()) if not endpoints: print('No endpoint found. Please deploy the model first.') sys.exit() else: AutoMLOps.monitor( alert_emails=['email'], target_field='Class', model_endpoint=aiplatform.Endpoint.list(filter='display_name="beans-model_endpoint"')[0].resource_name, monitoring_interval=1, auto_retraining_params=pipeline_params, drift_thresholds={'Area': 0.000001, 'Perimeter': 0.000001}, skew_thresholds={'Area': 0.000001, 'Perimeter': 0.000001}, training_dataset=f'bq://{os.getenv("TRAINING_DATASET")}', hide_warnings=False, monitoring_location=os.getenv('REGION'), ) run_inference() AutoMLOps によるコード自動生成 まずはじめに、 AutoMLOps.generate(...) で必要なファイルを自動生成します。 下記コマンド実行により AutoMLOps ディレクトリが自動生成されます。 このディレクトリには先ほど作成したパイプラインに基づいて、GCP の各種サービスを展開するために必要なマニフェストやスクリプトが含まれており、それぞれ provisioning や deploy を行うときに実行されるファイルとなっています。 またこの時 artifact_repo_location などの引数を使って、各種サービスを展開する際のリージョン指定や schedule_pattern でのスケジューリングパターンを指定できます。 python src/main.py --operation generate 上記コマンドを実行し Code Generation Complete. の文言が出力されれば成功です! AutoMLOps による provisioning, deploy では自動生成されたファイルを使って、実際にサービスの provisioning を行います。 以下のコマンドを叩いて AutoMLOps.provision(...) の実行します。 python src/main.py --operation provision 実体としては自動生成された AutoMLOps/provision/provision_resources.sh が実行されます。ここでは以下のサービスが自動で作成されますが、同名のものが既に存在する場合はスキップされます。 Artifact Registory Cloud Storage Bucket Pipeline Job Runner Service Account の作成および権限設定 Cloud Source Repogitory Pub/Sub Topic Cloud Run Function Cloud Build Cloud Scheduler 展開されるサービスの多さから、自動ながらもリッチな CI/CD パイプラインが自動構築される様子が伺えます。 続いてデプロイ作業を行います。以下のコマンドにより AutoMLOps.deploy(...) の実行します。 python src/main.py --operation deploy このコマンドを実行すると Cloud Source Repogitory へのコード転送などが行われ、これをトリガーに docker image のビルドなどが走ります。ビルドの詳細については自動生成された AutoMLOps/cloudbuild.yaml に書かれています。Component ごとに image を分けたい場合は別途対応が必要になりますが、この辺りも自動で生成してくれるのはありがたいですね。 ビルドがうまくいくと VertexAI 上でモデル訓練のためのパイプライン処理が走ります。 学習が完了するとモデルが Model Registory に出現し、エンドポイントに自動でデプロイされます。 また今回の場合、上記の VertexAI パイプラインは Cloud Scheduler の Pub/Sub トピック送信により Cloud Run 関数がトリガーされることで、定期的な実行もされるようになっています。 ここまで無事に実行されれば、あとはエンドポイントに対する推論リクエストが可能になります。 モデルエンドポイントのモニタリング 最後に、学習済みのエンドポイントに対して推論リクエストを送ります。以下のコマンドで AutoMLOps.monitor(...) を実行します。 VertexAI 上で monitoring job が作成されます。 import os from dotenv import load_dotenv from sklearn.preprocessing import LabelEncoder from sklearn.metrics import classification_report from google.cloud import bigquery, aiplatform import pandas as pd load_dotenv() def get_query(bq_input_table: str) -> str: # sample code そのままですが、実行時はご注意ください return f''' SELECT * FROM {bq_input_table} ''' def load_bq_data(query: str, client: bigquery.Client) -> pd.DataFrame: df = client.query(query).to_dataframe() return df def run_inference(num_samples: int=30): bq_client = bigquery.Client(project=os.getenv('PROJECT_ID')) df = load_bq_data(get_query(os.getenv('TRAINING_DATASET')), bq_client) sampled_df = df.sample(n=num_samples) X_sample = sampled_df.iloc[:, :-1][:num_samples].values.tolist() Y_sample = sampled_df.iloc[:, -1][:num_samples].values.tolist() endpoints = aiplatform.Endpoint.list(filter=f'display_name="beans-model_endpoint"') endpoint_name = endpoints[0].resource_name endpoint = aiplatform.Endpoint(endpoint_name) response = endpoint.predict(instances=X_sample) predictions = response[0] # calc accuracy le = LabelEncoder() y_true = df.iloc[:, -1].values.tolist() le.fit(y_true) y_true_encoded = le.transform(Y_sample) print(classification_report(y_true_encoded, predictions)) 推論の中身としては上記の inference.py に記述したものを行います。 学習データからランダムに 30 件データをサンプルし、結果を見てみます。100% の accuracy となっている推論結果が無事に返ってきました。 precision recall f1-score support 0 1.00 1.00 1.00 3 1 1.00 1.00 1.00 2 2 1.00 1.00 1.00 3 3 1.00 1.00 1.00 11 4 1.00 1.00 1.00 5 5 1.00 1.00 1.00 5 6 1.00 1.00 1.00 1 accuracy 1.00 30 macro avg 1.00 1.00 1.00 30 weighted avg 1.00 1.00 1.00 30 このとき、スキューやドリフトが検知されると monitoring job によりメールが飛んでくることがあります。閾値やメール送信先の設定は AutoMLOps.monitor(...) の引数で指定できます。 実際の運用を考えると、こういった検知までを手軽に組み込めるのは嬉しいですね。 おわりに この記事では AutoMLOps のサンプルコードを使用して実際にどのように動くのかを検証してみました。 AutoMLOps によって展開されるサービスはユーザが選択できる箇所もあり、希望に沿った柔軟な CI/CD パイプラインを組むことができます。例えば今回は Cloud Source Repogitory を利用していましたが、Github Actions や Gitlab, Bitbucket を代わりに使用することもできます。 この記事が「AutoMLOps、使ったことなかった」や「手軽に機械学習のCI/CDパイプラインを組みたかった」という気持ちを抱えていた人の一助になれば幸いです。 投稿 AutoMLOpsを使って機械学習CI/CDパイプラインを組んでみた は 株式会社AI Shift に最初に表示されました。
アバター
はじめに こんにちは,AIチームの下山です. 弊社では自動音声対話サービスVoicebotにおける施策の検証方法として,ベイジアンA/Bテストを採用しています. (ベイジアンA/Bの概要については こちらの記事 をご覧ください). 本記事では,ベイジアンA/Bにおいて,効果が高い施策の選択を間違える(例えば,AとBの比較を行い,本来はAの方がBより効果が高いのにBを選択してしまう)可能性をある程度抑えるために必要なサンプルサイズの見積もり方について,ヒューリスティックな方法を構築しましたのでそれについて紹介します. (私の調査不足かもしれませんが)ベイジアンA/Bにおけるサンプルサイズの見積もり方に関して扱っている日本語の記事はそれほど多くあるわけではないと思っており,この記事がこのトピックへの情報提供の一助になれば幸いです. モチベーションと背景 私たちはVoicebotでのユーザ体験に影響を与える変数(例えば,話速,シナリオ内容など)の最良値の探索にベイジアンA/Bテストを活用しています. ここでは,例として,VoicebotにおけるシナリオとしてAパターン・Bパターンを用意し,離脱率が低い方が良いという観点のもと,どちらのシナリオが良いか?の検証を考えます. また,AパターンBパターンどちらが良いのか?断定を保留にするのか?の判定は事後分布を用いて計算した確率に基づいて定量的に行うとします. A/Bテストの検証期間内に得られるデータは有限であるため,本来はAパターンよりBパターンの方が良かったとしても,データの偏りなどにより次のように判定してしまう場合が存在します: Aの方が良いと判定 Aが良いかBが良いか断定できない(保留) 実際,プロダクトで検証を行なっていると,同じ内容のA/Bテストを複数回行った際に,1回目では「Aの方が良い」と判定されたが2回目では「Bの方が良い」と判定されるように,何回目のテストかによって判定結果が変わってしまう事象が観測されました. つまり,そのようなA/Bテストでは,AとBのどちらが良いかの判断を間違えてしまっている可能性があります. 特に,このような事象はサンプルサイズが小さい時に発生しやすいことも分かっています. 本来はBの方が良いときに,判定1が起こってしまう確率を誤り率,判定2が起こってしまう確率を保留率と呼ぶことにすると,上述した問題に対処するために誤り率と保留率を十分抑えるサンプルサイズが知りたいというモチベーションが生まれます. 実は,A/Bテストに頻度論的な統計的仮説検定を用いた場合には,少なくとも誤り率を抑えるために必要なサンプルサイズを計算することができます. しかし,ベイジアンA/Bにおいては厳密な計算方法が現状は無いと思われます(私の勉強不足な可能性もありますので,ご存じの方がいましたらぜひ教えていただきたいです). そこで,本記事では,論文「 Beyond Power Calculations: Assessing Type S (Sign) and Type M (Magnitude) Errors 」の内容を参考に,実行予定のベイジアンA/Bにおいて,誤り率と保留率を抑えるために必要なサンプルサイズを見積もるためのヒューリスティックな方法を構築します. 見積もり方法とシミュレーション結果 上述の論文では,2施策の比較の統計的検定に対し,誤った施策を選択してしまう確率を検証後に見積もる方法が提案されています. この方法のキーアイデアは「シミュレーションにより同一内容の検証を複数回行うことで,そのデータを用いて検定方法が誤った施策を選択してしまう確率を評価する」です. このアイデアを活用します. 先ほどの例,シナリオAとBの比較,を考えます. ここでは,比較したい指標が離脱率,1通話に対し得られるデータが離脱したか否かの2値ですので,事前分布をベータ分布,事後分布をベルヌーイ分布とします. シナリオAに対する事後分布とシナリオBに対する事後分布の差を判定分布\(P_{A-B}\)と呼ぶことにすると,この判定分布を用いてAとBどちらが良いかを以下のように判定することにします: Aの離脱率がBの離脱率以下になる確率が \(T_{judge}\) 以上,つまり \(P_{A-B}(\{\leq 0 \}) \geq T_{judge} \),ならAの方がBより良いと判断 Bの離脱率がAの離脱率以下になる確率が \(T_{judge}\) 以上,つまり \(P_{A-B}(\{\geq 0 \}) \geq T_{judge} \),ならBの方がAより良いと判断 それ以外の場合,AとBどちらが良いかは保留 では,本当はBの方がAより良いと仮定した際,このベイジアンA/Bにおいて誤り率と保留率を抑えるために必要なサンプルサイズはいくつでしょうか. 先の論文のアイデアを活用し,サンプルサイズを見積もるシミュレーションに基づいた方法を以下のように構築します: 入力:Aの離脱率 \(\theta_A\), シナリオAに対する離脱率とシナリオBに対する離脱率の差 \(d\), サンプルサイズ \(N\) \(T\) 回繰り返す 確率 \(\theta_A\) のベルヌーイ分布から \(N\) サンプル \(D_A\) をサンプリング,確率 \(\theta_A - d\) のベルヌーイ分布から \(N\) サンプル \(D_B\) をサンプリング \(D_A\) と \(D_B\) を用いて作成した判定分布 \(P_{A-B}\) に基づいて,"Bが良い","Aが良い","保留"のいずれかを上述した判定ルールに基づき決定 T回中,"Aが良い"の割合・"保留”の割合を計算し,これらをそれぞれ誤り率・保留率とする この方法は,Aに対するデータの真の分布とBに対するデータの真の分布を仮定した仮想環境下において,同じ内容のベイジアンA/Bを何回も行うことで,誤り率と保留率を計測しています. そして,この仮想環境下でのベイジアンA/Bの誤り率と保留率が本番環境下(今回の例では,実際にVoicebotでシナリオAとシナリオBをテストすること)における誤り率と保留率を表しているだろうと考えます. ※ この考えは理論に基づいている訳ではなく,直感的にしたがっています.その意味で,"ヒューリスティックな方法"と述べています. ここまでの内容のコードは以下です: ここで,イテレーション回数は \(T=500\) としています. import numpy as np from scipy.stats import beta as scipy_beta def decision_rule(samples_diff, t_judge): a_leq_b_prob = np.count_nonzero(samples_diff <= 0) / len(samples_diff) b_leq_a_prob = np.count_nonzero(samples_diff >= 0) / len(samples_diff) if a_leq_b_prob >= t_judge: return 'A' elif b_leq_a_prob >= t_judge: return 'B' else: return 'nothing' def ABtesting(p_true_A, p_true_B, n_sample, t_judge): D_A = np.random.binomial(1, p_true_A, size=n_sample) D_B = np.random.binomial(1, p_true_B, size=n_sample) # A群に対する事後分布 S_A = D_A.sum() N_A = len(D_A) samples_A = scipy_beta.rvs(2 + S_A, 2 + N_A - S_A, size=(2, 30000)) # B群に対する事後分布 S_B = D_B.sum() N_B = len(D_B) samples_B = scipy_beta.rvs(2 + S_B, 2 + N_B - S_B, size=(2, 30000)) diff_samples = (samples_A - samples_B).flatten() win_treatment = decision_rule(diff_samples, t_judge) return win_treatment def type_error_estimates(p_true_A, true_diff, n_sample, t_judge): p_true_B = p_true_A - true_diff n_iters = 500 error_count = 0 no_win_count = 0 for _ in range(n_iters): win_treatment = ABtesting(p_true_A, p_true_B, n_sample=n_sample, t_judge=t_judge) if win_treatment == 'A': error_count += 1 elif win_treatment == 'nothing': no_win_count += 1 return error_count/n_iters, no_win_count/n_iters では,上の方法に基づいて,誤り率と保留率を抑制するために必要なサンプルサイズを見積もってみます. Aの離脱率 \(\theta_A\) はこれまでの入電記録の平均を取ることにし,それを仮に \(0.1\) だとします.AとBの差は期待される差を設定することにし,仮に \(d=0.03\) とします. また,AとBそれぞれの離脱率に差があると判断するための閾値である \(T_{judge}\) の値を仮に \(T_{judge} = 0.8\) とします. サンプルサイズを \(N = 10, 30, 50, 70, 100, 300, 500, 1000, 2000\) と変化させた時のエラー率と判断保留率を評価します. np.random.seed(42) n_samples = [10, 30, 50, 70, 100, 300, 500, 1000, 2000] error_rates, no_win_rates = [], [] for _ in range(5):     error_rates_tmp, no_win_rates_tmp = [], []     for n in n_samples:         error_rate, no_win_rate = type_error_estimates(p_true_A=0.1, true_diff=0.03, n_sample=n, t_judge=0.8)         error_rates_tmp.append(error_rate)         no_win_rates_tmp.append(no_win_rate)     error_rates.append(error_rates_tmp)     no_win_rates.append(no_win_rates_tmp) import matplotlib.pyplot as plt #plot figure plt.clf() fig, axes = plt.subplots(1, 2, figsize=(24, 8)) axes[0].plot(n_samples, np.mean(error_rates, axis=0)) axes[0].scatter(n_samples, np.mean(error_rates, axis=0)) axes[0].set_xscale('log') axes[0].set_title('Error rates', fontsize=20) axes[0].tick_params(labelsize=18) axes[1].plot(n_samples, np.mean(no_win_rates, axis=0)) axes[1].scatter(n_samples, np.mean(no_win_rates, axis=0)) axes[1].set_xscale('log') axes[1].set_title('No win rates', fontsize=20) axes[1].tick_params(labelsize=18) サンプルサイズに対する,誤り率と保留率の推移は上図のようになりました. この結果を見ると,今回のベイジアンA/Bの設定では,誤り率を5%程度に抑えたい場合,サンプル数を100以上は用意する必要がありそうだということが分かります. しかし,サンプルサイズ100程度だと,保留率が50%程度あり半分以上の割合で意思決定が保留されることになりそうだということもわかります. そして,保留率を10%以下に抑えたいとすると,サンプルサイズは1000以上は必要そうです. 終わり 本記事では,ベイジアンA/Bにおいて,誤った施策を選んでしまう確率・どの施策も選ばないと判断を保留する確率,これら2つの確率の抑制に必要なサンプルサイズを見積もるシミュレーションベースの方法を構築しました. ただ本記事の方法は理論的背景ではなく「同じ内容のA/Bテストを仮想的に何回も繰り返すことで,与えられたサンプルサイズに対して上記2つの確率を見積もる」という考え方に基づいたヒューリスティックな方法でした. 今回の記事がベイジアンA/Bにおけるサンプルサイズの決め方の参考の1つとなれば幸いです. 投稿 ベイジアンA/Bにおけるサンプルサイズを見積もる は 株式会社AI Shift に最初に表示されました。
アバター
はじめに こんにちは、AIチームの大竹です。 現在、AI Shiftではコールセンター業務自動化を目的とした自動音声対話システム、AI Messenger Voicebotを運用しています。 今回は、音声対話システムがカスタマーの意図を理解するための入り口として最も重要な技術である音声認識にフォーカスしたいと思います。音声認識の誤りが多くなると、後段の言語処理や対話管理に悪い影響を与えるため、音声認識誤りをいかに回避するかは極めて重要です。そして、音声認識の性能は収音環境やカスタマーの話し方の特徴など、様々な要因で左右されます。これは最先端の音声認識モデルを用いても容易に解決できない問題です。 そこで環境要因を排除するための方法として音声品質に着目したいと思います。音声認識が失敗しそうな場合、カスタマーに周辺環境の良い場所に移動してもらうなどの問いかけをすることで問題が部分的に解決できるのではないかと考えています。 今回の記事では、その前段階として、音声認識の誤り傾向と音声の品質との間にどれくらい関係があるかを分析したいと思います。 検証 方法 社内で作成したエンティティ認識用のデータセットに対して、音声認識をしてエンティティ認識の精度を測定します。別途、音声品質も測定しておきます。測定したエンティティ認識の精度と音声品質の間に関係があるかどうかを分析します。 データセットについて 名前、電話番号、郵便番号、生年月日などVoicebotでよく発話されるエンティティを27種類選定し、ランダムに作成した各エンティティに対し弊社メンバーで音声を読み上げデータを作成しました。各発話はエンティティのみを発話しており、フィラーなどは含まれません。 これらのエンティティタイプごとに99件の発話を用意し、全体の発話数は 27x99=2673 となっています。また、データセットに含まれる音声は電話回線を通して録音したものなので、電話音声品質になっています。 評価指標 エンティティ認識 音声認識結果が正解エンティティと完全一致しているかを判定します。 ただし、音声認識結果と正解テキストを比較する前に以下のテキスト処理を実行します。 数値表現を統一するため、漢数字やカンマなどの特殊文字を除去したり、漢数字をアラビア数字に変換したりする。 電話番号や郵便番号などの形式を統一するため、ハイフンや「の」などの区切り文字を除去する。 日付形式を標準化し、一貫した比較ができるようにする 音声品質 STOI (Short-Time Objective Intelligibility) 音声の可聴性を評価する指標です。STOIは 0から1まで の範囲でスコアを付け、1に近いほど音声が明瞭であることを示します。 STOI値が 高いほど、発言内容を聞き取りやすい ことを意味します。 PESQ (Perceptual Evaluation of Speech Quality) 知覚品質を評価する指標です。PESQは音声品質評価の標準的な指標で、 -0.5から4.5の範囲 でスコアを付けます。 スコアが高いほど音声品質が高く、リスナーにとって心地良い音声 となります。 SI-SDR(Scale-Invariant Signal-to-Distortion Ratio) 音声の歪みやノイズの度合いを評価する指標です。 SI-SDRの値が 低いほど、ノイズや歪みが多い ことを示します。 今回の検証では、 torchaudio-squim を用いて、前述したデータセットの音声に対して、上記の音声品質を推定します。torchaudio-squimを用いることで、参照音声が不要かつlightweightに音声品質の推定が可能です。 音声認識モデル 今回の検証では Google Cloudの音声認識API を用います。用いるモデルは以下の5種類です。 default command_and_search phone_call latest_short latest_long 各モデルの詳細については こちら をご参照ください。 分析方法 今回の検証では次の2つの方法を用いて、音声認識の精度と音声品質との間の関係を分析します。 相関分析 点双列相関係数 を使用し、音声品質とエンティティ認識が正解か否かの間にどれくらいの関係があるかを定量化します。ただし、点双列相関係数は連続変数と2値変数との間の線形関係を測定するものであるため、非線形の関係を捉えられないことに注意が必要です。 統計的検定 エンティティ認識が正解している場合と正解していない場合のデータを比較して、両者のグループの音声品質に統計的に有意な差があるかどうかを検定します。検定手法として、今回の検証では、データに正規分布を仮定しない Mann-Whitney U検定 を使用します。 結果 相関分析 以下にエンティティの予測が正解か否かと音声品質の各指標との相関係数を示します。 Model STOI PESQ SI-SDR default -0.0305 -0.0511* -0.0806* command_and_search -0.0768* -0.0652* -0.1324* phone_call -0.0103 -0.0402* -0.0100 latest_long -0.0115 -0.0237 -0.0349 latest_short -0.0444* -0.0699* -0.0467* (注): *は p < 0.05 で統計的に有意な差があることを示します。 いずれのモデルでもエンティティの予測が正解しているかどうかと、音声品質の指標との間の相関係数の絶対値が小さいので、両者に何らかの関係があるとは言えなさそうです。 統計的検定 分布の可視化 検定を行う前にエンティティ認識が正解している場合と正解していない場合で、各指標がどのように分布しているかをプロットしてみます。 下記は各指標に関するカーネル密度推定のプロットです。 いずれの分布も非対称な分布で、複数のピークを持つ分布もあります。エンティティ認識が正解しているグループ(match=1)と正解していないグループ(match=0)の分布を見比べると、密度の絶対値に差異はありますが、分布の形状の違いがあるかどうかはパッと見ただけではよく分かりません。 全体のデータに対する検定結果 音声品質の指標ごとに検定結果を示します。 正規化U統計量はエンティティ認識が正解している場合と正解していない場合のデータ群に対してどれだけの差があるかを示しています。この値が 0.5 から離れているほど差が大きいと言えます。 (注): *は p < 0.05 で統計的に有意な差があることを示します。 STOI Model 正規化U統計量 p値 default 0.478 0.086 command_and_search 0.451 0.000* phone_call 0.493 0.554 latest_long 0.491 0.495 latest_short 0.474 0.021* PESQ Model 正規化U統計量 p値 default 0.463 0.003* command_and_search 0.462 0.001* phone_call 0.480 0.081 latest_long 0.488 0.359 latest_short 0.458 0.000* SI-SDR Model 正規化U統計量 p値 default 0.451 0.000* command_and_search 0.417 0.000* phone_call 0.495 0.667 latest_long 0.474 0.049* latest_short 0.470 0.007* これらの表から、統計的に有意な差がありかつ、その差が大きいのは command_and_sarch のSTOI, SI-SDRあたりでしょうか。しかし、いずれの場合も差はそれほど大きくなさそうです。 このように、全データに対して分析してみた結果、それほど大きな差が見られなかったので、エンティティタイプごとの傾向を調べてみました。 エンティティタイプごとの検定結果 以下の画像は、STOI, PESQ, SI-SDRに関して、 0.5と正規化U統計量の差の絶対値 をヒートマップとしてプロットしたものです。米印(*)がついているものはp値が0.05未満のもので統計的に有意差が見られるものです。ヒートマップの 色が濃いほど エンティティ認識が正解しているグループと正解していないグループの 差が大きいこと を示しています。 この結果から、どうやらエンティティタイプごとに傾向があるようで、エンティティ番号9(「もう一度」と発話), エンティティ番号17(日付を発話)などは一部のモデルで、STOI, SI-SDRの差が大きいようです。また、 phone_call において、エンティティ番号3(名前を発話)の差が大きくなっています。これは、エンティティタイプとモデルの相性で音声品質が音声認識に与える影響の大きさが変化するからだと考えられます。 結論 今回は、音声認識の誤り傾向と音声の品質がどれくらい関係があるかを相関分析や統計的検定を用いた定量的に分析しました。 その結果、全体的には音声品質と音声認識の誤り傾向には関係がないと考えられるものの、特定のエンティティに対しては何らかの関係が見られそうだということがわかりました。 今回用いたデータセットは社内で作成した理想的な環境における比較的音声品質の良いデータでした。そのため、特筆すべき傾向が現れにくかったという可能性があります。次回は背景ノイズを付加したり、音量を調節したりして、実際の音声品質を模倣した場合にどのような傾向が見られるかというのを分析したいと思います。 投稿 音声品質と音声認識精度の関係について分析してみた(Part1) は 株式会社AI Shift に最初に表示されました。
アバター
はじめに こんにちは,東京大学大学院 修士2年の兵藤弘明です.私は2024年3月から4月にかけて,CA Tech JOBのインターンシップに参加し,株式会社AI Shift で議事録自動生成のための効果的な音声認識パイプラインの検証に取り組みました.この記事では取り組んだ内容を具体的に紹介します. タスクの背景 株式会社AI Shiftでは,オンライン会議の議事録を自動生成するシステムの技術検証を行っています.これは複数人の会話音声を入力とし,LLMを用いて会話の内容を要約し議事録のフォーマットにまとめたものを出力するシステムです. このシステムでは,入力の会話音声をテキストに書き起こしてLLMに渡します. 今回のインターンシップでは,書き起こし推定処理の現状分析と改善に取り組みました. 現状の問題点の分析 品質の高い議事録を生成するためには,書き起こしの際に以下の点を正確に推定することが重要です. 発話の認識内容 発話タイミング(各発話の順序) 入力となる会議音声の長さは数十分程度であり,また様々なノイズが含まれています.そのため,単に音声認識をかけるだけでは十分な書き起こし精度を得ることができません.これまでの検証では,発話区間 (=発話タイミング) 検出モデルを用いて発話のない音声区間を除外する処理を行っていました. しかしながら,検証データを用いて生成した議事録では,会議の決定事項の漏れや発言の前後関係の誤り、人名や組織名などの固有名詞の誤りが発生していました.こうした問題の原因を分析したところ,発話区間検出モデルにおいて,一部の発話の検出漏れやノイズの誤検知による発話タイミング誤りが発生しており,これらが議事録の品質低下の一因となっていることがわかりました. 改善手法 現状の処理により誤検知されているノイズを分析したところ,発話区間と比べて音量の小さいノイズが多く含まれていることがわかりました.そこで,音声信号の音量に対応する特徴量である エンベロープ を基準とした発話区間検出を実装しました.具体的には,発話区間検出モデルに音声を入力する前に,対数エンベロープの値が基準値以下となる区間を除外する処理を行います. これにより,ノイズの誤認識に起因する発話認識/発話順序誤りの発生頻度を低減することができ,議事録の正確性の向上が確認されました.今後の改善点としては,より多様なノイズが含まれる音声や,複数話者の音声がオーバーラップする音声に対しても頑健に動作する処理の実現が考えられます 振り返り 今回のインターンシップは,実環境のデータから生成した議事録の問題点を分析するところからスタートしました.改善が必要な点を洗い出し,様々な手法を試しながら要件に適した技術を組み込むという一通りの流れを体験したことで,企業での研究開発の様子を知ることができ,とても貴重な経験をすることができました. 約12日と短い期間でしたが,メンターの東さん( @naist_usamarer )をはじめ多くの方にサポートいただいたおかげで,様々な点で成長できたと感じています.この場を借りて改めて感謝申し上げます.本当にありがとうございました! 投稿 議事録自動生成システムにおける音声の書き起こし機構の改善 は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは、AIチームの戸田です 今回は近年Transformerの次のアーキテクチャになるのでは?と話題の状態空間モデル、Mambaを使った音声分類を試してみたいと思います。 Mamba Mambaは近年主流となっているTransformerの次のアーキテクチャとして期待されているモデルの一つです。LLMの文脈で目にすることが多いのですが、音声を扱う Audio-Mamba や、画像を扱う Vision-Mamba などの研究もされています。 Mamba自体については本記事では扱いませんので、詳細は 論文 をご参照ください。 Audio-Mamba https://arxiv.org/abs/2406.03344 Figure 1 Audio-Mamba はMambaをベースとした音声分類モデルです。現在主流となっている Audio Spectrogram Transformer と同様、Audio Spectrogramを小さな正方形(patch)に分割し、tokenとして順にモデルのEncoder部分に入力されます。Encoderは入力されたtokenから順に音の情報を読み取り、最終的にそれが何の音なのかを予測します。 論文ではAudio-Mambaは従来のAudio Spectrogram Transformerと比べて、特に長い音声シーケンスにおいて、計算効率とメモリ効率の両面で優れていると主張しています。pytorchベースで開発された学習/推論用ライブラリが Githubに公開 されていますので、今回はこちらを利用して検証を行いたいと思います。 検証 環境構築 リポジトリをCloneして README を参考に設定していくことで特に問題なく進みましたが、一点だけ詰まったところがありました。numpyのバージョンの整合性が取れなかったようで、以下のコマンドでバージョンを揃える必要がありました。 pip install -U numpy==1.26.0 私の環境が特殊だった可能性もありますが、同じように詰まった方は試してみてください。 データセット 今回検証に使うデータセットとして、以前 openSMILEを使った音声分類を行った際 に利用した ESC50 のを使います。ESC-50は環境音の分類手法のベンチマークで、5秒間の録音を50のクラスに分類する問題になっていますが、前回と同様、犬と猫の鳴き声に絞って使用したいと思います。つまり音声から、それが犬の鳴き声か猫の鳴き声か予測するタスクになります。 git clone https://github.com/karolpiczak/ESC-50.git import pandas as pd meta_df = pd.read_csv('./ESC-50/meta/esc50.csv') dog_or_cat = meta_df.query('category =="dog" or category =="cat"') ESC-50のメタデータを取得し、categoryがdogかcatのものに絞っています。このcategoryをラベルとして扱います。メタデータには他の要素もありますが、今回他に使うのは音声ファイルのパスが入っているfilenameカラムのみです。 以前のopenSMILEを使った時と同じSEEDでテストデータを分割しておきます。 from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( dog_or_cat["filename"], dog_or_cat["category"], test_size = 0.2, random_state = 0) 事前学習済みモデルでの予測 Audio-Mambaには事前学習済みモデルが何個か用意されています。ここではYouTubeから収集されたデータセットである VGGSound の事前学習済みモデルを使って犬と猫の鳴き声を分類してみます。事前学習済みモデルはREADMEにあるGoogleDriveのリンクからダウンロードできます。 examplesの推論コード を参考に音声ファイルからラベルを予測する関数を作りました。かなりコード量が多くなってしまうので折りたたみます。 予測関数(predict)の定義 import os import torch import torchaudio import numpy as np import src.models as models from src import dataloader from src.utilities.stats import calculate_stats from IPython.display import Audio, display import csv import warnings class Namespace: def __init__(self, **kwargs): self.__dict__.update(kwargs) data_args = Namespace( num_mel_bins = 128, target_length = 1024, mean = -5.0767093, std = 4.4533687, ) model_args = Namespace( model_type = 'base', n_classes = 309, imagenet_pretrain = False, imagenet_pretrain_path = None, aum_pretrain = True, aum_pretrain_path = '{GoogleDriveからダウンロードしてきたモデルのPATH}', aum_variant = 'Fo-Bi', device = 'cuda', ) AuM = models.AudioMamba( spectrogram_size=(data_args.num_mel_bins, data_args.target_length), patch_size=(16, 16), strides=(16, 16), embed_dim=768, num_classes=model_args.n_classes, imagenet_pretrain=model_args.imagenet_pretrain, imagenet_pretrain_path=model_args.imagenet_pretrain_path, aum_pretrain=model_args.aum_pretrain, aum_pretrain_path=model_args.aum_pretrain_path, bimamba_type='v1', ) AuM.to(model_args.device) AuM.eval() label_dict = {} with open("./Audio-Mamba-AuM/exps/vggsound/data/class_labels_indices.csv", 'r') as f: csv_reader = csv.DictReader(f) line_count = 0 for row in csv_reader: label_dict[row['index']] = row['display_name'] line_count += 1 def predict(audio_path): waveform, sr = torchaudio.load(audio_path) waveform = waveform - waveform.mean() fbank = torchaudio.compliance.kaldi.fbank( waveform, htk_compat=True, sample_frequency=sr, use_energy=False, window_type='hanning', num_mel_bins=data_args.num_mel_bins, dither=0.0, frame_shift=10 ) n_frames = fbank.shape[0] p = data_args.target_length - n_frames if p > 0: m = torch.nn.ZeroPad2d((0, 0, 0, p)) fbank = m(fbank) elif p < 0: fbank = fbank[0:data_args.target_length, :] label_indices = np.zeros(model_args.n_classes) label_indices = torch.FloatTensor(label_indices) fbank = (fbank - data_args.mean) / (data_args.std * 2) fbank = fbank.unsqueeze(0) label_indices = label_indices.unsqueeze(0) fbank = fbank.to(model_args.device) label_indices = label_indices.to(model_args.device) with torch.no_grad(): output = AuM(fbank) output = torch.sigmoid(output) output = output.cpu().numpy() top_idx = np.argsort(output[0])[-1] return label_dict[str(top_idx)] この予測関数を使って犬と猫の鳴き声分類を行います。VGGSoundの学習済みモデルは310クラスの音声分類で学習されていますが、直接犬と猫の鳴き声の分類ラベルがあるわけではありません。"dog growling" や "cat meowing" のような、より具体的な分類ラベルになっているので、予測したラベルに"dog" が含まれていれば犬の鳴き声、"cat" が含まれていれば猫の鳴き声と分類します。以下のコードで正解率を計算します。 results = [] for x, y in zip(X_test, y_test): pred = predict("./ESC-50/audio/" + x) results.append(y in pred) acc = np.mean(results) print(f"{acc=}") # 0.6875 正解率は 68.75% でした。 Fine-Tuning 続けて解きたいタスクでのFine-Tuningをしてみようと思います。Audio-MambaのリポジトリにはFine-Tuning用のシェルスクリプトのサンプルがいくつか用意されています。今回は aum-base_audioset-vggsound.sh をコピーしてきて以下の部分を変更して利用します。 + dataset=dog_cat - dataset=vggsound + aum_pretrain_path={GoogleDriveからダウンロードしてきたモデルのPATH} - aum_pretrain_path=/mnt/lynx2/users/mhamza/audiomamba/exp/aum-B_audioset/models/best_audio_model.pth + tr_data=./data/datafiles/dog_cat_train.json - tr_data=./data/datafiles/vgg_train.json + te_data=./data/datafiles/dog_cat_test.json - te_data=./data/datafiles/vgg_test.json + n_class=2 - n_class=309 + exp_root={結果の出力先ディレクトリPATH} - exp_root=/mnt/lynx2/users/mhamza/audiomamba + exp_name=ast-{任意の実験名} - exp_name=aum-base_audioset-vggsoun 続けてデータのフォーマットを合わせます。環境構築の際にCloneしてきたAudio-Mambaのリポジトリ内のexpsディレクトリに、対象音声ファイルのパスとそのラベルを配置します。 mkdir -p ./Audio-Mamba-AuM/exps/dog_cat/data/datafiles import json DIG_CAT_DIC = { "dog": "/m/dog_cat_0", "cat": "/m/dog_cat_1", } ROOT = "./ESC-50/audio" OUTPUT = "./Audio-Mamba-AuM/exps/dog_cat/data/" pd.DataFrame([ {"mid": DIG_CAT_DIC[k], "display_name": k} for k in DIG_CAT_DIC.keys() ]).reset_index().to_csv(f"{OUTPUT}/class_labels_indices.csv", index=None) train_lst = [{"wav" : f"{ROOT}/{x}", "labels": DIG_CAT_DIC[y]} for x, y in zip(X_train, y_train)] test_lst = [{"wav" : f"{ROOT}/{x}", "labels": DIG_CAT_DIC[y]} for x, y in zip(X_test, y_test)] with open(f"{OUTPUT}/datafiles/dog_cat_train.json", "w") as f: json.dump({"data": train_lst}, f, indent=4) with open(f"{OUTPUT}/datafiles/dog_cat_test.json", "w") as f: json.dump({"data": test_lst}, f, indent=4) これで準備は完了です。作成したシェルスクリプトを実行すると学習が開始します。私の環境(VertexAIワークベンチのデフォルト設定にT4を1枚接続したもの)では10分ほどで完了しました。正解率は 87.5% となり、事前学習済みモデルでの推論時よりも18%ほど改善しました。学習曲線(BCE loss)は以下のような形でした。 ちなみに今回はパラメータ設定などはいじっておらず、デフォルトで実行しました。以前のopenSMILEを使った分類では同様のテストデータで正解率93.75%だったので、まだまだ伸び代はあるのではないかと考えています。 おわりに 本記事ではAudio-Mambaを使った音声分類を試して見ました。 まだToy Dataでの検証レベルですが、一通り動かすことはできたので、プロダクトでの音声処理に使えるかも今後検証していきたいと思います。また、 RWKV や RetNet などTransformerの次のアーキテクチャと期待されているモデルは他にもあるのでこちらも試してみたいです。 最後までお読みいただき、ありがとうございました! 投稿 Audio-Mambaを使った音声分類 は 株式会社AI Shift に最初に表示されました。
アバター
はじめに こんにちは、AIチームの大竹です。 最近、高性能な日本語音声認識モデルのリリースが相次いでいます。普段、音声認識を用いたプロダクト開発をしている中で、各モデルの音声認識性能や推論速度がどれくらいのものなのか気になったので簡単な実験をして性能を比較してみました。 書き起こしや評価周りの実装も記載しているので参考にしていただけたら幸いです。 モデルの直近のリリースをまとめると、以下のようになっています。 ReazonSpeechコーパス のリリースを皮切りに日本語に特化した音声認識モデルの開発の勢いが加速しているように思えます。ReazonSpeechコーパスは、 地上波テレビ放送から収集された音声に基づいて構築されています。 v1では19,000時間、v2では35,000時間の音声が含まれていて、日本語音声認識モデルの学習リソースとしては世界一の規模となっています。 公開時期 モデル名 公開元 2024/4 kotoba-whisper Kotoba Technologies 2024/2 ReazonSpeech v2 Reazon Human Interaction Lab 2023/12 nue-asr rinna株式会社 2023/1 ReazonSpeech v1 Reazon Human Interaction Lab 比較対象モデル 今回の検証で比較するモデルは以下のようにしたいと思います。 モデル名 説明 kotoba-whisper 蒸留技術を用いてopenai/whisper-large-v3をもとに作成されたモデル。学習データセットは 1,253時間のReazonSpeechコーパスのサブセット。 reazonspeech-nemo NeMoのスクリプトを用いて35,000時間に拡張されたReazonSpeechコーパスでスクラッチから訓練されたモデル。アーキテクチャはRNN-T。 reazonspeech-espnet ESPNetのスクリプトを用いて35,000時間に拡張されたReazonSpeechコーパスでスクラッチから訓練されたモデル。アーキテクチャはHybrid CTC/Attention。 nue-asr 自己教師あり学習で事前学習された音声モデルとLLMを統合し、19,000時間のReazonSpeechコーパスを用いてファインチューニングしたモデル。 reazonspeech-nemoとreazonspeech-espnetは前述した表のReazonSpeech v2に該当します。 これらのモデルはいずれもReazonSpeechコーパスを用いて訓練されていますが、訓練手法およびモデルのアーキテクチャが異なるので書き起こし結果に特徴があると思います。 実験条件 今回の検証では以下の条件で実験を進めていきます。 Vertex AIで以下の環境のインスタンスを確保 CPU: n1-standard-8(8 vCPU、4 コア、30 GB メモリ) GPU: T4 1枚 CUDA: 11.8 その他の条件 バッチサイズ1で推論 コードは公開されているものをそのまま使用する 推論の際の数値のデータ型は公開されているコードおよびモデルに準拠する 環境構築 データセットのダウンロード 今回の検証では日本語の音声認識評価のために TEDxJP-10k というデータセットを用います。TEDxJP-10K は、TEDxの日本語プレゼンテーションを含むYouTubeのプレイリスト「TEDx talks in Japanese」から選ばれた音声と字幕データを利用して構築されたデータセットです。このデータセットには、合計で8.8時間の音声が含まれ、異なる性別・年齢・出身の273名の話者の声が収録されています。 どのモデルの訓練データにも含まれていないドメインの音声なので今回のような異なる複数のモデルを評価する際に有用だと思います。 公式リポジトリ にしたがってデータセットをダウンロードして整形します。想定通り処理が実行されると、 TEDxJP-10K_v1.1 というディレクトリが生成されます。 ディレクトリ直下の segments というテキストファイルを用いて、長い音声ファイルをセグメント分割します。(この処理はリポジトリの指示に記載されていないので別途実行する必要があます。) ライブラリのインストール name="python環境の名前" conda create -n $name -y python=3.10 poetry ffmpeg poetry install # pyproject.tomlを事前に用意してください pyproject.tomlのdependenciesは以下のようになっています。 [tool.poetry.dependencies] python = "3.10" torch = { url = "https://download.pytorch.org/whl/cu118/torch-2.3.0%2Bcu118-cp310-cp310-linux_x86_64.whl#sha256=ceb81d79fc7b017f51b1613f83b878efb8974cc946631fb9cd577bbaa2873293" } torchvision = { url = "https://download.pytorch.org/whl/cu118/torchvision-0.18.0%2Bcu118-cp310-cp310-linux_x86_64.whl#sha256=86b2970a75316a90e00ac66863b5414269ffd3e8c58683f1f40128da989285b6" } torchaudio = { url = "https://download.pytorch.org/whl/cu118/torchaudio-2.3.0%2Bcu118-cp310-cp310-linux_x86_64.whl#sha256=f21f08ebf87510a09f4ee3698997eeec4b8f2520e95218defbcd88e5c28d5112" } pandas = "^2.2" librosa = "^0.10.2.post1" jiwer = "^3.0.4" mecab-python3 = "^1.0.9" ruff = "^0.4.7" num2words = "^0.5" neologdn = "^0.5" 今回の検証では依存関係の解決のため、モデルごとに異なる仮想環境を作ります。 kotoba-whisper pip install --upgrade transformers accelerate reazonspeech-espnet git clone https://github.com/reazon-research/ReazonSpeech pip install ReazonSpeech/pkg/espnet-asr reazonspeech-nemo git clone https://github.com/reazon-research/ReazonSpeech pip install Cython pip install ReazonSpeech/pkg/nemo-asr nue-asr pip install git+https://github.com/rinnakk/nue-asr.git 書き起こし 今回の実験で使用するコードは基本的に各ライブラリでtutorial用に公開されているコードをそのまま使用したいと思います。データ型の設定なども公開されているものにします。 以下にReazonSpeechを用いた書き起こしコードを記載します。モデルのロードと推論の部分をモデルによって変更する必要があるので注意してください。kotoba-whisperとnue-asrの推論コードについては後述します。 import time from tqdm import tqdm wav_dir = Path('/path/to/tedxjp') def calc_duration(file_id): wav_path = wav_dir / f'{file_id}.wav' audio, sr = sf.read(wav_path) return len(audio) / sr def main(): ####################################################### # モデルごとに記述を変える部分 from reazonspeech.espnet.asr import load_model, transcribe, audio_from_path model = load_model() ####################################################### df = pd.read_csv(data_dir / 'text', sep=' ', header=None, names=['id', 'text']) df['duration'] = df['id'].apply(calc_duration) id2text = df[['id', 'text']].set_index('id').to_dict()['text'] paths2audio_files = [str(wav_dir / f'{file_id}.wav') for file_id in id2text.keys()] results = [] tic = time.perf_counter() for path in tqdm(paths2audio_files): ####################################################### # モデルごとに記述を変える部分 audio = audio_from_path(path) ret = transcribe(model, audio) results.append({ 'gt_text': id2text[Path(path).stem], 'pred_text': ret.text, }, ) ####################################################### toc = time.perf_counter() - tic pd.DataFrame(results).to_csv('results_reazon-nemo.csv', index=False) print(f'Elapsed time: {toc:.2f} seconds') print(f'RTF: {toc / df["duration"].sum():.3f}') モデルごとに記述を変える必要がある部分は以下のコードで置き換えてください。 kotoba-whisper モデルのロード model_id = "kotoba-tech/kotoba-whisper-v1.0" torch_dtype = torch.bfloat16 if device == 'cuda' else torch.float32 model_kwargs = {"attn_implementation": "sdpa"} if device == 'cuda' else {} model = pipeline( "automatic-speech-recognition", model=model_id, torch_dtype=torch_dtype, device=device, model_kwargs=model_kwargs ) generate_kwargs = {"language": "japanese", "task": "transcribe"} 推論 ret = model(path, generate_kwargs=generate_kwargs) results.append({ 'gt_text': id2text[Path(path).stem], 'pred_text': ret['text'], }, ) nue-asr モデルのロード model = nue_asr.load_model("rinna/nue-asr") tokenizer = nue_asr.load_tokenizer("rinna/nue-asr") 推論 ret = nue_asr.transcribe(model, tokenizer, path) results.append({ 'gt_text': id2text[Path(path).stem], 'pred_text': ret.text, }, ) 評価 今回の実験では、TEDxJP-10kデータセットと3つの評価指標を用いて音声認識モデルの性能を評価していきたいと思います。 評価指標 CER (Character Error Rate) :書き起こし結果の文字レベルの誤り率 WER (Word Error Rate):書き起こし結果の単語レベルの誤り率 RTF (Real Time Factor):1秒の音声を書き起こすのにどれくらいの時間がかかる実験 評価コード 今回用いた評価コードです。 書き起こしのコードを実行して得られたcsvファイルを評価コードの入力として渡します。 まず、書き起こされたテキストと正解テキストの正規化を行うためにnormalizeという関数を作用させます。normalizeでは以下の処理を行ってテキストを正規化しています。 こちら で定義されている特殊文字(句読点、!?など)を削除 UTF-16のラテン文字をASCIIに変換 数値を日本語へ変換 (1→一, 12→十二など) 正規化を行った後、jiwerというライブラリを用いて正解テキストと書き起こしテキストとの間の編集距離を計算し、WERおよびCERを算出しています。日本語の音声認識性能はCERを用いて評価することが一般的ですが、今回はmecabを用いて分かち書きをした上でWERの計算を行っています。 import pandas as pd import jiwer from argparse import ArgumentParser import MeCab from dataclasses import dataclass from num2words import num2words import re import neologdn symbols = [ "、", "。", " ", "!" ... # 省略 ] _SPECIALS = {ord(c): "" for c in symbols} def normalize(utt_txt): """Normalize text. Use for Japanese text. Args: utt_txt: String of Japanese text. Returns: utt_txt: Normalized """ # trim non-phonatory symbols in the text utt_txt = utt_txt.translate(_SPECIALS) # convert UTF-16 latin chars to ASCII utt_txt = neologdn.normalize(utt_txt) # replace all the numbers numbers = re.findall(r"\d+\.?\d*", utt_txt) transcribed_numbers = [num2words(item, lang="ja") for item in numbers] for nr in range(len(numbers)): old_nr = numbers[nr] new_nr = transcribed_numbers[nr] utt_txt = utt_txt.replace(old_nr, new_nr, 1) return utt_txt @dataclass class TotalErrors: deletions: int = 0 insertions: int = 0 substitutions: int = 0 distance: int = 0 length: int = 0 @property def error_rate(self): return self.distance / self.length def main(): parser = ArgumentParser() parser.add_argument("input", type=str, help="Path to the result csv file") args = parser.parse_args() df = pd.read_csv(args.input) mecab = MeCab.Tagger("-Owakati") total_word_errors = TotalErrors() total_char_errors = TotalErrors() for row in df.itertuples(): gt_text = normalize(row.gt_text) if pd.isna(row.pred_text): asr_text = "" else: asr_text = normalize(row.pred_text) gt_words = mecab.parse(gt_text) asr_words = mecab.parse(asr_text) word_errors = jiwer.process_words(gt_words, asr_words) total_word_errors.deletions += word_errors.deletions total_word_errors.insertions += word_errors.insertions total_word_errors.substitutions += word_errors.substitutions distance = ( word_errors.deletions + word_errors.insertions + word_errors.substitutions ) total_word_errors.distance += distance total_word_errors.length += len(gt_words.split()) char_errors = jiwer.process_characters(gt_text, asr_text) total_char_errors.deletions += char_errors.deletions total_char_errors.insertions += char_errors.insertions total_char_errors.substitutions += char_errors.substitutions distance = ( char_errors.deletions + char_errors.insertions + char_errors.substitutions ) total_char_errors.distance += distance total_char_errors.length += len(gt_text) print("----------- Word Error Rate -----------") print(f"WER: {total_word_errors.error_rate:.3f}") print("deletions:", total_word_errors.deletions) print("insertions:", total_word_errors.insertions) print("substitutions:", total_word_errors.substitutions) print("---------- Character Error Rate ----------") print(f"CER: {total_char_errors.error_rate:.3f}") print("deletions:", total_char_errors.deletions) print("insertions:", total_char_errors.insertions) print("substitutions:", total_char_errors.substitutions) 評価結果 model WER CER RTF ReazonSpeech v2 (ESPNet) 0.111 0.093 0.335 ReazonSpeech v2 (NeMo) 0.122 0.104 0.203 kotoba-whisper (transformers) 0.124 0.104 0.379 nue-asr (transformers) 0.177 0.166 0.142 日本語音声認識モデルの性能評価 (WER, CER, RTF) CERやWERでは、ESPNet→NeMo≒kotoba-whisper→nue-asrという順に、 推論速度(RTF)ではnue-asr→NeMo→ESPNet→kotoba-whisperという順に性能が良い結果となりました。 次に、いくつかのサンプルをピックアップして各モデルの書き起こし結果を比較したいと思います。 Ground Truth kotoba-whisper (transformers) nue-asr (transofrmers) ReazonSpeech v2 (ESPNet) RezonSpeech v2 (NeMo) いいんじゃないかしらと思ったんです いいんじゃないかしらと思ったんです 思ったんです。 いいんじゃないかしらと思ったんです。 いいんじゃないかしらと思ったんです。 それがまあ応用が出てきて それが応用が出てきて それが応用が出てきて。 それがまあ応用が出てきて。 それがまあ応用が出てきて。 え福島ってこんなふうなの 福島ってこんなふうなの? 福島ってこんなふうなの? えっ福島ってこんなふうなの? えっ福島ってこんなふうなの? 各モデルの書き起こしの比較 nue-asrは書き起こし結果が正解テキストと比べて短くなる傾向があることがわかります。フィラーや感嘆符の削除だけでなく、発話の一部が無視されてしまうような挙動があります。kotoba-whisperはフィラーや感嘆符を書き起こさず、文として簡潔に書き起こせています。対して、ReazonSpeechのモデルは音に忠実にフィラーや感嘆符を書き起こしている傾向があります。 このようにモデルごとに書き起こしに特徴があるのは面白いですね。 また、今回の検証では、 faster-whisper というwhisperの高速推論を可能にするライブラリを用いることでkotoba-whisperがどの程度高速化されるのかについても試してみました。ベースラインとしてオリジナルのOpenAI whisperに対する性能も測定していいます。 model WER CER RTF kotoba-whisper (transformers) 0.124 0.104 0.379 kotoba-whisper (faster-whisper, fp16) 0.153 0.133 0.087 whisper-large-v3 (faster-whisper, fp16) 0.152 0.136 0.228 faster-whisperによる高速化に関する実験 faster-whisperを用いることで、速度を約4~5倍に向上させることが可能です。オリジナルのモデルより性能が低下してしまうものの、whisper-large-v3と比較して性能に大きな差がないのでかなり有用なのではないでしょうか。 終わりに この記事では、オープンソースの日本語End-to-End音声認識モデルの性能を評価しました。 性能を比較したところ、どのモデルも音声認識性能が高く、CERやWERだけではその性能を十分に評価することが難しくなってるなと感じます。そのため、定性的な分析を行い、実際の使用シナリオに適した書き起こしをしてくれるモデルを選択することが重要だと改めて思いました。 今後も引き続き、日本語音声認識技術の発展に注目し、プロダクトへの適用可能性を探っていきたいと思います。 参考 https://github.com/reazon-research/ReazonSpeech https://github.com/sarulab-speech/jtubespeech/blob/master/scripts/align.py#L63 https://laboro.ai/wp-content/uploads/2020/12/IPSJ-SLP20134008.pdf 投稿 kotoba-whisper, ReazonSpeech, Nue-ASRの文字起こし性能を比較してみた は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは AIチームの戸田です 今回はLLMを軽量化するPruningを行うライブラリ、SliceGPTを使って日本語LLMのPruningを試してみたいと思います。 SliceGPT LLMに限らず、大規模なニューラルネットワークのパラメータを圧縮する手法の一つにPruningがあります。日本語では「枝刈り」や「剪定」とも訳されており、重要ではないと思われるノード間の重みを削除することでパラメータ数を削減する手法です。 こちらの論文 などが有名です。 Pruningのイメージ PruningはLLMの圧縮手法として有効な手段なのですが、性能を維持するためにPruning後にRecovery fine-tuningという学習を行わなければならず、これがコストになるという課題がありました。SliceGPTはこのRecovery fine-tuning無しで90%以上の性能を維持したまま、最大25%のパラメータ削減が可能であると言われています。詳細は 元論文 をご参照ください。 SliceGPT: Compress Large Language Models by Deleting Rows and Columns Figire 1 SliceGPTは こちらのリポジトリ から利用可能です。本記事では cyberagent/calm2-7b を使ってSliceGPTによるPruningを試してみようと思います。 Pruning ライブラリ自体はRADMEに従ってインストールできます。 git clone https://github.com/microsoft/TransformerCompression.git cd TransformerCompression pip install -e .[experiment] SliceGPTはLlama系のモデルをサポートしているのですが、Meta社が出しているオリジナルのLlama以外を扱うには少しコードの修正が必要です。 こちらのissue を参考に、adapters/llama_adapter.pyの227,228,247,248をコメントアウトします。 加えて、今回は日本語LLMの学習を行うので、Purningの重みを計算するためのデータに c4 の日本語データの一部を利用します。若干強引な手段になってしまいますが、学習コード(experiments/run_slicegpt.py)の162, 163行目を以下の関数に置換してください。 import pandas as pd from datasets import load_dataset from datasets import Dataset def load_c4j(n_load_data=1000, valid_ratio=0.2) examples = [] streaming_dataset = load_dataset('allenai/c4', 'ja', streaming=True) for example in streaming_dataset['train']: examples.append(example) if len(examples) == n_load_data: break n_valid = int(n_load_data * valid_ratio) test_dataset = Dataset.from_pandas(pd.DataFrame(examples[:n_valid])[["text"]]) train_dataset = Dataset.from_pandas(pd.DataFrame(examples[n_valid:])[["text"]]) return train_dataset, test_dataset これで一通りの準備は整いました。Pruningを開始するにはREADMEにある通り、以下のexperiments直下のrun_slicegpt.pyを実行します。 cd experiments mkdir slice_calm2 python run_slicegpt.py \ --model "cyberagent/calm2-7b" \ --save-dir ./slice_calm2 \ --sparsity 0.25 \ --device cuda \ --no-wandb \ --eval-baseline \ --hf-token {HuggingFaceのToken} 実行すると以下のような出力が得られました。 最終的には約15%のパラメータが削減できたようです。気をつけたいのは、論文のTable 1を参照するとわかるのですが、sparsityに設定した値がそのまま圧縮率になるわけではないということです。ここはベースとなるモデルのパラメータ数やPruningの計算に利用するデータによって変わってくるようです。 評価 Pruningされたモデルが性能を保持できているか、 JGLUE のJSQuADで評価します。JSQuADは日本語の質問応答タスクで、コンテキストとそれに関する質問が与えられ、コンテキスト内の情報を使って質問に答えられるか、という問題になります。例えば以下のような問題です。 コンテキスト: 太郎さんは毎朝7時に起きて、8時に家を出ます。 質問: 太郎さんは何時に家を出ますか? 期待する回答: 8時 評価のためのコードを以下に示します。 import transformers from transformers import AutoModelForCausalLM, AutoTokenizer from datasets import load_dataset from tqdm.auto import tqdm from slicegpt import gpu_utils, hf_utils, utils IS_BASE_EVAL = False # ベースとなるモデルを評価する場合はTrue ja_squad = load_dataset('shunk031/JGLUE', 'JSQuAD') if IS_BASE_EVAL: model = AutoModelForCausalLM.from_pretrained("cyberagent/calm2-7b", device_map="auto", torch_dtype="auto") tokenizer = AutoTokenizer.from_pretrained("cyberagent/calm2-7b") print("load base model") else: model_adapter, tokenizer = hf_utils.load_sliced_model( "cyberagent/calm2-7b", "./slice_calm2", sparsity=0.25 ) model = model_adapter.model.to("cuda") print("load sliced model") results = [] for d in tqdm(ja_squad["validation"]): prompt = ( f"[題名]:{d['title']}\n" f"[問題]:{d['context']}\n" f"[質問]:{d['question']}\n" f"[答え]:" ) token_ids = tokenizer.encode(prompt, return_tensors="pt") output_ids = model.generate( input_ids=token_ids.to("cuda"), max_new_tokens=20, do_sample=True, temperature=0.3, ) resp = tokenizer.decode(output_ids.cpu()[0, len(token_ids[0]):]) results.append(sum([i in resp for i in d['answers']['text']]) > 0) print("Accuracy:", sum(results) / len(results)) Promptは lm-evaluation-harness-jp-stable のものを参考にさせていただきました。また、出力の文章内に回答が含まれていたら正解、というかなり荒い評価になってしまっていますがあくまで比較のためということでご容赦ください。 ベースモデルのスコアが 0.7406 だったのに対して、Pruningモデルが 0.6769 で、確かに論文での主張の通り約90%の性能を保持できていることがわかりました。 おわりに 本記事ではSliceGPTを使ってCALM2-7bの日本語LLMのPruningを試してみました。 Recovery fine-tuning不要がメリットであるSliceGPTですが、Recovery fine-tuningを行うことで、Pruning後の性能をより向上させることができると言われており、そのための機能もライブラリにあるので、また試してみたいです。 最後までお読みいただきありがとうございました! 投稿 SliceGPTを使って日本語LLMをPruningしてみる は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは AIチームの戸田です 今回は先月スタンフォード大学が発表した新しいParameter-efficient fine-tuning(PEFT)のReFTを試してみたいと思います。 PEFT PEFTはLLMのような大規模な事前学習済みのニューラルネットワークのモデルを、効率的にfine-tuningする手法の総称です。モデル全体ではなく一部のパラメータだけを更新することで計算コストを大幅に削減できる上に、Full fine-tuning(モデル全体を学習)するのと同等の性能を達成することができると言われています。代表的なものに Low-Rank Adaptation(LoRA) が挙げられます。 LoRA: Low-Rank Adaptation of Large Language Models Figure 1 ReFT Representation Finetuning (ReFT)は、LoRAとよく似たPEFT手法です。違いは、LoRAがモデルの重みを部分的に更新するのに対し、ReFTはモデルの中間層の出力に介入する点です。LoRAと比べて非常に少ないパラメータの変更でモデルの挙動を制御でき、その効率はLoRAよりも10〜50倍向上すると言われています。元論文は こちら になります。 ReFT: Representation Finetuning for Language Models Figure 2. (1) ReFTを使うためのpythonライブラリ、 pyreft がGitHubで公開されているので、本記事ではこちらを試してみようと思います。 なお、本記事はReFTの学習を 一通り動かすことを目的 としており、パラメータ調整やデータクレンジングなどのより良い学習を行うための工夫は行いません。 使ってみる 以下でインストールできます pip install git+https://github.com/stanfordnlp/pyreft.git データセット 学習データは お嬢様コーパス を利用させていただきます。以下でダウンロードできます。 git clone https://github.com/matsuvr/OjousamaTalkScriptDataset.git pandasでダウンロードしたデータを読み込み、学習用に20件サンプリングします。 import pandas as pd train_df = pd.read_csv("./OjousamaTalkScriptDataset/ojousamatalkscript200.csv") sample_df = train_df.sample(20) モデルの読み込み 学習するモデルは小規模ながら高性能と期待されている MicrosoftのPhi-3 を使います。 import torch, transformers, pyreft device = "cuda" model_name_or_path = "microsoft/Phi-3-mini-4k-instruct" prompt_no_input_template = """<|user|>\n%s<|end|>\n<|assistant|>\n""" model = transformers.AutoModelForCausalLM.from_pretrained( model_name_or_path, torch_dtype=torch.bfloat16, device_map=device) tokenizer = transformers.AutoTokenizer.from_pretrained( model_name_or_path, model_max_length=2048, padding_side="right", use_fast=False) tokenizer.pad_token = tokenizer.unk_token 続けてReFTの学習の設定を行います。Phi-3のReftConfigの設定は こちらのissue を参考にしました。 reft_config = pyreft.ReftConfig( representations={ "component":"model.layers[15].output", "low_rank_dimension":4, "intervention":pyreft.LoreftIntervention( embed_dim=model.config.hidden_size, low_rank_dimension=4 ) } ) reft_model = pyreft.get_reft_model(model, reft_config) reft_model.set_device("cuda") reft_model.print_trainable_parameters() # trainable intervention params: 24,580 || trainable model params: 0 # model params: 3,821,079,552 || trainable%: 0.0006432737048652789 学習 以下のコードで学習を行います。パラメータはライブラリのREADMEを参考に設定しました training_args = transformers.TrainingArguments( num_train_epochs=100.0, output_dir="./my_phi3", per_device_train_batch_size=10, learning_rate=4e-3, logging_steps=40, report_to=[]) trainer = pyreft.ReftTrainerForCausalLM( model=reft_model, tokenizer=tokenizer, args=training_args, **data_module) _ = trainer.train() T4一枚で15分ほどで学習が完了しました。 予測 学習データにない文章を入力して、学習によってモデルの出力がどのように変わったか見てみましょう。 eval_df = train_df.iloc[[i for i in train_df.index if i not in sample_df.index]].sample(5) results = [] for _, row in eval_df.iterrows(): prompt = prompt_no_input_template % row["prompt"] prompt = tokenizer(prompt, return_tensors="pt").to(device) base_unit_location = prompt["input_ids"].shape[-1] - 1 # last position _, reft_response = reft_model.generate( prompt, unit_locations={"sources->base": (None, [[[base_unit_location]]])}, intervene_on_prompt=True, max_new_tokens=512, do_sample=True, eos_token_id=tokenizer.eos_token_id, early_stopping=True ) results.append({ "output": tokenizer.decode(reft_response[0], skip_special_tokens=True), "completion": row["completion"] }) こちらの結果とオリジナルの出力、そして学習していないデフォルトのPhi-3の出力を以下の表にまとめました 入力 オリジナルの出力 デフォルトのPhi-3の出力 学習したPhi-3の出力 次こそは上手くいきます そろそろ、いい加減にしていただけますか? わたくし、もう疲れましたわ この指示は、日本語で「それはとても上手く行けます」という意味を持つフレーズを簡潔に表現しています。日本語で同じ意味を持つフレーズとしては「それはとても上手くいきます」という表現が適切です。 わたくしがあまり上手ではありませんわ! もうダメかも 自分に自信を持って、頑張りましょう!きっと、上手くいきますわ I'm here to help! If you're feeling frustrated or if there's something specific you'd like assistance with, please let me know how I can support you. わたくしは、どんな状況でも前向きに考えて、価値を見いだしているわ! 人生が上手くいきません 人生は、自分次第でどうにでもなるものですわ。今日から、変えていくことができますわ 人生が上手くいかないと感じることは、普遍的な感情であり、個人の経験によって異なります。この感情を受け入れ、自己成長を目指すことが重要です。 α = Aging well with medication 薬をたくんだ ok このカフェ素敵ですね はい私もこちらのお店が大好きですわ。落ち着いた雰囲気がいいですわね この指示は、カフェに対する肯定的な感想を表現することを要求しています。以下に適切な日本語の回答を示します。 「本当に素敵なカフェですね。お弁当を食べながら、この雰囲気の中でリラックスしているのはいつものようです。」 この回答は、カフェの雰囲気や体験を評価し、肯定的な感想を伝えています。 ご存知のleecherですわ 好きな服のブランドはなんですか スタイリストさんにお任せなのでよく存じておりませんの。わたくしに似合うものを選んでくださっていますわ 私の好きな服のブランドについては、個人的な好みに基づいて答えることはできませんが、一般的に人々が好むブランドにはいくつかあります。例えば、ファッション業界で高い評価を受けているブランドには、ZARA、ミュラー、ナイキ、アディダスなどがあります。これらのブランドは、高品質な素材の使用、革新的なデザイン、そして個々のファッションスタイルに合わせた商品を提供しています。 わたしのせいですわ 学習データが少ないせいか、対話としては崩壊してしまっているのですが、お嬢様の発話スタイルを学習できているように見えます。全データで学習してみたらまた違うのかもしれません。通常のLoRAも試してみたいですね。 おわりに 本記事では高効率と期待されているPEFTのReFTをPhi-3の学習で試してみました。pyreftは現在LoRAでのFine-Tuningで多く活用されているHuggingFaceのPEFTライブラリの PEFT (名前がややこしいですね)と近いAPIでかなり使いやすかったです。 モデルやパラメータによって違うので一概に比較はできませんが、一般的なLoRAの学習時の学習パラメータ削減率(trainable%)が0.01〜0.1程度なのに対し、ReFTは0.001未満で、非常に高効率であることが感じられました。一方で学習自体は、うまくできていそうな雰囲気はあるものの実用に足るような結果ではなく、ここはこれまでのLoRAのようなPEFTと変わらないので、まだまだ試行錯誤していく必要がありそうです。 最後までお読みいただき、ありがとうございました! 投稿 高効率のLLM学習手法ReFTを試してみる は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは。 AIチームの干飯( @hosimesi11_ )です。今回はAI Shiftで取り組んでいる新規サービスであるAI Messenger Summaryの機械学習パイプラインと、Proof of Concept(PoC)から実際のプロダクトへと展開する過程についてご紹介します。 AI Messenger Summaryとは AI Messenger Summary はコールセンター事業における、会話内容の要約サービスになります( プレスリリース )。コールセンターでは、 顧客との会話内容をまとめるアフターコールワーク に多くの時間が割かれるという課題があります。この状況を解決するため、オペレーターと顧客の会話データや録音データを音声認識し、大規模言語モデル(LLM)で処理して要約し、導入企業に合わせたフォーマットで出力するようなサービスとなっています。要約の他、応対品質評価やラベリングなど複数のタスクに対応しています。 初期の課題感 AI Messenger Summaryでは、各企業ごとの要約が実現可能かどうかを判別するために、PoC(Proof of Concept)フェーズを設けていました。 初期では Gradio を使った一つのサーバ内で、音声認識や要約処理など全ての処理を同期的に行っていました。処理の例として以下のようなものがあります。 音声ファイルアップロード 前処理 音声認識 後処理 要約 初期のPoC用のGradioの画面 元々データサイエンティスト一人で環境を整備しており、PoC初期では十分な機能を果たせていましたが、導入企業が増えてくるにつれ徐々に課題感が出てきました。音声認識のパラメータの変更や、前処理の変更、複数のプロンプトの試行など個社ごとに試行錯誤する必要があります。また処理の特性上、一ファイルの一回の実行に数十分から数時間かかる場合もあります。PoCを行う企業が増えるにつれ、PoCの負担はさらに増すことが予想されました。 PoCの負担を下げつつ、素早くMVP(Minimum Viable Product)としてリリースできるようにするため、開発フェーズを以下の2つに定義し、段階的に達成するように進めることにしました。 Phase 1. PoCの負担削減と高速化 Phase 2. 実プロダクト実装   プロジェクトの進め方 Phase 1 Phase 1では、PoCの負担削減と高速化にだけ観点を置き、リッチな構成は取らないようにすることで実装コストを大幅に下げました。具体的には、要約処理などの画面に関係のない処理の中心をCloud Run Jobsに載せて処理を分離しました。これによって下記のことが可能になりました。 複数音声ファイルの処理を並列で同時実行 処理を分離し、画面から非同期で実行 slackへの失敗通知 処理のリトライや細かい部分での並列実行にworkflowツールを導入したかったのですが、スピード感とPhase 2の実プロダクト実装を踏まえて、スコープ外としました。また今のUIであるGradioはリプレイスされることが予期されたため、手をつけずにジョブの分離に注力しました。 元のシステム構成図 Phase 1システム構成図 PoCの高速化をしつつ、この段階から実プロダクトの設計も開始しました。こうすることで、大部分の実装を本番時にそのまま再利用でき、実プロダクトのリリースまでの時間を短縮することができました。 Phase 2 Phase 2は実プロダクトの実装になります。実プロダクトでは各企業のCTI(Computer Telephony Integration)やCRM(Customer Relationship Management)と自動連携することになります。さらにPoCの処理も全てこのシステムに載ってくることが予期されました。本番運用とPoCの処理の共通化を考慮するとリトライ処理なども完備したかったため、ワークフローツールを導入しました。インフラ基盤とセットでワークフローツールをいくつか検討しました。 インフラ基盤とワークフローツールの選定 ここでは以下のような方針を採用することを避けました。 流行っているという理由での技術選定 必要以上にリッチな構成 ML独自のツールの採用 これらの方針を避ける理由は次の通りです。 プロダクト立ち上げなので シンプルで管理しやすいシステム を目指したい ML独自のツールでは運用もMLエンジニアが全て見る必要があり、 ソフトウェアエンジニアと共同して運用できない そのため、以下を選定の基準としました。 達成したい目的を整理し、それを実現できる点 ソフトウェアエンジニアと共同で運用できる点 AI ShiftではクラウドサービスとしてGoogle Cloudを採用しており、ほとんどのアプリケーションがGKE上で動作しています。インフラ基盤の選定に関して、元々は素早く立ち上げるためにCloud Run Jobsを使用する予定でしたが、ワークフローツールとの兼ね合いや音声認識などをGPU上で動かしたい場面が想像できたため、既に動いているGKEを選定しました。他のものに比べて初期実装コストが少し高いですが、チームとしての 運用の知見 や アセット がある点も含めて選定しました。 ワークフローツールの選定に関しては社内の別プロダクトの方にもヒアリングしつつ、柔軟性や既存システムとの相性、運用ノウハウの点からArgo Workflowsを選定しました。AI ShiftがArgo CDをCDツールとして使用していることも、ソフトウェアエンジニアと一緒に運用する上で大きな利点となりました。 上記を含めて、Phase 2(プロダクト)のインフラ構成は以下になります。 Phase 2構成図 機械学習パイプラインの構築 AI Shiftの本番環境のGKEの管理には Kustomize が使用されているため、検証が終わり次第デプロイするまでの差分を減らすため、ローカルでもKustomizeを使用しました。本番ではCTIなどと自動連携し、PubSubへのパブリッシュをトリガーにワークフローを動かしたかったため、Argo Eventsも使用しています。 Argo Workflowsとは Kubernetes 上で並列ジョブを調整するためのオープンソースのワークフローツールです。柔軟なDAGを組むことができます。 Argo Eventsとは Kubernetes 用のイベント駆動型ワークフロー実行ツールで、Webhook・MQなど様々なツールに対応しています。Sensor、EventSource、EventBusの3つのコンポーネントからなります。 イメージ図 Google Cloudリソースの作成 まずは、以下のようにTerraformで必要なGoogle Cloudのリソースを作成しています。 Argo Workflows # External IP for Argo Workflows resource "google_compute_global_address" "argo_workflows_external_ip" { name = "argo-workflows-server-ip" address_type = "EXTERNAL" ip_version = "IPV4" project = var.project } # DNS Zone data "google_dns_managed_zone" "dns_zone" { name = "dns-zone" project = var.project } # DNS Record for Argo Workflows resource "google_dns_record_set" "argo_workflows" { project = var.dns_management_project name = "sample.${data.google_dns_managed_zone.dns_zone.dns_name}" type = "A" ttl = 300 managed_zone = data.google_dns_managed_zone.dns_zone.name rrdatas = [google_compute_global_address.argo_workflows_external_ip.address] lifecycle { ignore_changes = [rrdatas] } } # Access Policy for Argo Workflows resource "google_compute_security_policy" "argo_workflows_policy" { name = "${var.env}-argo-workflows-policy" rule { action = "deny(403)" priority = "2147483647" match { versioned_expr = "SRC_IPS_V1" config { src_ip_ranges = ["*"] } } description = "Default" } rule { action = "allow" priority = "1001" match { versioned_expr = "SRC_IPS_V1" config { src_ip_ranges = [ "xxx" ] } } description = "Allow IP Ranges" } } # History DB for Argo Workflows resource "google_sql_database" "argo_workflows_db" { name = "$argo_workflows_db" instance = var.db_instance_name charset = "utf8mb4" collation = "utf8mb4_general_ci" project = var.project } # Service Account for Argo Workflows resource "google_service_account" "argo_workflows_sa" { account_id = "argo-workflows-sa" display_name = "argo workflows service account" } # Attach secret manager accessor to Service Account for Argo Workflows resource "google_project_iam_member" "workflows_secret_manager_iam" { project = var.project role = "roles/secretmanager.secretAccessor" member = "serviceAccount:${google_service_account.argo_workflows_sa.email}" } # Attach pub/sub publisher to Service Account for Argo Workflows resource "google_project_iam_member" "workflows_pubsub_publisher_iam" { project = var.project role = "roles/pubsub.publisher" member = "serviceAccount:${google_service_account.argo_workflows_sa.email}" } # Attach storage object admin to Service Account for Argo Workflows resource "google_project_iam_member" "workflows_storage_iam" { project = var.project role = "roles/storage.objectAdmin" member = "serviceAccount:${google_service_account.argo_workflows_sa.email}" } # Attach artifact registry writer to Service Account for Argo Workflows resource "google_project_iam_member" "workflows_artifact_registry_iam" { project = var.project role = "roles/artifactregistry.writer" member = "serviceAccount:${google_service_account.argo_workflows_sa.email}" } # Attach service account token creator to Service Account for Argo Workflows resource "google_project_iam_member" "workflows_service_account_token_creator_iam" { project = var.project role = "roles/iam.serviceAccountTokenCreator" member = "serviceAccount:${google_service_account.argo_workflows_sa.email}" } # Attach cloud sql client to Service Account for Argo Workflows resource "google_project_iam_member" "workflows_cloudsql_client_iam" { project = var.project role = "roles/cloudsql.client" member = "serviceAccount:${google_service_account.argo_workflows_sa.email}" } # Attach Workload Identity to Service Account for Argo Workflows resource "google_service_account_iam_binding" "argo_workflows_workload_identity" { service_account_id = google_service_account.argo_workflows_sa.name role = "roles/iam.workloadIdentityUser" members = ["serviceAccount:${var.project}.svc.id.goog[argo-workflows/argo-workflows-sa]"] } # Bucket for Argo Workflows Log resource "google_storage_bucket" "argo_workflows_logs_bucket" { name = "argo-workflows-logs" location = var.region project = var.project force_destroy = true } Argo Events # Argo Eventsのservice account resource "google_service_account" "argo_events_sa" { account_id = "argo-events-sa" display_name = "用のargo events用のservice account" } # Attach pub/sub subscriber to Service Account for Argo Events resource "google_project_iam_member" "pubsub_subscriber_iam" { project = var.project role = "roles/pubsub.subscriber" member = "serviceAccount:${google_service_account.argo_events_sa.email}" } # Attach pub/sub viewer to Service Account for Argo Events resource "google_project_iam_member" "pubsub_viewer_iam" { project = var.project role = "roles/pubsub.viewer" member = "serviceAccount:${google_service_account.argo_events_sa.email}" } # Attach service account token creator to Service Account for Argo Events resource "google_project_iam_member" "workflows_service_account_token_creator_iam" { project = var.project role = "roles/iam.serviceAccountTokenCreator" member = "serviceAccount:${google_service_account.argo_events_sa.email}" } # Attach Workload Identity to Service Account for Argo Events resource "google_service_account_iam_binding" "argo_events_workload_identity" { service_account_id = google_service_account.argo_events_sa.id role = "roles/iam.workloadIdentityUser" members = ["serviceAccount:${var.project}.svc.id.goog[argo-events/argo-events-sa]"] }   Argo Workflowsのインストール Argo WorkflowsをGKEに構築していきます。(一部抜粋) まず、以下のページからinstall.yamlをダウンロードし、kustomizeの配下におきます。 https://github.com/argoproj/argo-events/releases ConfigMapを作成します。ここでは、ワークフローの履歴をMySQLに入れるための設定をしています。また、GCSにもログが上がるように設定しています。 ConfigMap apiVersion: v1 kind: ConfigMap metadata: name: workflow-controller-configmap namespace: argo-workflows data: parallelism: "10" namespace: argo-workflows resourceRateLimit: | limit: 10 burst: 1 persistence: | archive: true archiveTTL: 10d skipMigration: false connectionPool: maxIdleConns: 100 maxOpenConns: 100 nodeStatusOffLoad: true mysql: host: xxxxx port: 3306 database: xxxx tableName: argo_workflows userNameSecret: name: argo-workflows-secret key: MYSQL_USER passwordSecret: name: argo-workflows-secret key: MYSQL_PASSWORD artifactRepository: | archiveLogs: true gcs: bucket: sample-log-bucket keyFormat: "argo\ /{{workflow.creationTimestamp.Y}}\ /{{workflow.creationTimestamp.m}}\ /{{workflow.creationTimestamp.d}}\ /{{workflow.name}}\ /{{pod.name}}" Service Account ここで、Google Cloud側で作成済みのサービスアカウントとKubernetesのサービスアカウントを紐付けます。 apiVersion: v1 kind: ServiceAccount metadata: name: argo-workflows-sa namespace: argo-workflows annotations: iam.gke.io/gcp-service-account: sample-gcp-service-account Deployment Deploymentの定義をします。ここでは--auth-modeはclient、server、ssoが対応しています。 apiVersion: apps/v1 kind: Deployment metadata: name: argo-server namespace: argo-workflows spec: strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: argo-server template: metadata: labels: app: argo-server spec: serviceAccountName: argo-workflows-sa terminationGracePeriodSeconds: 60 nodeSelector: cloud.google.com/gke-nodepool: sample-node-pool containers: - name: argo-server args: ["server", "--secure=false", "--auth-mode=sso"] ports: - containerPort: 2746 livenessProbe: httpGet: path: / port: 2746 scheme: HTTP initialDelaySeconds: 3 periodSeconds: 30 timeoutSeconds: 1 successThreshold: 1 failureThreshold: 3 readinessProbe: httpGet: path: / port: 2746 scheme: HTTP initialDelaySeconds: 3 periodSeconds: 30 timeoutSeconds: 1 successThreshold: 1 failureThreshold: 3 lifecycle: preStop: exec: command: - /bin/sh - -c - sleep 10 Service apiVersion: v1 kind: Service metadata: name: argo-server namespace: argo-workflows annotations: cloud.google.com/neg: '{"ingress":true}' spec: type: ClusterIP selector: app: argo-server ports: - name: argo-workflows-ui port: 80 targetPort: 2746 protocol: TCP Ingress Ingressを作成します。ここでは先ほどTerraformで作成済みのExternal IPや、DNSレコードを設定します。 apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: argo-server-ingress namespace: argo-workflows annotations: kubernetes.io/ingress.global-static-ip-name: sample-ip-name networking.gke.io/managed-certificates: sample-certificates spec: defaultBackend: service: name: argo-server port: number: 80 rules: - host: sample-domain http: paths: - path: / pathType: Prefix backend: service: name: argo-server port: number: 80 これらをapplyすることで、Argo Serverが起動し、Argo UIが表示されます。 Argo Events Argo EventsをGKEに構築していきます。(一部抜粋) まず、以下のページからinstall.yamlをダウンロードし、kustomizeの配下におきます。 https://github.com/argoproj/argo-events/releases EventBus EventBusを定義します。 apiVersion: argoproj.io/v1alpha1 kind: EventBus metadata: name: argo-events-bus namespace: argo-events spec: jetstream: version: 2.9.12 replicas: 3 nodeSelector: cloud.google.com/gke-nodepool: sample-node-pool streamConfig: | maxAge: 24h settings: | max_file_store: 1GB startArgs: - "-D" EventSource EventSourceを作成します。ここでは先ほどTerraformで作成した、ワークフローをキックする際に使用するPub/Subを指定します。 apiVersion: argoproj.io/v1alpha1 kind: EventSource metadata: name: gcp-pubsub namespace: argo-events spec: eventBusName: argo-events-bus type: pubsub template: serviceAccountName: argo-events-sa spec: nodeSelector: cloud.google.com/gke-nodepool: sample-node-pool pubSub: workflow-trigger: jsonBody: true projectID: sample-project topic: sample-topic subscriptionID: sample-topic-sub Sensor どのワークフローをキックするかを定義します。ここで、dependenciesはEventSourceと名前を合わせる必要があります。 apiVersion: argoproj.io/v1alpha1 kind: Sensor metadata: name: pubsub-sensor namespace: argo-events spec: template: serviceAccountName: argo-events-sa eventBusName: argo-events-bus dependencies: - name: sample-trigger-dependency eventSourceName: gcp-pubsub eventName: workflow-trigger triggers: - template: name: trigger-template k8s: operation: create source: resource: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: generateName: workflow- namespace: argo-workflows spec: serviceAccountName: argo-workflows-sa workflowTemplateRef: name: sample-workflow arguments: parameters: - name: config valueFrom: path: /event parameters: - src: dependencyName: sample-trigger-dependency dataKey: body dest: spec.arguments.parameters.0.value これらをapplyすることで、Argo Events用のpodが作成されます。   Workflowの本体のデプロイ Argo Events経由でワークフローをキックしたいため、今回はWorkflowTemplateを用いて機械学習のワークフローを定義します。 大まかな処理は以下のようになります。 前処理 音声認識 後処理 要約 結果格納 はじめにentrypoint-jobを定義し、inputやファイルによってワークフローを切り替えるために判定用のjobを一つ入れています。 apiVersion: argoproj.io/v1alpha1 kind: WorkflowTemplate metadata: name: sample-workflow namespace: argo-workflows spec: templates: - name: main-job nodeSelector: cloud.google.com/gke-nodepool: sample-node-pool inputs: parameters: - name: config steps: - - name: entrypoint-job templateRef: name: entrypoint-job template: entrypoint arguments: parameters: - name: config value: "{{inputs.parameters.config}}" - - name: main-job templateRef: name: main-job template: main-audio - name: exit-handler-job steps: - - name: slack-notification templateRef: name: slack-notification-job template: slack-notification when: "{{workflow.status}} != Succeeded" arguments: parameters: - name: title value: "{{workflow.name}}が失敗しました" : 実際の処理やロジックは割愛しますが、高速化のため複数ステップで並列化させています。また、処理の内容によってはGPUを使用したいため、nodeSelectorによってインスタンスを分けています。 構築したワークフロー 今回構築したワークフローの全体像は以下のようになっています。音声を扱うのでMLロジックが盛りだくさんですが、柔軟なワークフローを組むことができます。 ワークフローの実行画面 おわりに 本記事では新規プロダクトであるAI Messenger Summaryのプロジェクトの進め方と、機械学習パイプラインについて紹介しました。AI Messenger Summaryでは機械学習パイプラインにArgo Workflowsを導入しました。すでにKubernetesを運用している方にとっては選択肢になりうると思います。 最後までお読みいただきありがとうございました! 投稿 Argo WorkflowsとGKEで構築するLLMを使った要約サービスの機械学習パイプライン は 株式会社AI Shift に最初に表示されました。
アバター
DALLE-3で生成 こんにちは AIチームの戸田です 今回は先日HuggingFaceのライブラリとして追加された Parler-TTS を使った音声合成を試してみたいと思います。 Parler-TTS Parler-TTS は、話者のスタイル (性別、ピッチ、話し方など) をテキストで指定して音声合成(TTS) を行える軽量なモデルを提供してくれるライブラリです。オープンソースで、推論だけでなく、学習や学習のための前処理コードも提供されています。 手法は こちらの論文 で提案されたもので、45,000時間のデータセットに対してアノテーションを行うことで様々なアクセントや発声スタイルでの高忠実度の音声生成を実現しています。以前まで話者のスタイルを指定する際は参照音声が必要だったのですが、これによりテキストによる指示のみでスタイルの指示を行うことができるようになりました。 Parler-TTSは MusicGen をベースとした軽量版モデル( parler_tts_mini_v0.1 )を提供しており、今回はこちらのモデルを試してみようと思います。 検証 Google Colaboratoryで試したいと思います。まずはParler-TTSをインストールします。 pip install git+https://github.com/huggingface/parler-tts.git 次に必要なライブラリを読み込み、モデルをロードします。 from parler_tts import ParlerTTSForConditionalGeneration from transformers import AutoTokenizer import soundfile as sf import torch device = "cuda:0" if torch.cuda.is_available() else "cpu" model = ParlerTTSForConditionalGeneration.from_pretrained("parler-tts/parler_tts_mini_v0.1").to(device) tokenizer = AutoTokenizer.from_pretrained("parler-tts/parler_tts_mini_v0.1") これで準備は整いました。まずはライブラリのREADMEにあるサンプルを試してみましょう。 prompt = "Hey, how are you doing today?" description = "A female speaker with a slightly low-pitched voice delivers her words quite expressively, in a very confined sounding environment with clear audio quality. She speaks very fast." input_ids = tokenizer(description, return_tensors="pt").input_ids.to(device) prompt_input_ids = tokenizer(prompt, return_tensors="pt").input_ids.to(device) generation = model.generate(input_ids=input_ids, prompt_input_ids=prompt_input_ids) audio_arr = generation.cpu().numpy().squeeze() sf.write("parler_tts_out.wav", audio_arr, model.config.sampling_rate) 音声合成するテキストをprompt、その音声のスタイルをdescriptionとして記述しています。簡単にスタイルを翻訳すると「低めの声で早口の女性がクリアな音質で、感情を込めて話します」でしょうか。 生成された音声がこちらになります。 では、ここからdescriptionを編集してスタイルを色々変えてみたいと思います。 男性の声にする A female speakerをA male speakerに変更して、声を男性にしてみたいと思います。 男性の声になりました。 ゆっくり話させる She speaks very fast.をShe speaks very slowly .に変更して話速を遅くしてみます。 再生時間で測ると1秒が3秒になるくらいに遅くなりました。 単調に話させる もとのdescriptionではher words quite expressivelyと、感情豊かに話すことが指示されていましたが、ここをher words very monotone と単調に話すように変更します。 ちょっと声のピッチが変わってしまったように聞こえますが、元の発話に比べて、かなり淡々とした話し方になりました。 声のピッチを上げる 元々slightly low-pitchedだった音声のピッチ指定部分をvery high -pitchedとし、高い音声にしてみます。 かなり高い声になりました。 ノイズをのせる もとのdescriptionではin a very confined sounding environment with clear audio qualityと綺麗な音質であるような指示がありますが、ここを消して、 The speaker's voice is quite close-sounding but very noisy. とわざとノイズを乗せてみようと思います。 あまりうまくいかなかったようです。元論文の 紹介ページ ではこのようなdescriptionで上手くノイズが乗っていたので、軽量版だと生成可能な表現が少ないのかもしれません。 おわりに 本記事ではParler-TTSを使ったスタイルを指定した音声合成を試してみました。 参照音声なしにピッチや話速などをテキストベースのみで指示できるのはかなり画期的でした。 私自身が判断できなかったので今回の記事には載せませんでしたが、英語でいうアメリカアクセントやイギリスアクセントなどの訛りのようなものも表現できるようです。 ノイズをのせるのはうまくいきませんでしたが、READMEを見ると今後徐々にスケールアップしていく旨が書かれていたので今後に期待したいと思います。また、学習コードも提供されているので、日本語モデルの学習も試してみたいです。 最後までお読み頂き、ありがとうございました! 投稿 Parler-TTSを使ってスタイルを指定して音声合成を行う は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは。AIチームの東です。 本記事では、2024年3月6日(水)~8日(金)で行われた 日本音響学会第151回(2024年春季)研究発表会 の参加報告をさせていただきます。 イベント、セッション 今年の春季研究会はオフライン形式で開催されました。春季研究会としては2020年以来、4年振りのオフライン形式となりました。 2020年以前の春季研究会は例年関東地方で開催されることが通例らしく、今年は拓殖大学(東京都)での開催となりました。 正門の様子。会期中は雷雨・雪など天候不良が続いていましたが、2日目は晴天となりました。 今回の研究会では以下の件数の講演・発表等がありました。 一般講演(口頭発表) :235件(全10会場) 招待講演:40件 ポスターセッション:210件 選奨行事:1件 特別講演:1件 開催された全てのイベントやセッションに参加することはできませんでしたが、その中からいくつかを選んでご紹介します。 学生・若手フォーラム主催イベント 今年は研究発表会の前日(3/5)に「Student Day」、研究発表会初日(3/6)に「学生コーヒーブレイク」「学生・若手飲み」が日本音響学会 学生・若手フォーラムの主催で開催されました。 「Student Day」ではこれまで学会期間中に開催されていた「会場・デモ見学」「ビギナーズセミナー」、「インタラクティブセッション」を1つのプログラムとしてまとめたもので、ハイブリッド形式(現地参加は学生のみ)で開催されました(弊社の親会社であるサイバーエージェントからも1件発表がありました)。 「学生コーヒーブレイク」では関連分野の学生同士、「学生・若手飲み」では関連分野の研究者と学生の交流を目的に開催されたそうなので、私も機会が合えば次回以降で参加させていただければと思います。 また、これらの情報は日本音響学会(ASJ)学生・若手フォーラムの HP や X 、 Facebook で発信されておりますので、今後のイベントについてはそちらをご参照ください。 口頭発表・ポスターセッション 今回の研究発表会では口頭発表・招待講演・ポスターセッション合わせて約500件の発表がありました。かなりの件数の発表が3日間で並行して行われるので大部分は見て回ることができず残念でしたが、弊社のプロダクトが抱える課題に関連する研究も多く、今後の参考にさせていただければと感じました。 また、今回は 親会社のサイバーエージェントから3件の発表 と賛助会員ポスター展示があり、選奨行事ではAI Labの 李莉 による研究が『独創研究奨励賞 板倉記念』に選ばれました。 以下、聴講させていただいた中で弊社のプロダクトにも関連のある研究をいくつか抜粋して紹介させていただきます。 [1-Q-31] 日本語日常会話の潜在的な発話スタイルに基づく対話シーンに応じた音声合成 ☆嶋崎 純一(名古屋工業大・工),上乃 聖,李 晃伸(名古屋工業大・工学研) こちらは対話中のインタラクションを考慮した音声を再現(音声合成)する研究についての発表でした。 こちらの発表では会話形式と話者関係性をIDで表現した情報、書き起こしテキストに因子分析を行って得られた特徴をそれぞれモデルの入力に加えた際の結果の評価、分析を行っていました。 弊社の電話応対サービス( AI Messenger Voicebot )ではより良い顧客体験にするための戦略の一つに音声合成が議題に上ることがあり、このような対話的な文脈を考慮した音声合成という研究は今後も注視していきたいと思います。 [2-P-15] 大規模言語モデルを用いた自由記述型音声感情認識の検討 〇安藤 厚志,増村 亮,田中 智大,牧島 直輝,庵 愛,山﨑 善啓,河田 尚孝,折橋 翔太(NTT) こちらは自由記述型のテキスト形式で音声感情を予測させるタスクの提案とその実現性に関する検証を行った研究です。従来の音声認識では事前に定義されたカテゴリを推定するタスクや2次元以上の感情次元を定義して空間上の値を予測するタスクが一般的ですが、この研究では音声感情を自由記述文で説明させるというタスクを提案しています。 その実現のためのアーキテクチャとして学習済みのLLMと音声エンコーダを組み合わせた構成を提案しているのですが、モデルを統合する部分のみのパラメータを学習時に更新させることで、比較的低コストで当該タスクを実行できる点が面白いと感じました。 同様のアーキテクチャを拡張して認識スタイルを制御できる音声認識モデルを構成する、等周辺タスクへの応用も考えられそうです。 [3-2-10] 過去の発話を考慮した文単位音声要約技術の検討 ◎松浦 孝平,芦原 孝典,森谷 崇史,三村 正人,叶 高朋,小川 厚徳,デルクロア マーク,浅見 太一(NTT) こちらは音声認識と音声要約の中間の粒度の出力を行う文単位の音声要約タスクを提案し、その実現性の検証を行った研究です。 論文では過去の発話情報を入力に考慮すると精度の向上が見込めるが、各文を独立に要約させてもほとんど同等の結果が得られた、と報告されていました。 弊社が提供している音声対話要約サービス( AI Messenger Summary )でも類似しているタスクを扱っており、音声データに対して要約を行う適切な粒度や技術構成については今後も検討を重ねていければと思います。 おわりに 今回、春季研究発表会としては初めてオフラインで参加させていただきました。 前回の研究発表会 に引き続き多くの最先端の研究成果に触れることができ、非常に多くの学びを得ることができました。今後も継続的に参加・発表を行っていければと思います。 このような機会を作ってくださった運営や発表者の皆様に感謝申し上げます。 最後までお読みいただきありがとうございました。 投稿 日本音響学会 第151回(2024年春季)研究発表会 参加報告 は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは。フロントエンドチームの安井です。今回はLLMと相性のいいReactのChartライブラリを探すために検証を行いました。ReactのChartライブラリといえば数多く種類が存在し、どれを採用するのがいいか迷うところです。下記のサイトはReactのChartライブラリが一覧で整理されており、これだけでも数十個候補になるライブラリがあることがわかります。 https://awesome.cube.dev/?tools=charts&frameworks=react はじめに 本記事では数多くあるReactのChartライブラリの中からLLMと相性がいいライブラリを様々な観点から考察することを目的とします。 前提 LLMからChartに注入する jsonデータ を生成する https://platform.openai.com/docs/guides/text-generation/json-mode レンダリングは Client で行う Vercel AI SDKではRSCを採用してServerでレンダリングしています https://sdk.vercel.ai/docs/concepts/ai-rsc 無料で使用可能な OSS もしくは 無料枠のあるプロダクト を対象にする jsonSchemaなどでのバリデーションはここでは考えない Goalイメージ HighCharts GPTというプロダクトを例に挙げますが、以下のようにLLMから生成した結果をグラフで表示させることを目的とします。 また、HighChartsは非常に優れたChartプロダクトですが、商用利用する際にはライセンスが有料となっているため今回は対象外としています。 https://www.highcharts.com/chat/gpt/ 結論 少し検証内容が長くなってしまうので、先に考察結果を紹介します。 オブジェクトベースのデータ構造で、LLMから生成されるデータを直接注入可能なライブラリが相性がいい。 ライブラリ名 シンプルさ 拡張性 LLMとの相性 データ構造のタイプ Recharts ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️ オブジェクトベースのデータ構造 AG Charts ⭐️⭐️⭐️⭐️ ⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ オブジェクトベースのデータ構造 Ant Design Charts ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️ オブジェクトベースのデータ構造 nivo ⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️ オブジェクトベースのデータ構造 Unovis ⭐️⭐️⭐️ ⭐️⭐️ ⭐️⭐️⭐️ オブジェクトベースのデータ構造 Chart.js ⭐️⭐️⭐️ ⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️ 配列ベースのデータ構造 apexcharts ⭐️⭐️⭐️ ⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️ 配列ベースのデータ構造 1st Step: 最終更新日が半年以内のライブラリに絞り込む まずは数あるライブラリの中から 最終更新日が半年以内 のものに絞り込みました。どれだけ魅力的なライブラリでも適切にメンテナンスされていなければ現場で採用するのは難しくなります。その結果以下の7件が該当しました。 *満遍なく検討しましたが、これが全てではありません。 ライブラリ名 Star数 説明 URL Chart.js 63K 最もStar数の多いChartライブラリで、HTML5を使用したレスポンシブでモダンなチャートを簡単に実装できます。 Chart.js Recharts 22K Reactベースのライブラリで、宣言的にカスタマイズ性高くチャートを構築できます。 Recharts apexcharts 14K インタラクティブでモダンなデザインのチャートを提供するライブラリです。多種多様なタイプをサポートしており、簡単にリッチなチャートを作成できます。 apexcharts AG Charts 59 高性能なJavaScriptチャートライブラリで、AG Gridの開発者によって作られました。大量のデータを扱うアプリケーション向けに最適化されており、スムーズなアニメーションとインタラクティブなチャート作成が可能です。 AG Charts nivo 12K Reactをベースにした豊富なコンポーネントを提供するライブラリです。SVGとCanvasのレンダリングをサポートしており、大量のデータや複雑なインタラクションを扱うチャートに適しています。 nivo Ant Design Charts 1.8K Ant Design システムに基づいたReactのチャートライブラリで、優れたデザインとUXを提供します。data visualizationを簡単かつ効果的に実装できるように設計されています。 Ant Design Charts Unovis 1.7K 強力なdata visualizationとデータ分析のためのJavaScriptライブラリで、複雑なデータセットを扱う際に特化しています。カスタマイズ可能で、多彩なタイプとインタラクティブな機能を備えています。 Unovis 2nd Step: 各ライブラリの比較から共通点を見つける 候補に上がるライブラリを絞ったので、次に各ライブラリを網羅的に確認しました。 すると、グラフに渡すデータ構造に大きく2つのパターンがあることがわかりました。 「 オブジェクトベースのデータ構造 」か「 配列ベースのデータ構造 」に分かれる。   実際にRecharts(オブジェクトベース)とChart.js(配列ベース)を例に挙げてそれぞれ解説をします。 オブジェクトベースのデータ構造(Rechartsなど) オブジェクトベースのデータ構造は以下の例です。 Labelとそれに紐づくValueがオブジェクトで関連付けられている (Labelを軸に置いた構造) const data = [ { label: 'March', pv: 23, uv: 13 }, { label: 'April', pv: 45, uv: 25 }, { label: 'May', pv: 56, uv: 36 }, { label: 'June', pv: 67, uv: 47 } ]; https://recharts.org/en-US/examples メリット ラベルに対して紐づくデータが構造化されている 配列ベースに対してデータの不一致がおきずらい。 デメリット データ構造の中にValue値以外の情報を含めずらい 配列ベースのデータ構造で解説しますが、オブジェクトベースのデータ構造ではあくまでLabelに関連するValueを定義することに適しています。 配列ベースのデータ構造(Chart.jsなど) 配列ベースのデータ構造は以下の例です。 LabelとValueが別の配列で管理されており、Indexで関連付けをしている (Valueを軸に置いた構造) const data = { labels: ["March", "April", "May", "June"], datasets: [ { label: 'pv', data: [23, 45, 56, 67], }, { label: 'uv', data: [13, 25, 36, 47], }, ] }; https://www.chartjs.org/docs/latest/samples/line/line.html メリット 各Valueに対して追加情報を付与できる。 const data = { labels: ["March", "April", "May", "June"], datasets: [ { label: 'pv', data: [23000, 45000, 56000, 67000], // ↓ のような情報 borderColor: "red", backgroundColor: "white", fill: false }, ] } オブジェクトベースのデータ構造だった場合、それぞれのValueに対して追加情報を付与したい場合はデータ構造の外で定義する必要が生まれます。 デメリット データ量が増すほど複雑になる 以下の例のようにデータ量が増えた際にはオブジェクトベースの方がシンプルな構造を保ったまま拡張することができます。 また、配列ベースだとIndexがズレた場合Labelに対してValueが不一致が起きる可能性があります。 オブジェクトベースのデータ構造が複雑化した場合 const data = [ { label: 'March', pv: 23000, uv: 13000, revenue: 2000, newVisitors: 7000, sessions: 15000, bounceRate: 45 }, { label: 'April', pv: 45000, uv: 25000, revenue: 4500, newVisitors: 15000, sessions: 30000, bounceRate: 50 }, { label: 'May', pv: 56000, uv: 36000, revenue: 6000, newVisitors: 20000, sessions: 40000, bounceRate: 55 }, { label: 'June', pv: 67000, uv: 47000, revenue: 8000, newVisitors: 22000, sessions: 50000, bounceRate: 60 } ]; 配列ベースのデータ構造が複雑化した場合 const data = { labels: ["March", "April", "May", "June"], datasets: [ { label: 'pv', data: [23000, 45000, 56000, 67000], }, { label: 'uv', data: [13000, 25000, 36000, 47000], }, { label: 'revenue', data: [2000, 4000, 6000, 8000], }, { label: 'newVisitors', data: [7000, 15000, 20000, 22000], }, { label: 'sessions', data: [15000, 30000, 40000, 50000], }, { label: 'bounceRate', data: [45, 50, 55, 60], } ] }; 考察 これまでの内容から以下のことが考えられます。 オブジェクトベースの場合、 シンプルさと確実性に長けている 配列ベースは拡張性が高い一方で、 データ量によっては複雑性が増し、データの不一致を起こす可能性が高まる トークン数を抑えながら、確実性高くグラフデータを生成するという観点でオブジェクトベースのデータ構造の方がLLMと相性がいいと考えられる。 各ライブラリをパターンで仕分ける 1st Stepで絞り込んだライブラリをパターンで分けたところ、以下の表のようになりました。 ライブラリ データ構造のタイプ Recharts オブジェクトベースのデータ構造 AG Charts オブジェクトベースのデータ構造 Ant Design Charts オブジェクトベースのデータ構造 Unovis オブジェクトベースのデータ構造 nivo オブジェクトベースのデータ構造 Chart.js 配列ベースのデータ構造 apexcharts 配列ベースのデータ構造 3rd Step: 実際にLLMからグラフを生成する では実際に全てのライブラリでLLMからjsonデータを生成し、グラフで表示してみます。 前提 全てのライブラリに対して共通の内容でグラフを表示させてみます。 補足 system roleの内容を各Chartライブラリに合わせて設定します。 入力内容がuser roleの内容として送信されます。 複雑なプロンプトではなく最小限で構築します。 入力する内容 第1四半期(Q1)の売上は5,000,000ドルです。 第2四半期(Q2)の売上は5,500,000ドルです。 第3四半期(Q3)の売上は6,000,000ドルです。 第4四半期(Q4)では、売上が6,500,000ドルに達しました。 期待する出力結果 四半期のQ1〜Q4をX軸のLabelに表示 各期に対応するValueを表示 3つの項目で評価 そして、それぞれ私の体感になりますが以下3つの項目でライブラリを比較していきます。 各項目に対して最大⭐️5つで評価します。 シンプルさ 注入するdataの構造がシンプルかどうか 拡張性 グラフ自体のカスタマイズ性 LLMとの相性 LLMと組み合わせた時の相性 Rechartsの場合 system prompt { role: "system", content: "You are a helpful assistant designed to output JSON. Return Data Format isdata: { label: {label}, sales: {value} }`.", }, generated data { "data": [ { "label": "Q1", "sales": 5000000 }, { "label": "Q2", "sales": 5500000 }, { "label": "Q3", "sales": 6000000 }, { "label": "Q4", "sales": 6500000 } ] } component.tsx <LineChart data={data}> <CartesianGrid strokeDasharray="3 3" /> <XAxis dataKey="label" /> <YAxis /> <Tooltip /> <Legend /> <Line type="monotone" dataKey="sales" stroke="#8884d8" activeDot={{ r: 8 }} /> </LineChart> 評価 シンプルさ:⭐️⭐️⭐️⭐️⭐️ 拡張性:⭐️⭐️⭐️⭐️ LLMとの相性:⭐️⭐️⭐️ コメント データ構造が非常にシンプル で、データ量の増加にも耐えうる構造だと感じました。 拡張性は高く柔軟にカスタマイズ可能ですが、内容によってはその分コード量が増えることが予想されます。 LLMとの相性の観点で気になったこととして、 コンポーネント側でX軸、Y軸の変数名(ここではlabelとsales)を指定する必要があります 。そのため、LLMから生成される値を事前に知っておく、もしくは一致させる必要があるのは注意点かと思いました。 AG Chartsの場合 system prompt { role: "system", content: `You are a helpful assistant designed to output JSON. Return Data Format is "data: { title: { text: {title}, }, data: [{ quarter: {quarter}, sales: {value} }], series: [{ type: 'line', xKey: 'quarter', yKey: 'sales' }], }".`, }, generated data { "data": { "title": { "text": "Quarterly Sales Data" }, "data": [ { "quarter": "Q1", "sales": 5000000 }, { "quarter": "Q2", "sales": 5500000 }, { "quarter": "Q3", "sales": 6000000 }, { "quarter": "Q4", "sales": 6500000 } ], "series": [ { "type": "line", "xKey": "quarter", "yKey": "sales" } ] } } component.tsx <AgChartsReact options={data} /> 評価 シンプルさ:⭐️⭐️⭐️⭐️ 拡張性:⭐️⭐️ LLMとの相性:⭐️⭐️⭐️⭐️⭐️ コメント AG Chartsを使用して一番メリットに感じた点は、 LLMから生成されたデータをそのままコンポーネントに注入できること です。 そのため、LLM側と実装側でデータの調整をすることがなくなります。 その一方、実装側でカスタマイズするには少し不便に感じたことからシンプルにLLMと組み合わせる場合に相性がいいように感じました。 また、AG Chartsの無料枠が充実している利点はありますが、 プロダクトとして展開されていることから、よりリッチなコンポーネントを使用する場合に料金が発生 してしまいます。 Ant Design Chartsの場合 system prompt { role: "system", content: "You are a helpful assistant designed to output JSON. Return Data Format is `data: { label: {label}, sales: {value} }`.", }, generated data { "data": [ { "label": "Q1", "sales": 5000000 }, { "label": "Q2", "sales": 5500000 }, { "label": "Q3", "sales": 6000000 }, { "label": "Q4", "sales": 6500000 } ] } component.tsx const config = { xField: 'label', yField: 'sales', point: { shapeField: 'square', sizeField: 4, }, interaction: { tooltip: { marker: false, }, }, }; <Line data={data} {...config} /> 評価 シンプルさ:⭐️⭐️⭐️⭐️⭐️ 拡張性:⭐️⭐️⭐️ LLMとの相性:⭐️⭐️⭐️⭐️ コメント Ant Design ChartsもAG Chartsに似て、 LLMから生成されるデータを素直に注入することができます 。(上記のconfigデータ自体もLLMに生成させることも可能です。) また、データ構造も非常にシンプルなためLLMと相性が良く実装することができそうです。 一つ懸念があるとすると、 ドキュメントの大半が中国語で書かれていることで実装に詰まった場合で苦戦しそう な予感がしました。 nivoの場合 system prompt { role: "system", content: `You are a helpful assistant designed to output JSON. Return Data Format is "data: [ { "id": "sales", "color": rgb(147 197 253), "data": [{ x: quarter, y: sales }] } ] ".`, }, generated data { "data": [ { "id": "sales", "color": "rgb(147, 197, 253)", "data": [ { "x": "Q1", "y": 5000000 }, { "x": "Q2", "y": 5500000 }, { "x": "Q3", "y": 6000000 }, { "x": "Q4", "y": 6500000 } ] } ] } component.tsx <ResponsiveLine data={data} margin={{ top: 50, right: 110, bottom: 50, left: 60 }} xScale={{ type: 'point' }} yScale={{ type: 'linear', min: 'auto', max: 'auto', stacked: true, reverse: false }} yFormat=" >-.2f" axisTop={null} axisRight={null} axisLeft={{ tickSize: 5, tickPadding: 5, tickRotation: 0, legend: 'sales', legendOffset: -40, legendPosition: 'middle', truncateTickAt: 0 }} /> 評価 シンプルさ:⭐️⭐️ 拡張性:⭐️⭐️⭐️⭐️⭐️ LLMとの相性:⭐️⭐️ コメント nivoは特に 拡張性に富んだライブラリ に感じました。その一方で、シンプルな実装をしたい場合は他に比べてコードが肥大化してしまう欠点もあります。 特にコンポーネントのコードを見ていただくとわかるとおり、他のライブラリに比べてもコード量が多いことがわかります。 Unovisの場合 system prompt { role: "system", content: `You are a helpful assistant designed to output JSON. Return Data Format is type Data = { quarter: number; sales: number } "data: { quarter: {quarter}, sales: {value} }". `, }, generated data { "data": [ { "quarter": 1, "sales": 5000000 }, { "quarter": 2, "sales": 5500000 }, { "quarter": 3, "sales": 6000000 }, { "quarter": 4, "sales": 6500000 } ] } component.tsx <VisXYContainer> <VisLine data={data} x={useCallback(d => d.quarter, [])} y={useCallback(d => d.sales, [])} /> <VisAxis type="x" /> <VisAxis type="y" /> </VisXYContainer> 評価 シンプルさ:⭐️⭐️⭐️ 拡張性:⭐️⭐️⭐️⭐️ LLMとの相性:⭐️⭐️⭐️ コメント Univosはシンプルなデータ構造でグラフを構築できますが、Recharts同様に コンポーネント側がLLMから生成される変数を知っておく必要がある 点がデメリットに感じました。 また使用するデータがnumber型である必要があったため、プロンプトで型を指定する必要がありました。 Chart.jsの場合 system prompt { role: "system", content: `You are a helpful assistant designed to output JSON. Return Data Format is type quarter = string[] type sales = number[] { 'data': { 'labels': {quarter}, 'datasets': [ { 'label': 'sales', 'data': {sales}, 'borderColor': 'rgb(147, 197, 253)', 'backgroundColor': 'rgba(147, 197, 253, 0.5)' } ] } }`, }, generated data { "data": { "labels": ["Q1", "Q2", "Q3", "Q4"], "datasets": [ { "label": "sales", "data": [5000000, 5500000, 6000000, 6500000], "borderColor": "rgb(147, 197, 253)", "backgroundColor": "rgba(147, 197, 253, 0.5)" } ] } } component.tsx <Line data={data} /> 評価 シンプルさ:⭐️⭐️⭐️ 拡張性:⭐️⭐️⭐️ LLMとの相性:⭐️⭐️⭐️⭐️ コメント Chart.jsは配列ベースの構造のため、データ量が少ない場合はシンプルな構造になりますが、データ量が増えるとデータ構造も複雑になっていきます。 しかし、提供されているコンポーネント自体は非常にわかりやすくシンプルなため、 複雑なグラフを描画する目的でなければスムーズにLLMと組み合わせ可能 に感じました。 生成されるデータを直接注入可能な点も非常に相性が良く感じます。 apexchartsの場合 system prompt { role: "system", content: `You are a helpful assistant designed to output JSON. Return Data Format is "data: { options: { chart: { id: {id} }, xaxis: { quarter: {quarter} } }, series: [ { name: "sales", data: {sales} } ] }". `, }, generated data { "data": { "options": { "chart": { "id": "sales" }, "xaxis": { "quarter": ["Q1", "Q2", "Q3", "Q4"] } }, "series": [ { "name": "sales", "data": [5000000, 5500000, 6000000, 6500000] } ] } } component.tsx <Chart options={data.options} series={data.series} // 🐱 このtpyeを自由に切り替えることで同一データでも見せ方を可変にできる type="line" /> 評価 シンプルさ:⭐️⭐️⭐️ 拡張性:⭐️⭐️⭐️ LLMとの相性:⭐️⭐️⭐️⭐️ コメント apexchartsは独特なデータ構造を持っていることから、プロンプトで正確に生成結果を制御する必要があります。 しかし、コンポーネントに対して生成されたデータを素直に注入できることと、 同一のデータから複数のtypeに切り替え可能 なことは非常に強みに感じました。 考察結果 ライブラリ名 シンプルさ 拡張性 LLMとの相性 データ構造のタイプ Recharts ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️ オブジェクトベースのデータ構造 AG Charts ⭐️⭐️⭐️⭐️ ⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ オブジェクトベースのデータ構造 Ant Design Charts ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️ オブジェクトベースのデータ構造 nivo ⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️ オブジェクトベースのデータ構造 Unovis ⭐️⭐️⭐️ ⭐️⭐️ ⭐️⭐️⭐️ オブジェクトベースのデータ構造 Chart.js ⭐️⭐️⭐️ ⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️ 配列ベースのデータ構造 apexcharts ⭐️⭐️⭐️ ⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️ 配列ベースのデータ構造 AG ChartsとAnt Design Charts、Chart.jsは 生成されるデータを直接注入可能 なことからLLMと相性がいいと判断しました。 RechartsとAnt Design Chartsは データ構造が非常にシンプル なため、トークン数も抑えながら確実性高くデータを生成できると考えます。 apexchartsは同一の生成されたデータから、 複数typeのグラフを描画できる ことは非常にメリットに感じました。 まとめ 今回は数あるReactのChartライブラリから個人の視点でLLMと相性がいいと考えられるものを考察しました。 初めに各ライブラリを比較する中でデータ構造が「オブジェクトベースのデータ構造」と「配列ベースのデータ構造」の二種類に分かれることに着目しました。そして、 データ量が複雑化した場合のケースを考慮すると、使用するトークン数と確実性の観点から「オブジェクトベースのデータ構造」がLLMと相性がいい と考えました。 その上で各ライブラリを上記二つに分類し、それぞれLLMからグラフを生成してみました。その結果、ライブラリごとに強みやLLMとの相性が異なり、大きく特徴が分かれる結果になりました。 今回はどのライブラリが一番LLMと相性がいいかを断定することはしませんが、これまでに整理したChartライブラリの特性とLLMとの相性、グラフのUIなどから総合的に判断していただけるといいかと思います。 投稿 LLMと相性のいいReactのChartライブラリを考察してみた🦜 は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは AIチームの戸田です 固有表現抽出(NER)や品詞タグ付けなどのNLPタスクを行うためのPythonライブラリに spaCy があります。シンプルなAPIで拡張性も高く、AI ShiftでもプロダクトのNLPロジック部分やデータ分析など多くの場面で利用しています。 そんな非常にお世話になっているspaCyですが、処理パイプラインにLLMを統合できる spacy-llm がリリースされました。今回の記事ではそのspacy-llmを使って極性分類やNERなどのNLPタスクをzero-shotで解いてみようと思います。 事前準備 以下のコマンドでインストールすることができます。 pip install spacy-llm 日本語を処理するための spacy[ja] がインストールされていない場合はここで一緒にインストールしておいてください。 本記事ではLLMとしてGPT-3.5 turboを利用しますので、環境変数OPENAI_API_KEYを設定しておきます。 設定ファイルの準備 spaCyで処理パイプラインを設定する際は、Pythonコードで設定する方法と設定ファイル(*.cfg)で設定する2パターンがありますが、今回は後者の設定ファイルで設定する方法で行います。 以下のような内容のファイルを準備しておきます。 [nlp] lang = "ja" pipeline = ["llm"] [components] [components.llm] factory = "llm" [components.llm.task] # ここにタスク設定を記述する [components.llm.model] @llm_models = "spacy.GPT-3-5.v1" config = {"temperature": 0.0} 前述の通りLLMには OpenAIのGPT-3.5 を使用します。このほかにspaCyでは AnthropicのClaude や CohereのCommand をサポートしています。加えて MetaのLlama2 などHuggingface Hubで公開している、いわゆるOpenLLMも使用することができます。 様々なNLPタスクを解いてみる 極性分類 極性分類を行う際は設定ファイルのcomponents.llm.taskに以下のような設定をしてください。 [components.llm.task] @llm_tasks = "spacy.Sentiment.v1" これをsentiment.cfgとして保存し、これを読み込むことで極性分類のパイプラインを作ります。 from spacy_llm.util import assemble nlp = assemble("sentiment.cfg") texts = [ "ここのラーメン美味しいね!", # Positiveを期待 "急に雨降ってきたけど傘忘れちゃった", # Negativeを期待 ] for text in texts: doc = nlp(text) print(f"{text}: score = {doc._.sentiment}") # 出力 # ここのラーメン美味しいね!: score = 1.0 # 急に雨降ってきたけど傘忘れちゃった: score = 0.2 scoreの範囲は0〜1で、高いほどPositiveといえるので、結果は期待通りとみえます。 文章分類 文章分類を行う際は設定ファイルのcomponents.llm.taskに以下のような設定をしてください。labelsには分類ラベルをリストで指定します。 [components.llm.task] @llm_tasks = "spacy.TextCat.v3" labels = ["経済", "エンタメ", "スポーツ", "国際"] これをclassify.cfgとして保存し、これを読み込むことで文章分類のパイプラインを作ります。 テストデータはこの記事を書いた時の Yahooニュース のタイトルから作成しました。 nlp = assemble("classify.cfg") texts = [ "日銀 マイナス金利解除の公算大", # 経済を期待 "磯部磯兵衛物語 実写ドラマ化", # エンタメを期待 "大谷 栗山氏とWBC以来の再会", # スポーツを期待 "TikTok禁止法案 米下院で可決", # 国際を期待 ] for text in texts: doc = nlp(text) # 結果は.catsにdefaultdict形式で{'エンタメ': 0.0, 'スポーツ': 0.0, '国際': 1.0, '経済': 0.0}のような形式で出力されているので、valueが最大のkeyを取得する pred = max(doc.cats, key=doc.cats.get) print(f"{text}: {pred}") # 出力 # 日銀 マイナス金利解除の公算大: 経済 # 磯部磯兵衛物語 実写ドラマ化: エンタメ # 大谷 栗山氏とWBC以来の再会: スポーツ # TikTok禁止法案 米下院で可決: 国際 期待通りにニュース記事のタイトルを分類できました。 固有表現抽出(NER) NERを行う際は設定ファイルのcomponents.llm.taskに以下のような設定をしてください。labelsには抽出する固有表現のラベルをリストで設定します。 [components.llm.task] @llm_tasks = "spacy.NER.v3" labels = ["組織名", "日付"] これをner.cfgとして保存し、これを読み込むことでNERのパイプラインを作ります。 テストデータには AI ShiftのMessageの最初の一文 を使います。 nlp = assemble("ner.cfg") text = "株式会社AI Shiftは、サイバーエージェントの子会社として2019年8月30日に設立いたしました。 AI Shiftという社名には、AIにシフトすることで人ならではの仕事に向き合える社会を作るという想いが込められています。" doc = nlp(text) spacy.displacy.render(doc, style="ent", jupyter=True) # jupyter notebookの場合 print([(ent.text, ent.label_) for ent in doc.ents]) # CLIの場合 # [("株式会社AI Shift", "組織名"), ("サイバーエージェント", "組織名"), ("2019年8月30日", "日付"), ("AI Shift", "組織名")] ここではjupyter notebookでの結果を示します。 AI Shiftやサイバーエージェントなど、適切に固有表現を認識できていることがわかります。 翻訳 翻訳を行う際は設定ファイルのcomponents.llm.taskに以下のような設定をしてください。target_langに翻訳対象の言語を指定してください。 [components.llm.task] @llm_tasks = "spacy.Translation.v1" target_lang = "English" これをtranslation.cfgとして保存し、これを読み込むことで翻訳のパイプラインを作ります。 nlp = assemble("translation.cfg") texts = [ "昨日初めてキャビアを食べました", "大きな犬が川沿いの道を歩いている", ] for text in texts: doc = nlp(text) print("日:", text) print("英:", doc._.translation) print() # 出力 # 日: 昨日初めてキャビアを食べました # 英: I ate caviar for the first time yesterday # # 日: 大きな犬が川沿いの道を歩いている # 英: A large dog is walking along the riverbank. 一般的な文章ですが、うまく翻訳できているのではないでしょうか? 要約 要約を行う際は設定ファイルのcomponents.llm.taskに以下のような設定をしてください。 [components.llm.task] @llm_tasks = "spacy.Summarization.v1" これをsummary.cfgとして保存し、これを読み込むことで要約のパイプラインを作ります。 テストデータには 以前書いたブログ の導入の文を利用します。 nlp = assemble("summary.cfg") text = """近年注目を集めている大規模言語モデル、ChatGPTやClaudeなどのAPIから利用できるサービスはもちろんですが、最近はMetaのLlama2などのオープンモデル、つまりLocalにダウンロードして使うことのできるモデルの開発も活発です。 オープンモデルを試すには強力なGPU環境が必要です。例えばLlama2 70Bモデルを動かすとなると、GPUのメモリは80GB以上は必要になってきます。しかし現在GPUは不足していると言われており、ハードウェアはもちろんのこと、クラウドサービスでもなかなかA100のような十分なスペックを持つGPUを確保することが困難です。量子化などのテクニックはありますが、70Bクラスになるとこれにも限界があります。 私が以前参加したKaggleのコンペティション、LLM Science Examではsimjeg氏によって、LLMの層ごとに推論処理を行うことで、メモリ16GBのT4上でもPlatypus2-70B-instructの推論を行う手法の提案がありました。 airllmはこのアプローチを参考にし、シンプルなインターフェースで使えるようにしてくれたライブラリです。理論上GPUのメモリはなんと4GBでよいと言われています。 今回はこのairllmを使ってT4上でmeta-llama/Llama-2-70b-chat-hfの推論を試してみたいと思います。""" doc = nlp(text) print(doc._.summary) # 出力 # 大規模言語モデルの注目が高まっており、オープンモデルの開発も活発化している。強力なGPU環境が必要であり、LLM Science Examではメモリ16GBのT4上で推論を行う手法が提案された。airllmはこのアプローチを参考にしたライブラリで、GPUのメモリは4GBで十分とされている。 LLMを使った要約はhallucinationが起こる可能性がありますが、今回は過不足なく要約できているように見えます。 おわりに 本記事ではspacy-llmを使って様々なNLPタスクをzero-shotで解いてみました。 以前 LLMをZero Shotテキスト分類器として利用する方法 を紹介しましたが、プロンプトの調整が面倒だったり、NERのような出力が可変なタスクへの対応などができなかったりと、実タスクで応用はまだ難しそうでしたが、spacy-llmでは非常に簡単に実装することができ、予測結果も非常に正確でした。 今回試したタスクのほかにも独自にカスタムタスクを定義できたり、パラメータ調整(例えば要約で出力語彙数を制限する)やfew-shot learningなども導入することができますので、興味のある方は ドキュメント をご参照いただければと思います。 最後までお読みいただきありがとうございました! 投稿 spacy-llmで色々なNLPタスクをzero-shotで解いてみる は 株式会社AI Shift に最初に表示されました。
アバター
1. はじめに こんにちは。AIチームの友松です。 2024年3月11日(月)〜3月15日(金)に 神戸国際会議場 で行われた 言語処理学会第30回年次大会 で、弊社からポスター発表で2件、口頭発表で1件の発表を行いました。 昨年は4年ぶりのオフライン(+オンラインのハイブリッド)開催&沖縄開催ということもあり、盛り上がりを見せておりましたが、今回の神戸開催における 統計情報 は投稿数599(昨年は577), 参加者数2045(昨年は1828, ※除当日参加者)となり過去最大となったようです。 2. 各発表資料と発表内でのディスカッション 2.1 RAGにおける自己認識的不確実性の評価 発表情報 P6:ポスター   3月13日(水) 9:30-11:00 P6-24   ○二宮大空, 戸田隆道 (AI Shift) 発表資料 ディスカッション Q: Retrieverによる検索が成功するかどうかを事前にわかっていれば回答を控える方法を考える必要はないのではないですか? A: 実運用時には検索が成功するかどうかは事前にはわかりません。今回の実験では、RAGのGeneratorにおける回答を控える能力を計測するために、Retrieverの検索は全て事前に行なっておき、実験データの検索の正誤を元に分割しています。しかし、推論時には検索の正誤に関する情報が与えられないので、正しく回答を控えるためには、Generatorで処理する必要があります。 Q: Retrieverで得られた情報も使うことで回答すべきか控えるべきかの判断が高精度に行えるのではないか? A: そうだと思います。例えば、検索時に得られた類似度の値はその情報の信頼性を表しているものと捉えることも考えられるので、Generatorが情報を判断する上で非常に有効だと考えられます。今後試してみたいと思います。 Q: 推論時にも回答をするか回答を控えるかの二値分類の処理をすると良いのではないか? A: そうだと思います。今回の実験では試せませんでしたが、Generatorで処理する前に、回答を控えるかどうかを二値分類タスクとしてLLMに解かせることで精度向上する可能性があります。今後試してみたいと思います。 2.2 RAGにおけるLLMの学習と評価:FAQタスクへの応用 発表情報 P5:ポスター   3月12日(火) 18:00-19:30 P5-5   ○長澤春希, 戸田隆道 (AI Shift) 発表資料 ディスカッション Q: Retriever の性能が気になる A: 今回は retriever の性能は評価の対象外としていました。ただそれぞれの条件で与える関連文書は全て共通のものとしています。 Q: RAGのみ, LoRAのみ, RAG+LoRA のそれぞれの運用において、それぞれを最適化した際の性能上限が気になる A: 今回は比較的 naive な設定での比較となっていました。それぞれを最適化させた場合、またそれぞれにおける質的な振る舞いは変化することが予測されます。 Q: 他ドメインデータでの結果が気になる A: 先の最適化の話と関連し、各種設定での性能はデータセットによって変わるものだと予見されます。特に事業応用を考える場合は、様々な運用方法を考えて、実際に試してみる姿勢が重要であると推察されます。 Q: LoRA のランクが大きいように思われる A: 経験則的に、LoRAのランク設定は目的に応じて適宜変更することが肝要であると考えられ、今回のように新規に知識を獲得させるような場合では、少し大きめに設定しないとうまくいかないことがありました。 Q: LoRA は出力スタイルのチューニング、RAG は外部情報参照というそれぞれの得手不得手があると考えられる。今回のように "対決" させるのではなく長所を組み合わせることが大事なのでは? A: 大変鋭い指摘でした。このような組み合わせはより柔軟なニーズ対応を可能にするアプローチであると考えられるので、大変参考になりました。 2.3 LCTG Bench: 日本語LLMの制御性ベンチマークの構築 発表情報 D11:テーマセッション1:人間と計算機のことばの評価(3)   3月14日(木) 14:10-16:00 D11-2   ○栗原健太郎 (AI Shift/サイバーエージェント), 三田雅人, 張培楠, 佐々木翔大, 石上亮介 (サイバーエージェント), 岡崎直観 (東工大) ディスカッション Q: 生成品質の評価ではGPT-4を使っているようですが,GPT-4からは文章が出力されるのでは。どのようにしてスコアに最終的に変換しましたか。 A: GPT-4の出力が「適切」「不適切」のいずれかになるようにプロンプトを作成しました。 Q: 今回の項目の他に検討した項目はあるのでしょうか? A: 今回採用したフォーマットの制御性の定義が「余計なものを出力しない」だったのですが、検討した内容の一つに「json形式で出力できるか」というものがありました。ただ、こちらは日本語LLMにとって非常に難易度が高いタスクだったので実施しないこととしました。 Q: 2値では判断が難しいものものに拡張される予定はあるのでしょうか? (例えばスタイル(positive-negative, polite-rude)など A: positive-negativeは過去に検討した項目の一つで、拡張の可能性もあると思います!positive-negative程度であればJGLUEのMARC-jaなどのスコアから多くの言語モデルで容易に回答できると言えるので、考えておりました。polite-rudeでも判別が高精度でできるという調査があれば検討の余地あるかと思います。 Q: たとえば,IFEvalのようにフォーマットの制御性(※)を多様化したうえで,制御対象の数を2個以上にすると,難易度を調節できるかもしれないと感じました. A: 難易度調整の話は没になりましたが構想にありました。当初の構想の原案として、制御性を1つのみ、2つ組み合わせ、3つ以上というようなレベル分けを検討していたのですが、2つ以上から日本語LLMにとっての難易度が極端に上がりまともなスコアを得られないというのが予備実験を実施した手応えでしたので、制御性1つのみ(つまり最も簡単なレベルのみ)というデザインになりました。 Q: 達成条件が明確ではない制御への拡張(例えば、小学生が理解できるようにして)について、お考えがあれば教えてください。(そういうものを作りたいなと思っていたのでした。また議論させてください。) A: 小学生が理解できるようにして  このような達成条件が不明瞭なものを納得感のある形で評価するのは今のところ難しいなと感じています。(それこそGPT-4などの高性能とされているLLMによる評価が主流?)従来のBERTベースのモデルが容易に回答可能な分類のレベルであれば、ある程度達成条件が曖昧になってもいいと考えています!(今回はコードベースで判別できるくらい明確になるよう設計しました) Q: もし根本的な限界 = 指示チューニングでは改善不能な特徴なのだとしたら,制御性を調べるタスクとしてあまり妥当ではないのかもしれないとも思いましたが,わかりません. A: この点については、インストラクションチューニングで改善できるのかは確かに不明ですね。ただ、文字数の制御性自体は関心が強いだろうという認識であり、GPT-4でも現状難しい項目となっていますが、いつの日かより強いLLMが出たその日にこのベンチマークを使って、「今まで難しかった文字数も制御できるようになった!」みたいな議論ができると面白いのかなと思っています。妥当性はともかくとして、設けるべき観点の一つかなと考えています。 3. 2023年度言語処理学会最優秀論文賞 会期中に、弊社所属の栗原 健太郎主著の 「JGLUE: 日本語言語理解ベンチマーク」 が 2023年度言語処理学会最優秀論文賞 を受賞しました。 ※研究内容は弊社での取組みではなく、学生時代の研究になります 。 また、研究内容について、招待論文で発表を行いました。 2023年度の言語処理学会論文賞は4件です。 #NLP2024 で表彰し、ご講演いただきました。 https://t.co/pAvImd1aXH 栗原 健太郎さん、塚越 駿さん、李 凌寒さん、大村 和正さん、おめでとうございます! pic.twitter.com/MnalqrULP2 — NLP2024 KOBE (@anlpmeeting) March 11, 2024 4. おわりに 昨年の沖縄で開催された言語処理学会では「緊急パネル:ChatGPTで自然言語処理は終わるのか?」のようなセッションが企画されたり、そのセッションが行われた夜にOpenAI社からGPT-4が発表されたりとNLPがこれからどうなっていくかというところに非常に注目が集まっていましたが、ChatGPTで自然言語処理は終わっておらず過去最大の開催となりました。生成AIに絡んだデータセットの構築、生成AIの挙動に関する検証、生成AIの評価に関する研究が多かったのが印象的でした。 毎度になりますが、このような素晴らしい学会を開催頂いた運営の皆様に感謝申し上げます。 来年は長崎開催が発表され、来年もAI Shiftから発表ができるように日々の活動をしていこうと思います。 投稿 言語処理学会第30回年次大会(NLP2024) 発表報告 は 株式会社AI Shift に最初に表示されました。
アバター
こんにちはAI Shiftの栗原です。3月11日(月)から3月15日(金)に 神戸国際会議場 で 言語処理学会年次大会 が実施され、AI Shiftからは口頭発表1件とポスター発表2件の合計3件の発表を行います。 本記事では各発表の概要と、ポスター発表については議論したいポイントについて取り上げたいと思います。 1. AI Shiftからの発表 3月12日(火) 18:00-19:30 P5-5 RAGにおけるLLMの学習と評価:FAQタスクへの応用 長澤春希, 戸田隆道 (AI Shift) 3月13日(水) 9:30-11:00 P6-24 RAGにおける自己認識的不確実性の評価 二宮大空, 戸田隆道 (AI Shift) 3月14日(木) 14:10-16:00 D11-2 LCTG Bench: 日本語LLMの制御性ベンチマークの構築 栗原健太郎 (AI Shift/サイバーエージェント), 三田雅人, 張培楠, 佐々木翔大, 石上亮介 (サイバーエージェント), 岡崎直観 (東工大) 2. 各発表の概要と議論したいポイント ポスター発表 2.1 RAGにおけるLLMの学習と評価:FAQタスクへの応用 概要 外部情報を参照しながらLLMを活用する代表的な方法としてRAGがあるが、一方でLoRAなどの軽量な学習手法も提案されている。LLMの実運用を考えた際、どちらが有用なのかは明らかとなっていない。本研究ではFAQタスクを例にとり、RAG、LoRA及びその組み合わせについての性能差を検証し、直接比較による評価で両者を組み合わせた際にMean Reciprocal Rank スコアが最も高い傾向となることを報告する。 議論したいポイント RAG精度向上のための施作 RAGシステムにおける評価手法の選定 事業応用を前提とした時のLLMとの付き合い方 2.2 RAGにおける自己認識的不確実性の評価 概要 カスタマーサポート事業においてユーザーからの質問にRAGで回答する場合、検索で答えが得られなかったときは回答を控えることが期待される。そこで、回答を控える正確さを"自己認識的不確実性(Self-Aware Uncertainty)"と定めて評価する。実験の結果、 Gemini-ProはGPT-4-turboよりも正しく回答を控えることができる傾向にあった。 議論したいポイント Hallucinationを防ぐための方法として、答えを控える以外により適切な方法はどのようなものが考えられるか リアルタイム性を保ったままRAGの精度を向上させる方法 他にどのようなRAGの評価方法が有効そうか 口頭発表 2.3 LCTG Bench: 日本語LLMの制御性ベンチマークの構築 概要 LLMの事業応用において性能を評価する際には、生成内容の評価の他に、指定のフォーマットや文字数を満たせるかという「生成結果の制御性」の評価も必要と考えている。本研究を通じて、日本語LLMの制御性を評価するベンチマークLCTGを構築した。さらにLCTGを用いた評価実験の考察も実施した。 議論したいポイント 実際に事業にLLMを適用する場合に、(本実験での検証項目の有無を問わず)制御性を気にした場面があるか否か 皆様が実施している、あるいは求めているLLMの評価方法 LLMの事業適用における、意思決定の方法 3. おわりに 今年もAI Shiftの他に,CyberAgentグループから4件の発表とスポンサーブースの出展があります。そちらの発表もぜひお越しください! 僕個人も神戸は人生初上陸で非常に楽しみにしております! 当日皆様と活発な議論やお話ができることを楽しみにしております! 投稿 言語処理学会第30回年次大会(NLP2024) でAI Shiftから3件の発表を行います は 株式会社AI Shift に最初に表示されました。
アバター