【numpy】newaxis を使った次元拡張のテクニック

はい、承知いたしました。【numpy】newaxis を使った次元拡張のテクニック の詳細な説明を含む記事を記述します。約5000語を目指し、技術的な詳細、豊富なコード例、そして関連する概念(ブロードキャスト、reshapeなど)との比較を含めて解説します。

以下が記事の内容です。


【NumPy徹底解説】numpy.newaxis を活用した次元拡張のテクニック

はじめに:NumPyと次元の重要性

数値計算ライブラリであるNumPyは、Pythonで高速かつ効率的な多次元配列操作を可能にします。データサイエンス、機械学習、科学技術計算など、幅広い分野でNumPyは基盤となっています。NumPy配列(ndarray)の最も重要な特性の一つは「次元(dimension)」または「軸(axis)」の概念です。1次元配列はベクトル、2次元配列は行列、3次元配列はテンソルと見なすことができ、より高次元のデータ構造を扱うことも珍しくありません。

配列の次元や形状(shape)は、配列に対する操作、特に異なる形状を持つ配列間での演算(ブロードキャスト)において極めて重要になります。配列の形状を意図通りに操作できることは、NumPyを効果的に使いこなすための必須スキルです。

この記事では、NumPy配列の形状を操作するためのシンプルでありながら非常に強力なテクニック、numpy.newaxis の使い方に焦点を当てて詳細に解説します。newaxis が何であるか、どのように使用するのか、そしてそれがなぜNumPyの多くの操作、特にブロードキャストにおいて不可欠なのかを、豊富なコード例と共に深く掘り下げていきます。

NumPy配列の基本:次元、形状、インデックス参照

newaxis の説明に入る前に、NumPy配列の基本的な概念を簡単に復習しましょう。

次元 (Dimension / Axis)

配列の次元は、そのデータを記述するために必要なインデックスの数です。
* 1次元配列: 要素にアクセスするために1つのインデックスが必要です。例:[1, 2, 3]
* 2次元配列: 要素にアクセスするために2つのインデックスが必要です(行と列)。例:[[1, 2], [3, 4]]
* 3次元配列: 要素にアクセスするために3つのインデックスが必要です。例:[[[1, 2], [3, 4]], [[5, 6], [7, 8]]]

これらの次元は「軸(axis)」とも呼ばれます。NumPyでは、軸は通常0から始まります。1次元配列には軸0が1つ、2次元配列には軸0(行)と軸1(列)の2つ、3次元配列には軸0、軸1、軸2の3つがあります。

“`python
import numpy as np

1次元配列

arr1d = np.array([1, 2, 3, 4])
print(f”1次元配列: {arr1d}”)
print(f”次元数: {arr1d.ndim}”) # ndimは次元数を返す
print(f”形状: {arr1d.shape}”) # shapeはタプルで形状を返す

2次元配列

arr2d = np.array([[1, 2, 3], [4, 5, 6]])
print(f”\n2次元配列:\n{arr2d}”)
print(f”次元数: {arr2d.ndim}”)
print(f”形状: {arr2d.shape}”) # (行数, 列数)

3次元配列

arr3d = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
print(f”\n3次元配列:\n{arr3d}”)
print(f”次元数: {arr3d.ndim}”)
print(f”形状: {arr3d.shape}”) # (軸0のサイズ, 軸1のサイズ, 軸2のサイズ)
“`

形状 (Shape)

配列の形状は、各次元(軸)のサイズを要素とするタプルです。例えば、arr2d の形状が (2, 3) であることは、軸0に沿ってサイズが2、軸1に沿ってサイズが3であることを意味します。

NumPyの多くの操作は、入力配列の形状と出力配列の形状がどのように関連するかを理解している必要があります。newaxis は、この形状タプルにサイズ1の新しい次元を挿入するために使用されます。

インデックス参照 (Indexing)

配列の要素や部分配列にアクセスするには、インデックス参照(またはスライス)を使用します。
arr[i], arr[i, j], arr[i, j, k] のように、次元数に応じたインデックスまたはスライスを指定します。
スライス : は「その次元のすべての要素」を意味します。

“`python
arr2d = np.array([[10, 11, 12], [13, 14, 15]])

特定の要素にアクセス

print(f”\narr2d[0, 1]: {arr2d[0, 1]}”) # 11

スライスを使用して部分配列にアクセス

print(f”arr2d[0, :]: {arr2d[0, :]}”) # 1行目全体 -> [10 11 12] (形状 (3,) の1次元配列)
print(f”arr2d[:, 1]: {arr2d[:, 1]}”) # 2列目全体 -> [11 14] (形状 (2,) の1次元配列)
print(f”arr2d[:, :]:\n{arr2d[:, :]}”) # 配列全体 -> 形状 (2, 3) の2次元配列
“`

インデックス参照やスライスの結果は、元の配列よりも次元が減ることがあります(例えば、2次元配列から1行を取り出すと1次元配列になる)。newaxis は、これとは逆に、インデックス参照の際に次元を「増やす」ために使用されます。

numpy.newaxis とは何か?

numpy.newaxis は、NumPy配列のインデックス参照の際に使用される特殊なオブジェクトです。その実体は単に None ですが、インデックスとして使用された場合に特別な意味を持ちます。

numpy.newaxis をインデックスタプル内のどこかに挿入すると、その場所にサイズ1の新しい次元が追加された配列ビュー(view)が生成されます。要素の総数やデータ自体は変わりませんが、配列の形状次元数が変わります。

例えば、形状が (n,) の1次元配列に newaxis を適用すると、形状が (1, n) の2次元配列(行ベクトルとして扱える)や (n, 1) の2次元配列(列ベクトルとして扱える)に変換できます。

“`python
import numpy as np

arr = np.array([1, 2, 3])
print(f”元の配列: {arr}, 形状: {arr.shape}, 次元数: {arr.ndim}”)

np.newaxis をインデックスの最初の位置に挿入

arr[np.newaxis, …] と等価 (NumPyは自動的に … を補完)

arr_row = arr[np.newaxis, :]
print(f”先頭に次元を追加: {arr_row}, 形状: {arr_row.shape}, 次元数: {arr_row.ndim}”)

np.newaxis をインデックスの最後の位置に挿入

arr_col = arr[:, np.newaxis]
print(f”末尾に次元を追加:\n{arr_col}, 形状: {arr_col.shape}, 次元数: {arr_col.ndim}”)
“`

出力を見るとわかるように、arr は形状 (3,) の1次元配列でしたが、arr[np.newaxis, :] は形状 (1, 3) の2次元配列に、arr[:, np.newaxis] は形状 (3, 1) の2次元配列にそれぞれ変換されました。どちらの場合も要素数は3で変わりません。

newaxis の基本的な使い方と形状の変化

newaxis は、インデックス参照 [] の中にカンマ区切りで指定します。インデックス参照のタプル内のどの位置に newaxis を置くかによって、新しい次元が挿入される位置が決まります。他の位置には通常 : (その次元のすべての要素)を指定します。

1次元配列に新しい次元を追加する

元の形状 使い方 結果の形状 説明
(n,) arr[np.newaxis, :] (1, n) 軸0の前に新しい軸を追加
(n,) arr[:, np.newaxis] (n, 1) 軸0の後に新しい軸を追加

“`python
import numpy as np

arr = np.array([10, 20, 30, 40, 50])
print(f”元の配列: {arr.shape}”) # (5,)

先頭に新しい次元を追加

arr[np.newaxis, :] は arr[None, :] と同じ意味

arr_shape1 = arr[np.newaxis, :]
print(f”arr[np.newaxis, :].shape: {arr_shape1.shape}”) # (1, 5)

末尾に新しい次元を追加

arr_shape2 = arr[:, np.newaxis]
print(f”arr[:, np.newaxis].shape: {arr_shape2.shape}”) # (5, 1)

複数の新しい次元を追加することも可能

arr_shape3 = arr[np.newaxis, :, np.newaxis]
print(f”arr[np.newaxis, :, np.newaxis].shape: {arr_shape3.shape}”) # (1, 5, 1)
“`

2次元配列に新しい次元を追加する

2次元配列 (rows, cols)newaxis を適用すると、3次元配列になります。

元の形状 使い方 結果の形状 説明
(r, c) arr[np.newaxis, :, :] (1, r, c) 軸0(行)の前に新しい軸を追加
(r, c) arr[:, np.newaxis, :] (r, 1, c) 軸0と軸1の間に新しい軸を追加
(r, c) arr[:, :, np.newaxis] (r, c, 1) 軸1(列)の後に新しい軸を追加

“`python
import numpy as np

arr2d = np.array([[1, 2, 3], [4, 5, 6]])
print(f”元の2D配列: {arr2d.shape}”) # (2, 3)

軸0の前に新しい次元を追加

arr2d_shape1 = arr2d[np.newaxis, :, :]
print(f”arr2d[np.newaxis, :, :].shape: {arr2d_shape1.shape}”) # (1, 2, 3)

軸0と軸1の間に新しい次元を追加

arr2d_shape2 = arr2d[:, np.newaxis, :]
print(f”arr2d[:, np.newaxis, :].shape: {arr2d_shape2.shape}”) # (2, 1, 3)

軸1の後に新しい次元を追加

arr2d_shape3 = arr2d[:, :, np.newaxis]
print(f”arr2d[:, :, np.newaxis].shape: {arr2d_shape3.shape}”) # (2, 3, 1)
“`

高次元配列でも同様に、インデックス参照内の np.newaxis の位置に応じて、サイズ1の新しい次元が挿入されます。

newaxis をなぜ使うのか?:最も重要なユースケース「ブロードキャスト」

numpy.newaxis が最も頻繁かつ効果的に使用されるのは、NumPyの強力な機能であるブロードキャスト(Broadcasting)を可能にするためです。

ブロードキャストは、形状が異なるNumPy配列間での算術演算(加算、減算、乗算、除算など)を可能にするメカニズムです。厳密なルールに基づいて、NumPyは次元を「ストレッチ」して、演算を実行するために配列を一時的に互換性のある形状に変換しようとします。ただし、ブロードキャストが成功するためには、配列の形状が特定の互換性ルールを満たしている必要があります。

ブロードキャストの互換性ルール:
2つの配列の次元を後方から(末尾から)比較していきます。
1. 次元のサイズが等しい場合、互換性があります。
2. どちらかの次元のサイズが 1 の場合、互換性があります。サイズ1の次元は、もう一方の次元のサイズに合わせて「ストレッチ」されます。
3. どちらの次元のサイズも1ではなく、かつ等しくない場合、互換性がなく、エラー(ValueError: operands could not be broadcast together)が発生します。
比較する次元がなくなった場合(一方の配列が他方より次元が少ない場合)、次元が少ない方の配列の形状の先頭にサイズ1の次元が追加されたものとして比較が続けられます。

newaxis は、このブロードキャスト互換性ルールの「どちらかの次元のサイズが 1 の場合」という条件を満たすために、意図的にサイズ1の次元を作成するために使用されます。これにより、本来ブロードキャストできない形状の配列同士を演算できるようになります。

ブロードキャストの例:1D配列と2D配列の演算

例として、形状 (M, N) の2次元配列と形状 (N,) の1次元配列の加算を考えます。これは、2次元配列の各行に1次元配列を加算する操作に相当します。

“`python
import numpy as np

matrix = np.array([[1, 2, 3], [4, 5, 6]]) # 形状 (2, 3)
vector = np.array([10, 20, 30]) # 形状 (3,)

matrix と vector を加算

ブロードキャストが適用される

result = matrix + vector
print(f”Matrix:\n{matrix}”)
print(f”Vector: {vector}”)
print(f”Result of matrix + vector:\n{result}”)
print(f”Result shape: {result.shape}”) # (2, 3)
“`

これはNumPyのブロードキャストによって成功します。shapes (2, 3)(3,) を後方から比較すると:
* 末尾の次元: 3 と 3。等しいのでOK。
* 次の次元: matrixは 2、vectorは次元がない。vectorの形状の先頭にサイズ1が補われ (1, 3) と見なされる。21。片方が1なのでOK。
全体として互換性があり、vectorが各行に対してブロードキャストされて加算が実行されます。

では、形状 (M, N) の2次元配列の各列に形状 (M,) の1次元配列をブロードキャストして加算したい場合はどうでしょうか?

“`python
import numpy as np

matrix = np.array([[1, 2, 3], [4, 5, 6]]) # 形状 (2, 3)
vector_col = np.array([100, 200]) # 形状 (2,)

matrix の各列に vector_col を加算したい

shapes (2, 3) と (2,)

後方から比較: 3 と 2。等しくないし、どちらも1ではない -> ブロードキャスト不可!

result = matrix + vector_col # これはエラーになる!

print(result) # ValueError

“`

shapes (2, 3)(2,) ではブロードキャストできません。ここで newaxis の出番です。vector_col を形状 (2, 1) の2次元配列(列ベクトル)に変換すれば、shapes (2, 3)(2, 1) となり、ブロードキャストが可能になります。

形状 (2,)vector_col(2, 1) に変換するには、末尾に新しい次元を追加します。つまり vector_col[:, np.newaxis] を使用します。

“`python
import numpy as np

matrix = np.array([[1, 2, 3], [4, 5, 6]]) # 形状 (2, 3)
vector_col = np.array([100, 200]) # 形状 (2,)

vector_col を形状 (2, 1) に変換

vector_col_reshaped = vector_col[:, np.newaxis] # 形状 (2, 1)
print(f”vector_col_reshaped:\n{vector_col_reshaped}, shape: {vector_col_reshaped.shape}”)

matrix と 形状 (2, 1) の vector_col_reshaped を加算

shapes (2, 3) と (2, 1)

後方から比較: 3 と 1。片方が1なのでOK。

次の次元: 2 と 2。等しいのでOK。

互換性あり!

result_col = matrix + vector_col_reshaped
print(f”\nResult of matrix + vector_col_reshaped:\n{result_col}”)
print(f”Result shape: {result_col.shape}”)
“`

この例では、vector_col の形状 (2,)vector_col[:, np.newaxis] によって (2, 1) に変換しました。shapes (2, 3)(2, 1) のブロードキャストは以下のようになります。
* matrix shape: (2, 3)
* vector_col_reshaped shape: (2, 1)
* 後方から比較:
* 次元2 (インデックス1): 3 と 1 -> OK (1がストレッチされて3になる)
* 次元1 (インデックス0): 2 と 2 -> OK (等しい)
結果として、vector_col_reshaped の各要素 [[100], [200]] が、matrix の対応する行 [[1, 2, 3], [4, 5, 6]] の各要素 [1, 2, 3] および [4, 5, 6] に対して、列方向にブロードキャスト(繰り返し)されて加算が行われ、最終的な形状は (2, 3) となります。

ブロードキャストにおける newaxis の他の応用例

1D配列同士の外積(Outer Product)

形状 (M,) の1次元配列 v1 と形状 (N,) の1次元配列 v2 の外積は、結果として形状 (M, N) の2次元配列となり、その要素 [i, j]v1[i] * v2[j] となります。これは v1 を列ベクトル (M, 1) に、v2 を行ベクトル (1, N) に変換して、通常の行列乗算ではなく要素ごとの乗算を行うことで実現できます。

“`python
import numpy as np

v1 = np.array([1, 2, 3]) # 形状 (3,)
v2 = np.array([10, 20]) # 形状 (2,)

v1 を列ベクトル形状 (3, 1) に

v1_col = v1[:, np.newaxis]
print(f”v1_col shape: {v1_col.shape}”) # (3, 1)

v2 を行ベクトル形状 (1, 2) に

v2_row = v2[np.newaxis, :]
print(f”v2_row shape: {v2_row.shape}”) # (1, 2)

v1_col と v2_row の要素ごとの乗算 (ブロードキャスト)

shapes (3, 1) と (1, 2)

後方から比較: 1 と 2 -> OK (1がストレッチされて2になる)

次の次元: 3 と 1 -> OK (1がストレッチされて3になる)

結果形状は (3, 2)

outer_product = v1_col * v2_row
print(f”\nOuter product using newaxis:\n{outer_product}”)
print(f”Outer product shape: {outer_product.shape}”) # (3, 2)

np.outer 関数を使っても同じ結果が得られる

print(f”\nOuter product using np.outer:\n{np.outer(v1, v2)}”)
“`

ここでは、v1[:, np.newaxis] (形状 (3, 1)) と v2[np.newaxis, :] (形状 (1, 2)) の要素ごとの乗算を行うために newaxis が使用されています。ブロードキャストのルールにより、v1_col は列方向に2回、v2_row は行方向に3回ストレッチされ、形状 (3, 2) の中間配列が生成されたかのように演算が行われます。

複数のデータポイント間の距離計算

データサイエンスや機械学習では、複数のデータポイント間の距離(例えばユークリッド距離)を計算することがよくあります。2つの集合 A (形状 (N_A, D)) と B (形状 (N_B, D)) があり、それぞれ N_A 個および N_B 個の D 次元のデータポイントを表すとします。集合 A の各点と集合 B の各点の間のすべてのペアワイズ距離を計算したい場合、結果は形状 (N_A, N_B) の行列になります。

素朴な方法では二重ループになりますが、NumPyとブロードキャストを使えば効率的に計算できます。差の二乗和の平方根を計算するために、まず差 A[i] - B[j] を計算する必要があります。これをすべての ij のペアに対して行うために、A を形状 (N_A, 1, D) に、B を形状 (1, N_B, D) に変換します。

“`python
import numpy as np

2次元空間上の2つのデータポイント集合を想定

A = np.array([[1, 1], [2, 2], [3, 3]]) # 形状 (3, 2) – 3点, 各点2次元
B = np.array([[10, 10], [20, 20]]) # 形状 (2, 2) – 2点, 各点2次元

A を形状 (3, 1, 2) に変換

A_reshaped = A[:, np.newaxis, :]
print(f”A_reshaped shape: {A_reshaped.shape}”) # (3, 1, 2)

B を形状 (1, 2, 2) に変換

B_reshaped = B[np.newaxis, :, :]
print(f”B_reshaped shape: {B_reshaped.shape}”) # (1, 2, 2)

形状 (3, 1, 2) と (1, 2, 2) の差を計算

ブロードキャストが適用される

後方から比較:

次元3 (インデックス2): 2 と 2 -> OK

次元2 (インデックス1): 1 と 2 -> OK (1が2にストレッチ)

次元1 (インデックス0): 3 と 1 -> OK (1が3にストレッチ)

結果形状は (3, 2, 2)

diff = A_reshaped – B_reshaped
print(f”\nDifference shape: {diff.shape}”) # (3, 2, 2)

差の二乗を計算

sq_diff = diff**2 # shape (3, 2, 2)

最後の軸 (D次元の要素) に沿って合計する (axis=2)

sum_sq_diff = sq_diff.sum(axis=2) # shape (3, 2)
print(f”Sum of squared differences shape: {sum_sq_diff.shape}”) # (3, 2)

平方根を取って距離行列を得る

distance_matrix = np.sqrt(sum_sq_diff)
print(f”\nDistance Matrix:\n{distance_matrix}”)
“`

この例では、A[:, np.newaxis, :] によって A の形状 (N_A, D)(N_A, 1, D) に、B[np.newaxis, :, :] によって B の形状 (N_B, D)(1, N_B, D) に変換されています。これにより、これらの配列間の減算が形状 (N_A, N_B, D) にブロードキャストされ、A の各点と B の各点の間のすべての差ベクトルが計算されます。その後、この差ベクトル行列に対して軸2 (D) に沿って二乗和を計算し、最後に平方根を取ることで距離行列 (N_A, N_B) が得られます。これも newaxis による形状操作がブロードキャストを可能にする強力な例です。

配列の各要素から行/列の平均を引く

これも一般的なデータ前処理タスクです。例えば、形状 (M, N) の行列があり、各要素からその要素が属する行の平均値を引きたいとします。

“`python
import numpy as np

matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) # 形状 (3, 3)

各行の平均を計算

row_means = matrix.mean(axis=1) # axis=1 は列方向の平均 (行ごとに)
print(f”Row means: {row_means}, shape: {row_means.shape}”) # (3,)

matrix から row_means を引きたい

matrix (3, 3) と row_means (3,)

後方から比較: 3 と 3 -> OK

次の次元: 3 と 次元なし -> row_means は (1, 3) と見なされる。3 と 1 -> OK

-> row_means が行方向にブロードキャストされてしまう!これは期待する動作ではない

result = matrix – row_means # これはエラーにはならないが、期待する結果と異なる可能性がある

例えば matrix[0,:] – row_means (matrixの1行目 – row_means) は [1-1, 2-2, 3-3] = [0, 0, 0] となるが、

期待するのは matrix[0,:] – row_means[0] を matrix の1行全体に適用すること。

matrix[0, :] – row_means[0] = [1, 2, 3] – 2.0 = [-1.0, 0.0, 1.0]

この操作を行全体に適用したい

各行の平均 (形状 (3,)) を、形状 (3, 1) の列ベクトルに変換する

row_means_col = row_means[:, np.newaxis]
print(f”Row means as column vector:\n{row_means_col}, shape: {row_means_col.shape}”) # (3, 1)

matrix (3, 3) から row_means_col (3, 1) を引く

ブロードキャストが適用される

後方から比較: 3 と 1 -> OK (1がストレッチされて3になる)

次の次元: 3 と 3 -> OK

結果形状は (3, 3)

normalized_matrix = matrix – row_means_col
print(f”\nMatrix after subtracting row means:\n{normalized_matrix}”)
“`

この例では、matrix.mean(axis=1) で得られた形状 (3,) の行平均ベクトルを、row_means[:, np.newaxis] を使って形状 (3, 1) の列平均ベクトルに変換しています。これにより、形状 (3, 3)matrix と形状 (3, 1)row_means_col の間でブロードキャストが実行され、row_means_col の各要素(各行の平均値)が対応する matrix の行全体にブロードキャストされて減算が行われます。

同様に、各列の平均を引く場合は、列平均ベクトルを形状 (1, N) の行ベクトルに変換してから演算します。

“`python
import numpy as np

matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) # 形状 (3, 3)

各列の平均を計算

col_means = matrix.mean(axis=0) # axis=0 は行方向の平均 (列ごとに)
print(f”Column means: {col_means}, shape: {col_means.shape}”) # (3,)

col_means (形状 (3,)) は matrix (形状 (3, 3)) とそのままブロードキャスト可能

後方から比較: 3 と 3 -> OK

次の次元: 3 と 次元なし -> col_means は (1, 3) と見なされる。3 と 1 -> OK

-> この場合は newaxis は不要

normalized_matrix_col = matrix – col_means
print(f”\nMatrix after subtracting column means:\n{normalized_matrix_col}”)
``
この場合は、形状
(3, 3)(3,)はブロードキャスト互換性があるためnewaxisは不要です。これは、形状(3,)の配列が先頭にサイズ1の次元を持つ(1, 3)のように扱われ、形状(3, 3)(1, 3)` の後方からの比較で互換性があるためです。

これらの例からわかるように、newaxis はブロードキャストを精密に制御し、NumPy配列の形状を操作して目的の要素ごとの演算を効率的に行うための非常に強力なツールです。

newaxis と他の形状操作方法の比較

NumPyには newaxis 以外にも配列の形状を変更する方法がいくつかあります。それぞれの方法には得意なことや使い分けがあります。

arr.reshape()

reshape() メソッドは、配列の要素数を変えずに、指定した形状の新しい配列ビューを返します。元の形状と新しい形状の要素数は同じである必要があります。新しい形状はタプルで指定します。次元の追加、削除、サイズの変更など、様々な形状変更が可能です。

“`python
import numpy as np

arr = np.arange(6) # [0 1 2 3 4 5], shape (6,)

形状 (2, 3) にリシェイプ

arr_reshaped1 = arr.reshape(2, 3)
print(f”reshape(2, 3):\n{arr_reshaped1}, shape: {arr_reshaped1.shape}”) # (2, 3)

形状 (3, 2) にリシェイプ

arr_reshaped2 = arr.reshape((3, 2))
print(f”reshape((3, 2)):\n{arr_reshaped2}, shape: {arr_reshaped2.shape}”) # (3, 2)

形状 (1, 6) にリシェイプ (newaxis[:, np.newaxis] と同様の機能)

arr_reshaped3 = arr.reshape(1, 6)
print(f”reshape(1, 6):\n{arr_reshaped3}, shape: {arr_reshaped3.shape}”) # (1, 6)

形状 (6, 1) にリシェイプ (newaxis[:, np.newaxis] と同様の機能)

arr_reshaped4 = arr.reshape(6, 1)
print(f”reshape(6, 1):\n{arr_reshaped4}, shape: {arr_reshaped4.shape}”) # (6, 1)

形状 (2, 3, 1) にリシェイプ (newaxis[:, :, np.newaxis] と同様の機能)

arr_reshaped5 = arr.reshape(2, 3, 1)
print(f”reshape(2, 3, 1):\n{arr_reshaped5}, shape: {arr_reshaped5.shape}”) # (2, 3, 1)
“`

reshape は、要素数を保ったまま任意の形状に変換したい場合に強力です。一方 newaxis は、サイズ1の次元を特定の場所に挿入するという、より限定的で具体的な操作です。

newaxis はインデックス参照の一部として使われるため、配列の部分集合を取り出すスライスやインデックス指定と同時に次元を追加できます。reshape は通常、配列全体に対して適用されます。

また、reshape は新しい形状のタプルを明示的に指定する必要がありますが、newaxis はインデックス内の位置で新しい次元の場所を指定するため、より直感的に「ここにサイズ1の次元を追加したい」という意図を表現できます。特にブロードキャストのために「末尾に次元を追加して列ベクトルにしたい」といった場合は arr[:, np.newaxis] が簡潔です。

numpy.expand_dims()

numpy.expand_dims() 関数は、newaxis とほぼ同じ機能を提供しますが、関数形式で次元を追加します。np.expand_dims(arr, axis) のように使用し、axis 引数で新しい次元を挿入する位置を指定します。

“`python
import numpy as np

arr = np.array([10, 20, 30]) # 形状 (3,)

axis=0 に新しい次元を追加 (先頭)

arr_expanded1 = np.expand_dims(arr, axis=0)
print(f”np.expand_dims(arr, axis=0):\n{arr_expanded1}, shape: {arr_expanded1.shape}”) # (1, 3)

これは arr[np.newaxis, :] と同じ

axis=1 に新しい次元を追加 (末尾、元の軸の数 + 1番目の位置)

arr_expanded2 = np.expand_dims(arr, axis=1)
print(f”np.expand_dims(arr, axis=1):\n{arr_expanded2}, shape: {arr_expanded2.shape}”) # (3, 1)

これは arr[:, np.newaxis] と同じ

2D配列 (2, 3) の場合

arr2d = np.array([[1, 2, 3], [4, 5, 6]]) # 形状 (2, 3)
arr2d_expanded = np.expand_dims(arr2d, axis=1)
print(f”\nnp.expand_dims(arr2d, axis=1):\n{arr2d_expanded}, shape: {arr2d_expanded.shape}”) # (2, 1, 3)

これは arr2d[:, np.newaxis, :] と同じ

“`

expand_dims(arr, axis=k) は、インデックス k の位置に新しい軸を追加します。k=0 は先頭、k=arr.ndim (または -1) は末尾を意味します。負のインデックスも使用でき、例えば axis=-1 は常に末尾に次元を追加します。

expand_dimsnewaxis は機能的には非常に似ています。
* newaxis はインデックス参照の一部として使用され、コード内で形状変更の位置が視覚的にわかりやすい(例: arr[:, np.newaxis] は「列ベクトル」に見える)。
* expand_dims は関数呼び出しであり、特に次元を追加する軸のインデックスを変数で指定したい場合などに便利です(例: axis_to_add = 1; np.expand_dims(arr, axis=axis_to_add))。

どちらを使用するかは、コードの可読性や状況によって異なります。多くのNumPyユーザーは、単純な次元追加には newaxis を好んで使用します。

numpy.atleast_1d, atleast_2d, atleast_3d

これらの関数は、入力が少なくとも指定された次元数を持つことを保証します。必要であれば、先頭にサイズ1の次元を追加して次元数を増やします。しかし、新しい次元の挿入位置を細かく制御する用途には向いていません。

“`python
import numpy as np

arr0d = np.array(10) # 0次元スカラー, shape ()
arr1d = np.array([1, 2, 3]) # 1次元ベクトル, shape (3,)
arr2d = np.array([[1, 2], [3, 4]]) # 2次元配列, shape (2, 2)

print(f”np.atleast_1d(arr0d).shape: {np.atleast_1d(arr0d).shape}”) # (1,)
print(f”np.atleast_1d(arr1d).shape: {np.atleast_1d(arr1d).shape}”) # (3,)
print(f”np.atleast_1d(arr2d).shape: {np.atleast_1d(arr2d).shape}”) # (2, 2)

print(f”\nnp.atleast_2d(arr0d).shape: {np.atleast_2d(arr0d).shape}”) # (1, 1)
print(f”np.atleast_2d(arr1d).shape: {np.atleast_2d(arr1d).shape}”) # (1, 3)
print(f”np.atleast_2d(arr2d).shape: {np.atleast_2d(arr2d).shape}”) # (2, 2)

print(f”\nnp.atleast_3d(arr0d).shape: {np.atleast_3d(arr0d).shape}”) # (1, 1, 1)
print(f”np.atleast_3d(arr1d).shape: {np.atleast_3d(arr1d).shape}”) # (1, 3, 1)
print(f”np.atleast_3d(arr2d).shape: {np.atleast_3d(arr2d).shape}”) # (2, 2, 1)
“`

これらの関数は、入力が最低限必要な次元数を持っていることを保証したい場合に便利ですが、newaxis のように特定の軸位置にサイズ1の次元を挿入する柔軟性はありません。例えば atleast_2d は、1D配列を常に (1, N) の形状に変換します(行ベクトルにする)。newaxis なら (N, 1) (列ベクトル)にも簡単にできます。

どのような時に newaxis を使うべきか?

以上の比較を踏まえると、newaxis は特に以下のような状況で有効です。

  1. ブロードキャストを有効にする、または制御する: これが newaxis の最も一般的な用途です。形状が異なる配列間で要素ごとの演算を行いたいが、現在の形状ではブロードキャストできない場合に、一方または両方の配列にサイズ1の次元を挿入してブロードキャスト互換性を持たせます。特に、ベクトルを行ベクトル (1, N) や列ベクトル (N, 1) に変換して行列との演算を可能にする場合によく使われます。
  2. インデックス参照と同時に次元を追加する: 配列の一部を抽出する際に、取り出した部分配列の次元数を減らさずに保持したい場合や、逆に次元を追加したい場合に newaxis をインデックス参照と組み合わせて使用できます。
  3. コードの意図を明確にする: arr[:, np.newaxis] は、「この配列を列ベクトルとして扱う」という意図をコード上で明確に表現します。同様に arr[np.newaxis, :] は「行ベクトルとして扱う」ことを示します。
  4. 固定された位置にサイズ1の次元を追加する: 次元を追加したい位置がコード上で静的に決まっている場合、newaxis をインデックスに記述するのが直感的で簡潔です。追加する軸のインデックスが実行時に変数によって決まる場合は np.expand_dims の方が適していることがあります。

例えば、1次元配列 v を NNGPU (torch.nn.Conv1d など) のバッチ入力として使いたい場合、通常は形状 (batch_size, channels, sequence_length) が期待されます。もし v が単一シーケンスのデータ (sequence_length,) であり、チャンネル数が1の場合、v(1, 1, sequence_length) に変換する必要があります。これは v[np.newaxis, np.newaxis, :] または np.expand_dims(np.expand_dims(v, axis=0), axis=0) (あるいは np.expand_dims(v, axis=(0, 0))) で実現できます。newaxis の方が記述が短い傾向があります。

newaxis を使う上での注意点とヒント

  • newaxisNone のエイリアス: np.newaxis は実際には定数 None です。コードで arr[None, :] と書いても同じように動作しますが、np.newaxis を使う方がコードの意図(次元を追加していること)が明確になり推奨されます。
  • 常にカンマが必要: newaxis はインデックス参照のタプル内に置く必要があります。他のインデックスやスライスとの間をカンマで区切るのを忘れないでください (arr[np.newaxis, :] はOK, arr[np.newaxis :] はNG)。
  • 配列のビュー: newaxis によって返される配列は、通常、元の配列のビュー(view)です。つまり、新しい配列の要素を変更すると元の配列の要素も変更されます(データは共有されている)。明示的にデータのコピーを作成したい場合は、.copy() メソッドを使用します。
  • 次元数の増加: newaxis を一つ使うごとに、配列の次元数は1つ増加します。
  • 形状の確認: newaxis を使った後は、必ず .shape 属性を確認して、期待する形状になっているか確認する習慣をつけましょう。特にブロードキャストを行う前には、演算に関わる全ての配列の形状を確認し、ブロードキャスト互換性のルールを満たしているか頭の中でシミュレーションすることが重要です。

より実践的な例:画像データとニューラルネットワーク

画像データは通常、高さ(Height)、幅(Width)、チャンネル数(Channels)を持つ3次元配列として扱われます。例えばRGB画像なら形状は (H, W, 3) です。深層学習フレームワーク(TensorFlow, PyTorchなど)で画像を扱う場合、多くの場合、入力データはミニバッチ形式で、かつ特定の次元順序(チャンネルファーストまたはチャンネルラスト)が期待されます。例えば、PyTorchのConv2D層への入力は (batch_size, channels, height, width) の形状を期待することが多いです。

もし (H, W, 3) の単一のRGB画像をPyTorchのConv2D層に入力したい場合、以下の変換が必要になります。

  1. バッチ次元の追加: 単一画像なのでバッチサイズは1。形状を (1, H, W, 3) に。
  2. チャンネル次元の位置変更: (1, H, W, 3) から (1, 3, H, W) に。

ここで newaxis がバッチ次元の追加に役立ちます。

“`python
import numpy as np

import torch # PyTorchを使う場合

例として、擬似的な画像データを作成 (高さ100, 幅200, チャンネル3)

image_hwc = np.random.rand(100, 200, 3) # 形状 (100, 200, 3)
print(f”Original image shape (HWC): {image_hwc.shape}”)

フレームワークが期待する形状 (Batch, Channels, Height, Width) に変換したい

1. バッチ次元を追加 (先頭に追加)

image_b_hwc = image_hwc[np.newaxis, :, :, :]

またはより簡潔に

image_b_hwc = image_hwc[np.newaxis, …] # … は残りの全ての次元を意味する
print(f”After adding batch dimension (B, H, W, C): {image_b_hwc.shape}”) # (1, 100, 200, 3)

2. チャンネル次元を移動

NumPyの transpose や moveaxis を使う

transpose(0, 3, 1, 2) または moveaxis(3, 1)

image_bchw = np.moveaxis(image_b_hwc, 3, 1)
print(f”After moving channel dimension (B, C, H, W): {image_bchw.shape}”) # (1, 3, 100, 200)

この image_bchw (NumPy配列) を PyTorch Tensor に変換してNNに入力する

pytorch_tensor = torch.from_numpy(image_bchw)

print(f”PyTorch tensor shape: {pytorch_tensor.shape}”) # torch.Size([1, 3, 100, 200])

“`

この例では、image_hwc[np.newaxis, ...] の部分で newaxis を使用して先頭にバッチ次元(サイズ1)を追加しています。このように、newaxis は他のライブラリが要求する特定の形状にデータを整形する際にも非常に役立ちます。

まとめ

この記事では、NumPyの numpy.newaxis について詳細に解説しました。

  • newaxis はインデックス参照 [] 内で使用され、その位置にサイズ1の新しい次元を挿入します。
  • 配列のデータや要素数は変化しませんが、配列の形状次元数が変わります。
  • newaxis の最も重要な用途は、異なる形状の配列間での演算を可能にするブロードキャストを制御することです。サイズ1の次元を追加することで、ブロードキャスト互換性のルールを満たすように配列の形状を調整できます。
  • 1次元配列を明示的に行ベクトル (1, N) や列ベクトル (N, 1) に変換する場合によく使用されます。
  • newaxisNone のエイリアスですが、np.newaxis と記述する方が意図が明確です。
  • reshape() は任意の形状に変換する汎用的なメソッド、expand_dims() は関数形式で軸インデックスを指定して次元を追加する方法であり、それぞれ newaxis とは異なる状況で使い分けられます。atleast_xD は最低限の次元数を保証する場合に使用されます。

NumPyで効率的かつ柔軟な配列操作を行うためには、ブロードキャストの仕組みを理解し、それを newaxis のようなツールを使って制御する能力が不可欠です。最初は少し難しく感じるかもしれませんが、様々な形状の配列で newaxis を試し、結果の shape を確認しながら練習することで、その強力さと便利さが理解できるようになります。

NumPyのドキュメントや他の資料を参照しながら、ぜひ実際に手を動かして newaxis をマスターしてください。それはあなたの数値計算やデータ処理のスキルを大きく向上させるでしょう。


以上で、約5000語を目指した numpy.newaxis に関する詳細な解説記事となります。NumPyの基本から入り、newaxis の機能、主な用途(ブロードキャスト)、他の手法との比較、実践的な例、注意点まで幅広くカバーしました。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール