Pekerjaan nyata jarang berhenti pada satu perkalian matriks. Sering kali kita menghadapi tumpukan transformasi kecil — satu matriks per cabang, per gambar, atau per langkah waktu — dan seluruhnya harus dikalikan. Menulis loop for bisa saja, tetapi NumPy menyediakan jalan yang lebih ringkas: np.matmul (dan operator @) memperlakukan array berdimensi lebih dari dua sebagai batch, yaitu tumpukan matriks yang dikalikan sekaligus, dengan broadcasting pada sumbu batch.

Tumpukan Matriks sebagai Array Tiga Dimensi #

Aturan @ untuk input N-D (N > 2): dua sumbu terakhir adalah matriks, sedangkan seluruh sumbu di depannya adalah batch. Anggota batch yang seposisi dikalikan satu sama lain:

python
import numpy as np

batch = np.array([[[1, 2],
                   [3, 4]],
                  [[5, 6],
                   [7, 8]]])
print(batch.shape)
hasil = batch @ batch
print(hasil.shape)
print(hasil)

Output yang Dihasilkan:

text
(2, 2, 2)
(2, 2, 2)
[[[  7  10]
  [ 15  22]]

 [[ 67  78]
  [ 91 106]]]

Bedah baris demi baris:

  1. Baris 3-6 (batch = np.array(...)): tumpukan dua matriks 2x2 — batch[0] adalah matriks A dan batch[1] adalah matriks B dari contoh Chapter Matrix Multiplication.
  2. Baris 7 (batch.shape): (2, 2, 2) dibaca “dua matriks, masing-masing berordo 2x2”.
  3. Baris 8-9 (hasil = batch @ batch): @ menghitung batch[0] @ batch[0] dan batch[1] @ batch[1], lalu menumpuk keduanya; hasilnya batch baru berbentuk (2, 2, 2).

Nilainya cocok dengan perkalian manual per matriks: hasil[0] adalah [[7, 10], [15, 22]] (yaitu A @ A) dan hasil[1] adalah [[67, 78], [91, 106]] (yaitu B @ B).

Matriks Dua Dimensi di Salah Satu Sisi #

Bila salah satu operand hanya 2-D, matmul memperlakukannya sebagai satu matriks yang di-broadcast ke seluruh batch — analog dengan penambahan sumbu baru pada Chapter Pengenalan Broadcasting. Perhatikan bentuk hasilnya dengan teliti:

python
import numpy as np

batch = np.array([[[1, 2],
                   [3, 4]],
                  [[5, 6],
                   [7, 8]]])
M = np.array([[1, 0],
              [0, 2]])
print((batch @ M).shape)
print(batch @ M)

Output yang Dihasilkan:

text
(2, 2, 2)
[[[ 1  4]
  [ 3  8]]

 [[ 5 12]
  [ 7 16]]]

Bedah baris demi baris:

  1. Baris 9 ((batch @ M).shape): hasilnya (2, 2, 2), bukan (2, 2) — operand 2-D tidak mengurangi jumlah anggota batch.
  2. Baris 10 (print(batch @ M)): M dikalikan ke setiap anggota: batch[0] @ M = [[1, 4], [3, 8]] dan batch[1] @ M = [[5, 12], [7, 16]].

Aturan yang sama berlaku bila matriks 2-D berada di sisi kiri (M @ batch tetap (2, 2, 2)), dan bila operand kanan berupa vektor 1-D: batch @ v berbentuk (2, 2) — perkalian matriks-vektor untuk setiap anggota batch (batched matvec).

Loop vs Batch: Membuktikan Kesamaan Hasil #

Cara paling meyakinkan untuk memahami semantik batch adalah membandingkannya dengan loop eksplisit:

python
import numpy as np

batch = np.array([[[1, 2],
                   [3, 4]],
                  [[5, 6],
                   [7, 8]]])
M = np.array([[1, 0],
              [0, 2]])

hasil_loop = np.stack([batch[i] @ M for i in range(batch.shape[0])])
hasil_batch = batch @ M
print(np.array_equal(hasil_loop, hasil_batch))

Output yang Dihasilkan:

text
True

Bedah baris demi baris:

  1. Baris 10 (hasil_loop = np.stack(...)): list comprehension mengalikan tiap anggota batch dengan M satu per satu, lalu np.stack menumpuk hasil-hasil 2-D itu menjadi batch (2, 2, 2) kembali.
  2. Baris 11 (hasil_batch = batch @ M): versi satu baris tanpa loop Python.
  3. Baris 12 (np.array_equal(...)): True — kedua jalur menghasilkan nilai yang sama persis.

Studi Kasus: Banyak Transformasi Kecil #

Rotasi 2-D adalah contoh klasik: setiap sudut memiliki satu matriks rotasi, dan kita ingin memutar satu titik dengan beberapa sudut sekaligus. Kita bangun tumpukan (3, 2, 2) matriks rotasi, lalu kalikan dengan satu vektor kolom titik (2, 1):

python
import numpy as np

sudut = np.array([0.0, 30.0, 60.0])
rad = np.deg2rad(sudut)
c = np.cos(rad)
s = np.sin(rad)

rot = np.empty((3, 2, 2))
rot[:, 0, 0] = c
rot[:, 0, 1] = -s
rot[:, 1, 0] = s
rot[:, 1, 1] = c

titik = np.array([[2.0], [0.0]])
hasil = rot @ titik
print(hasil.shape)
print(hasil)

Output yang Dihasilkan:

text
(3, 2, 1)
[[[2.        ]
  [0.        ]]

 [[1.73205081]
  [1.        ]]

 [[1.        ]
  [1.73205081]]]

Bedah baris demi baris:

  1. Baris 3-6: sudut 0°, 30°, dan 60° dikonversi ke radian, lalu np.cos dan np.sin dihitung per sudut — c dan s masing-masing berbentuk (3,).
  2. Baris 8-12 (rot): np.empty((3, 2, 2)) menyiapkan tumpukan kosong; slicing rot[:, 0, 0] = c dan seterusnya mengisi pola matriks rotasi [[cos, -sin], [sin, cos]] untuk ketiga sudut sekaligus.
  3. Baris 14 (titik): titik (2, 0) ditulis sebagai vektor kolom berbentuk (2, 1) — konversi dari array 1-D dibahas pada Chapter Transpose Matriks.
  4. Baris 15-19 (rot @ titik): operand (2, 1) di-broadcast ke seluruh batch; hasil (3, 2, 1) berisi satu titik hasil per sudut: (2, 0) untuk 0°, (1.73, 1) untuk 30°, dan (1, 1.73) untuk 60°.

Tabel berikut merangkum kombinasi bentuk yang sudah kita uji:

Operand kiriOperand kananBentuk hasilArti
(2, 2, 2)(2, 2, 2)(2, 2, 2)matriks ke-i dikalikan matriks ke-i
(2, 2, 2)(2, 2)(2, 2, 2)satu matriks untuk seluruh batch
(2, 2, 2)(2,)(2, 2)satu vektor per anggota batch
(3, 2, 2)(2, 1)(3, 2, 1)batch transformasi terhadap satu titik