Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

情報検索の国際学会 ICTIR2023 / SIGIR2023 に現地参加して来ました

こんにちは。
Acroquestのデータサイエンスチーム「YAMALEX」に所属する@shin0higuchiです😊
YAMALEXチームでは、コンペティションへの参加や自社製品開発、技術研究などに日々取り組んでいます。

さて、先日7/23-7/27にICTIR2023およびSIGIR2023という情報検索関連の国際会議が台北で開催されました。年に一度開かれる国際会議で、毎年この分野の研究を色々と聞くことができ、個人的にとても楽しみにしているイベントです。
ここ数年はCOVID-19の事情もありオンラインで参加していたのですが、今年は思い切って現地参加して来ましたので、その様子を共有したいと思います。
sigir.org

概要

会場は台北のTICC(Taipei International Conventional Center)という場所でした。台北101のすぐ近くにある広くてキレイなところです。


日程としては全5日間あったのですが、例年ICTIRとSIGIRが合同で開催されるような形になっており、今回はICTIRのセッションは初日がメインで残りはSIGIR、というイメージです。(厳密にいうともう少しややこしい気はしますが)

詳細

ICTIR

ICTIRのKeynote speakerは酒井哲也先生@早稲田大学で、「Evaluating Parrots and Sociopathic Liars」というタイトルでの発表でした。
(酒井先生は私の大学時代の恩師で、久しぶりにお会いできて嬉しかったです)

会話形式での検索やチャットボットの出力結果を評価するお話。
ざっくり言うと、ChatGPTのようなモデルのアウトプットの良し悪しをきちんと評価しようということです。
詳しくは公開されているスライドや論文をご覧いただければと思いますが、SWAN(Schematised Weighted Average Nugget score)という評価指標を提案しており、レスポンスのテキストをNuggetに分割し様々な観点でスコアをつけるような手法です。

アカデミックな立場での検索評価は非常に厳密で、実務で求められるレベルと隔たりがある状況は実態としてありつつも、
生成AIを活用する立場だからこそ、関連度だけでなく正当性や有害性など含めてきちんと評価する責任があると感じています。会の中での質疑でもありましたが、もちろんSWANで考慮されている評価軸全てを取り入れる必要があるというより、用途/ユースケース等に応じて必要な指標を吟味することが重要だと理解しました。
waseda.app.box.com
arxiv.org

SIGIR

SIGIRとしてのオープニングは2日目にあり、参加者数や論文の傾向などが紹介されました。
今回のSIGIRは800以上ものFullPaperがSubmitされ、これまでで最多でした。トピックとしては圧倒的にレコメンデーション関連の論文が多かったようです。
中国の方の論文数が圧倒的ですね。



今年はオンラインとオフラインのハイブリッド開催というカタチで、発表者も会場orオンラインという形式でした。
自分は、ジャンルとしてはレコメンダーに興味があったので、その分野の発表を中心に参加していました。


ランチのお弁当。昔中国に住んでいたせいもあってか、なんだか懐かしい味がしました。

また、ポスター発表もおこなわれていました。
発表者にその場で説明してもらったり質問したりできるのは現地参加のメリットとして大きかった印象です。
ポスターセッションを回る時間は多めに確保することができたので、ざっと全ポスターを見て回ることができました。
発表の様子は次の写真のような形です。近くで軽食やコーヒーが提供されていたので、ゆっくり休憩しながら発表を回れたのも良かったです。

3日目にあったBanquetは、グランドホテルというところでおこなわれ、豪華な料理をいただきつつライブパフォーマンスなどを楽しみました。



Banquetでは、他国の参加者の方と話しながら食事をしました。
これもオンライン参加だと得られない貴重な機会だと感じました。

最後に

今年はタイミング的に、論文の大半をLLMの内容が占めるということはまだありませんでしたが、
やはり会の中ではLLMのことを話している人が多く、情報検索分野も大きく影響を受けているのを感じました。今後さらに増えてくると思います。
アクロクエストではChatGPT関連のサービスを提供しているので、引き続きキャッチアップしたいと思います。

また、普段フルリモートで自宅から仕事をしているので、実際に海外の現地に行って空気を感じられたのは良かったです。
来年はワシントンで開催されるそうなので、もし可能であれば参加したいところです😊

Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

Kaggle Grandmasterと一緒に働きたエンジニアWanted! - Acroquest Technology株式会社のデータサイエンティストの採用 - Wantedlywww.wantedly.com

データサイエンスチームYAMALEXでGPT関連の発表をしてきました(ChatGPT Meetup/JJUGナイトセミナー)

こんにちは。
Acroquestのデータサイエンスチーム「YAMALEX」に所属する@shin0higuchiです😊
YAMALEXチームでは、コンペティションへの参加や自社製品開発、技術研究などに日々取り組んでいます。

先週5/23(火)、5/25(木)に、
佐々木(@Ssk1029Takashi )と 私でそれぞれGPT関連の発表をおこないました。
簡単に発表の概要をご紹介します。

JJUGナイトセミナー (5/23)

私はJJUG(日本Javaユーザー会)が主催する「JJUGナイトセミナー『AI × Java 祭』」で発表をおこないました。
jjug.doorkeeper.jp


JJUGナイトセミナーはオンラインで月に1回程度開催されており、Javaの最新情報や事例などが発表される勉強会です。
今回は「AI × Java 祭」ということで、主にAIによるコードアシストにフォーカスした内容となっていました。

私の発表内容としては、ChatGPTを提供しているOpenAIやそのAPIの概要を説明したうえで、実際の使い方の例を紹介しました。また、Java向けのクライアントを利用してAPIを呼び出すデモを実施しました。
タスク判定や文章要約など、様々なことに利用可能ですので、興味がある方は是非ご覧ください。

発表資料

speakerdeck.com

イベント動画

www.youtube.com

ChatGPT Meetup (5/25)

こちらはChatGPTのユーザーコミュニティによる勉強会で、GPT/ChatGPTについて、10以上のLT発表がありました。
GPT活用における様々なTipsが得られる濃い内容だったと思います。
佐々木は、「Semantic Kernelを使ってGPTと外部ツールを簡単に連携する」というテーマで発表をおこないました。

chatgpt.connpass.com

Semantic Kernelは、Microsoftが公開しているOSSで、LLMを活用したアプリケーションを開発するためのSDKとなっています。
コンテキストを保持することで連続した会話を扱ったり、スキルと呼ばれる単位でLLMに渡したいプロンプトを管理したりすることが可能です。
また、プランナーと呼ばれる機能によって、処理の実行計画を作り、ステップごとに処理を進めることも可能です。

アプリケーションに組み込むうえで非常に便利なソフトウェアと言えると思います。
興味があれば、是非発表資料をご覧ください。

発表資料

speakerdeck.com

まとめ

大規模言語モデルの発達が様々な領域でのイノベーションに繋がっており、今後も目が離せないですね。
引き続き最新情報へのキャッチアップ/発信をして行きたいと思います。

今回の記事は以上となります。お読みいただきありがとうございました。

Acroquest Technologyでは、キャリア採用を行っています。


  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

Azure OpenAI Serviceを活用したいエンジニア募集! - Acroquest Technology株式会社のデータサイエンティストの採用 - Wantedlywww.wantedly.com

Semantic Kernelを使ってGPTと外部ツールを簡単に連携してみる

こんにちは。最近湿度が上がってきてつらい@Ssk1029Takashiです。
最近当社ではAzure OpenAI Serviceを活用した検索ソリューションに取り組んでおり、私も開発として携わっています。
www.acroquest.co.jp

そんな中でもOpenAIのGPT周りのアップデートが激しく、GPT-4のリリースなどニュースに事欠きません。
特にChatGPT PluginsというChatGPTと外部のデータソースやツールなどを連携する枠組み発表され、よりChatGPTにできることが広がっています。
その中で先月MicrosoftがSemantic KernelというSDKを発表しました。

Semantic KernelとはGPT-3などの大規模言語モデルアプリ開発に統合するC#で開発されたOSSSDKです。
これを使うことで、ChatGPT PluginsのようにGPTと外部ツールを組み合わせたアプリが簡単に開発できるようになります。
今回は、このSemantic Kernelを使ってElasticsearch検索した結果の要約を出力するアプリを作ってみます。

Semantic Kernelとは

Semantic KernelとはMicrosoftが発表した大規模言語モデルを使って外部ツールと連携したアプリ開発ができるようになるSDKです。
詳しくは以下の記事で説明されています。
qiita.com

簡単に動作イメージの例を図にすると以下のようになります。

個人的にSemantic Kernelのうれしいポイントは以下の2つになります。

  1. Plannerという機能を使えばユーザーの入力に対してどのスキルをどの順番に実行するか分解できる
  2. スキルはテンプレートを使ってGPTに聞くものと自分で作ったプログラムを実行するものが定義できる

それぞれのポイントについて簡単に説明します。

Planner機能

一つ目のポイントについては、Semantic Kernelではスキルの組み合わせ方が主に2通りあります。

  1. プログラムとして事前にどの順番に実行するか実装しておいてそのロジック通りに実行する
  2. Planner機能を使ってプロンプトからどのスキルをどの順番に実行するかGPTに推測させる

例えばアプリに検索スキルと要約スキルが登録されている状態で、ユーザーが「Pythonについて検索した結果を要約して下さい」という入力をしたとします。
そうすると、Planner機能はこの入力から検索スキル→要約スキルという順番で実行すると結果を得られる、というように入力をタスクごとに分解して実行計画を作成します。

スキル拡張

2つ目のポイントについて、Semantic Kernelでは自分で作成できるスキルには2種類あります

  1. Semantic Function: テンプレートを使ってGPTを呼び出しGPTの出力を結果として返すスキル
  2. Native Function: 自分で作成した関数を実行して結果を返すスキル

これにより、要約などGPTを駆使したスキルを作ってもよいし、Elasticsearchを検索するなど外部ツールを活用したスキルも作れるようになります。

実際に作ってみる

ここまでSemantic Kernelについてポイントを説明したので実際に作ってみます。
冒頭で書いた通り、今回作るアプリはElasticsearch検索した結果を要約できるアプリなので、以下の手順が必要になります。

  1. 要約するSemantic Functionとキーワード抽出するSemantic Functionを作成する
  2. Elasticsearchに検索するNative Functionを作成する
  3. ユーザーの入力から適切なスキルを呼び出して結果を得るプログラムを作成する

それでは順番にやっていきましょう。

1. Semantic Functionを作る

まずは、Semantic Functionを作成します。
Semantic Functionの作成のために用意するのは以下の2ファイルになります。

  1. config.json
  2. skprompt.txt

例として、前段の出力を要約したものを出力するスキルを定義してみます。
まず、config.jsonは以下のような内容になっており、アプリの説明や、GPTへのパラメータ、スキルの引数などを定義します。

{
    "schema": 1,
    "type": "completion",
    "description": "Summarize given text or any text document",
    "completion": {
        "max_tokens": 1024,
        "temperature": 0.0,
        "top_p": 0.0,
        "presence_penalty": 0.0,
        "frequency_penalty": 0.0
    },
    "input": {
        "parameters": [
            {
                "name": "input",
                "description": "Text to summarize",
                "defaultValue": ""
            }
        ]
    }
}

特に注意が必要なのは各descriptionの内容になります。
これはPlannerで実行計画を出すときに、GPTがどのツールを使うかを出力するときの入力に使用されます。
なので、このdescriptionの記述が明確でなかったり、事実と異なるという場合には適切にPlannerが実行されません。

次に、skprompt.txtの内容は以下のようになっています。

Summarize input sentence in three lines in Japanese.

input={{$input}}
summary=

内容としてはGPTに入力するプロンプトをテンプレートとして書いています。
スキルの引数は{{$input}}の部分に挿入されます。

このようにSemantic Functionではスキル自体の定義とGPTへの入力テンプレートを定義することで作成します。

2. Native Functionを作る

次にプログラムを動作させるNative Functionを作成します。
Native Functionについても用意するものはシンプルで以下のようなC#ソースコードを用意するだけです。

using Microsoft.SemanticKernel.SkillDefinition;
using Microsoft.SemanticKernel.Orchestration;

namespace <スキルを配置するディレクトリ名に合わせる>;

public class <スキル名>
{
    [SKFunction("<作成するFunctionの説明>")]
    public string Search(string input)
    {
        <ここに実行するコードを書く>
    }
}

今回は入力された文字列でElasticsearchを検索して、検索結果を返すので、以下のように実装します。

using Microsoft.SemanticKernel.SkillDefinition;
using Microsoft.SemanticKernel.Orchestration;
using Elastic.Clients.Elasticsearch;
using Elastic.Transport;

namespace MySkillsDirectory;

public class MyRetrieverSkill
{
    private static ElasticsearchClientSettings settings = new ElasticsearchClientSettings(new Uri("xxx"))
            .Authentication(new BasicAuthentication("xxx", "xxx"));
    private ElasticsearchClient client = new ElasticsearchClient(settings);

    [SKFunction("Search for documents in Japanese relevant to your input and return 1 document")]
    public string Search(string input)
    {
        var response = client.Search<Blog>(s => s 
            .Index("target-document")
            .From(0)
            .Size(1)
            .Query(q => q
                .Match(c => c
                    .Field(b => b.Content)
                    .Query(input)
                ) 
            )
        );
        var document = response.Documents.First();
        string result = "title:" + document.Title + "\n" + "content:" + document.Content;
        return result.Substring(0,1500);
    }
}

public class Blog
{
    public string? Title { get; set; }
    public string? Content { get; set; }
}

今回Elasticsearchには当ブログの直近5記事分をtitleに記事タイトル、contentに本文を入れています。

{
  "title": "NFLのPlayer Contact Detectionで金メダル獲得&コンペ振り返り",
  "content": """
  皆さんこんにちは
機械学習チームYAMALEXの@tereka114です。
YAMALEXは Acroquest 社内で発足した、会社の未来の技術を創る、機械学習がメインテーマのデータサイエンスチームです。
~~~"""
}

作成したスキルを配置する

上記で作成したスキルはプロジェクト内のディレクトリに配置します。
以下のようなディレクトリ構成になります。

プロジェクトディレクトリ
├── MySkillsDirectory
│   ├── MyRetrieverSkill
│   │   └── SummaryDocument
│   │       ├── config.json
│   │       └── skprompt.txt
│   └── MyRetrieverSkill.cs

また、注意が必要なのはスキルを配置するディレクトリ名はNative Functionのコード中のnamespaceで定義した値に合わせましょう。
今回でいうと、MySkillsDirectoryとなっています。

実際に動かしてみる

ここまででスキルの作成はできたので実際に動かしてみましょう。

まずは、以下のコードでOpenAI APIを使用する準備とスキルのインポートを行います。

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.CoreSkills;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.KernelExtensions;

using MySkillsDirectory;

IKernel kernel = Microsoft.SemanticKernel.Kernel.Builder.Build();
kernel.Config.AddOpenAITextCompletionService(
    "davinci", "text-davinci-003", ""
);

// Plannerスキルを使用できるようにする。
var planner = kernel.ImportSkill(new PlannerSkill(kernel));

var skillsDirectory = Path.Combine(System.IO.Directory.GetCurrentDirectory(), "MySkillsDirectory");
// Semantic Functionをインポート
kernel.ImportSemanticSkillFromDirectory(skillsDirectory, "MyRetrieverSkill");
// Native Functionをインポート
kernel.ImportSkill(new MyRetrieverSkill(), "MyRetrieverSkill");

次にPlannerを使って入力プロンプトから実行計画を立ててみましょう。

var ask = "GPTをアシスタントとして使うための情報があるか検索して要約してください";
var originalPlan = await kernel.RunAsync(ask, planner["CreatePlan"]);

Console.WriteLine("Original plan:\n");
Console.WriteLine(originalPlan.Variables.ToPlan().PlanString);

すると以下のようにXMLで作成された実行計画が出力されます。

<goal>
GPTをアシスタントとして使うための情報があるか検索して要約してください
</goal>
<plan>
  <function.MyRetrieverSkill.Search input="GPTをアシスタントとして使うための情報" setContextVariable="SEARCH_RESULT"/>
  <function.MyRetrieverSkill.SummaryDocument input="$SEARCH_RESULT" setContextVariable="SUMMARY_RESULT"/>
  <function._GLOBAL_FUNCTIONS_.BucketOutputs input="$SUMMARY_RESULT" bucketCount="3" bucketLabelPrefix="Result" />
</plan>

想定通り、検索→要約で回答が得られるというように計画を立ててくれています。
さらに、検索スキルに入力される文字列が「GPTをアシスタントとして使うための情報」となっているので、ユーザーの入力から自動で抽出してくれているようです。

ためしに少し複雑な命令を入れてみましょう。
「GPTに関して、アシスタントとして使う方法と、出力が事実か確かめる方法についてそれぞれ検索して結果をまとめて要約してください」
この場合だと2回検索が走りそれぞれの結果をまとめて要約という形が想定されますが、Plannerの結果はどうなるでしょうか。

<goal>
GPTに関して、アシスタントとして使う方法と、出力が事実か確かめる方法についてそれぞれ検索して結果をまとめて要約してください
</goal>
<plan>
  <function.MyRetrieverSkill.Search input="GPT and how to use it as an assistant" setContextVariable="SEARCH_RESULT_1"/>
  <function.MyRetrieverSkill.Search input="GPT and how to verify the output is true" setContextVariable="SEARCH_RESULT_2"/>
  <function._GLOBAL_FUNCTIONS_.BucketOutputs input="$SEARCH_RESULT_1" bucketCount="1" bucketLabelPrefix="RESULT_1"/>
  <function._GLOBAL_FUNCTIONS_.BucketOutputs input="$SEARCH_RESULT_2" bucketCount="1" bucketLabelPrefix="RESULT_2"/>
  <function.MyRetrieverSkill.SummaryDocument input="$RESULT_1;$RESULT_2" appendToResult="RESULT__SUMMARY"/>
</plan>

検索のinputが英語になっていますが、実行計画としては期待通りになってそうです。
SummaryDocumentのinputで検索結果がまとめられていますね。すごい。。

Plannerで実行計画を立てた後はそれを順番に実行していくだけです。
以下のようにスキルの終了状態を見ながら順番に実行していきます。

var executionResults = originalPlan;
int step = 1;
int maxSteps = 10;
while (!executionResults.Variables.ToPlan().IsComplete && step < maxSteps)
{
    var results = await kernel.RunAsync(executionResults.Variables, planner["ExecutePlan"]);
    if (results.Variables.ToPlan().IsSuccessful)
    {
        Console.WriteLine($"Step {step} - Execution results:\n");
        Console.WriteLine(results.Variables.ToPlan().PlanString);

        if (results.Variables.ToPlan().IsComplete)
        {
            Console.WriteLine($"Step {step} - COMPLETE!");
            Console.WriteLine(results.Variables.ToPlan().Result);
            break;
        }
    }
    else
    {
        Console.WriteLine($"Step {step} - Execution failed:");
        Console.WriteLine(results.Variables.ToPlan().Result);
        break;
    }
    
    executionResults = results;
    step++;
    Console.WriteLine("");
}

上記で「GPTをアシスタントとして使うための情報があるか検索して要約してください」を通して実行してみると以下のような出力が得られます。

Original plan:

<goal>
GPTをアシスタントとして使うための情報があるか検索して要約してください
</goal>
<plan>
  <function.MyRetrieverSkill.Search input="GPTをアシスタントとして使うための情報" setContextVariable="SEARCH_RESULT"/>
  <function.MyRetrieverSkill.SummaryDocument input="$SEARCH_RESULT" setContextVariable="SUMMARY_RESULT"/>
  <function._GLOBAL_FUNCTIONS_.BucketOutputs input="$SUMMARY_RESULT" bucketCount="3" bucketLabelPrefix="Result" />
</plan>
Step 1 - Execution results:

<goal>
GPTをアシスタントとして使うための情報があるか検索して要約してください
</goal><plan>
  <function.MyRetrieverSkill.SummaryDocument input="$SEARCH_RESULT" setContextVariable="SUMMARY_RESULT" />
  <function._GLOBAL_FUNCTIONS_.BucketOutputs input="$SUMMARY_RESULT" bucketCount="3" bucketLabelPrefix="Result" />
</plan>

Step 2 - Execution results:

<goal>
GPTをアシスタントとして使うための情報があるか検索して要約してください
</goal><plan>
  <function._GLOBAL_FUNCTIONS_.BucketOutputs input="$SUMMARY_RESULT" bucketCount="3" bucketLabelPrefix="Result" />
</plan>

Step 3 - Execution results:

<goal>
GPTをアシスタントとして使うための情報があるか検索して要約してください
</goal><plan>
</plan>
Step 3 - COMPLETE!
Ssk1029TakashiはGPT-3を使ってAIアシスタントを作る第一歩を検証します。命令文からタスクの種類、時間情報、コンテンツ内容、誰がを抜き出す必要があります。GPT-3は事前学習の段階でInformation Extractionに近いタスクをすでに学習していることが推測されます。試してみると、素の言語モデルで解けてしまうことがわかりました。

Elasticsearchに入っているブログを検索してほしい結果を取得することができました。
順番に実行している様子もわかります。

まとめ

今回はMicrosoft発のSemantic Kernelを使って、ユーザーの入力を解釈して外部ツールと連携するアプリを作ってみました。
こちらがロジックを決めなくても入力をGPTが解釈してできるというのは夢が広がりますね。
スキルを作れば拡張もしやすいので様々な局面で使用できそうです。
一応、Pythonで書かれた機能を限定したPreviewのブランチもあるので、こちらも期待したいところです。
それではまた。

Acroquest Technologyでは、キャリア採用を行っています。


  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。


www.wantedly.com



Amazon CodeWhispererでどの程度コーディングが効率化できそうか試してみた

ここのところ気温も暖かくなり、外に出かけるのが楽しみになってきた、カメラ好き機械学習エンジニアの@yktm31です。

いま世間を賑わせている生成系AI、ChatGPTは私にとって欠かせないものになりました。
そんな中つい先日、AWSから「Amazon CodeWhisperer」がGAになりました。

といことで、さっそく試してみました。

目次

概要

Amazon CodeWhispererは、リアルタイムでAIによるコード提案をしてくれるサービスです。類似のサービスとしては「GitHub Copilot」があります。

以下のように、コードを書いている途中でもその先を予測し、候補を出してくれるというものです。

aws.amazon.com

特徴

CodeWhispererは、他のコード生成AIに比べ以下のような特徴があるようです。

  • AWSサービスに最適化されたコード提案
  • AWSサービスを利用するためのライブラリや、ベストプラクティスに則ったコードスニペットの提案など、書いたコメントやコードから提案される内容が、AWSサービス利用のために最適化されているようです。
  • セキュリティスキャン
  • この機能の対象はJavaJavaScriptPythonになります。
    OWASP Top 10にあたるセキュリティ脆弱性のあるコードや、ベストプラクティスに倣っていないコードなどを検出し、修正案も出してくれるようです。
  • ライセンス検知
  • AIが提案したコードが、どのようなライセンスのオープンソースを参照して生成されたものか、チェックできるようです。
    オープンソースのコードをそのライセンスのフラグとともに学習しているため、ライセンス違反していないかチェックすることができるようです。
  • SSOサポート
  • エンタープライズ向けに、SSO統合がサポートされているようです。

    サポート

    サポートされるプログラミング言語

    Amazon CodeWhispererで生成可能なプログラミング言語は15種類あるようです。

    その中でも、JavaPythonJavaScript、TypeScript、C#の5つが、学習データの質が高い(=より精度の高い提案をしてくれると想定できる)ようです。

    サポートされるIDE

    以下のIDEがサポートされているようです。

    • Visual Studio Code
    • すべてのJetBrains IDE
      • IDEによってAmazon CodeWhispererのふるまいが変わることはないようです
    • AWS Cloud9
    • AWS Lambda コンソールエディタ

    docs.aws.amazon.com

    サポートされる自然言語

    コード生成のためのコメントは、現時点では、英語以外は公式にはサポートしていないようです。 ただ、学習データに英語以外の言語が含まれているため、日本語を利用した場合でも、コードの候補が出てくることもあるようです。

    使い方

    利用開始方法

    AWSのサイトに動画で各IDEでの利用方法が説明されています。 VSCodeでの利用を試してみましたが、インストールは非常に簡単でした。

    VS Code拡張機能AWS Toolkit」をインストールします

    ②サイドパネルからAWS Toolkitのアイコンを選択し、CodeWhispererを開始します

    ③認証を求められるので認証方法を選び認証を通します

    AWS LambdaコンソールやAWS Cloud9で利用する際は、codewhisperer:GenerateRecommendationsのIAMポリシーをIAM userかroleにアタッチする必要があるようです。

    aws.amazon.com

    基本操作

    VS Codeでは、以下のような操作で行います。

    アクション キー操作
    コードを提案させる Alt + C
    別の候補を出す 矢印キー(←、→)
    提案されたコードを採用する Tabキー

    もちろん、コーディング中に自動で続きのコードの提案も出てくるのですが、 Alt + Cで明示的に提案させることができる点が、使い勝手がよいと感じました。

    VS Code拡張機能IntelliJ系のキーマップにするものを入れていると、上記の操作が上手くいかないことがあるので注意です。

    Lambdaで、DynamoDBのレコードを取得する処理と、そのユニットテストを書いてみた

    実際どんなコードを生成してくれるのか、まずは基本的な処理として、DynamoDBのレコードを取得するようなコードを書いてみました。
    言語はPythonを利用し、pytestとmotoを利用したユニットテストも生成してみました。

    結論として、100%そのまま動くわけではありませんでしたが、手直しは1~2割程度で、8割のコードは自動生成されたものが使えました。

    コメントを書いて「Ctrl+C」で提案されたコードを使い、手直しする際も、最初の数文字入力してから、「Ctrl+C」をすれば、続きの実装が提案され非常に楽にできました。 生産性は十分向上するレベルだと感じました。

    DynamoDBへのアクセスを担うRepositoryクラスの実装をしている様子を撮影してみました。

    以下に実装したコードの全体像を載せます。 CodeWhispererを使って、手直しつつ、5分かからずに書くことができました。 モックのDynamoDBテーブル作成など、正直面倒な部分も、さくっと一発作成されるので、かなり体験がよかったです。

    リポジトリクラスとLambda handler

    import boto3
    
    # Write a DynamodbRepository class for accessing DynamoDB.
    # Use boto3.client('dynamodb') to create a client.
    # get_item/put_item methods and error handling are required.
    
    class DynamodbRepository:
        def __init__(self, table_name):
            self.client = boto3.client('dynamodb', region_name='us-east-1')
            self.table_name = table_name
    
        def get_item(self, key):
            try:
                response = self.client.get_item(TableName=self.table_name, Key=key)
                return response['Item']
            except Exception as e:
                print(e)
                return None
    
        def put_item(self, item):
            try:
                response = self.client.put_item(TableName=self.table_name, Item=item)
                return response
            except Exception as e:
                print(e)
                return None
        
        # Write delete_item method here.
        def delete_item(self, key):
            try:
                response = self.client.delete_item(TableName=self.table_name, Key=key)
                return response
            except Exception as e:
                print(e)
                return None
    
    
    # Write a handler for get user item from DynamoDB using DynamodbRepository.
    # Trable name is 'users' and key is 'user_id'
    
    def get_user_item(event, context):
        table_name = 'users'
        dynamodb_repository = DynamodbRepository(table_name)
    
        key = {'user_id': {'S': event['pathParameters']['user_id']}}
        user_item = dynamodb_repository.get_item(key)
        if user_item is None:
            return {
                'statusCode': 404,
                'body': 'User not found'
            }
        else:
            return {
                'statusCode': 200,
                'body': user_item
            }
    

    テストコード

    import boto3
    from handler import DynamodbRepository, get_user_item
    
    from moto import mock_dynamodb
    
    # write a test for DynamodbRepository.get_item()
    # Table item schema is {user_id: str, name: str, age: int}
    
    @mock_dynamodb
    def test_get_item():
        client = boto3.client("dynamodb", region_name="us-east-1")
        client.create_table(
            TableName="users",
            KeySchema=[
                {"AttributeName": "user_id", "KeyType": "HASH"},
            ],
            AttributeDefinitions=[
                {"AttributeName": "user_id", "AttributeType": "S"},
            ],
            ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
        )
    
        # put item to table using boto3 client
        client.put_item(
            TableName="users",
            Item={
                "user_id": {"S": "1"},
                "name": {"S": "John"},
                "age": {"N": "20"}, 
            }
        )
    
        repo = DynamodbRepository('users')
        item = repo.get_item( key={'user_id': {'S': '1'}})
        
        # assert
        assert item["user_id"]['S'] == "1"
        assert item["name"]['S'] == "John"
        assert item["age"]['N'] == "20"
    
    
    # write test code for get_user_item handler using pytest. using @mock_dynamodb and prepare data.
    
    @mock_dynamodb
    def test_get_user_item():
        client = boto3.client("dynamodb", region_name="us-east-1")
        client.create_table(
            TableName="users",
            KeySchema=[
                {"AttributeName": "user_id", "KeyType": "HASH"},
            ],
            AttributeDefinitions=[
                {"AttributeName": "user_id", "AttributeType": "S"},
            ],
            ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
        )
    
        # put item to table using boto3 client
        client.put_item(
            TableName="users",
            Item={
                "user_id": {"S": "1"},
                "name": {"S": "John"},
                "age": {"N": "20"}, 
            }
        )
    
        # prepare event
        event = {
            "pathParameters": {
                "user_id": "1"
            }
        }
    
        # call handler
        response = get_user_item(event, None)
    
        # assert
        assert response["statusCode"] == 200
    

    実装として、もう少し丁寧に作りたい部分はありつつも、十分助けになるサポートになると感じました。 より使いこなすには、コメントを詳細に、より具体的するなど、指示の出し方のスキルが大事になりそうです。

    なお、DynamoDB操作に関しては、直近非推奨になってしまったboto3.resourceを使った実装が提案されることもありました。 これは、学習段階ではboto3.resourceが利用されたコードがあったのだと想像します。

    boto3.resourceに関するドキュメントは以下です。
    https://boto3.amazonaws.com/v1/documentation/api/latest/guide/resources.html

    コード参照(Code references)を試してみる

    CodeWhispererでコードを生成していると、VS Codeであれば、「CODEWHISPERER REFERENCE LOG」の部分に 生成したコードがどんなオープンソースのコードを参照したか教えてくれます。

    今回は、「Apache-2.0 license」のオープンソースのコードを参照しているようなので、問題なさそうです。

    この機能はデフォルトでONになっており、オプトアウト可能なものです。

    CodeWhispererを利用した結果、意図せずライセンス違反をする、なんてことを防げる便利な機能だと思いました。

    docs.aws.amazon.com

    セキュリティスキャンを試してみる

    試しに、セキュリティキーをハードコーディングしてみると、ちゃんと警告を出してくれます。 ただし、セキュリティスキャンは、明示的に「Run Security Scan」を実行する必要があります。(画像赤枠部分)

    次に、ベストプラクティスに沿っていない書き方をしてみます。

    AWSリソースを関数実行のたびに作成するのは非効率だと、警告が出ています。

    セキュリティスキャン機能を使うことで問題に気づいて対処すれば、問題も防げ、レビュイーの負担も減って嬉しいですね。

    使い方については、AWSYoutubeチャンネルには、PythonでDDDベースのサーバレスアプリをAmazon CodeWhispererで作るデモもあります。 この動画では、より大規模に開発をしているので、参考になりそうです。

    www.youtube.com

    また、どんな使い方があるのかも、ドキュメントで例が載っていました。 こちらも参考にできそうです。 docs.aws.amazon.com

    ドキュメントからわかったこと

    最後に、ドキュメント等を一通り読んでわかったことです。 実際に業務で利用する上で、確認しておきたいプランの違いやセキュリティについて、特に重要だと感じた部分をまとめてみました。

    安全性・セキュリティ

    この手のサービスで気になるのは、セキュリティや安全性だと思います。 ドキュメントやFAQsのページで説明されている内容で、特に重要と思った項目を挙げてみたいと思います。

    CloudTrail適用
    • Amazon CloudTrailを使用し、CodeWhispererに関連するアクティビティを記録することができます。
    • これにより、AWSインフラのコンプライアンス、セキュリティ、運用監査を確保できます。
    TLSサポート
    • CodeWhispererでは、TLSを使用してIDEAWSサービス間のデータを暗号化し、コードやデータのセキュリティと機密性を確保しています。
    データ収集
    • Professionalプランでは、CodeWhispererは、コードの提案を提供するためのコードスニペット、コメント、カーソル位置、IDEで開いているファイルの内容などのコンテンツ収集はしていないようです。
    • クライアント側のメトリクスやテレメトリーなどは、Individual・Professionalの両方でデータがAWSに送信されるようになっているようです。
    • メトリクス・テレメトリーは、オプトアウト可能です。オプトアウト方法は、後ほど触れたいと思います。
    著作権
    • Amazon CodeWhisperer のFAQsページでは、はっきりと、CodeWhispererで生成したコードを含め、開発者本人が所有することが明記されています。
    • FAQsの原文を引用すると、こう表現されています。「Just like with your IDE, you own the code that you write, including any code suggestions provided by CodeWhisperer. 」
    • 生成AIで作成したコードは誰が所有するのか?という問題は、しばしば議論される領域で、他のサービスではコードの所有が誰にあるのか明言していないものもあります。CodeWhispererがここで「you own the code」と明言しているのは、業務で利用するという点で、一つハードルをクリアしやすくなっていると思いました。
    収集されたコードの漏洩
    • Professionalでは、「No. Content processed by CodeWhisperer Professional, such as code snippets, comments, and contents from files open in the IDE, is not stored or used to train the model, and therefore will never be reproduced in a code suggestion for another user.」と書かれており、はっきりと否定されています。
    • Individualでは、「We have safeguards designed to prevent reproduction of unique private code collected from CodeWhisperer Individual users.」という説明がされています。
    • このことから、コード漏洩のリスクとしては、Professionalプランを利用する方が安全であると思われます。

    ProfessionalとIndividualの違い

    ユースケース
    • Professional利用は、組織や企業向けに設計されています。ソフトウェア開発プロジェクトに取り組むチームのニーズに対応する追加機能と構成を提供します。
    • Individual利用は、CodeWhispererの機能から恩恵を受けたいが、高度な機能や組織レベルの構成を必要としない個人の開発者や小規模なチーム向けに設計されています。
    データの収集と使用
    • Professionalでは、コードスニペット、コメント、IDEで開いているファイルの内容など、CodeWhispererが処理したコンテンツは保存されず、サービス向上のために使用されていないようです。
    • Individualでは、CodeWhispererは、コードスニペット、コメント、IDEで開いているファイルからのコンテンツなど、ユーザのコンテンツを保存し、サービスの品質を改善し開発するために使用されるようです。
    メトリクスの送信
    • ProfessionalとIndividualの両方で、サービス改善のためにクライアントサイドのメトリクスを収集し、使用されるようです。IDEの設定でオプトアウトすることができるようです。
    ユーザ管理
    • Professionalでは、AWS管理者はAWSマネジメントコンソールから組織レベルで設定を一元的に構成することができるようです。組織レベルで、特定のポリシーや設定を強制することができるようです。
    • Individualでは、ユーザーはIDE内で設定を調整することができますが、中央管理機能はありません。

    料金と制限

    IndividualとProfessionalの、料金・制限の面での違いをまとめました。

    機能 Individual Professional
    料金 無料 $19/ユーザー/月
    認証 AWS Builder ID AWS IAM Identity Center
    コード生成言語 全てのサポート言語 全てのサポート言語
    コード参照 (Code references) あり あり
    コードセキュリティスキャン $50/ユーザー/月 $500/ユーザー/月
    組織ライセンス管理 なし あり
    組織ポリシー管理 なし あり

    オプトアウト方法

    CodeWhisperer利用時にAWSに送信されるデータのオプトアウト方法についてみてみます。

    先に触れたように、Individualプランではコメント、IDEで開いているファイルの内容などのコンテンツがAWSに送信されます。 IDEのメトリクスやテレメトリーは、IndividualでもProfessionalでもAWSに送信されます。

    それぞれのプラン、IDEでのオプトアウト方法は、以下のドキュメントに記載がありました。

    docs.aws.amazon.com

    まとめ

    Amazon CodeWhispererを使用してみて、使い勝手の良さが非常によかったです。 特に、AWSサービスを利用する実装は、他のAIコード生成サービスよりも、CodeWhispererに軍配が上がる感触を持ちました。 セキュリティスキャン、ライセンス検知などの機能も便利で、コード品質向上のために実際的に役に立つと感じました。

    また、セキュリティ・安全性の観点から、業務での利用はProfessionalを利用することが望ましいように思います。 組織の要件に応じて、オプトアウトすることも忘れずに適用しておきたいところです。

    今回はPythonでLambda関数を書く例でしたが、CodeWhispererを活用すれば、CDKの実装やFargateで動かすAPIサーバーなんかも、楽につくれるようになるのではと思います。

    生成AI、万歳!
    それでは

    Acroquest Technologyでは、キャリア採用を行っています。
    • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
    • Elasticsearch等を使ったデータ収集/分析/可視化
    • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
    • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
      少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com

    セマンティック検索の活用で、Elasticsearchの検索が根本的に変わる!?

    こんにちは、@shin0higuchiです😊
    業務では、Elasticsearchに関するコンサルティングを担当しています。

    最近すっかり春らしく、暖かくなってきました。
    新年を迎えたばかりの感覚でしたが、あっという間に時が経ちますね。

    さて、今回の記事では、Elasticsearchの検索を根本的に変える可能性を秘めたセマンティック検索に関して書かせていただきます。

    概要

    Elasticsearchは元々、キーワードベースのアプローチを主に取っており、クエリで指定されたキーワードを対象のドキュメント内で検索し、それらの出現頻度や位置に基づいて結果をランク付けしています。この方法では、文脈や意図に関係なく、単純にキーワードの一致度に基づいて検索結果が返されます。

    一方、セマンティック検索とは、ユーザーのクエリの背後にある文脈と意図を理解しようとする検索手法で、キーワードだけに頼るのではなく、より正確で関連性のある結果を提供できます。このアプローチでは、自然言語処理NLP)や機械学習(ML)技術を活用して、単語や概念の関係やコンテンツの意味を理解し、検索クエリを最も適切な結果とよりよくマッチさせることができます。

    最近GPTなどを始めとするLLM(大規模言語モデル)の話題をよく耳にするかと思いますが、それに類する学習済みモデルの力をElasticsearchの検索に活かすことが出来る、というお話です。
    今回はGoogleが開発したBERTというモデルをベースにした学習済みモデルを実際に取り込んで利用します。

    Elasticsearch 8.7 で何が変わったか?

    実を言うとこれまでも、PyTorchの学習済みモデルをElasticsearchに取り込んで、一部のNLPタスクに利用することは可能でした。
    たとえば 過去にElasticsearch勉強会で発表させていただいた通り、質問応答のタスクなどにも対応しています。
    speakerdeck.com

    これまでセマンティック検索を活用しづらかった背景としては、「クエリを事前にベクトル化(embedding)してからElasticsearchに問い合わせる必要がある」という制約が挙げられます。
    つまり、せっかく機械学習モデルをElasticsearchに取り込んでも、一度外部でベクトル化をおこなってから検索しなければならなかったのです。
    Elasticsearch 8.7 では、検索のリクエストに文字列を渡してモデルを指定すると、検索と一緒にベクトル化もおこなってくれるようになりました。

    なお、日本語解釈の精度を課題と考えている方もいらっしゃると思いますが、取り込む学習済みモデルに依存するためここでは深く言及しません。また、今回は後述の通り日本語データで学習したモデルを利用することである程度検索精度を上げられる想定です。

    具体的な利用方法

    モデルの取り込み

    まずは下準備です。

    PyTorchモデルを Elandというライブラリを利用して取り込みます。
    github.com

    今回はHugging Faceで公開されている学習済みモデルを利用します。
    cl-tohoku/bert-base-japanese-v2 は日本語のWikipediaで学習したモデルですので、日本語の検索で効果を発揮してくれることを期待しましょう。

    eland_import_hub_model --url https://XX.XX.XX.XX:9200 --hub-model-id cl-tohoku/bert-base-japanese-v2 -u elastic --task-type text_embedding -p XXXX --ca-certs XXXX


    取込みが完了したモデルは、次のようにスタートのエンドポイントを叩くことで利用可能になります。

    POST _ml/trained_models/cl-tohoku__bert-base-japanese-v2/deployment/_start

    取り込んだモデルは、「Trained Models」のメニューから確認することができます。
    ※lang_ident_model_1 というモデルも表示されますが、こちらはデフォルトで入っているモデルであり、今回の操作とは無関係です。

    サンプルデータの登録

    今回は当ブログ、Taste of Tech Topicsの記事を取り込んでみます。
    ひとまずタイトルと本文を検索対象にしてみましょう。

    Elasticsearchでは、ingest pipeline という機能を利用することで、ドキュメント登録時にベクトル化処理をおこなうことが可能です。
    inference processor で、先ほど取り込んだモデルのID「cl-tohoku__bert-base-japanese-v2」を指定します。
    また、最終的には vector.content, vector.title といったフィールドに値を入れたいので、rename processorも利用しています。

    PUT _ingest/pipeline/text_embedding
    {
      "processors": [
        {
            "inference": {
              "target_field": "inference.content",
              "model_id": "cl-tohoku__bert-base-japanese-v2",
              "inference_config": {
                "text_embedding": {}
              },
              "field_map": {
                "content": "text_field"
              }
            }
          },
          {
            "rename": {
              "field": "inference.content.predicted_value",
              "target_field": "vector.content"
            }
          },
           {
            "inference": {
              "target_field": "inference.title",
              "model_id": "cl-tohoku__bert-base-japanese-v2",
              "inference_config": {
                "text_embedding": {}
              },
              "field_map": {
                "title": "text_field"
              }
            }
          },
          {
            "rename": {
              "field": "inference.title.predicted_value",
              "target_field": "vector.title"
            }
          }
      ]
    }
    


    次に、ElasticsearchのMapping(スキーマ)を定義しておきます。
    ポイントとしては、dense_vectorというデータ型でフィールドを定義する点です。
    今回は詳細な説明を省きますが、 indexパラメータをtrueにすること、similarityパラメータを指定することが必要になります。
    k-nearest neighbor (kNN) search | Elasticsearch Guide [8.7] | Elastic

    PUT blogs
    {
      "mappings": {
        "_source": {
          "excludes":["vector.title", "vector.content", "content"]
        }, 
        "properties": {
          "title": {
            "type": "text",
            "analyzer": "kurmoji"
          },
          "content": {
            "type": "text",
            "analyzer": "kurmoji"
          },
          "vector":{
            "properties": {
              "title": {
                "type": "dense_vector",
                "dims": 768,
                "index": true,
                "similarity": "l2_norm"
              },
              "content": {
                "type": "dense_vector",
                "dims": 768,
                "index": true,
                "similarity": "l2_norm"
              }
            }
          }
        }
      }
    }
    

    続いてドキュメントを登録します。
    今回は次のように、Bulkリクエストで登録をおこないます。先ほど登録したパイプラインを指定するようにしましょう。

    POST blogs/_bulk?pipeline=text_embedding
    {"index":{}}
    {"title": "XXXX", "content": "XXXX"}
    {"index":{}}
    {"title": "XXXX", "content": "XXXX"}
    ...

    検索してみる

    まず最初は、ベクトル検索を利用せずに「GPT3による質問応答システム」というクエリで全文検索を行ってみます。

    GET blogs/_search
    {
      "query": {
        "multi_match": {
          "query": "GPT3による質問応答システム",
          "fields": ["content", "title"],
          "operator": "or"
        }
      }
    }
    

    この際の検索結果は次のようになりました。

    順位 タイトル
    1 GPT-3を使って根拠付きで正確に質問応答してくれるシステムを作ってみる
    2 GPTが出した回答の確からしさを見えるようにしてみる
    3 第49回Elasticsearch勉強会で、ElasticsearchによるNLP(質問応答)の発表をしてきました
    4 コンテナ内のアプリが複数種類のログを出力する場合の収集方法
    5 GPT-3を使って自分だけのAIアシスタントを作る第一歩
    6 Azure Container Instancesを使ってAngular+FastAPIなWebアプリを動かしてみた
    7 Karateに性能試験とUI試験を任せてみる
    8 新しいデータ基盤アーキテクチャである「データレイクハウス」について調べてみた
    9 特徴量エンジニアリングのライブラリ xfeat を使ってみて便利だったこと
    10 NFLのPlayer Contact Detectionで金メダル獲得&コンペ振り返り


    この全文検索の結果では、「GPT3による質問応答システム」というクエリを形態素解析した結果でOR検索をおこなうため、GPT-3に関係ない質問応答関連の記事も上位に来てしまいます。
    たとえば「第49回Elasticsearch勉強会で、ElasticsearchによるNLP(質問応答)の発表をしてきました」という記事が3番目に来ていますが、この記事はGPT-3に関連が無く、望んだ検索結果ではありません。

    ここにセマンティック検索を追加することで検索結果が改善するかを見てみましょう。

    セマンティック検索を利用するには通常の search APIにknnパラメータを指定して利用する形になります。以下のようなリクエストになります。
    num_candidates の件数を各シャードで集めて、その中でスコアの高い k件を返却します。

    GET blogs/_search
    {
      "query": {
        "multi_match": {
          "query": "GPT3による質問応答システム",
          "fields": ["content", "title"],
          "operator": "or"
        }
      },
      "knn": [
        {
          "field": "vector.title",
          "k": 3,
          "num_candidates": "100",
          "query_vector_builder": {
            "text_embedding": {
              "model_id": "cl-tohoku__bert-base-japanese-v2",
              "model_text": "GPT3による質問応答システム"
            }
          }
        },
        {
          "field": "vector.content",
          "k": 3,
          "num_candidates": "100",
          "query_vector_builder": {
            "text_embedding": {
              "model_id": "cl-tohoku__bert-base-japanese-v2",
              "model_text": "GPT3による質問応答システム"
            }
          }
        }
      ]
    }
    


    以下のような結果が返ってきました。

    順位 タイトル
    1 GPT-3を使って根拠付きで正確に質問応答してくれるシステムを作ってみる
    2 GPTが出した回答の確からしさを見えるようにしてみる
    3 GPT-3を使って自分だけのAIアシスタントを作る第一歩
    4 第49回Elasticsearch勉強会で、ElasticsearchによるNLP(質問応答)の発表をしてきました
    5 Azure Container Instancesを使ってAngular+FastAPIなWebアプリを動かしてみた
    6 Karateに性能試験とUI試験を任せてみる
    7 NFLのPlayer Contact Detectionで金メダル獲得&コンペ振り返り
    8 新しいデータ基盤アーキテクチャである「データレイクハウス」について調べてみた
    9 Rustでトライ木による辞書検索のベンチマークをとってみた
    10 特徴量エンジニアリングのライブラリ xfeat を使ってみて便利だったこと


    初回の全文検索のみの検索結果と、knnによる検索を加えた後の検索結果を並べて比較してみましょう。
    先ほどよりもGPT-3関連の記事がより上位に来ており、検索結果としては意図したものに近くなった印象があります。

    順位 全文検索のみの場合 ベクトル検索を追加した場合
    1 GPT-3を使って根拠付きで正確に質問応答してくれるシステムを作ってみる GPT-3を使って根拠付きで正確に質問応答してくれるシステムを作ってみる
    2 GPTが出した回答の確からしさを見えるようにしてみる GPTが出した回答の確からしさを見えるようにしてみる
    3 第49回Elasticsearch勉強会で、ElasticsearchによるNLP(質問応答)の発表をしてきました GPT-3を使って自分だけのAIアシスタントを作る第一歩
    4 コンテナ内のアプリが複数種類のログを出力する場合の収集方法 第49回Elasticsearch勉強会で、ElasticsearchによるNLP(質問応答)の発表をしてきました
    5 GPT-3を使って自分だけのAIアシスタントを作る第一歩 Azure Container Instancesを使ってAngular+FastAPIなWebアプリを動かしてみた
    6 Azure Container Instancesを使ってAngular+FastAPIなWebアプリを動かしてみた Karateに性能試験とUI試験を任せてみる
    7 Karateに性能試験とUI試験を任せてみる NFLのPlayer Contact Detectionで金メダル獲得&コンペ振り返り
    8 新しいデータ基盤アーキテクチャである「データレイクハウス」について調べてみた 新しいデータ基盤アーキテクチャである「データレイクハウス」について調べてみた
    9 特徴量エンジニアリングのライブラリ xfeat を使ってみて便利だったこと Rustでトライ木による辞書検索のベンチマークをとってみた
    10 NFLのPlayer Contact Detectionで金メダル獲得&コンペ振り返り 特徴量エンジニアリングのライブラリ xfeat を使ってみて便利だったこと

    3位までにGPT関連の記事がランクインするようになりました。
    4位以降はある程度質問応答に関する内容が書いてあったり、下位の記事は「システム」といった一般的な単語によってスコアが上がっているものと思われます。
    簡単な検証ではありますが、8.7の新機能を利用することで、より意図したものに近い検索結果を得ることができるようになりました。

    最後に

    いかがだったでしょうか?
    今回ご紹介したセマンティック検索を用いれば、キーワードベースの検索だけではうまく類似度が取れないドキュメントも、上手くヒットさせることができるケースがありそうです。

    言語モデルの発達は近年すさまじいスピードで進んでいます。
    Elasticsearchの検索にも当たり前に取り込まれるようになる時代が来ていると思いますので、是非活用してみてください。
    今回の記事は以上となります。最後までお読みいただきありがとうございました。


    4/18追記:
    本記事に関して第53回のElasticsearch勉強会で発表させていただくことになりました。ご興味ございましたら是非ご参加ください。
    www.meetup.com


    Acroquest Technologyでは、キャリア採用を行っています。


    • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
    • Elasticsearch等を使ったデータ収集/分析/可視化
    • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
    • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

     
    少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

    世界初のElastic認定エンジニアと一緒に働きたい人Wanted! - Acroquest Technology株式会社のデータサイエンティストの採用 - Wantedlywww.wantedly.com

    Transformers4Recで簡単にセッションベースなレコメンデーションを試してみた

    アクロクエスアドベントカレンダー 12月14日 の記事です。
    こんにちは。最近テニス熱が再燃している@Ssk1029Takashiです。

    深層学習の界隈はここ最近はTransformerアーキテクチャが様々な分野で高い精度を出しています。
    そんな中NVIDIAから、Transformerを使ってセッションベースのレコメンドを学習できるTransformers4Recというライブラリがリリースされています。
    github.com
    簡単に精度が高いレコメンドが試せるライブラリとのことなので、チュートリアルをベースに試してみました。

    ブログの内容は以下になります。
    注意点として、ライブラリの使い方に主眼を置いているので、モデルの詳細な中身や前処理の具体的なコードの説明はこの記事では説明していません。

    セッションベースのレコメンデーションとは

    ライブラリの話に入る前に、そもそもセッションベースのレコメンデーションとは何かを説明します。

    ここでいうセッションとは、ユーザーがサイトの操作を開始して、何か商品を購入するまでにどの商品を見たかを指します。
    例えば、ECサイトで一つの服を買うまでに、帽子を見たり、違うブランドの服を見たりしてから、最終的には購入に至るというのが一般的なユーザーの行動かと思います。

    レコメンドでは一般的にはユーザーに焦点を当てて、行動履歴をとり、そのユーザー、もしくは属性が近いユーザーがよく見るものをレコメンドします。
    ただし、この手法は問題が指摘されており、ユーザーも時間が経てば見たいもの・買いたいものも全く変わるので、欲しくないアイテムがレコメンドされるケースも出てしまいます。
    例えば、テレビのような家電はそんなに頻繁に買わないのに、一回見ただけでやたらとおすすめされて煩わしいという経験がある人も多いかと思います。

    https://developer.nvidia.com/blog/transformers4rec-building-session-based-recommendations-with-an-nvidia-merlin-library/

    この問題を解決するために、セッションベースレコメンデーションでは、ユーザーの履歴ではなく、セッション内の行動によってのみ、次にレコメンドする商品を決めています。
    これにより、短期的にユーザーがどのようなものが欲しいかをより正確にできるようにするというのが利点になります。

    Transformers4Recとは

    NVIDIAから出ているTransformerベースのモデルでセッションベースのレコメンドを高精度で実現できるライブラリです。
    公式サイトの説明によると以下が主なうれしいポイントのようです。

    1. PyTorchとTF kerasでのカスタムモデル定義に対応しているので、どちらかしか使えないという人もモデルの拡張ができる
    2. HuggingFace上のTransformerモデル(XLNet、GPT-2など)をライブラリから利用できるため、複数モデルの検証が楽
    3. ライブラリの機能でNVIDIA Triton Serverというリアルタイム推論サーバーを簡単に立てられるので、デプロイが簡単

    より詳細には以下のページに書いてあるので、ぜひ読んでみてください。
    developer.nvidia.com

    実際に使ってみる

    Transformers4Recは以下のページにExampleが公開されているので、こちらに沿って試してみます。
    nvidia-merlin.github.io

    検証ではRecsys Challange 2015で使用されたyoochooseのデータセットを使いました
    データにはとある小売業者のECサイトでのユーザーがセッションごとのどの商品をクリックしたかという履歴が含まれています。

    具体的には以下のようにセッションID、日時、商品ID、商品カテゴリが含まれています。

    データ例

    実行環境の立ち上げ

    当記事で検証時の環境は以下になります

    項目 内容
    OS Ubuntu 20.04 LTX
    GPU RTX3090
    メモリ 128GB

    nvidia-dockerから以下のコマンドで実行用のコンテナを立ち上げます。

    docker run --gpus all  --name transformers4rec  -it -p 8888:8888 -p 8797:8787 -p 8796:8786 --ipc=host --cap-add SYS_NICE nvcr.io/nvidia/merlin/merlin-pytorch:22.11 /bin/bash
    

    そのあとコンテナ内でJupyter Labを立ち上げれば実行環境の構築は完了です。

    cd / ; jupyter-lab --allow-root --ip='0.0.0.0' --NotebookApp.token=''
    

    学習データ準備

    このままだと学習データとして利用できないので、前処理します。
    実行する内容は以下の流れになります。

    1. 同一セッション内で同一商品が連続しているデータをまとめる
    2. セッションID・日時でソートする
    3. 各履歴ごとに特徴量を計算して置き換える

    計算する特徴量は以下の2つになります。

    • 週のうち何日目に発生したイベントかを正規化した値(et_dayofweek_sin-list_seq)
    • どれくらい新しい新しい商品か(product_recency_days_log_norm-list_seq)

    データ処理の実装自体は以下のページに乗っているので、重要な箇所以外はこの記事では省略するので参照してください。
    nvidia-merlin.github.io

    最終的にデータの形式は以下のようにセッションごとに一行のデータになります。

    データ変換後のテーブル

    ここまで実行した前処理は以下のコードでNVTablarの機能で処理内容を保存することができます。

    workflow.save('workflow_etl')
    

    今回はあくまで検証用なので、使用する日付のデータを区切って、学習・評価・テスト用にデータを分割します。

    from transformers4rec.data.preprocessing import save_time_based_splits
    sessions_gdf = sessions_gdf[sessions_gdf.day_index>=178]
    save_time_based_splits(data=nvt.Dataset(sessions_gdf),
                           output_dir= "./preproc_sessions_by_day",
                           partition_col='day_index',
                           timestamp_col='session_id', 
                          )
    

    上記を実行すると以下のように日付と学習・評価・テストでデータを分けてparquet形式で保存されます。

    preproc_sessions_by_day/
        179/
            valid.parquet
            test.parquet
            train.parquet
        182/
            valid.parquet
            test.parquet
            train.parquet
        181/
            valid.parquet
            test.parquet
            train.parquet
        178/
            valid.parquet
            test.parquet
            train.parquet
        180/
            valid.parquet
            test.parquet
            train.parquet

    学習

    ここまででデータはそろえられたので、学習を実行しましょう。

    以下のコードでインプットのデータ形式を定義します。

    from merlin_standard_lib import Schema
    
    SCHEMA_PATH = "schema_demo.pb"
    schema = Schema().from_proto_text(SCHEMA_PATH)
    schema = schema.select_by_name(
       ['item_id-list_seq', 'category-list_seq', 'product_recency_days_log_norm-list_seq', 'et_dayofweek_sin-list_seq']
    )
    

    上記のコードで出てくるschema_demo.pbというのは、protocol buffers形式で以下の内容を記載したファイルになります。
    内容としては各カラムごとのカラム名データ形式を定義しています。

    feature {
      name: "session_id"
      type: INT
      int_domain {
        name: "session_id"
        min: 1
        max: 9249733 
        is_categorical: false
      }
      annotation {
        tag: "groupby_col"
      }
    }
    feature {
      name: "item_id-list_seq"
      value_count {
        min: 2
        max: 185
      }
      type: INT
      int_domain {
        name: "item_id/list"
        min: 1
        max: 52742
        is_categorical: true
      }
      annotation {
        tag: "item_id"
        tag: "list"
        tag: "categorical"
        tag: "item"
      }
    }
    feature {
      name: "category-list_seq"
      value_count {
        min: 2
        max: 185
      }
      type: INT
      int_domain {
        name: "category-list_seq"
        min: 1
        max: 337
        is_categorical: true
      }
      annotation {
        tag: "list"
        tag: "categorical"
        tag: "item"
      }
    }
    feature {
      name: "product_recency_days_log_norm-list_seq"
      value_count {
        min: 2
        max: 185
      }
      type: FLOAT
      float_domain {
        name: "product_recency_days_log_norm-list_seq"
        min: -2.9177291
        max: 1.5231701
      }
      annotation {
        tag: "continuous"
        tag: "list"
      }
    }
    feature {
      name: "et_dayofweek_sin-list_seq"
      value_count {
        min: 2
        max: 185
      }
      type: FLOAT
      float_domain {
        name: "et_dayofweek_sin-list_seq"
        min: 0.7421683
        max: 0.9995285
      }
      annotation {
        tag: "continuous"
        tag: "time"
        tag: "list"
      }
    }

    インプットを定義したら、次は学習するモデルを定義します。

    from transformers4rec import torch as tr
    
    max_sequence_length, d_model = 20, 320
    # Define input module to process tabular input-features and to prepare masked inputs
    input_module = tr.TabularSequenceFeatures.from_schema(
        schema,
        max_sequence_length=max_sequence_length,
        continuous_projection=64,
        aggregation="concat",
        d_output=d_model,
        masking="mlm",
    )
    
    # Define Next item prediction-task 
    prediction_task = tr.NextItemPredictionTask(hf_format=True, weight_tying=True)
    
    # Define the config of the XLNet Transformer architecture
    transformer_config = tr.XLNetConfig.build(
        d_model=d_model, n_head=8, n_layer=2, total_seq_length=max_sequence_length
    )
    
    # Get the end-to-end model 
    model = transformer_config.to_torch_model(input_module, prediction_task)
    

    ここで注意なのは、公式exampleのnotebookにはtr.NextItemPredictionTaskメソッドの引数にhf_format=Trueは定義されていませんが、この定義がないとエラーになります。
    どうやら最新版のnvidia-merlinのDockerイメージを使用すると発生する問題のようですが、はまってしまいました。。

    後は学習のハイパーパラメータなどの設定をして、学習・評価します。

    recsys_trainer = tr.Trainer(
        model=model,
        args=training_args,
        schema=schema,
        compute_metrics=True)
    
    from transformers4rec.torch.utils.examples_utils import fit_and_evaluate
    OT_results = fit_and_evaluate(recsys_trainer, start_time_index=178, end_time_index=180, input_dir='./preproc_sessions_by_day')
    

    こちらの処理が環境すると以下のように、評価結果が分かります。

    {'indexed_by_time_eval_/next-item/avg_precision@10': [0.08110713958740234,
      0.06632552295923233,
      0.13616926968097687],
     'indexed_by_time_eval_/next-item/avg_precision@20': [0.08427499234676361,
      0.0700748860836029,
      0.143798828125],
     'indexed_by_time_eval_/next-item/ndcg@10': [0.10763730853796005,
      0.08923665434122086,
      0.17738889157772064],
     'indexed_by_time_eval_/next-item/ndcg@20': [0.11937256157398224,
      0.10299879312515259,
      0.20573727786540985],
     'indexed_by_time_eval_/next-item/recall@10': [0.19306358695030212,
      0.1627039611339569,
      0.30797773599624634],
     'indexed_by_time_eval_/next-item/recall@20': [0.23969171941280365,
      0.21724942326545715,
      0.4220779240131378]}

    今回はデータを絞ったのもあり精度としては伸びしろがありますが、それでもrecall@10で0.2付近が出ているのはさすがですね。

    ここまで出来たら、作成したモデルと前処理のworkflowをまとめて保存します。
    ここで保存したモデルとworkflowは推論時に使用します。

    from nvtabular.inference.triton import export_pytorch_ensemble
    from nvtabular.workflow import Workflow
    workflow = Workflow.load("workflow_etl")
    
    export_pytorch_ensemble(
        model,
        workflow,
        sparse_max=recsys_trainer.get_train_dataloader().dataset.sparse_max,
        name= "t4r_pytorch",
        model_path= "/workspace/TF4Rec/models/",
        label_columns =[],
    )
    

    推論

    推論にはNVIDIAのライブラリであるNVIDIA triton inference serverを使用します。
    詳細な説明は省略しますが、フレームワークを問わず学習済みモデルをロードして、gRPC・HTTPリクエスト経由で推論ができるライブラリです。
    詳細には以下のページを参照してください。
    developer.nvidia.com

    推論用サーバーを立ち上げる

    まずは推論用のtriton serverを立ち上げます。
    今Dockerコンテナを起動しているシェルとは別のシェルを開いたのち、以下の手順で今回のコードを実行しているコンテナの中からtriton severを立ち上げます。

    docker exec -it transformers4rec /bin/bash
    tritonserver --model-repository=/workspace/TF4Rec/models/ --model-control-mode=explicit
    
    推論を実行する

    まずは、立ち上げているtriton serverに接続するためのクライアントを作成します。

    import tritonhttpclient
    try:
        triton_client = tritonhttpclient.InferenceServerClient(url="localhost:8000", verbose=True)
        print("client created.")
    except Exception as e:
        print("channel creation failed: " + str(e))
    triton_client.is_server_live()
    

    クライアントを作成した後は、triton serverにモデルを学習モデルを読み込むようにリクエストを送信ます。

    triton_client.load_model(model_name="t4r_pytorch")
    triton_client.get_model_repository_index()
    

    モデルを読み込めたら、テスト用のデータを送信して推論してみます。
    ここで、ポイントなのは推論時にクライアント側で前処理の必要がないということです。
    先ほど、モデルを保存するときに一緒に前処理のworkflowも保存しているので、サーバー側で前処理から推論までend-to-endで実行してくれます。

    import pandas as pd
    interactions_merged_df = pd.read_parquet("/workspace/data/interactions_merged_df.parquet")
    interactions_merged_df = interactions_merged_df.sort_values('timestamp')
    batch = interactions_merged_df[-50:]
    sessions_to_use = batch.session_id.value_counts()
    filtered_batch = batch[batch.session_id.isin(sessions_to_use[sessions_to_use.values>1].index.values)]
    
    import nvtabular.inference.triton as nvt_triton
    import tritonclient.grpc as grpcclient
    
    inputs = nvt_triton.convert_df_to_triton_input(filtered_batch.columns, filtered_batch, grpcclient.InferInput)
    
    output_names = ["output"]
    
    outputs = []
    for col in output_names:
        outputs.append(grpcclient.InferRequestedOutput(col))
        
    MODEL_NAME_NVT = "t4r_pytorch"
    
    with grpcclient.InferenceServerClient("localhost:8001") as client:
        response = client.infer(MODEL_NAME_NVT, inputs)
        print(col, ':\n', response.as_numpy(col))
    

    この状態で出力される値は、セッションごとの各アイテムの推薦度のlogitsが出力されているので、以下のコードでitem_idに直した形で出力します。

    from transformers4rec.torch.utils.examples_utils import visualize_response
    visualize_response(filtered_batch, response, top_k=5, session_col='session_id')
    

    以下の出力が得られればOKです。

    - Top-5 predictions for session `11457123`: 3170 || 429 || 1301 || 70 || 2909
    
    - Top-5 predictions for session `11467406`: 475 || 1216 || 1085 || 1672 || 597
    
    - Top-5 predictions for session `11528554`: 999 || 166 || 1672 || 1157 || 33
    
    - Top-5 predictions for session `11336059`: 1672 || 206 || 184 || 1157 || 33
    
    - Top-5 predictions for session `11445777`: 1672 || 289 || 597 || 2707 || 33
    
    - Top-5 predictions for session `11493827`: 206 || 30 || 61 || 2 || 69
    
    - Top-5 predictions for session `11425751`: 800 || 1157 || 166 || 302 || 429
    
    - Top-5 predictions for session `11399751`: 475 || 1672 || 33 || 597 || 1216
    
    - Top-5 predictions for session `11311424`: 27 || 70 || 33 || 1348 || 3170
    
    - Top-5 predictions for session `11257991`: 2034 || 997 || 800 || 429 || 1157
    
    - Top-5 predictions for session `11561822`: 555 || 423 || 1672 || 3225 || 33
    
    - Top-5 predictions for session `11421333`: 800 || 445 || 1157 || 2034 || 429
    
    - Top-5 predictions for session `11270119`: 1569 || 61 || 597 || 1672 || 1216
    
    - Top-5 predictions for session `11401481`: 1672 || 61 || 1219 || 848 || 423
    
    - Top-5 predictions for session `11394056`: 206 || 61 || 69 || 2 || 313

    このように推論対象にした各セッションごとに次にお勧めするTop5を得ることができました。

    まとめ

    この記事ではexampleを題材にして、Transformers4Recの大まかな使い方を見ていきました。
    使ってみてよかった点は以下になります。

    1. モデルの定義が標準を使う分には楽
    2. 学習時に実施した前処理を保存して、推論時に前処理→推論を推論サーバー上でend-to-endで実行できるのは手間が減って楽

    というのが特によかったです。
    ただ、ドキュメントがまだ整備され切っていないところもあるので、カスタマイズが必要になると手探りになる部分もあります。

    なので、まず簡単にセッションベースのレコメンデーションを試すときにはTransformers4Recを使ってみるのもよさそうです。
    それではまた。

    Acroquest Technologyでは、キャリア採用を行っています。


    • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
    • Elasticsearch等を使ったデータ収集/分析/可視化
    • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
    • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
     
    少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。


    www.wantedly.com



    Pandasのメモリ削減方法を整理した

    皆さんこんにちは
    機械学習チーム YAMALEXチームの@tereka114です。最近、寒いので、鍋を中心に食べて生きています。
    検証段階でも、規模の大きなデータを扱う機会が増えてきて、Pandasのメモリ消費量が厳しいと感じてきたので、その削減や効率化のテクニックまとめたいと思いました。
    有名なものからマイナーなものまで、思いつく限り書いてみます。

    そもそもなぜ、Pandasのメモリ削減技術が必要なのか

    Pandasで扱うデータの多くのファイルはCSV,Parquet, JSON(JSONL)になります。
    これらのファイルは扱いやすいこともあり、頻繁に利用されています。
    しかし、特にログデータを扱う場合、10GBを超えるなどファイルの容量が膨らみ、所謂、普通のマシンで解析する場合、メモリに乗り切らずに実装が困難になります。

    準備

    まずは、データを準備します。
    次のコードで実装します。1000万レコードほどのデータを作成します。

    import pandas as pd
    import numpy as np
    
    N = 10000000
    df = pd.DataFrame({
        "id": [i for i in range(N)],
        "user_id": np.array(["user_0", "user_1", "user_2", "user_3","user_4", "user_5"])[np.random.randint(0,6, N)],
        "category_id": np.random.randint(0, 10, N),
        "use": np.array([True, False])[np.random.randint(0,2, N)],
        "sales": np.random.randint(0, 1000000, N),
        "rate": np.random.rand(N),
    })
    df.to_csv("./sample_data.csv", index=False)
    

    この結果を次の方法で読み込み、メモリを計測します。
    計測結果は391MBの消費量ですので、ここからメモリを削減するといったことを試みます。

    df = pd.read_csv("./sample_data.csv")
    df.memory_usage().sum() / 1024**2 #  391.006591796875
    

    Pandasのメモリ削減

    先程準備したファイルを元にPandasのメモリを削減しながら処理をする方式を紹介します。

    1. 型修正

    Pandasで読み込んだままのメモリだと、int64やfloat64が最初に使われるので効率が悪いです。
    そのため、最大、最小の値を利用して、int8やfloat32など、よりメモリを消費しない型に変換することも一つの手段です。

    この削減は非常に有名な実装があるので、貼っておきます。この実装を適用することで133MBまで消費量が削減されます。
    この実装の内部では各カラムの値の最大、最小を計算し、データの精度を落とさず、最もメモリを消費できる型変換をかけます。
    ただし、booleanの自動変換はできないので、自分で、.astype(bool)などで変換しましょう。

    def reduce_mem_usage(df):
        """ iterate through all the columns of a dataframe and modify the data type
            to reduce memory usage.        
        """
        start_mem = df.memory_usage().sum() / 1024**2
        print('Memory usage of dataframe is {:.2f} MB'.format(start_mem))
        
        for col in df.columns:
            col_type = df[col].dtype
            
            if col_type != object:
                c_min = df[col].min()
                c_max = df[col].max()
                if str(col_type)[:3] == 'int':
                    if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
                        df[col] = df[col].astype(np.int8)
                    elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
                        df[col] = df[col].astype(np.int16)
                    elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
                        df[col] = df[col].astype(np.int32)
                    elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
                        df[col] = df[col].astype(np.int64)  
                else:
                    if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
                        df[col] = df[col].astype(np.float16)
                    elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
                        df[col] = df[col].astype(np.float32)
                    else:
                        df[col] = df[col].astype(np.float64)
            else:
                df[col] = df[col].astype('category')
    
        end_mem = df.memory_usage().sum() / 1024**2
        print('Memory usage after optimization is: {:.2f} MB'.format(end_mem))
        print('Decreased by {:.1f}%'.format(100 * (start_mem - end_mem) / start_mem))
        
        return df
    
    df.memory_usage().sum() / 1024**2 #133.51
    

    2. 逐次読み込み

    マシンによってはそもそも巨大なデータは読み込めないことがあります。
    Pandasには、巨大データでも読み込むために逐次ファイルから読み込む機能があります。
    前述の型修正と組み合わせることで、メモリの削減をしつつ、全てのファイルを読み込めます。

    reader = pd.read_csv("./sample_data.csv", chunksize=1000000)
    concat_df = pd.concat([reduce_mem_usage(part_df) for part_df in reader])
    concat_df.memory_usage().sum() / 1024**2 #133.51
    

    3. 読み込み時の型指定

    2の別解です。ファイルから分割して読めるといってもやはり結合は面倒です。
    ファイル読み込み時にdtypeを指定する方法があります。予め型わかっている場合はこちらを利用した方が効率的です。

    df = pd.read_csv("./sample_data.csv", dtype={
        "id": "int32",
        "user_id": "category",
        "category_id": "int8",
        "use": "bool",
        "sales": "int32",
        "rate": "float16"
    })
    df.memory_usage().sum() / 1024**2 # 123.97
    

    4. 逐次読み込み&集約

    メモリに載りきらない膨大なデータはログや行動記録であることも多いです。
    ログは、例えば、ユーザごとに集約すれば、ユーザ分のみのデータを作成できます。
    実際の用途として、特徴量を作成する際に分割統治法の考え方を用いて集約すれば、メモリを節約できます。

    user_idを元にsales, rateを合計集約するようなケースを考えましょう。
    その場合、読み込んだチャンクごとに集約を行い、最後に部分ごとの合計を合算しても、結果はまとめて計算するのと変わらないので次の実装で計算ができます。
    ただし、全ての場合(NG例:平均の計算)で可能ではないので、計算が正しくなるか検証してからにしましょう。

    reader = pd.read_csv("./sample_data.csv", chunksize=1000000)
    concat_df = pd.concat([part_df.groupby("user_id")["rate", "sales"].sum().reset_index() for part_df in reader])
    concat_df.groupby("user_id")["rate", "sales"].sum()
    

    5. 不要なものを読み込まない

    そもそも全てのカラムが必要ないケースもあります。
    例えば、user_idとsalesで、user_idごとのsalesの平均を計算する場合を考えます。
    その場合、user_id, sales以外のカラムは不要になるので、そもそも読み込む必要はありません。

    part_df = pd.read_csv("./sample_data.csv", usecols=['user_id', 'sales'])
    part_df = reduce_mem_usage(part_df)
    part_df.groupby("user_id")["sales"].mean()
    concat_df.memory_usage().sum() / 1024**2 #48.684
    

    6. 不要なカラム/DataFrameを消す

    プログラムの最後まで全てのカラムが必要なケースは少ないと思います。
    そのため、基本ではありますが、不要になったカラムやDataFrameは消しましょう。

    # use列消去
    del df["use"] 
    df.memory_usage().sum() / 1024**2 # 113.44
    
    # dfそのものを消す
    del df
    import gc
    gc.collect()
    

    番外編:そもそもPandasを利用しない

    ここまで、Pandasのメモリ削減の話をしていましたが、そもそもPandasを利用しないといった方法があります。
    最近だとPolarsと呼ばれるPandasとはAPIは異なりますが、所謂DataFrame系の構造操作ができるライブラリがあります。
    Pandasと比較して、高速、メモリが省メモリ、並列処理など様々な恩恵があり、データによってはPolarsを利用するほうが良いかもしれません。

    www.pola.rs

    最後に

    本日はPandasで巨大なデータを扱うためのメモリ削減の方法に関して紹介しました。
    有名なものから、自分なりの工夫も紹介してみましたのでぜひ使って良いPandasライフをお過ごしください。
    では、また年明けたら会いましょう。

    Acroquest Technologyでは、キャリア採用を行っています。


    • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
    • Elasticsearch等を使ったデータ収集/分析/可視化
    • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
    • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
     
    少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。


    www.wantedly.com