Skip to content

Sinh ảnh (1): VAE, GAN, CycleGAN, Pix2Pix

This content is not available in your language yet.

Sau bài này, bạn sẽ:

  • Hiểu mô hình sinh (generative model) là gì: học phân phối của dữ liệu để lấy mẫu ra ảnh mới.
  • Tự dẫn được ELBO (evidence lower bound) của VAE (variational autoencoder), công thức KL dạng đóng với phân phối Gauss, và giải thích được vì sao cần mẹo tham số hóa lại (reparameterization trick).
  • Hiểu trò chơi giữa bộ sinh (generator) và bộ phân biệt (discriminator) của GAN (generative adversarial network): bộ phân biệt tối ưu, hàm mất mát không bão hòa, và vì sao GAN khó huấn luyện.
  • Phân biệt được Pix2Pix (cần ảnh ghép cặp) với CycleGAN (không cần cặp), và chọn được cách hợp với dữ liệu mình có.

Bạn có 1 797 ảnh chữ số viết tay cỡ 8×88 \times 8 (bộ load_digits của scikit-learn). Bạn muốn máy vẽ ra chữ số mới: trông như viết tay, nhưng không trùng ảnh nào đã có.

Hãy nghĩ mỗi ảnh là một điểm trong không gian 64 chiều (64 điểm ảnh). Ảnh chữ số thật không rải đều khắp không gian đó. Chọn bừa 64 số, bạn được nhiễu lốm đốm, gần như không bao giờ ra một chữ số. Ảnh thật chỉ tụ ở một vùng rất nhỏ, có hình dạng phức tạp. “Vẽ ảnh mới” nghĩa là rút một điểm từ đúng vùng đó: lấy mẫu từ phân phối của dữ liệu thật, pdata(x)p_{\text{data}}(\mathbf{x}).

Ta không có công thức của pdatap_{\text{data}}, chỉ có các mẫu của nó. Mô hình sinh phải học từ mẫu một cách lấy mẫu mới.

Ý thứ nhất: nén rồi giải nén. Mọi chữ số “3” khác nhau ở vài yếu tố: nghiêng nhiều hay ít, nét dày hay mảnh, vòng trên to hay nhỏ. Nếu mô tả được ảnh bằng vài con số như vậy (một biến ẩn, latent variable, ký hiệu z\mathbf{z}), và biết các con số đó thường phân bố thế nào, thì sinh ảnh dễ: rút vài con số, rồi “giải nén” thành ảnh. Autoencoder đã có bộ giải nén. Thứ còn thiếu là một vùng z\mathbf{z} có trật tự, để rút bừa một z\mathbf{z} trong vùng đó vẫn ra ảnh hợp lý. VAE thêm đúng điều này.

Ý thứ hai: người làm giả và người kiểm tra. Một người tập vẽ tiền giả, một người tập soi tiền. Người soi giỏi lên thì người vẽ phải vẽ kỹ hơn, và ngược lại. Không ai viết ra công thức “tiền thật trông thế nào”; người vẽ chỉ biết mình bị bắt ở đâu. GAN là trò chơi này giữa hai mạng nơ-ron.

Sau đó còn một biến thể rất hay dùng: không sinh từ con số ngẫu nhiên, mà biến ảnh này thành ảnh kia, như bản phác thảo thành ảnh chụp, ảnh ngày thành ảnh đêm. Đó là Pix2Pix và CycleGAN.

Bài toán của bài này là: chỉ có một tập ảnh mẫu, làm sao học được một cách rút ra ảnh mới trông như thật, và làm sao huấn luyện cách đó bằng gradient?

Ảnh được trải phẳng thành vectơ x\mathbf{x} (với chữ số 8×88 \times 8 là 64 số). Biến ẩn z∈RJ\mathbf{z} \in \mathbb{R}^J có JJ chiều, theo ký hiệu của bài báo VAE. Phân phối tiên nghiệm (prior) của z\mathbf{z} là p(z)=N(0,I)p(\mathbf{z}) = \mathcal{N}(\mathbf{0}, \mathbf{I}). Bộ mã hóa qϕ(z∣x)q_\phi(\mathbf{z} \mid \mathbf{x}) có tham số ϕ\phi, bộ giải mã pθ(x∣z)p_\theta(\mathbf{x} \mid \mathbf{z}) có tham số θ\theta.

Ở phần GAN, GG là bộ sinh và DD là bộ phân biệt: một mạng nhận ảnh, trả ra xác suất ảnh đó là thật. Chữ DD này khác DD số chiều embedding ở các bài ViT, và khác chữ DD trong DKLD_{\mathrm{KL}} (độ lệch KL).

Cách quen thuộc để học pθ(x)p_\theta(\mathbf{x}) là làm lớn likelihood của dữ liệu: chọn θ\theta để log⁡pθ(x)\log p_\theta(\mathbf{x}) lớn ở các ảnh thật. Với mô hình có biến ẩn, ảnh được sinh qua hai bước: rút z∼p(z)\mathbf{z} \sim p(\mathbf{z}), rồi rút x∼pθ(x∣z)\mathbf{x} \sim p_\theta(\mathbf{x} \mid \mathbf{z}). Xác suất của một ảnh là tổng trên mọi cách sinh ra nó:

pθ(x)=∫pθ(x∣z) p(z) dzp_\theta(\mathbf{x}) = \int p_\theta(\mathbf{x} \mid \mathbf{z})\, p(\mathbf{z})\, d\mathbf{z}

Tích phân này trên mọi z\mathbf{z} không tính được khi bộ giải mã là mạng nơ-ron. Rút bừa z\mathbf{z} để ước lượng cũng không ổn: gần như mọi z\mathbf{z} rút bừa cho ra ảnh khác hẳn x\mathbf{x}, nên cần vô số mẫu. Ta cần biết z\mathbf{z} nào có khả năng sinh ra x\mathbf{x}. Đó là việc của bộ mã hóa.

VAE (Kingma và Welling, 2014) sửa autoencoder ở một chỗ: bộ mã hóa không trả về một vectơ z\mathbf{z}, mà trả về một phân phối của z\mathbf{z}. Với mỗi ảnh, bộ mã hóa trả ra hai vectơ μ\boldsymbol\mu và σ\boldsymbol\sigma (cùng JJ chiều), và

qϕ(z∣x)=N(μ,diag(σ2))q_\phi(\mathbf{z} \mid \mathbf{x}) = \mathcal{N}\big(\boldsymbol\mu, \mathrm{diag}(\boldsymbol\sigma^2)\big)

Hiểu nôm na: ảnh không được đóng đinh vào một điểm, mà vào một “đám mây” nhỏ. Bộ giải mã phải dựng lại ảnh từ bất kỳ điểm nào trong đám mây. Đồng thời, mọi đám mây bị kéo về gần N(0,I)\mathcal{N}(\mathbf{0}, \mathbf{I}). Hai lực này làm vùng z\mathbf{z} liền mạch: không còn lỗ hổng giữa các ảnh, nên rút z\mathbf{z} từ N(0,I)\mathcal{N}(\mathbf{0}, \mathbf{I}) rồi giải mã vẫn ra ảnh có nghĩa. Hai lực đó chính là hai số hạng của ELBO dưới đây.

Bước 1: viết log⁡pθ(x)\log p_\theta(\mathbf{x}) thành một kỳ vọng. log⁡pθ(x)\log p_\theta(\mathbf{x}) không phụ thuộc z\mathbf{z}, nên lấy kỳ vọng của nó theo bất kỳ phân phối nào của z\mathbf{z} vẫn được chính nó. Chọn phân phối qϕ(z∣x)q_\phi(\mathbf{z} \mid \mathbf{x}):

log⁡pθ(x)=Eqϕ(z∣x)[log⁡pθ(x)]\log p_\theta(\mathbf{x}) = \mathbb{E}_{q_\phi(\mathbf{z} \mid \mathbf{x})}\big[\log p_\theta(\mathbf{x})\big]

Bước 2: dùng quy tắc nhân xác suất. Phân phối đồng thời viết được theo hai cách: pθ(x,z)=pθ(x) pθ(z∣x)p_\theta(\mathbf{x}, \mathbf{z}) = p_\theta(\mathbf{x})\, p_\theta(\mathbf{z} \mid \mathbf{x}). Chia cho pθ(z∣x)p_\theta(\mathbf{z} \mid \mathbf{x}), ta được pθ(x)=pθ(x,z)/pθ(z∣x)p_\theta(\mathbf{x}) = p_\theta(\mathbf{x}, \mathbf{z}) / p_\theta(\mathbf{z} \mid \mathbf{x}). Sau đó nhân cả tử và mẫu với cùng một số qϕ(z∣x)q_\phi(\mathbf{z} \mid \mathbf{x}) (không đổi giá trị), rồi tách log của tích thành tổng hai log:

log⁡pθ(x)=Eq[log⁡pθ(x,z)pθ(z∣x)]=Eq[log⁡pθ(x,z)qϕ(z∣x)]+Eq[log⁡qϕ(z∣x)pθ(z∣x)]\begin{aligned} \log p_\theta(\mathbf{x}) &= \mathbb{E}_{q}\left[\log \frac{p_\theta(\mathbf{x}, \mathbf{z})}{p_\theta(\mathbf{z} \mid \mathbf{x})}\right] \\ &= \mathbb{E}_{q}\left[\log \frac{p_\theta(\mathbf{x}, \mathbf{z})}{q_\phi(\mathbf{z} \mid \mathbf{x})}\right] + \mathbb{E}_{q}\left[\log \frac{q_\phi(\mathbf{z} \mid \mathbf{x})}{p_\theta(\mathbf{z} \mid \mathbf{x})}\right] \end{aligned}

Ở đây Eq\mathbb{E}_q viết tắt cho Eqϕ(z∣x)\mathbb{E}_{q_\phi(\mathbf{z} \mid \mathbf{x})}.

Bước 3: nhận ra độ lệch KL. Số hạng thứ hai đúng là định nghĩa của DKL(qϕ(z∣x)∥pθ(z∣x))D_{\mathrm{KL}}\big(q_\phi(\mathbf{z} \mid \mathbf{x}) \parallel p_\theta(\mathbf{z} \mid \mathbf{x})\big), luôn không âm. Bỏ nó đi thì vế phải nhỏ đi hoặc giữ nguyên, nên số hạng thứ nhất là một cận dưới của log⁡pθ(x)\log p_\theta(\mathbf{x}). Ta gọi nó là ELBO:

log⁡pθ(x)≥LELBO=Eq[log⁡pθ(x,z)qϕ(z∣x)]\log p_\theta(\mathbf{x}) \ge \mathcal{L}_{\text{ELBO}} = \mathbb{E}_{q}\left[\log \frac{p_\theta(\mathbf{x}, \mathbf{z})}{q_\phi(\mathbf{z} \mid \mathbf{x})}\right]

Khoảng cách giữa hai vế đúng bằng độ lệch KL vừa bỏ: qϕq_\phi càng gần phân phối thật pθ(z∣x)p_\theta(\mathbf{z} \mid \mathbf{x}), cận càng sát. Vì thế làm lớn ELBO vừa đẩy log⁡pθ(x)\log p_\theta(\mathbf{x}) lên, vừa dạy bộ mã hóa đoán đúng z\mathbf{z} nào sinh ra x\mathbf{x}. Chữ “evidence” (bằng chứng) trong tên là log⁡pθ(x)\log p_\theta(\mathbf{x}).

Bước 4: tách thành tái tạo trừ KL. Viết lại pθ(x,z)=pθ(x∣z) p(z)p_\theta(\mathbf{x}, \mathbf{z}) = p_\theta(\mathbf{x} \mid \mathbf{z})\, p(\mathbf{z}) (sinh z\mathbf{z} trước, rồi x\mathbf{x}), rồi tách log thành hai phần:

LELBO=Eq[log⁡pθ(x∣z)]+Eq[log⁡p(z)qϕ(z∣x)]=Eq[log⁡pθ(x∣z)]⏟(1)−DKL(qϕ(z∣x)∥p(z))⏟(2)\begin{aligned} \mathcal{L}_{\text{ELBO}} &= \mathbb{E}_{q}\big[\log p_\theta(\mathbf{x} \mid \mathbf{z})\big] + \mathbb{E}_{q}\left[\log \frac{p(\mathbf{z})}{q_\phi(\mathbf{z} \mid \mathbf{x})}\right] \\ &= \underbrace{\mathbb{E}_{q}\big[\log p_\theta(\mathbf{x} \mid \mathbf{z})\big]}_{\text{(1)}} - \underbrace{D_{\mathrm{KL}}\big(q_\phi(\mathbf{z} \mid \mathbf{x}) \parallel p(\mathbf{z})\big)}_{\text{(2)}} \end{aligned}

Dòng thứ hai đổi dấu vì log⁡(p/q)=−log⁡(q/p)\log(p/q) = -\log(q/p). Số hạng (1) là số hạng tái tạo (reconstruction): nó lớn khi bộ giải mã dựng lại được x\mathbf{x} từ z\mathbf{z} rút trong đám mây của x\mathbf{x}. Số hạng (2) là KL kéo qϕq_\phi về tiên nghiệm: nó nhỏ khi đám mây gần N(0,I)\mathcal{N}(\mathbf{0}, \mathbf{I}). Đây là hai lực đã nói ở trên. Khi huấn luyện, ta làm nhỏ −LELBO-\mathcal{L}_{\text{ELBO}}, lấy trung bình trên các ảnh của một lô.

Cả qϕq_\phi và p(z)p(\mathbf{z}) đều là Gauss, nên số hạng KL có công thức tính thẳng, không cần lấy mẫu. Các chiều độc lập, nên log mật độ là tổng theo từng chiều, và KL là tổng của JJ độ lệch KL một chiều. Xét một chiều, q=N(μ,σ2)q = \mathcal{N}(\mu, \sigma^2), p=N(0,1)p = \mathcal{N}(0, 1). Log mật độ của hai phân phối là:

log⁡q(z)=−12log⁡(2π)−log⁡σ−(z−μ)22σ2log⁡p(z)=−12log⁡(2π)−z22\begin{aligned} \log q(z) &= -\tfrac12 \log(2\pi) - \log \sigma - \frac{(z - \mu)^2}{2\sigma^2} \\ \log p(z) &= -\tfrac12 \log(2\pi) - \frac{z^2}{2} \end{aligned}

Trừ hai dòng, hằng số log⁡(2π)\log(2\pi) triệt tiêu. Lấy kỳ vọng theo z∼qz \sim q, cần hai kết quả: Eq[(z−μ)2]=σ2\mathbb{E}_q[(z - \mu)^2] = \sigma^2 (định nghĩa phương sai) và Eq[z2]=μ2+σ2\mathbb{E}_q[z^2] = \mu^2 + \sigma^2 (phương sai cộng bình phương trung bình). Thay vào:

DKL(q∥p)=Eq[log⁡q(z)−log⁡p(z)]=−log⁡σ−σ22σ2+μ2+σ22=12(μ2+σ2−1−log⁡σ2)\begin{aligned} D_{\mathrm{KL}}(q \parallel p) &= \mathbb{E}_q\big[\log q(z) - \log p(z)\big] \\ &= -\log \sigma - \frac{\sigma^2}{2\sigma^2} + \frac{\mu^2 + \sigma^2}{2} \\ &= \tfrac12\big(\mu^2 + \sigma^2 - 1 - \log \sigma^2\big) \end{aligned}

Dòng cuối gom các số hạng và viết −log⁡σ=−12log⁡σ2-\log \sigma = -\tfrac12 \log \sigma^2. Cộng trên JJ chiều:

DKL(qϕ(z∣x)∥N(0,I))=12∑j=1J(μj2+σj2−1−log⁡σj2)D_{\mathrm{KL}}\big(q_\phi(\mathbf{z} \mid \mathbf{x}) \parallel \mathcal{N}(\mathbf{0}, \mathbf{I})\big) = \frac12 \sum_{j=1}^{J} \big(\mu_j^2 + \sigma_j^2 - 1 - \log \sigma_j^2\big)

Kiểm nhanh: với μj=0\mu_j = 0, σj=1\sigma_j = 1, mỗi số hạng bằng 0+1−1−0=00 + 1 - 1 - 0 = 0, đúng như mong đợi khi hai phân phối trùng nhau. Đây là công thức ở phụ lục B của bài báo VAE, và ô NumPy bên dưới so nó với ước lượng Monte Carlo. Trong code, bộ mã hóa thường xuất log⁡σ2\log \sigma^2 thay vì σ\sigma, để σ\sigma luôn dương mà không cần ràng buộc.

Số hạng tái tạo là một kỳ vọng theo qϕq_\phi, nên ta ước lượng nó bằng cách rút z\mathbf{z} (bài báo VAE dùng một mẫu cho mỗi ảnh khi lô đủ lớn, ví dụ 100 ảnh). Vấn đề: để học bộ mã hóa, ta cần đạo hàm của loss theo μ\boldsymbol\mu và σ\boldsymbol\sigma. Nhưng “rút một số ngẫu nhiên từ N(μ,σ2)\mathcal{N}(\boldsymbol\mu, \boldsymbol\sigma^2)” không phải một phép tính có đạo hàm: lan truyền ngược không biết đi qua bước rút thăm thế nào.

Mẹo là tách phần ngẫu nhiên ra ngoài. Rút ϵ∼N(0,I)\boldsymbol\epsilon \sim \mathcal{N}(\mathbf{0}, \mathbf{I}), một nhiễu không phụ thuộc tham số nào, rồi tính

z=μ+σ⊙ϵ\mathbf{z} = \boldsymbol\mu + \boldsymbol\sigma \odot \boldsymbol\epsilon

với ⊙\odot là nhân từng phần tử. Dời một biến chuẩn đi μj\mu_j và giãn ra σj\sigma_j lần thì được đúng N(μj,σj2)\mathcal{N}(\mu_j, \sigma_j^2), nên z\mathbf{z} vẫn có phân phối mong muốn. Nhưng giờ, với ϵ\boldsymbol\epsilon đã rút xong, z\mathbf{z} là một hàm bình thường của μ\boldsymbol\mu và σ\boldsymbol\sigma: ∂zj/∂μj=1\partial z_j / \partial \mu_j = 1, ∂zj/∂σj=ϵj\partial z_j / \partial \sigma_j = \epsilon_j. Gradient chảy từ bộ giải mã, qua z\mathbf{z}, về bộ mã hóa như qua mọi lớp khác.

Số hạng tái tạo cụ thể là gì tùy cách chọn pθ(x∣z)p_\theta(\mathbf{x} \mid \mathbf{z}). Coi mỗi điểm ảnh (giá trị trong [0,1][0, 1]) là một biến Bernoulli thì −log⁡pθ(x∣z)-\log p_\theta(\mathbf{x} \mid \mathbf{z}) là cross-entropy nhị phân cộng trên các điểm ảnh; coi là Gauss có phương sai cố định thì nó là tổng bình phương sai số (cộng một hằng số). Bài báo VAE dùng cả hai loại, và code của bài dùng loại Bernoulli.

Bộ giải mã Bernoulli hay Gauss ở trên coi các điểm ảnh độc lập khi đã biết z\mathbf{z}. Giả sử cùng một vùng z\mathbf{z} ứng với vài ảnh hơi khác nhau, ví dụ nét số “1” lệch trái một chút hoặc lệch phải một chút. Sai số bình phương nhỏ nhất khi bộ giải mã trả ra trung bình của các ảnh đó: một nét nhòe nằm giữa. Mà vùng z\mathbf{z} luôn có chồng lấn, vì số hạng KL ép mọi đám mây phình ra và dồn về giữa. Kết quả: ảnh sinh ra trông như trung bình của nhiều ảnh, tức là mờ. Bài báo Pix2Pix cũng ghi nhận điều tương tự cho mất mát L1 và L2 khi sinh ảnh: chúng làm ảnh mờ. Ảnh mẫu của ô VAE bên dưới cho thấy đúng hiện tượng này.

GAN (Goodfellow và cộng sự, 2014) bỏ hẳn likelihood. Bộ sinh GG biến nhiễu z∼p(z)\mathbf{z} \sim p(\mathbf{z}) thành ảnh G(z)G(\mathbf{z}); phân phối của các ảnh này gọi là pgp_g. Bộ phân biệt DD nhận một ảnh và trả ra D(x)∈(0,1)D(\mathbf{x}) \in (0, 1), xác suất nó cho là ảnh thật. Hai mạng chơi một trò chơi minimax:

min⁡Gmax⁡DV(D,G)=Ex∼pdata[log⁡D(x)]+Ez∼p(z)[log⁡(1−D(G(z)))]\min_G \max_D V(D, G) = \mathbb{E}_{\mathbf{x} \sim p_{\text{data}}}\big[\log D(\mathbf{x})\big] + \mathbb{E}_{\mathbf{z} \sim p(\mathbf{z})}\big[\log\big(1 - D(G(\mathbf{z}))\big)\big]

Đọc từng phần: DD muốn VV lớn, tức D(x)D(\mathbf{x}) gần 1 với ảnh thật và D(G(z))D(G(\mathbf{z})) gần 0 với ảnh giả. Đây chính là cross-entropy nhị phân của một bộ phân loại “thật hay giả”, đổi dấu. GG chỉ tác động lên số hạng thứ hai và muốn nó nhỏ, tức muốn DD bị lừa.

Bộ phân biệt tối ưu. Giữ GG cố định. Viết kỳ vọng thành tích phân theo x\mathbf{x} (ảnh giả G(z)G(\mathbf{z}) có phân phối pgp_g):

V=∫(pdata(x)log⁡D(x)+pg(x)log⁡(1−D(x))) dxV = \int \Big( p_{\text{data}}(\mathbf{x}) \log D(\mathbf{x}) + p_g(\mathbf{x}) \log\big(1 - D(\mathbf{x})\big) \Big)\, d\mathbf{x}

DD được chọn tự do ở từng x\mathbf{x}, nên ta làm lớn biểu thức trong ngoặc tại từng điểm. Với a=pdata(x)a = p_{\text{data}}(\mathbf{x}), b=pg(x)b = p_g(\mathbf{x}), hàm alog⁡y+blog⁡(1−y)a \log y + b \log(1 - y) có đạo hàm a/y−b/(1−y)a/y - b/(1 - y), bằng 0 khi y=a/(a+b)y = a/(a + b). Vậy:

D∗(x)=pdata(x)pdata(x)+pg(x)D^*(\mathbf{x}) = \frac{p_{\text{data}}(\mathbf{x})}{p_{\text{data}}(\mathbf{x}) + p_g(\mathbf{x})}

Ý nghĩa: ở chỗ ảnh thật dày hơn ảnh giả, D∗D^* nghiêng về “thật”. Bài báo GAN thay D∗D^* trở lại VV và chứng minh giá trị nhỏ nhất theo GG là −log⁡4-\log 4, đạt khi và chỉ khi pg=pdatap_g = p_{\text{data}}. Lúc đó D∗=1/2D^* = 1/2 ở mọi nơi: bộ phân biệt chỉ còn đoán may rủi. Đó là điểm cân bằng mà trò chơi nhắm tới.

Hàm mất mát không bão hòa. Đầu quá trình học, ảnh giả rất dở, DD nhận ra dễ dàng: D(G(z))≈0D(G(\mathbf{z})) \approx 0. Viết D=σ(a)D = \sigma(a) với aa là logit (đầu ra trước sigmoid). Đạo hàm theo aa:

∂∂alog⁡(1−σ(a))=−σ(a),∂∂alog⁡σ(a)=1−σ(a)\frac{\partial}{\partial a} \log\big(1 - \sigma(a)\big) = -\sigma(a), \qquad \frac{\partial}{\partial a} \log \sigma(a) = 1 - \sigma(a)

Khi σ(a)≈0\sigma(a) \approx 0, đạo hàm thứ nhất gần 0: hàm log⁡(1−D)\log(1 - D) bão hòa (saturate), GG gần như không nhận được tín hiệu học. Đạo hàm thứ hai gần 1. Vì vậy, ngay trong bài báo gốc, Goodfellow và cộng sự đề nghị cho GG làm lớn log⁡D(G(z))\log D(G(\mathbf{z})) thay vì làm nhỏ log⁡(1−D(G(z)))\log(1 - D(G(\mathbf{z}))). Hai mục tiêu cùng hướng, nhưng cái sau cho gradient mạnh ở đầu. Trong code, nó chỉ là cross-entropy nhị phân của ảnh giả với nhãn 1.

Huấn luyện xen kẽ. Mỗi vòng: vài bước cập nhật DD (bài báo gốc dùng một bước), rồi một bước cập nhật GG. Khi cập nhật DD, ảnh giả được tách khỏi đồ thị tính toán để không sửa nhầm GG.

  • Không có một hàm mất mát giảm dần để theo dõi. Hai mạng cùng thay đổi, loss của mỗi bên phụ thuộc bên kia. Loss GG tăng có thể vì DD vừa giỏi lên, không phải vì GG tệ đi. Phân tích D∗D^* ở trên giả định DD luôn tối ưu và hai mạng đủ lớn; thực tế không có điều đó, và quá trình có thể dao động thay vì hội tụ.
  • Mode collapse (sụp mode). GG tìm ra vài ảnh lừa được DD rồi chỉ sinh những ảnh đó, bỏ qua phần lớn sự đa dạng của dữ liệu (ví dụ chỉ vẽ một hai chữ số). Bài báo GAN gọi nó là “kịch bản Helvetica” và khuyên không cập nhật GG quá nhiều mà không cập nhật DD. Ô GAN bên dưới đếm xem ảnh giả thuộc những chữ số nào để thử phát hiện điều này.
  • Nhạy với kiến trúc và siêu tham số. DCGAN (Radford và cộng sự, 2016) đưa ra một bộ quy tắc giúp GAN tích chập học ổn định hơn: thay pooling bằng tích chập có stride (ở DD) và tích chập chuyển vị (ở GG), dùng batch normalization ở cả hai mạng, bỏ các lớp kết nối đầy đủ ẩn, GG dùng ReLU và Tanh ở lớp ra, DD dùng LeakyReLU. Họ dùng Adam với tốc độ học 0.00020.0002 và β1=0.5\beta_1 = 0.5; ô GAN của bài dùng lại hai số này. DCGAN còn cho thấy làm phép cộng trừ trên vectơ z\mathbf{z} cho ra thay đổi có nghĩa trên ảnh khuôn mặt.

Pix2Pix: GAN có điều kiện, ảnh ghép cặp

Phần tiêu đề “Pix2Pix: GAN có điều kiện, ảnh ghép cặp”

Nhiều bài toán không cần sinh ảnh từ hư không, mà cần dịch ảnh sang ảnh (image-to-image translation): bản đồ nhãn thành ảnh đường phố, ảnh đen trắng thành ảnh màu, nét phác thảo thành ảnh chụp. Ở mục này, x\mathbf{x} là ảnh nguồn và y\mathbf{y} là ảnh đích (một ảnh, không phải nhãn lớp). Pix2Pix (Isola và cộng sự, 2017) cần dữ liệu ghép cặp (x,y)(\mathbf{x}, \mathbf{y}).

Đây là một GAN có điều kiện (conditional GAN): cả GG và DD đều được xem ảnh nguồn. DD không hỏi “ảnh này có thật không” mà hỏi “ảnh này có phải đích thật của ảnh nguồn này không”:

LcGAN(G,D)=Ex,y[log⁡D(x,y)]+Ex,z[log⁡(1−D(x,G(x,z)))]\mathcal{L}_{\text{cGAN}}(G, D) = \mathbb{E}_{\mathbf{x}, \mathbf{y}}\big[\log D(\mathbf{x}, \mathbf{y})\big] + \mathbb{E}_{\mathbf{x}, \mathbf{z}}\big[\log\big(1 - D(\mathbf{x}, G(\mathbf{x}, \mathbf{z}))\big)\big]

Vì có đáp án y\mathbf{y}, Pix2Pix thêm mất mát L1 kéo ảnh sinh về gần đáp án, và chọn L1 thay L2 vì L1 ít làm mờ hơn:

G∗=arg⁡min⁡Gmax⁡D  LcGAN(G,D)+λ Ex,y,z[∥y−G(x,z)∥1]G^* = \arg\min_G \max_D \; \mathcal{L}_{\text{cGAN}}(G, D) + \lambda\, \mathbb{E}_{\mathbf{x}, \mathbf{y}, \mathbf{z}}\big[\lVert \mathbf{y} - G(\mathbf{x}, \mathbf{z}) \rVert_1\big]

Tác giả dùng λ=100\lambda = 100. Chia việc rõ ràng: L1 lo phần tổng thể (màu, bố cục đúng chỗ), DD lo phần chi tiết sắc nét mà L1 làm nhòe. Ba lựa chọn thiết kế:

  • Bộ sinh U-Net: mạng mã hóa – giải mã có nối tắt (skip connection) từ lớp thứ ii sang lớp thứ n−in - i, để chi tiết như vị trí cạnh đi thẳng từ ảnh vào sang ảnh ra mà không phải chui qua chỗ hẹp nhất. Bạn gặp kiến trúc này ở bài Phân đoạn ngữ nghĩa.
  • Bộ phân biệt PatchGAN: DD chấm “thật hay giả” cho từng mảnh N×NN \times N của ảnh rồi lấy trung bình, thay vì một điểm cho cả ảnh. Mảnh 70×7070 \times 70 cho kết quả tốt nhất trong thí nghiệm của tác giả. Mảnh nhỏ thì ít tham số, chạy nhanh, và áp được cho ảnh cỡ bất kỳ.
  • Nhiễu qua dropout: tác giả thấy GG học cách bỏ qua z\mathbf{z} đưa vào trực tiếp, nên chỉ giữ ngẫu nhiên dưới dạng dropout, bật cả khi dùng. Dù vậy, đầu ra vẫn ít đa dạng.

Nhiều khi không có cặp: không ai có ảnh cùng một con ngựa đúng tư thế đó nhưng là ngựa vằn. CycleGAN (Zhu và cộng sự, 2017) học với hai tập ảnh không ghép cặp: miền XX (ví dụ ảnh ngựa) và miền YY (ảnh ngựa vằn).

Chỉ dùng GAN thì chưa đủ. Một bộ sinh G:X→YG: X \to Y có thể sinh ra ảnh ngựa vằn rất thật mà không liên quan gì tới con ngựa đưa vào; DD chỉ kiểm “có giống ngựa vằn không”. Có vô số ánh xạ như vậy. CycleGAN thêm một bộ sinh ngược F:Y→XF: Y \to X và đòi đi một vòng phải về chỗ cũ: ngựa → ngựa vằn → ngựa phải ra con ngựa ban đầu. Đó là mất mát nhất quán chu trình (cycle consistency loss):

Lcyc(G,F)=Ex[∥F(G(x))−x∥1]+Ey[∥G(F(y))−y∥1]\mathcal{L}_{\text{cyc}}(G, F) = \mathbb{E}_{\mathbf{x}}\big[\lVert F(G(\mathbf{x})) - \mathbf{x} \rVert_1\big] + \mathbb{E}_{\mathbf{y}}\big[\lVert G(F(\mathbf{y})) - \mathbf{y} \rVert_1\big]

Muốn quay về được x\mathbf{x}, ảnh G(x)G(\mathbf{x}) phải giữ đủ thông tin về x\mathbf{x}: tư thế, nền, bố cục. Hàm mục tiêu đầy đủ có hai trò chơi GAN (GG với DYD_Y, FF với DXD_X) cộng λLcyc\lambda \mathcal{L}_{\text{cyc}}, với λ=10\lambda = 10 trong bài báo. Khi huấn luyện, tác giả thay log-likelihood của GAN bằng bình phương sai số (least-squares GAN) cho ổn định hơn, và dùng PatchGAN 70×7070 \times 70 làm bộ phân biệt.

Giới hạn tác giả tự nêu: CycleGAN đổi tốt màu sắc và kết cấu (ngựa sang ngựa vằn, ảnh sang tranh Monet), nhưng gần như thất bại khi cần đổi hình dạng, như chó thành mèo.

  • Chất lượng và đa dạng. VAE mờ; GAN nét nhưng dễ sụp mode và khó huấn luyện. Diffusion, ở bài Sinh ảnh (2): Diffusion, Stable Diffusion, LoRA, giữ được ưu điểm của cả hai ở mức đáng kể, và Stable Diffusion lại dùng một autoencoder để nén ảnh.
  • Đánh giá ảnh sinh khó hơn đánh giá bộ phân loại: không có nhãn đúng để so. Bài này chỉ dùng mẹo nhỏ trong ô GAN (đếm chữ số bằng một bộ phân loại).
  • Bài chỉ dùng MLP trên ảnh 8×88 \times 8 cho chạy nhanh trên CPU; mô hình thật dùng mạng tích chập như DCGAN và U-Net.

Hình dưới đặt hai cách sinh ảnh cạnh nhau. Hãy nhìn chỗ z\mathbf{z} đến từ đâu: ở VAE, z\mathbf{z} được rút từ đám mây (μ,σ)(\boldsymbol\mu, \boldsymbol\sigma) do bộ mã hóa trả ra; ở GAN, z\mathbf{z} là nhiễu thuần, và ảnh thật chỉ đi vào DD, không bao giờ đi qua GG.

Bảng dưới so sánh bốn mô hình của bài. Cột “Hàm mất mát” ghi dạng trong bài báo gốc.

Tiêu chíVAEGANPix2PixCycleGAN
Bài báoKingma và Welling, 2014Goodfellow và cộng sự, 2014Isola và cộng sự, 2017Zhu và cộng sự, 2017
Dữ liệu cầnMột tập ảnh, không nhãnMột tập ảnh, không nhãnCặp ảnh (nguồn, đích)Hai tập ảnh của hai miền, không cần cặp
Sinh từz∼N(0,I)\mathbf{z} \sim \mathcal{N}(\mathbf{0}, \mathbf{I})Nhiễu z\mathbf{z}Ảnh nguồnẢnh của miền kia
Hàm mất mát−-ELBO: tái tạo + KLMinimax GG với DD (thường dùng bản không bão hòa)cGAN + λ\lambda L1, λ=100\lambda = 100Hai GAN + λLcyc\lambda \mathcal{L}_{\text{cyc}}, λ=10\lambda = 10
Điểm mạnhHọc ổn định, có bộ mã hóa ảnh → z\mathbf{z}, có cận dưới của likelihoodẢnh sắc nétBám sát ảnh nguồn, cần ít ảnh (vài trăm cặp)Không cần dữ liệu cặp
Điểm yếuẢnh mờKhó huấn luyện, mode collapse, không có bộ mã hóaCần cặp ảnh, đầu ra ít đa dạngKhó đổi hình dạng, huấn luyện bốn mạng

Code trong bài chạy thử lần cuối tháng 11/2026: ô NumPy với Python 3.14 và NumPy 2.4 (bản có trong Pyodide), hai ô PyTorch với Python 3.14, PyTorch 2.15 và scikit-learn 1.9 trên CPU. Ô NumPy chạy ngay trên trang, hai ô PyTorch chạy trên máy bạn. Các ô độc lập, không dùng biến của nhau.

Ô dưới đây lấy một q(z∣x)q(\mathbf{z} \mid \mathbf{x}) hai chiều với μ=(1,−0.5)\boldsymbol\mu = (1, -0.5), σ=(0.5,1.5)\boldsymbol\sigma = (0.5, 1.5). Nó tính KL với N(0,I)\mathcal{N}(\mathbf{0}, \mathbf{I}) bằng công thức dạng đóng, rồi ước lượng lại bằng Monte Carlo: rút z=μ+σ⊙ϵ\mathbf{z} = \boldsymbol\mu + \boldsymbol\sigma \odot \boldsymbol\epsilon, lấy trung bình của log⁡q(z)−log⁡p(z)\log q(\mathbf{z}) - \log p(\mathbf{z}), với số mẫu tăng dần. Cuối cùng, nó ước lượng đạo hàm của KL theo μ\boldsymbol\mu và σ\boldsymbol\sigma qua chính các mẫu z\mathbf{z}, như lan truyền ngược làm.

Mẹo tham số hóa lại và KL: Monte Carlo so với dạng đóng

# Mẹo tham số hóa lại và KL(q || N(0, I)): Monte Carlo so với dạng đóng
import numpy as np
rng = np.random.default_rng(0)
mu = np.array([1.0, -0.5]) # trung bình của q(z | x), 2 chiều
sigma = np.array([0.5, 1.5]) # độ lệch chuẩn của q(z | x)
# KL dạng đóng giữa Gauss chéo và N(0, I)
kl_exact = 0.5 * np.sum(mu**2 + sigma**2 - 1 - np.log(sigma**2))
print(f"closed form KL = {kl_exact:.4f}")
def log_normal(z, m, s):
# log mật độ của Gauss chéo, cộng theo từng chiều
return np.sum(-0.5 * ((z - m) / s) ** 2 - np.log(s)
- 0.5 * np.log(2 * np.pi), axis=1)
print(" n KL (MC) error")
for n in [10, 100, 1_000, 10_000, 100_000]:
eps = rng.standard_normal((n, 2)) # nhiễu không phụ thuộc mu, sigma
z = mu + sigma * eps # mẹo tham số hóa lại
kl_mc = np.mean(log_normal(z, mu, sigma)
- log_normal(z, 0.0, 1.0))
print(f"{n:>6} {kl_mc:8.4f} {kl_mc - kl_exact:+.4f}")
# z có đúng trung bình và độ lệch chuẩn mong muốn
print("mean of z:", np.round(z.mean(axis=0), 3))
print("std of z: ", np.round(z.std(axis=0), 3))
# Đạo hàm đi qua z = mu + sigma * eps (eps giữ cố định):
# viết theo eps, log q chỉ còn -log(sigma) + hằng số,
# còn -log p(z) = z^2 / 2 + hằng số. Đạo hàm từng chiều:
# dKL/dmu = E[z], dKL/dsigma = E[z * eps] - 1 / sigma
grad_mu = z.mean(axis=0)
grad_sigma = (z * eps).mean(axis=0) - 1 / sigma
print("grad mu MC:", np.round(grad_mu, 3), " exact:", mu)
print("grad sigma MC:", np.round(grad_sigma, 3),
" exact:", np.round(sigma - 1 / sigma, 3))

The first run downloads Python, about 10 MB.

Đọc kết quả:

  • closed form KL = 1.1627: chiều thứ nhất góp 12(1+0.25−1+1.386)≈0.818\frac12(1 + 0.25 - 1 + 1.386) \approx 0.818, chiều thứ hai góp 12(0.25+2.25−1−0.811)≈0.344\frac12(0.25 + 2.25 - 1 - 0.811) \approx 0.344.
  • Bảng Monte Carlo: với 10 mẫu, ước lượng là 0.6253, lệch hơn 0.5. Với 1 000 mẫu, sai số còn khoảng 0.02; với 10 000 và 100 000 mẫu, còn dưới 0.005. Sai số của trung bình mẫu giảm cỡ 1/n1/\sqrt{n}, và lên xuống ngẫu nhiên, nên không giảm đều từng dòng. Công thức dạng đóng cho đúng con số mà không tốn mẫu nào: đó là lý do VAE dùng nó cho số hạng KL, chỉ lấy mẫu cho số hạng tái tạo.
  • mean of z, std of z: gần (1,−0.5)(1, -0.5) và (0.5,1.5)(0.5, 1.5). Dời và giãn nhiễu chuẩn cho đúng phân phối qq.
  • Hai dòng grad: đạo hàm theo μ\boldsymbol\mu ước lượng được (1.001,−0.503)(1.001, -0.503), đúng là μ\boldsymbol\mu (đạo hàm của 12μj2\frac12 \mu_j^2); theo σ\boldsymbol\sigma được (−1.497,0.84)(-1.497, 0.84), so với σj−1/σj=(−1.5,0.833)\sigma_j - 1/\sigma_j = (-1.5, 0.833). Gradient đi qua bước lấy mẫu được, nhờ z\mathbf{z} là hàm của μ,σ\boldsymbol\mu, \boldsymbol\sigma khi ϵ\boldsymbol\epsilon đã cố định.

File dưới đây dựng một VAE bằng MLP: bộ mã hóa 64→128→(μ,log⁡σ2)64 \to 128 \to (\boldsymbol\mu, \log \boldsymbol\sigma^2) với J=2J = 2, bộ giải mã 2→128→642 \to 128 \to 64. Điểm ảnh chia cho 16 về [0,1][0, 1], tái tạo dùng cross-entropy nhị phân (bộ giải mã Bernoulli), KL dùng công thức dạng đóng. Huấn luyện 60 epoch, lô 64 ảnh, Adam. Cứ vài epoch, file in hai số hạng (tính trung bình mỗi ảnh); cuối cùng nó rút một z\mathbf{z} từ N(0,I)\mathcal{N}(\mathbf{0}, \mathbf{I}), giải mã, và in ảnh bằng ký tự (từ là trắng tới # là đậm nhất). Cách cài PyTorch bản CPU có ở bài Xây dựng LLM đầu tiên.

Run on your machine

VAE tí hon trên load_digits

Install
pip install torch scikit-learn
Hardware
CPU là đủ, không cần GPU, không tải gì từ mạng. Khoảng 6 giây trên máy phát triển khi rảnh, phần lớn là nạp PyTorch; có thể vài phút khi máy đang bận
# VAE tí hon (MLP) trên ảnh chữ số 8x8 của scikit-learn, chạy CPU
import torch
from torch import nn
from torch.nn import functional as F
from sklearn.datasets import load_digits
torch.manual_seed(0)
X = torch.tensor(load_digits().data / 16.0, dtype=torch.float32)
J = 2 # số chiều của biến ẩn z
class VAE(nn.Module):
def __init__(self):
super().__init__()
self.enc = nn.Sequential(nn.Linear(64, 128), nn.ReLU())
self.mu = nn.Linear(128, J)
self.logvar = nn.Linear(128, J) # học log sigma^2 cho ổn định
self.dec = nn.Sequential(nn.Linear(J, 128), nn.ReLU(),
nn.Linear(128, 64)) # ra logit
def forward(self, x):
h = self.enc(x)
mu, logvar = self.mu(h), self.logvar(h)
eps = torch.randn_like(mu)
z = mu + torch.exp(0.5 * logvar) * eps # tham số hóa lại
return self.dec(z), mu, logvar
model = VAE()
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(1, 61):
tot_rec = tot_kl = 0.0
for idx in torch.randperm(len(X)).split(64):
x = X[idx]
logits, mu, logvar = model(x)
# tái tạo: cross-entropy nhị phân từng điểm ảnh, cộng trên 64 ô
rec = F.binary_cross_entropy_with_logits(
logits, x, reduction="sum") / len(x)
# KL dạng đóng với N(0, I), trung bình trên lô
kl = 0.5 * (mu**2 + logvar.exp() - 1 - logvar).sum() / len(x)
loss = rec + kl # = -ELBO
opt.zero_grad()
loss.backward()
opt.step()
tot_rec += rec.item() * len(x)
tot_kl += kl.item() * len(x)
if epoch in (1, 5, 10, 20, 40, 60):
print(f"epoch {epoch:2d} recon {tot_rec / len(X):6.2f}"
f" KL {tot_kl / len(X):5.2f}")
# Sinh ảnh mới: lấy z từ N(0, I) rồi qua bộ giải mã
with torch.no_grad():
img = torch.sigmoid(model.dec(torch.randn(1, J)))[0].view(8, 8)
for row in img:
print("".join(" .:-=+*#"[int(v * 7.99)] for v in row))

Sample output

From one real run.

epoch 1 recon 38.65 KL 0.33
epoch 5 recon 27.75 KL 0.73
epoch 10 recon 26.45 KL 0.61
epoch 20 recon 24.14 KL 1.55
epoch 40 recon 23.07 KL 2.03
epoch 60 recon 22.74 KL 2.13
.++:
+*=-
.*-.-
.*-:-.
.+--+:
+..+-
+=-*:
.+*=.

Đọc kết quả:

  • Tái tạo giảm, từ 38.65 xuống 22.74. Nó không về 0 được: nhiều điểm ảnh có giá trị xám (ví dụ 0.5), mà cross-entropy nhị phân với đích xám có giá trị nhỏ nhất lớn hơn 0. Hơn nữa, chỉ 2 số z\mathbf{z} phải mang thông tin của 64 điểm ảnh.
  • KL tăng, từ 0.33 lên 2.13. Lúc đầu, bộ giải mã chưa biết dùng z\mathbf{z}, nên chi phí rẻ nhất là để qq gần sát tiên nghiệm (KL nhỏ). Khi học, mô hình chấp nhận trả thêm KL để z\mathbf{z} mang thông tin về ảnh, đổi lại tái tạo tốt hơn nhiều. Đây là hai lực của ELBO giằng co nhau.
  • Ảnh sinh ra có dáng một chữ số (nét vòng, giống số 0 hay 8), nhưng không có ô # nào: các điểm ảnh đều ở mức trung gian. Bộ giải mã trả về xác suất từng điểm ảnh, gần với trung bình của các ảnh ứng với vùng z\mathbf{z} đó, nên nét bị nhòe. Đúng là hiện tượng mờ đã giải thích ở mục Lý thuyết.

Hãy thử đổi J = 2 thành J = 8: tái tạo sẽ tốt hơn, KL lớn hơn.

File thứ hai huấn luyện một GAN bằng MLP trên cùng bộ ảnh, đưa về [−1,1][-1, 1] cho khớp lớp Tanh ở đầu ra của GG. GG: 16→128→6416 \to 128 \to 64, DD: 64→128→164 \to 128 \to 1 (ra logit, LeakyReLU 0.2). Mỗi bước cập nhật DD một lần rồi GG một lần, với loss không bão hòa (nhãn 1 cho ảnh giả ở bước GG). Adam với 0.00020.0002 và β1=0.5\beta_1 = 0.5, như DCGAN. Ở vài epoch, file in loss hai bên của lô cuối, cùng trung bình đầu ra của DD trên ảnh thật (D(real)) và trên ảnh giả (D(fake)) của lô đó. Cuối cùng, nó sinh 1 000 ảnh và nhờ một bộ hồi quy logistic (học trên ảnh thật) đoán xem mỗi ảnh là chữ số nào, để xem GG có sinh đủ mười chữ số không.

Run on your machine

GAN tí hon trên load_digits

Install
pip install torch scikit-learn numpy
Hardware
CPU là đủ, không cần GPU, không tải gì từ mạng. Khoảng 10 giây trên máy phát triển khi rảnh; có thể vài phút khi máy đang bận
# GAN tí hon (MLP) trên ảnh chữ số 8x8, loss không bão hòa, chạy CPU
import numpy as np
import torch
from torch import nn
from torch.nn import functional as F
from sklearn.datasets import load_digits
from sklearn.linear_model import LogisticRegression
torch.manual_seed(0)
digits = load_digits()
X = torch.tensor(digits.data / 8.0 - 1, dtype=torch.float32) # về [-1, 1]
Z = 16 # số chiều của nhiễu z
G = nn.Sequential(nn.Linear(Z, 128), nn.ReLU(),
nn.Linear(128, 64), nn.Tanh()) # bộ sinh
D = nn.Sequential(nn.Linear(64, 128), nn.LeakyReLU(0.2),
nn.Linear(128, 1)) # bộ phân biệt, ra logit
opt_g = torch.optim.Adam(G.parameters(), lr=2e-4, betas=(0.5, 0.999))
opt_d = torch.optim.Adam(D.parameters(), lr=2e-4, betas=(0.5, 0.999))
ones, zeros = torch.ones(64, 1), torch.zeros(64, 1)
for epoch in range(1, 101):
# 28 lô đủ 64 ảnh mỗi epoch (bỏ 5 ảnh dư để lô nào cũng đủ nhãn)
for idx in torch.randperm(len(X))[:28 * 64].view(28, 64):
real, fake = X[idx], G(torch.randn(64, Z))
# Bước D: thật -> nhãn 1, giả -> nhãn 0 (detach: không sửa G)
loss_d = (F.binary_cross_entropy_with_logits(D(real), ones)
+ F.binary_cross_entropy_with_logits(
D(fake.detach()), zeros))
opt_d.zero_grad()
loss_d.backward()
opt_d.step()
# Bước G, không bão hòa: làm D(G(z)) gần 1, tức max log D(G(z))
loss_g = F.binary_cross_entropy_with_logits(D(fake), ones)
opt_g.zero_grad()
loss_g.backward()
opt_g.step()
if epoch in (1, 10, 25, 50, 100):
with torch.no_grad():
p_real = torch.sigmoid(D(real)).mean().item()
p_fake = torch.sigmoid(D(fake)).mean().item()
print(f"epoch {epoch:3d} loss D {loss_d.item():.3f} loss G "
f"{loss_g.item():.3f} D(real) {p_real:.2f}"
f" D(fake) {p_fake:.2f}")
# Ảnh giả thuộc chữ số nào? Hỏi một bộ phân loại học trên ảnh thật
clf = LogisticRegression(max_iter=2000).fit(X.numpy(), digits.target)
with torch.no_grad():
samples = G(torch.randn(1000, Z)).numpy()
print("digits among 1000 fakes:",
np.bincount(clf.predict(samples), minlength=10))

Sample output

From one real run.

epoch 1 loss D 1.229 loss G 0.571 D(real) 0.68 D(fake) 0.57
epoch 10 loss D 1.263 loss G 0.642 D(real) 0.60 D(fake) 0.53
epoch 25 loss D 1.139 loss G 0.791 D(real) 0.61 D(fake) 0.46
epoch 50 loss D 1.295 loss G 0.780 D(real) 0.52 D(fake) 0.46
epoch 100 loss D 1.268 loss G 0.785 D(real) 0.53 D(fake) 0.46
digits among 1000 fakes: [ 77 90 148 115 128 112 81 64 104 81]

Đọc kết quả:

  • D(real) và D(fake) cùng dồn về gần 0.5 (0.53 và 0.46 ở epoch 100): bộ phân biệt không còn tách được ảnh thật với ảnh giả một cách rõ ràng, đúng hướng của điểm cân bằng D∗=1/2D^* = 1/2. Hai số không bằng hẳn 0.5 và còn lên xuống giữa các epoch: hai mạng nhỏ và trò chơi không hội tụ gọn như phân tích lý thuyết.
  • Loss không giảm dần như khi huấn luyện bộ phân loại. Loss DD đi lên xuống quanh 1.1 tới 1.3, gần mức 2ln⁡2≈1.3862\ln 2 \approx 1.386 của một bộ phân biệt đoán 0.5 cho mọi ảnh. Loss GG tăng từ 0.571 lên khoảng 0.78, tức −ln⁡0.46-\ln 0.46: lúc đầu DD còn ngây thơ nên dễ lừa, về sau DD giỏi hơn nên lừa khó hơn. Loss GG tăng ở đây không có nghĩa GG tệ đi.
  • Mười chữ số đều có mặt, mỗi chữ số từ 64 đến 148 trong 1 000 ảnh giả (lý tưởng là khoảng 100 mỗi chữ số). Lần chạy này không bị sụp mode. Phép đếm chỉ kiểm độ đa dạng: bộ phân loại luôn gán một chữ số nào đó, kể cả cho ảnh xấu, nên nó không nói ảnh có đẹp hay không.

Hãy thử bỏ .detach() ở bước DD, hoặc đổi nhãn ones ở bước GG thành loss bão hòa (làm nhỏ −-BCE với nhãn zeros), rồi xem hai số D(fake) và bảng đếm thay đổi thế nào.

Question 1 of 4 Trong VAE, bạn lấy z\mathbf{z} thẳng từ N(μ,diag(σ2))\mathcal{N}(\boldsymbol\mu, \mathrm{diag}(\boldsymbol\sigma^2)) bằng torch.distributions.Normal(mu, sigma).sample(), thay vì viết z=μ+σ⊙ϵ\mathbf{z} = \boldsymbol\mu + \boldsymbol\sigma \odot \boldsymbol\epsilon. Code vẫn chạy. Điều gì hỏng?
Question 2 of 4 Giữ bộ sinh GG cố định. Ở một ảnh x\mathbf{x}, mật độ dữ liệu thật là pdata(x)=0.3p_{\text{data}}(\mathbf{x}) = 0.3 và mật độ ảnh giả là pg(x)=0.1p_g(\mathbf{x}) = 0.1. Bộ phân biệt tối ưu D∗(x)D^*(\mathbf{x}) bằng bao nhiêu?
Question 3 of 4 Đầu quá trình huấn luyện GAN, DD nhận ra ảnh giả dễ dàng: D(G(z))≈0.01D(G(\mathbf{z})) \approx 0.01. Viết D=σ(a)D = \sigma(a) với aa là logit. Vì sao nên cho GG làm lớn log⁡D(G(z))\log D(G(\mathbf{z})) thay vì làm nhỏ log⁡(1−D(G(z)))\log(1 - D(G(\mathbf{z})))?
Question 4 of 4 Bạn có 3 000 ảnh chụp đường phố ban ngày và 2 000 ảnh ban đêm, chụp ở những nơi khác nhau. Bạn muốn biến ảnh ngày thành ảnh đêm. Nên chọn cách nào?

Ý chính:

  • Mô hình sinh học cách lấy mẫu từ phân phối của dữ liệu thật. VAE học bằng cách làm lớn ELBO =Eq[log⁡pθ(x∣z)]−DKL(qϕ(z∣x)∥N(0,I))= \mathbb{E}_q[\log p_\theta(\mathbf{x} \mid \mathbf{z})] - D_{\mathrm{KL}}(q_\phi(\mathbf{z} \mid \mathbf{x}) \parallel \mathcal{N}(\mathbf{0}, \mathbf{I})), một cận dưới của log⁡pθ(x)\log p_\theta(\mathbf{x}); sinh ảnh bằng cách rút z∼N(0,I)\mathbf{z} \sim \mathcal{N}(\mathbf{0}, \mathbf{I}) rồi giải mã.
  • Với Gauss chéo, KL có dạng đóng 12∑j(μj2+σj2−1−log⁡σj2)\frac12 \sum_j (\mu_j^2 + \sigma_j^2 - 1 - \log \sigma_j^2). Mẹo z=μ+σ⊙ϵ\mathbf{z} = \boldsymbol\mu + \boldsymbol\sigma \odot \boldsymbol\epsilon đưa phần ngẫu nhiên ra ngoài để gradient đi qua bước lấy mẫu. Ảnh VAE hay mờ vì bộ giải mã trả về trung bình của các ảnh có thể.
  • GAN cho bộ sinh GG và bộ phân biệt DD chơi minimax. D∗=pdata/(pdata+pg)D^* = p_{\text{data}} / (p_{\text{data}} + p_g), cân bằng khi pg=pdatap_g = p_{\text{data}}. Trong thực tế, GG dùng loss không bão hòa; huấn luyện khó, dễ dao động và sụp mode.
  • Pix2Pix: GAN có điều kiện trên ảnh ghép cặp, cộng λ\lambda L1, bộ sinh U-Net và bộ phân biệt PatchGAN. CycleGAN: hai miền không ghép cặp, hai bộ sinh và mất mát nhất quán chu trình F(G(x))≈xF(G(\mathbf{x})) \approx \mathbf{x}.

Đọc thêm:

Content licensed under CC BY-NC-SA 4.0. Code under the MIT license.About