NumPyの使い方
NumPy は『数の入った箱を、まとめて計算するための道具』です。Python のリストでも数は持てますが、配列の形を保ったまま速く計算したい場面では NumPy が本領を発揮します。
このノートでは、観測値を並べた 1 次元配列から始めて、行列、条件抽出、集約、行列積まで進みます。大事なのは式の見た目より、配列がいまどんな形をしているかを見失わないことです。
リストの延長ではなく、計算のための器として見る
NumPy を学び始めるときは、関数名を増やすよりも shape と dtype を読む習慣を先に作る方が早道です。配列の長さや行列の縦横が分かると、あとでブロードキャストや行列積が出てきても急に魔法っぽく見えなくなります。
最初の数セルでは、同じデータでも 1 次元なのか 2 次元なのかで見え方が変わることを確かめます。
そのあとで、配列どうしの足し算、形の違う配列の足し算、条件に合う要素の取り出し、行や列ごとの集計へ進みます。『何ができるか』より『いまどの軸を相手にしているか』を意識しながら読むのがコツです。
配列の形を見るところから始める
ndarray は NumPy の基本の配列型です。dtype は中身の型、shape は並び方を表します。
(4,)は 4 個並んだ 1 次元配列(3, 2)は 3 行 2 列の 2 次元配列
この書き方に慣れておくと、後で出てくる reshape, axis, broadcast がかなり読みやすくなります。
ひとつの配列を、切り口を変えて見る
このノートでは、毎回まず出力を見てから『いま配列は何行何列か』『どの要素が残ったのか』を言葉にしてみると理解が定着しやすくなります。NumPy は操作自体は短く書けますが、配列の形を頭の中で追えるかどうかが理解の分かれ目です。
shape を見失わないために
reshape は要素数を変えずに並べ替える操作です。np.arange(1, 13).reshape(3, 4) は『1 から 12 までの 12 個の値を、3 行 4 列に並べ替えた』だけです。
また、乱数生成では loc, scale, size がそれぞれ中心、ばらつき、生成する形を表します。seed を固定するのは、毎回同じ乱数列を再現して比較しやすくするためです。
import numpy as np
最初の一歩は、とにかく配列を作って shape と dtype を眺めることです。ここが曖昧なまま先へ進むと、配列演算はすぐに暗号っぽく見えてしまいます。
a = np.array([1, 2, 3, 4], dtype=np.float64)
b = np.array([[10, 20], [30, 40], [50, 60]])
print("a:", a)
print("a.shape:", a.shape, "a.dtype:", a.dtype)
print("b:\n", b)
print("b.shape:", b.shape, "b.dtype:", b.dtype)
NumPy の強さは、要素ごとの計算を配列全体にそのまま書けることです。1 個ずつ for で回しても同じ結果は作れますが、NumPy では式の形を保ったまままとめて計算できます。
x = np.array([1.0, 2.0, 3.0, 4.0])
y = np.array([2.0, 4.0, 6.0, 8.0])
print("x + y:", x + y)
print("x * y:", x * y)
print("x / y:", x / y)
print("x >= 2.5:", x >= 2.5)
ブロードキャストは、形の違う配列どうしを計算できるように、NumPy が足りない次元を補って解釈する仕組みです。
ここでは offset が 1 行分のずれを表していて、それを各行に同じように足しています。『本当に複製している』というより、『そう見なして計算している』と考えると整理しやすくなります。
matrix = np.array([[1., 2., 3.], [4., 5., 6.], [7., 8., 9.]])
offset = np.array([10., 20., 30.])
print("matrix.shape:", matrix.shape)
print("offset.shape:", offset.shape)
print("matrix + offset:\n", matrix + offset)
インデキシングは、必要な部分だけを抜き出すための操作です。
スライスは位置で切り出し、ブールマスクは条件で切り出します。前処理では後者が特に重要で、『80 点以上だけ残す』『欠損していない行だけ使う』といった処理の原型になっています。
arr = np.arange(1, 13).reshape(3, 4)
print("arr:\n", arr)
print("2行目:", arr[1])
print("1〜2行, 2〜4列:\n", arr[0:2, 1:4])
mask = arr % 2 == 0
print("mask:\n", mask)
print("偶数だけ:", arr[mask])
sum, mean, max のような集約では、axis が『どの向きにまとめるか』を決めています。
axis=0: 行をまたいで、列ごとにまとめるaxis=1: 列をまたいで、行ごとにまとめる
ここは言葉だけで覚えるより、出力の shape がどう変わるかを見る方がずっと分かりやすいです。
scores = np.array([[72, 88, 65], [91, 77, 84], [68, 79, 95]])
print("全体平均:", scores.mean())
print("科目ごとの平均(axis=0):", scores.mean(axis=0))
print("学生ごとの合計(axis=1):", scores.sum(axis=1))
行列積は、線形代数の記号がそのままコードへ近づいてくる場面です。@ を見ると難しく感じやすいですが、やっていることは『左の行と右の列を対応づけて掛け合わせ、足し合わせる』というあの操作です。
W = np.array([[0.2, -0.3], [0.4, 0.1], [0.5, -0.2]])
x = np.array([1.0, 2.0, -1.0])
b = np.array([0.1, -0.2])
logits = x @ W + b
print("logits:", logits)
乱数は便利ですが、毎回違う値が出ると学習や検証で比較しづらくなります。 のように種を固定するのは、『いまは偶然の違いではなく、コードの違いだけを見たい』という意思表示です。
rng = np.random.default_rng(seed=42)
samples = rng.normal(loc=0.0, scale=1.0, size=(5, 3))
print(samples)
print("列平均:", samples.mean(axis=0))
最後は標準化です。平均を引いて、標準偏差で割ると、配列の中心を 0 にそろえつつ、ばらつきの大きさも比較しやすくなります。
機械学習では、単位の違う特徴量を並べる前処理としてよく出てきます。ここでは式を覚えるより、『大きさの基準を合わせるための変換』として理解できれば十分です。
features = np.array([
[160, 55],
[170, 68],
[175, 70],
[180, 80],
], dtype=np.float64)
mu = features.mean(axis=0)
sigma = features.std(axis=0)
scaled = (features - mu) / sigma
print("mu:", mu)
print("sigma:", sigma)
print("scaled:\n", scaled)
print("scaled mean:", scaled.mean(axis=0))
NumPy を読む力は、派手な関数を知っていることよりも、配列の形を崩さずに追えるかどうかで決まります。
分からなくなったら、毎回 shape を出す、axis を言葉にする、結果の配列が 1 次元になったのか 2 次元のままなのかを確認する。この 3 つに戻るのがいちばん堅実です。