Mengubah shape hanyalah setengah keterampilan; setengah lainnya adalah mengambil bagian yang tepat dari sebuah tensor. Kita perlu memeriksa satu prediksi, menyalin sekelompok channel, atau menyaring contoh yang nilainya melewati ambang batas. Kabar baiknya, PyTorch meniru sintaks indexing NumPy hampir tanpa perubahan, sehingga muscle memory dari section Python dasar langsung berlaku. Satu perilaku yang wajib dipahami sejak awal: sebagian cara indexing menghasilkan tampilan yang berbagi memori dengan tensor asal, sehingga mutasi kecil bisa mengubah data induk secara senyap.

Akses Dasar dan Slicing #

Aturan mainnya ringkas: setiap indeks yang diberikan menghapus satu sumbu dari hasil, sedangkan rentang (start:stop) mempertahankan sumbu tersebut. Indeks negatif berhitung dari belakang. Berkas indeks_dan_slice.py mendemonstrasikan semuanya pada satu matriks 3×4 berisi angka acak yang dikunci dengan seed:

python
"""Mengakses elemen tensor: indexing dasar, slicing, mask, gather."""

import torch

torch.manual_seed(42)
X = torch.randint(0, 10, (3, 4))
print("X:")
print(X)

print()
print("X[1, 2]   (satu elemen) :", X[1, 2].item())
print("X[1]      (satu baris)  :", X[1].tolist())
print("X[-1]     (baris akhir) :", X[-1].tolist())
print("X[:, 2]   (satu kolom)  :", X[:, 2].tolist())
print("X[0:2, 1:3] (sub-blok)  :")
print(X[0:2, 1:3])
print("X[[0, 2], [1, 3]]       :", X[[0, 2], [1, 3]].tolist())

mask = X > 5
print()
print("mask X > 5:")
print(mask)
print("X[mask]                 :", X[mask].tolist())

idx = torch.tensor([[3], [0], [1]])
print()
print("X.gather(1, idx):")
print(X.gather(1, idx))
print("X.index_select(0, torch.tensor([2, 0])):")
print(X.index_select(0, torch.tensor([2, 0])))

Output yang Dihasilkan:

text
X:
tensor([[2, 7, 6, 4],
        [6, 5, 0, 4],
        [0, 3, 8, 4]])

X[1, 2]   (satu elemen) : 0
X[1]      (satu baris)  : [6, 5, 0, 4]
X[-1]     (baris akhir) : [0, 3, 8, 4]
X[:, 2]   (satu kolom)  : [6, 0, 8]
X[0:2, 1:3] (sub-blok)  :
tensor([[7, 6],
        [5, 0]])
X[[0, 2], [1, 3]]       : [7, 4]

mask X > 5:
tensor([[False,  True,  True, False],
        [ True, False, False, False],
        [False, False,  True, False]])
X[mask]                 : [7, 6, 6, 8]

X.gather(1, idx):
tensor([[4],
        [6],
        [3]])
X.index_select(0, torch.tensor([2, 0])):
tensor([[0, 3, 8, 4],
        [2, 7, 6, 4]])

Mari kita telaah kode di atas baris per baris:

  1. Baris 5-6 (seed dan tensor): torch.manual_seed(42) mengunci generator acak sehingga torch.randint menghasilkan matriks yang sama setiap eksekusi — nilai keluaran contoh ini bisa dihitung ulang.
  2. Baris 11-14 (akses tunggal): X[1, 2] mengambil satu angka (dua sumbu hilang), X[1] menyisakan satu vektor (satu sumbu hilang), X[-1] membaca baris terakhir, dan X[:, 2] memakai : untuk “semua baris” lalu indeks 2 untuk kolom ketiga.
  3. Baris 15-16 (X[0:2, 1:3]): rentang di kedua sumbu mempertahankan dua sumbu hasil — sub-blok 2 baris × 2 kolom; batas atas rentang tidak ikut terambil.
  4. Baris 17 (X[[0, 2], [1, 3]]): dua list indeks dipasangkan sejajar, menghasilkan pasangan (0,1) dan (2,3) — mekanisme ini disebut advanced indexing dan hasilnya selalu berupa salinan, bukan tampilan.
  5. Baris 19-23 (boolean mask): X > 5 menghasilkan tensor bool ber-shape sama dengan X; memberikannya sebagai indeks menyaring semua elemen bertanda True menjadi vektor [7, 6, 6, 8] — pola standar untuk menyaring data.
  6. Baris 25-30 (gather dan index_select): gather(1, idx) mengambil satu elemen per baris pada kolom yang berbeda-beda sesuai idx — baris 0 ambil kolom 3, baris 1 ambil kolom 0, baris 2 ambil kolom 1. index_select(0, ...) memungut baris dalam urutan bebas dan selalu menyimpan sumbunya.

Sintaks NumPy versus torch #

KebutuhanNumPyPyTorchHasil
satu elemenX[1, 2]X[1, 2]skalar
satu baris/kolomX[1], X[:, 2]samarank turun satu
rentangX[0:2, 1:3]samasumbu dipertahankan
boolean maskX[X > 5]samavektor nilai lolos
indeks listX[[0, 2], [1, 3]]samasalinan
ambil per barisnp.take_along_axisX.gather(dim, idx)bentuk mengikuti idx
pilih barisX[rows]X.index_select(0, rows)sumbu tetap

Jebakan Memori: View versus Salinan #

Slicing rentang dan akses : menghasilkan view — tensor baru yang menunjuk blok memori yang sama dengan induknya. Menulis ke view sama dengan menulis ke tensor asal. Berkas mutasi_view.py membuktikan tiga jalur yang berbeda:

python
"""Membedakan hasil indexing yang berbagi memori dan yang menyalin."""

import torch

X = torch.ones(3, 3)

baris = X[0]
baris[1] = 99.0
print("ubah hasil slicing -> X[0, 1] =", X[0, 1].item())

salinan = X[1].clone()
salinan[0] = -5.0
print("ubah clone()       -> X[1, 0] =", X[1, 0].item())

blok = X[[1, 2]]
blok[0, 0] = -7.0
print("ubah indeks list   -> X[1, 0] =", X[1, 0].item())

print()
print("X akhir:")
print(X)

Output yang Dihasilkan:

text
ubah hasil slicing -> X[0, 1] = 99.0
ubah clone()       -> X[1, 0] = 1.0
ubah indeks list   -> X[1, 0] = 1.0

X akhir:
tensor([[ 1., 99.,  1.],
        [ 1.,  1.,  1.],
        [ 1.,  1.,  1.]])

Mari kita telaah kode di atas baris per baris:

  1. Baris 7-9 (mutasi view): X[0] adalah view; menulis baris[1] = 99.0 menembus memori induk sehingga X[0, 1] berubah menjadi 99.0 meskipun kita tidak pernah menyentuh X secara langsung.
  2. Baris 11-13 (clone()): salinan menempati memori sendiri, sehingga penulisan -5.0 tidak berdampak apa pun pada X.
  3. Baris 15-17 (indeks list): berbeda dari slicing, advanced indexing selalu menyalin — mutasi pada blok juga tidak menyentuh X, terlihat dari nilai X[1, 0] yang tetap 1.0.

Panduan lengkap seluruh pola indexing PyTorch tersedia di docs.pytorch.org/docs/stable/tensor_indexing.html . Kini kita bisa mengisi dan mengambil isi tensor dengan leluasa; Chapter berikutnya membahas operasi yang sesungguhnya dipakai model — elementwise, matmul, reduksi, dan penggabungan tensor.