Sinh ảnh (2): Diffusion, Stable Diffusion, LoRA
This content is not available in your language yet.
Mục tiêu
Phần tiêu đề “Mục tiêu”Sau bài này, bạn sẽ:
- Hiểu ý tưởng của mô hình khuếch tán (diffusion model): làm hỏng ảnh bằng nhiễu từng chút một, rồi dạy một mạng gỡ nhiễu ngược lại.
- Tự dẫn được dạng đóng của DDPM, và hiểu vì sao mạng chỉ cần dự đoán nhiễu bằng một hàm mất mát MSE.
- Giải thích được ba mảnh của Stable Diffusion: latent diffusion (khử nhiễu trong không gian nén của VAE), điều kiện văn bản qua cross-attention, và classifier-free guidance.
- Đếm được số tham số khi gắn LoRA vào attention của U-Net, và cài được một DDPM tí hon bằng PyTorch.
Cần biết trước
Phần tiêu đề “Cần biết trước”- Image Generation (1): VAE, GAN, CycleGAN, Pix2Pix, in Computer Vision(Not translated yet) (Learned)
Trực giác
Phần tiêu đề “Trực giác”Nhỏ một giọt mực vào cốc nước
Phần tiêu đề “Nhỏ một giọt mực vào cốc nước”Nhỏ một giọt mực vào cốc nước. Lúc đầu bạn còn thấy rõ hình giọt mực. Vài giây sau nó loang ra. Vài phút sau, cả cốc chỉ còn một màu xám đều. Hiện tượng đó gọi là khuếch tán (diffusion), và nó chỉ đi một chiều: không ai thấy cốc nước xám tự gom lại thành giọt mực.
Mô hình khuếch tán làm điều tương tự với ảnh. Lấy một ảnh, cộng thêm một chút nhiễu ngẫu nhiên. Lặp lại vài trăm lần. Cuối cùng ảnh chỉ còn là nhiễu thuần, không ai đoán được ảnh gốc.
Phần thú vị là đi ngược lại. Mỗi bước thêm nhiễu chỉ làm hỏng ảnh một chút. Nhìn một ảnh hơi nhiễu, việc đoán “phần nào là nhiễu vừa thêm vào” không quá khó. Nếu dạy được một mạng làm việc nhỏ đó, ta cho nó chạy ngược từ nhiễu thuần: mỗi bước gỡ một chút nhiễu, và sau đủ số bước, một ảnh mới hiện ra.
Vì sao không sinh ảnh trong một bước?
Phần tiêu đề “Vì sao không sinh ảnh trong một bước?”GAN và VAE sinh ảnh trong một lần chạy mạng: từ ra thẳng ảnh. Đó là một bước nhảy rất xa, từ nhiễu tới một ảnh hoàn chỉnh. GAN học bước nhảy đó bằng một cuộc đấu khó giữ cân bằng; VAE học bằng một hàm mất mát tái tạo nên ảnh dễ bị mờ.
Diffusion chia bước nhảy xa thành nghìn bước ngắn. Mỗi bước là một bài toán hồi quy bình thường: nhìn ảnh nhiễu, đoán nhiễu. Huấn luyện ổn định như học một mạng hồi quy, không có cuộc đấu nào. Đổi lại, sinh một ảnh phải chạy mạng nhiều lần.
Từ chữ số tới “phi hành gia cưỡi ngựa”
Phần tiêu đề “Từ chữ số tới “phi hành gia cưỡi ngựa””Trên chữ số , ý tưởng này chạy được ngay. Nhưng ảnh màu có gần 800 nghìn số, và chạy mạng nghìn lần trên ngần ấy số rất tốn. Ta còn muốn điều khiển ảnh bằng một câu lệnh, như “một phi hành gia cưỡi ngựa”. Và muốn dạy mô hình một phong cách mới mà không huấn luyện lại gần một tỉ tham số.
Ba mong muốn đó dẫn tới ba mảnh của Stable Diffusion trong bài: khử nhiễu trong không gian nén, điều kiện bằng chữ, và LoRA.
Bài toán của bài này: làm sao dạy một mạng đảo ngược quá trình thêm nhiễu, để đi từ nhiễu thuần tới một ảnh mới; làm việc đó thế nào cho rẻ trên ảnh lớn, theo đúng một câu lệnh, và tinh chỉnh với rất ít tham số?
Lý thuyết
Phần tiêu đề “Lý thuyết”Ký hiệu
Phần tiêu đề “Ký hiệu”Ảnh gốc là , ảnh sau bước thêm nhiễu là , với . Ở bài này là số bước khuếch tán, không phải nhiệt độ khi lấy mẫu token ở phần LLM. Ở mỗi bước có một số nhỏ gọi là phương sai nhiễu của bước đó; dãy gọi là lịch nhiễu (noise schedule). Ta đặt thêm:
là nhiễu Gauss chuẩn cùng cỡ với ảnh: mỗi phần tử độc lập, trung bình 0, phương sai 1. DDPM (Ho, Jain, Abbeel, 2020) đưa điểm ảnh về đoạn trước, để ảnh và nhiễu có thang đo tương đương.
Quá trình thuận: thêm nhiễu từng bước
Phần tiêu đề “Quá trình thuận: thêm nhiễu từng bước”Một bước thêm nhiễu của DDPM là:
trong đó là một nhiễu chuẩn mới, độc lập với mọi bước trước. Đây là quá trình thuận (forward process). Nó không có gì để học: chỉ là công thức cố định.
Vì sao nhân ảnh với chứ không giữ nguyên? Nếu mỗi phần tử của có phương sai 1, thì phần ảnh đóng góp phương sai , phần nhiễu đóng góp , cộng lại vẫn là 1. Thang đo của ảnh giữ nguyên qua mọi bước, không phình ra. Chỉ có tỉ lệ giữa “ảnh” và “nhiễu” thay đổi.
DDPM dùng và lịch tuyến tính: tăng đều từ tới . Bước đầu thêm rất ít nhiễu, bước cuối thêm nhiều hơn.
Dạng đóng: nhảy thẳng tới bước
Phần tiêu đề “Dạng đóng: nhảy thẳng tới bước ttt”Muốn có , phải thêm nhiễu 500 lần sao? Không cần. Ta dẫn một công thức đi thẳng từ .
Bước 1: thay một bước vào bước kế. Viết theo bằng chính công thức trên, rồi thay vào công thức của :
Dòng thứ hai chỉ là nhân vào trong ngoặc.
Bước 2: gộp hai nhiễu thành một. Hai số hạng cuối là hai nhiễu Gauss độc lập, trung bình 0, phương sai lần lượt và . Tổng của hai biến Gauss độc lập vẫn là Gauss, và phương sai của tổng bằng tổng phương sai:
Vậy hai số hạng đó có cùng phân phối với , với là một nhiễu chuẩn duy nhất:
Bước 3: lặp lại tới . Công thức vừa có giống hệt công thức một bước, chỉ thay bằng tích . Thay tiếp , rồi , mỗi lần tích dài thêm một thừa số. Tới , tích là :
Đây là kết quả chính của quá trình thuận. Với lịch của DDPM (số lấy từ ô NumPy bên dưới):
| (phần ảnh) | (phần nhiễu) | |
|---|---|---|
| 100 | 0.9471 | 0.3209 |
| 250 | 0.7239 | 0.6899 |
| 500 | 0.2803 | 0.9599 |
| 1000 | 0.0064 | 1.0000 |
Ở , ảnh chỉ còn hệ số 0.0064: gần như là nhiễu chuẩn thuần. Đó là lý do việc sinh ảnh có thể bắt đầu từ một nhiễu chuẩn bất kỳ.
Huấn luyện: dự đoán nhiễu
Phần tiêu đề “Huấn luyện: dự đoán nhiễu”Nhìn lại dạng đóng. Nếu biết , biết , và biết đúng nhiễu đã cộng vào, ta giải ra được ảnh gốc:
(chuyển số hạng nhiễu sang vế trái rồi chia cho ). Vậy “đoán nhiễu” và “đoán ảnh sạch” là hai cách nói của cùng một việc. DDPM chọn cho mạng đoán nhiễu. Gọi mạng đó là : nhận ảnh nhiễu và số bước, trả ra một tensor cùng cỡ ảnh. Mạng biết nên biết ảnh đang nhiễu nhiều hay ít.
Ho và cộng sự dẫn hàm mất mát từ một cận dưới của log-likelihood (mục 3 của paper), rồi thấy dạng rút gọn, bỏ các trọng số, cho ảnh đẹp hơn. Dạng rút gọn đó chỉ là MSE giữa nhiễu thật và nhiễu đoán:
Một bước huấn luyện (Thuật toán 1 của paper) vì thế rất gọn: lấy một ảnh , chọn ngẫu nhiên đều trong , sinh một , tạo bằng dạng đóng, rồi đi một bước gradient descent trên . Dạng đóng là thứ làm việc này rẻ: mỗi mẫu chỉ cần một phép cộng, dù .
Trong DDPM, là một U-Net: mạng tích chập có nhánh co và nhánh giãn đối xứng, đầu ra cùng cỡ đầu vào (xem bài Semantic segmentation). Số bước được đổi thành một vectơ kiểu embedding vị trí hình sin của Transformer rồi cộng vào từng khối của mạng. Một mạng dùng chung cho mọi .
Lấy mẫu: đi ngược từng bước
Phần tiêu đề “Lấy mẫu: đi ngược từng bước”Đã có , ta sinh ảnh mới theo Thuật toán 2 của paper. Bắt đầu từ , rồi với :
với khi và ở bước cuối. Đọc công thức từ trong ra ngoài:
- Trừ đi một phần nhiễu dự đoán. Mạng đoán toàn bộ nhiễu tích lũy ; bước này chỉ gỡ phần ứng với một bước, nên nhân với hệ số nhỏ .
- Chia cho . Quá trình thuận đã nhân ảnh với ; chia ra là trả lại thang đo cũ.
- Cộng thêm một chút nhiễu mới . Từ một có nhiều hợp lý, không chỉ một. Thêm nhiễu là lấy mẫu một trong số đó, giữ cho ảnh đa dạng. Paper thử và một lựa chọn khác, thấy kết quả tương tự.
Phần trong ngoặc là trung bình của bước ngược mà paper dẫn ra (phương trình 11), bài này không dẫn lại chi tiết. Điều cần nhớ: mỗi bước gọi mạng một lần, nên lấy mẫu DDPM tốn lần chạy U-Net cho một ảnh.
DDIM: ít bước hơn, không cần huấn luyện lại
Phần tiêu đề “DDIM: ít bước hơn, không cần huấn luyện lại”1000 lần chạy mạng là chậm. DDIM (Song, Meng, Ermon, 2021) giữ nguyên mạng đã huấn luyện, chỉ đổi cách đi ngược. Ở bước , nó ước lượng ảnh sạch bằng công thức giải ngược ở trên:
rồi dùng lại chính dạng đóng để nhảy tới một bước bất kỳ, với nhiễu là nhiễu vừa đoán:
Đây là bản tất định của DDIM: không cộng nhiễu mới. Vì có thể cách hàng chục bước, chỉ cần khoảng 50 bước thay vì 1000. Paper báo cáo DDIM cho ảnh chất lượng cao nhanh hơn DDPM 10 tới 50 lần tính theo thời gian chạy.
Latent diffusion: khử nhiễu trong không gian nén
Phần tiêu đề “Latent diffusion: khử nhiễu trong không gian nén”Ảnh có số. Chạy U-Net hàng chục lần trên ngần ấy số, cho mỗi ảnh, là đắt; huấn luyện còn đắt hơn. Phần lớn số đó mô tả chi tiết nhỏ mà mắt khó nhận ra.
Rombach và cộng sự (2022) đề xuất latent diffusion: tách việc thành hai phần.
- Một autoencoder, huấn luyện một lần. Bộ mã hóa nén ảnh thành latent nhỏ hơn nhiều, bộ giải mã dựng lại ảnh. Autoencoder chính của paper có một phạt KL nhẹ về phía phân phối chuẩn, họ hàng gần của VAE ở bài trước.
- Diffusion chạy trên latent. Mọi công thức ở trên giữ nguyên, chỉ thay ảnh bằng latent . Hàm mất mát là . Sinh xong latent thì cho qua một lần để ra ảnh.
Ở Stable Diffusion v1, autoencoder có hệ số thu nhỏ và latent 4 kênh: ảnh thành latent . Với ảnh :
Diffusion làm việc trên một tensor nhỏ hơn 48 lần (tự tính). Paper thử từ 1 tới 32 và thấy và cân bằng tốt nhất giữa chất lượng và chi phí; quá lớn làm mất chi tiết.
Điều kiện bằng chữ: cross-attention
Phần tiêu đề “Điều kiện bằng chữ: cross-attention”Để ảnh theo câu lệnh , latent diffusion cho câu lệnh qua một bộ mã hóa chữ , rồi chèn kết quả vào U-Net bằng cross-attention. Gọi là đặc trưng của U-Net ở một tầng, trải phẳng thành dãy vị trí. Một lớp cross-attention tính:
rồi như mọi attention. Truy vấn đến từ ảnh, khóa và giá trị đến từ chữ: mỗi vị trí trong latent “hỏi” các token của câu lệnh xem mình nên vẽ gì. Paper viết các tích này theo cột (); bài viết theo hàng cho khớp với bài Multimodal.
Trong paper, mô hình chữ-ra-ảnh dùng một Transformer học cùng U-Net làm . Stable Diffusion v1 (2022), xây trên latent diffusion, thay nó bằng bộ mã hóa chữ của CLIP ViT-L/14 và đóng băng bộ mã hóa này. Theo README chính thức, mô hình có U-Net 860 triệu tham số và bộ mã hóa chữ 123 triệu tham số, huấn luyện trên một tập con của LAION-5B, trước ở rồi tinh chỉnh ở . Vì CLIP đã học ghép ảnh với chữ (bài CLIP, BLIP, GLIP), đặc trưng chữ của nó vốn mang nghĩa “trông như thế nào”.
Classifier-free guidance
Phần tiêu đề “Classifier-free guidance”Mô hình có điều kiện thường “nghe lời” chưa đủ: ảnh hợp lý, nhưng không bám sát câu lệnh. Classifier-free guidance (Ho và Salimans, 2022) khuếch đại ảnh hưởng của câu lệnh mà không cần thêm mạng nào.
Khi huấn luyện: thỉnh thoảng thay câu lệnh bằng một câu lệnh rỗng (bản Stable Diffusion v1-4 bỏ điều kiện 10% số lần). Nhờ vậy một mạng học được hai việc: đoán nhiễu khi có câu lệnh, , và khi không có, .
Khi lấy mẫu: mỗi bước chạy mạng hai lần rồi trộn. Paper viết với trọng số :
Thêm rồi bớt , rồi gom theo , ta được dạng mà thư viện hay dùng, với hệ số guidance :
Hiệu trong ngoặc là “hướng mà câu lệnh kéo ảnh đi”. là sinh không điều kiện, là dùng đúng dự đoán có điều kiện, đi xa hơn theo hướng đó. Script lấy mẫu của Stable Diffusion v1 mặc định . Paper cho thấy tăng guidance đổi độ đa dạng lấy độ bám và độ rõ của từng ảnh: quá mạnh thì ảnh kém đa dạng.
LoRA cho diffusion
Phần tiêu đề “LoRA cho diffusion”Bạn muốn mô hình vẽ theo phong cách tranh của mình, hay vẽ đúng con mèo nhà bạn. Fine-tune cả U-Net 860 triệu tham số thì tốn bộ nhớ, và mỗi phong cách là một bản sao vài GB.
LoRA (Hu và cộng sự, 2022) ra đời cho LLM, đã dẫn chi tiết ở bài Đánh giá mô hình và PEFT. Nhắc lại: với một ma trận đóng băng , LoRA học thêm và hạng nhỏ:
mỗi ma trận học số, và lúc đầu nên mô hình xuất phát đúng từ mô hình gốc.
Với Stable Diffusion, chỗ tự nhiên để gắn LoRA là attention của U-Net: bốn phép chiếu truy vấn, khóa, giá trị và đầu ra (to_q, to_k, to_v, to_out) của mọi lớp self-attention và cross-attention. Script huấn luyện LoRA của thư viện diffusers làm đúng như vậy, hạng mặc định . Cross-attention là nơi chữ gặp ảnh, nên chỉnh nó là chỉnh cách mô hình hiểu câu lệnh thành hình.
Đếm thử một lớp cross-attention ở tầng kênh, đặc trưng chữ 768 chiều, : to_q và to_out là , mỗi cái số; to_k và to_v là , mỗi cái số. Cộng lại số. Trên cả 32 lớp attention của U-Net, ô PyTorch bên dưới đếm được số, khoảng 0.09% của U-Net. Lưu ở 32 bit là khoảng 3.2 MB (tự tính), thay vì vài GB.
DreamBooth (Ruiz và cộng sự, 2023) là một cách tinh chỉnh khác: dạy mô hình một đối tượng cụ thể từ vài ảnh, gắn nó với một từ định danh hiếm trong câu lệnh. DreamBooth gốc tinh chỉnh cả mô hình; trong thực tế người ta hay ghép nó với LoRA để rẻ hơn.
Giới hạn
Phần tiêu đề “Giới hạn”- Lấy mẫu chậm. Mỗi bước một lần chạy mạng, hai lần nếu có guidance. DDIM giảm còn khoảng 50 bước; còn nhiều cách nhanh hơn nữa ngoài phạm vi bài.
- Lịch nhiễu không phải luật cố định. Stable Diffusion v1 dùng lịch khác lịch của DDPM (hộp “Lỗi hay gặp” bên dưới); ô PyTorch của bài cũng phải đổi lịch vì nhỏ.
- Chất lượng phụ thuộc autoencoder. Chi tiết bị mất khi nén thì diffusion không lấy lại được, ví dụ chữ nhỏ hay khuôn mặt xa.
Minh họa
Phần tiêu đề “Minh họa”Hình dưới cho thấy dạng đóng của quá trình thuận trên một ảnh nhỏ. Thanh trượt chọn bước ; ảnh được trộn từ ảnh gốc và một nhiễu cố định, nên bạn thấy chính ảnh đó chìm dần vào cùng một lớp nhiễu. Đồ thị nhỏ vẽ hai hệ số và theo , có dấu ở bước hiện tại. Hãy thử:
- Đặt : ảnh vẫn rõ, hệ số ảnh là 0.9471, hệ số nhiễu 0.3209.
- Kéo tới : hai hệ số gần bằng nhau (0.7239 và 0.6899), hai đường trên đồ thị sắp cắt nhau. Ảnh vẫn nhận ra được nhưng đã lốm đốm.
- Kéo tới : hệ số ảnh chỉ còn 0.2803. Nhìn kỹ mới thấy bóng của hình.
- Kéo hết tới : hệ số ảnh 0.0064, ảnh là nhiễu thuần. Đây là điểm xuất phát khi sinh ảnh.
Bảng dưới đặt ba họ mô hình sinh ảnh cạnh nhau. Hãy nhìn hai dòng “Huấn luyện” và “Sinh một ảnh”: diffusion đổi tốc độ sinh lấy sự ổn định khi học.
| Tiêu chí | VAE | GAN | Diffusion |
|---|---|---|---|
| Ý tưởng | Nén ảnh thành rồi giải mã | sinh ảnh giả, phân biệt thật giả | Thêm nhiễu từng bước, học gỡ nhiễu ngược lại |
| Huấn luyện | Ổn định: tái tạo cộng KL | Khó giữ cân bằng giữa và | Ổn định: MSE trên nhiễu |
| Sinh một ảnh | Một lần chạy bộ giải mã | Một lần chạy | Hàng chục tới 1000 lần chạy mạng |
| Chất lượng ảnh | Thường hơi mờ | Sắc nét | Sắc nét |
| Đa dạng | Tốt | Dễ sụp mode (mode collapse) | Tốt |
| Vai trò trong Stable Diffusion | Autoencoder nén ảnh thành latent | Không dùng | U-Net khử nhiễu trên latent |
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, NumPy 2.4, scikit-learn 1.8 và matplotlib (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. Ô NumPy lần đầu chạy lâu hơn, vì trình duyệt phải tải thêm scikit-learn và matplotlib.
Quá trình thuận trên một chữ số
Phần tiêu đề “Quá trình thuận trên một chữ số”Ô dưới đây lấy chữ số đầu tiên của load_digits (một số 0, ảnh ), đưa về , rồi dùng dạng đóng với lịch tuyến tính của DDPM để tạo ở , cùng một nhiễu . Phần cuối kiểm lại phép dẫn: thêm nhiễu từng bước 500 lần cho 20 000 bản sao của một điểm ảnh , rồi so trung bình và độ lệch chuẩn với dạng đóng.
Thêm nhiễu theo dạng đóng của DDPM
import matplotlib.pyplot as pltimport numpy as npfrom sklearn.datasets import load_digits
# Ảnh x_0: chữ số đầu tiên (số 0), đưa từ [0, 16] về [-1, 1] như DDPMx0 = load_digits().images[0] / 8.0 - 1.0
# Lịch tuyến tính của DDPM: beta_1 = 1e-4 tới beta_T = 0.02, T = 1000T = 1000betas = np.linspace(1e-4, 0.02, T)alpha_bar = np.cumprod(1.0 - betas) # alpha_bar[t - 1] ứng với bước t
rng = np.random.default_rng(0)eps = rng.standard_normal(x0.shape) # một nhiễu chung cho mọi t
ts = [0, 100, 250, 500, 1000]fig, axes = plt.subplots(1, len(ts), figsize=(10, 2.4))for ax, t in zip(axes, ts): ab = 1.0 if t == 0 else alpha_bar[t - 1] # t = 0: ảnh gốc xt = np.sqrt(ab) * x0 + np.sqrt(1 - ab) * eps # dạng đóng print(f"t={t:4d} sqrt(ab)={np.sqrt(ab):.4f} " f"sqrt(1-ab)={np.sqrt(1 - ab):.4f}") ax.imshow(xt, cmap="gray", vmin=-2.5, vmax=2.5) ax.set_title(f"t = {t}") ax.axis("off")plt.show()
# Kiểm dạng đóng: thêm nhiễu từng bước, 20000 lần, cho một pixel x0 = 1x = np.ones(20000)for b in betas[:500]: x = np.sqrt(1 - b) * x + np.sqrt(b) * rng.standard_normal(x.size)print(f"step by step, t=500: mean={x.mean():.4f} std={x.std():.4f}")print(f"closed form, t=500: mean={np.sqrt(alpha_bar[499]):.4f} " f"std={np.sqrt(1 - alpha_bar[499]):.4f}")The first run downloads Python, about 10 MB.
Đọc kết quả:
- Năm dòng đầu là bảng ở mục Lý thuyết. Hai hệ số cắt nhau ngay sau ; tới phần ảnh chỉ còn 0.0064.
- Hình: cùng một nhiễu, nên từ trái sang phải bạn thấy đúng một chữ số chìm dần, như trong VIZ.
- Hai dòng cuối: thêm nhiễu từng bước cho trung bình 0.2742 và độ lệch chuẩn 0.9624; dạng đóng cho 0.2803 và 0.9599. Chênh lệch nằm trong sai số của 20 000 mẫu (độ lệch chuẩn của trung bình mẫu khoảng ). 500 bước nhỏ thật sự gộp được thành một bước.
Một DDPM tí hon trên dữ liệu hai chiều
Phần tiêu đề “Một DDPM tí hon trên dữ liệu hai chiều”Ảnh thật cần U-Net và GPU. Để thấy cả vòng huấn luyện và lấy mẫu chạy trên CPU, file dưới đây thay ảnh bằng điểm hai chiều: 2000 điểm hình hai nửa vầng trăng (make_moons), chuẩn hóa về trung bình 0, độ lệch chuẩn 1. Mạng là một MLP nhận và trả về 2 số. Vòng huấn luyện là Thuật toán 1, vòng lấy mẫu là Thuật toán 2 với , đúng các công thức ở trên.
Vì chỉ có bước, lịch được nhân 10 lần: từ tới . Giữ lịch gốc với thì chỉ khoảng 0.36, còn rất nhiều dữ liệu, và việc bắt đầu lấy mẫu từ nhiễu chuẩn sẽ sai. Cách cài PyTorch bản CPU có ở bài Xây dựng LLM đầu tiên.
Run on your machine
DDPM tí hon: huấn luyện và lấy mẫu trên hai nửa vầng trăng
- Install
pip install torch scikit-learn- Hardware
- CPU là đủ, không cần GPU. Khoảng 2 phút trên máy phát triển, có thể lâu hơn trên máy chậm hoặc đang bận
import torchfrom sklearn.datasets import make_moonsfrom torch import nn
torch.manual_seed(0)# Dữ liệu thật: 2000 điểm hình hai nửa vầng trăng, chuẩn hóa về trung bình 0x, _ = make_moons(2000, noise=0.05, random_state=0)data = torch.tensor(x, dtype=torch.float32)data = (data - data.mean(0)) / data.std(0)
# T = 100 bước nên beta lớn hơn DDPM gốc 10 lần: 1e-3 tới 0.2T = 100betas = torch.linspace(1e-3, 0.2, T)alphas = 1 - betasabar = torch.cumprod(alphas, 0)
# eps_theta(x_t, t): MLP nhận (x, y, t / T), trả về nhiễu dự đoán (2 số)net = nn.Sequential(nn.Linear(3, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, 2))eps_theta = lambda xt, t: net(torch.cat([xt, t[:, None] / T], 1))opt = torch.optim.Adam(net.parameters(), lr=1e-3)
for step in range(5001): # Thuật toán 1 của DDPM x0 = data[torch.randint(0, len(data), (256,))] t = torch.randint(0, T, (256,)) # chỉ số 0..T-1 ứng với bước 1..T eps = torch.randn_like(x0) a = abar[t][:, None] xt = a.sqrt() * x0 + (1 - a).sqrt() * eps loss = ((eps - eps_theta(xt, t)) ** 2).mean() # L_simple opt.zero_grad() loss.backward() opt.step() if step % 1000 == 0: print(f"step {step:4d} loss {loss.item():.3f}")
with torch.no_grad(): # Thuật toán 2: đi ngược từ nhiễu thuần noise = torch.randn(1000, 2) x = noise.clone() for i in reversed(range(T)): t = torch.full((1000,), i) e = eps_theta(x, t) x = (x - betas[i] / (1 - abar[i]).sqrt() * e) / alphas[i].sqrt() if i > 0: x = x + betas[i].sqrt() * torch.randn_like(x)
# Khoảng cách từ mỗi điểm tới điểm thật gần nhấtgap = lambda p: torch.cdist(p, data).min(1).valuesfor name, p in [("noise x_T", noise), ("samples x_0", x)]: d = gap(p) print(f"{name}: mean dist {d.mean():.3f}, " f"within 0.1: {(d < 0.1).float().mean():.0%}")Sample output
From one real run. A model gives a different answer each time.
step 0 loss 0.895step 1000 loss 0.192step 2000 loss 0.214step 3000 loss 0.236step 4000 loss 0.197step 5000 loss 0.243noise x_T: mean dist 0.262, within 0.1: 35%samples x_0: mean dist 0.042, within 0.1: 90%Đọc kết quả:
- Loss giảm nhanh từ 0.895 xuống quanh 0.2 rồi dao động. Loss không về 0, và điều đó là bình thường: ở lớn, gần như toàn nhiễu, không mạng nào đoán đúng được chính xác nhiễu nào đã cộng vào. Mỗi lô lấy ngẫu nhiên, nên loss nhảy lên xuống theo lô.
- Hai dòng cuối đo khoảng cách từ mỗi điểm tới điểm thật gần nhất. 1000 điểm nhiễu ban đầu cách trung bình 0.262, chỉ 35% nằm trong bán kính 0.1 của dữ liệu thật (con số này không nhỏ vì hai nửa vầng trăng phủ khá rộng vùng giữa). Sau 100 bước đi ngược, khoảng cách còn 0.042 và 90% điểm nằm sát dữ liệu: các điểm đã gom lại thành hai nửa vầng trăng.
Số của bạn có thể lệch một chút so với kết quả mẫu, tùy phiên bản PyTorch và CPU.
Đếm tham số LoRA trong attention của U-Net
Phần tiêu đề “Đếm tham số LoRA trong attention của U-Net”File thứ hai tự viết một lớp LoRALinear đúng công thức , gắn vào bốn phép chiếu của một lớp attention có cỡ như U-Net của Stable Diffusion v1, rồi đếm tham số cho cả 32 lớp attention (16 khối Transformer, mỗi khối một self-attention và một cross-attention). Không tải mô hình nào, chỉ dựng các lớp tuyến tính đúng cỡ.
Run on your machine
Tham số LoRA trong attention của U-Net
- Install
pip install torch- Hardware
- CPU là đủ. Vài giây, phần lớn là thời gian nạp PyTorch
import torchfrom torch import nn
torch.manual_seed(0)
class LoRALinear(nn.Module): """h = W0 x + (alpha / r) B A x, W0 đóng băng."""
def __init__(self, base, r=4, alpha=4): super().__init__() self.base = base.requires_grad_(False) self.A = nn.Parameter(torch.randn(r, base.in_features) * 0.01) self.B = nn.Parameter(torch.zeros(base.out_features, r)) # B = 0 self.scale = alpha / r
def forward(self, x): return self.base(x) + self.scale * (x @ self.A.T) @ self.B.T
def attention(d, ctx): # Bốn phép chiếu của một lớp attention trong U-Net của Stable Diffusion # v1: q từ ảnh (d chiều), k và v từ chữ (ctx chiều), out về lại d chiều return nn.ModuleDict({"to_q": nn.Linear(d, d, bias=False), "to_k": nn.Linear(ctx, d, bias=False), "to_v": nn.Linear(ctx, d, bias=False), "to_out": nn.Linear(d, d)})
def count(d, ctx): att = attention(d, ctx) full = sum(p.numel() for p in att.parameters()) for name in list(att.keys()): att[name] = LoRALinear(att[name]) lora = sum(p.numel() for p in att.parameters() if p.requires_grad) return att, full, lora
att, full, lora = count(320, 768) # cross-attention ở tầng 320 kênhprint(f"cross-attn d=320: full {full}, LoRA r=4: {lora}")x = torch.randn(2, 4096, 320) # 64 x 64 vị trí của latent, 2 ảnhprint("same output at init:", torch.equal(att["to_q"](x), att["to_q"].base(x)))
# U-Net SD v1 có 16 khối Transformer, mỗi khối 1 self-attn (ctx = d)# và 1 cross-attn (ctx = 768). Số kênh d của 16 khối:widths = [320] * 2 + [640] * 2 + [1280] * 2 + [1280] + \ [1280] * 3 + [640] * 3 + [320] * 3total = sum(count(d, d)[2] + count(d, 768)[2] for d in widths)print(f"LoRA r=4 on all 32 attention layers: {total}")print(f"share of an 860M U-Net: {total / 860e6:.3%}")Sample output
From one real run.
cross-attn d=320: full 696640, LoRA r=4: 13824same output at init: TrueLoRA r=4 on all 32 attention layers: 797184share of an 860M U-Net: 0.093%Đọc kết quả:
full 696640, LoRA r=4: 13824: lớp cross-attention có gần 700 nghìn tham số (nhiều nhất ởto_k,to_vvì nhận 768 chiều), LoRA chỉ học 13 824, khớp phép đếm tay ở mục Lý thuyết.same output at init: True: nên lúc đầu lớp LoRA cho đúng đầu ra của lớp gốc, trên một latent (4 096 vị trí).797184và0.093%: cả U-Net chỉ học khoảng 0.8 triệu số. Con số này trùng với số tham số mà thư việnpeftbáo khi gắn LoRA vào U-Net dựng theo cấu hình Stable Diffusion v1 trongdiffusers(kiểm riêng, không nằm trong bài).
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:
- Diffusion thêm nhiễu Gauss vào ảnh qua bước nhỏ (ở đây là số bước, không phải nhiệt độ), rồi học đi ngược. Nhờ tổng các Gauss độc lập vẫn là Gauss, ta nhảy thẳng tới bước bất kỳ: .
- DDPM huấn luyện mạng đoán nhiễu bằng MSE, rồi sinh ảnh bằng 1000 bước ngược từ nhiễu thuần. DDIM dùng cùng mạng, nhảy qua nhiều bước, chỉ cần khoảng 50.
- Stable Diffusion chạy diffusion trên latent của một autoencoder (ảnh thành latent , nhỏ hơn 48 lần), nhận câu lệnh qua bộ mã hóa chữ CLIP và cross-attention, và dùng classifier-free guidance để bám câu lệnh.
- LoRA gắn vào các phép chiếu attention của U-Net: với chỉ khoảng 0.8 triệu tham số, chưa tới 0.1% U-Net, đủ để dạy một phong cách hay một đối tượng.
Đọc thêm:
- Ho, J., Jain, A., Abbeel, P. (2020). Denoising Diffusion Probabilistic Models, arXiv:2006.11239, mục 2, 3.2 và Thuật toán 1, 2. Nguồn gốc của mọi công thức DDPM trong bài; mục 3.2 dẫn bước ngược.
- Song, J., Meng, C., Ermon, S. (2021). Denoising Diffusion Implicit Models, arXiv:2010.02502, mục 4. Cách đi ngược tất định, ít bước, dùng lại mạng của DDPM.
- Rombach, R., Blattmann, A., Lorenz, D., Esser, P., Ommer, B. (2022). High-Resolution Image Synthesis with Latent Diffusion Models, arXiv:2112.10752, mục 3. Latent diffusion và cross-attention; nền của Stable Diffusion. Thông số của Stable Diffusion v1 nằm ở README của CompVis.
- Ho, J., Salimans, T. (2022). Classifier-Free Diffusion Guidance, arXiv:2207.12598, mục 3. Ngắn; công thức guidance và thí nghiệm về đổi đa dạng lấy chất lượng.
- Hu, E. J. và cộng sự (2022). LoRA: Low-Rank Adaptation of Large Language Models, arXiv:2106.09685; Ruiz, N. và cộng sự (2023). DreamBooth: Fine Tuning Text-to-Image Diffusion Models for Subject-Driven Generation, arXiv:2208.12242; và hướng dẫn LoRA của Hugging Face
diffusers, cho biết các lớp được gắn LoRA trong U-Net.
Your browser is blocking storage. Your progress will be lost when you reload the page.
Content licensed under CC BY-NC-SA 4.0. Code under the MIT license.About