Taste of Tech Topics

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

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



Elastic のエンドポイントセキュリティ(Elastic Defend)でできることを整理してみた

この記事は Elastic Stack (Elasticsearch) Advent Calendar 12日目の記事です。

こんにちは、@shin0higuchiです😊
近頃めっきり寒くなりましたが皆さまいかがお過ごしでしょうか。私は毎日暖房の効いた自室でテレワークライフを送っています。自宅最高です。

さて、今回はElastic Security の機能である、Endpoint Securityについて紹介しようと思います。
端末にElastic Agentをインストールすることでウィルス対策ソフトとして動作するとともに、ログをElasticsearchに集約してアラートや分析を行うことが可能です。
Elastic Agent に Elastic Defend Integration をインストールすることで利用できる機能となります。
※今までは Endpoint Security Integrationでしたが、最近 Elastic Defend という名称に変更されたようです。
www.elastic.co

この記事ではインストールから不審な挙動の検知、検知後の対応までの流れを簡単にご紹介します。

インストール/設定手順

Kibanaにアクセスし、Securityのメニューから設定を開始することができます。
この記事では、Elastic Agentを管理するための Fleet Server が事前にローカルマシンにインストールされている前提で話を進めます。
Fleet Server のセットアップについては、Set up Fleet Server | Fleet and Elastic Agent Guide [8.5] | Elastic をご覧ください。

Elastic Defendでは、収集対象を柔軟に設定できますが、簡単に設定できるようにいくつかのプリセットが用意されています。
大別すると通常のクライアントにインストールする Traditional Endpoint presets と Cloud Workloads presets の2種類があり、それぞれで集める対象のデータセットにいくつかのプリセットがあります。
今回はNext Generation Antivirususを選ぶことにします(設定方法は後述)。


KibanaのFleet Server Policy を開き、「Add Integration」のボタンからElastic Defend Integrationを追加してみましょう。
以下のキャプチャのように、設定入力画面まで進みます。



設定画面に、先ほど説明したプリセット設定を選ぶ箇所があるので、Next Generation Antivirusus(NGAV)を選択します。
また、そのすぐ下にあるAgent Policyの箇所は「Fleet Server Policy」を選んでください。

Fleet Server Policy に、Elastic Defend の Integration が追加されていることが分かります。

Agent名をクリックすると、設定の確認・変更をおこなうことができます。
今回はNGAVプリセットであらかじめ適切に設定されているので変更はしませんが、マルウェア防御の設定やデータ収集の設定が入っていることが確認できますね。ちなみに、マルウェア防御の機能は、不審な挙動を検知した際に通知だけをおこなう「Detect」と、実行を止めるところまでおこなう「Prevent」の設定があります。



不審な挙動の検知/ブロック(Detection)

試しに不審なプログラムを実行しようすると、キャプチャ画像のようなポップアップが表示され、プログラムが実行されないようにブロックしてくれていることがわかります。
また、ブロックリストとしてルールベースの禁止アプリケーションを指定したり、信頼されたアプリケーションを明示的に指定したりすることも可能です。

ブロックした内容は、KibanaのAlert画面で確認することが可能です。

アラートの詳細を見て分析をおこなうためのビューも非常に充実しているのですが、この記事では割愛します。

検知後の対応(Response)

Host Isolation

マルウェア感染の疑いがある端末が見つかった場合、ネットワークから当該端末を切断することが可能です。Endpoint一覧画面の「Actions」から「Isolate host」を実行します。

この操作によりネットワークから切断されるので、他の端末に感染が広がるリスクを抑えることが可能です。
なお、Kibanaのプラットフォームとの通信はブロックされないので、後述のRespond機能を利用して調査・対処をおこない、対処が完了したのちは「Actions」から「Release host」を実行することでネットワークに復帰させることができます。

Response console

上記のHost Isolation中に、当該端末のプロセス一覧を取得したり、プロセスをKillしたりすることも可能です。
Endpoint一覧画面の「Actions」から「Respond」を選択すると専用のコンソールに遷移します。

isolate, release, status, processes 等のコマンドを利用して操作することが可能です。
詳しくはhttps://www.elastic.co/guide/en/security/8.5/response-actions.htmlを参照してください。
※この機能を利用するにはEnterpriseサブスクリプションが必要となります。

まとめ

いかがだったでしょうか。
Elastic Agent の Elastic Defend Integration を利用することで、マルウェア検知や分析、対処まで一貫しておこなうことができます。Elasticsearchというと検索用のOSSというイメージをお持ちの方も多いかと思いますが、セキュリティ分野でも力を発揮してくれそうですね。ご興味のある方はTrialライセンスを有効化することで試用できますので、是非お試しいただければと思います。


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


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

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

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
世界初のElastic認定エンジニアと一緒に働きたい人Wanted! - Acroquest Technology株式会社のデータサイエンティストの採用 - Wantedlywww.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



Spring Modulith でモジュラモノリスなアプリの構造を検証してみた

アクロクエスアドベントカレンダー 12月9日 の記事です。

普段は Java, Python でバックエンドの開発をしている大塚優斗です😃

最近は Spring フレームワークのメジャーアップデートなどで盛り上がっていますね!

10月にこんな記事を見かけて、Spring Modulith がとても気になっていたので、手元で試したことを書いていきます✍️

Spring Modulith とは

Spring Modulith はモジュラモノリスな Spring Boot アプリケーションの開発をサポートするプロジェクトです。

元々は Moduliths というプロジェクトでしたが、現在はプロジェクト名が Spring Modulith に変更され、Spring Boot 3 をベースラインとした実験的なプロジェクトとなっています。

spring.io

モジュラモノリスは、モノリスのように単一でデプロイ可能な形を保ちつつ、マイクロサービスのようにモジュール性を持ったアーキテクチャです。
モジュラモノリスの説明は、日本語でとてもわかりやすい記事が書かれているので、そちらを参照してください。

モジュラモノリスに移行する理由 ─ マイクロサービスの自律性とモノリスの一貫性を両立させるアソビューの取り組み - エンジニアHub|Webエンジニアのキャリアを考える!

モジュラモノリスでは、自己完結したビジネスドメインに合わせた疎結合なモジュールを作ることが重要と言われています。
そうすることによって、モジュールごとに独立して開発することが容易になり、あるビジネスドメインで仕様の変更が起きたとしても、影響範囲を対応するモジュールに留めることができるなどのメリットがあるためです。

しかし、単一のリポジトリで開発するため、やろうと思えば各モジュールを密結合にすることができてしまいます。
例えば、Java では子パッケージのクラスを参照するには、子パッケージのクラスに public 修飾子をつける必要がありますが、これによって他モジュールのクラスからアクセスできるようになってしまいます。 さらに、コンパイラはこれらの意図しない参照に対して、警告してくれません。

Spring Modulith では、各モジュールを疎結合に保つためにモジュールの境界ルールを定め、それをテストとして扱うことで、開発者が良くモジュール化されたコードを実装することをサポートしてくれます。

Spring Modulith でできること

公式ドキュメントによると、主に下記のようなことができるようです。

  1. モジュール構造の検証
  2. モジュールに閉じた結合テスト
  3. イベントによるモジュール同士の連携
  4. モジュールのドキュメント化
  5. モジュール同士の連携のモニタリング

この記事では、5番目以外の項目を試していきたいと思います。

この記事内で扱うサンプルコードでは、Maven 3, Java 17, Spring Boot 3.0.0 を使用しています。

0. Spring Modulith でのパッケージの扱いについて

各機能を見ていく前に Spring Modulith におけるパッケージの扱いについて、説明しておきます。

Spring Modulith では、メインアプリケーションクラスが存在するパッケージ直下のサブパッケージをアプリケーションモジュールと呼び、あるドメインに対応するモジュールとして扱います。
そして、アプリケーションモジュール直下の public なクラスは、そのモジュールの公開インターフェースとして扱われます。
また、アプリケーションモジュール配下のサブパッケージは、他のモジュールからアクセスされない内部的なものとして扱われます。

後述しますが、Spring Modulith ではモジュール構造にいくつかのルールが決められており、モジュール構造の検証テストを実行した際に、決まっているルールに違反した場合、テストが失敗します。

この記事内のサンプルコードでは、公式ドキュメントと同様に Order モジュールと Inventory モジュールを扱います。
ファイル構成は以下の通りです。

src/main/java/com/example
└── samplemodulith
    ├── SampleModulithApplication.java
    ├── inventory  # アプリケーションモジュール①
    │   ├── InventoryService.java  # アプリケーションモジュール直下のファイルは公開インターフェースとして扱われる
    │   └── internal  # 他モジュールからはアクセスされないカプセル化されたパッケージ
    │       └── InternalInventoryComponent.java
    └── order  # アプリケーションモジュール②
        ├── Order.java
        ├── OrderCompleted.java
        └── OrderService.java
src/test/java/com/example
└── samplemodulith
    ├── DocumentationTests.java
    ├── ModularityTests.java
    ├── SampleModulithApplicationTests.java
    ├── inventory
    │   └── InventoryIntegrationTests.java
    └── order
        └── OrderIntegrationTests.java
pom.xml

サンプルコードのパッケージ構造

1. モジュール構造の検証

Spring Modulith によるアプリケーションモジュールの検証では以下の3つのことができ、それぞれどういった場合にテストが失敗するかというルールがあります。

  1. 循環参照の検知
    • アプリケーションモジュール間で循環参照している場合にテストが失敗する
  2. 別モジュールへのアクセス違反の検知
    • あるアプリケーションモジュールが別のアプリケーションモジュールの内部パッケージを参照した場合にテストが失敗する
  3. 依存するアプリケーションモジュールの明示
    • 明示的にモジュールの依存関係を定義したときに他のアプリケーションモジュールへの依存があった場合にテストが失敗する

3 はオプション的な立ち位置なので、基本的な 1 と 2 をそれぞれ試していきます。

循環参照の検知

モジュール間に循環参照があった場合にテストで検知できることを試していきます。

サンプルコードでは、循環参照を作るためにコンストラクタで OrderService クラスと InventoryService クラスが相互依存するように定義しています。

package com.example.samplemodulith.order;

import com.example.samplemodulith.inventory.InventoryService;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final InventoryService inventoryService;

    public OrderService(InventoryService inventoryService) {
        this.inventoryService = inventoryService;
    }
}
package com.example.samplemodulith.inventory;

import com.example.samplemodulith.order.OrderService;
import org.springframework.stereotype.Service;

@Service
public class InventoryService {

    private final OrderService orderService;

    public InventoryService(OrderService orderService) {
        this.orderService = orderService;
    }
}

モジュール間で循環参照がある

モジュール構造の検証は ApplicationModules インスタンスverify メソッドで行えます。

package com.example.samplemodulith;

import org.junit.jupiter.api.Test;
import org.springframework.modulith.model.ApplicationModules;

public class ModularityTests {

    @Test
    void verifyModularity() {
        var modules = ApplicationModules.of(SampleModulithApplication.class);
        modules.forEach(System.out::println);
        modules.verify();
    }
}

テストを実行すると、以下のようにモジュール同士が循環参照していることをわかりやすく表示してくれました。

org.springframework.modulith.model.Violations: - Cycle detected: Slice inventory -> 
                Slice order -> 
                Slice inventory
  1. Dependencies of Slice inventory
    - Constructor <com.example.samplemodulith.inventory.InventoryService.<init>(com.example.samplemodulith.order.OrderService, com.example.samplemodulith.inventory.internal.InternalInventoryComponent)> has parameter of type <com.example.samplemodulith.order.OrderService> in (InventoryService.java:0)
    - Field <com.example.samplemodulith.inventory.InventoryService.orderService> has type <com.example.samplemodulith.order.OrderService> in (InventoryService.java:0)
  2. Dependencies of Slice order
    - Constructor <com.example.samplemodulith.order.OrderService.<init>(com.example.samplemodulith.inventory.InventoryService, org.springframework.context.ApplicationEventPublisher)> has parameter of type <com.example.samplemodulith.inventory.InventoryService> in (OrderService.java:0)
    - Field <com.example.samplemodulith.order.OrderService.inventoryService> has type <com.example.samplemodulith.inventory.InventoryService> in (OrderService.java:0)

別モジュールへのアクセス違反の検知

次は、あるモジュールが他モジュールの内部パッケージにアクセスしている場合に、テストで検知されることを確認してみます。

Order モジュールが Inventory モジュールの内部パッケージにあるクラスに依存するコードを用意します。

package com.example.samplemodulith.order;

import com.example.samplemodulith.inventory.internal.InternalInventoryComponent;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final InternalInventoryComponent internalInventoryComponent;

    public OrderService(InternalInventoryComponent internalInventoryComponent) {
        this.internalInventoryComponent = internalInventoryComponent;
    }

OrderモジュールのクラスがInventoryモジュールの内部パッケージにアクセスしている

先ほどと同じテストを実行すると、以下のようにエラーになりました。

org.springframework.modulith.model.Violations: - Module 'order' depends on non-exposed type com.example.samplemodulith.inventory.internal.InternalInventoryComponent within module 'inventory'!
OrderService declares constructor OrderService(InternalInventoryComponent, ApplicationEventPublisher) in (OrderService.java:0)
- Module 'order' depends on non-exposed type com.example.samplemodulith.inventory.internal.InternalInventoryComponent within module 'inventory'!
Field <com.example.samplemodulith.order.OrderService.internalInventoryComponent> has type <com.example.samplemodulith.inventory.internal.InternalInventoryComponent> in (OrderService.java:0)
- Module 'order' depends on non-exposed type com.example.samplemodulith.inventory.internal.InternalInventoryComponent within module 'inventory'!
Constructor <com.example.samplemodulith.order.OrderService.<init>(com.example.samplemodulith.inventory.internal.InternalInventoryComponent, org.springframework.context.ApplicationEventPublisher)> has parameter of type <com.example.samplemodulith.inventory.internal.InternalInventoryComponent> in (OrderService.java:0)

内部パッケージの中には、同一モジュールの公開インタフェースからの参照があるために public になるクラスもありますが、これらのクラスが別モジュールからアクセスされた場合に、テストで検知できるのはとても助かりますね。

2. モジュールに閉じた結合テスト

この章では、以下の2点を確認していきます。

  1. 単一のアプリケーションモジュールで結合テストができること
  2. Bootstrap モードによって、結合テスト時に他モジュールの Bean 生成ができること

単一のアプリケーションモジュールで結合テストができること

Spring Modulith では、ある単一のモジュールの結合テストを独立して実行できます。
これにより不要な Bean 生成をせずに済みます。

Spring Modulith では、 Spring Boot ではお馴染みの @SpringBootTest アノテーションの代わりに、@ApplicationModuleTest アノテーションを用いて、テストを実行します。

package com.example.samplemodulith.inventory;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.modulith.test.ApplicationModuleTest;

@ApplicationModuleTest
public class InventoryIntegrationTests {

    @Autowired
    private InventoryService inventoryService;

    @Test
    void test() {
    }
}

テスト実行後、以下のようなログが出力されます。 STANDALONE モードで起動され、他モジュールの依存性がないことがわかりますね。
他モジュールへの依存があった場合は、テストが失敗します。

Bootstrapping @ModuleTest for inventory in mode STANDALONE (class com.example.samplemodulith.SampleModulithApplication)========================================================================================================================
## inventory ##
> Logical name: inventory
> Base package: com.example.samplemodulith.inventory
> Direct module dependencies: none
> Spring beans:
  + ….InventoryService
  + ….internal.InternalInventoryComponent

Bootstrap モードによって、結合テスト時に他モジュールの Bean 生成ができること

次に、あるモジュールが他のモジュールの Bean に依存するパターンを見ていきます。

OrderモジュールがInventoryモジュールに依存している

この場合、2つのモジュール内で定義されている Bean を生成する必要がありますが、Spring Modulith には直接依存しているモジュールの Bean も同時に Bootstrap する DIRECT_DEPENDENCIES モードがあります。

@ApplicationModuleTest(mode = ApplicationModuleTest.BootstrapMode.DIRECT_DEPENDENCIES)
class OrderIntegrationTests {

    @Autowired
    private InventoryService inventoryService;

    @Test
    void test() {
    }
}

実行後のログは以下のようになります。

Order モジュールが Inventory モジュールに依存しており、Order モジュールの結合テストでは InventoryService クラスが注入されていることを確認できます。

Bootstrapping @ModuleTest for order in mode DIRECT_DEPENDENCIES (class com.example.samplemodulith.SampleModulithApplication)=============================================================================================================================
## order ##
> Logical name: order
> Base package: com.example.samplemodulith.order
> Direct module dependencies: inventory
> Spring beans:
  + ….OrderService
=============================================================================================================================
Included dependencies:
=============================================================================================================================
## inventory ##
> Logical name: inventory
> Base package: com.example.samplemodulith.inventory
> Direct module dependencies: none
> Spring beans:
  + ….InventoryService
  + ….internal.InternalInventoryComponent
=============================================================================================================================

ただし、依存先のモジュールが利用できない場合でもテストができるように、依存先モジュールの Bean をモックする方がベターです。

従来通り Spring Boot の @MockBean アノテーションを使用します。

@ApplicationModuleTest
class OrderIntegrationTests {

    @MockBean
    private InventoryService inventoryService;

    @Test
    void test() {
    }
}
Bootstrapping @ModuleTest for order in mode STANDALONE (class com.example.samplemodulith.SampleModulithApplication)====================================================================================================================
## order ##
> Logical name: order
> Base package: com.example.samplemodulith.order
> Direct module dependencies: inventory
> Spring beans:
  + ….OrderService

Order モジュールだけでテストが実行できていることが確認できました。

3. イベントによるモジュール同士の連携

よりモジュール同士を疎結合にするために、Spring Modulith ではモジュール間のやりとりに Spring Application Events を使うことを推奨しています。

モジュール間のやり取りにイベントを用いることで、呼び出し元が呼び出し先について知る必要がなくなり、テスト時も呼び出し先の Spring Bean に依存/モックする必要がなくなります。

Kafka などのメッセージングシステムを使用することに似ていますが、Spring Application Events は Spring Framework が提供しているため、追加の依存関係/インフラは必要ありません。

OrderService の実装は以下のようになります。InventoryService に依存しない形になりました。

package com.example.samplemodulith.order;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final ApplicationEventPublisher events;

    public OrderService(ApplicationEventPublisher events) {
        this.events = events;
    }

    public void complete() {
        events.publishEvent(new OrderCompleted());
    }
}
package com.example.samplemodulith.order;

public class OrderCompleted {
}

InventoryServiceはOrderCompletedイベントをlistenする

テストでは、@ApplicationModuleTest アノテーションによって、テストメソッドに PublishEvents インスタンスを注入することができます。

ログからも他のモジュールに依存しない形でテストできていることがわかります。

import org.springframework.modulith.test.PublishedEvents;

import static org.assertj.core.api.Assertions.assertThat;

@ApplicationModuleTest
class OrderIntegrationTests {

    @Autowired
    private OrderService orderService;

    @Test
    void complete_success(PublishedEvents events) {
        orderService.complete();

        assertThat(events.ofType(OrderCompleted.class)).hasSize(1);
    }
}
Bootstrapping @ModuleTest for order in mode STANDALONE (class com.example.samplemodulith.SampleModulithApplication)====================================================================================================================
## order ##
> Logical name: order
> Base package: com.example.samplemodulith.order
> Direct module dependencies: none
> Spring beans:
  + ….OrderService

4. モジュールのドキュメント化

SpringModulith では、モジュール間の関係を表すモジュールコンポーネント図と、モジュールキャンバスと呼ばれるモジュールの概要表を生成できます。

ドキュメントの生成には、ApplicationModules を使用し、以下のテストコードでモジュールコンポーネント図とモジュールキャンバスを作成しています。

package com.example.samplemodulith;

import org.junit.jupiter.api.Test;
import org.springframework.modulith.docs.Documenter;
import org.springframework.modulith.model.ApplicationModules;

public class DocumentationTests {

    ApplicationModules modules = ApplicationModules.of(SampleModulithApplication.class);

    @Test
    void writeDocumentationSnippets() {
        new Documenter(modules)
                .writeModulesAsPlantUml()
                .writeIndividualModulesAsPlantUml()
                .writeModuleCanvases();
    }
}

テスト実行後、target ディレクトリ配下に以下のような形で生成されたドキュメントが置かれます。

target/spring-modulith-docs
├── components.uml
├── module-beanreference.adoc
├── module-beanreference.uml
├── module-catalog.adoc
├── module-catalog.uml
├── module-inventory.adoc
├── module-inventory.uml
├── module-order.adoc
├── module-order.uml
├── module-typereference.adoc
└── module-typereference.uml

モジュールコンポーネント

コンポーネント図のスタイルには C4 か UML を選択できます。
図中でモジュール間の依存関係を表している矢印は以下の3つに分かれます。

  1. depends on
    • 他モジュールの型を参照している
  2. uses
    • 他モジュールのSpring Beanを参照している
  3. listen to
    • 他モジュールのイベントを待ち受けている

モジュールコンポーネント図の例。説明のためにtypereferenceモジュールとbeanreferenceモジュールを追加しています

モジュールキャンバス

Spring Modulith では、モジュールの概要を表形式でまとめたものをモジュールキャンバスと呼んでいます。
モジュールキャンバスは、以下のセクションに分かれており、Asciidoc ファイルで生成されます。

  1. アプリケーションモジュールのベースパッケージ名
  2. モジュールで公開されている Spring Beans
  3. jMolecules によって集約として扱われている集約ルート
  4. モジュールによって publish されるイベント
  5. モジュールによって listen されるイベント
  6. モジュールで公開されている Spring Boot Configuration properties

試しに Inventory モジュールのモジュールキャンバスを見てみると以下のようになっていました。

Inventoryモジュールのキャンバス

より多くのセクションが含まれている表の例が、公式ドキュメントに書かれているので、そちらも参照してください。

まとめ

今回は、Spring Modulith の基本的な使い方を手を動かしながら試してみました。

モジュラモノリスでアプリを作る際に、モジュール構成のテストをすることで、モジュールを疎結合に保ちながら開発できるのは心強いと思いました。

ちなみに、Spring Modulith 開発者の Oliver Drotbohm さんによると、2023年の第二四半期に非実験版のプロジェクトに昇格させる予定のようなので楽しみですね!

また、この記事で紹介できていない機能もあるので、興味がある方はぜひご自身で調べてみてください。

参考

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

LambdaのSnapStartをSpring Cloud Functionで検証してみた

3歳の息子がきらきら星を歌っていたので、きらきら星変奏曲モーツァルト)を弾いたら、「それは違う」と否定されてしまった@phonypianistです。

AWS re:Invent 2022でLambdaのSnapStartが発表されました。サーバーレス界隈に衝撃が走っていますw
ということで、本ブログのアドベントカレンダー12/8の記事として、SnapStartを紹介します。

Lambda SnapStartとは

LambdaのInitフェーズに時間がかかる問題、いわゆるコールドスタート問題を解決するための機能です。

aws.amazon.com

Lambdaのライフサイクルは大まかには、「初期化 (Init)」「起動 (Invoke)」「シャットダウン (Shutdown)」の3つのフェーズに分かれています。

https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html#runtimes-lifecycle より引用

最初の呼び出し、もしくはしばらく時間が経過したりスケールアウトしたりする際に「初期化」が行われますが、この部分はプログラムを実行するための環境準備やライブラリ読み込みなど、通常重い処理が行われます。 とりわけ、JavaではJava VMの起動があり、PythonやNode.jsに比べると「初期化」フェーズに時間がかかります。

SnapStartでは、Lambdaが実行されたときの状態のスナップショットを取っておき、それを復元することで、「初期化」フェーズの高速化を行います。 これにより、JavaではJava VMの起動が省略され、圧倒的に速くLambdaを起動することができるようになります。

実際にどれくらい高速になるのか、起動に時間がかかるSpringアプリケーション(Spring Cloud Function)を使って試してみました。 また、スナップショットのサイズはLambdaのメモリサイズに依存すると思われるため、Lambdaのメモリサイズにも影響するか検証してみました。

検証の概要

Spring Cloud Functionで作ったエコー(送られてきた文字をそのまま返す)アプリケーションをLambda上で動かし、CloudShellからリクエストを送って性能を検証します。

SnapStart検証構成

コールドスタートを確実に発生させるために、アプリケーションではリクエストを受けたときに1秒スリープさせます。

準備

プロジェクトの生成

Spring Cloud Functionのコードを用意します。 Spring Initializrで依存関係に「Spring Cloud Function」を追加してプロジェクトを生成します。今回はMavenプロジェクトにしました。 注意点として、LambdaはJava 11しか対応していないため、最新のSpring Boot 3は選択できません。Spring Boot 2とJava 11を選択してください。

pom.xmlの修正

生成したプロジェクトにはAWS用のライブラリがないため、pom.xmlにspring-cloud-function-adapter-awsを追加します。

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-function-adapter-aws</artifactId>
</dependency>

Spring Bootが生成するJARファイルはそのままではLambdaから呼び出せないため、maven-shade-pluginでJARファイルを生成するように設定します。

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <dependencies>
        <dependency>
          <groupId>org.springframework.boot.experimental</groupId>
          <artifactId>spring-boot-thin-layout</artifactId>
          <version>1.0.28.RELEASE</version>
        </dependency>
      </dependencies>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-shade-plugin</artifactId>
      <dependencies>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-maven-plugin</artifactId>
          <version>3.0.0</version>
        </dependency>
      </dependencies>
      <configuration>
        <createDependencyReducedPom>false</createDependencyReducedPom>
        <shadedArtifactAttached>true</shadedArtifactAttached>
        <shadedClassifierName>aws</shadedClassifierName>
        <transformers>
          <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
            <resource>META-INF/spring.handlers</resource>
          </transformer>
          <transformer implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer">
            <resource>META-INF/spring.factories</resource>
          </transformer>
          <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
            <resource>META-INF/spring.schemas</resource>
          </transformer>
        </transformers>
      </configuration>
    </plugin>
  </plugins>
</build>

これにより、 mvn clean package を実行することで、Lambdaで設定可能な snapstartdemo-0.0.1-SNAPSHOT-aws.jar ファイルが生成できます。

Javaコードの実装

簡易的に実装します。 メインクラスに echo メソッドを実装し、1秒待つ処理を追加します。

@SpringBootApplication
public class SnapStartDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(LambdademoApplication.class, args);
    }

    @Bean
    public Function<Flux<String>, Flux<String>> echo() {
        return flux -> flux.doOnNext(this::sleep);
    }

    private void sleep(String text) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // Do nothing.
        }
    }

}

serverless.yml

今回はServerless Frameworkでデプロイします。 512MB/1024MB/2048MB/4096MBの4種類のメモリサイズ×SnapStartのON/OFFの組み合わせで計8個のLambda関数を作成します。 アプリケーションはすべて同じとします。

SnapStartを有効にするために、resources.extensionsでSnapStartのApplyOnに PublishedVersions を指定します。

また、SnapStartはLambdaのバージョン指定での実行が必要です。Serverless FrameworkのデフォルトではAPI Gatewayから呼ばれるLambdaのバージョンは $LATEST になり、SnapStartのオプションを有効にしていてもSnapStartが機能しません。そのため、serverless-plugin-canary-deploymentsプラグインを用いて、API GatewayからLambdaを呼び出す際にLambdaバージョンが指定されるようにします。

service: snapstartdemo

provider:
  name: aws
  stage: dev
  region: ap-northeast-1
  runtime: java11
  timeout: 15
  logRetentionInDays: 7
  iamRoleStatements:
    - Effect: Allow
      Action:
        - codedeploy:*
      Resource:
        - '*'

package:
  # mvn clean packageで生成される、AWS用のjarファイルを指定する
  artifact: target/snapstartdemo-0.0.1-SNAPSHOT-aws.jar

functions:

  # これ以降は、SnapStart有効/無効の順番に、メモリサイズが異なるLambda関数を定義する
  # SnapStart有効/無効の設定は、下のresourcesに定義する

  SpringCloudFunction512MBSnapStartEnabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 512
    events:
      - http:
          method: get
          path: /snapstart/512/enabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction512MBSnapStartDisabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 512
    events:
      - http:
          method: get
          path: /snapstart/512/disabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction1024MBSnapStartEnabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 1024
    events:
      - http:
          method: get
          path: /snapstart/1024/enabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction1024MBSnapStartDisabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 1024
    events:
      - http:
          method: get
          path: /snapstart/1024/disabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction2048MBSnapStartEnabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 2048
    events:
      - http:
          method: get
          path: /snapstart/2048/enabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction2048MBSnapStartDisabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 2048
    events:
      - http:
          method: get
          path: /snapstart/2048/disabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction4096MBSnapStartEnabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 4096
    events:
      - http:
          method: get
          path: /snapstart/4096/enabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction4096MBSnapStartDisabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 4096
    events:
      - http:
          method: get
          path: /snapstart/4096/disabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

resources:
  - extensions:
      # SnapStartを有効にする設定を行う(デフォルトは無効)
      SpringCloudFunction512MBSnapStartEnabledLambdaFunction:
        Properties:
          SnapStart:
            ApplyOn: PublishedVersions
      SpringCloudFunction1024MBSnapStartEnabledLambdaFunction:
        Properties:
          SnapStart:
            ApplyOn: PublishedVersions
      SpringCloudFunction2048MBSnapStartEnabledLambdaFunction:
        Properties:
          SnapStart:
            ApplyOn: PublishedVersions
      SpringCloudFunction4096MBSnapStartEnabledLambdaFunction:
        Properties:
          SnapStart:
            ApplyOn: PublishedVersions

plugins:
  # Lambdaのバージョンを設定するためのプラグインを指定する
  - serverless-plugin-canary-deployments

デプロイ

deployコマンドでデプロイします。

serverless deploy

検証方法

クラスメソッドさんの以下の記事でまとまっていますので、こちらを参考にさせていただきました。 CloudShellから、API Gatewayに対して ab コマンドで100並列でリクエストを送ります。

dev.classmethod.jp

検証結果

Lambdaに割り当てたメモリサイズ毎に、SnapStartの無効/有効時のコールドスタートにかかった時間は、以下のようになりました。

メモリサイズ毎のコールドスタートにかかった時間(最小/最大/平均)
 SnapStart無効SnapStart有効
メモリサイズ最小最大平均最小最大平均
512MB4278.455049.414737.08195.01416.18285.83
1024MB4534.515200.474764.89267.76398.31326.71
2048MB3721.234659.764161.39203.59813.66321.07
4196MB3084.843607.293222.51212.74768.53353.76
メモリサイズ毎のコールドスタートにかかった時間(パーセンタイル)
 SnapStart無効SnapStart有効
メモリサイズp50p90p99p50p90p99
512MB4717.684881.765049.41283.59359.71416.18
1024MB4745.574906.425200.47321.88373.49398.31
2048MB4132.394345.264659.76345.51406.48813.66
4196MB3213.913354.723607.29354.87424.72768.53

90パーセンタイル値をグラフにすると、以下のようになりました。

割り当てメモリ毎のコールドスタート時間

どのメモリサイズでも、SnapStart有効の場合はSnapStart無効の場合に比べて約10倍も時間が短縮しています。

なお、Lambdaのメモリサイズを大きくすればするほど、SnapStartなしでの起動時間は短くなっていますが、SnapStart有効の場合はわずかに長くなっています。これはおそらく、スナップショットのサイズがメモリサイズに依存して大きくなっているものと思われますが、気にするほどではなさそうです。

まとめ

SnapStartを有効にすることで、JavaのLambdaコールドスタートの時間を大幅に短縮することができました。 今後はLambda関数をJavaで実装する、という選択もありかと思います。

ただ、X-Rayが使用できない、512MBより大きなエフェメラルストレージが使用できない等の制限があるため、注意が必要。 加えて、LambdaではJava 11(Amazon Corretto 11)しか選択できないのが残念なところ。 早くJava 17に対応してもらえないかなー。

では。

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

YAMALEX が第3回 AI 王に参加しました

アクロクエスアドベントカレンダー 12月7日 の記事です。

こんにちは、機械学習チーム YAMALEX メンバの駿です。

この度、東北大学理化学研究所が主催する「 AI 王~クイズ AI 日本一決定戦~第3回コンペティション」(以下、AI王)に YAMALEX として参加しました。
初めて参加するコンペで、結果としては入賞できませんでしたが、今回は参加した感想をお伝えします。

技術的な詳細はこちらのスライドにまとまっているので、併せてごらんください。

speakerdeck.com

YAMALEX とは

YAMALEX とは2022年夏に Acro 社内で発足した、 会社の未来の技術を創る、データサイエンスチーム です。
Kaggle Grandmaster の山本を中心に、若手のデータサイエンティスト5人がメンバーです。

最先端技術の調査、実証を通し、会社としてのデータ分析事業の方向性を示すと同時に先端を切り開くデータサイエンティストになることを目指して活動しています。

YAMALEX についてもっと知りたい方はこちらの記事もご覧ください。

コンペ最終日の YAMALEX メンバ MTG

AI 王とは

AI王とは、日本の質問応答研究を推進させることを目的として、クイズ問題を題材とした質疑応答データセットを用いて、より正確に答えを出すことができるモデルを作成するコンペティションです。

AI王 ロゴ

2020年の第1回から毎年行われており、今回が第3回となります。

第1回は20個の選択肢の中からクイズの回答を選択するものでしたが、第2回からは自由回答になっていて、 回を重ねるごとに難易度が上がっています。
そんな中、 YAMALEX として AI 王となるため、挑戦を決めました。

参加のモチベーション

  1. QA の経験を積みたい

    QA システム自体は、チームメンバがほぼ扱ったことが無かったので このコンペを知った際に、チャレンジしたくなった(今回、初参加です)。

  2. 日本語の NLP コンペに参加したい

    Kaggle などには参加したことはあるが、問題のほとんどが英語の文章なので 日本のコンペにチームとして出たかった。

  3. チームでまとまってコンペに参加してみたい

    YAMALEX チームはまだ個人参加はあれど、チームでコンペに取り組んだ ことがなかったので、チームで取り組んでみたかった。

スケジュール

時期
概要
内容
ある10月末頃 AI 王へのチャレンジが始動する とあるチームメンバが唐突に本格的に AI 王をやりたいと発言したところから、私たち YAMALEX チーム5人の挑戦は始まりました。
~11月7日 作戦を立てる 既存ベンチマークの動作確認と昨年度のソリューションを読んで作戦を立てて、分担を決めました。
役割は、 Retriever ( BPR/DPR ) / FiD /外部データセット取得で、3人、1人、1人ほどです。
Retriever が最もやることが多かったのでその部分に人を多めに担当してもらうようになりました。
~11月14日頃 チーム毎に作業を進める チームに分かれて以下に取り組んでいました。
QA に取り組んだメンバがいないため勘所が分からないこともあり、一つ一つ、影響のあるパラメータや手法を考えて確認をしていました。
  • 外部データセットの取得・実験
  • モデルのパラメータチューニング作業
  • 機械学習モデルとは別に Elasticsearch を使ったテキスト検索を Retriever に導入
11月18日頃 外部データを追加する 計算時間が膨大になる Wikipedia のデータを新たに追加する解法を取り入れました。
Dev/Test 間の LB での相関が取れなくなり思うようにスコアが伸びず、頭を抱えるようになったのはこの頃です。
リーダーボード
締切直前
アンサンブルを導入する 複数のモデルを計算する時間が厳しい中、シンプルな Voting を行うアンサンブルを試みました。
アンサンブルのバリエーションもいくつかあると思いますが、この部分は詰め切れませんでした。
最終提出は LB 最高スコアに届きませんが、このアンサンブルした結果にしています。
Dev/Test で相関が取れていないことから問題傾向によるブレがあると予測し、安定した結果を出すためにシンプルな Voting を行うアンサンブルが妥当だといった判断です。
11月23日 リーダーボード提出締切 テストデータでのスコアの確認、他チームの進捗確認がこれ以降できなくなる。
リーダーボード
締切後
Dockerfileを作成する リーダーボードへの提出が締め切られ、システムの最終提出のみとなってからは、 Dockerfile の作成に追われていました。
提出のために不要なファイルを消すなど試行錯誤をしていました。
11月26日 システム最終提出締切 本当に最後の締め切り。ここで提出した Docker イメージを使って未知の問題を解き、スコアと順位が確定する。

感想

  1. チームとして話し合いながら進めるのが楽しい!

    私はあまり NLP が得意ではないのですが、モデル改良に向けてチームでの話し合いに参加しました。

    その中で、クイズを見ながら「こういう問題にはこういうアプローチをしたい」や「これができると精度上がらないですか」 などの意見を出して、できそう/できなそうを議論することができました。

    人がクイズを答えるときにやっていることと同様にやろうとすると、 複数モデルを用意して、問題文から適切なモデルを選択しないといけず、 複雑かつ巨大になってしまうことなども分かり、興味深かったです。
    例:ですが問題は「ですが」の前と後の対比をできるモデルがあれば強いが、他の問題に対しては全く使い物にならない。

    締め切りまじかになると、普段の生活では味わえない、ひりひり感を会話から感じる事ができました。

  2. NLP 分野の知識が広がった!

    AI 王では主催者がベースラインのモデルを提供しており、初心者でも環境があれば動かせる状態から始めることができました。

    また、ベースラインを動かして各機能の入出力や中のコードを見ることで、 QA タスクのアプローチを知ることができました。
    一つのやり方を学ぶことができたため、別の機会に別のモデルを見たときには、 今回のモデルと比較することで、どんなことをやっているかが分かりやすくなると思います。

  3. スコアが上がっていくのを見るのが楽しい!

    これはコンペティションなら言わずもがなですね。

    下の図はリーダーボードスコア(LB Score)といって、提出回数ごとに、AIによる質疑応答のスコアの遷移を表しています。
    当社のデータだけを示していますが、コンペ開催中は、リーダーボードに各チームの暫定スコアと順位が表示されており、 上の人のスコアに徐々に近づいて追い抜いていく、逆に追い越されるなど、 他チームとの駆け引きも面白いポイントでした。

    今回コンペのYAMALEXチームスコアの遷移

まとめ

今回は AI 王に参加した感想を書いてみました。

残念ながら、初参加で入賞!とはいきませんでしたが、確実に YAMALEX チームとしての強さとチームワークは上がったと思います。 この経験を糧に次の挑戦をしていきます。

開催していただいた運営の方々、ありがとうございました!

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

Elastic Stack Advent Calendar 2022(2022年のElastic Search Platformを振り返る)

こんにちは、アクロクエストテクノロジー株式会社でElastic Stackのコンサルティング業務を担当している吉岡です。
本記事は、Elastic Stack (Elasticsearch) Advent Calendar 2022 の5日目の内容になります。

■Elastic Stackリリース概要

  • Elastic Stack Version 7.17.0リリース(2022年2月)
  • Elastic Stack Version 8.0.0リリース(2022年2月)
  • Elastic Stack Version 8.1.0リリース(2022年3月)
  • Elastic Stack Version 8.2.0リリース(2022年5月)
  • Elastic Stack Version 8.3.0リリース(2022年6月)
  • Elastic Stack Version 8.4.0リリース(2022年8月)
  • Elastic Stack Version 8.5.0リリース(2022年11月)


2022年1月時点での最新バージョンはVer.7.16.2、2022年12月5日時点での最新版はVer.8.5.2です。
待ちに待ったVer.8のリリースです。Ver.7.0.0がリリースされたのが2019年4月ですので、約3年振りのメジャーバージョンアップ!Ver.8.0.0リリース後、マイナーバージョンが5回上がっていますが、マイナーチェンジとは思えない多くの機能が盛り込まれています。

本ブログでは、Enterprise Search、Observablility、Elastic Security、3つのソリューションのコアとなる「Elastic Search Platform」の進化について紹介していきます。

■Elastic Search Platformとは

Elasticsearch、Kibana、Logstash、Beatsとう主要プロダクト群を以前はElastic Stackと呼んでいましたが、Ver.8.0あたりからElasticsearch、Kibana、Integrations(Logstash/Beats/Elastic Agent)という分け方になり、総称が「Elastic Search Platform」となりました。Elastic Stackと同様の意味に捉えてもらえればと思います。

Elastic Search Platformをまとめるとこのような感じです。

Elastic Search Platform

■Ver.7.17.0における進化ポイント

パフォーマンスの向上

  • インデックス設定の重複排除
    • Ver.7.16以前では、インデックスのSetting/Mapping情報は各インデックスに紐づいていたため、Setting/Mappingが全く同一だとしても、重複した情報全てをヒープに保持する非効率な仕様でした。特に、日/週/月などの単位でインデックスを集約する時系列データでは、インデックスが増加するほど、Setting/Mapping重複によるヒープ逼迫の影響が大きくなります。
    • Ver.7.17.0では、複数のインデックスに使用される同一のSetting/Mappingが1セットに削減されます。Elastic社の公式発表によると、Auditbeatのインデックスが10,000個あるとき、7.16以前ではヒープを約500MB消費するのに対して、Ver.7.17.0ではヒープ使用量が1MB(約500分の1)になるそうです。ノード間通信の量も削減されるため、クラスタのパフォーマンス向上につながります。

■Ver.8.0.0~Ver.8.5.0における進化ポイント

パフォーマンスの向上

  • インデックスサイズ
    • 圧縮率向上によりインデックスサイズが縮小(8.0.0)
    • 時系列メトリック データに最適な「時系列データ ストリーム (TSDS) 機能」がリリース。インデックスサイズを約30%削減(8.5.0)
  • ストレージサイズ
    • Doc value only field:ストレージサイズを20%以上削減(8.1.0)
  • インデクシング性能
    • ジオポイントのインデクシング性能が10~15%高速化(8.0.0)
    • Doc value only field:インデクシング性能を20%以上向上(8.1.0)
  • 検索性能
  • ヒープ使用量
    • リソース使用量を大幅に削減、データノードが保持可能なインデックス数が増加(8.3.0)

Ver.8.0.0以降、インデックスサイズ/ヒープ/ストレージの効率化に関する機能強化が多く、結果としてVer.7.Xと8.3以降で、10倍以上のシャードを保持できるようになっています。ぜひVer.8.3以降を使いましょう。

可視化/Kibana UI

  • Discover
    • フィールド統計機能が追加(8.0.0)
    • フィールド統計機能ジオデータに対応(8.2.0)
  • Lens
    • Gauge, Waffle, Mosaicが追加(8.1.0)
    • 新しいメトリックが追加(Tech Preview)(8.4.0)
    • 簡単にChoropleth Maps を作成可能に(8.2.0)
    • Annotation機能が追加(8.2.0)
    • クエリベースのAnnotation機能が追加(8.5.0)
  • Maps
    • Shapeファイルのアップロード機能を追加(8.1.0)
    • GeoServer不要でカスタムマップを利用可能に(8.1.0)
  • Dashboard
    • コントロール専用エリアが追加(8.3.0)
    • タイムスライダー機能が追加(8.5.0)
  • その他
    • フリートパッケージマネージャーから機械学習モデルやSecurityパッケージがインストール可能に(8.0.0)

Discoverが機能強化で使いやすくなりました。最初にフィールドの統計分析からデータ傾向を掴み、KQLを使ってドキュメント抽出/調査、その後必要に応じて異常値検知用のアラートを作成。このようなデータの基礎調査作業がDashboardを作ることなく可能です。

Search/ML/NLP

  • Search
    • 近似最近傍(ANN)検索機能(8.0.0)
  • ML管理機能
    • 機械学習モデル管理画面が追加(8.1.0)
    • 機械学習モデルをスペース単位で管理可能に(8.2.0)
    • 機械学習モデルのテスト画面が追加(8.2.0)
  • NLP
    • PyTorchモデルのインポートが可能に(8.0.0)
    • Fill-mask、固有表現抽出、テキスト分類、Embeddingに対応(8.0.0)
    • 質問応答(Question-Answering)に対応(8.3.0)

PyTorchモデルのインポートが可能になったことで、Elasticsearch単体で様々なデータ分析ができるようになりました。今後も様々なNLPタスクがサポートされるはずです。非常に楽しみですね。

ちなみに、現時点でのElasticsearchにおけるNLPの変遷をまとめると以下のようになります。

NLPの変遷

ログ分析/監視

  • Kibana Alert
    • スヌーズ機能が追加(8.2.0)
    • Discoverからアラートルールを作成可能に(8.3.0)
  • データのエンリッチ
    • Lookup Runtime Fieldのサポート(8.2.0)
    • クエリのタイミングでデータ結合が可能(リアルタイムエンリッチ)
    • Enrich Processorとルックアップデータ更新に対する追従が容易
  • AIOps
    • ログパターン解析機能が追加(8.5.0)
    • Log Rate Spike原因調査機能が追加(8.5.0)

これまで、Elasticsearch上でデータのエンリッチを行うにはEnrich Processorの一択でした。Enrich Processorはルックアップ先が固定の場合は向いていますが、頻繁にルックアップデータが更新されるようなユースケースでは、再ルックアップ(Update by Query + Pipeline指定)が必要で使い勝手が良くありません。その課題を解決するのが、Ver.8.2.0でリリースされたLookup Runtime Fieldです。Lookup Runtime Fieldはクエリタイムジョインなので、常に最新のルックアップデータをエンリッチ可能です。柔軟な可視化を行うのに非常に便利です。

まとめ

2022年のElastic Search Platformを振り返って、特徴的な機能強化/新機能をピックアップしてみましたが、その中でも、NLP関連の進化が個人的に興味深いですね。Ver.8.0以降、BERTなどのPyTorch機械学習モデルをElasticsearchでダイレクトに使用したり、モデルを使った推論をElasticsearch内でネイティブに実行することができるようになり、データのエンリッチや検索の幅が大きく広がっています。来年も目が離せませんね。

Elastic Stack

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

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

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

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