TECH PLAY

TensorFlow

イベント

該当するコンテンツが見つかりませんでした

マガジン

技術ブログ

.table-of-contents > li > ul > li > ul { display: none } はじめに こんにちは、データシステム部MLOpsブロックの 木村 です。MLOpsブロックではZOZOTOWN、WEAR by ZOZOをはじめとして、弊社で提供するさまざまなサービスに関わるML機能を開発・運用しています。 本記事で紹介する ZOZOマッチ (以下、本アプリ)は「ファッションで恋する」をコンセプトとしたマッチングアプリです。本アプリもML機能を持ち、MLOpsブロックが機能を開発・運用しています。 本アプリの特徴的なML機能として ファッションジャンル診断 (以下、ジャンル診断)があります。ジャンル診断とは全身画像をストリートやモードなどZOZOが定義した12種類のファッションジャンルに分類し、該当するジャンルの割合を「ジャンル傾向」として円グラフで表示する機能です。 ジャンル診断には2つの利用方法があります。1つ目はプロフィールに登録した自分の全身画像から自分の「ジャンル傾向」を判定する方法です。2つ目は好みのコーディネート画像を複数枚選んで自分の「好みの雰囲気のジャンル傾向」を判定する方法です。これら2つの利用方法によって「ファッションで恋する」レコメンドを実現しています。 このジャンル診断を実現するために、全身画像からファッションジャンルを判定する「ジャンル診断API」を開発しました。ジャンル診断APIはMLモデルによる推論処理を行い、全身画像を入力として12種類のジャンルそれぞれに属する確率を返します。 今回、このジャンル診断APIの推論サーバーとして Triton Inference Server (以下、Triton)を導入しました。TritonはNVIDIAが提供するオープンソースの推論サーバーです。 本記事ではジャンル診断APIの開発で推論サーバーにTritonを採用した背景から、導入時に遭遇した課題とその解決策、導入効果までを説明します。Tritonの導入にあたり 公式ドキュメント の説明だけでは理解しにくかった点もあり、いくつかの課題に直面しました。これらの知見がTritonを使った推論サーバーの構築・運用を検討する際の参考になれば幸いです。 本記事の内容は2026年2月時点の情報であることにご留意ください。 目次 はじめに 目次 Tritonを導入した背景 従来のML推論API構成とその課題 推論サーバーの選定 Tritonを採用するメリット 推論処理の高速化 処理の分離による保守性向上 設定変更の容易さ 推論単体の性能検証 ジャンル診断APIへの導入経緯 システム構成 全体構成 リクエストの流れ 通信プロトコルの選定 Tritonがサポートするプロトコル HTTPとgRPCの比較 Kubernetesリソースの構成 Tritonコンテナの起動設定 Deploymentマニフェスト InitコンテナとVolume Mountによるモデル共有 OpenTelemetryによるトレース設定 ヘルスチェック設定 セキュリティ設定 Triton Podへの接続設定 GCSの認証設定 オートスケーリングの設定 モデルのディレクトリ構成 モデルの設定 推論ゲートウェイAPIの実装 Triton接続の設定 StyleFeatureExtractorクラスの実装 推論リクエスト処理の実装 導入時の課題と解決策 課題1:gRPCで負荷分散されない 原因 解決策 1. Headless Serviceの採用 2. クライアント側でround_robinロードバランシングを設定 スケールアウト時の負荷分散 課題2:Datadog APMでトレースが連携されない 原因 解決策 1. W3C Trace Contextによるトレース伝播 2. Triton専用NodeにDatadog AgentをDaemonSetで配置 3. Tritonの起動オプションでトレース設定 課題3:PyTorchモデルがロードされない 原因 解決策 導入効果 導入時の注意点 コスト面の考慮 まとめ Tritonを導入した背景 従来のML推論API構成とその課題 MLOpsブロックでは従来、前処理から推論、後処理までの全処理を単一のAPI内で実行していました。Pod起動時に Google Cloud Storage (以下、GCS)からMLモデルをダウンロードし、入力データの取得から後処理までを同一プロセス内で実行する構成です。 この構成には2つの課題がありました。 1つ目はリソース効率が悪いことです。画像ダウンロードのようなI/Oバウンドな処理とMLモデルでの推論のようなCPU/GPUバウンドな処理ではボトルネックが異なります。しかし単一のAPIではこれらを分離できません。このリソース効率の悪さはパフォーマンス面とコスト面の両方に影響がありました。パフォーマンス面では、どちらか一方がボトルネックでもAPI全体を水平・垂直スケールする必要があり、ボトルネックではない処理のリソースまで増やすことになっていました。コスト面では、推論にGPUを使用したい場合、すべてのAPI PodにGPUを割り当てる必要があります。ただし実際にGPUを使うのは推論処理の間だけです。前処理・後処理の間もGPUを占有し続けるためGPU使用率が低くなり、無駄なコストが発生していました。 2つ目は推論速度の向上が難しいことです。ML推論APIではモデルの推論処理に要する時間が全体のレイテンシの大部分を占めます。しかし推論処理は単一のAPI内に組み込まれていることで、汎用的なWebフレームワーク上で実行されるため推論に特化した最適化手段が限られていました。そのため推論のレイテンシ改善が難しく、リアルタイム推論を活用したUXの良い機能の提供が困難でした。 推論サーバーの選定 これらの課題を解決するため推論サーバーの導入を検討しました。主要な推論サーバーを比較した結果は次の通りです。 名称 対応フレームワーク 実装言語 TorchServe PyTorch専用 Java/Python TensorFlow Serving TensorFlow専用 C++ BentoML 複数対応 Python Triton Inference Server 複数対応 C++ MLOpsブロックではPyTorchやTensorFlowなど複数のMLフレームワークを利用しています。また今回のジャンル診断だけでなく今後の別プロジェクトでも活用できる共通基盤として、複数フレームワークに対応しつつ高速な推論基盤が必要でした。 TorchServe と TensorFlow Serving はどちらも特定フレームワークのモデルで手軽にAPIを作成したい場合に有効です。しかし、複数フレームワークに対応していません。 BentoML は様々なMLフレームワークに対応していますが、Python実装のためパフォーマンス面で不利な場合があります。 一方でTritonは様々なMLフレームワークに対応しておりC++実装のため高速です。構成が複雑でキャッチアップコストはありますが、汎用性・機能性が最も高く共通基盤として最適と判断しTritonを選定しました。 Tritonを採用するメリット 複数の推論バックエンドに対応していることに加えてTritonを導入するメリットは次の4つがあります。 推論処理の高速化 処理の分離による保守性向上 設定変更の容易さ 推論単体の性能検証 以降で各メリットについて詳しく説明します。 推論処理の高速化 FastAPIなどPythonベースの推論サーバーは汎用的なWebフレームワークであり推論処理に最適化されていません。一方Tritonは推論処理に特化したサーバーです。リクエストの受付と推論実行を別々のスレッドプールで処理するため、推論中も新しいリクエストを受け付けられます。またC++で実装されているためPythonのGlobal Interpreter Lockの制約を受けず、推論処理をマルチスレッドで並列実行できます。 また Dynamic Batching により複数の推論リクエストをサーバー側でバッチ処理できます。最初のリクエストを受け取ってから指定した待機時間内に届いたリクエストを動的にまとめることで、個別処理よりもスループットが向上します。 さらに instance_group により同一モデルの複数インスタンスを並列実行できます。デフォルトでは同時に1つのリクエストのみ実行されますが、インスタンス数を増やすことで複数のリクエストを並列処理できます。 処理の分離による保守性向上 推論処理をTritonに委譲することでAPIサーバーは前処理・後処理に、Tritonは推論に専念できます。この分離により各コンポーネントの責務が明確になりコードの保守性が向上します。 スケーリングの観点でも大きなメリットがあります。具体的には水平スケール(Pod数の増減)や垂直スケール(CPUやメモリの割り当て変更)をAPIサーバーとTritonそれぞれに対して個別に適用できるようになります。推論がボトルネックの場合はTriton Podのみを、前処理・後処理がボトルネックの場合はAPIサーバー Podのみをそれぞれ独立してスケールできます。従来の一体型構成では推論のボトルネック解消のためにPod全体をスケールする必要がありました。分離構成ではボトルネックとなっているコンポーネントのみをスケールすればよくリソース効率が向上します。またAPIサーバーとTritonのメトリクスを個別に監視することでどちらがボトルネックになっているかを把握しやすくなります。 設定変更の容易さ Tritonではモデルの設定を config.pbtxt で宣言的に管理します。バッチサイズの上限、モデルインスタンス数、Dynamic Batchingの待機時間などをコード修正なしで変更できます。 従来のPythonベースの実装でもバッチサイズ等を環境変数で外部化できます。一方 config.pbtxt では max_batch_size や dynamic_batching 、 instance_group など推論サーバーに必要な設定項目が標準化されています。これらの設定項目を自前で設計する必要がありません。また config.pbtxt とモデルファイルを含むディレクトリをそのまま別環境にコピーすれば動作するため、環境間での設定の共有も容易になります。 推論単体の性能検証 従来の構成では推論性能の測定にはAPI全体への検証が必要であり、前処理・後処理の影響を含んだ結果しか得られませんでした。 Triton公式の Perf Analyzer を使用することで推論部分のみの性能を切り出して測定できます。Perf AnalyzerはTritonで実行される機械学習モデルの推論パフォーマンスを測定・最適化するためのCLIツールです。汎用的な負荷試験ツールでTritonをテストする場合はテンソル形式でのリクエスト組み立てを自前で実装する必要があります。一方Perf Analyzerではモデルのメタデータを自動取得してリクエストを生成するためその実装が不要です。さらにサーバー内部のレイテンシをqueueとcomputeに分解して出力します。queueは推論スケジューラのキュー内でリクエストが待機していた時間を示し、computeは入力データの準備、実際の推論実行、出力データの取得にかかった時間を示します。この分解によりボトルネックの切り分けが容易になり、モデル更新時のパフォーマンス確認や推論パラメータの調整に役立ちます。 ジャンル診断APIへの導入経緯 ここではなぜジャンル診断APIにTritonを導入したのかを説明します。 ジャンル診断APIは「ファッションで恋する」を実現するZOZOマッチの根幹となる機能です。このAPIには低レイテンシとスケーラビリティの2つの要件がありました。 オンボーディング時に使用されるAPIのため遅延があるとユーザー離脱につながります。またプロフィール画像を変更するたびに診断が実行されるためユーザー増加に伴いスケーラビリティも必要です。前述の通りTritonはC++実装やDynamic Batchingにより低レイテンシと高スループットを実現でき、これらの要件に適していると判断しました。 さらにTritonは社内での導入実績がありませんでした。既存の本番サービス(ZOZOTOWN、WEAR等)は利用者数が多く、問題発生時の影響範囲が大きくなります。一方ZOZOマッチはサービス立ち上げ段階であり、ZOZOTOWNやWEARなどの既存サービスと比較するとユーザー数が少ないです。また新規構築のため既存システムとの依存関係も少なく、新技術の検証に適していると判断しました。 以降ではジャンル診断APIのシステム構成とTritonを利用した推論API開発時に遭遇した課題について説明します。 システム構成 全体構成 本システムではAPIサーバーとTritonを分離する構成を採用しました。前述の「 処理の分離による保守性向上 」で説明した通り、この構成により責務の明確化とボトルネックに応じた柔軟なスケーリングが可能になります。 リクエストの流れ 次の図にリクエストの流れを示します。 ユーザーが自分の全身画像をプロフィール画像として登録するとBackend Serverがその画像データをAPIサーバーに送信します。APIサーバーはリサイズと正規化の前処理を行いgRPCプロトコルでTritonに推論リクエストを送信します。Tritonは12種類のファッションジャンルそれぞれに対して0〜1のスコアを返します。スコアが高いほどそのジャンルに該当する可能性が高いことを示します。以下はレスポンス例です。 { " genres ": [ { " genre_id ": 1 , " score ": 0.045 } , { " genre_id ": 2 , " score ": 0.696 } , ... { " genre_id ": 12 , " score ": 0.333 } ] } APIサーバーがこの診断結果をBackend Serverに返し、最終的にユーザーへレスポンスが返却されます。なおオンボーディング時に表示される選択肢画像についてはバッチ処理で事前にスコアを算出済みのためリアルタイムでの推論は不要です。リアルタイム推論が必要なのはユーザーがプロフィール画像を登録・変更した場合のみです。 通信プロトコルの選定 上記の通りAPIサーバーとTriton間の通信にはgRPCを採用しています。ここではTritonがサポートするプロトコルとgRPCを選定した理由を説明します。 Tritonがサポートするプロトコル TritonはHTTPとgRPCの2種類のプロトコルで推論リクエストを受け付けます。HTTPはポート8000、gRPCはポート8001で公開されます。どちらのプロトコルも同じ推論機能を提供しますがパフォーマンス特性が異なります。 HTTPとgRPCの比較 TritonのHTTPエンドポイントはHTTP/1.1ベースで、curlや各種HTTPライブラリで簡単にリクエストでき、ブラウザからも直接アクセスできるため広く利用されています。一方gRPCはHTTP/2ベースでサーバー間の内部通信に適していますが、ブラウザAPIでサポートされていないためブラウザから直接呼び出すことができません。しかし本システムではAPIサーバーからTritonへのサーバー間通信のみを対象としておりブラウザからのアクセスは不要なため、この制約は問題になりません。 gRPCがサーバー間通信に適している理由はHTTP/2による多重化です。HTTP/1.1では1つのTCP接続で同時に1つのリクエストしか処理できないため、高頻度のリクエストでは接続のオーバーヘッドが課題となります。HTTP/2では単一の接続上で複数のリクエストを同時に処理できます。本システムではAPIサーバーからTritonへ頻繁にリクエストを送信するためこの多重化の特性が有効です。 一方でgRPCを採用する際の注意点があります。HTTP/2の長時間接続がKubernetesの負荷分散と相性が悪いという問題です。この課題と解決策については後述の「 課題1:gRPCで負荷分散されない 」で説明します。 Kubernetesリソースの構成 MLOpsブロックではパブリッククラウドにGoogle Cloudを使用し Google Kubernetes Engine (以下、GKE)上にサービスを構築しています。ここではTritonをGKE上でデプロイするために作成したKubernetesリソースを解説します。TritonをGKE上で動かすために以下のリソースを作成しました。 Deployment :Triton Podの稼働 Service :Triton Podへのネットワークアクセスの提供 ServiceAccount :GCSアクセスのための認証設定 ScaledObject :KEDAによるオートスケーリングの設定 Tritonコンテナの起動設定 TritonをデプロイするためのDeployment構成を解説します。まず全体のマニフェストを示し、その後に各設定の詳細を説明します。 Deploymentマニフェスト apiVersion : apps/v1 kind : Deployment metadata : name : triton-inference-server labels : app : triton-inference-server spec : selector : matchLabels : app : triton-inference-server template : metadata : labels : app : triton-inference-server spec : serviceAccountName : triton-inference-server affinity : nodeAffinity : requiredDuringSchedulingIgnoredDuringExecution : nodeSelectorTerms : - matchExpressions : - key : cloud.google.com/gke-nodepool operator : In values : - "<triton-nodepool-name>" tolerations : - key : "dedicated" operator : "Equal" value : "<triton-nodepool-name>" effect : "NoSchedule" initContainers : - name : gcloud image : gcr.io/google.com/cloudsdktool/cloud-sdk:418.0.0 env : - name : MODEL_REPOSITORY_PATH value : gs://your-bucket/model_repository - name : MODEL_DIR value : "/tmp/models/" volumeMounts : - name : models mountPath : "/tmp/models/" command : - "/bin/sh" - "-c" - "gsutil cp -r $(MODEL_REPOSITORY_PATH)/* $(MODEL_DIR)" containers : - name : triton-inference-server image : "nvcr.io/nvidia/tritonserver:25.03-py3" imagePullPolicy : IfNotPresent resources : limits : cpu : 3000m memory : 3Gi command : [ "tritonserver" ] args : - "--model-store=/tmp/models/" - "--trace-config" - "mode=opentelemetry" - "--trace-config" - "opentelemetry,url=http://$(DD_AGENT_HOST):4318/v1/traces" - "--trace-config" - "rate=1" - "--trace-config" - "level=TIMESTAMPS" - "--grpc-max-connection-age=30000" - "--grpc-max-connection-age-grace=10000" env : - name : DD_AGENT_HOST valueFrom : fieldRef : fieldPath : status.hostIP volumeMounts : - name : models mountPath : "/tmp/models/" ports : - containerPort : 8000 name : http - containerPort : 8001 name : grpc livenessProbe : initialDelaySeconds : 10 periodSeconds : 5 failureThreshold : 6 httpGet : path : /v2/health/live port : http readinessProbe : initialDelaySeconds : 10 periodSeconds : 5 failureThreshold : 6 httpGet : path : /v2/health/ready port : http volumes : - name : models emptyDir : {} securityContext : runAsUser : 1000 fsGroup : 1000 以下、マニフェストの各設定について詳しく解説します。 InitコンテナとVolume Mountによるモデル共有 本システムでは Initコンテナ でGCSからMLモデルをダウンロードしています。InitコンテナはPod内でアプリケーションコンテナの前に実行されるコンテナで、マニフェストの initContainers フィールドで定義します。ダウンロード先のディレクトリをTritonコンテナでもVolume MountすることでダウンロードしたMLモデルをTritonから参照できる仕組みです。 initContainers : - name : gcloud image : gcr.io/google.com/cloudsdktool/cloud-sdk:418.0.0 env : - name : MODEL_REPOSITORY_PATH value : gs://your-bucket/model_repository - name : MODEL_DIR value : "/tmp/models/" volumeMounts : - name : models mountPath : "/tmp/models/" command : - "/bin/sh" - "-c" - "gsutil cp -r $(MODEL_REPOSITORY_PATH)/* $(MODEL_DIR)" containers : - name : triton-inference-server volumeMounts : - name : models mountPath : "/tmp/models/" volumes : - name : models emptyDir : {} emptyDir はPod起動時に作成される一時ボリュームで同一Pod内のコンテナ間でデータを共有できます。 Triton Pod起動時の処理の流れは次の通りです。 Initコンテナが起動し、GCSからモデルリポジトリ全体を /tmp/models/ にダウンロード Initコンテナが正常終了 アプリケーションコンテナ(Triton)が起動し、同じ /tmp/models/ からモデルをロード この構成によりモデルファイルをコンテナイメージに含める必要がなく、モデル更新時もイメージの再ビルドが不要になります。 OpenTelemetryによるトレース設定 本システムではDatadog APMでモニタリングを行っています。Tritonは OpenTelemetry (以下、OTel)形式でのトレース出力に対応しています。そのためTritonコンテナ起動時に以下の引数を指定することでDatadog APMと連携できます。 args : - "--trace-config" - "mode=opentelemetry" - "--trace-config" - "opentelemetry,url=http://$(DD_AGENT_HOST):4318/v1/traces" - "--trace-config" - "rate=1" - "--trace-config" - "level=TIMESTAMPS" env : - name : DD_AGENT_HOST valueFrom : fieldRef : fieldPath : status.hostIP 各パラメータの意味は次の通りです。 パラメータ 設定値 説明 mode opentelemetry OTelのAPIを使用してトレースを出力 opentelemetry,url http://$(DD_AGENT_HOST):4318/v1/traces トレースデータの送信先 rate 1 N番目のリクエストごとにトレース。 1 は全リクエスト、 1000 なら1000件に1件(デフォルト: 1000) level TIMESTAMPS 各リクエストの実行タイムスタンプを記録 DD_AGENT_HOST には status.hostIP (NodeのIP)を指定しています。Datadog Agentの推奨構成ではUNIXドメインソケットを使用します。しかしTriton(25.03時点)はOTLP Exporter経由のトレース送信においてUNIXドメインソケットに対応しておらず、HTTPプロトコルのみに対応しています。そのためホストIPとポートを指定する構成としました。 ヘルスチェック設定 livenessProbe : httpGet : path : /v2/health/live port : http readinessProbe : httpGet : path : /v2/health/ready port : http Tritonがサポートするヘルスチェックエンドポイントを livenessProbe、readinessProbe に設定しています。 /v2/health/live はTritonがリクエストを受け取れる状態かを確認し /v2/health/ready はすべてのモデルが正常にロードされたかを確認します。 セキュリティ設定 securityContext : runAsUser : 1000 fsGroup : 1000 セキュリティの観点からコンテナは非rootユーザーで実行しています。 runAsUser でコンテナ内のプロセスを実行するユーザーIDを指定しrootでの実行を避けています。 fsGroup でマウントされたボリュームのグループ所有権を同じIDに設定することで非rootユーザーでもモデルファイルへアクセスできるようにしています。 Triton Podへの接続設定 APIサーバーからTritonに接続するためのServiceを作成します。 KubernetesのServiceは複数のPodへのアクセスを抽象化するリソースです。通常のClusterIP ServiceではServiceに仮想IP(Cluster IP)が割り当てられ、クライアントはこの単一のIPアドレスにアクセスします。コンテナからの通信は kube-proxy によりDNATされ背後のPodにロードバランシングされます。 一方 Headless Service は clusterIP: None を指定したServiceであり仮想IPが割り当てられません。DNS名前解決ではServiceに紐づくすべてのPodのIPアドレスが直接返されます。これによりクライアント側で接続先のPodを選択できます。 本システムではHeadless Serviceを使用しています。通常のClusterIP Serviceを使用しない理由はgRPCの負荷分散に関係しています。詳細は後述の「 課題1:gRPCで負荷分散されない 」で説明します。 以下がServiceのマニフェストです。 apiVersion : v1 kind : Service metadata : name : triton-inference-server labels : app : triton-inference-server spec : type : ClusterIP clusterIP : None ports : - port : 8000 targetPort : http name : http-inference-server - port : 8001 targetPort : grpc name : grpc-inference-server selector : app : triton-inference-server 前述の通り8000番ポート(HTTP)はヘルスチェック用、8001番ポート(gRPC)は推論リクエスト用に公開しています。 GCSの認証設定 InitコンテナがGCSからモデルをダウンロードするにはGCSへのアクセス権限が必要です。GKEでは Workload Identity を使用することでKubernetesのServiceAccountとGCPのサービスアカウントを紐付けられます。これによりPod内からサービスアカウントキーを管理することなくGCPリソースにアクセスできます。 apiVersion : v1 kind : ServiceAccount metadata : name : triton-inference-server annotations : iam.gke.io/gcp-service-account : <gcp-service-account>@<project-id>.iam.gserviceaccount.com オートスケーリングの設定 KEDAを使用してCPU使用率に基づくオートスケーリングを設定しています。KEDAはKubernetes Event-driven Autoscalingの略で様々なメトリクスに基づいてPod数を自動調整できます。 KEDAについては下記の記事でも触れられていますのでぜひこちらもご参照ください。 techblog.zozo.com apiVersion : keda.sh/v1alpha1 kind : ScaledObject metadata : name : triton-inference-server spec : scaleTargetRef : name : triton-inference-server maxReplicaCount : 5 minReplicaCount : 2 cooldownPeriod : 300 triggers : - type : cpu metricType : Utilization metadata : value : "50" この設定ではCPU使用率50%を超えるとスケールアウトし、最小2台〜最大5台の範囲でPod数を調整します。最小2台としているのは可用性を担保するためです。 モデルのディレクトリ構成 Tritonでモデルを管理するには所定のディレクトリ構成に従う必要があります。本システムではその制約に従い、GCS上のモデルリポジトリを次のような構成にしました。 model_repository/ └── genre_extract_torchscript/ # モデル名(config.pbtxtのnameを指定する場合は一致させる) ├── config.pbtxt # モデルの入出力形式やバッチサイズなどを定義 ├── 1/ # モデルのバージョン │ └── model.pt └── 2/ # モデルのバージョン └── model.pt ディレクトリ名がそのままモデル名として使われます。今回は genre_extract_torchscript です。config.pbtxtで name フィールドを指定する場合はディレクトリ名と一致させる必要があります。 1/ や 2/ はバージョンを示すディレクトリでバージョン番号をディレクトリ名として使用します。この構成により複数バージョンのモデルを同時に管理でき、バージョン切り替えも容易になります。 実際にロードするバージョンはconfig.pbtxtのversion policyで制御でき、デフォルトでは最新バージョンのみがロードされます。config.pbtxtの詳細は「 モデルの設定 」で説明します。 モデルの設定 Tritonではモデルごとに config.pbtxt を用意します。config.pbtxtではモデルの入出力形式やバッチサイズなどを定義します。ジャンル診断モデルで使用しているconfig.pbtxtを以下に示します。 name: "genre_extract_torchscript" platform: "pytorch_libtorch" max_batch_size: 1 input [ { name: "image_input" data_type: TYPE_FP32 format: FORMAT_NCHW dims: [ 3, 224, 224 ] } ] output [ { name: "genre_scores" data_type: TYPE_FP32 dims: [ 12 ] } ] この設定では224×224ピクセルのRGB画像を入力として受け取り、12次元のベクトルを出力します。本システムでは12種類のファッションジャンルに対するスコアに対応しています。 platform フィールドの pytorch_libtorch はTorchScript形式のPyTorchモデルを使用することを示しています。 input[].format フィールドのNCHW形式とは画像データの配列順序を表しており、N(バッチサイズ)、C(チャンネル数=3)、H(高さ=224)、W(幅=224)の順にデータが並びます。 前述の「 推論処理の高速化 」で説明したDynamic Batchingを有効にするには以下のように設定します。 dynamic_batching { max_queue_delay_microseconds: 100 } max_queue_delay_microseconds はバッチを形成するためにリクエストを待機する最大時間をマイクロ秒で指定します。この待機時間内に届いたリクエストをまとめて1回の推論で処理します。 本システムでは max_batch_size: 1 としてDynamic Batchingを無効にしています。リリース直後でリクエスト数が少なくバッチを形成するための待機時間がレイテンシ悪化につながるためです。リクエスト数が増加した場合は max_batch_size を増やし dynamic_batching を有効にすることでスループットを向上できます。 GPUで推論する場合は instance_group で kind: KIND_GPU を指定します。 instance_group [ { kind: KIND_GPU count: 2 } ] count はGPUごとに起動するモデルインスタンス数を指定します。本システムではCPUで推論しています。リリース直後はリクエスト数が少なくCPUでも十分な処理性能を確保できるためです。GPUはCPUと比べてコストが高いため、リクエスト数の増加によりCPUでの処理が追いつかなくなった段階でGPUへの移行を検討します。 推論ゲートウェイAPIの実装 推論ゲートウェイAPIはBackend Serverからのリクエストを受け取り推論結果を返すAPIです。本システムでは tritonclient ライブラリのgRPCクライアントを使用しています。 Triton接続の設定 tritonclientで推論リクエストを送信する際の設定項目は以下の通りです。 設定項目 必須/任意 説明 接続先URL 必須 Tritonサーバーのホストとポート。gRPCのデフォルトポートは8001 モデル名 必須 config.pbtxtの name フィールドと一致させる モデルバージョン 任意 使用するモデルのバージョン番号。省略時は最新バージョンを使用 タイムアウト 任意 リクエストのタイムアウト時間 gRPCチャンネル設定 任意 gRPCチャンネルに渡す引数 本システムではこれらの設定を以下のように定義しています。 # 接続先URL: KubernetesのHeadless Service名とgRPCポートを指定 triton_url = "triton-inference-server.match-genre-extract.svc.cluster.local:8001" # モデル名: config.pbtxtのnameフィールドと一致させる model_name = "genre_extract_torchscript" # モデルバージョン model_version = "1" # タイムアウト timeout_ms = 1000 # gRPCチャンネル設定: ロードバランシングポリシーをround_robinに設定 channel_args = [( "grpc.lb_policy_name" , "round_robin" )] channel_args でgRPCクライアントのロードバランシングポリシーを設定できます。値の指定によりTritonサーバーへの負荷分散の方法を指定できます。この設定の詳細は後述の「 課題1:gRPCで負荷分散されない 」で説明します。 StyleFeatureExtractorクラスの実装 Tritonへの推論リクエストを行うクラスを実装します。なおコード例では入出力名やデータ型といったモデル依存の設定値をハードコーディングしていますが、実際の運用では環境変数から取得しています。 import numpy as np import tritonclient.grpc as grpcclient from opentelemetry.propagate import inject from tritonclient.grpc import InferResult from api.env_settings import settings class StyleFeatureExtractor : _INPUT_NAME = "image_input" _OUTPUT_NAME = "genre_scores" _DATA_TYPE_FP32 = "FP32" def __init__ (self): self.triton_client = grpcclient.InferenceServerClient( url=settings.triton_url, channel_args=settings.channel_args ) def predict_score_and_vector (self, image_data: np.ndarray) -> list [MLGenreScore]: trace_headers = {} inject(trace_headers) inputs = [ grpcclient.InferInput( self._INPUT_NAME, image_data.shape, self._DATA_TYPE_FP32 ) ] inputs[ 0 ].set_data_from_numpy(image_data) outputs = [grpcclient.InferRequestedOutput(self._OUTPUT_NAME)] response: InferResult = self.triton_client.infer( model_name=settings.model_name, inputs=inputs, outputs=outputs, model_version=settings.triton_model_version, headers=trace_headers, timeout=settings.triton_timeout_ms, ) scores = response.as_numpy(self._OUTPUT_NAME) _INPUT_NAME と _OUTPUT_NAME は前述の config.pbtxt で定義した入出力名と一致させます。 __init__ では前述の設定値を使って InferenceServerClient を初期化します。 predict_score_and_vector メソッドではW3C Trace Contextヘッダーを準備しています。これはDatadog APMとの連携に使用するもので、詳細は「 課題2-Datadog APMでトレースが連携されない 」で説明します。 推論リクエスト処理の実装 前述の StyleFeatureExtractor を使用して推論します。まず前処理としてダウンロードした画像をモデルの入力形式に変換します。tritonclientは入力データとしてnumpy配列を受け取るため、PyTorchテンソルから変換して渡します。 # 前処理 PREDICT_IMAGE_TRANSFORM = transforms.Compose([ transforms.Resize(( 224 , 224 )), transforms.ToTensor(), transforms.Normalize(mean=[ 0.485 , 0.456 , 0.406 ], std=[ 0.229 , 0.224 , 0.225 ]), ]) img_tensor = PREDICT_IMAGE_TRANSFORM(img) image_data = img_tensor.unsqueeze( 0 ).numpy() # 推論リクエスト ml_genre_scores = STYLE_FEATURE_EXTRACTOR.predict_score_and_vector(image_data) # 後処理... transforms.Resize で224×224ピクセルにリサイズし、 transforms.ToTensor でPyTorchテンソルに変換します。 transforms.Normalize ではImageNetデータセットの平均と標準偏差で正規化しています。最後にtritonclient用として unsqueeze(0) でバッチ次元を追加し numpy() でnumpy配列へ変換します。 推論リクエストでは前述の StyleFeatureExtractor の predict_score_and_vector メソッドを呼び出してTritonに送信します。 後処理では、Tritonから返却された12種類のファッションジャンルに対するスコアを、サービス要件に合わせたジャンルIDへマッピングして診断結果を生成します。詳細は本記事のスコープ外のため省略します。 以上が推論ゲートウェイAPIの実装です。 導入時の課題と解決策 ここまでTritonの導入方法を説明しましたが、導入にあたりいくつかの課題に直面しました。本節ではそれらの課題とその解決策を説明します。 課題1:gRPCで負荷分散されない Tritonサーバーでデプロイした推論APIの性能を測るために実施した負荷試験においてTriton Podを1台から複数台構成にしても処理できるリクエスト数がほとんど増加しませんでした。Pod数を増やして、適切に負荷分散がされれば、処理できるリクエスト数は増加します。 調査の結果、負荷が均等に分散されておらず、特定のPodのCPU使用率が100%近くに達する一方で他のPodはほとんど使用されていないことが判明しました。 原因 この問題はgRPCの特性およびKubernetesの負荷分散の仕組みに起因していました。 gRPCはHTTP/2上に構築されたL7プロトコルです。HTTP/1.1では1つのTCPコネクション上で同時に1つのリクエストしか処理できないため複数の並行リクエストには複数のTCPコネクションが必要です。一方HTTP/2では単一の長寿命なTCPコネクションを維持しその上で複数のリクエストを多重化します。 しかしこの特性はKubernetesの負荷分散との相性に問題があります。KubernetesにおいてService経由のトラフィックの負荷分散を担うのはkube-proxyです。kube-proxyはiptablesやIPVSを使用してTCPコネクション単位で負荷分散を行いコネクションが確立されるタイミングで振り分け先のPodを決定します。ただしkube-proxyはL4での負荷分散を行うため、L7プロトコルであるgRPCの個々のリクエストを認識できません。HTTP/2では単一のTCPコネクションが長時間維持されるためそのコネクション上のすべてのgRPCリクエストが最初の振り分け先Podへ集中してしまいます。 次の図に問題のある構成を示します。クライアントからのTCPコネクションが1つのPodに集中し他のPodはアイドル状態になっています。 解決策 gRPCの負荷分散の問題に対しては主に2つの解決策があります。 1つ目は Linkerd や Istio などのサービスメッシュの導入です。各Podにサイドカープロキシを注入してL7で負荷分散を行うためアプリケーションコードを変更せずにgRPC負荷分散を実現できます。一方で各Podにサイドカーが必要となりCPUやメモリを消費します。またコントロールプレーンの運用も必要になります。 2つ目はクライアントサイド負荷分散です。gRPCクライアントの設定でHeadless ServiceとDNSベースの負荷分散を利用します。サイドカーを経由しないため高いパフォーマンスを実現できます。一方でクライアント側での設定変更が必要であり負荷分散アルゴリズムはround_robinなど基本的なものに限定されます。 本システムではクライアントサイド負荷分散を採用しました。追加コンポーネントが不要で既存のKubernetesリソースとgRPCクライアントの設定変更のみで実現できるためです。また本システムではAPIサーバーからTritonへの内部通信のみが対象でありクライアント側の設定変更が可能です。推論処理の所要時間がほぼ一定であるためround_robinによる均等分散で十分な負荷分散の効果が得られると判断しました。具体的には次の2つの変更をしました。 1. Headless Serviceの採用 通常のClusterIP Serviceの代わりにHeadless Service( clusterIP: None )を使用しました。Headless ServiceはDNSクエリに対してPodのIPアドレスリストを直接返すためクライアント側で接続先を制御できます。 2. クライアント側でround_robinロードバランシングを設定 gRPCクライアントの設定で round_robin ロードバランシングポリシーを指定しました。これによりDNSで取得した複数のPod IPに対してリクエストを均等に分散できます。 tritonclient ライブラリでは InferenceServerClient の channel_args パラメータでgRPCチャンネルオプションを設定できます。 channel_args: list [ tuple [ str , str ]] = [( "grpc.lb_policy_name" , "round_robin" )] self.triton_client = grpcclient.InferenceServerClient( url=settings.triton_url, channel_args=settings.channel_args # round_robinを設定 ) grpc.lb_policy_name を round_robin に設定することでHeadless ServiceのDNSが返す複数のPod IPに対してリクエストごとに接続先を切り替えます。 次の図に解決後の構成を示します。クライアントがDNSから取得した複数のPod IPに対してround_robinでリクエストを分散しています。 スケールアウト時の負荷分散 上記のクライアントサイド負荷分散により起動時に存在するTriton Pod間での負荷分散は実現できました。しかし HPA などによるスケールアウトで新しいPodを追加した場合、そのPodへトラフィックを分散できない問題が残りました。 gRPCクライアントはすべてのSubchannelが切断された場合のみDNS名を再解決する仕様になっています。本システムではAPIサーバー起動時にTritonクライアントを生成しアプリケーション動作中は同じ接続を維持します。そのため既存の接続が維持されている限りDNS再解決は行われずスケールアウトで追加された新しいPodを検出できません。 この問題に対してはサーバー側で接続が存続できる最大時間を設定することで解決しました。gRPCは接続がクローズされるとDNS名を再解決するためサーバー側で定期的に接続を切断することでクライアントに再接続を促し、新しいPodを含めた負荷分散を実現できます。 具体的にはTritonの起動オプションで以下を設定しました。 --grpc-max-connection-age=30000 --grpc-max-connection-age-grace=10000 grpc-max-connection-age は接続が存在できる最大時間をミリ秒で指定するパラメータです。この時間を超えるとサーバーが接続を終了します。また grpc-max-connection-age-grace は接続終了後に処理中のRPCが完了するまでの猶予時間です。 30秒という値は新しいPodがReadyになってから最大30秒で負荷分散が開始されることを意味します。HPAによるスケールアウト自体に数分かかることを考えると30秒の遅延は許容範囲です。10秒という猶予時間は本システムの推論処理が非機能要件で1秒以内と定義されているため処理中のRPCを完了させるのに十分な時間です。 これらの変更によりPod数の増加に応じて線形にスループットが向上するようになりました。 課題2:Datadog APMでトレースが連携されない APIサーバーとTritonを分離した構成を導入したところDatadog APMでAPIサーバーとTritonのトレースを連携できない問題が発生しました。従来の一体型構成では1本のトレースとして追跡できていましたが、分離した構成では別々のトレースとして表示され同一リクエストの処理として紐付けられませんでした。障害調査時にエンドツーエンドでレイテンシを分析するためにはトレースの連携が必要でした。 原因 この問題はトレースコンテキストの伝播形式の違いに起因していました。 分散トレーシングではサービス間でトレース情報をHTTPヘッダーにより伝播し、複数のサービスを横断するリクエストを1本のトレースとして追跡します。しかしAPIサーバーとTritonでは伝播形式が異なっていました。 APIサーバー側:Datadog独自形式( x-datadog-* ヘッダー) Triton側: OpenTelemetry 形式のみサポート 各トレーシングツールで独自のヘッダー形式を使用していたため、サービス間でトレース情報が正しく伝播されず、トレースの分断が発生していました。 次の図は Monitor OpenTelemetry with Datadog and W3C Trace Context から引用した異なるトレーサーが混在する環境での問題を示しています。OTel TracerとDD Tracerがそれぞれ独自形式のTrace IDを使用するため各サービスのSpanがDatadogに送信されても同一トレースとして紐付けられません。図右側のAPM FlamegraphではDD Tracerを使用するSERVICE BのSpanのみが表示され、OTel Tracerを使用するSERVICE AとCは別トレース扱いになっています。本システムでも同様の問題が発生していました。 解決策 この問題を解決するために次の3つの対応をしました。 W3C Trace Contextによるトレース伝播 Triton専用NodeにDatadog AgentをDaemonSetで配置 Tritonの起動オプションでトレース設定 1. W3C Trace Contextによるトレース伝播 W3C Trace Contextは異なるトレーシングシステム間でトレース情報を共有するための標準規格です。HTTPヘッダー( traceparent 、 tracestate )を通じてトレースIDを伝播します。 APIサーバーからTritonへのリクエスト時にOTelの inject 関数を使用してトレース情報をヘッダーに埋め込みます。 from opentelemetry.propagate import inject def predict_score_and_vector (self, image_data: np.ndarray): # 現在のトレースコンテキストをW3C Trace Context形式でヘッダーに埋め込む trace_headers = {} inject(trace_headers) # Tritonへの推論リクエスト時にヘッダーを付与 response = self.triton_client.infer( model_name=self.model_name, inputs=inputs, outputs=outputs, headers=trace_headers, # W3C Trace Contextヘッダー timeout=settings.triton_timeout_ms, ) inject 関数はddtraceが管理する現在のスパンのトレースIDとスパンIDをW3C Trace Context形式のヘッダーに変換します。Triton側はこのヘッダーを読み取り同じトレースIDで新しいスパンを作成します。これによりddtraceで計装されたAPIサーバーとOTelで計装されたTritonの間でトレースが連携されます。本実装ではddtrace 2.21.8を使用しています 1 。 2. Triton専用NodeにDatadog AgentをDaemonSetで配置 TritonはOTel形式でトレースを出力するためOpenTelemetry Protocol(以下、OTLP)形式を受け取れるDatadog Agentが必要です。 KubernetesにおけるDaemonSetはクラスタ内の各NodeでPodが1つずつ稼働することを保証するリソースです。ログ収集やモニタリングエージェントなど各Nodeで実行が必要なシステム機能のデプロイに適しています。次の図にNode内でのトレース送信の流れを示します。 OTelで計装されたTritonからのトレースは同一Node上のDatadog AgentがOTLP形式で受け取りDatadog Backendに送信します。Tritonが稼働するNodeにDaemonSetでDatadog Agentを配置しport 4318でOTLPリクエストを受け取るよう設定しました。以下はその設定例です。 apiVersion : apps/v1 kind : DaemonSet metadata : name : triton-datadog-agent spec : template : spec : affinity : nodeAffinity : requiredDuringSchedulingIgnoredDuringExecution : nodeSelectorTerms : - matchExpressions : - key : cloud.google.com/gke-nodepool operator : In values : - <triton-nodepool-name> containers : - name : datadog-agent image : datadog/agent:7.56.1 env : - name : DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT value : "0.0.0.0:4318" ports : - containerPort : 4318 hostPort : 4318 name : traceporthttp protocol : TCP なお hostPort を使用するとNodeのポートが外部に公開されクラスタへのネットワーク侵入経路となる可能性があるため絶対に必要な場合を除き避けることが推奨されています。本記事の構成ではTritonが稼働するNode上でのみDatadog Agentと通信するために使用しておりプライベートなGKEクラスタ内での利用を想定しています。パブリックなクラスタで利用する場合はファイアウォールルールやネットワークポリシーで適切にアクセス制限を行ってください。 3. Tritonの起動オプションでトレース設定 Triton側では起動オプションでOTelによるトレース出力を有効にしました。 --trace-config level=TIMESTAMPS --trace-config rate=1 --trace-config mode=opentelemetry --trace-config opentelemetry,url=http://$(DD_AGENT_HOST):4318/v1/traces これらの設定によりAPIサーバーからTritonへの流れが1本のトレースとして表示され、スパンごとのボトルネック分析が可能になりました。次の図は本システムで実際にDatadog APMに表示されたトレースです。 APIサーバーとTritonのSpanが1本のトレースとして連携されていることがわかります。 課題3:PyTorchモデルがロードされない Tritonを起動したところPyTorchモデルのロードに失敗してTritonが起動しませんでした。モデルファイルは正しいパスに配置されているにもかかわらず以下のようなエラーが出力されました。 UNAVAILABLE: Internal: failed to load model 'genre_extract_torchscript': PytorchStreamReader failed locating file constants.pkl: file not found このエラーは、通常のPyTorchモデルを、TorchScript形式のモデルとして読み込もうとした場合に発生します。 原因 TritonのPyTorch BackendはTorchScript形式のモデルのみをサポートしています。配置していたモデルはTorchScript形式ではなかったためロードできませんでした。 解決策 PyTorch Backend ではすべてのPyTorchモデルをTorchScript形式に変換する必要があります。本システムでは torch.jit.script を使用してモデルを変換しました。 # Before: PyTorchモデルの重みをそのまま保存 torch.save(model.state_dict(), 'best_model.pt' ) # After: TorchScript形式で保存 jit_script = torch.jit.script(model).eval() jit_script.save(f "{model_dir}/best_model_torchscript.pt" ) この変更によりモデルが正常にロードされました。 導入効果 本節では本番運用を通じて実際に確認できた効果を紹介します。 性能面では同じ総Pod数で目標レイテンシ以内に処理可能なリクエスト数が約38%向上しました。Triton導入前はAPI Pod 4台(CPU)で24rpsが上限でした。導入後はAPI Pod 3台とTriton Pod 1台(計4台、いずれもCPU)で33rpsまで処理できるようになりました。 運用面では config.pbtxt による宣言的な設定管理により設定変更が容易になりました。 項目 Before(Triton導入前) After(Triton導入後) バッチサイズ設定 コード修正が必要 max_batch_size で設定 推論インスタンス数設定 ワーカー数やPod数を調整 instance_group で設定 Dynamic Batching設定 自前実装が必要 dynamic_batching で設定 モデル入出力定義 コード内で暗黙的に定義 input / output で設定 モデルバージョン管理 独自の命名規則で運用 ディレクトリ構造で標準化 またTriton公式のPerf Analyzerにより前処理・後処理の影響を排除した推論単体の性能検証が可能になりました。 スケーラビリティの面ではジャンル診断APIの低レイテンシとスケーラビリティの要件に備えた体制が整いました。APIサーバーとTritonで責務を分離したことで水平スケールや垂直スケールをそれぞれ独立して適用できます。推論がボトルネックになればTritonにGPUを割り当て、前処理がボトルネックになればAPIサーバーのCPUを増強するといった柔軟なリソース配分が可能です。Dynamic Batchingの有効化も設定変更だけで対応できます。 本番稼働から半年以上が経過し大きな障害なく安定稼働しています。社内でもTriton導入を検討するチームがあり本番運用の実績とナレッジを共有できる状態になりました。 導入時の注意点 コスト面の考慮 Tritonを導入する際はコスト面での検討が重要です。リクエスト数が少ない場合、逆にコスト増となる可能性があります。 MLOpsブロックではAPI系サービスの可用性担保のためAPIサーバーを最小3台構成としており、導入前は3台で運用していました。導入後はAPIサーバー3台に加えて可用性担保のためTriton Pod 2台が必要になり合計5台構成となりました。同スペックのPodを使用しているため単純計算で約67%のコスト増です。 一方前述の性能検証で示した通りTriton導入により同Pod数でスループットが約38%向上しています。つまりAPIサーバーのみで同等のスループットを得るには5台以上のPodが必要となる計算です。リクエスト数の増加でスケールアウトが必要な場合、Triton構成はコスト効率が良くなります。 現時点ではサービス開始直後でリクエスト数が少なく最小構成での運用となっているためコスト削減効果があるとは言えません。ただし運用・保守性の向上というメリットは得られています。またリクエスト増加時には同Pod数でスループットが約38%向上している点に加えGPUの活用やDynamic Batchingの有効化によるさらなるコスト効率の改善が見込まれます。 まとめ 本記事ではZOZOマッチのジャンル診断APIにTritonを導入した背景から課題と解決策、導入効果までを紹介しました。 社内初の導入で様々な課題に直面しましたがそれぞれ解決できました。 結果として同Pod数でスループットが約38%向上しconfig.pbtxtによる宣言的管理で運用性が向上しました。またAPIサーバーとTritonで役割を分担することで責務が明確になりました。 導入前は社内での運用実績がなく、トラブル発生時の対応やナレッジ不足を懸念していました。しかし本番稼働から半年以上が経過し大きな障害なく安定稼働しています。社内でもTriton導入を検討するチームがあり本番運用の実績とナレッジを共有できる状態になりました。 本記事で紹介した導入時の知見がこれからTritonの導入を検討している方の参考になれば幸いです。 最後になりますがZOZOでは一緒にサービスを作り上げてくれる方を募集中です。MLOpsブロックでも絶賛採用を行っているためご興味ある方は以下のリンクからぜひご応募ください。 hrmos.co 将来のバージョンではこの手動でのヘッダー埋め込みが不要になる可能性があります。詳細は GitHub Discussion を参照してください。 ↩
SCSK いわいです。 前回はRaspberry Pi 5で気温/気圧/湿度センサーを使って測定し、 Webで表示、DBに取得データを検索するシステムを構築しました。 今回は測定したデータからAIを使って気温/気圧/湿度をリアルタイム予測してみます。 今回は前回セットアップした環境をそのまま流用します。 Raspberry Piで気温/気圧/湿度計測 結果をWebサーバで見てみよう Raspberry Pi 5で気温/気圧/湿度センサーを使って測定し、Webで表示するシステムを構築したいと思います。DBに取得データを格納し、あとから検索できるといろいろ便利です。 blog.usize-tech.com 2025.12.08   過去データから現在値を予測す る 過去データから現在値を予測します。これには機械学習結果からの推論(教師あり学習)を使用します。 温度/湿度/気圧の予測のために「線形回帰」「非線形回帰」「LSTM(Long Short Term Memory)」の3つを試してみます。 「線形回帰」はデータの関係性が直線(線形)の場合で表せる場合に使われます。 ⇒グラフにした時にだいたいまっすぐな線で表せる 例えば商品の売上と広告費用などが該当します。 「非線形回帰」は「データの関係性が曲線(非線形)」で表せる場合に使われます。 ⇒グラフにした時に曲がった線で表せる 例えば投げたボールの高さと時間の関係が該当します。 「LSTM」は時系列データや文章など、時間の流れや順番が大事なデータをうまく使える機械学習のモデルです。 ⇒過去と今の情報を組み合わせて考えられる仕組み 例えば株価の予測、文章の意味理解が該当します。 普通の再帰型ニューラルネットワークは「昔のこと」をすぐ忘れてしまいますが、LSTMは 「長い・短い記憶をうまく使い分けできる」ので、長い文章や長期的な傾向も扱えるようです。 なんだか今回のテーマに合致しそうな気配です。 ざっくりまとめると以下になります。 方式 得意なデータ 値の関係 過去の情報との関係 例 線形回帰 数値 & シンプル 直線 考慮しない 商品の売上と広告費用 非線形回帰 (RF予測) 数値 & 複雑 曲線 考慮しない 投げたボールの高さと経過時間 LSTM 時系列・文章・音声等 複雑 + 順番 重視する 株価予測、文書生成 これらの3つの方式を実装してどの予測値が実測値に近いか確認してみます。   システムのイメージ 前回作成したFlaskアプリケーションに機能を追加します。今回はWeb画面に測定結果と予測値を表示します。 蓄積した測定結果から予測モデルを作成して、予測モデルを使ってリアルタイムで現在の温度/湿度/気圧を予測してみます。 イメージはこんなカンジで。 今回のシステムで導入する機能と各ライブラリの説明は以下のとおりです。 機能 ライブラリ 説明 Webサーバ Flask 軽量なWebフレームワーク。センサー値や予測結果をWebアプリとしてブラウザに表示。 センサー通信 Smbus2 ラズパイとI2C通信する。BME280と通信するために利用。   bme280 Bosch製の温湿度・気圧センサー BME280用のPythonライブラリ。データ取得する。 データ保存 sqlite3 軽量な組み込み型データベースSQLiteを操作するためのライブラリ。計測データをローカルDBに保存・検索するために利用。 時刻処理 datetime 計測時刻の記録に利用。ローカルDBに保存するtimestampを生成。 ファイル管理 (new) os OSレベルの操作。LSTMモデル/Scalerファイル(ディープラーニング結果ファイル)の存在確認に利用。   joblib Pythonオブジェクトを高速に保存・読み込みするためのライブラリ。学習済みScalerを保存・読み込みするために利用。 数値処理 (new) numpy 数値計算ライブラリ。線形回帰やLSTMに渡すデータを配列に整形するために利用。 機械学習 (new) scikit-learn 線形回帰、非線形回帰をつかった予測のために利用。   tensorflow LSTM予測のために利用。 前回導入済みのライブラリに加え、ファイル管理用ライブラリ(joblib)、数値処理ライブラリ(numpy)、機械学習用ライブラリ(scikit-learn、tensorflow)を追加します。ファイル管理用ライブラリであるosはデフォルトでインストールされています。   過去のデータからLSTMモデルを作成する LSTMモデルを作成するために高速演算用ライブラリのnumpy、機械学習ライブラリのscikit-learnとtensorflow、ファイル生成用ライブラリjoblibをインストールします。 sudo pip install numpy –break-system-packages  sudo pip install tensorflow –break-system-packages sudo pip install scikit-learn –break-system-packages sudo pip install joblib –break-system-packages ローカルに測定結果を蓄積しているDBファイルを元にLSTMモデルとScalerファイルを生成します。 Scalerファイルとは学習時のデータの最大値/最小値、標準偏差等を求めて、それぞれのデータをを0~1の数値に 置き換えるための定義ファイルとのこと。 この定義ファイルがないとそもそもどんな情報を元に学習した結果なのかわからず、 予測もできないため、学習時と予測時には同じ定義ファイルを使う必要があります。 import sqlite3 import numpy as np import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense from sklearn.preprocessing import MinMaxScaler import joblib # スケーラー保存用 # ====== 設定 ====== DB_FILE = "bme280_data.db" TREND_WINDOW = 10 # LSTM の timesteps MODEL_FILE = "bme280_lstm_model.keras" SCALER_FILE = "bme280_scaler.save" # ====== SQLite からデータ取得 ====== def load_data(): with sqlite3.connect(DB_FILE) as conn: c = conn.cursor() c.execute(""" SELECT temperature, humidity, pressure FROM measurements ORDER BY timestamp ASC """) rows = c.fetchall() data = np.array(rows, dtype=np.float32) return data # shape = (n_samples, 3) # ====== LSTM 用系列データ作成 ====== def create_sequences(data, window_size): X, y = [], [] for i in range(len(data) - window_size): X.append(data[i:i + window_size]) y.append(data[i + window_size]) return np.array(X), np.array(y) # ====== メイン処理 ====== def main(): # --- データロード --- data = load_data() if len(data) <= TREND_WINDOW: raise ValueError("データ数が TREND_WINDOW 以下です") # --- 正規化 --- scaler = MinMaxScaler() data_scaled = scaler.fit_transform(data) # --- 系列化 --- X, y = create_sequences(data_scaled, TREND_WINDOW) print("X shape:", X.shape) # (samples, timesteps, features) print("y shape:", y.shape) # (samples, features) # --- LSTM モデル --- model = Sequential([ LSTM(32, input_shape=(X.shape[1], X.shape[2])), Dense(X.shape[2]) # temperature, humidity, pressure ]) model.compile( optimizer="adam", loss="mse" ) model.summary() # --- 学習 --- model.fit( X, y, epochs=100, batch_size=16, verbose=1 ) # --- 保存(.keras 形式) --- model.save(MODEL_FILE) joblib.dump(scaler, SCALER_FILE) print("✅ モデル保存:", MODEL_FILE) print("✅ スケーラー保存:", SCALER_FILE) # ====== 実行 ====== if __name__ == "__main__": main() 予測用に直近10件のデータを測定し、学習結果から次の1件のデータを予測するLSTMモデルを作成しています。 これで温度/湿度/気圧予測の準備ができました。   Pythonスクリプト作成/実行 今回もChatGPTを利用してPythonスクリプトを作りました。 前回の構成に過去のデータから現在の気温/湿度/気圧を線形予測、RF予測、LSTM予測した結果を 表示する機能を追加しています。 線形予測は直近5000件のデータから現在の各値を予測、RF予測は過去の測定値からランダムな特徴やデータの一部を選定、 50パターンの決定木 = forestを生成して、その平均値から各値を予測するように設計しています。 Raspberry PiでWebサーバを起動します。   実行結果 上から「実測値」、「線形予測値」、「RF予測値」、「LSTM予測値」を表示しています。 線形予測はだいぶ外れた値、RF予測は実測値にかなり近い値、LSTM予測は若干ずれた値となりました。 現実では温度/湿度/気温には以下の傾向があります。 温度:平坦⇒微増/微減⇒平坦 湿度:ジグザグ 気圧:ほぼ一定+揺れ この現象に対してそれぞれのリアルタイム予測はざっくりと以下のように動きます。 線形予測:微増/微減したら次も微増/微減するはず ⇒そもそも現実と合致してないが傾向はわかる RF予測:大体前と同じぐらいの値では? ⇒ほぼ正解 LSTM予測:過去の値からみてちょっと変えたほうがそれっぽい? ⇒賢すぎてノイズが発生することもあるがクセは覚えられる 今回のケースではそれぞれの予測は得意な分野があることがわかりました。 線形予測は「傾向予測、急激な変化を検出する」、RF予測は「リアルタイム予測、直近予測をする」、 LSTM予測は「周期的な予測、1時間後の予測をする」が得意なようです。 勉強になりました。
1 はじめに2 背景3 取り組んだ内容3.1 ライフイベントの推定結果を特徴量に追加3.2 階層構造を明示したマルチ タスク学習モデルの導入3.3 予測確率の補正3.4 配信サイズの動的変更4 オフラ...

動画

該当するコンテンツが見つかりませんでした

書籍