Google スプレッドシートで、自然言語で質問できる関数を作ってみよう(Google Cloud Vertex AI 後編)

2024年3月29日掲載

Google Cloud

システムエンジニアの成瀬です。

日々刻々と進化を続けているAIやクラウドサービスを検証したり、AIを活用したアプリを作ったりしています。

目次

  • この記事は、「Google スプレッドシートで、自然言語で質問できる関数を作ってみよう(Google Cloud Vertex AI 」の後編になります。(前・中・後の3部構成に変更したため、今回は中編の続きとなります)
  • Google Cloud Vertex AI 前編では、Text Embedding APIを使用して、テキストどうしの意味的な類似度を判定する方法について説明しました。
  • Google Cloud Vertex AI 中編では、テキスト生成AIをQ&Aに利用するにあたっての課題と、望ましい回答を引き出すための方法について説明しました。
  • この後編では、Q&Aアプリをつくりながら、中編の最後に残った課題の解決方法について説明します。

Q&Aアプリをつくってみよう

前回(中編)のプロンプト実験室で題材にした「おそうじボット」について、一問一答型のアプリを作ってみましょう。

このアプリでは、プロンプト実験室の最後に提起した、「質問に関連する情報をどのように用意すればいいか?」という問いに対する一つの解決策として、前編で作成したテキストの類似度を判定する関数を利用して、取扱説明書の中から質問に最も関連する部分を抽出するアプローチをとります。

アプリの画面構成

作成するアプリの画面構成は以下の通りです。

fig.1
  1. 質問:アプリに答えてほしい質問を入力します。
  2. 質問の埋め込みベクトル:質問に対する埋め込みベクトルを取得します。
  3. 指示:プロンプトで与える指示を設定しておきます。
  4. 関連コンテンツ:データシートから「2.質問の埋め込みベクトル」と最も関連の高いコンテンツを表示します。右のセルにはその類似度も表示します。
  5. 回答:生成された回答を表示します。
  6. プロンプト(デバッグ用):モデルに送信されたプロンプトを表示します。

メニューの「おそうじボットアンサー」->「回答生成」を選択することで、関数を実行してモデルが生成した回答を表示するようにします。

アプリの処理概要

アプリが質問に回答するための処理は、以下の3つのフェーズに分かれています。

fig.2

0.事前準備

まず、取扱説明書を適当な単位に分割しておきます。

つぎに分割した取扱説明書の各テキストをベクトル化しておきます。

1.質問の入力

質問が入力された時点で、質問テキストをベクトル化します。

つぎに質問ベクトルと各取扱説明書ベクトル間のコサイン類似度を算出して、最も関連性の高い取扱説明書テキストをコンテキストとして抽出します。

2.回答生成を実行

メニューから回答生成を実行した時点で、「指示、コンテキスト、質問」からプロンプトを構築します。

つぎに生成AIにプロンプトを送信して、回答を生成します。

カスタム関数の作成

このアプリのために新たに作成する関数は以下の3つです。

(処理概要図の緑の箱に相当します)

A. ベクトル化

B. 比較・抽出

C. プロンプトの構築

※「C1. 回答の生成」は中編で作成した「GENERATE_TEXT」関数を使用します。

A.ベクトル化: 埋め込みベクトルを取得するカスタム関数(日本語対応版)

この関数はVertex AI の Embedding APIを呼び出して、テキストに対する埋め込みベクトルを取得します。

埋め込みベクトルって何?と思った方は前編をご覧ください。

埋め込みベクトルを取得するカスタム関数は前編でも作成しましたが、前編の記事作成後にテキスト埋め込みモデルの多言語対応版「textembedding-gecko-multilingual」が公開されていたので、対応するカスタム関数を作成します。

参考:テキストエンベディング

概要

Vertex AI APIを呼び出してテキストに対する埋め込みベクトルを取得する関数です。

関数の構文

TEXT_EMBEDDINGS_MULTILINGUAL( テキスト)

  • テキスト:埋め込みベクトルを取得する対象のテキストです。
使用例

TEXT_EMBEDDINGS_MULTILINGUAL( "トイレに行きたいのですが" )

戻り値

文字列化された768次元のベクトルです。

(扱いやすくするために、文字列化します)

B.比較・抽出: 質問に関連する情報を抽出するカスタム関数

概要

対象ベクトルと最も関連している情報を複数のベクトル化したテキストから検索します。

ベクトルどうしのコサイン類似度を比較して、最も高い類似度のテキストを抽出します。

関数の構文

CONTENT_SEARCH(対象ベクトル, 検索範囲)

  • 対象ベクトル:埋め込みベクトルです
  • 検索範囲:検索する範囲を2列N行のレンジ形式で指定します。

(1列目:テキスト、2列目:ベクトル) 例:A1:B2

使用例

CONTENT_SEARCH("[-0.03439481556415558, ...]", A1:B2)

戻り値

[[ 最も関連するテキスト, 類似度 ]]

C.プロンプトの構築: プロンプトを構築して回答を生成する関数

概要

シート上の質問・指示・コンテキストからプロンプトを組み立てて、カスタム関数の「GENERATE_TEXT」を実行して結果を表示します。

メニューから回答生成を実行するための内部関数です。

関数の構文

generate_answer_()

カスタム関数の更新

中編のプロンプト実験室で作ったコードの更新と新しいコードを追加します。

  1. スクリプトエディタを開きます。
  2. ファイル「PART2.gs」内の全てを選択して、デリートキーかバックスペースキーを押してクリアします。
  3. 以下のコードを全て選択してコピーします。
/**
 * スプレッドシートを開いたときにカスタムメニューを表示します。
 *
 * copyright (c) 2024 SoftBank Corp.
 */
function onOpen() {
  const ui = SpreadsheetApp.getUi();
  ui.createMenu("プロンプト実験室")
    .addItem('開く', 'show_prompt_labo_')
    .addToUi();
  ui.createMenu('おそうじボットアンサー')
  .addItem('回答生成', 'generate_answer_')
  .addToUi();
}



/**
 * サイドバーを表示します。
 *
 * copyright (c) 2024 SoftBank Corp.
 */
function show_prompt_labo_() {
  const html = HtmlService.createHtmlOutputFromFile('PROMPT_LABO')
    .setTitle('プロンプト実験室');
  SpreadsheetApp.getUi()
    .showSidebar(html);
}



/**
 * 入力情報からテキスト生成関数を実行して、結果をシートに表示します。
 *
 * copyright (c) 2024 SoftBank Corp.
 */
function generate_answer_() {

  const spread_sheet = SpreadsheetApp.getActive()
  const sheet = spread_sheet.getSheetByName("アンサー")

  const question = sheet.getRange(1, 2).getValue()
  const instructions = sheet.getRange(3, 2).getValue()
  const context = sheet.getRange(4, 2).getValue()
  const prompt = `${instructions}\n\n${context}\n\n[質問]:${question}\n[回答]:`

  const answer = GENERATE_TEXT(prompt)
  sheet.getRange(5, 2, 2, 1).setValues([[answer], [prompt]])
}



/**
 * 生成AI APIを使用して、テキストを生成します。
 *
 * @param {"トイレはどこですか"} PROMPT プロンプト {文字列}
 *     プロンプトのテキストです。
 * @return {string}
 *
 * @customfunction
 * copyright (c) 2024 SoftBank Corp.
 */
function GENERATE_TEXT(PROMPT, MODEL = "text-bison") {

  let access_info;
  try {
    access_info = get_access_token_(GCP_CREDS.client_email, GCP_CREDS.private_key)
  }
  catch (e) {
    console.error(e)
    throw "内部エラーが発生しました"
  }
  const access_token = access_info.access_token

  const headers = {
    "Authorization": "Bearer " + access_token
  }

  const API_ENDPOINT = "us-central1-aiplatform.googleapis.com"
  const url = `https://${API_ENDPOINT}/v1/projects/${GCP_CREDS.project_id}/locations/us-central1/publishers/google/models/${MODEL}:predict`

  const payload = {
    instances: [
      {
        prompt: PROMPT
      },
    ],
    parameters: {
      "candidateCount": 1,
      "maxOutputTokens": 1024,
      "temperature": 0.0,
      "topP": 0.1,
      "topK": 1,
      "opt_out": true,
    }
  }

  const params = {
    method: "post",
    contentType: "application/json",
    headers: headers,
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  }

  const res = UrlFetchApp.fetch(url, params)
  if (res.getResponseCode() != 200) {
    console.error(res.getContentText())
    throw "内部エラー"
  }

  const res_json = JSON.parse(res.getContentText())
  return res_json.predictions[0].content
}



/**
 * 対象ベクトルと最も関連している情報を複数のテキストから検索します。
 *
 * @param {"[-0.03439481556415558, ...]"} VECTOR 対象ベクトル {文字列}
 *     埋め込みベクトルです。
 * @param {A2:B7} RANGE 検索する範囲 {レンジ}
 *     検索する範囲を2列N行のレンジ形式で指定します。(1列目:テキスト、2列目:ベクトル)
 * @return [[ string, float ]]
 *
 * @customfunction
 * copyright (c) 2024 SoftBank Corp.
 */
function CONTENT_SEARCH(VECTOR, RANGE) {

  let top = {
    similarity: -10,
    index: 0,
  }
  for (let i = 0; i < RANGE.length; i += 1) {

    let s = VECTOR_SIMILARITY(VECTOR, RANGE[i][1])
    if (s > top.similarity) {
      top.similarity = s
      top.index = i
    }
  }

  return [[RANGE[top.index][0], top.similarity]]
}



/**
 * テキスト埋め込みAPIを使用して、テキストに対する埋め込みベクトルを取得します。
 * 結果は文字列化されたベクトルです。
 *
 * @param {"トイレはどこですか"} TEXT テキスト {文字列} 
 *     埋め込み対象のテキストです。
 * @return {string}
 * 
 * @customfunction
 * copyright (c) 2024 SoftBank Corp.
 */
function TEXT_EMBEDDINGS_MULTILINGUAL(TEXT) {

  if ( TEXT === "" ) {
    throw "テキストが空白です"
  }

  const url = `https://us-central1-aiplatform.googleapis.com/v1/projects/${GCP_CREDS.project_id}/locations/us-central1/publishers/google/models/textembedding-gecko-multilingual:predict`

  let access_info;
  try {
    access_info = get_access_token_(GCP_CREDS.client_email, GCP_CREDS.private_key)
  }
  catch (e) {
    console.error(e)
    throw "内部エラーが発生しました"
  }
  const access_token = access_info.access_token

  const headers = {
    "Authorization": "Bearer " + access_token
  }

  const payload = {
    "instances": [
      {
        "content": TEXT,
      }
    ],
    "parameters": {
      "opt_out": true,
    }
  }

  const params = {
    method: "POST",
    headers: headers,
    contentType: "application/json",
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  }

  const res = UrlFetchApp.fetch(url, params)
  const res_json = JSON.parse(res.getContentText())

  if ("error" in res_json) {
    console.error(res_json.error)
    throw "内部エラーが発生しました"
  }

  const vector = res_json.predictions[0].embeddings.values
  return JSON.stringify(vector)
}
  1. コピーしたコードを、ファイル「PART2.gs」内にペーストして、「プロジェクトを保存」ボタンをクリックします。
  2. スプレッドシートに戻ります。
  3. ブラウザのリロードボタンなどで、スプレッドシートを再読み込みします。

データシートの作成

質問に関連する情報を検索するためのデータシートを作成します。

  1. スプレッドシートに新しいシートを追加して、名前を「データ」に変更します。
  2. おそうじボットの取扱説明書の主な抜粋とそのベクトル埋め込み関数を以下の通りに入力します。
 A列B列

行1

取扱説明書

ベクトル

行2

# 家庭用全自動掃除機「おそうじボット」について

家庭用全自動掃除機「おそうじボット™」はソウジング・テクノロジー社が開発した最新鋭の全自動掃除ロボットです。

掃除だけでなく、色々なお手伝いをすることができます。

=TEXT_EMBEDDINGS_MULTILINGUAL(A2)

行3

# こんなことができます

おそうじボットは掃除はもちろん、他にも色々なことができます。

・部屋の掃除

・電話をかける

・目覚まし時計

・写真撮影

・お湯を沸かす

・その他いろいろ

=TEXT_EMBEDDINGS_MULTILINGUAL(A3)

行4

# 電源のオン・オフ


## 電源をオンにする。

おそうじボットの電源ボタンを押すと電源がオンになり、おそうじボットの両目(LED)が点灯します。


## 電源をオフにする。

おそうじボットの電源ボタンを30秒以上押すと電源がオフになり、おそうじボットの両目(LED)が消灯します。

=TEXT_EMBEDDINGS_MULTILINGUAL(A4)

行5

# 掃除について

おそうじボットは掃き掃除と雑巾がけができます。

ただし、掃き掃除と雑巾がけは同時にできません。


## 掃き掃除

おそうじボットの右手に箒をセットします。

自動的に掃き掃除を開始します。


## 雑巾がけ

おそうじボットの左手に雑巾をセットします。

自動的に雑巾がけを開始します。

=TEXT_EMBEDDINGS_MULTILINGUAL(A5)

行6

# 電話をかける


## 電話をかける

おそうじボットの頭にあるおかっぱ型受話器を外します。

次に受話器の数字ボタンで電話をかける相手の番号を押します。


## 電話を切る

受話器をおそうじボットの頭に戻します。

=TEXT_EMBEDDINGS_MULTILINGUAL(A6)

行7

# アラームの設定方法


## アラームのセット

おそうじボットの背中のアラームボタンを押します。

アラームボタンの下にある時刻ダイヤルで時刻を設定します。

その後、もう一度アラームボタンを押します。


## アラームの解除

おそうじボットの背中のアラームボタンを3秒以上長押しします。

=TEXT_EMBEDDINGS_MULTILINGUAL(A7)

アンサーシートの作成

次にアプリのインターフェースとなる、アンサーシートを作成します。

  1. スプレッドシートに新しいシートを追加して、名前を「アンサー」に変更します。
  2. シートに以下の通りに入力します。
 A列B列

行1

質問

 

行2

質問の埋め込みベクトル

=TEXT_EMBEDDINGS_MULTILINGUAL(B1)

行3

指示

以下の情報だけを使用して質問に回答してください。
質問に対する回答が不明な場合は「わかりません」とだけ答えてください。

行4

コンテキスト=CONTENT_SEARCH(B2,'データ'!A2:B7)

行5

回答 

行6

プロンプト
(デバッグ用)

 

質問してみよう

それでは、アプリに質問をしてみましょう。

まず、セルB1の「質問」に以下のテキストを入力します。

おそうじボットはどんなことができるの?

質問の入力と連動して、セルB2の「質問の埋め込みベクトル」に質問に対応するベクトルが表示されます。

つづいて、データシートの中から質問と最も関連するテキストが、セルB4の「コンテキスト」に表示されます。

fig.3

次に、メニューの「おそうじボットアンサー」をクリックして、「回答生成」を選択します。

セルB5に生成された回答が表示されます。

また、実際に送信したプロンプトの内容が、セルB6に表示されます。

fig.4

役割を与える

今度は、質問に以下のテキストを入力して、回答生成を実行してみましょう。

山田くんに電話したいんだけど

回答結果は「わかりません」と表示されてしまいました。

fig.5

コンテキストには最も関連していそうな「電話のかけ方」が与えられているのに、なぜ回答がわからなかったのでしょうか。

プロンプト実験室で同じ質問をした結果は以下のようになりました。

fig.6

もしかすると、山田くんの電話番号がわからなったので、答えるのをあきらめてしまったのかもしれませんね...

それでは、生成AIモデルの使命感?を引き出すために、指示の内容に「回答者としての役割」を追加してみましょう。

セルB3の指示の内容を以下の文言に変更します。

あなたはお客様問い合わせ窓口のベテラン係員です。
以下の情報だけを使用して、質問に回答してください。
該当する回答が見つからなかった場合は、「わかりません」とだけ答えてください。

では、もう一度、回答生成を実行してみましょう。

fig.7

今度は取扱説明書に基づいた回答をしてくれましたね。

ちなみに、指示を以下のように変えるだけでも役割を与えたときと、ほぼ同様の効果がありました。

以下の情報だけを参照して、困っている人を助けてあげてください。
該当する回答が見つからなかった場合は、「わかりません」とだけ答えてください。

fig.8

役割を与えたり、指示の仕方を変えるだけで結果が変わるのは興味深いですね。

大規模言語モデルの知識の背景には、多種多様な人格が潜んでいるため、役割や立場による回答の仕方も記憶しているのかもしれません。

グラウンディングのまとめ

今回のアプリでのグラウンディングの方法について整理しておきます。

事前準備
  1. 質問に回答するためのデータセットを用意する
  2. データセットを適当な単位に分割する(チャンク化)
  3. 分割したデータをベクトル化しておく(インデックス化)
実行時
  1. 質問をベクトル化する
  2. 質問と最も類似度の高い分割データをコンテキストとして抽出する(インデックスマッチング)
  3. 指示、コンテキスト、質問からプロンプトを構築する
  4. プロンプトから回答を生成

おわりに

今回はテキスト生成AIをQ&Aに利用するために、生成AIモデルの知識を一時的に補強して望ましい回答を引き出すための「グラウンディング」という方法について説明しました。

また、コンテキストに与えるテキストを、ベクトルどうしの類似度から抽出する方法を説明しました。

活用のためのアイデア

グラウンディングによるQ&Aで回答精度を上げるためにはどうすればいいか、以下の点について検討してみてください。

  • チャンク化の粒度 - どれぐらいの長さが適切か?
  • インデックス化の方法 - BM25などの古典的な方法と比べてどうか? また、併用したらどうなるか?
  • マッチング方法 - 1対1以外ではどうなるか?
  • 適切なパラメータ - 最も精度が高くなるのはどの組み合わせか?
  • 適切なモデル - 最も精度が高くなるのはどのモデルか?

 

今回試したプロンプトはあくまでも一例です。

解答例を追加したり、指示やコンテキストの区切り方を変えたり、それらの順序を変えるなど、色々試して、ご自身のユースケースでのベストプラクティスを見つけてみてください。

Vertex AI DIYプランの紹介

「活用のためのアイデア」でも触れた通り、Q&Aの精度を上げるためには色々な試行錯誤が必要です。また、データセットが大規模になるほど、精度を維持することも難しくなります。

そこで、生成AI導入やシステム構築に不安がある方向けに「Vertex AI DIYプラン」をご紹介します。

Vertex AI DIYプランでは、Vertex AI Search による、 社内文書などのエンタープライズ検索の構築をエンジニアがサポートしてくれます。

興味のある方は、一度相談してみてはいかがでしょうか?

詳細については、関連サービスの「Vertex AI DIYプラン」からご確認ください。

関連サービス

Vertex AI DIYプラン

Vertex AI Search を使って社内文書を検索する生成AIを構築してみませんか?

ソフトバンクのエンジニアが構築をサポートします。

Google の生成AIの導入を考えている方はもちろん、どのようなものか確認したいという方でもご活用いただけます。

Google Workspace

Google スプレッドシート、Gmail、Google カレンダー、Google Chat、Google ドライブ、Google Meet などのさまざまなサービスがあらゆる働き方に対応する業務効率化を実現します。

Google Cloud

Google サービスを支える、信頼性に富んだクラウドサービスです。お客さまのニーズにあわせて利用可能なコンピューティングサービスに始まり、データから価値を導き出す情報分析や、最先端の機械学習技術が搭載されています。

おすすめの記事

条件に該当するページがございません