這是我在國立東華大學資訊工程學系「深度學習基石與實務」課程的第二個作業。作業表面上是在 CelebA 上訓練 GAN、生成一批人臉,實際上卻把三件事綁在一起:生成影像、建立可追蹤的 synthetic dataset,以及訓練一個能分辨真假的模型。
我一開始把問題想得很直覺:生成器越逼真,成果就越好。真正跑過幾輪模型之後,才發現事情沒有這麼單純。生成器越接近真實資料,判別器越難找到可以泛化的線索;判別器若只記住資料前處理或某一個 checkpoint 的風格,漂亮的 accuracy 也沒有太大意義。這篇筆記保留模型失敗、中間 epoch、loss 曲線與評估圖,記錄我如何在清晰度、多樣性、穩定性和運算成本之間做取捨。
Purpose:這份作業在問什麼
這份作業的目標不是只交出一張「看起來像人臉」的圖片,而是一條可以被檢查、被重跑的影像生成與分類流程:
- 在 CelebA 上訓練 LadaGAN,產生有多樣性的 64×64 人臉。
- 保留不同 epoch 的 checkpoint,從每個 checkpoint 生成固定數量的圖片,建立足夠大的 synthetic dataset。
- 把真實圖片和生成圖片混合,訓練多個分類 backbone,找出比較能辨識兩種影像分布的判別器。
- 使用最後的 EfficientNetV2-B1 對測試資料做真偽判斷,輸出 test.csv。
所以我想回答的問題不是「能不能生成一張好看的臉」,而是:
當生成器已經做得很逼真時,判別器到底還能不能學到穩定、可泛化的差異?
作業流程:從生成到判別
HackMD 報告把整個工作拆成三個階段。第一階段訓練 LadaGAN;第二階段從生成 checkpoint 建立判別器資料;第三階段用教師提供的 1,000 張測試圖片產生最後的分類結果。
64x64"] --> B["LadaGAN training
36 epochs"] B --> C["Checkpoints
epoch 1-30"] C --> D["150K synthetic faces"] A --> E["Real images"] D --> F["300K discriminator set"] E --> F F --> G["EfficientNetV2-B1
240x240"] G --> H["Real / Fake prediction"] H --> I["test.csv
1,000 images"]
三個階段的實際數字
第一階段以 64×64、像素值正規化到 [-1, 1] 的 CelebA 人臉訓練 LadaGAN。訓練曲線記錄到第 36 個 epoch;生成資料的腳本則以第 1 到第 30 個 checkpoint、每個 checkpoint 5,000 張圖片為主要設定:
30 checkpoints × 5,000 images = 150,000 synthetic faces
第二階段再把 150,000 張生成圖片和 202,600 張真實圖片按報告中的比例抽樣,組成 300,000 張判別器資料。報告中提到的分布是 127,650 張 fake、172,350 張 real;這個比例比單純各半抽樣更接近原始資料配置。
第三階段是教師提供的 1,000 張測試圖片,模型輸出 test.csv。這 1,000 張資料是最後要預測的測試集,不能和前面的 30,000 張評估資料混稱。
報告中的流程圖有一處把生成總數標成 180,000,但 generate_image.py 的設定與後續資料描述是 30 個 checkpoint、每個 5,000 張,也就是 150,000 張。本文以實作段落的 150,000 為主,並保留這個差異,避免把兩段數字拼成一個看似精確、其實無法追溯的結論。
LadaGAN 的架構與訓練方式
LadaGAN 的生成器不是單純把卷積層一路堆高,而是把 Transformer-style attention、residual connection 和逐步上採樣放在同一個生成流程裡。報告中記錄的主要設定是:
| 元件 | 設定 |
|---|---|
| latent noise | 128 維 |
| generator output | 64×64 RGB |
| generator model dimensions | [1024, 256, 64] |
| attention heads | [4, 4, 4] |
| MLP dimensions | [512, 512, 512] |
| discriminator input | 64×64 影像 patch embedding |
| discriminator patch size | 4 |
我把訓練流程理解成兩個互相拉扯的目標:
- 生成器要讓 fake image 通過判別器,同時維持 feature matching,避免只追求某一種容易成功的臉型。
- 判別器要同時看 real 和 fake,學會把生成影像的細微分布差異轉成可用的 binary signal。
報告中的生成器 loss 可以概括成:
generator_loss = adversarial_loss + 10 × feature_matching_loss
判別器則使用 real/fake 的 sigmoid cross entropy。這個 10 不是裝飾性超參數:它直接改變了生成器在「騙過判別器」和「維持特徵分布」之間的重心。
先看失敗:模型演進的四個階段
我沒有把最終的 LadaGAN 當成一開始就知道的答案。實驗中比較過的模型,剛好把 GAN 常見的問題逐一暴露出來:
| 階段 | 得到的改善 | 暴露出的問題 |
|---|---|---|
| DCGAN | 建立最基本的生成流程 | 圖片較模糊、局部紋理不足,容易出現 mode collapse |
| DCGAN + Self-Attention | 更早捕捉全局關係,細節變清楚 | 模式集中更嚴重,生成結果可能突然崩潰 |
| WGAN-GP | 多樣性與訓練穩定度改善 | gradient penalty 加上多次 critic update,速度約慢 3–4 倍,細節反而變糊 |
| LadaGAN | 以 Linear Additive-Attention 取得清晰度、多樣性和效率的折衷 | 圖片太像真的之後,判別器更難找到穩定的泛化線索 |
以下是報告中各階段保留下來的實際 sample grid。epoch 數字是各階段自己的訓練進度,不能直接拿來互相比較;它們的用途是呈現每種架構在當時暴露出什麼問題。







為什麼 attention 不是萬靈丹
Self-Attention 讓模型可以在更大的範圍內交換資訊,這確實有助於眼睛、鼻子、臉型等區域形成比較一致的關係。但它也會把「一組已經成功的特徵組合」快速傳播到更多位置。當梯度主要鼓勵某幾種臉型時,attention 可能讓這些成功模式被反覆複製,清晰度提高的同時,多樣性反而下降。
WGAN-GP 解決的是另一個問題:讓 critic 的學習訊號更平滑,降低 mode collapse 的風險。但每次 generator update 前需要多次 critic update,再加上 gradient penalty,訓練成本明顯增加。這就是為什麼我最後沒有只看「哪一個模型的理論保證比較漂亮」,而是回到這份作業要交付的完整 pipeline,選擇成本可接受、輸出也足夠清楚的 LadaGAN。
36 個 epoch 的 loss:生成器變強,也讓判別器更難工作
下面是 HackMD 報告中的原始 loss 與 score 圖。左側是 generator/discriminator loss,右側是 real/fake score;圖上的 fake score 是判別器記錄的 raw score,不是 sigmoid 後的機率。

報告把曲線分成四個階段,這樣比只看最後一個數字更能看出訓練發生了什麼:
| 訓練階段 | Generator loss | Discriminator loss | Real score | Fake score |
|---|---|---|---|---|
| epoch 0 → 5 | 3.74 → 0.98 | 1.55 → 1.17 | 0.26 → 0.38 | -3.71 → -0.44 |
| epoch 6 → 15 | 0.98 → 0.95 | 1.17 → 1.18 | 0.38 → 0.35 | -0.44 → -0.38 |
| epoch 16 → 25 | 0.95 → 0.94 | 1.18 → 1.19 | 0.35 → 0.34 | -0.38 → -0.36 |
| epoch 26 → 36 | 0.94 → 0.94 | 1.19 → 1.19 | 0.34 → 0.34 | -0.36 → -0.36 |
最後一個記錄點是:
- generator loss:0.936
- discriminator loss:1.185
- real score:0.339
- fake score:-0.358
我對這張圖的解讀是:前五個 epoch 是主要的快速學習期,生成器 loss 大幅下降,real/fake score 也快速靠近相對穩定的區間;第 6 個 epoch 之後,兩個 loss 幾乎進入平台期。這不代表生成器「停止變好」,而是代表後面的改善需要從圖片品質和多樣性觀察,不能只靠 loss 繼續下降來判斷。
把每個 checkpoint 都留下來
這一組圖是報告中不同 epoch 的實際生成結果。每張大圖是 contact sheet,裡面包含同一個 checkpoint 產生的多張人臉;我保留 epoch 1、2、5、10、15、20、25、30、36,讓讀者可以沿著同一條時間軸看品質與多樣性如何變化。









我在這裡特別不挑一張最漂亮的臉當 hero image。對 GAN 來說,整個 grid 更重要:它能讓人看見是否只剩一兩種臉型、背景是否反覆出現、細節變清楚時多樣性有沒有同步保留。
判別器:從接近 coin flip 到 EfficientNetV2-B1
生成器完成之後,真正的第二個實驗才開始。判別器資料不是只拿一張 real 和一張 fake,而是把不同 checkpoint 的生成結果集中起來,再和真實資料混合。這個設計讓判別器面對的是一個比較寬的 fake distribution,而不是某一個 epoch 的單一風格。
Backbone 比較
初期比較 EfficientNet B0–B5 和 MobileNetV3-Large 時,結果一度接近 coin flip。報告把這段失敗保留下來,因為它提醒我:模型名稱更大,不代表在目前的輸入尺寸、資料比例和 preprocessing 下就一定更會分類。
| Backbone | 報告中的觀察 | 我怎麼使用這個結果 |
|---|---|---|
| EfficientNet B0–B5 | validation 約 0.48–0.53 | 視為初期 baseline,沒有直接沿用 |
| MobileNetV3-Large | 從頭訓練約 0.76 | 有改善,但仍不足以支撐最後判斷 |
| EfficientNetV2-B1 | README 摘要約 90% validation;HackMD 的 30,000 張評估接近 99.907% | 作為最後的判別器 |
EfficientNetV2-B1 的設定
最後使用 ImageNet 預訓練的 EfficientNetV2-B1,輸入尺寸調整為 240×240×3。backbone 大部分先 freeze,只解凍最後 20 層做微調;分類 head 則是:
GlobalAveragePooling2D → Dropout(0.3) → Dense(256, relu) → BatchNorm → Dropout(0.5) → Dense(1, sigmoid)
訓練時使用 mixed precision、AdamW(learning rate 1e-4、weight decay 1e-5),並搭配 EarlyStopping、ReduceLROnPlateau 和 ModelCheckpoint。這裡的重點不是把網路堆到最大,而是讓輸入尺度、資料數量和微調範圍一起對齊。

這張圖比單一 validation accuracy 多提供了三種線索:
- loss 是否一起下降,還是只有 accuracy 看起來變好。
- train/validation 的差距是否逐步放大,作為 overfit 警訊。
- learning rate 調整後,模型是否還能繼續改善。
30,000 張評估資料的結果

依照 HackMD 評估圖中的 confusion matrix:
- 12,755 張 fake 被正確判為 fake。
- 0 張 fake 被誤判為 real。
- 28 張 real 被誤判為 fake。
- 17,217 張 real 被正確判為 real。
- 總數 30,000 張,正確 29,972 張,計算為約 99.907%。
圖中另外列出 balanced accuracy 0.9992、sensitivity 0.9984、specificity 1.0000,ROC AUC 和 PR AUC 都是 1.0。這些結果很亮眼,但不能只用一句「判別器很強」帶過:它們也可能表示 fake/real 的資料處理流程仍留下了某些穩定的 domain cue,值得在下一輪實驗中用跨來源資料或更嚴格的 split 驗證。
我刻意保留的數據界線
這份專案的 README 和 HackMD 報告有不同層次的結果摘要:
- README 寫的是約 90% validation accuracy。
- HackMD 另外記錄 30,000 張評估資料中 28 張誤判,也就是約 99.907%。
- 作業最後還有 1,000 張教師測試圖片,輸出 test.csv;沒有可對照的公開標籤時,不能把它宣稱成同樣的 accuracy。
我把這三個數字分開寫,因為它們很可能對應不同的 split、不同階段或不同評估設定。除非補上 manifest、切分方式與每個數字的 checkpoint,否則把它們合併成一個「最後準確率」會讓文章看起來完整,卻無法真正重現。
工程問題:150K 張圖不是按一下就完成
GPU 記憶體
一次把 150,000 張圖片全部放進記憶體,生成階段很容易遇到 GPU memory overflow。我最後採用 batch generation,定期清理 TensorFlow session,並在適合的地方使用 mixed precision。這些做法不改變模型概念,但決定了實驗能不能跑完。
訓練環境
主要環境是 autoDL 上的 RTX 4090 24GB、Ubuntu 20.04、Python 3.9.16、TensorFlow 2.13.1、PyTorch 2.0.1、CUDA 11.8、cuDNN 8.6;另外在 Ubuntu 22.04、Python 3.10.12 的本地環境做驗證。
HackMD 附錄另出現 PyTorch 2.7.1 和 120GB RAM 的紀錄,我把它視為附錄中的環境快照,而不是把不同段落拼成一套「唯一正確」的環境。若要重新跑,我會把版本、資料 manifest、checkpoint 命名和 random seed 鎖在同一份設定檔。
分類不應只看模型大小
初期模型接近 coin flip 時,我沒有直接換成更大的 backbone,而是一起檢查:
- fake/real 的數量比例與 label 產生腳本。
- 生成圖片與真實圖片的 resize、normalize 和色彩格式。
- EfficientNet 的輸入尺寸是否適合目前的資訊量。
- validation split 是否把同一個 checkpoint 的風格洩漏到 train 和 validation。
- classification head 是否太容易記住背景、壓縮痕跡或前處理差異。
這些檢查比單純把 B0 換成 B5 更接近問題本身。
報告中的主要取捨
HackMD 將 LadaGAN 的 FID 記錄為 1.81,並以 SOTA-level 描述;報告也記錄約 19M 參數、單張 RTX 4090/3080Ti 上 30 epochs 小於 5 小時。因為這些數字缺少完整的 benchmark 設定、資料版本和對照組,我在這篇文章把它們標成「報告中的結果」,不把 FID 1.81 直接宣稱成可以和所有外部論文公平比較的結論。
這次最重要的取捨可以濃縮成下面這張表:
| 方案 | 清晰度 | 多樣性 | collapse | 成本 | 整體判斷 |
|---|---|---|---|---|---|
| DCGAN + Attention | 高 | 低 | 嚴重 | 1× | 細節好看,但不夠可靠 |
| WGAN-GP | 中 | 高 | 明顯改善 | 約 3–4× | 穩定,但速度與細節成本高 |
| LadaGAN | 高 | 高 | 輕微 | 中等、報告記錄小於 2× | 最符合這次作業的交付條件 |
我從這份作業真正學到的事
第一,生成模型的品質不是單一排行榜。清晰度、多樣性、訓練穩定性和生成成本必須放在同一張表上看;只挑最漂亮的 sample,會忽略 mode collapse 和不可重現性。
第二,判別器的 accuracy 必須連同資料切分一起解讀。當 fake image 來自多個 checkpoint,模型可能學到 checkpoint 的壓縮痕跡、背景或 resize 差異,而不是真正的「生成影像特徵」。這也是為什麼我把 90% validation、30,000 張評估和 1,000 張教師測試資料分開記錄。
第三,失敗的模型不是多餘的歷史。Attention-DCGAN 讓我看到清晰度和多樣性的衝突;WGAN-GP 讓我看到穩定性有實際的運算成本;最後選 LadaGAN,是因為它在這個作業需要的 pipeline 中取得比較合理的平衡,而不是因為它在所有指標都絕對最好。
如果再做一次
下一輪我會先補上資料治理,再追求更高的指標:
- 為每張圖片保存 source_type、source_epoch、checkpoint、seed 和 preprocessing 版本。
- 明確分開 train、validation、hold-out test,避免同一個 checkpoint 的風格同時出現在不同 split。
- 對每個 epoch 做固定 seed 的 sample grid,並加上 FID、KID、precision/recall for generative models。
- 讓判別器在跨 checkpoint、跨來源與不同壓縮設定的資料上測試,確認它學到的是生成分布,而不是資料管線的指紋。
- 用同一份 environment lock 和命名規則重跑全部 pipeline,讓別人可以從 train_celeba.py、generate_image.py、LabelData.py、efficientnetv2.py 到 test_face.py 逐段核對。