NumPyインデクシング&スライシング:テンソルを自在に切り取る方法

1. なぜインデクシング/スライシングが重要なのか?



ディープラーニングを行うと、テンソルを頻繁に操作します。

  • バッチから先頭数サンプルだけを抜き出す
  • 画像から特定のチャネル(R/G/B)だけを選択する
  • シーケンスデータから一部のタイムステップだけを切り取る
  • ラベルから特定のクラスだけを抽出する

これらすべての作業は結局「インデクシング(indexing) & スライシング(slicing)」です。

そしてPyTorchのTensorのインデクシング/スライシング構文はNumPyとほぼ同じため、NumPyでしっかり学んでおくとディープラーニングコードの作成が格段に楽になります。


2. 基本インデクシング:1次元から把握

2.1 1次元配列インデクシング

import numpy as np

x = np.array([10, 20, 30, 40, 50])

print(x[0])  # 10
print(x[1])  # 20
print(x[4])  # 50
  • インデックスは0から始まる
  • x[i]はi番目の要素

2.2 負数インデクシング

後ろから数えたいときは負数インデックスを使います。

print(x[-1])  # 50 (最後)
print(x[-2])  # 40

PyTorchでも同様に動作します。


3. スライシングの基本:start:stop:step



スライシングはx[start:stop:step]の形で使用します。

x = np.array([10, 20, 30, 40, 50])

print(x[1:4])    # [20 30 40], 1以上4未満
print(x[:3])     # [10 20 30], 最初から3未満
print(x[2:])     # [30 40 50], 2から終わりまで
print(x[:])      # 全体コピーのような感覚

stepを入れると間隔を置くことができます。

print(x[0:5:2])  # [10 30 50], 0~4まで2つおき
print(x[::2])    # [10 30 50], 上と同じ

ディープラーニングでは、例えばタイムステップを2個飛ばしたり、特定の間隔でサンプルを選択したりするときに便利です。


4. 2次元以上の配列インデクシング:行と列を扱う

2次元配列からが実質的に「行列/バッチ」になるので、ディープラーニング的な感覚が出てきます。

import numpy as np

X = np.array([[1, 2, 3],
              [4, 5, 6],
              [7, 8, 9]])  # shape: (3, 3)

4.1 行/列単一インデクシング

print(X[0])      # 最初の行: [1 2 3]
print(X[1])      # 2番目の行: [4 5 6]

print(X[0, 0])   # 1行1列: 1
print(X[1, 2])   # 2行3列: 6
  • X[i] → i番目の行(1D配列)
  • X[i, j] → i番目の行、j番目の列の値(スカラー)

4.2 行スライシング

print(X[0:2])    # 0~1行
# [[1 2 3]
#  [4 5 6]]

print(X[1:])     # 1行から終わりまで
# [[4 5 6]
#  [7 8 9]]

これは通常バッチで前/後の一部サンプルだけを切るときにPyTorchでそのまま使うパターンです。

4.3 列スライシング

print(X[:, 0])   # 全行, 0番目の列 → [1 4 7]
print(X[:, 1])   # 全行, 1番目の列 → [2 5 8]
  • : は「その次元全体」を意味
  • X[:, 0]は「行は全部、列は0番目」という意味

ディープラーニングで:

  • (batch_size, feature_dim)配列で 特定のfeatureだけを抜きたいとき:X[:, k]

5. 3次元以上:バッチ × チャネル × 高さ × 幅

画像データを例に見てみましょう。

# (batch, height, width) と仮定
images = np.random.randn(32, 28, 28)  # 32個, 28x28画像

5.1 特定サンプル1つだけ抜く

img0 = images[0]        # 最初の画像, shape: (28, 28)
img_last = images[-1]   # 最後の画像

5.2 バッチの一部だけ使う

first_8 = images[:8]    # 最初の8個だけ, shape: (8, 28, 28)

5.3 画像の一部領域だけ切る(crop)

# 中央の20x20領域だけ
crop = images[:, 4:24, 4:24]  # shape: (32, 20, 20)

PyTorchでも:

# images_torch: (32, 1, 28, 28) などのテンソルと仮定
center_crop = images_torch[:, :, 4:24, 4:24]

インデクシング/スライシングの概念がほぼそのままです。


6. スライシングは通常「ビュー(view)」です

重要なポイント1つ:

スライシング結果は通常元の配列の「ビュー(view)」です。 つまり、新しくデータをコピーしたのではなく元を見ている窓という意味です。

x = np.array([10, 20, 30, 40, 50])
y = x[1:4]   # ビュー

print(y)     # [20 30 40]
y[0] = 999

print(y)     # [999  30  40]
print(x)     # [ 10 999  30  40  50]  ← 元も変わる!

この特徴は:

  • メモリを節約し高速だが
  • うっかり元を変えてしまうリスクがある

完全に別の配列にしたいときはcopy()を使います。

x = np.array([10, 20, 30, 40, 50])
y = x[1:4].copy()

y[0] = 999
print(x)  # [10 20 30 40 50], 元保持

PyTorchでも似た概念があるので、 「ビュー vs コピー」の感覚を掴んでおくとデバッグがずっと楽になります。


7. ブールインデクシング:条件で要素を抜く

ブールインデクシングは「条件を満たす要素だけを選択」するときに使います。

import numpy as np

x = np.array([1, -2, 3, 0, -5, 6])

mask = x > 0
print(mask)      # [ True False  True False False  True]

pos = x[mask]
print(pos)       # [1 3 6]
  • x > 0True/Falseで構成された配列
  • x[mask]Trueの位置だけ抜く

組み合わせ例:

X = np.array([[1, 2, 3],
              [4, 5, 6],
              [-1, -2, -3]])

pos = X[X > 0]
print(pos)  # [1 2 3 4 5 6]

ディープラーニングでよく使われるパターン:

  • 特定条件を満たすサンプルだけ抽出(例:ラベルが特定値のものだけ)
  • 損失計算時にマスクをかけて一部値だけ平均を取るなど

PyTorchでも:

import torch

x = torch.tensor([1, -2, 3, 0, -5, 6])
mask = x > 0
pos = x[mask]

ほぼ同じように使えます。


8. 整数配列/リストインデクシング(ファンシーインデクシング)

整数インデックスの配列/リストで一度に欲しい位置を抜くこともできます。

x = np.array([10, 20, 30, 40, 50])

idx = [0, 2, 4]
print(x[idx])  # [10 30 50]

2次元でも可能です。

X = np.array([[1, 2],
              [3, 4],
              [5, 6]])  # shape: (3, 2)

rows = [0, 2]
print(X[rows])  
# [[1 2]
#  [5 6]]

ディープラーニングで例:

  • ランダムに混ぜたインデックス配列でバッチサンプルを抜く
  • 特定位置のラベル/予測だけ集めて統計を取る

PyTorchでも同じスタイルで使えます。


9. よく使うインデクシングパターンまとめ

ディープラーニング視点でよく登場するパターンをまとめると:

import numpy as np

# (batch, feature)
X = np.random.randn(32, 10)

# 1) 最初の8個サンプルだけ
X_head = X[:8]              # (8, 10)

# 2) 特定feature(例:3番目の列)だけ
f3 = X[:, 3]                # (32,)

# 3) 偶数インデックスサンプルだけ
X_even = X[::2]             # (16, 10)

# 4) ラベルが1のサンプルだけ選択
labels = np.random.randint(0, 3, size=(32,))
mask = labels == 1
X_cls1 = X[mask]            # ラベル1に該当するサンプルだけ

# 5) ランダムシャッフル後、最初24個をtrain、残り8個をval
indices = np.random.permutation(len(X))
train_idx = indices[:24]
val_idx = indices[24:]

X_train = X[train_idx]
X_val = X[val_idx]

上記パターンはPyTorchテンソルでもほぼそのまま使えます。 結局NumPyインデクシングに慣れること = テンソルデータを自由に「切り分け、混ぜ、選択」できる力を養うことです。


10. まとめ

今回見た内容をまとめると:

  • インデクシング:x[i], x[i, j], 負数インデクス
  • スライシング:start:stop:step, :, 多次元インデクシング(X[:, 0], X[:8], X[:, 4:8])
  • スライシングは通常ビュー(view)なので元が変わる可能性 → 必要ならcopy()
  • ブールインデクシング:条件でフィルタリング(x[x > 0], X[labels == 1])
  • 整数配列インデクシング:欲しいインデックスをまとめて一度に選択(x[[0,2,4]])

これを身につければ、PyTorchテンソルで「バッチ切り、チャネル選択、マスク適用、サンプル混ぜ」などの作業がずっと楽になります。

image