Tech Waves

produced by Hakuhodo DY ONE

本ブログは、株式会社Hakuhodo DY ONEの開発チームによるエンジニアブログです。
それぞれのメンバーが業務を通して得た技術情報や、各種セミナーの参加レポート、またその他トピックについて情報発信を行っています。

Google Cloud WorkflowsでJupyter Notebookを自動実行する際の落とし穴と対策

c3543e56-5b37-43a6-b6ad-3757003f9ea1-image_gen_gemini_flash_img_GRbF4ydrFh4rF6wlkTSp0.png

こんにちは、ダンです。

Google Cloud Workflowsを使ってJupyter Notebookを自動実行してみたけれど、なぜかライブラリのバージョン競合でエラーが出る...そんな経験はありませんか?

本番環境で定期的にモデルを再学習・予測を実行する際、ライブラリの管理が適切でないと、一見動作しているコードが突然失敗することがあります。本記事では、Google Cloud Workflows経由でJupyter Notebook(Colab Enterprise / Workbench)を実行する際のライブラリインストールとインポートの落とし穴、そしてそれらを確実に回避するテクニックを、実戦で培ったノウハウとともに紹介します。

 

Jupyter Notebookとは?

Jupyter Notebookは、ブラウザ上でコードの実行、テキスト(Markdown)、数式、図表をひとまとめにして作成・共有できる、オープンソースの対話型コンピューティング環境です。

データサイエンスや機械学習の現場で最も広く利用されているツールの1つです。

主な特徴

  • 対話型実行 (Interactive Execution): コードを「セル」単位で実行できるため、計算結果をすぐに確認しながらプログラムを構築できます。
  • データの視覚化: matplotlibseaborn などのライブラリを使用し、コードのすぐ下にグラフや図形を直接表示させることができます。
  • ナラティブなドキュメント作成: プログラムのロジックを説明するテキストや数式(LaTeX)を記述できるため、単なるソースコードではなく「実行可能なレポート」として活用できます。
  • 多言語対応: Pythonを中心に、RやJulia、Scalaなど40以上のプログラミング言語をサポートしています。

役割

機械学習モデルの試作(EDA)、トレーニング、および予測結果の検証をGUIベースで行うために選定しました。

Agent Platform(旧 : Vertex AI )のColab Enterpriseや Workbenchを通じてGoogle Cloud上で管理されています。

Google Cloud Workflowsとは?

Google Cloud Workflowsは、Google Cloudの各サービスや外部のHTTPベースのAPIを、定義された順序で連携・自動化(オーケストレーション)する、フルマネージドなサーバーレスツールです。

参考 :

主な特徴

  • サーバーレスで低コスト: 実行環境の管理が不要で、実行されたステップ数に応じて課金されるため、運用コストを最小限に抑えられます。
  • 高い信頼性とエラーハンドリング: ステップ間の再試行ロジック(Retry logic)やエラー捕捉(Error handling)をYAML形式で簡単に定義でき、複雑なプロセスも確実に実行できます。
  • 長時間実行のサポート: Workflowsは最大1年間実行を維持できるため、時間のかかるバッチ処理や承認フローの管理にも適しています。
  • シームレスなサービス連携: BigQuery、Cloud Functions、そして今回使用するNotebook APIなどのGoogle Cloudサービスを簡単に呼び出すことができます。

役割

作成したJupyter Notebookをプログラムからトリガーし、データの前処理からモデル実行までを自動化するために採用しました。

Cloud Schedulerと組み合わせることで、定期的なモデルの更新(再学習)や予測の自動実行を可能にしています。

現状と課題

直面した制約

  • セキュリティルール: 外部アクセスが必要な場合は、Cloud NATによるセキュアな通信環境が必要です。
  • ランタイムの制限: Workflowsからの実行環境(Colab Enterpriseのランタイム)には numpypandas がプリインストールされていますが、bigframes などの最新ライブラリが不足していたり、既存ライブラリのバージョンが古かったりします。

採用した解決策と理由

複数の解決案の中から、以下の理由でGCS + カスタムインストール方式を採用しました:

  • Docker化の場合: Artifact Registryを管理する工数が発生し、 Cloud RunやVertex AI Trainingなどの追加サービスも利用が必要になります。全体のアーキテクチャーが複雑になり、デプロイ〜実行までの時間も5〜10分増加します。
  • カスタムランタイムの場合: インフラコストが増加し、管理負担が大きいです。
  • GCS + pip install方式: 最小限の追加インフラで、比較的低コストに実装できます。

GCS(Google Cloud Storage)にPythonライブラリファイル(.whl ファイル)を格納しておき、ノートブックの実行時にそれらをダウンロード・インストールする手法を採用しました。

Workflowsでの実行フロー

Notebook API(Colab Enterprise API)を経由してJupyter Notebookを実行します。基本的なフローは以下の通りです:

  1. ノートブックファイル(.ipynb)をGCSにアップロード
  2. Colab Enterpriseでランタイムテンプレートを事前に作成
  3. Workflowsから Notebook API を呼び出し、作成したテンプレートで実行

詳細なWorkflows設定については、Google Cloudの公式ドキュメントなどをご参照ください。本記事では、Notebookの内部実装(ライブラリ管理)に焦点を当てています。

Notebook APIの参考:


実行時の注意点と解決策

ここからが本題です。WorkflowsでJupyter Notebookを実行する際に、Notebook実行環境の周りで陥りやすい罠が2つあります。


注意点①:ライブラリインストール場所の競合

現状:デフォルト実装の問題点

デフォルトの挙動では、GCSから /tmp/libs ディレクトリにライブラリをダウンロードし、そのまま pip install を実行します。

import glob
import os
import subprocess

# /tmp/libs ディレクトリを作成する
staging_dir = "/tmp/libs"
os.makedirs(staging_dir, exist_ok=True)

lib_uri = "gs://bucket-name/ml-library/*.whl"

# GCSからライブラリをダウンロード
result = subprocess.run(
    f"gcloud storage cp {lib_uri} {staging_dir}/",
    shell=True,
    capture_output=True,
    text=True,
    timeout=300,
)
if result.returncode != 0:
    raise RuntimeError(result.stderr or result.stdout)

# ダウンロード確認
downloaded = [f for f in os.listdir(staging_dir) if f.endswith(".whl")]
print(f"Downloaded {len(downloaded)} files to {staging_dir}")

# ライブラリをインストール
wheel_files = sorted(glob.glob(f"{staging_dir}/*.whl"))
if not wheel_files:
    raise FileNotFoundError(f"No wheel files found in {staging_dir}/")

install_cmd = f"pip install --no-index --find-links={staging_dir}/ {staging_dir}/*.whl --force-reinstall"
result = subprocess.run(install_cmd, shell=True, capture_output=True, text=True, timeout=600)
if result.returncode != 0:
    raise RuntimeError(result.stderr or result.stdout)

問題:バージョン競合エラー

numpy などをインポートしようとすると、以下のエラーが発生することがあります:

ERROR:root:cannot import name '_center' from 'numpy._core.umath'
(/usr/local/lib/python3.12/dist-packages/numpy/_core/umath.py)

原因:システムディレクトリへのインストール

この方法では、ライブラリはシステムのデフォルトディレクトリ(/usr/local/lib/python3.12/dist-packages/)にインストールされます。

しかし、Colab Enterpriseには numpypandas が最初からインストールされています。そのため、--force-reinstall を使っても以下の問題が発生します:

  • 古い共有オブジェクト(.soファイル)がメモリ上に残る
  • インポート時にシステム側の古いバージョンが優先される
  • 新しくインストールしたバージョンと衝突してエラーになる

解決方法:カスタムディレクトリへの隔離

システムディレクトリを避け、独自のディレクトリにインストールして sys.path を操作します。

import os, sys, subprocess, glob

# インストール先を定義
custom_lib_path = os.path.abspath("/tmp/my_libs")
os.makedirs(custom_lib_path, exist_ok=True)

# --target を指定して隔離インストール
wheel_files = glob.glob("/tmp/libs/*.whl")
install_cmd = [
    sys.executable, "-m", "pip", "install",
    "--target", custom_lib_path,
    "--no-index", "--find-links", "/tmp/libs/",
    "--ignore-installed"
] + wheel_files
subprocess.run(install_cmd, check=True)

# カスタムパスを最優先に追加
if custom_lib_path not in sys.path:
    sys.path.insert(0, custom_lib_path)

ポイント: --target オプションにより、ライブラリはカスタムディレクトリに隔離され、システムディレクトリの古いライブラリとの衝突を回避できます。



注意点②:インポート順序と依存関係の罠

現状:慣習的なインポート順序

通常、Pythonで機械学習を行う際は、以下の順番でインポートするのが一般的です:

import pandas as pd
import numpy as np
from sklearn.linear_model import LogisticRegression
import matplotlib.pyplot as plt
import bigframes.pandas as bpd

問題:RecursionErrorやインポートエラーの発生

カスタムディレクトリ(/tmp/my_libs)に最新ライブラリをインストールしていても、上記の順番でインポートすると以下のエラーが発生することがあります:

  1. RecursionError: maximum recursion depth exceeded(無限ループ)
  2. ImportError / TypeError: バージョン間の不整合による内部関数の読み込み失敗

原因:隠れた依存関係とバージョンの混在

この問題の核心は以下の点にあります:

  • Scikit-learn や BigFrames などの高レベルライブラリは、インポート時にバックグラウンドで自動的に matplotlib や numpy を呼び出します
    • sklearn を先にインポートすると、まだ sys.path の優先順位が反映される前に、システム側(/usr/local/lib)の古いライブラリがメモリにロードされてしまいます
  • その後にカスタムパスから新しいライブラリをロードしようとしても、メモリ上では「一部は新しく、一部は古い」というバージョンの混在が発生します
  • 結果として、内部関数が互いのオブジェクトを正しく認識できず、エラーを引き起こします

解決方法:キャッシュ削除とインポート順序の厳守

Google Cloud Workflows経由の実行ではカーネルの再起動ができないため、コード内で明示的に「古い情報を忘れさせる」必要があります。

1. sys.modules からのキャッシュ削除

新しいパス(/tmp/my_libs)を追加した直後に、既にシステムによってロードされている可能性のある関連ライブラリのキャッシュをすべて削除します。これにより、次の import 文で必ずカスタムパスから読み込まれるようになります。

2. 「低層」から「高層」へのインポート順序

最初に、依存関係の土台となるライブラリ(numpy, matplotlib)を単独でインポートします。その後に、それらを利用するライブラリ(sklearn, bigframes)をインポートする順序を徹底します。

最終的な解決コード

import sys
import os

# 1. カスタムパスを最優先に設定
custom_lib_path = os.path.abspath("/tmp/my_libs")
if custom_lib_path not in sys.path:
    sys.path.insert(0, custom_lib_path)

# 2. 既存モジュールのキャッシュを徹底的に削除(再ロードを強制)
# システム標準ではなくカスタムパス側から読み込むようにする
target_libraries = ['sklearn', 'numpy', 'scipy', 'pandas', 'matplotlib', 'pyarrow', 'bigframes']
for module_name in list(sys.modules.keys()):
    if any(module_name == lib or module_name.startswith(lib + '.') for lib in target_libraries):
        del sys.modules[module_name]

# 3. 依存関係が少ない「土台」ライブラリから順にインポート
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt

# 4. 最後に、上記ライブラリに依存している「高層」ライブラリをインポート
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
import bigframes.pandas as bpd

# バージョン確認(期待したパスから読み込まれているかチェック)
print(f"Numpy version: {np.__version__} from {np.__file__}")


まとめ

Google Cloud WorkflowsでJupyter Notebookを安定して自動実行するためのポイントは、以下の3点に集約されます:

  1. インストール場所の隔離
    • -target オプションを使用して /tmp/my_libs などの別ディレクトリにインストール
    • システム側の古いライブラリとの衝突を避ける
  2. インポートキャッシュのクリア
    • 実行中にカーネルの再起動ができないため、sys.modules から古いキャッシュを削除
    • 新しいライブラリの再ロードを強制する
  3. インポート順序の遵守
    • numpymatplotlib などの基礎となるライブラリを先にインポート
    • その後に sklearn などの依存ライブラリを読み込み、バージョンの混在を防ぐ

これらの手順を徹底することで、ライブラリの競合によるエラー(TypeErrorRecursionError)を未然に防ぎ、スムーズな自動実行が可能になります。
ぜひ、参考にしてみてください。

この記事を書いた人

ダンカックダット (id:one-dang-khacdat)

Google Cloud上でデータエンジニアリング領域で開発を主に担当しています。生成AIの周りにも興味があって活用しています。