AWS

【Amazon Bedrock】Titan Embeddings Generation 1を利用して過去のブログ記事から入力に関連する記事を出力してみた

はじめに

こんにちは、omkです。
BedrockがGAしてから周りが生成AIで盛り上がっているので私も便乗してみようと思います。
なんかRAGチックなシステムを作ってみたかったのでまずは手始めにEmbeddingの勉強から始めてみました。

完成物

これまでにomkが書いた技術ブログの記事(80強ありました)の中から入力された内容に最も近いタイトルの記事を出力するシステムです。
技術ブログなので記事の大半はタイトルが「〇〇で□□してみた」の構成となるやってみた系の記事です。
〇〇や□□には具体的なサービス名や概念、動作が入っていることが多いです。
記事の内容は一切把握させていないです。

Lambdaでプログラムを実行しSSM Parameter Storeからブログ記事一覧を取得し、BedrockでEmbeddingします。
EmbeddingのモデルにはAmazonから提供される「Titan Embeddings Generation 1」を利用します。

プログラム

プログラムについてはこちらの記事を参考にさせていただきました。ありがとうございます。
結局、Embeddingって何者?

イベントに入力された内容と各ブログ記事のタイトルをベクトル化して、各記事で入力とのコサイン類似度を比較します。
以下のコードでは実行のたびにEmbeddingしているので、本来は別のところに結果をストアしておくと良いと思います。

import json
import botocore
import boto3
import sys
import numpy as np

region = "us-east-1"

def cos_similarity(a,b):
    return np.dot(a,b) / ((np.sqrt(np.dot(a,a))) * (np.sqrt(np.dot(b,b))))

def get_blog_titles():
    ssm_client = boto3.client('ssm', region_name=region)

    response = ssm_client.get_parameter(
        Name="blog_titles",
        WithDecryption=False
    )
    return response['Parameter']['Value'].split(',')

def get_embedding_vector(body):
    bedrock_runtime_client = boto3.client('bedrock-runtime', region_name=region)

    modelId = "amazon.titan-embed-text-v1"
    contentType = "application/json"
    accept = "*/*"

    requestbody = json.dumps({
        "inputText": body
    } )  

    vector = bedrock_runtime_client.invoke_model(
        modelId=modelId,
        contentType=contentType,
        accept=accept, 
        body=requestbody
    )

    return vector

def lambda_handler(event, context):

    if not 'question' in event:
        sys.exit(1)

    blog_titles = get_blog_titles()
    print(blog_titles)

    question_vector = json.loads(get_embedding_vector(event.get('question')).get('body').read()).get('embedding')
    similarities = {}

    for title in blog_titles:
        title_vector = json.loads(get_embedding_vector(title).get('body').read()).get('embedding')
        similarity = cos_similarity(question_vector,title_vector)
        similarities[title] = similarity

    similarity_ranking = dict(sorted(similarities.items(), key=lambda x:x[1], reverse=True))
    print(similarity_ranking)

    top_title = next(iter(similarity_ranking))

    if similarity_ranking.get(top_title) > 0.5:
        return "もっとも関連すると思われる記事はこちらです: " + top_title
    else:
        return "関連する記事はありませんでした"

最新のAWS SDKはLambdaレイヤーで導入してます。
リージョンは今回は北米にしましたがおそらく東京でも使えます。

検証

では、ちゃんと意味を見て記事を探し出してくれるか検証していきます。
以下の記事を狙い撃ちで入力を変えていきます。
GlueからJDBCでRDSに接続して書き込みしてみた

検証1 キーワードを含めて具体的に書いた場合

リクエスト内容は以下。

{
  "question": "GlueのJDBCでの接続方法"
}

結果。

もっとも関連すると思われる記事はこちらです: GlueからJDBCでRDSに接続して書き込みしてみた

ちゃんとほしい内容で返してくれましたね。

{'GlueからJDBCでRDSに接続して書き込みしてみた': 0.8286902789381606(ry)}

類似度も高いです。

検証2 キーワードぼかして書いた場合

ちょっと文言を変えてみます。

{
  "question": "Glueでデータベースにコネクション"
}

まぁまぁ同じ内容ですがキーワードをぼかして入力しました。

結果。

"もっとも関連すると思われる記事はこちらです: GlueからJDBCでRDSに接続して書き込みしてみた"

ええやん。

{
 'GlueからJDBCでRDSに接続して書き込みしてみた': 0.7977876979089453,
 'Glue Data Quality(プレビュー)を使ってData Catalogのテーブルが崩れていないか評価するステートマシンを作ってみた': 0.7505795458520156,
 'Glueジョブで一定期間を過ぎたデータのストレージクラスを移行してみた': 0.7122019213670158,
 'Glue Crawlerで作成した複数のテーブルにテーブルプロパティを一括で追加出来るCLIワンライナーを作成してみた': 0.6861097005065949,
 'AWS Glue ETL ジョブからのスキーマの更新、新規パーティションの追加する際のテーブルの更新範囲を検証してみた': 0.678379881095122,
 'GlueでMarketplaceのConnectorを使ってCloudwWatch Logsからログを取得してみた': 0.6762688941475452,
 'CloudFormationでGlueジョブの継続的なログ記録を設定してみた': 0.6689275385872873,
 'AthenaからJDBCでRDS(MySQL)に接続してクエリしてみた': 0.6539003619543496,
 'AWS GlueでExcelファイルをSparkDataFrameに変換してみた': 0.6455463752965447,
 'Aurora MySQLでリストア権限のあるDBを個別にダンプするスクリプト': 0.5932904496386749
 (ry)

Glueがキーワードになるので上位はGlueが占めています。
Glueの記事はそれなりにありますが一番良いものを選んでくれました。
他にもデータベース関連の記事がポイントが高めなのでちゃんと意味を理解していることが分かります。

検証3 抽象的に書いた場合

かなり抽象的にしてみます。

{
  "question": "AWSでデータ分析"
}

何をしたいのか詳しく聞いてみないと分からない内容になりましたね。

結果。

{'Amazon Kinesis Data FirehoseでApacheのアクセスログをJSON形式でS3にアップロードしてみた': 0.8184313523050883,
'【Lambda】Amazon Kinesis Data FirehoseのApacheログ出力を時間順にソートしてみた': 0.7779160288022091,
'AWS認定を全冠してみた': 0.768124223402185,
'【小ネタ】AWS WAFでレートベースルールで遮断したIPアドレスをCloudWatch Logs Insightsで調べる方法': 0.7649368943049988,
'AWS App Runnerでコンテナを自動デプロイしてみた': 0.7647949033459877,
'AWS IoT EventsでIoTデバイスの動作を監視してみた': 0.7580944388495815,
'AWS GlueでExcelファイルをSparkDataFrameに変換してみた': 0.7578875860484139,
'【小ネタ】 AWS Service Catalogでプロビジョニングされた製品のステータスが「汚染」になった': 0.7392715227586724,
'AWS IoTに1つの証明書で複数のAWSアカウントへの接続に対応してみた': 0.7356840184719408,
'AWS BackupでS3をリストアする際の動作をケースごとに調べてみた': 0.735147555584017,
(ry)

Kinesisはデータ分析系のサービスですが全体的にAWSの文言に引っ張られすぎてちょっと「そういうことじゃないんだなぁ」感が出る結果に……

検証4 関係無い内容を書いた場合

新卒1年目にふざけて書いていなければ無いはず。

{
  "question": "肉じゃがの玉ねぎをとろとろに煮込むコツ"
}

結果。

"関連する記事はありませんでした"

ばっちりですね。

{
'AWS App Runnerでコンテナを自動デプロイしてみた': 0.47382935341401594,
'AWS IoTにEasy-RSAで作った自己証明書で接続してみた': 0.47165200596358803,
'Glue Python ShellでAmazon Comprehendを用いてアンケートの結果に否定的なコメントが入っている場合に注意書きをするジョブを作ってみた': 0.46959091699853944,
'AWS認定を全冠してみた': 0.4676372593608963, 
(ry)

お料理要素はゼロです。正味、日本語であるだけでこれぐらいの類似度は出る気はします。

まぁ関係無い記事を紹介されることはなかったので良しとします。

まとめ

ぱっとお遊びで書いただけでもこれだけしっかり動くものになったので面白いですね。
使いやすいですし何より活用の幅が広くて何でも出来そうな気になりますね。
今後も使っていこうと思います。

以上、最後までお付き合いありがとうございました。

返信を残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA