Tensor di dalam model jarang datang dengan bentuk yang sama: bias satu layer berbentuk (C,) harus bertemu aktivasi (N, C), dan statistik per channel berbentuk (1, C, 1, 1) harus dikurangkan dari batch (N, C, H, W). Broadcasting adalah aturan yang mengizinkan operasi tersebut tanpa satu pun perulangan — dan PyTorch mewarisinya apa adanya dari NumPy, sehingga aturan yang kita pelajari pada section Python dasar tetap berlaku. Chapter ini menerapkan aturan tersebut pada kasus paling khas deep learning, membaca pesan galatnya sampai ke makna setiap angka, lalu membedakan expand yang hemat memori dari repeat yang menyalin.

Aturan yang Sama, Wadah yang Lebih Tinggi #

Mekanismenya tetap: kedua shape dibandingkan dari dimensi paling kanan; dua dimensi kompatibel bila sama atau salah satunya 1; dimensi yang hilang pada operand lebih pendek diperlakukan sebagai 1. Tensor berdimensi 1 direntangkan menyusuri dimensi tersebut, dan hasil operasi memakai ukuran terbesar tiap dimensi. Bedanya hanya panggungnya — di deep learning, broadcasting paling sering terjadi di rank 3 dan 4, bukan matriks kecil.

Kasus yang akan kita selesaikan adalah normalisasi batch gambar: kurangi rata-rata tiap channel dari seluruh piksel channel itu, lalu bagi oleh simpangan bakunya. Rata-rata per channel dihitung dengan menggabungkan sumbu N, H, dan W sekaligus:

python
"""Normalisasi batch gambar (N, C, H, W) per channel lewat broadcasting."""

import torch

torch.manual_seed(42)
skala = torch.tensor([1.0, 10.0, 100.0]).reshape(1, 3, 1, 1)
batch = torch.randn(2, 3, 2, 2) * skala

print("batch shape :", tuple(batch.shape))
print("batch:")
print(batch)

rata = batch.mean(dim=(0, 2, 3), keepdim=True)
std = batch.std(dim=(0, 2, 3), keepdim=True)
print("shape rata  :", tuple(rata.shape), "nilai:",
      [round(x, 4) for x in rata.flatten().tolist()])

norm = (batch - rata) / std
print("mean per channel setelah:")
print(norm.mean(dim=(0, 2, 3)))
print("std per channel setelah:")
print(norm.std(dim=(0, 2, 3)))

a = torch.ones(2, 3, 4, 5)
b = torch.ones(3, 4, 2)
try:
    a + b
except RuntimeError as galat:
    print()
    print("RuntimeError:", galat)

Output yang Dihasilkan:

text
batch shape : (2, 3, 2, 2)
batch:
tensor([[[[  1.9269,   1.4873],
          [  0.9007,  -2.1055]],

         [[  6.7842, -12.3454],
          [ -0.4307, -16.0467]],

         [[ 35.5860, -68.6623],
          [-49.3356,  24.1488]]],


        [[[ -1.1109,   0.0915],
          [ -2.3169,  -0.2168]],

         [[ -3.0973,  -3.9571],
          [  8.0341,  -6.2160]],

         [[-59.2001,  -6.3074],
          [-82.8554,  33.0898]]]])
shape rata  : (1, 3, 1, 1) nilai: [-0.168, -3.4094, -21.692]
mean per channel setelah:
tensor([-2.9802e-08,  0.0000e+00, -5.2154e-08])
std per channel setelah:
tensor([1., 1., 1.])

RuntimeError: The size of tensor a (5) must match the size of tensor b (2) at non-singleton dimension 3

Mari kita telaah kode di atas baris per baris:

  1. Baris 6-7 (pembuatan batch): randn(2, 3, 2, 2) dikalikan skala berbentuk (1, 3, 1, 1) — broadcasting pertama sudah terjadi di sini: nilai [1, 10, 100] direntangkan ke seluruh piksel tiap channel sehingga ketiga channel sengaja berskala berbeda.
  2. Baris 13-14 (statistik per channel): dim=(0, 2, 3) menggabungkan sumbu batch, tinggi, dan lebar sekaligus; keepdim=True menahan sumbu yang digabungkan dengan panjang 1, menghasilkan rata berbentuk (1, 3, 1, 1) — tiga angka, satu untuk tiap channel.
  3. Baris 18 (norm = (batch - rata) / std): shape (2,3,2,2) dikurangi (1,3,1,1) — dimensi 1 pada N, H, dan W direntangkan mengikuti batch — lalu dibagi std dengan mekanisme sama. Verifikasi di bawahnya menunjukkan rata-rata tiap channel nol dan simpangan satu (toleransi 1e-8 berasal dari pembulatan float32).
  4. Baris 24-30 (kasus gagal): a berbentuk (2,3,4,5) dan b (3,4,2) diratakan menjadi (1,3,4,2); dibandingkan dari kanan, 5 berhadapan dengan 2 dan keduanya bukan 1 — operasi ditolak.

Membaca Pesan Galat Bentuk #

Pesan The size of tensor a (5) must match the size of tensor b (2) at non-singleton dimension 3 pada keluaran di atas terlihat padat, tetapi setiap bagiinya memberi petunjuk.

expand versus repeat #

Broadcasting menghitung hasil tanpa benar-benar menyalin operand kecil, tetapi kadang kita butuh tensor yang benar-benar berbentuk besar — misalnya untuk disimpan atau dioperasikan lebih jauh. Dua jalurnya berbeda mahalnya: expand membuat tampilan dengan stride 0 (menunjuk data yang sama), sedangkan repeat menduplikasi data sungguhan. Berkas expand_vs_repeat.py membedah keduanya:

python
"""expand membuat tampilan tanpa menyalin; repeat menyalin data sungguhan."""

import torch

v = torch.tensor([10.0, 20.0, 30.0])

e = v.unsqueeze(0).expand(2, 3)
r = v.unsqueeze(0).repeat(2, 1)

print("expand(2, 3) : shape", tuple(e.shape), "stride", e.stride())
print("repeat(2, 1) : shape", tuple(r.shape), "stride", r.stride())
print("memori data expand :", e.untyped_storage().nbytes(), "byte")
print("memori data repeat :", r.untyped_storage().nbytes(), "byte")

print()
try:
    e.mul_(2.0)
except RuntimeError as galat:
    print("e.mul_(2.0) RuntimeError:", str(galat).split(".")[0] + ".")

r.mul_(2.0)
print("r.mul_(2.0) berhasil:")
print(r)

Output yang Dihasilkan:

text
expand(2, 3) : shape (2, 3) stride (0, 1)
repeat(2, 1) : shape (2, 3) stride (3, 1)
memori data expand : 12 byte
memori data repeat : 24 byte

e.mul_(2.0) RuntimeError: unsupported operation: more than one element of the written-to tensor refers to a single memory location.
r.mul_(2.0) berhasil:
tensor([[20., 40., 60.],
        [20., 40., 60.]])

Mari kita telaah kode di atas baris per baris:

  1. Baris 7-8 (dua jalur): expand(2, 3) melebarkan sumbu 0 dari 1 menjadi 2 tanpa menyalin; repeat(2, 1) mengulang seluruh blok dua kali.
  2. Baris 10-13 (bukti memori): stride (0, 1) pada hasil expand berarti seluruh baris menunjuk tiga elemen yang sama — datanya hanya 12 byte, separuh dari repeat yang benar-benar menyimpan 6 elemen.
  3. Baris 16-22 (batas expand): operasi in-place mul_ pada tensor hasil expand ditolak, karena satu lokasi memori mewakili beberapa elemen sekaligus — mengalikannya dua kali akan menghasilkan nilai yang salah; repeat bebas dari masalah ini karena tiap elemen punya rumah sendiri.

Aturan broadcasting PyTorch didokumentasikan lengkap di docs.pytorch.org/docs/stable/notes/broadcasting.html . Semua operasi sejauh ini berjalan maju saja; padahal inti pelatihan adalah menghitung gradien ke belakang. Mekanisme yang merekam setiap operasi lalu membaliknya — autograd — adalah chapter terpenting pada section ini.