Sinh ảnh (1): VAE, GAN, CycleGAN, Pix2Pix
Mục tiêu
Phần tiêu đề “Mục tiêu”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ó.
Cần biết trước
Phần tiêu đề “Cần biết trước”- Các kiến trúc CNN hiện đại: LeNet, AlexNet, VGG, GoogLeNet, ResNet, Xception, SENet, YOLO, thuộc phần Học sâu (Đã học)
- Xác suất thống kê, thuộc phần Toán cho AI (Đã học)
Trực giác
Phần tiêu đề “Trực giác”Sinh ảnh là lấy mẫu
Phần tiêu đề “Sinh ảnh là lấy mẫu”Bạn có 1 797 ảnh chữ số viết tay cỡ (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, .
Ta không có công thức của , 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.
Hai ý tưởng
Phần tiêu đề “Hai ý tưởng”Ý 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 ), 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 có trật tự, để rút bừa một 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?
Lý thuyết
Phần tiêu đề “Lý thuyết”Ký hiệu
Phần tiêu đề “Ký hiệu”Ảnh được trải phẳng thành vectơ (với chữ số là 64 số). Biến ẩn có chiều, theo ký hiệu của bài báo VAE. Phân phối tiên nghiệm (prior) của là . Bộ mã hóa có tham số , bộ giải mã có tham số .
Ở phần GAN, là bộ sinh và là bộ phân biệt: một mạng nhận ảnh, trả ra xác suất ảnh đó là thật. Chữ này khác số chiều embedding ở các bài ViT, và khác chữ trong (độ lệch KL).
Mô hình sinh học gì
Phần tiêu đề “Mô hình sinh học gì”Cách quen thuộc để học là làm lớn likelihood của dữ liệu: chọn để 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 , rồi rút . Xác suất của một ảnh là tổng trên mọi cách sinh ra nó:
Tích phân này trên mọi không tính được khi bộ giải mã là mạng nơ-ron. Rút bừa để ước lượng cũng không ổn: gần như mọi rút bừa cho ra ảnh khác hẳn , nên cần vô số mẫu. Ta cần biết nào có khả năng sinh ra . Đó là việc của bộ mã hóa.
VAE: từ autoencoder tới phân phối
Phần tiêu đề “VAE: từ autoencoder tới phân phối”VAE (Kingma và Welling, 2014) sửa autoencoder ở một chỗ: bộ mã hóa không trả về một vectơ , mà trả về một phân phối của . Với mỗi ảnh, bộ mã hóa trả ra hai vectơ và (cùng chiều), và
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 . Hai lực này làm vùng liền mạch: không còn lỗ hổng giữa các ảnh, nên rút từ 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.
Dẫn ELBO từng bước
Phần tiêu đề “Dẫn ELBO từng bước”Bước 1: viết thành một kỳ vọng. không phụ thuộc , nên lấy kỳ vọng của nó theo bất kỳ phân phối nào của vẫn được chính nó. Chọn phân phối :
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: . Chia cho , ta được . Sau đó nhân cả tử và mẫu với cùng một số (không đổi giá trị), rồi tách log của tích thành tổng hai log:
Ở đây viết tắt cho .
Bước 3: nhận ra độ lệch KL. Số hạng thứ hai đúng là định nghĩa của , 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 . Ta gọi nó là ELBO:
Khoảng cách giữa hai vế đúng bằng độ lệch KL vừa bỏ: càng gần phân phối thật , cận càng sát. Vì thế làm lớn ELBO vừa đẩy lên, vừa dạy bộ mã hóa đoán đúng nào sinh ra . Chữ “evidence” (bằng chứng) trong tên là .
Bước 4: tách thành tái tạo trừ KL. Viết lại (sinh trước, rồi ), rồi tách log thành hai phần:
Dòng thứ hai đổi dấu vì . 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 từ rút trong đám mây của . Số hạng (2) là KL kéo về tiên nghiệm: nó nhỏ khi đám mây gần . Đây là hai lực đã nói ở trên. Khi huấn luyện, ta làm nhỏ , lấy trung bình trên các ảnh của một lô.
KL dạng đóng với Gauss chéo
Phần tiêu đề “KL dạng đóng với Gauss chéo”Cả và đề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 độ lệch KL một chiều. Xét một chiều, , . Log mật độ của hai phân phối là:
Trừ hai dòng, hằng số triệt tiêu. Lấy kỳ vọng theo , cần hai kết quả: (định nghĩa phương sai) và (phương sai cộng bình phương trung bình). Thay vào:
Dòng cuối gom các số hạng và viết . Cộng trên chiều:
Kiểm nhanh: với , , mỗi số hạng bằng , đú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 thay vì , để luôn dương mà không cần ràng buộc.
Mẹo tham số hóa lại
Phần tiêu đề “Mẹo tham số hóa lại”Số hạng tái tạo là một kỳ vọng theo , nên ta ước lượng nó bằng cách rút (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 và . Nhưng “rút một số ngẫu nhiên từ ” 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 , một nhiễu không phụ thuộc tham số nào, rồi tính
với là nhân từng phần tử. Dời một biến chuẩn đi và giãn ra lần thì được đúng , nên vẫn có phân phối mong muốn. Nhưng giờ, với đã rút xong, là một hàm bình thường của và : , . Gradient chảy từ bộ giải mã, qua , 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 . Coi mỗi điểm ảnh (giá trị trong ) là một biến Bernoulli thì 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.
Vì sao ảnh VAE hay mờ
Phần tiêu đề “Vì sao ảnh VAE hay mờ”Bộ giải mã Bernoulli hay Gauss ở trên coi các điểm ảnh độc lập khi đã biết . Giả sử cùng một vùng ứ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 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: trò chơi hai người
Phần tiêu đề “GAN: trò chơi hai người”GAN (Goodfellow và cộng sự, 2014) bỏ hẳn likelihood. Bộ sinh biến nhiễu thành ảnh ; phân phối của các ảnh này gọi là . Bộ phân biệt nhận một ảnh và trả ra , xác suất nó cho là ảnh thật. Hai mạng chơi một trò chơi minimax:
Đọc từng phần: muốn lớn, tức gần 1 với ảnh thật và 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. chỉ tác động lên số hạng thứ hai và muốn nó nhỏ, tức muốn bị lừa.
Bộ phân biệt tối ưu. Giữ cố định. Viết kỳ vọng thành tích phân theo (ảnh giả có phân phối ):
được chọn tự do ở từng , nên ta làm lớn biểu thức trong ngoặc tại từng điểm. Với , , hàm có đạo hàm , bằng 0 khi . Vậy:
Ý nghĩa: ở chỗ ảnh thật dày hơn ảnh giả, nghiêng về “thật”. Bài báo GAN thay trở lại và chứng minh giá trị nhỏ nhất theo là , đạt khi và chỉ khi . Lúc đó ở 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ở, nhận ra dễ dàng: . Viết với là logit (đầu ra trước sigmoid). Đạo hàm theo :
Khi , đạo hàm thứ nhất gần 0: hàm bão hòa (saturate), 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 làm lớn thay vì làm nhỏ . 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 (bài báo gốc dùng một bước), rồi một bước cập nhật . Khi cập nhật , ảnh giả được tách khỏi đồ thị tính toán để không sửa nhầm .
Vì sao GAN khó huấn luyện
Phần tiêu đề “Vì sao GAN khó huấn luyện”- 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 tăng có thể vì vừa giỏi lên, không phải vì tệ đi. Phân tích ở trên giả định 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). tìm ra vài ảnh lừa được 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 quá nhiều mà không cập nhật . Ô 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 (ở ) và tích chập chuyển vị (ở ), dùng batch normalization ở cả hai mạng, bỏ các lớp kết nối đầy đủ ẩn, dùng ReLU và Tanh ở lớp ra, dùng LeakyReLU. Họ dùng Adam với tốc độ học và ; ô 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ơ 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, là ảnh nguồn và 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 .
Đây là một GAN có điều kiện (conditional GAN): cả và đều được xem ảnh nguồn. 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”:
Vì có đáp án , 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:
Tác giả dùng . Chia việc rõ ràng: L1 lo phần tổng thể (màu, bố cục đúng chỗ), 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ứ sang lớp thứ , để 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: chấm “thật hay giả” cho từng mảnh của ảnh rồi lấy trung bình, thay vì một điểm cho cả ảnh. Mảnh 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 học cách bỏ qua đư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.
CycleGAN: không cần cặp
Phần tiêu đề “CycleGAN: không cần cặp”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 (ví dụ ảnh ngựa) và miền (ảnh ngựa vằn).
Chỉ dùng GAN thì chưa đủ. Một bộ sinh 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; 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 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):
Muốn quay về được , ảnh phải giữ đủ thông tin về : tư thế, nền, bố cục. Hàm mục tiêu đầy đủ có hai trò chơi GAN ( với , với ) cộng , với 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 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.
Giới hạn
Phần tiêu đề “Giới hạn”- 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 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.
Minh họa
Phần tiêu đề “Minh họa”Hình dưới đặt hai cách sinh ảnh cạnh nhau. Hãy nhìn chỗ đến từ đâu: ở VAE, được rút từ đám mây do bộ mã hóa trả ra; ở GAN, là nhiễu thuần, và ảnh thật chỉ đi vào , không bao giờ đi qua .
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í | VAE | GAN | Pix2Pix | CycleGAN |
|---|---|---|---|---|
| Bài báo | Kingma và Welling, 2014 | Goodfellow và cộng sự, 2014 | Isola và cộng sự, 2017 | Zhu và cộng sự, 2017 |
| Dữ liệu cần | Một tập ảnh, không nhãn | Một tập ảnh, không nhãn | Cặp ảnh (nguồn, đích) | Hai tập ảnh của hai miền, không cần cặp |
| Sinh từ | Nhiễu | Ảnh nguồn | Ảnh của miền kia | |
| Hàm mất mát | ELBO: tái tạo + KL | Minimax với (thường dùng bản không bão hòa) | cGAN + L1, | Hai GAN + , |
| Điểm mạnh | Học ổn định, có bộ mã hóa ảnh → , có cận dưới của likelihood | Ảnh sắc nét | Bá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óa | Cần cặp ảnh, đầu ra ít đa dạng | Khó đổi hình dạng, huấn luyện bốn mạng |
Ví dụ code
Phần tiêu đề “Ví dụ code”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.
Tham số hóa lại và KL bằng NumPy
Phần tiêu đề “Tham số hóa lại và KL bằng NumPy”Ô dưới đây lấy một hai chiều với , . Nó tính KL với bằng công thức dạng đóng, rồi ước lượng lại bằng Monte Carlo: rút , lấy trung bình của , với số mẫu tăng dần. Cuối cùng, nó ước lượng đạo hàm của KL theo và qua chính các mẫu , 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 đóngimport 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ềusigma = 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ốnprint("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 / sigmagrad_mu = z.mean(axis=0)grad_sigma = (z * eps).mean(axis=0) - 1 / sigmaprint("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))Lần chạy đầu cần tải Python, khoảng 10 MB.
Đọc kết quả:
closed form KL = 1.1627: chiều thứ nhất góp , chiều thứ hai góp .- 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ỡ , 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 và . Dời và giãn nhiễu chuẩn cho đúng phân phối .- Hai dòng
grad: đạo hàm theo ước lượng được , đúng là (đạo hàm của ); theo được , so với . Gradient đi qua bước lấy mẫu được, nhờ là hàm của khi đã cố định.
VAE tí hon trên chữ số
Phần tiêu đề “VAE tí hon trên chữ số 8×88 \times 88×8”File dưới đây dựng một VAE bằng MLP: bộ mã hóa với , bộ giải mã . Điểm ảnh chia cho 16 về , 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 từ , 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.
Chạy trên máy bạn
VAE tí hon trên load_digits
- Cần cài
pip install torch scikit-learn- Phần cứng
- 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 CPUimport torchfrom torch import nnfrom torch.nn import functional as Ffrom 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))Kết quả mẫu
Lấy từ một lần chạy thật.
epoch 1 recon 38.65 KL 0.33epoch 5 recon 27.75 KL 0.73epoch 10 recon 26.45 KL 0.61epoch 20 recon 24.14 KL 1.55epoch 40 recon 23.07 KL 2.03epoch 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ố 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 , nên chi phí rẻ nhất là để gần sát tiên nghiệm (KL nhỏ). Khi học, mô hình chấp nhận trả thêm KL để 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 đó, 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.
GAN tí hon trên chữ số
Phần tiêu đề “GAN tí hon trên chữ số 8×88 \times 88×8”File thứ hai huấn luyện một GAN bằng MLP trên cùng bộ ảnh, đưa về cho khớp lớp Tanh ở đầu ra của . : , : (ra logit, LeakyReLU 0.2). Mỗi bước cập nhật một lần rồi một lần, với loss không bão hòa (nhãn 1 cho ảnh giả ở bước ). Adam với và , 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 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 có sinh đủ mười chữ số không.
Chạy trên máy bạn
GAN tí hon trên load_digits
- Cần cài
pip install torch scikit-learn numpy- Phần cứng
- 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 CPUimport numpy as npimport torchfrom torch import nnfrom torch.nn import functional as Ffrom sklearn.datasets import load_digitsfrom 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ộ sinhD = nn.Sequential(nn.Linear(64, 128), nn.LeakyReLU(0.2), nn.Linear(128, 1)) # bộ phân biệt, ra logitopt_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ậtclf = 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))Kết quả mẫu
Lấy từ một lần chạy thật.
epoch 1 loss D 1.229 loss G 0.571 D(real) 0.68 D(fake) 0.57epoch 10 loss D 1.263 loss G 0.642 D(real) 0.60 D(fake) 0.53epoch 25 loss D 1.139 loss G 0.791 D(real) 0.61 D(fake) 0.46epoch 50 loss D 1.295 loss G 0.780 D(real) 0.52 D(fake) 0.46epoch 100 loss D 1.268 loss G 0.785 D(real) 0.53 D(fake) 0.46digits 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 . 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 đi lên xuống quanh 1.1 tới 1.3, gần mức của một bộ phân biệt đoán 0.5 cho mọi ảnh. Loss tăng từ 0.571 lên khoảng 0.78, tức : lúc đầu còn ngây thơ nên dễ lừa, về sau giỏi hơn nên lừa khó hơn. Loss tăng ở đây không có nghĩa 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 , hoặc đổi nhãn ones ở bước 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.
Lỗi hay gặp
Phần tiêu đề “Lỗi hay gặp”Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Tóm tắt và đọc thêm
Phần tiêu đề “Tóm tắt và đọc thêm”Ý 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 , một cận dưới của ; sinh ảnh bằng cách rút rồi giải mã.
- Với Gauss chéo, KL có dạng đóng . Mẹo đư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 và bộ phân biệt chơi minimax. , cân bằng khi . Trong thực tế, 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 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 .
Đọc thêm:
- Kingma, D. P., Welling, M. (2014). Auto-Encoding Variational Bayes, arXiv:1312.6114, mục 2.2–2.4, mục 3 và phụ lục B. Ngắn; có cận dưới, mẹo tham số hóa lại và cách tính KL dạng đóng. Bài báo dùng cho ELBO (đại lượng cần làm lớn), còn bài này dùng để khỏi lẫn với hàm mất mát.
- Kingma, D. P., Welling, M. (2019). An Introduction to Variational Autoencoders, arXiv:1906.02691, chương 2. Viết lại VAE chậm rãi, có nhiều hình và giải thích hơn bài báo gốc.
- Goodfellow, I. và cộng sự (2014). Generative Adversarial Networks, arXiv:1406.2661, mục 3, 4 và 6. Trò chơi minimax, đoạn về loss không bão hòa, chứng minh và điểm cân bằng .
- Radford, A., Metz, L., Chintala, S. (2016). Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks, arXiv:1511.06434, mục 3. Bộ quy tắc kiến trúc của DCGAN.
- Isola, P., Zhu, J.-Y., Zhou, T., Efros, A. A. (2017). Image-to-Image Translation with Conditional Adversarial Networks, arXiv:1611.07004, mục 3; và Zhu, J.-Y., Park, T., Isola, P., Efros, A. A. (2017). Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks, arXiv:1703.10593, mục 3 và 6. Hàm mục tiêu của Pix2Pix và CycleGAN, cùng các ca thất bại của CycleGAN.
Trình duyệt đang chặn lưu dữ liệu. Tiến độ sẽ mất khi bạn tải lại trang.
Nội dung theo giấy phép CC BY-NC-SA 4.0. Mã nguồn theo giấy phép MIT.Giới thiệu