ブロードキャスティング — 多次元配列演算ルール
このトピックを完了すると、次のことが理解できるようになります。
NumPyのブロードキャスティングが存在する理由を理解し、3つのルールを適用して演算が可能かどうかを判断し、ブロードキャスティングを使用して効率的なコードを作成できるようになります。
ブロードキャスティングが必要なのはなぜですか?
import numpy as np
# すべての要素に10を追加したいarr = np.array([1, 2, 3, 4, 5])result = arr + 10print(result) # [11 12 13 14 15]arrは5つの要素を持ち、10はスカラー(1つの要素)です。サイズが異なるにもかかわらず、加算が可能です。これは、NumPyが10を[10, 10, 10, 10, 10]に自動的に拡張するためです。この自動拡張がブロードキャスティングです。
ブロードキャスティングがない場合:
# 毎回、次のようなコードを書く必要がありますresult = arr + np.full(5, 10) # 非効率同じ形状での演算 — 基本
サイズが同じ場合、要素ごとの演算になります。
a = np.array([1, 2, 3])b = np.array([10, 20, 30])
print(a + b) # [11 22 33]print(a * b) # [10 40 90]これは2次元配列にも適用されます。
A = np.array([[1, 2], [3, 4]])B = np.array([[10, 20], [30, 40]])
print(A + B)# [[11 22]# [33 44]]ブロードキャスティングルール — 3つのルール
サイズが異なる場合、NumPyは3つのルールを順番に適用します。
ルール1:次元数が異なる場合、小さい配列の先頭に1を追加します。
a = np.array([[1, 2, 3], # shape: (2, 3) [4, 5, 6]])b = np.array([10, 20, 30]) # shape: (3,)
# ルール1:bの形状(3,)を(1, 3)に拡張# ルール2を適用した後:(1, 3) -> (2, 3)print(a + b)# [[11 22 33]# [14 25 36]]ルール2:サイズが1の次元を、他の配列と一致するように拡張します。
a = np.array([[1, 2, 3]]) # shape: (1, 3)b = np.array([[10], # shape: (3, 1) [20], [30]])
# a: (1, 3) -> 行方向に拡張 -> (3, 3)# b: (3, 1) -> 列方向に拡張 -> (3, 3)print(a + b)# [[11 12 13]# [21 22 23]# [31 32 33]]ルール3:サイズが異なり、どちらの側も1でない場合、エラーになります。
a = np.array([1, 2, 3]) # shape: (3,)b = np.array([10, 20]) # shape: (2,)
# 3 vs 2 — どちらの側も1ではない -> エラー!# a + b -> ValueError: operands could not be broadcast together視覚的に理解する
(4, 3) + (3,) -> OK!
a: [[1 2 3] b: [10 20 30]
[4 5 6] ↓ ルール1: (1, 3)
[7 8 9] ↓ ルール2: (4, 3)
[0 1 2]]
b': [[10 20 30]
[10 20 30]
[10 20 30]
[10 20 30]]
結果: [[11 22 33]
[14 25 36]
[17 28 39]
[10 21 32]](3, 1) + (1, 4) -> OK! 結果の形状: (3, 4)
a: [[1] b: [[10 20 30 40]]
[2] ↓ ルール2: (3, 4)
[3]] b': [[10 20 30 40]
↓ ルール2 [10 20 30 40]
a': [[1 1 1 1] [10 20 30 40]]
[2 2 2 2]
[3 3 3 3]]
結果: [[11 12 13 14]
[12 13 14 15]
[13 14 15 16]]実践的なパターン — 正規化
# 各列の平均を引いて、中心化するdata = np.array([[170, 60, 30], [180, 75, 25], [165, 55, 35], [175, 70, 28]])# shape: (4, 3) — 4人のデータ、3つの測定値(身長、体重、年齢)
col_mean = data.mean(axis=0) # shape: (3,) — 各列の平均print(col_mean) # [172.5 65. 29.5]
centered = data - col_mean # (4, 3) - (3,) -> ブロードキャスティング!print(centered)# [[ -2.5 -5. 0.5]# [ 7.5 10. -4.5]# [ -7.5 -10. 5.5]# [ 2.5 5. -1.5]]Zスコア正規化
col_std = data.std(axis=0)z_scores = (data - col_mean) / col_std # ブロードキャスティングを2回!(4, 3) - (3,) -> OK、(4, 3) / (3,) -> OK。この単一のパターンで、データセット全体を正規化できます。
ループとの比較
# 悪い:Pythonループ — 遅いdata = np.random.rand(10000, 100)mean = data.mean(axis=0)
result = np.zeros_like(data)for i in range(data.shape[0]): for j in range(data.shape[1]): result[i, j] = data[i, j] - mean[j]
# 良い:ブロードキャスティング — 100倍以上高速result = data - meanブロードキャスティングは、内部でCで実装されたベクトル演算です。Pythonループよりも数十倍から数百倍高速であり、コードは1行で済みます。
実践的なパターン — 距離行列
2つのポイントセット間のすべての距離を一度に計算します。
# 3つのポイント:(1, 0)、(2, 3)、(4, 1)points = np.array([[1, 0], [2, 3], [4, 1]]) # shape: (3, 2)
# すべてのペア間のユークリッド距離diff = points[:, np.newaxis, :] - points[np.newaxis, :, :]# (3, 1, 2) - (1, 3, 2) -> ブロードキャスティング -> (3, 3, 2)
dist = np.sqrt((diff ** 2).sum(axis=2))print(dist.round(2))# [[0. 3.16 3.16]# [3.16 0. 2.83]# [3.16 2.83 0. ]]np.newaxisを使用して次元を追加し、ブロードキャスティングを使用してすべてのペア間の差を一度に計算します。これは、二重のforループよりも数十倍高速です。
注意 — メモリの爆発
a = np.random.rand(10000, 1) # shape: (10000, 1)b = np.random.rand(1, 10000) # shape: (1, 10000)c = a + b # shape: (10000, 10000) — 1億個の要素!# メモリ:10000 × 10000 × 8バイト = 800MBブロードキャスティングは概念的に拡張されますが、結果の配列は実際にメモリを占有します。(10000, 1) + (1, 10000)は、サイズ(10000, 10000)の配列を作成します。大量のデータの場合、結果の形状を事前に計算して、メモリ使用量を確認します。
# メモリ使用量を予測result_shape = (10000, 10000)dtype_size = 8 # float64 = 8バイトmemory_bytes = result_shape[0] * result_shape[1] * dtype_sizeprint(f"{memory_bytes / 1e6:.0f} MB") # 800 MB形状の互換性の簡単なチェック
右から一致させ、各次元が同じであるか、どちらかの側が1である場合、互換性があります。
(4, 3) + (3,) -> (4, 3) + (1, 3) -> OK -> (4, 3)
(4, 3) + (4, 1) -> OK -> (4, 3)
(3, 1) + (1, 4) -> OK -> (3, 4)
(4, 3) + (2,) -> (4, 3) + (1, 2) -> 3 vs 2 -> エラー
(2, 3, 4) + (3, 1) -> (2, 3, 4) + (1, 3, 1) -> OK -> (2, 3, 4)まとめ
| ルール | 説明 |
|---|---|
| ルール1 | 次元数が異なる場合、小さい配列の先頭に1を追加します。 |
| ルール2 | サイズが1の次元を、他の配列と一致するように拡張します。 |
| ルール3 | サイズが異なり、どちらの側も1でない場合、エラーになります。 |
ブロードキャスティングは、NumPyの最も強力な機能の1つです。データ正規化、距離計算、行列変換などを、forループなしで実行できます。重要なのは、形状を見て、すぐに「右から一致させ、同じか1であればOK」と判断することです。
同じ原則はpandasにも適用されます。DataFrame - Seriesのような演算は、列に沿ってブロードキャストされます。NumPyのブロードキャスティングを理解すると、自然にpandasのベクトル演算も理解できるようになります。
import pandas as pd
df = pd.DataFrame({"A": [10, 20, 30], "B": [40, 50, 60]})means = df.mean() # Series: A=20, B=50centered = df - means # ブロードキャスティング!各列から平均を引くprint(centered)# A B# 0 -10.0 -10.0# 1 0.0 0.0# 2 10.0 10.0