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:
"""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:
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:
- Baris 5-6 (seed dan tensor):
torch.manual_seed(42)mengunci generator acak sehinggatorch.randintmenghasilkan matriks yang sama setiap eksekusi — nilai keluaran contoh ini bisa dihitung ulang. - 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, danX[:, 2]memakai:untuk “semua baris” lalu indeks2untuk kolom ketiga. - 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. - 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. - Baris 19-23 (boolean mask):
X > 5menghasilkan tensorboolber-shape sama denganX; memberikannya sebagai indeks menyaring semua elemen bertandaTruemenjadi vektor[7, 6, 6, 8]— pola standar untuk menyaring data. - Baris 25-30 (
gatherdanindex_select):gather(1, idx)mengambil satu elemen per baris pada kolom yang berbeda-beda sesuaiidx— 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 #
| Kebutuhan | NumPy | PyTorch | Hasil |
|---|---|---|---|
| satu elemen | X[1, 2] | X[1, 2] | skalar |
| satu baris/kolom | X[1], X[:, 2] | sama | rank turun satu |
| rentang | X[0:2, 1:3] | sama | sumbu dipertahankan |
| boolean mask | X[X > 5] | sama | vektor nilai lolos |
| indeks list | X[[0, 2], [1, 3]] | sama | salinan |
| ambil per baris | np.take_along_axis | X.gather(dim, idx) | bentuk mengikuti idx |
| pilih baris | X[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:
"""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:
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:
- Baris 7-9 (mutasi view):
X[0]adalah view; menulisbaris[1] = 99.0menembus memori induk sehinggaX[0, 1]berubah menjadi99.0meskipun kita tidak pernah menyentuhXsecara langsung. - Baris 11-13 (
clone()): salinan menempati memori sendiri, sehingga penulisan-5.0tidak berdampak apa pun padaX. - Baris 15-17 (indeks list): berbeda dari slicing, advanced indexing selalu menyalin — mutasi pada
blokjuga tidak menyentuhX, terlihat dari nilaiX[1, 0]yang tetap1.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.