BigQuery ML でモデルは作れた。でも、アプリから「今この 1 件」の予測を取りたい。
ML.PREDICT はテーブルをまとめてスコアリングするには最適ですが、Web アプリケーションがユーザー 1 人分の予測をリアルタイムに必要とする場面には向きません。当記事では、BigQuery ML で学習したモデルを Cloud Storage にエクスポートし、Gemini Enterprise Agent Platform(旧 Vertex AI)の Endpoint にデプロイして、HTTPS 経由でオンライン予測を実行するまでを、実際に動かした結果とあわせて解説します。
概要
ML.PREDICTだけでは埋まらないもの
BigQuery ML は、テーブルから学習済みモデルへの最短経路です。CREATE MODEL 文を書けば、特徴量のエンコードと学習ループは BigQuery が引き受け、ML.PREDICT がテーブルをスコアリングします。
しかし、その最後の部分が同時に制約でもあります。ML.PREDICT は SQL 関数です。BigQuery のジョブとして、テーブルに対して、BigQuery のレイテンシ特性で動作します。昨夜分のトランザクションをまとめてスコアリングする用途には最適です。一方、Web アプリケーションが特定のユーザー 1 人の予測を今すぐ必要とする場面には向きません。
このギャップを埋めるのが EXPORT MODEL です。BigQuery ML のモデルを TensorFlow SavedModel として Cloud Storage にシリアライズします。モデルが通常のアーティファクトのディレクトリになれば、Agent Platform のサービング機能一式をそのまま適用できます。ビルド済みサービングコンテナ、Model Registry、オートスケーリングとトラフィック分割を備えた Endpoint、そしてモデルモニタリングです。
結果として得られるのは、明快な役割分担です。データがある場所で学習し、トラフィックがある場所で配信する、ということです。
当記事は、BigQuery ML の基礎知識があることを前提としています。
構築するもの
Palmer Penguins データセットを用いた多クラス分類モデルを構築します。生息する島、体の計測値、性別から、ペンギンの種類を予測します。モデルは小さく、精度も特筆すべきものではありません。当記事の主題はモデリングではなく、BigQuery から Endpoint までの経路だからです。
BigQuery 公開データセット
│ EXPORT DATA
▼
gs://<bucket>/data/*.csv
│ bq load
▼
BigQuery テーブル
│ CREATE MODEL (LOGISTIC_REG)
▼
BigQuery ML モデル ──ML.PREDICT──► SQL 内でのバッチスコアリング
│ EXPORT MODEL
▼
gs://<bucket>/models/ TensorFlow SavedModel
│ gcloud ai models upload (+ ビルド済み tf2-cpu サービングコンテナ)
▼
Agent Platform Model Registry
│ endpoints create → deploy-model (+ 推論ドリフト検出)
▼
Agent Platform Endpoint ──POST :predict──► HTTPS 経由の単一予測
最初の 2 ステップにある Cloud Storage の往復は、厳密には必須ではありません。公開テーブルから直接学習することもできます。あえて含めているのは、実際のパイプラインではほぼ必ず Cloud Storage から学習データを読み込むこと、そして元データとエクスポート後のモデルを 1 つのバケットに並べて確認できることが理由です。
エクスポートできるモデルタイプ
BigQuery ML のすべてのモデルタイプがエクスポートできるわけではありません。また、エクスポート形式はタイプによって異なります。
| モデルタイプ | エクスポート形式 |
|---|---|
LOGISTIC_REG、LINEAR_REG、KMEANS、PCA、AUTOENCODER、MATRIX_FACTORIZATION
|
TensorFlow SavedModel |
BOOSTED_TREE_*、RANDOM_FOREST_*
|
XGBoost Booster |
DNN_*、DNN_LINEAR_COMBINED_*、AUTOML_*
|
TensorFlow SavedModel |
時系列モデル(ARIMA_PLUS)とリモートモデルはエクスポートできません。当記事では LOGISTIC_REG を使用するため、以降の工程はすべて TensorFlow が対象です。
事前準備
環境
- 課金が有効な Google Cloud プロジェクト
- BigQuery、Cloud Storage、Vertex AI の各 API が有効であること。2026年9月現在、Agent Platform の API 名は
aiplatform.googleapis.comのままであり、CLI もgcloud aiのままです。Google Cloud Next '26 での Vertex AI からの名称変更は、どちらにも影響していません - Cloud Shell、または
gcloudの認証が済んだローカルシェル
gcloud services enable \
bigquery.googleapis.com \
storage.googleapis.com \
aiplatform.googleapis.com
カスタムモデル配信の割り当てを確認する
Endpoint のデプロイには、リージョンごとのカスタムモデル配信用 CPU の割り当てが必要です。この機能を一度も使っていないプロジェクトでは、これが 0 のことがあります。
コンソールの IAM と管理 → 割り当てとシステム上限で Custom model serving を絞り込み、対象リージョンの custom_model_serving_n1_standard_cpus が 2 以上であることを確認してください。0 の場合は、この時点で引き上げを申請します。承認には 1 日ほどかかることがありますが、待っている間もモデルの登録までは進められます。
割り当て不足によるデプロイの失敗は即座に発生します。ノードのプロビジョニングを 10 分待たされてから失敗する他のエラーとは異なり、原因の判別は容易です。
変数の設定
以降の手順ではこれらの変数を繰り返し使用します。最初に設定します。
export PROJECT_ID=$(gcloud config get-value project)
export REGION="us-central1"
export DATASET="penguins_demo"
export TABLE="penguins"
export MODEL="penguins_species_model"
export BUCKET="gs://${PROJECT_ID}-bqml-export"
export ENDPOINT_NAME="penguins-endpoint"
gcloud config set ai/region "${REGION}"
データセットとバケットを同じロケーションに作成する
ここが、後からやり直すのが最も面倒な設定判断です。EXPORT MODEL は、BigQuery のデータセットとエクスポート先の Cloud Storage バケットが同じロケーションにあることを要求します。ロケーションをまたぐエクスポートはできません。
元テーブルである bigquery-public-data.ml_datasets.penguins は US マルチリージョンにあります。したがってデータセットも US である必要があり、バケットも同様です。us-central1 は US マルチリージョンとは別のロケーションであり、エクスポートは拒否されます。
bq --location=US mk --dataset "${PROJECT_ID}:${DATASET}"
gcloud storage buckets create "${BUCKET}" \
--location=US \
--uniform-bucket-level-access
作成後、両方が意図したロケーションにあることを確認します。デフォルト設定を引き継いで別のロケーションに作られていないかを、ここで確かめておきます。
bq show --format=prettyjson "${PROJECT_ID}:${DATASET}" | grep -i location
gcloud storage buckets describe "${BUCKET}" --format="value(location,locationType)"
いずれも US と multi-region が返ります。
サービスエージェントにバケットの読み取り権限を付与する
デプロイ時に Cloud Storage からエクスポート済みモデルを読み取るのは、操作しているユーザーではありません。Agent Platform のサービスエージェント、つまりプロジェクト内の Google 管理サービスアカウントです。このサービスを一度も使ったことがないプロジェクトでは、サービスエージェント自体がまだ存在せず、当然、作成したばかりのバケットに対する権限も持っていません。
export PROJECT_NUMBER=$(gcloud projects describe "${PROJECT_ID}" --format="value(projectNumber)")
gcloud beta services identity create \
--service=aiplatform.googleapis.com \
--project="${PROJECT_ID}"
gcloud storage buckets add-iam-policy-binding "${BUCKET}" \
--member="serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-aiplatform.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"
2つ目のコマンドはサービスエージェントを強制的に作成し、そのアドレスを出力します。
この手順を飛ばしても、ここではエラーになりません。デプロイ開始から約 10 分後に、Cloud Storage の権限エラーとして失敗します。その時点では、ノードのプロビジョニングを待ち終えた後です。
学習データの準備
公開データセットをCloud Storageにエクスポートする
EXPORT DATA はクエリ結果を直接 Cloud Storage に書き出すため、この工程も GoogleSQL の中で完結します。
一部の行では体重または性別が欠損しています。当記事ではこれらを除外して例をシンプルに保ちますが、実際のプロジェクトでは補完するかどうかを意識的に判断してください。
EXPORT DATA OPTIONS(
uri = 'gs://YOUR_BUCKET/data/penguins-*.csv',
format = 'CSV',
overwrite = true,
header = true
) AS
SELECT
species,
island,
culmen_length_mm,
culmen_depth_mm,
flipper_length_mm,
body_mass_g,
sex
FROM `bigquery-public-data.ml_datasets.penguins`
WHERE body_mass_g IS NOT NULL
AND sex IN ('MALE', 'FEMALE');
CLI から実行する場合は以下のとおりです。
bq query --use_legacy_sql=false \
"EXPORT DATA OPTIONS(
uri = '${BUCKET}/data/penguins-*.csv',
format = 'CSV',
overwrite = true,
header = true
) AS
SELECT species, island, culmen_length_mm, culmen_depth_mm,
flipper_length_mm, body_mass_g, sex
FROM \`bigquery-public-data.ml_datasets.penguins\`
WHERE body_mass_g IS NOT NULL AND sex IN ('MALE', 'FEMALE')"
URI 内の * は必須です。EXPORT DATA は結果が小さなファイル 1 つであっても、常にシャーディングします。実際には penguins-000000000000.csv が 1 つだけ生成されます。
詳細:公式ドキュメント(EXPORT DATA ステートメント)
CSVをテーブルにロードする
スキーマの自動検出がヘッダー行を読み取るため、スキーマを手書きする必要はありません。
bq --location=US load \
--source_format=CSV \
--autodetect \
"${PROJECT_ID}:${DATASET}.${TABLE}" \
"${BUCKET}/data/penguins-*.csv"
bq show --schema --format=prettyjson "${PROJECT_ID}:${DATASET}.${TABLE}"
species、island、sex は STRING として読み込まれます。注意が必要なのは計測値の列です。自動検出は、元テーブルの型ではなく、CSV に含まれる値から各列の型を推論するためです。
| 列 | 検出される型 |
|---|---|
culmen_length_mm、culmen_depth_mm
|
FLOAT |
flipper_length_mm、body_mass_g
|
INTEGER |
後者の 2 列は全行が整数値であり、CSV は型情報を持ちません。そのため、公開テーブルでは FLOAT64 であるにもかかわらず、自動検出は整数と判断します。Cloud Storage を経由したことで、型が黙って狭められたことになります。
当記事では、この検出結果をそのまま使用します。ただし、学習時の型が、そのまま Endpoint が要求する型になります。後述するリクエストボディを書く際に、この表を思い出してください。
BigQuery MLでのモデル学習
モデルの作成
model_type='LOGISTIC_REG' は、二項分類と多クラス分類の両方をカバーします。BigQuery ML はラベル列を確認して自動的に選択します。異なる値が 2 つなら二項ロジスティック回帰、3 つ以上なら多クラス分類です。species は 3 種類あるため、多クラス分類が自動的に選ばれます。別のモデルタイプを指定する必要はありません。
CREATE OR REPLACE MODEL `YOUR_PROJECT.penguins_demo.penguins_species_model`
OPTIONS(
model_type = 'LOGISTIC_REG',
input_label_cols = ['species']
) AS
SELECT * FROM `YOUR_PROJECT.penguins_demo.penguins`;
BigQuery Studio のモデル詳細。モデルタイプは Logistic regression、ロケーションは US

ここで実施していない作業に注目してください。island と sex の one-hot エンコード、計測値のスケーリング、学習データとテストデータの分割は、いずれも書いていません。BigQuery ML は入力列に自動前処理を適用し、データの分割も自身で行います。このとき構築されるエンコードの語彙は、後のエクスポートでモデルに書き込まれます。デプロイした Endpoint が "Biscoe" のような生の文字列を受け付けられるのは、そのためです。
当記事の主題はエクスポート経路であるため、学習の説明は意図的に簡潔にしています。
エクスポート前の評価
この確認に必要な時間は 30 秒程度です。ここで見つかる誤りは、デプロイ後に発覚すると 15 分の待ち時間に化けます。
SELECT * FROM ML.EVALUATE(MODEL `YOUR_PROJECT.penguins_demo.penguins_species_model`);
続いて、後で Endpoint に送信する予定のインスタンスを、そのままスコアリングします。
SELECT predicted_species
FROM ML.PREDICT(
MODEL `YOUR_PROJECT.penguins_demo.penguins_species_model`,
(SELECT 'Biscoe' AS island,
45.2 AS culmen_length_mm,
14.8 AS culmen_depth_mm,
215 AS flipper_length_mm,
5150 AS body_mass_g,
'FEMALE' AS sex)
);
2 つの整数リテラルに注意してください。flipper_length_mm と body_mass_g は INT64 として学習されており、BigQuery は FLOAT64 から INT64 への暗黙の変換を許しません。ここに 215.0 と書くと、クエリはエラーになります。
Column flipper_length_mm with type FLOAT64 cannot be converted to type INT64
from training implicitly according to the coercion rule
逆方向は安全です。INT64 から FLOAT64 へは自動的に拡張されます。したがって整数リテラルは、自動検出がどちらの型を選んでいても動作します。整数値の特徴量に対しては、これが安全側の書き方です。
このクエリは Gentoo penguin (Pygoscelis papua) を返します。この結果は記録しておいてください。記事の最後で実行する Endpoint 呼び出しの対照結果になります。Endpoint がこれと異なる結果を返した場合、原因はモデルではなくサービング経路にあります。
モデルをCloud Storageにエクスポートする
EXPORT MODELの実行
EXPORT MODEL `YOUR_PROJECT.penguins_demo.penguins_species_model`
OPTIONS(URI = 'gs://YOUR_BUCKET/models/');
CLI では、bq extract が同じ処理を行います。
bq extract --destination_format=ML_TF_SAVED_MODEL \
-m "${PROJECT_ID}:${DATASET}.${MODEL}" \
"${BUCKET}/models/"
エクスポートされる内容
gcloud storage ls -r "${BUCKET}/models/"
models/
├── saved_model.pb
├── explanation_metadata.json
├── fingerprint.pb
├── assets/
│ ├── island.txt
│ └── sex.txt
└── variables/
├── variables.data-00000-of-00001
└── variables.index
assets/ に含まれるのは、カテゴリ列の語彙です。island と sex の 2 ファイルのみで、ラベルである species の語彙はここにはありません。ラベルの語彙はグラフ側に埋め込まれます。
explanation_metadata.json は Explainable AI を構成する際に使用するメタデータ、fingerprint.pb は SavedModel の同一性を示すフィンガープリントです。当記事の手順ではどちらも使用しませんが、エクスポート時に生成されます。
Cloud Storage の models/ フォルダ。saved_model.pb が assets/、variables/ と同じ階層にある

これは標準的な TensorFlow SavedModel のディレクトリです。BigQuery 固有の要素はもう含まれていません。それがこの工程の要点です。TensorFlow Serving でも、ローカルの tf.saved_model.load() でも、SavedModel を扱えるあらゆる環境で読み込めます。
以降の工程に関わる点が 2 つあります。
1 つ目は、saved_model.pb が models/ の直下にあることです。次の手順で --artifact-uri に渡すのは、このディレクトリのパス、つまり gs://<bucket>/models/ です。バケットのルートでも、.pb ファイル自体のパスでもありません。
2 つ目は、サービングシグネチャが出力するテンソルが 3 つあり、いずれもラベル列の名前を元にしている点です。
| 出力 | 型 | 内容 |
|---|---|---|
predicted_species |
STRING | 予測されたクラス |
species_values |
STRING 配列 | クラスラベルの一覧 |
species_probs |
FLOAT 配列 |
species_values と対応する確率 |
エクスポートされたモデルでは、BigQuery 側の列の型にかかわらず、予測ラベルは常に STRING です。ラベル列が INT64 のモデルは、4 ではなく "4" を返します。整数を前提とした後続コードはここで壊れます。BigQuery 内の ML.PREDICT は元の型を返すため、見落としやすい差異です。
出力テンソルの名前は、デプロイ前にローカルで確認できます。TensorFlow に付属する saved_model_cli を使えば、費用をかけずにサービングシグネチャを直接読み取れます。
mkdir -p /tmp/bqml-model
gcloud storage cp -r "${BUCKET}/models" /tmp/bqml-model
saved_model_cli show --dir /tmp/bqml-model/models \
--tag_set serve --signature_def serving_default
コピー先のディレクトリは事前に作成してください。存在しない場合、gcloud storage cp -r は Destination URL must name an existing directory で失敗します。また saved_model_cli は TensorFlow に含まれるコマンドです。未インストールの環境では command not found になります。
Model Registryへのインポート
サービングコンテナの選択
Dockerfile を書く必要はありません。Google は TensorFlow Serving が動作するビルド済み予測コンテナを公開しています。アーティファクトのディレクトリを指定すれば、起動時に SavedModel を読み込みます。
us-docker.pkg.dev/vertex-ai/prediction/tf2-cpu.2-12:latest
2026年9月現在、上記のイメージを使用しています。アーティファクトを生成した TensorFlow のバージョン以上のイメージを選択してください。ロジスティック回帰であれば CPU が適切です。特徴量が 6 つの線形モデルでは、GPU ノードにしてもレイテンシは変わらず、費用だけが大きく増えます。
アップロード
gcloud ai models upload \
--region="${REGION}" \
--display-name="${MODEL}" \
--container-image-uri=us-docker.pkg.dev/vertex-ai/prediction/tf2-cpu.2-12:latest \
--artifact-uri="${BUCKET}/models/"
続いてモデル ID を取得し、実際に登録された内容を確認します。
export MODEL_ID=$(gcloud ai models list \
--region="${REGION}" \
--filter="displayName=${MODEL}" \
--format="value(name)" | head -n1 | awk -F/ '{print $NF}')
gcloud ai models describe "${MODEL_ID}" --region="${REGION}" \
--format="value(displayName,artifactUri,containerSpec.imageUri)"
Model Registry に登録された penguins_species_model。デフォルトのバージョンは 1、ソースはカスタム トレーニング(インポート)

artifactUri はこの時点で必ず確認してください。アップロードコマンドは、指定したパスに読み込み可能なモデルが存在するかを検証しません。誤った URI でも問題なく登録され、デプロイ開始から 10 分後に失敗します。しかもエラーは原因ではなく Endpoint を指し示すため、原因の特定に時間がかかります。
Endpointへのデプロイ
Endpointの作成とモデルのデプロイ
gcloud ai endpoints create \
--region="${REGION}" \
--display-name="${ENDPOINT_NAME}"
Endpoint の ID を取得します。ここでは、一致する Endpoint がちょうど 1 つであることを先に確認してください。
gcloud ai endpoints list --region="${REGION}" \
--filter="displayName=${ENDPOINT_NAME}" --format="value(name)"
export ENDPOINT_ID=$(gcloud ai endpoints list \
--region="${REGION}" \
--filter="displayName=${ENDPOINT_NAME}" \
--format="value(name)" | head -n1 | awk -F/ '{print $NF}')
作成コマンドとデプロイコマンドは、まとめて実行せず 1 つずつ実行することを推奨します。ネットワークの一時的な切断で gcloud がクラッシュしても、サーバー側の処理は継続します。そこでコマンドをまとめて再実行すると、Endpoint が 2 つ作成され、head -n1 はそのどちらかを黙って選びます。結果として、意図しない Endpoint にデプロイされ、ノード 2 台分の課金が発生します。
gcloud ai endpoints deploy-model "${ENDPOINT_ID}" \
--region="${REGION}" \
--model="${MODEL_ID}" \
--display-name="${ENDPOINT_NAME}-deployed" \
--machine-type=n1-standard-2 \
--min-replica-count=1 \
--max-replica-count=1 \
--traffic-split=0=100
デプロイでは、ノードのプロビジョニング、サービングコンテナの取得、モデルの読み込みが行われます。最大 15 分を見込んでください。デプロイ済みモデルの有無は、以下のコマンドで確認します。
gcloud ai endpoints describe "${ENDPOINT_ID}" --region="${REGION}" \
--format="value(deployedModels[0].id)"
出力が空であれば、まだ処理中です。
モデルモニタリングの有効化
デプロイ済みモデルは、静かに劣化します。それをアラートに変えるのがモニタリングです。
Agent Platform には 2 つの目的が用意されており、どちらを選べるかは前提条件で決まります。
| 目的 | 比較対象 | 学習データが必要か |
|---|---|---|
| 学習サービングスキュー検出 | サービング時の入力と、元の学習データ | 必要 |
| 推論ドリフト検出 | 直近のサービング入力と、それ以前のサービング入力 | 不要 |
推論ドリフト検出はベースラインの指定が不要です。そのため、学習データが移動済み、あるいは削除済みのモデルを含め、どの Endpoint でも有効化できます。いずれの目的を選んだ場合もリクエストとレスポンスのロギングが有効になり、サンプリングされたペイロードが Agent Platform によってプロジェクト内の BigQuery データセットに書き込まれます。そのストレージ費用も見込んでおいてください。
コンソールから Endpoint を作成する場合、モニタリングは作成ウィザードの 1 ステップとして設定できます。Agent Platform → オンライン予測 → エンドポイント → 作成と進み、モデルのモニタリングのステップでモニタリングを有効にし、通知先メールアドレスを入力して、目的として推論ドリフトの検出を選択します。
前節のように CLI で Endpoint を作成済みの場合は、ウィザードを使うと別の Endpoint が新たに作られてしまいます。その場合は、後述のコマンドで既存の Endpoint にモニタリングジョブを追加してください。
エンドポイント作成ウィザードの「モデルのモニタリング」。モニタリングを有効化し、通知メールを設定する

「モニタリングの目的」で推論ドリフトの検出を選択。しきい値を空欄にするとデフォルトの 0.3 が適用される

CLI では、デプロイ済みの Endpoint に対する別のコマンドとして実行します。
gcloud ai model-monitoring-jobs create \
--region="${REGION}" \
--display-name="${ENDPOINT_NAME}-drift" \
--endpoint="${ENDPOINT_ID}" \
--emails="$(gcloud config get-value account)" \
--prediction-sampling-rate=1.0 \
--monitoring-frequency=1 \
--feature-thresholds=island=0.3,culmen_length_mm=0.3,culmen_depth_mm=0.3,flipper_length_mm=0.3,body_mass_g=0.3,sex=0.3
学習データのソースを指定せずに --feature-thresholds を渡すことが、このジョブをスキュー検出ではなくドリフト検出にします。--monitoring-frequency の単位は時間で、1 は 1 時間ごとの監視を意味します。サンプリングレート 1.0 はすべてのリクエストをロギングします。検証用途では問題ありませんが、本番環境では過剰です。トラフィック量が把握できた段階で下げてください。
詳細:公式ドキュメント(特徴のスキューとドリフトのモニタリング)
オンライン予測の実行
リクエストの送信
リクエストボディは、インスタンスのリストを包む構造です。各インスタンスは、モデルの入力列名をキーとするオブジェクトであり、その JSON の型は学習時のスキーマと一致している必要があります。flipper_length_mm と body_mass_g は学習テーブルで INT64 だったため、ここでも小数点を付けずに記述します。前述の ML.PREDICT の対照クエリと同じ書き方です。TensorFlow Serving は 215.0 を int64 のテンソルに変換しません。リクエスト自体が拒否されます。
cat > request.json <<'EOF'
{
"instances": [
{
"island": "Biscoe",
"culmen_length_mm": 45.2,
"culmen_depth_mm": 14.8,
"flipper_length_mm": 215,
"body_mass_g": 5150,
"sex": "FEMALE"
}
]
}
EOF
gcloud ai endpoints predict "${ENDPOINT_ID}" \
--region="${REGION}" \
--json-request=request.json
指定するのは --json-request です。上記のボディ全体を受け取ります。名前の似た --json-instances は、"instances" のラッパーを持たない改行区切りのインスタンスを想定しているため、このファイルは受け付けません。
アプリケーションが実際に発行することになる REST 呼び出しは、以下のとおりです。
curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" \
"https://${REGION}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${REGION}/endpoints/${ENDPOINT_ID}:predict" \
-d @request.json
レスポンスの読み方
{
"predictions": [
{
"species_values": [
"Adelie Penguin (Pygoscelis adeliae)",
"Chinstrap penguin (Pygoscelis antarctica)",
"Gentoo penguin (Pygoscelis papua)"
],
"predicted_species": [
"Gentoo penguin (Pygoscelis papua)"
],
"species_probs": [
8.4369615777143419e-05,
0.00028677094830009692,
0.99962885943592272
]
}
],
"deployedModelId": "xxxxxxxxxxxxxxxxxxx",
"model": "projects/xxxxxxxxxxxx/locations/us-central1/models/xxxxxxxxxxxxxxxxxxx",
"modelDisplayName": "penguins_species_model",
"modelVersionId": "1"
}
curl で Endpoint を呼び出した結果。predicted_species が BigQuery 内の ML.PREDICT と同じ Gentoo を返している

predicted_species は、先ほどの ML.PREDICT の結果と一致しています。エクスポートとサービング経路がモデルを正しく保持したことの確認です。事前に対照クエリを実行しておく理由がここにあります。それがなければ、この JSON はもっともらしいだけの出力に過ぎません。
配列については 2 点あります。species_values はラベル値のアルファベット順で返ります。Adelie、Chinstrap、Gentoo の順であり、確率の高い順ではありません。したがって予測されたクラスはインデックス 0 にはなく、そこを読むコードはたまたま正しく動くことしかありません。species_probs は species_values と位置で対応します。この対応があるため、0.9996 がインデックス 2、つまり Gentoo に対応すると読み取れます。
確率の値や、レスポンス内でのフィールドの出現順は実行環境によって異なります。フィールド名と、species_values のアルファベット順という並びは変わりません。
なお、スカラーである予測値も含め、すべてのフィールドが配列として返ります。これは BigQuery ML 固有の挙動ではなく、TensorFlow Serving の出力形式です。クライアント側のコードでは predictions[0].predicted_species[0] のように参照します。
落とし穴
データセットとバケットのロケーションは一致させる
EXPORT MODEL にロケーションをまたぐモードはありません。US マルチリージョンのデータセットから us-central1 のバケットへはエクスポートできません。名前は似ていますが、この 2 つは別のロケーションです。どちらのリソースを作る前にもロケーションを決めてください。後からバケットを作り直す場合、既に格納済みのデータも移動することになります。
--artifact-uriはSavedModelのディレクトリを指す
アップロードコマンドは、URI の内容を検証せずに受け付けます。saved_model.pb を直接含むディレクトリを指していない場合でも、モデルの登録は成功し、デプロイが開始され、約 10 分後にモデルの読み込みエラーで失敗します。デプロイ後ではなくアップロード直後に、gcloud ai models describe で確認してください。
autodetectが特徴量の型を決め、Endpointがそれを引き継ぐ
bq load --autodetect は、CSV の元になったテーブルではなく、CSV に含まれる値から型を読み取ります。値がすべて整数である FLOAT64 の列、つまり flipper_length_mm と body_mass_g は INTEGER として読み込まれます。CSV は型を持たず、215 は整数に見えるためです。
この判断は、そのまま HTTPS の Endpoint まで伝播します。学習時のスキーマがモデルの入力型を決め、入力型はエクスポートされた SavedModel のサービングシグネチャに焼き込まれ、サービングコンテナがそれを強制します。
BigQuery ML.PREDICT に 215.0 を渡す → クエリエラー。FLOAT64 は INT64 に変換できない
Endpoint {"flipper_length_mm": 215.0} → TensorFlow Serving がリクエストを拒否
原因は同じですが、前者の代償は数秒、後者の代償はデプロイ 1 回分です。ロード後に bq show --schema を実行し、実際のスキーマに合わせてリクエストボディを書いてください。
整数値の特徴量には整数リテラルが安全です。INT64 は FLOAT64 へ暗黙に拡張されるため、215 はどちらの型で学習したモデルでも受け付けられます。一方 215.0 は FLOAT64 の場合しか通りません。データにかかわらず浮動小数点数として扱いたい場合は、自動検出を使わず、ロード時にスキーマを明示してください。
予測ラベルは文字列で返る
前述のとおりですが、このワークフローで最も多い連携時の不具合であるため、改めて記載します。エクスポートされた分類モデルは、ラベルを必ず STRING で返します。BigQuery 内の ML.PREDICT は元の列の型で返します。BigQuery に対しては正しく動作するコードが、Endpoint に対しては比較に失敗し、しかもエラーにならずに誤った結果を返します。
デプロイ済みモデルは課金され続ける
--min-replica-count=1 は、ノードが常時 1 台稼働することを意味します。リクエストの有無にかかわらず、ノード時間単位で課金されます。これがこのワークフロー全体で支配的な費用です。数百行の BigQuery ML の学習と Cloud Storage の使用量は、これに比べれば無視できる水準です。
トラフィックが断続的で、レイテンシの要求も厳しくない場合、Endpoint はそもそも適した形ではないかもしれません。バッチ予測は処理した分だけ課金され、Cloud Run 上のサービングコンテナはリクエストがない間ゼロにスケールします。エクスポートした SavedModel は、そのいずれでも動作します。
再学習してもEndpointは更新されない
CREATE OR REPLACE MODEL が置き換えるのは BigQuery 内のモデルだけです。エクスポート済みのアーティファクト、Model Registry の登録内容、デプロイ済みモデルは、いずれも以前の重みで配信を続けます。再学習した場合は、エクスポート、新しいモデルバージョンとしてのアップロード、デプロイを再実行する必要があります。本番環境で運用する前に、自動化しておくべきパイプラインです。
クリーンアップ
継続的な費用が発生するのは Endpoint だけですが、依存関係によるエラーを避けるため、順序に沿って削除します。
DEPLOYED_ID=$(gcloud ai endpoints describe "${ENDPOINT_ID}" --region="${REGION}" \
--format="value(deployedModels[0].id)")
gcloud ai endpoints undeploy-model "${ENDPOINT_ID}" \
--region="${REGION}" --deployed-model-id="${DEPLOYED_ID}"
gcloud ai endpoints delete "${ENDPOINT_ID}" --region="${REGION}"
gcloud ai models delete "${MODEL_ID}" --region="${REGION}"
gcloud storage rm -r "${BUCKET}"
bq rm -r -f --dataset "${PROJECT_ID}:${DATASET}"
課金が止まるのは undeploy-model の時点です。デプロイ済みモデルを持たない Endpoint には費用が発生しません。作業を中断して後日再開する場合は、undeploy だけ実行して残りは保持しておくとよいです。再開時は、Model Registry に残ったモデルに対して deploy-model を 1 回実行するだけで済みます。
モデルモニタリングを有効にした場合、Endpoint を削除した後も 2 つのリソースが残ります。1 つはモニタリングジョブ自身で、スケジュールに従って動作を続けます。
gcloud ai model-monitoring-jobs list --region="${REGION}"
gcloud ai model-monitoring-jobs delete JOB_ID --region="${REGION}"
もう 1 つは、リクエストとレスポンスのロギング用に作成された BigQuery データセットです。紐づいていた Endpoint の ID を含む名前で残ります。
bq ls
bq rm -r -f --dataset "${PROJECT_ID}:model_deployment_monitoring_ENDPOINT_ID"
データがある場所で学習し、トラフィックがある場所で配信する。 EXPORT MODEL から :predict までの経路が一度つながれば、BigQuery ML は「SQL の中だけのモデル」ではなくなります。