NumPyのブロードキャスト(形状の自動変換)

NumPy配列ndarray同士の二項演算(四則演算など)ではブロードキャスト(broadcasting)という仕組みによりそれぞれの形状shapeが同じになるように自動的に変換される。 ここでは以下の内容について説明します。

NumPyのブロードキャストのルール ブロードキャストの具体例 二次元配列の例 三次元配列の例

ブロードキャストできない(エラーになる)場合 掛け算(乗算)のブロードキャスト 要素積(アダマール積) 行列積

ブロードキャスト結果を取得する関数 配列を任意の形状にブロードキャスト: np.broadcast_to() 複数の配列をブロードキャスト: np.broadcast_arrays()

公式ドキュメントのブロードキャストについての説明ページは以下。

Broadcasting — NumPy v1.16 Manual

任意の形状に変換したい場合はreshape()やnp.newaxisを使います。

NumPyのブロードキャストのルール

NumPyにおけるブロードキャストは以下の2つのルールによって実行される。

次元数を揃える 2つの配列の次元数が異なる場合、次元数が少ない方の配列の先頭にサイズ(長さ)が1の新しい次元を追加して次元数を揃える。

各次元のサイズ(長さ)を揃える 2つの配列の各次元のサイズが一致しない場合、サイズが1である次元は他方の配列の次元のサイズに引き伸ばされる(値が繰り返される)。 2つの配列のどちらのサイズも1ではない次元が存在するとブロードキャストできずにエラーとなります。

なお、配列ndarrayの次元数はndim属性、形状はshape属性で取得できます。

ブロードキャストの具体例

二次元配列の例

二次元配列と一次元配列 二次元配列と一次元配列の2つのndarrayを例とします。ブロードキャストの結果が分かりやすいように片方はzeros()ですべての要素を0にしています。データ型dtypeを整数intにしているのは見た目のためで特に意味はない。

import numpy as np

a = np.zeros((3, 3), np.int)
print(a)
# [[0 0 0]
#  [0 0 0]
#  [0 0 0]]

print(a.shape)
# (3, 3)

b = np.arange(3)
print(b)
# [0 1 2]

print(b.shape)
# (3,)

一次元配列の形状shapeが(3,)となっているのは「要素数が1のタプルは末尾にカンマが付く」というPythonの仕様のため。うしろに何かが省略されているわけではないので末尾のカンマは特に気にしなくてもよい。

この2つのndarrayの足し算(加算)の結果は以下のようになる。

print(a + b)
# [[0 1 2]
#  [0 1 2]
#  [0 1 2]]

上述のルールに則って次元数が少ない方(一次元配列b)を明示的に変換していく。 まず、ルール1で次元数が揃えられる。先頭にサイズ(長さ)が1の新しい次元が追加され、(1, 3)(1行3列)の二次元配列となります。ここではreshape()メソッドを使っている。

b_1_3 = b.reshape(1, 3)
print(b_1_3)
# [[0 1 2]]

print(b_1_3.shape)
# (1, 3)

次に、ルール2により各次元のサイズ(長さ)が揃えられる。(1, 3)(1行3列)が(3, 3)(3行3列)となります。引き伸ばされた部分は元の部分の繰り返し。ここではnp.tile()を使っている。

numpy.tile — NumPy v1.16 Manual

print(np.tile(b_1_3, (3, 1)))
# [[0 1 2]
#  [0 1 2]
#  [0 1 2]]

値が0の配列との加算結果(=ブロードキャストされた結果)と同じ配列が得られていることが分かる。 なお、ここでは説明のためreshape()とnp.tile()を使ったが、ブロードキャスト結果の配列ndarrayが取得したい場合は専用の関数np.broadcast_to(), np.broadcast_arrays()があります。後述。 二次元配列と二次元配列 片方の配列を(3, 1)(3行1列)の二次元配列とすると加算結果は以下の通り。

b_3_1 = b.reshape(3, 1)
print(b_3_1)
# [[0]
#  [1]
#  [2]]

print(b_3_1.shape)
# (3, 1)

print(a + b_3_1)
# [[0 0 0]
#  [1 1 1]
#  [2 2 2]]

この場合、次元数はすでに揃っているので、ブロードキャストではルール2に従って(3, 1)(3行1列)が(3, 3)(3行3列)に引き伸ばされる処理のみが行われている。

print(np.tile(b_3_1, (1, 3)))
# [[0 0 0]
#  [1 1 1]
#  [2 2 2]]

これまでの例では片方の配列のみがブロードキャストで変換されていたが、二項演算で処理される2つの配列が両方ともブロードキャストで変換される場合もあります。 (1, 3)(1行3列)と(3, 1)(3行1列)の配列を例とします。加算結果は以下の通り。

print(b_1_3)
# [[0 1 2]]

print(b_1_3.shape)
# (1, 3)

print(b_3_1)
# [[0]
#  [1]
#  [2]]

print(b_3_1.shape)
# (3, 1)

print(b_1_3 + b_3_1)
# [[0 1 2]
#  [1 2 3]
#  [2 3 4]]

ルール2は「2つの配列の各次元のサイズが一致しない場合、サイズが1である次元は他方の配列の次元のサイズに引き伸ばされる(値が繰り返される)」というもの。(1, 3)も(3, 1)も(3, 3)に変換される。

print(np.tile(b_1_3, (3, 1)))
# [[0 1 2]
#  [0 1 2]
#  [0 1 2]]

print(np.tile(b_3_1, (1, 3)))
# [[0 0 0]
#  [1 1 1]
#  [2 2 2]]

print(np.tile(b_1_3, (3, 1)) + np.tile(b_3_1, (1, 3)))
# [[0 1 2]
#  [1 2 3]
#  [2 3 4]]

片方が一次元配列でも同様。

c = np.arange(4)
print(c)
# [0 1 2 3]

print(c.shape)
# (4,)

print(b_3_1)
# [[0]
#  [1]
#  [2]]

print(b_3_1.shape)
# (3, 1)

print(c + b_3_1)
# [[0 1 2 3]
#  [1 2 3 4]
#  [2 3 4 5]]

一次元配列は(4,) → (1, 4) → (3, 4)、二次元配列は(3, 1) → (3, 4)というように変換が行われている。

print(np.tile(c.reshape(1, 4), (3, 1)))
# [[0 1 2 3]
#  [0 1 2 3]
#  [0 1 2 3]]

print(np.tile(b_3_1, (1, 4)))
# [[0 0 0 0]
#  [1 1 1 1]
#  [2 2 2 2]]

print(np.tile(c.reshape(1, 4), (3, 1)) + np.tile(b_3_1, (1, 4)))
# [[0 1 2 3]
#  [1 2 3 4]
#  [2 3 4 5]]

なお、次元のサイズを引き伸ばすルール2の処理の対象となるのは元のサイズが1の場合のみ。それ以外はブロードキャストできずにエラーとなります。後述。

三次元配列の例

ルール1は次元数の差が2以上でも適用される。 三次元配列と一次元配列を例とすると加算結果は以下の通り。

a = np.zeros((2, 3, 4), dtype=np.int)
print(a)
# [[[0 0 0 0]
#   [0 0 0 0]
#   [0 0 0 0]]
#
#  [[0 0 0 0]
#   [0 0 0 0]
#   [0 0 0 0]]]

print(a.shape)
# (2, 3, 4)

b = np.arange(4)
print(b)
# [0 1 2 3]

print(b.shape)
# (4,)

print(a + b)
# [[[0 1 2 3]
#   [0 1 2 3]
#   [0 1 2 3]]
#
#  [[0 1 2 3]
#   [0 1 2 3]
#   [0 1 2 3]]]

(4, ) → (1, 1, 4) → (2, 3, 4)という流れで形状shapeが変換される。

b_1_1_4 = b.reshape(1, 1, 4)
print(b_1_1_4)
# [[[0 1 2 3]]]

print(np.tile(b_1_1_4, (2, 3, 1)))
# [[[0 1 2 3]
#   [0 1 2 3]
#   [0 1 2 3]]
#
#  [[0 1 2 3]
#   [0 1 2 3]
#   [0 1 2 3]]]

ブロードキャストできない(エラーになる)場合

上述のように、次元のサイズを引き伸ばすルール2の処理の対象となるのは元のサイズが1の場合のみ。次元のサイズが異なっていてどちらの配列のサイズも1でないとブロードキャストできずにエラーとなります。

a = np.zeros((4, 3), dtype=np.int)
print(a)
# [[0 0 0]
#  [0 0 0]
#  [0 0 0]
#  [0 0 0]]

print(a.shape)
# (4, 3)

b = np.arange(6).reshape(2, 3)
print(b)
# [[0 1 2]
#  [3 4 5]]

print(b.shape)
# (2, 3)

# print(a + b)
# ValueError: operands could not be broadcast together with shapes (4,3) (2,3)

以下の場合も同様。

a = np.zeros((2, 3, 4), dtype=np.int)
print(a)
# [[[0 0 0 0]
#   [0 0 0 0]
#   [0 0 0 0]]
#
#  [[0 0 0 0]
#   [0 0 0 0]
#   [0 0 0 0]]]

print(a.shape)
# (2, 3, 4)

b = np.arange(3)
print(b)
# [0 1 2]

print(b.shape)
# (3,)

# print(a + b)
# ValueError: operands could not be broadcast together with shapes (2,3,4) (3,)

この例では、後ろ側(形状shapeでいう右側)に新たな次元を追加するとブロードキャストされる。

b_3_1 = b.reshape(3, 1)
print(b_3_1)
# [[0]
#  [1]
#  [2]]

print(b_3_1.shape)
# (3, 1)

print(a + b_3_1)
# [[[0 0 0 0]
#   [1 1 1 1]
#   [2 2 2 2]]
#
#  [[0 0 0 0]
#   [1 1 1 1]
#   [2 2 2 2]]]

ブロードキャストできるかどうかは形状shapeを右寄せして考えると分かりやすい。 NG (2, 3, 4) ( 3)

OK (2, 3, 4) ( 3, 1) -> (1, 3, 1) -> (2, 3, 4)

右寄せして縦に比べたときにサイズが異なっている場合は片方が1でないとブロードキャストできない。 具体的な例として、画像の配列の場合、カラー画像は形状が(高さ, 幅, 色)の三次元配列(色は赤・緑・青の3)、単色画像は形状が(高さ, 幅)の二次元配列となります。

カラー画像の各色の値に対して単色画像の値を足したり引いたりしたい場合、そのままだと高さと幅が同じでもブロードキャストできない。np.newaxisなどを使って単色画像の最後に次元を追加する必要があります。

以下のようなイメージ。 NG (y, x, 3) ( y, x)

OK (y, x, 3) (y, x, 1) -> (y, x, 3)

掛け算(乗算)のブロードキャスト

これまでの例は足し算(加算)だったが、引き算(減算)や割り算(除算)などでも同様にブロードキャストが行われる。 掛け算(乗算)でも要素積に対しては同様にブロードキャストされるが、行列積の場合は異なるので注意。

要素積(アダマール積)

*演算子あるいはnp.multiply()では同じ位置の要素同士が掛け算(乗算)される。

a = b = np.arange(3)
print(a)
# [0 1 2]

print(a.shape)
# (3,)

print(a * b)
# [0 1 4]

print(np.multiply(a, b))
# [0 1 4]

このような要素ごとの積はアダマール積と呼ばれる。

アダマール積 - Wikipedia

形状shapeが異なる場合、これまでの例のようにブロードキャストされる。

a_1_3 = a.reshape(1, 3)
print(a_1_3)
# [[0 1 2]]

print(a_1_3.shape)
# (1, 3)

b_3_1 = b.reshape(3, 1)
print(b_3_1)
# [[0]
#  [1]
#  [2]]

print(b_3_1.shape)
# (3, 1)

print(a_1_3 * b_3_1)
# [[0 0 0]
#  [0 1 2]
#  [0 2 4]]

print(np.multiply(a_1_3, b_3_1))
# [[0 0 0]
#  [0 1 2]
#  [0 2 4]]


print(a * b_3_1)
# [[0 0 0]
#  [0 1 2]
#  [0 2 4]]

print(np.multiply(a, b_3_1))
# [[0 0 0]
#  [0 1 2]
#  [0 2 4]]

行列積

要素ごとの積ではなくいわゆる行列の積(matrix product)を計算したい場合はnp.matmul(), @演算子, np.dot()を使います。 @演算子はnp.matmul()と等価だがPython3.5で追加されたものでそれより前のバージョンでは使えない。np.matmul()と np.dot()は三次元以上の多次元配列の扱いが異なる。

numpy.matmul — NumPy v1.16 Manual numpy.dot — NumPy v1.16 Manual

二次元配列の場合はどれでも同じだが、公式ドキュメントではmatmul()または@演算子の使用が好ましいとされています。

If both a and b are 2-D arrays, it is matrix multiplication, but using matmul or a @ b is preferred. numpy.dot — NumPy v1.16 Manual

ここでは二次元および一次元配列の行列積について述べる。 行列積の演算のルール 二次元および一次元配列の行列積の演算においては、

1つ目の配列が一次元だと最初にサイズ(長さ)が1の新しい次元が追加され、結果からはその次元が取り除かれる 2つ目の配列が一次元だと最後にサイズ(長さ)が1の新しい次元が追加され、結果からはその次元が取り除かれる

というルールが適用される。 形状shapeに着目すると以下のように一般化される。@演算子で行列積を算出しているものとします。 2D @ 2D (n, m) @ (m, p) = (n, p)

1D @ 2D or 2D @ 1D (m, ) @ (m, p) -> (1, m) @ (m, p) = (1, p) -> (p, ) (n, m) @ (m, ) -> (n, m) @ (m, 1) = (n, 1) -> (n, )

1D @ 1D (m, ) @ (m, ) -> (1, m) @ (m, 1) = (1, 1) -> scalar

以下、具体例を示す。 二次元配列と二次元配列の行列積 二次元配列同士の行列積の結果は二次元配列となります。

print(a_1_3 @ b_3_1)
# [[5]]

print(np.matmul(a_1_3, b_3_1))
# [[5]]

print(np.dot(a_1_3, b_3_1))
# [[5]]

print(type(a_1_3 @ b_3_1))
# <class 'numpy.ndarray'>

print((a_1_3 @ b_3_1).shape)
# (1, 1)

二次元配列と一次元配列の行列積 一次元配列は上述のように最初か最後に次元が追加されて行列積が算出される。結果からは追加された次元は削除されるので一次元配列となります。

print(a_1_3 @ b)
# [5]

print(np.matmul(a_1_3, b))
# [5]

print(np.dot(a_1_3, b))
# [5]

print(type(a_1_3 @ b))
# <class 'numpy.ndarray'>

print((a_1_3 @ b).shape)
# (1,)

一次元配列と一次元配列の行列積 一次元配列同士の行列積の結果はスカラーとなります。両方の配列に上述のルールが適用されています。

print(a @ b)
# 5

print(np.matmul(a, b))
# 5

print(np.dot(a, b))
# 5

print(type(a @ b))
# <class 'numpy.int64'>

行列積の注意点 行列積は1つ目の配列の列数と2つ目の配列の行数が一致していないと計算できないが、それを揃えるために自動的にブロードキャストされたりはしない。

a = np.arange(6).reshape(2, 3)
print(a)
# [[0 1 2]
#  [3 4 5]]

b = np.arange(2).reshape(1, 2)
print(b)
# [[0 1]]

# print(a @ b)
# ValueError: shapes (2,3) and (1,2) not aligned: 3 (dim 1) != 1 (dim 0)

要素積やその他の四則演算のブロードキャストのようにサイズが足りない次元を引き伸ばしたい(値を繰り返したい)場合はnp.tile()を使います。

print(np.tile(b, (3, 1)))
# [[0 1]
#  [0 1]
#  [0 1]]

print(a @ np.tile(b, (3, 1)))
# [[ 0  3]
#  [ 0 12]]

ブロードキャスト結果を取得する関数

配列を任意の形状にブロードキャスト: np.broadcast_to()

配列ndarrayを任意の形状shapeにブロードキャストしたい場合はnp.broadcast_to()を使います。

numpy.broadcast_to — NumPy v1.16 Manual

第一引数に元のndarray、第二引数に形状shapeを示すタプルやリストを指定します。ブロードキャストされたndarrayが返される。

a = np.arange(3)
print(a)
# [0 1 2]

print(a.shape)
# (3,)

print(np.broadcast_to(a, (3, 3)))
# [[0 1 2]
#  [0 1 2]
#  [0 1 2]]

print(type(np.broadcast_to(a, (3, 3))))
# <class 'numpy.ndarray'>

ブロードキャストできない形状を指定するとエラー。

# print(np.broadcast_to(a, (2, 2)))
# ValueError: operands could not be broadcast together with remapped shapes [original->remapped]: (3,) and requested shape (2,2)

複数の配列をブロードキャスト: np.broadcast_arrays()

複数の配列ndarrayをブロードキャストして形状を揃えたい場合はnp.broadcast_arrays()を使います。

numpy.broadcast_arrays — NumPy v1.16 Manual

可変長引数になっているので複数の配列をカンマ区切りで指定します。ndarrayのリストが返される。

a = np.arange(3)
print(a)
# [0 1 2]

print(a.shape)
# (3,)

b = np.arange(3).reshape(3, 1)
print(b)
# [[0]
#  [1]
#  [2]]

print(b.shape)
# (3, 1)

arrays = np.broadcast_arrays(a, b)

print(type(arrays))
# <class 'list'>

print(len(arrays))
# 2

print(arrays[0])
# [[0 1 2]
#  [0 1 2]
#  [0 1 2]]

print(arrays[1])
# [[0 0 0]
#  [1 1 1]
#  [2 2 2]]

print(type(arrays[0]))
# <class 'numpy.ndarray'>

ブロードキャストできない配列の組み合わせを指定するとエラー。

c = np.zeros((2, 2))
print(c)
# [[0. 0.]
#  [0. 0.]]

print(c.shape)
# (2, 2)

# arrays = np.broadcast_arrays(a, c)
# ValueError: shape mismatch: objects cannot be broadcast to a single shape

シェア

関連カテゴリー

Python NumPy

NumPy配列の行・列ごとの合計、平均、最大、最小などを算出 『Python Data Science Handbook』(英語の無料オンライン版あり) NumPyのバージョンを確認(np.version) NumPy配列ndarrayの最大値・最小値のインデックス(位置)を取得 NumPy配列ndarrayから条件を満たす要素・行・列を抽出、削除 NumPy配列ndarrayとPython標準のリストを相互に変換 NumPyで全要素を同じ値で初期化した配列ndarrayを生成 Python, NumPyで画像処理(読み込み、演算、保存) NumPyで空の配列ndarrayを生成するemptyとempty_like pandas参考書『Pythonによるデータ分析入門』の注意点 NumPy配列ndarrayの行・列を任意の順番に並べ替え、選択(抽出) Pythonでの画像処理、Pillow, NumPy, OpenCVの違いと使い分け NumPyのデータ型dtype一覧とastypeによる変換(キャスト) NumPy配列ndarrayの条件を満たす要素数をカウント Python, NumPy(OpenCV)で画像を二値化処理

Last Updated: 6/26/2019, 10:34:03 PM