こんにちは!日々のデータ分析やAIモデルの実験、本当にお疲れ様です。
JupyterLabを開いて、何十万行もあるデータフレームの処理や、重いループ処理を実行したとき…画面の左上の丸印(カーネルステータス)が黒く塗りつぶされ、ジーッと固まったまま動かなくなるあの時間、もどかしいですよね。
「私のパソコン、Core i7(あるいはMシリーズのApple Silicon)なのに、なぜ1つのコアしか働いていないんだろう?」と疑問に思ったことはありませんか?
そう、JupyterLabの標準的なPythonカーネル(IPython)は、基本的に「単一スレッド(シングルプロセス)」で動いています。つまり、マルチコアCPUを持っていても、デフォルトではそのうちの「たった1つの部屋」しか使っていないのです。これでは、宝の持ち腐れですよね。
今回は、このもどかしい限界を突破し、JupyterLab上であなたのPCのCPUコアをフル回転させる「並列処理(マルチコア活用術)」を授けます。
これをマスターすれば、今まで数分かかっていたデータの前処理やシミュレーションが、数秒で終わるようになりますよ。毎日のコーディングが劇的に快適になる体験を、一緒に見ていきましょう!
—
1. なぜJupyterLabの標準環境ではCPUが遊んでしまうのか?
私たちが普段使っているPythonには、GIL(Global Interpreter Lock:グローバルインタプリタロック)という仕様があります。これは、簡単に言うと「Pythonのコードを安全に実行するために、1つのプロセス内では同時に1つのスレッドしかPythonのバイトコードを実行してはいけない」というルールです。
このGILのせいで、CPUのコアを複数使おうとしても、1つのプロセスの中ではマルチスレッドによる劇的な速度向上が望めません(特に重い計算処理の場合)。
そこで登場するのが、「マルチプロセス(複数の独立したPythonを立ち上げる)」というアプローチです。
OSから見れば別々のプログラムが動いていることになるため、GILの呪縛から逃れ、4コアなら4倍、8コアなら8倍近い速度で処理を分散させることができます。
今回は、このマルチプロセスをJupyterLabから極めてエレガントに操るための2つの武器、`joblib` と `Dask` を使います。
—
2. 開発環境のセットアップ:知る人ぞ知る「正しい」ライブラリ導入
まずは、JupyterLab上で並列処理を安全に行うための環境を整えます。
Anaconda(またはMiniconda/Mamba)環境を使っている前提で、ターミナル(またはJupyterLab内のターミナル)を開き、以下のコマンドを実行してください。
競合を防ぐため、データサイエンスに必要なパッケージが整った環境に並列処理ライブラリを導入します
conda install -c conda-forge joblib dask distributed ipywidgets -y
- `joblib`: scikit-learnの内部でも使われている、非常にシンプルかつ堅牢な並列化ライブラリ。ループ処理の並列化に最強です。
- `dask`: 大規模データの遅延評価や、高度なタスクグラフによる並列分散処理を行うためのフレームワーク。
- `distributed`: Daskの頭脳であり、JupyterLab上に美しい「Dask Dashboard(リアルタイム監視画面)」を描画するために必須です。
—
3. 手始めの第一歩:`joblib` でforループを並列化する
まずは、最も手軽に導入できる `joblib` から始めましょう。
「重たい処理を行うforループ」を、魔法のひと手間でマルチコアに分散させます。
以下のコードをJupyterLabの新しいセルに貼り付けて実行してみてください。
import time
from joblib import Parallel, delayed
1. 非常にシンプルなお手伝い関数(重たい処理のシミュレーション)
与えられた数値を1秒間スリープ(計算に見立てる)させてから2乗を返します
def heavy_computation(i):
time.sleep(1) # 重たい計算やAPIリクエストのつもり
return i 2
測ってみましょう:まずは普通の逐次処理(シングルコア)
print(“— 逐次処理(通常)の開始 —“)
start_time = time.time()
0から7までの8つのタスクを1つずつ順番に処理
results_sequential = [heavy_computation(i) for i in range(8)]
print(f”逐次処理にかかった時間: {time.time() – start_time:.2f} 秒”)
これを普通に実行すると、8秒かかりますよね(1秒 × 8回)。
では、ここからが本番です。`joblib` を使って、これを一気に全コアで並列処理してみましょう。
測ってみましょう:joblibによる並列処理(マルチコア)
print(“\n— joblibによる並列処理の開始 —“)
start_time = time.time()
Parallel(n_jobs=-1) と書くだけで、PCの「すべてのCPUコア」を使い切る設定になります
delayed() の中に「実行したい関数」と「引数」を包みます
results_parallel = Parallel(n_jobs=-1)(
delayed(heavy_computation)(i) for i in range(8)
)
print(f”並列処理にかかった時間: {time.time() – start_time:.2f} 秒”)
print(f”計算結果: {results_parallel}”)
【ここに注目!】
たったこれだけのコードで、実行時間が8秒から「およそ1〜2秒」に短縮されたはずです(CPUのコア数に依存します)。
`n_jobs=-1` という指定は、「このマシンの物理・論理コアをすべて使い切れ」というエンジニアにとって最高に心地よい命令です。
—
4. 本格派の選択:`Dask` とダッシュボードによる可視化
`joblib` はループの並列化には最高ですが、「データサイズが大きすぎてメモリに乗り切らない」「複数の処理が複雑に絡み合っている」という場合には、`Dask(ダスク)` の出番です。
さらに、Daskの真骨頂は、JupyterLabの画面上に「いま、どのCPUがどれくらい働いていて、メモリをどれくらい消費しているか」をリアルタイムで映し出すダッシュボード(Dask Dashboard)を表示できる点にあります。これが見えるようになると、まるで自分がシステムエンジニアの司令塔になったような高揚感を味わえます。
Daskローカルクラスターの起動とダッシュボードの表示
JupyterLabのセルで以下を実行してください。
from dask.distributed import Client
import dask.delayed as delayed
import time
1. ローカルPC上にDaskの司令塔(Client)を立てます
これにより、バックグラウンドでマルチプロセスのワーカーが立ち上がります
client = Client()
JupyterLab上でダッシュボードのURLを確認する
リンクをクリックすると、現在のCPU/メモリ負荷がリアルタイムで視覚化されるタブが開きます!
client
実行すると、セルに `Client` の情報(Dashboardのリンク付き)が表示されます。そのリンクをクリック(または新しいタブで開く)してみてください。CPUの使用率やメモリの使用量が美しいグラフでリアルタイムに踊り出すのが見えます。
Daskを使った並列処理のHelloWorld
では、Daskの「遅延評価(Lazy Evaluation)」という強力な仕組みを使って、処理を並列実行してみましょう。
遅延関数の定義(@delayed デコレーターを使うことで、「今は実行せず、後でまとめてスケジュールする」予約になります)
@delayed
def process_data(x):
time.sleep(2) # 重たい処理を模す
return x 10
4つのタスクを定義する
t1 = process_data(1)
t2 = process_data(2)
t3 = process_data(3)
t4 = process_data(4)
この時点ではまだ計算は始まっていません。
.compute() を呼んだ瞬間に、Daskが最適化されたスケジュールを組み、全コアを使って並列実行します!
print(“計算を開始します…”)
start = time.time()
まとめて計算を実行
results = dask.compute(t1, t2, t3, t4)
print(f”Daskでの処理時間: {time.time() – start:.2f} 秒”)
print(f”結果: {results}”)
先ほど開いたDask Dashboardの画面を見ていましたか?
計算が走った瞬間、CPUのグラフがガッツリと跳ね上がり、4つのタスクが綺麗に並列処理された様子がグラフとして描画されたはずです。この「目に見える最適化」が、開発者のモチベーションを猛烈に高めてくれます。
最後に、作成したクラスターを綺麗に閉じおわります。
セッションが終わったら必ずクライアントを閉じ、プロセスを解放しましょう
client.close()
—
5. 現場のプロが教える「マルチプロセス開発の重大な注意点」
さて、ここまで読んで「よし、明日から全部のコードを `n_jobs=-1` にしよう!」と思ったあなた、ちょっと待ってください。
マルチプロセス(並列処理)には、知っておかなければ痛い目を見る「落とし穴」があります。ここを知っているかどうかが、プロとアマの分かれ道です。
① データのコピーオーバーヘッド(IPCのコスト)
マルチプロセスは、Pythonの独立したプロセスを複数立ち上げます。そのため、メインのプロセスにある巨大なデータフレームや配列を、それぞれのワーカー(子プロセス)に渡す際、データをコピーする時間(プロセス間通信のコスト)が発生します。
あまりにも小粒な処理を大量に並列化すると、データをコピーする時間の方が長くなり、かえって遅くなる(オーバーヘッド負けする)現象が起きます。
- 対策: ある程度まとまった塊(チャンク)ごとに処理を分割する。
② JupyterLab特有の注意:Jupyterの再起動
マルチプロセスでバグや無限ループを踏んでしまった場合、カーネルのメモリ空間が汚染されたり、ゾンビプロセスが残ることがあります。
もし「なんだか処理がフリーズしたまま戻らない」「CPU使用率が100%のまま落ちない」という事態になったら、焦らずJupyterLabのメニューから [Kernel] -> [Restart Kernel] を実行し、OS側のリソースを綺麗にリセットしてください。
—
おわりに
お疲れ様でした!
今回は、JupyterLabの単一カーネルの限界を突破し、`joblib` と `Dask` を用いてローカルのCPUコアを極限まで使い倒すマルチコア活用術を解説しました。
- いつものforループを `joblib.Parallel` で包むだけで劇的に速くなること。
- `Dask` を使えば、処理の負荷状況をリアルタイムのダッシュボードで視覚的に監視できること。
これらを自分の手で動かせるようになったあなたは、もう「ただJupyterを使っている人」ではありません。裏側のハードウェアアーキテクチャまで意識してコードを最適化できる、ワンランク上のAI・データサイエンスエンジニアへの第一歩を踏み出しています。
明日からのコーディングが、待ち遠しくなりましたね。
あなたの開発環境が、最高にパワフルで快適なものになりますように!