PixelCNN, bir görüntünün ortak olasılığını pikseller ve renk kanalları üzerinde sıralı koşullu olasılıklara ayıran autoregressive bir üretici modeldir. Aäron van den Oord ve arkadaşları modeli 2016’da Pixel Recurrent Neural Networks makalesinde, PixelRNN ailesinin tamamen konvolüsyonel ve en hızlı çeşidi olarak tanıttı. Maskeli konvolüsyon sayesinde eğitimde bütün koşullu dağılımlar tek bir ileri geçişte hesaplanır; örnekleme ise piksel piksel ve kanal kanal ilerler.
Aşağıda olasılık ayrıştırması, A ve B maskelerinin uzamsal ve renk kanalı kuralları, PyTorch ile basitleştirilmiş bir uygulama, örnekleme döngüsü ve bits/dim ölçütü ele alınıyor. Son bölümler kör nokta (blind spot) sorununu çözen Gated PixelCNN’i, sınıf koşullamayı ve PixelCNN++ iyileştirmelerini özetliyor.
Autoregressive ayrıştırma
n×n boyutlu bir görüntüdeki pikseller satır satır, soldan sağa sıralanır. Olasılığın zincir kuralı ortak dağılımı koşullu dağılımların çarpımına ayırır:
p(x) = ∏ p(x_i | x_1, ..., x_{i-1}), i = 1, ..., n²
Renkli görüntüde her piksel üç değerden oluşur. PixelCNN bir piksel içindeki kanalları da sıraya koyar: önce kırmızı (R), sonra R’ye bağlı yeşil (G), en son R ve G’ye bağlı mavi (B):
p(x_i | x_<i) = p(x_i,R | x_<i) · p(x_i,G | x_<i, x_i,R) · p(x_i,B | x_<i, x_i,R, x_i,G)
Her kanal değeri 0 ile 255 arasında ayrık bir sayıdır ve 256 sınıflı bir softmax ile modellenir. PixelRNN makalesi bu kategorik dağılımın şekil hakkında varsayım yapmadığını, çok tepeli (multimodal) dağılımları da temsil edebildiğini ve sürekli dağılımlara göre daha iyi sonuç verdiğini raporlar. Aynı fikir WaveNet’te ham ses örneklerine uygulanır; dil modellerindeki “sonraki token” tahmini de aynı ayrıştırmaya dayanır.
Maskeli konvolüsyon: A ve B maskeleri
Sıradan bir konvolüsyon, merkezdeki pikselin sağındaki ve altındaki komşuları da görür. Bu, henüz üretilmemiş değerlere bakmak anlamına gelir. PixelCNN, çekirdek ağırlıklarını bir maskeyle çarparak bu bağlantıları keser. 5×5 bir çekirdeğin uzamsal maskesi şöyledir (* merkez konumdur):
1 1 1 1 1
1 1 1 1 1
1 1 * 0 0
0 0 0 0 0
0 0 0 0 0
Merkez konumda renk kanalı kuralı geçerlidir. Özellik haritaları R, G ve B gruplarına ayrılır:
- A maskesi yalnız ilk katmanda kullanılır. Bir renk grubu, merkezdeki pikselin yalnız kendinden önceki renklerini görür: R hiçbirini, G yalnız R’yi, B ise R ve G’yi.
- B maskesi sonraki katmanlarda kullanılır ve aynı rengin kendisine de bağlantı verir. Gizli katmandaki G özellikleri zaten yalnız izin verilen bilgiden hesaplandığı için bu bağlantı geleceği sızdırmaz.
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
class MaskedConv2d(nn.Conv2d):
"""Kanal i'nin rengi i % 3'tür: 0 = R, 1 = G, 2 = B."""
def __init__(self, mask_type, in_ch, out_ch, kernel_size):
super().__init__(in_ch, out_ch, kernel_size, padding=kernel_size // 2)
assert mask_type in ("A", "B")
mask = torch.ones_like(self.weight) # (out_ch, in_ch, k, k)
c = kernel_size // 2
mask[:, :, c, c + 1 :] = 0 # merkez satırda sağdaki konumlar
mask[:, :, c + 1 :, :] = 0 # merkezin altındaki satırlar
out_color = torch.arange(out_ch).view(-1, 1) % 3 # (out_ch, 1)
in_color = torch.arange(in_ch).view(1, -1) % 3 # (1, in_ch)
if mask_type == "A":
allowed = in_color < out_color # yalnız önceki renkler
else:
allowed = in_color <= out_color # önceki renkler ve aynı renk
mask[:, :, c, c] = allowed.float() # (out_ch, in_ch)
self.register_buffer("mask", mask)
def forward(self, x):
# Maske her ileri geçişte uygulanır; gradyan da maskelenmiş ağırlıktan akar.
return F.conv2d(x, self.weight * self.mask, self.bias, padding=self.padding)
Maskeyi self.weight.data üzerine yerinde yazmak yerine her ileri geçişte ağırlıkla çarpmak, optimizer adımından sonra maskenin bozulmasını önler.
Basitleştirilmiş PixelCNN modeli
Aşağıdaki model basitleştirilmiş bir örnektir: 7×7 A maskeli ilk katman, ardından 3×3 B maskeli katmanlar ve 1×1 B maskeli bir çıkış katmanı. Orijinal PixelCNN, B maskeli katmanları residual bloklar içinde kullanır ve çıkıştan önce iki adet 1×1 katman koyar (PixelRNN makalesi, Tablo 1).
class PixelCNN(nn.Module):
def __init__(self, hidden=120, n_layers=7):
super().__init__()
# hidden 3'e bölünebilir olmalı ki R, G ve B grupları eşit büyüklükte olsun.
layers = [MaskedConv2d("A", 3, hidden, 7), nn.ReLU()]
for _ in range(n_layers):
layers += [MaskedConv2d("B", hidden, hidden, 3), nn.ReLU()]
layers += [MaskedConv2d("B", hidden, 3 * 256, 1)]
self.net = nn.Sequential(*layers)
def forward(self, x):
# x: (B, 3, H, W), [0, 1] aralığına ölçeklenmiş piksel değerleri
b, _, h, w = x.shape
logits = self.net(x) # (B, 768, H, W)
# Kanal k = değer * 3 + renk olduğundan k % 3 rengi verir ve maskeyle uyuşur.
# view(b, 3, 256, ...) yazmak bu eşleşmeyi bozar ve geleceği sızdırır.
return logits.view(b, 256, 3, h, w) # (B, 256 sınıf, 3 renk, H, W)
Çıkış katmanı her konum için 3 × 256 = 768 değer üretir: her renk kanalı için ayrı bir 256 sınıflı dağılım. Son view çağrısı sınıf boyutunu ikinci sıraya koyar; F.cross_entropy sınıf boyutunu tam olarak orada bekler.
Eğitim: piksel başına cross-entropy
Eğitimde gerçek görüntü girdi olarak verilir ve her konumdaki her renk için doğru değerin olasılığı en büyüklenir. Maskeler sayesinde bütün koşullu dağılımlar tek ileri geçişte paralel hesaplanır; bu yüzden eğitim hızlıdır.
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
def nll_loss(model, x_int):
# x_int: (B, 3, H, W), 0..255 arası tamsayı (torch.long)
logits = model(x_int.float() / 255.0) # (B, 256, 3, H, W)
return F.cross_entropy(logits, x_int) # boyut başına ortalama, nats
device = "cuda" if torch.cuda.is_available() else "cpu"
train_set = datasets.CIFAR10(
"data",
train=True,
download=True,
transform=transforms.PILToTensor(), # uint8, (3, 32, 32), 0..255
)
loader = DataLoader(train_set, batch_size=64, shuffle=True)
model = PixelCNN().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(10):
model.train()
for x, _ in loader:
x = x.to(device).long() # (64, 3, 32, 32), 0..255
loss = nll_loss(model, x)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f"epoch {epoch}: {loss.item() / math.log(2):.3f} bits/dim (son batch)")
transforms.PILToTensor() değerleri doğrudan 0..255 tamsayı olarak verir. ToTensor() ile gelen [0, 1] aralığındaki değerleri (x * 255).long() ile geri çevirmek, kayan nokta hatası yüzünden bazı değerleri bir alt tamsayıya düşürebilir; o yolu seçerseniz (x * 255).round().long() kullanın.
Bu kayıp, modelin verdiği negatif log-olabilirliktir (negative log-likelihood). Veri dağılımı ile model arasındaki cross-entropy’yi en küçüklemek, Kullback-Leibler divergence D_KL(p_veri ‖ p_model) değerini en küçüklemekle aynı anlama gelir, çünkü aradaki fark verinin sabit entropisidir. Eğitim tek bir maximum likelihood hedefiyle ilerler; GAN’lerdeki generator ile discriminator arasındaki oyun burada yoktur.
Örnekleme: H × W × C adım
Üretimde gelecek değerler bilinmediği için her renk kanalı sırayla örneklenir ve görüntüye yazılır. 32×32 RGB bir görüntü için bu, 32 · 32 · 3 = 3072 ileri geçiş demektir.
@torch.no_grad()
def sample(model, n=16, height=32, width=32, device="cpu"):
model.eval()
x = torch.zeros(n, 3, height, width, dtype=torch.long, device=device)
for i in range(height):
for j in range(width):
for c in range(3): # R, G, B sırası
logits = model(x.float() / 255.0) # (n, 256, 3, H, W)
probs = F.softmax(logits[:, :, c, i, j], dim=1) # (n, 256)
x[:, c, i, j] = torch.multinomial(probs, 1).squeeze(1) # (n,)
return x # (n, 3, H, W), 0..255
Henüz üretilmemiş konumlar sıfırla doludur; maskeler bu değerlerin sonucu etkilemesini engeller. Bu basit döngü her adımda bütün ağı yeniden çalıştırır. Ramachandran ve arkadaşları (2017), önceki adımlarda hesaplanan gizli durumları önbellekte tutarak bu tekrarı kaldırdı ve PixelCNN++ için 183 kata varan hızlanma raporladı. Önbellekte tutulan şey ağın gerçek gizli durumlarıdır; yalnız konuma göre sonuç saklayan bir önbellek, görüntü değiştikçe eski ve yanlış değerler döndürür.
Değerlendirme: bits/dim
PixelCNN tam olasılık (exact likelihood) hesaplayabildiği için modeller test verisindeki negatif log-olabilirlikle karşılaştırılır. Sonuç, boyut sayısına bölünüp bit cinsine çevrilir:
bits/dim = −log₂ p(x) / D, D = H · W · C (CIFAR-10 için 32 · 32 · 3 = 3072)
Bir görüntünün olasılığını hesaplamak, örneklemenin aksine tek ileri geçiş gerektirir:
@torch.no_grad()
def bits_per_dim(model, loader, device="cpu"):
model.eval()
total_nats, total_dims = 0.0, 0
for x, _ in loader: # PILToTensor ile gelen uint8 görüntüler
x = x.to(device).long() # (B, 3, H, W), 0..255
logits = model(x.float() / 255.0) # (B, 256, 3, H, W)
total_nats += F.cross_entropy(logits, x, reduction="sum").item()
total_dims += x.numel() # B * 3 * H * W
return total_nats / (total_dims * math.log(2))
Her değeri eşit olasılıkla tahmin eden bir model 8 bits/dim verir. CIFAR-10 test kümesinde raporlanan değerler: PixelCNN 3,14, Gated PixelCNN 3,03, PixelRNN 3,00 ve PixelCNN++ 2,92 bits/dim. Boyut başına perplexity gerekirse 2 ** bits_per_dim ile hesaplanır; bütün görüntü için hesaplanan perplexity çok büyük bir sayı olduğundan pratikte kullanılmaz.
Kör nokta ve Gated PixelCNN
Maskeli 3×3 konvolüsyonlar üst üste konduğunda alıcı alan (receptive field) büyür, ancak merkezin sağ üstünde kalan bir bölge hiç görülmez. van den Oord ve arkadaşlarının 2016 tarihli Conditional PixelCNN makalesine göre bu kör nokta, 3×3 filtrelerde olası alıcı alanın dörtte birine kadar çıkabilir.
Gated PixelCNN bu sorunu iki ayrı konvolüsyon yığınıyla çözer. Dikey yığın (vertical stack) mevcut satırın üstündeki bütün satırları görür ve alıcı alanı dikdörtgen biçimde, boşluksuz büyür. Yatay yığın (horizontal stack) 1×n boyutlu konvolüsyonlarla yalnız mevcut satırda soldaki pikselleri görür. Makale, bu maskeli konvolüsyonların ⌈n/2⌉×n boyutlu sıradan konvolüsyonlar ve ardından dolgu ile kırpma yoluyla piksel kaydırmasıyla da uygulanabileceğini belirtir. Her katmanda dikey yığının çıktısı 1×1 konvolüsyonla yatay yığına eklenir. Ters yönde bağlantı yoktur, çünkü yatay yığının bilgisi dikey yığına geçerse dikey yığın aşağıdaki ve sağdaki pikselleri dolaylı olarak görmüş olur.
Makale, katmanlar arasındaki ReLU yerine LSTM’deki çarpımsal kapılardan esinlenen kapılı bir aktivasyon kullanır:
y = tanh(W_f * x) ⊙ σ(W_g * x)
Burada * konvolüsyon, ⊙ eleman bazında çarpım, σ sigmoid fonksiyonudur. Bu değişikliklerle Gated PixelCNN, CIFAR-10 ve ImageNet’te PixelRNN’in log-olabilirliğine ulaşırken eğitim süresinin yarısından azını gerektirir.
Koşullu PixelCNN
Conditional PixelCNN, görüntüyü bir koşul vektörü h’ye bağlı üretir: p(x | h). Koşul, her kapılı katmanda doğrusal olmayan fonksiyondan önce eklenen bir terimdir:
y = tanh(W_f * x + V_fᵀ h) ⊙ σ(W_g * x + V_gᵀ h)
h bir sınıfın one-hot kodlaması ise V_fᵀ h her katmana eklenen ve sınıfa bağlı bir bias’tır. Bu terim konuma bağlı değildir; “görüntüde köpek olsun” bilgisini taşır, köpeğin nerede duracağını belirlemez. Konuma bağlı koşul gerektiğinde h önce bir dekonvolüsyon ağıyla görüntü boyutunda bir s haritasına çevrilir ve maskesiz 1×1 konvolüsyonla eklenir. Sınıf bias’ı PyTorch’ta bir embedding tablosuyla kurulabilir:
class ClassBias(nn.Module):
"""One-hot h için V^T h terimi: sınıf başına öğrenilen bir bias vektörü."""
def __init__(self, num_classes, channels):
super().__init__()
self.embed = nn.Embedding(num_classes, channels) # (num_classes, C)
def forward(self, features, labels):
# features: (B, C, H, W), labels: (B,) sınıf indeksleri
bias = self.embed(labels)[:, :, None, None] # (B, C, 1, 1)
return features + bias # bütün konumlara aynı bias yayınlanır
Bu modül her maskeli konvolüsyonun çıktısına, aktivasyondan önce eklenir. Bias piksel değerlerine bağlı olmadığı için maskelerin sağladığı nedenselliği bozmaz. Sınıf bilgisini girdi görüntüsüne ek kanal olarak bağlamak ise ilk katmanın beklediği kanal sayısını değiştirir ve makaledeki yöntem değildir.
PixelCNN++ ve sonrası
Salimans ve arkadaşlarının 2017 tarihli PixelCNN++ çalışması modeli beş noktada değiştirdi: 256 sınıflı softmax yerine ayrıklaştırılmış lojistik karışım (discretized logistic mixture) olasılığı, R/G/B alt pikselleri yerine bütün piksele koşullama, birden fazla çözünürlüğü yakalamak için alt örnekleme, ek kısa yol bağlantıları ve dropout ile düzenlileştirme. Bu değişikliklerle CIFAR-10’da 2,92 bits/dim elde edildi.
PixelCNN tek başına günümüz görüntü üretiminde kullanılan bir model değildir; metinden görüntü üretiminde diffusion ve flow tabanlı modeller öne geçti. Fikir ise başka modellerin parçası olarak sürdü: VQ-VAE (van den Oord ve ark., 2017), görüntülerden öğrendiği ayrık gizil kodlar üzerinde önsel dağılım olarak bir PixelCNN, ses için ise bir WaveNet eğitir.
Sınırlar
- Yavaş örnekleme: Görüntü boyutu ve kanal sayısıyla doğrusal artan sayıda ileri geçiş gerekir. Önbellekleme hızlandırır ama sıralı yapıyı değiştirmez.
- Kör nokta: Orijinal maskeli yığında alıcı alanın bir kısmı görülmez; Gated PixelCNN’in iki yığınlı yapısı bunu giderir.
- Olasılık ve görsel kalite: Theis ve arkadaşları (2015), yüksek boyutlu veride ortalama log-olabilirlik ile örneklerin görsel kalitesinin büyük ölçüde birbirinden bağımsız olduğunu gösterdi (arXiv:1511.01844). İyi bir bits/dim değeri, örneklerin göze iyi göründüğünü garanti etmez.
- Güçlü yanlar: Tam olasılık hesaplanır, eğitim tek bir maximum likelihood hedefiyle kararlı ilerler ve model verideki her örneğe olasılık vermeye zorlanır.
Sonuç
PixelCNN, görüntü olasılığını açıkça modellemek, modelleri bits/dim ile karşılaştırmak veya autoregressive bir önsel kurmak gerektiğinde uygun ve öğretici bir mimaridir. Hızlı örnekleme ya da yüksek çözünürlüklü, algısal kalitesi yüksek görüntü üretimi hedefleniyorsa diffusion veya flow tabanlı modeller daha uygun bir başlangıçtır.
Birincil kaynaklar
- van den Oord, Kalchbrenner, Kavukcuoglu (2016): Pixel Recurrent Neural Networks, arXiv:1601.06759
- van den Oord ve ark. (2016): Conditional Image Generation with PixelCNN Decoders, arXiv:1606.05328
- Salimans ve ark. (2017): PixelCNN++, arXiv:1701.05517
- Ramachandran ve ark. (2017): Fast Generation for Convolutional Autoregressive Models, arXiv:1704.06001
- van den Oord, Vinyals, Kavukcuoglu (2017): Neural Discrete Representation Learning (VQ-VAE), arXiv:1711.00937
- PyTorch torch.nn.functional.cross_entropy belgesi