এই পর্বে যা শিখবেন
- Autoencoder থেকে VAE-তে যাওয়ার কারণ, আর posterior কেন সরাসরি হিসাব করা যায় না
- ELBO ধাপে ধাপে বের করা, আর তাকে reconstruction ও KL অংশে ভাঙা
- Gaussian KL-এর সূত্র হাতে, আর reparameterization trick-এ gradient-এর পথ সংখ্যা দিয়ে
- MNIST আর Fashion-MNIST-এ VAE চালিয়ে ELBO, importance sampling-এ likelihood আর latent space দেখা
- β-VAE, posterior collapse, KL annealing, conditional VAE আর VQ-VAE-এর মূল ধারণা
১ · Autoencoder থেকে VAE — লুকানো কারণের মডেল
পর্ব ৭৫ (Autoencoder — compression, denoising আর representation)-এ একটি ছবিকে ছোট কোডে চেপে আবার ফিরিয়ে আনা হয়েছে। সেখানে একটা সমস্যাও দেখা যায়: কোডের জায়গায় এলোমেলো সংখ্যা বসিয়ে decoder চালালে বেশিরভাগ সময় অর্থহীন ছবি আসে। কারণ সাধারণ autoencoder কোডগুলোকে কোথায় রাখবে, তার কোনো নিয়ম নেই; train ছবির কোডগুলো ছড়িয়ে-ছিটিয়ে থাকে, মাঝখানের ফাঁকা জায়গা decoder কখনো দেখেনি। Variational autoencoder (VAE) এই কোড-জায়গাকে একটি নির্দিষ্ট বণ্টনে বাঁধে, যাতে সেখান থেকে নমুনা তুলে নতুন ছবি বানানো যায়।
ডাক্তারের রোগনির্ণয়ের কথা ভাবুন। রোগ (z) জানা থাকলে কোন উপসর্গ (x) দেখা দেবে, তা বলা তুলনামূলক সহজ: “ডেঙ্গু হলে জ্বর আর গায়ে ব্যথা প্রায় নিশ্চিত।” এটা p(x | z). কিন্তু রোগী আসেন উপসর্গ নিয়ে, আর ডাক্তারকে উল্টো প্রশ্নের উত্তর দিতে হয়: “এই উপসর্গে কোন রোগের সম্ভাবনা কত?” — এটা p(z | x). নিখুঁতভাবে বলতে হলে সম্ভাব্য সব রোগের হিসাব একসাথে করতে হয়। অভিজ্ঞ ডাক্তার তাই একটি দ্রুত, আনুমানিক রায় দেন। VAE-এর encoder ঠিক এই অভিজ্ঞ ডাক্তার: উপসর্গ দেখে এক ধাপে একটি আনুমানিক বণ্টন বলে দেয়।
VAE-র গল্পটা দুই লাইনের। প্রথমে একটি latent variable z তোলা হয় prior p(z) = N(০, I) থেকে, অর্থাৎ ১৬টি স্বাধীন সংখ্যা, প্রতিটির গড় ০ আর variance ১. তারপর decoder নামের একটি neural network z থেকে ৭৮৪টি pixel-এর প্রতিটির জন্য একটি probability দেয়, আর প্রতিটি pixel আলাদা টসে ০ বা ১ হয়: pθ(x | z). এখানে θ হলো decoder-এর weight. z যে জায়গায় থাকে, তাকে বলে latent space. একটি ছবির মোট probability পেতে সব সম্ভাব্য z-এর ওপর যোগ (আসলে integral) করতে হয়:
pθ(x) = ∫ pθ(x | z) p(z) dz
এই সংখ্যাকে বলে evidence বা marginal likelihood. এর log-likelihood বাড়ানোই হতো স্বাভাবিক লক্ষ্য, পর্ব ৭৪ (Generative modeling-এর মানচিত্র)-এর maximum likelihood. কিন্তু ১৬ মাত্রার ওপর একটি neural network-এর integral-এর কোনো বন্ধ সূত্র নেই। নমুনা তুলে গড় নেওয়ার (Monte Carlo) সরল চেষ্টাও প্রায় ব্যর্থ হয়: prior থেকে তোলা বেশিরভাগ z একটি নির্দিষ্ট “৭”-এর ছবির জন্য p(x | z) প্রায় শূন্য দেয়, যেন খড়ের গাদায় সুঁই খোঁজা। সেকশন ৫-এ এটি মাপা হয়েছে: MNIST-এর ১,০০০টি test ছবিতে prior থেকে ১,০০০টি z তুলে গড় নিলে log p(x)-এর অনুমান দাঁড়ায় −১৮২.৩২ nats, অথচ encoder-কে পথপ্রদর্শক করলে একই ১,০০০টি নমুনায় −৭৪.৪৫ — প্রায় ১০৮ nats-এর পার্থক্য, পুরোটাই খারাপ অনুমানের দোষ।
উল্টো দিকের প্রশ্নটিও আটকে যায়। Bayes-এর সূত্রে posterior p(z | x) = p(x | z) p(z) / p(x), আর হরে সেই একই p(x). তাই VAE একটি দ্বিতীয় network আনে, encoder, যা প্রতিটি x-এর জন্য একটি সহজ বণ্টন দেয়: qφ(z | x) = N(μφ(x), diag σφ2(x)). এখানে φ হলো encoder-এর weight, আর diag মানে প্রতিটি মাত্রা আলাদা, তাদের মধ্যে সম্পর্ক ধরা হয় না। প্রতিটি ছবির জন্য আলাদা করে optimisation না চালিয়ে একটি network সব ছবির জন্য এক ধাপে উত্তর দেয় — একে বলে amortized inference.
VAE-তে দুটি network একসাথে শেখে: decoder pθ(x | z) ছবি বানায়, আর encoder qφ(z | x) অজানা posterior-এর একটি আনুমানিক রূপ দেয়। দুজনকে একসাথে শেখানোর জন্য একটি লক্ষ্য দরকার, যা হিসাব করা যায় — সেটিই ELBO.
২ · ELBO — log p(x) = ELBO + KL
মেশিন লার্নিং কোর্সের পর্ব ৪৮-এ variational inference-এর সাধারণ রূপে এই অভেদ দেখেছেন। এখানে একই হিসাব VAE-র চোখে, একটি ছবি x ধরে। মূল কৌশল এক লাইনের: log p(x) z-এর ওপর নির্ভর করে না, তাই যেকোনো q(z | x)-এর ওপর তার গড় নিলেও সে একই থাকে। তারপর Bayes দিয়ে p(x) = p(x, z) / p(z | x) লিখে ভেতরে q দিয়ে গুণ আর ভাগ:
log p(x) = Eq[log p(x, z) − log q(z | x)] + Eq[log q(z | x) − log p(z | x)]
প্রথম গড়টির নাম ELBO (evidence lower bound), দ্বিতীয়টি KL(q(z | x) ‖ p(z | x)), আসল posterior থেকে q-এর দূরত্ব। KL divergence কখনো ঋণাত্মক হয় না (AI-এর ভিত্তি কোর্সের পর্ব ৩৮), তাই ELBO ≤ log p(x). দূরত্বটি আমরা হিসাব করতে পারি না, কারণ তাতে আসল posterior লাগে; কিন্তু ELBO পারি। এই ভাঙনের পুরো পথ একটি খেলনা উদাহরণে দেখুন, যেখানে z মাত্র দুটি মান নিতে পারে, তাই সবকিছু হাতে হিসাব করা যায়।
| ধাপ | |
| হিসাব |
- মডেল. Prior দুই মানে সমান ভাগ। Decoder বলে: z = ১ হলে এই ছবি দেখার probability ০.৮, z = ০ হলে ০.২. বাস্তব VAE-তে z ১৬টি continuous সংখ্যা আর p(x | z) একটি network, কিন্তু যুক্তি হুবহু এক।
- Evidence. দুটি z-এর ওপর যোগ: p(x) = ০.৫. এখানে দুটি পদ, তাই সহজ। VAE-তে এই যোগ হয়ে যায় ১৬ মাত্রার integral, যার বন্ধ সূত্র নেই — পুরো সমস্যার মূল এখানে।
- Posterior. Bayes: p(z = ১ | x) = p(x, z = ১) / p(x) = ০.৪ / ০.৫ = ০.৮. হরে p(x) লাগল; তাই evidence না জানলে posterior-ও জানা যায় না।
- Encoder-এর অনুমান. ধরুন encoder বলল q(z = ১ | x) = ০.৬. আসল ০.৮ থেকে একটু দূরে, যেমনটা সীমিত network-এ প্রায়ই হয়।
- ELBO. Eq[log p(x, z) − log q]: z = ১ অংশে p(x, z) = ০.৫ × ০.৮ = ০.৪, z = ০ অংশে ০.৫ × ০.২ = ০.১. ফল −০.৭৯৭৮, যা log p(x) = −০.৬৯৩১-এর চেয়ে ছোট, অর্থাৎ সত্যিই নিম্নসীমা।
- ফাঁক = KL. q থেকে আসল posterior (০.৮, ০.২)-এর KL = ০.১০৪৭. যোগ করলে ঠিক −০.৬৯৩১. অভেদটি সংখ্যায় মিলল: log p(x) = ELBO + KL(q ‖ posterior).
- দুই অংশ. log p(x, z) = log p(x | z) + log p(z) লিখলে ELBO = Eq[log p(x | z)] − KL(q ‖ p(z)). প্রথমটি reconstruction (−০.৭৭৭৭), দ্বিতীয়টি prior থেকে q-এর দূরত্ব (০.০২০১)। দুটোই হিসাব করা যায়, কারণ এতে আসল posterior নেই। VAE ঠিক এই রূপটাই কোডে লেখে।
- টানটান সীমা. q যদি হুবহু posterior হয় (০.৮), KL = ০ আর ELBO = log p(x). তাই ELBO বাড়ানো দুই কাজ একসাথে করে: decoder-কে ভালো করে (log p(x) বাড়ে) আর encoder-কে posterior-এর কাছে টানে (ফাঁক কমে)।
VAE-র loss তাই এই ELBO-র ঋণাত্মক, প্রতিটি ছবির জন্য:
loss(x) = −Eq(z|x)[log pθ(x | z)] + KL(qφ(z | x) ‖ N(০, I))
প্রথম পদ হলো reconstruction term: encoder-এর দেওয়া z থেকে decoder আসল ছবিটি কতটা ফিরিয়ে আনতে পারে। ০/১ pixel-এ এটি ঠিক binary cross-entropy-র যোগফল, যা পর্ব ১৩ (Deep learning-এর loss)-এ দেখেছেন। দ্বিতীয় পদ একটি নিয়ন্ত্রক: প্রতিটি ছবির q-কে prior-এর কাছে থাকতে বলে, যাতে কোডগুলো N(০, I)-এর চারপাশে ঘন হয়ে থাকে আর নমুনা তোলার সময় ফাঁকা জায়গা কম পড়ে। দুই পদের টানাটানিই VAE-র চরিত্র ঠিক করে, যা সেকশন ৭-এ মাপা হবে। আরেক পথেও এই সীমা পাওয়া যায়: log-এর ভেতরে গড় থাকলে Jensen-এর অসমতা দিয়ে log E[·] ≥ E[log ·].
৩ · Gaussian KL হাতে — নমুনা ছাড়াই
Loss-এর KL অংশে দুটিই Gaussian: q = N(μ, σ2) আর prior N(০, ১), প্রতি মাত্রায় আলাদা। KL = Eq[log q − log p] খুলে লিখলে log q-এর গড় −½ ln(২πσ2) − ½, আর log p-এর গড় −½ ln(২π) − ½(μ2 + σ2), কারণ q-এর ওপর E[z2] = μ2 + σ2. বিয়োগ করলে ২π কেটে যায়:
KL(N(μ, σ2) ‖ N(০, ১)) = ½ (μ2 + σ2 − ১ − ln σ2)
প্রতিটি পদের অর্থ আছে। μ2 বলে: কোড কেন্দ্র থেকে যত দূরে, দণ্ড তত বেশি। σ2 − ১ − ln σ2 শূন্য হয় শুধু σ = ১-এ, আর σ ছোট হলে −ln σ2 দ্রুত বাড়ে: encoder যদি খুব নিশ্চিত হয়ে একটি বিন্দুতে চেপে বসতে চায়, তার দাম দিতে হয়। মাত্রাগুলো স্বাধীন বলে মোট KL হলো প্রতি মাত্রার KL-এর যোগফল। Encoder সাধারণত σ নয়, log σ2 দেয়, যাতে output যেকোনো বাস্তব সংখ্যা হতে পারে আর σ সবসময় ধনাত্মক থাকে।
| ধাপ | |
| হিসাব |
- সূত্র. প্রতিটি মাত্রায় চারটি সংখ্যা লাগে: μ2, σ2, ১ আর ln σ2. কোনো নমুনা বা integral নেই; এজন্যই VAE-তে KL অংশ নিখুঁতভাবে হিসাব হয়।
- মাত্রা ১. এই ছবির জন্য encoder বলছে: z1 সম্ভবত ০.৮-এর কাছে, অনিশ্চয়তা ০.৫. বর্গগুলো আর ln ০.২৫ = −১.৩৮৬৩ আলাদা করে লিখে নিন।
- মাত্রা ১-এর KL. −ln σ2 যোগ মানে +১.৩৮৬৩. ফল ০.৬৩৮১ nats. এর প্রায় অর্ধেক এসেছে σ ছোট হওয়া থেকে (σ2 − ১ − ln σ2 = ০.৬৩৬৩), বাকি অর্ধেক μ থেকে (μ2 = ০.৬৪); দুটির যোগফলের অর্ধেকই ০.৬৩৮১।
- মাত্রা ২. μ প্রায় ০, σ প্রায় ১. ln ০.৯০২৫ = −০.১০২৬.
- মাত্রা ২-এর KL. মাত্র ০.০০৭৫ nats. এই মাত্রা ছবিটি সম্পর্কে প্রায় কিছুই বলে না; q এখানে prior-এর প্রায় হুবহু।
- মোট. Diagonal Gaussian-এ মাত্রাগুলো স্বাধীন, তাই KL যোগ হয়: ০.৬৪৫৭ nats. ln ২ দিয়ে ভাগ করলে প্রায় ০.৯৩ bit — মোটামুটি “এই ছবির কোড পাঠাতে prior-এর তুলনায় এক bit-এর মতো বাড়তি তথ্য” হিসেবে পড়া যায়।
- log σ² রূপ. কোডে encoder সরাসরি lv = ln σ2 দেয়, আর σ2 = elv. সূত্রে বসালে একই ০.৬৩৮১. সেকশন ৫-এর কোডে ঠিক এই লাইনটিই আছে।
- পড়া. যে মাত্রায় প্রায় সব ছবিতে KL শূন্যের কাছে, decoder সেই মাত্রা ব্যবহার করছে না। একে বলে নিষ্ক্রিয় latent মাত্রা; সেকশন ৭-এ সত্যিকারের run-এ এগুলো গোনা হয়েছে।
সূত্রটি ঠিক কি না, পাতার ভেতরেই নমুনা দিয়ে যাচাই করুন: q থেকে অনেক z তুলে log q − log p-এর গড় নিলে একই সংখ্যার কাছে আসা উচিত। সঙ্গে সেকশন ৫-এর সত্যিকারের MNIST run থেকে ১৬টি মাত্রার গড় KL বসানো আছে।
import numpy as np
np.set_printoptions(suppress=True, linewidth=56)
def kl_gauss(mu, sd): # KL(N(mu, sd²) ‖ N(0, 1)), প্রতি মাত্রা
return 0.5 * (mu**2 + sd**2 - 1 - np.log(sd**2))
mu = np.array([0.8, -0.1])
sd = np.array([0.5, 0.95])
k = kl_gauss(mu, sd)
print("সূত্রে: ", k.round(4), " মোট", k.sum().round(4))
rng = np.random.default_rng(42)
z = mu + sd * rng.standard_normal((200_000, 2)) # q থেকে নমুনা
log_q = -0.5 * np.log(2 * np.pi * sd**2) - (z - mu)**2 / (2 * sd**2)
log_p = -0.5 * np.log(2 * np.pi) - z**2 / 2
d = log_q - log_p
print("Monte Carlo: ", d.mean(0).round(4), " মোট", d.mean(0).sum().round(4))
print("std error: ", (d.std(0) / np.sqrt(len(d))).round(4))
# সত্যিকারের run: MNIST, z = 16, β = 1, seed 42; test-এ প্রতি মাত্রার গড় KL
kl_real = np.array([2.367, 2.238, 2.435, 2.768, 0.002, 1.837, 2.326, 1.440,
0.001, 1.413, 0.001, 0.001, 1.805, 1.603, 1.562, 1.985])
print("বড় থেকে ছোট:", np.sort(kl_real)[::-1])
print("মোট KL =", kl_real.sum().round(2), "nats =",
(kl_real.sum() / np.log(2)).round(1), "bits")
print("KL < 0.01 এমন মাত্রা:", np.flatnonzero(kl_real < 0.01))সূত্রে দুই মাত্রার KL ০.৬৩৮১ আর ০.০০৭৫, মোট ০.৬৪৫৭ nats — stepper-এর সংখ্যা হুবহু। ২,০০,০০০টি নমুনার Monte Carlo দিল ০.৬৩৯৭ আর ০.০০৭১, মোট ০.৬৪৬৮; প্রতিটি পার্থক্য মোটামুটি এক standard error-এর (০.০০১৫ আর ০.০০০৩) মধ্যে, অর্থাৎ নমুনার স্বাভাবিক ওঠানামা। সূত্রটি ঠিক, আর নমুনা ছাড়াই নিখুঁত — তাই VAE KL-এর জন্য নমুনা তোলে না, শুধু reconstruction-এর জন্য তোলে। নিচের অংশ সত্যিকারের run-এর: ১৬টি মাত্রার ১২টিতে গড় KL ১.৪ থেকে ২.৮ nats, কিন্তু চারটিতে (৪, ৮, ১০, ১১ নম্বর) মাত্র ০.০০১–০.০০২. এই চারটি মাত্রা কার্যত prior-এর মতো, কোনো ছবির খবর রাখে না — সেকশন ৭-এর নিষ্ক্রিয় মাত্রা। মোট ২৩.৭৮ nats, প্রায় ৩৪ bit: গড়ে একটি ছবির কোডে prior-এর তুলনায় এতটুকু তথ্য।
৪ · Reparameterization trick — এলোমেলোতার ভেতর দিয়ে gradient
Reconstruction পদে একটি গড় আছে: Eq(z|x)[log p(x | z)]. এটি হুবহু হিসাব করা যায় না, তাই প্রতিটি ছবির জন্য q থেকে একটি z তুলে তার মান দিয়ে কাজ চালানো হয়। সমস্যা হলো, “একটি নমুনা তোলা” কোনো মসৃণ function নয়। Encoder যদি μ একটু বাড়ায়, তোলা z কতটা বদলায়? প্রশ্নটাই অস্পষ্ট, কারণ নমুনা প্রতিবার নতুন এলোমেলো সংখ্যা। ফলে backpropagation (পর্ব ৪, হাতে-কলমে backpropagation) encoder পর্যন্ত পৌঁছানোর পথ পায় না।
সমাধানটি চোখে পড়ার মতো সরল। N(μ, σ2) থেকে নমুনা তোলা আর আগে N(০, ১) থেকে ε তুলে z = μ + σ·ε বানানো — বণ্টনের দিক থেকে হুবহু একই। কিন্তু দ্বিতীয় রূপে এলোমেলোতা পুরোটা ε-তে, যা কোনো parameter-এর ওপর নির্ভর করে না; আর z হয়ে যায় μ আর σ-এর একটি সাধারণ, অবকলনযোগ্য function: ∂z/∂μ = ১, ∂z/∂σ = ε. একে বলে reparameterization trick. মেশিন লার্নিং কোর্সের পর্ব ৪৮-এ black-box VI-তে এর সাধারণ রূপ দেখেছেন; নিচের ছবিতে VAE-র ভেতরে তার জায়গা।
VAE-র এক ধাপ আর gradient-এর পথ
One VAE training step; the gradient reaches μ and σ through z = μ + σ·ε, never through ε
এবার একটি ছোট উদাহরণে gradient-এর পথ সংখ্যা দিয়ে। Decoder-এর loss-কে সরল রাখতে ধরা হয়েছে L(z) = (z − ১)2, যেন decoder চায় z = ১ হোক। Encoder-এর output μ = ০.৮, σ = ০.৫, আর তোলা ε = −১.২.
| ধাপ | |
| হিসাব |
- সরাসরি নমুনা. “N(০.৮, ০.২৫) থেকে একটি সংখ্যা তোলো” — এই কাজের ভেতরে μ আছে, কিন্তু তোলা সংখ্যাটি μ-এর একটি মসৃণ function হিসেবে লেখা নেই। Autograd এখানে থেমে যায়।
- ε আলাদা. একই বণ্টন, অন্যভাবে লেখা: ε = −১.২ তোলা হলো N(০, ১) থেকে, তারপর z = ০.২. এখন z হলো μ আর σ-এর সাধারণ যোগ-গুণ।
- Loss. L = ০.৬৪. Chain rule-এর প্রথম কড়া dL/dz = −১.৬: z বাড়লে loss কমবে।
- μ-তে. z-এ μ সরাসরি যোগ হয়েছে, তাই ∂z/∂μ = ১ আর dL/dμ = −১.৬. Gradient ε পার হয়ে নয়, তার পাশ দিয়ে μ-তে পৌঁছাল।
- σ আর lv-তে. ∂z/∂σ = ε, তাই dL/dσ = −১.৬ × −১.২ = ১.৯২. Encoder দেয় lv, আর σ = elv/2, তাই আরেকটি কড়া σ/২ = ০.২৫: dL/dlv = ০.৪৮. এই ε-তে σ কমালে loss কমত, কারণ ε ঋণাত্মক বলে বড় σ z-কে ১ থেকে আরও দূরে ঠেলেছে।
- KL-এর gradient. KL = ½(μ2 + elv − ১ − lv) থেকে সরাসরি: ∂/∂μ = μ = ০.৮ (কেন্দ্রের দিকে টান), ∂/∂lv = ½(elv − ১) = −০.৩৭৫ (σ-কে ১-এর দিকে বাড়ানোর টান)। এখানে কোনো নমুনা লাগে না।
- এক ধাপ. দুই পদ যোগ: gμ = −০.৮, glv = ০.১০৫. Learning rate ০.১-এ gradient descent: μ বাড়ে ০.৮৮-এ, lv সামান্য কমে, σ প্রায় ০.৪৯৭. Reconstruction μ-কে ১-এর দিকে টানে, KL ০-এর দিকে; ফল মাঝামাঝি।
- একটি ε-এর সীমা. হুবহু গড় loss (μ − ১)2 + σ2, যার gradient −০.৪ আর ১.০. একটি ε-এর উত্তর (−১.৬, ১.৯২) দিক ঠিক রাখলেও মাপে আলাদা; অনেক ছবি আর অনেক ধাপে গড় নিলে ঠিক হয়। নিচের কোডে এই গড় আর একটি বিকল্প পদ্ধতির তুলনা।
Reparameterization ছাড়াও gradient পাওয়ার একটি পুরোনো পথ আছে: score-function estimator (REINFORCE নামেও পরিচিত), যা ∇μ E[f(z)] = E[f(z) · ∇μ log q(z)] ব্যবহার করে। এতে f-এর অবকলন লাগে না, তাই discrete z-এও চলে, কিন্তু সাধারণত অনেক বেশি ওঠানামা করে। নিচের ব্লকে দুটিকে একই সমস্যায় পাশাপাশি রাখা হয়েছে।
import numpy as np
rng = np.random.default_rng(0)
mu, sd = 0.8, 0.5
f = lambda z: (z - 1.0) ** 2 # খেলনা decoder-loss
# হুবহু: E[f] = (mu - 1)^2 + sd^2
print("হুবহু gradient: μ", round(2 * (mu - 1), 3), " σ", 2 * sd)
eps = -1.2 # stepper-এর একটি ε
z = mu + sd * eps
print("একটি ε = -1.2: z", round(z, 2), " μ", round(2 * (z - 1), 3),
" σ", round(2 * (z - 1) * eps, 3))
for n in (10, 1000, 100_000):
e = rng.standard_normal(n)
z = mu + sd * e
rep_mu = 2 * (z - 1) # pathwise: df/dz · dz/dμ
rep_sd = 2 * (z - 1) * e # pathwise: df/dz · dz/dσ
sf_mu = f(z) * (z - mu) / sd**2 # score-function (REINFORCE)
print(f"n={n:>6} reparam μ {rep_mu.mean():+.3f} σ {rep_sd.mean():+.3f}"
f" | score μ {sf_mu.mean():+.3f}")
e = rng.standard_normal(100_000)
z = mu + sd * e
print("প্রতি নমুনার std (μ-এর gradient):")
print(" reparam", round((2 * (z - 1)).std(), 3),
" score", round((f(z) * (z - mu) / sd**2).std(), 3))হুবহু gradient −০.৪ (μ) আর ১.০ (σ); stepper-এর একটি ε = −১.২ দিয়েছে −১.৬ আর ১.৯২, দিক ঠিক কিন্তু মাপ অনেক দূরে। ১০টি নমুনার গড়ে reparameterization −০.৩১৫ আর ০.৫২৩, ১,০০০টিতে −০.৪৫০ আর ০.৯৭৬, ১,০০,০০০টিতে −০.৪০০ আর ১.০০০ — হুবহু মানে পৌঁছায়। Score-function পথও শেষে −০.৪০২-এ আসে, অর্থাৎ দুটিই গড়ে ঠিক (unbiased), কিন্তু ১০টি নমুনায় সে দিয়েছে −০.১৭৩. শেষ দুই লাইনে আসল পার্থক্য: প্রতি নমুনায় reparameterization-এর std ১.০০৩, score-function-এর ২.০৯২, অর্থাৎ variance প্রায় ৪.৪ গুণ; একই নির্ভুলতার জন্য প্রায় চার গুণ নমুনা লাগত। বড় মডেলে এই ব্যবধান সাধারণত আরও বাড়ে, তাই continuous latent-এ VAE reparameterization ব্যবহার করে।
৫ · সত্যিকারের run — MNIST আর Fashion-MNIST-এ VAE
এবার আসল আকারে, পর্ব ৭৪-এর হুবহু একই শর্তে, যাতে সংখ্যাগুলো সেখানকার মডেলের পাশে বসানো যায়। বাজারঘরের পণ্যের ছবির বদলে Fashion-MNIST (Zalando Research, MIT licence): জামা, জুতা, ব্যাগের ২৮×২৮ ধূসর ছবি; এগুলো আসল দোকানের ছবি নয়, একটি পরিচিত গবেষণা-ডেটাসেট। পাশে MNIST-এর হাতের লেখা সংখ্যা (এর licence সাধারণত CC BY-SA 3.0 বলা হয়; নিজের কাজে ব্যবহারের আগে উৎস দেখে নিন)। প্রতিটি pixel ১২৮ সীমায় ০/১; train-এর প্রথম ৫৫,০০০টি ছবিতে শেখা, শেষ ৫,০০০টি val, আর test-এর ১০,০০০টি ছবি সবশেষে একবার মাপা।
নকশা: encoder ৭৮৪ → ৫১২ → ২৫৬ (ReLU), তারপর দুটি আলাদা layer থেকে μ আর log σ2; decoder উল্টো পথে ২৫৬ → ৫১২ → ৭৮৪টি logit. Latent ২ আর ১৬ মাত্রা। Adam (lr ০.০০১), batch ১২৮, ৫০ epoch; val-এর −ELBO সবচেয়ে কম যে epoch-এ, সেই weight রাখা হয়েছে। Python 3.12.13, torch 2.15.0 (CPU build), numpy 2.5.4; সব CPU-তে, ৩টি thread-এ (লেখার সময়ের version), অন্য কাজের সঙ্গে ভাগ করা যন্ত্রে। মূল অংশ (vae.py থেকে ছাঁটা ও সংক্ষেপিত; আসল ফাইলে encoder আলাদা method-এ):
def forward(self, x): # x: (batch, 784), 0/1
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 # reparameterisation
logits = self.dec(z)
rec = F.binary_cross_entropy_with_logits(logits, x, reduction="none").sum(1)
kl = 0.5 * (mu.pow(2) + logvar.exp() - 1 - logvar).sum(1)
return rec, kl # প্রতি ছবি, nats
rec, kl = model(xb)
loss = (rec + beta * kl).mean() # batch-এর গড়
opt.zero_grad(); loss.backward(); opt.step()খেয়াল করুন, rec আর kl দুটোই প্রতি ছবিতে যোগফল (sum), তারপর batch-এ গড়। একটিকে pixel-গড় আর অন্যটিকে যোগফল নিলে KL-এর ওজন অজান্তে ৭৮৪ গুণ বদলে যায় — AI-এর লেখা VAE কোডের একটি চেনা ভুল। MNIST, ১৬ মাত্রা, seed ৪২-এর লগ (ছাঁটা; প্রতি লাইনে val-এর reconstruction, KL, −ELBO আর তার bits/dim):
$ python vae.py mnist 16 1 42 50 # data z_dim beta seed epochs
mnist_z16_b1_s42 params=1079600 torch=2.15.0+cpu
epoch beta_eff val_rec val_kl val_negelbo val_bpd sec
1 1.00 113.09 15.41 128.50 0.2365 2
5 1.00 72.02 20.81 92.83 0.1708 12
10 1.00 65.12 22.27 87.39 0.1608 29
20 1.00 60.89 23.17 84.06 0.1547 57
30 1.00 59.26 23.61 82.87 0.1525 86
40 1.00 58.32 24.31 82.63 0.1521 134
50 1.00 58.40 23.85 82.25 0.1514 171
{"data": "mnist", "z": 16, "beta": 1.0, "seed": 42, "warmup": 0, "epochs": 50, "best_epoch": 50, "params": 1079600, "test_rec": 58.2763265625, "test_kl": 23.7836359375, "test_negelbo": 82.05996250000001, "test_elbo_bpd": 0.1510044655029137, "test_iwae200_nats": 75.53347778320312, "test_iwae200_bpd": 0.13899460434913635, "iwae_s": 15.8, "active_units": 12, "fit_s": 170.9, …}
var_mu 1.038 0.980 0.989 1.031 0.001 1.004 1.047 0.907 0.001 0.893 0.001 0.001 0.975 0.931 0.902 0.974
kl_per_dim 2.37 2.24 2.43 2.77 0.00 1.84 2.33 1.44 0.00 1.41 0.00 0.00 1.80 1.60 1.56 1.99
শেষ লাইনের test_iwae200 হলো importance sampling-এ log p(x)-এর অনুমান। ELBO নিম্নসীমা মাত্র, তাই পর্ব ৭৪-এর হুবহু likelihood-এর পাশে বসাতে আরেকটু ভালো অনুমান দরকার। কৌশলটি importance sampling: q(z | x) থেকে K টি z তুলে p(x, z) / q(z | x) অনুপাতের গড় নেওয়া, যার log আবার একটি নিম্নসীমা, তবে K বাড়লে log p(x)-এর দিকে ওঠে (IWAE সীমা)। Prior থেকে নমুনা তুললে (সেকশন ১-এর “খড়ের গাদা”) কী হয়, সেটিও পাশে মাপা হয়েছে, প্রথম ১,০০০টি test ছবিতে:
| অনুমান (১,০০০টি test ছবির গড়, nats) | MNIST, z = ১৬ | Fashion, z = ১৬ |
|---|---|---|
| Prior থেকে নমুনা, K = ১,০০০ | −১৮২.৩২ | −১৯০.৬৬ |
| ELBO (q থেকে একটি নমুনা, K = ১) | −৮১.৭২ | −১২০.৫২ |
| IWAE, K = ১০ | −৭৭.৪১ | −১১৬.২০ |
| IWAE, K = ১০০ | −৭৫.৬২ | −১১৪.৪৬ |
| IWAE, K = ১,০০০ | −৭৪.৪৫ (৮.৩ s) | −১১৩.৫৮ (৬.৮ s) |
সত্যিকারের run, seed ৪২-এর মডেল, test-এর প্রথম ১,০০০টি ছবি; সংখ্যা বড় মানে ভালো। K = ১-এর সারিটি নিজেই এলোমেলো: একই মডেলে আরেকবার মাপায় MNIST-এ −৮২.০৯ আর Fashion-এ −১২০.০৮ এসেছিল। বন্ধনীতে CPU-তে ১,০০০টি ছবির সময়। K বাড়ার সঙ্গে অনুমান একঘেয়ে ভাবে ওঠে, কিন্তু ধীর হয়ে আসে; K = ১,০০০-ও একটি নিম্নসীমা, তাই আসল log p(x) এর চেয়ে সামান্য বেশি হতে পারে।
এবার মূল টেবিল: পুরো ১০,০০০টি test ছবিতে, K = ২০০, পর্ব ৭৪-এর ফলের পাশে।
| মডেল | test rec (nats) | test KL (nats) | ELBO bits/dim | IWAE-200 bits/dim | fit সময় (s) |
|---|---|---|---|---|---|
| MNIST, z = ২ | ১২৭.৭৮ | ৭.২১ | ০.২৪৮৪ | ০.২৪০৮ | ১৮১ |
| MNIST, z = ১৬ | ৫৭.৭৫–৫৮.২৮ | ২৩.৭৪–২৪.০১ | ০.১৫০৪–০.১৫১০ | ০.১৩৯০–০.১৩৯৩ | ১৩৮–১৭১ |
| Fashion, z = ২ | ১৫৮.৮০ | ৭.৪৪ | ০.৩০৫৯ | ০.২৯৭২ | ১৮৩ |
| Fashion, z = ১৬ | ১০১.২৩–১০১.৫৯ | ২০.৭১–২১.০৫ | ০.২২৪৫–০.২২৫০ | ০.২১৩২–০.২১৩৭ | ১৩৬–১৪২ |
সব সংখ্যা এই যন্ত্রের সত্যিকারের run, একটি ভাগে, β = ১. z = ১৬-এর ঘরে seed ৪২, ৪৩, ৪৪-এর পরিসর; z = ২ শুধু seed ৪২. Test ১০,০০০টি ছবি, IWAE-এর K = ২০০ (পুরো test-এ প্রায় ১১–১৯ সেকেন্ড)। দুই নকশাতেই parameter প্রায় ১০.৭–১০.৮ লাখ (z = ১৬-এ ১০,৭৯,৬০০)। সময় CPU-তে, শুধু মাত্রা হিসেবে পড়ুন।
- পর্ব ৭৪-এর পাশে MNIST: স্বাধীন Bernoulli ০.৩৭৮৮, FVSBN ০.১৪০১, MADE ০.১২৭৩–০.১২৭৬ bits/dim. ১৬ মাত্রার VAE-র importance-sampled অনুমান ০.১৩৯০–০.১৩৯৩: FVSBN-এর প্রায় সমান, MADE-এর চেয়ে প্রায় ০.০১১–০.০১২ bits/dim (প্রতি ছবিতে প্রায় ৬.২–৬.৫ nats) খারাপ। শুধু ELBO (০.১৫১) বসালে ব্যবধান দ্বিগুণ দেখাত — তাই ELBO দিয়ে তুলনা অন্যায্য।
- Fashion-MNIST-এ: স্বাধীন Bernoulli ০.৭০৫০, PCA + GMM ০.৫৭৯০ (ঊর্ধ্বসীমা), FVSBN ০.২০৩১–০.২০৩২, MADE ০.১৯৩৫–০.১৯৩৬. VAE ০.২১৩২–০.২১৩৭: প্রথম দুটির চেয়ে অনেক ভালো, দুটি autoregressive মডেলের চেয়েই খারাপ। স্বাধীন pixel-এর decoder ছোট কিনারা আর নকশার খুঁটিনাটি ধরতে পারে না, যা autoregressive মডেল প্রতিবেশী pixel দেখে ধরে।
- বিনিময়ে কী পাওয়া গেল: নমুনা এক forward pass-এ (MADE-এ ৭৮৪টি ধাপ), আর ১৬টি সংখ্যার একটি ব্যবহারযোগ্য latent কোড (সেকশন ৬)।
- Seed আর মাত্রা: তিন seed-এর পার্থক্য ০.০০০৬ bits/dim-এর মধ্যে। ২ মাত্রায় VAE অনেক খারাপ (MNIST ০.২৪১), কারণ দুটি সংখ্যায় একটি ছবির বেশি তথ্য ধরা যায় না; তবু তা স্বাধীন Bernoulli-র চেয়ে ভালো। MNIST-এর একটি run-এর সেরা epoch শেষটি (৫০), তাই আরও epoch-এ হয়তো সামান্য উন্নতি হতো; পরীক্ষা করা হয়নি। প্রবন্ধের এলোমেলোভাবে ০/১ করা MNIST-এর সংখ্যার সঙ্গে এগুলো মেলানো যাবে না।
Epoch ধরে reconstruction আর KL কীভাবে বদলায়, আর ২-মাত্রার VAE ছবিগুলোকে latent-এ কোথায় বসায়, নিচের ব্লকে সত্যিকারের লগ আর encoder output থেকে আঁকা হয়েছে (val অংশ; MNIST-এর ২-D মানচিত্রে ২০০টি এলোমেলো test ছবির μ)।
import pandas as pd
import matplotlib.pyplot as plt
c = pd.read_csv("vae_curves.csv") # val, প্রতি epoch, nats
m = pd.read_csv("map2d.csv") # MNIST z=2: ২০০টি test ছবির μ
c["negelbo"] = c.rec + c.kl
for run, g in c.groupby("run", sort=False):
b = g.loc[g.negelbo.idxmin()]
print(f"{run:12s} সেরা epoch {int(b.epoch):2d}: rec {b.rec:6.2f}"
f" KL {b.kl:5.2f} -ELBO {b.negelbo:6.2f}")
print("epoch 1-এ KL:", c[c.epoch == 1].set_index("run").kl.to_dict())
print("২-D μ-এর std:", m[["z1", "z2"]].std().round(2).tolist())
print("শ্রেণিভিত্তিক গড় μ (০, ১, ৭):")
print(m.groupby("label")[["z1", "z2"]].mean().loc[[0, 1, 7]].round(2))
fig, ax = plt.subplots(1, 3, figsize=(11, 3.4))
for run, g in c.groupby("run", sort=False):
ax[0].plot(g.epoch, g.rec, label=run)
ax[1].plot(g.epoch, g.kl, label=run)
ax[0].set_title("val reconstruction (nats)")
ax[1].set_title("val KL (nats)")
for a in ax[:2]:
a.set_xlabel("epoch"); a.legend(fontsize=8)
sc = ax[2].scatter(m.z1, m.z2, c=m.label, cmap="tab10", s=8)
ax[2].set_title("MNIST z=2: encoder mean")
ax[2].set_xlabel("z1"); ax[2].set_ylabel("z2")
fig.colorbar(sc, ax=ax[2], ticks=range(10))
fig.subplots_adjust(wspace=0.4)
plt.show()তিনটি run-এর সেরা val epoch মিলেছে টেবিল আর লগের সঙ্গে (এই ফাইলে সংখ্যা এক দশমিকে ছাঁটা, তাই শেষ ঘরে সামান্য তফাত)। বাঁয়ের ছবিতে reconstruction শুরুতে দ্রুত নামে, তারপর ধীরে। মাঝের ছবিতে উল্টো: warm-up ছাড়া KL শুরুতে ছোট (MNIST-এ ১৫.৪) আর ধীরে বাড়ে, কারণ encoder ধীরে ধীরে z-এ তথ্য রাখতে শেখে; warm-up-এ প্রথম epoch-এ ৪৭.২, তারপর β বাড়তেই নেমে প্রায় ২৭-এ থিতু, তবু warm-up ছাড়া run-এর (প্রায় ২৪) ওপরে। ডানের মানচিত্রে ২-মাত্রার VAE কোনো label না দেখেই অঙ্কগুলোকে আলাদা অঞ্চলে বসিয়েছে: ০-এর গড় μ (−১.৭৭, ০.৬২), ১-এর (১.৬৫, −০.৯৭), ৭-এর (০.৯০, ০.৯৯). μ-এর std দুই অক্ষেই প্রায় ১ (১.০২ আর ০.৯৮), অর্থাৎ KL পদ কোডগুলোকে N(০, ১)-এর মাপে ধরে রেখেছে। মাঝখানে কয়েকটি শ্রেণি পরস্পরের ওপর পড়েছে, যা ২ মাত্রার সীমা।
৬ · Latent space — নমুনা, interpolation আর মানচিত্র
নমুনা: prior N(০, I) থেকে z তুলে decoder চালানো — encoder লাগে না। Interpolation: দুটি ছবির μ-এর মাঝখানের সরলরেখায় কয়েকটি বিন্দু নিয়ে decode করা; KL পদ latent-কে ফাঁকহীন রাখে বলে মাঝখানের বিন্দুগুলোও সাধারণত বিশ্বাসযোগ্য ছবি দেয়। মানচিত্র: ২-মাত্রার latent-এ একটি নিয়মিত grid decode করে দেখা, কোন অঞ্চলে কী থাকে।
নিচের ব্লক সত্যিকারের run-এর ছবি আঁকে (seed ৪২-এর মডেল, সেরা val epoch-এর weight): প্রথম সারিতে MNIST-এর ১৬-মাত্রার VAE থেকে ৮টি নমুনা, দ্বিতীয় সারিতে Fashion-MNIST-এর, তৃতীয় সারিতে MNIST-এর একটি “১” থেকে একটি “৭”-এ interpolation (test-এর প্রথম ১ আর প্রথম ৭), আর নিচে MNIST-এর ২-মাত্রার VAE-তে z1, z2 ∈ {−২, −১, ০, ১, ২}-এর ৫×৫ grid. প্রতিটি ছবি decoder-এর pixel-probability, ১৬ ধাপে (৪ bit) চেপে base64-এ পাতায় রাখা; তাই এগুলো নমুনার “গড় ছবি”, ০/১ টস করা ছবি নয়।
import base64
import numpy as np
import matplotlib.pyplot as plt
rows = {}
for line in open("vae_imgs.txt"):
name, data = line.strip().split(":")
imgs = []
for b in data.split(","):
v = np.frombuffer(base64.b64decode(b), np.uint8)
imgs.append(np.stack([v >> 4, v & 15], 1).reshape(28, 28) / 15)
rows[name] = np.array(imgs) # 4-bit → pixel probability
fig, axes = plt.subplots(8, 8, figsize=(8, 8.4))
for ax in axes.ravel():
ax.axis("off")
for r, name in enumerate(["mnist_samples", "fashion_samples", "mnist_interp"]):
for c in range(8):
axes[r, c].imshow(rows[name][c], cmap="gray_r", vmin=0, vmax=1)
print(f"{name:16s} গড় pixel probability {rows[name].mean():.3f}")
g = rows["mnist_grid"] # z1, z2 ∈ {-2, -1, 0, 1, 2}
for i in range(25):
axes[3 + i // 5, i % 5].imshow(g[i], cmap="gray_r", vmin=0, vmax=1)
axes[3, 6].text(0, 0.5, "5x5 grid\nof z=2\nlatent\n(top: z2=-2)", fontsize=9)
print("grid-এর প্রতিটি ছবির ‘কালি’ (সারি = z2, কলাম = z1):")
print(g.reshape(5, 5, -1).mean(2).round(2))
fig.subplots_adjust(wspace=0.05, hspace=0.05)
plt.show()গড় pixel probability-গুলো (MNIST নমুনা ০.১২৯, Fashion ০.৩৪৮) দেখায় Fashion-এর ছবিতে কালির ভাগ অনেক বেশি, যা ডেটার স্বভাব। ছবিতে MNIST-এর নমুনায় ৭, ৮, ০, ৬, ২, ৩ চেনা যায়, কয়েকটি আধাআধি (একটি ৩ আর ৮-এর মাঝামাঝি); Fashion-এ ব্যাগ, জুতা, জামা, প্যান্ট আর সোয়েটার আসে, কিন্তু নকশা আর কিনারা ঝাপসা — সেকশন ৫-এর likelihood-এর ব্যবধান চোখেও দেখা যায় (কারণ ভাবনার প্রশ্ন ১-এ)। Interpolation-এ “১” প্রথম তিন ধাপে একটু একটু হেলে, চতুর্থ ধাপে হঠাৎ “৭” হয়ে যায়; মাঝে কোনো অর্থহীন ছবি নেই, কিন্তু রূপান্তর সমান গতিতে নয়। ৫×৫ grid-এ পাশাপাশি ঘরগুলো দেখতে কাছাকাছি: ডানের ওপরের দিকে ১, নিচে ০ থেকে ৯ হয়ে ৭, ওপরে বাঁয়ে ৫ আর ৩, আর grid-এর অবস্থানগুলো বাঁয়ের মানচিত্রের শ্রেণি-গড়ের সঙ্গে মেলে। ‘কালি’র ছাপা টেবিলে বাঁ দিকের ঘর (০ আর ৫-এর মতো চওড়া অঙ্ক) বেশি কালি নেয়, ডান ওপরের ১ সবচেয়ে কম (০.০৫)।
৭ · β-VAE, posterior collapse আর KL annealing
সেকশন ২-এর loss-এ KL-কে একটি সংখ্যা β দিয়ে গুণ করলে পাওয়া যায় β-VAE: loss = rec + β·KL. β = ১ হলে এটি ঠিক −ELBO. β > ১ মানে latent-এ তথ্য রাখার দাম বেশি; β < ১ মানে কম। কতগুলো latent মাত্রা আসলে কাজ করছে, তা মাপার প্রচলিত উপায় active units: test ছবিগুলোর ওপর Eq[zu] (অর্থাৎ μu)-এর variance ০.০১-এর বেশি হলে মাত্রা u সক্রিয় (Burda ও সহকর্মী, ২০১৬)। যে মাত্রায় μ সব ছবিতে প্রায় একই, encoder সেখানে কোনো ছবির খবর রাখছে না।
| ডেটা, β | test rec | test KL | −ELBO (β = ১ ধরে) | IWAE-200 bits/dim | active (১৬-এর মধ্যে) |
|---|---|---|---|---|---|
| MNIST, β = ০.৫ | ৪৯.৬১ | ৩৩.৬৭ | ৮৩.২৭ | ০.১৪১৯ | ১৫ |
| MNIST, β = ১ | ৫৮.২৮ | ২৩.৭৮ | ৮২.০৬ | ০.১৩৯০ | ১২ |
| MNIST, β = ৪ | ৮৭.৪৩ | ১০.৭৪ | ৯৮.১৭ | ০.১৫৯৭ | ৮ |
| Fashion, β = ০.৫ | ৯৫.২৬ | ২৭.৯৪ | ১২৩.২০ | ০.২১৫৪ | ১৩ |
| Fashion, β = ১ | ১০১.২৪ | ২১.০৫ | ১২২.২৯ | ০.২১৩৭ | ১১ |
| Fashion, β = ৪ | ১২৫.৪৬ | ৯.৯৫ | ১৩৫.৪০ | ০.২২৮৯ | ৭ |
সত্যিকারের run, z = ১৬, seed ৪২, ৫০ epoch, test ১০,০০০টি ছবি; rec, KL আর −ELBO nats-এ। −ELBO সবসময় β = ১ ধরে হিসাব করা (rec + KL), তাই সারিগুলো তুলনাযোগ্য। Active মাত্রা গোনা হয়েছে test-এ μ-এর variance > ০.০১ নিয়মে; সক্রিয় মাত্রাগুলোর variance ছিল প্রায় ০.৩৭ থেকে ১.২৫, নিষ্ক্রিয়গুলোর ০.০০১৪-এর বেশি নয়, মাঝামাঝি প্রায় কিছু নেই।
ছবিটা দুই ডেটাসেটেই একই। β বাড়ালে KL কমে (MNIST-এ ৩৩.৬৭ → ২৩.৭৮ → ১০.৭৪), reconstruction খারাপ হয় (৪৯.৬১ → ৫৮.২৮ → ৮৭.৪৩), আর active মাত্রা কমে (১৫ → ১২ → ৮)। আসল ELBO আর likelihood-এর সেরা β = ১-এ, যেমনটা হওয়ার কথা, কারণ β = ১-ই ঠিক ELBO বাড়ায়। β = ০.৫ প্রায় সমান, কিন্তু সামান্য খারাপ: সে z-এ বেশি তথ্য রেখে reconstruction ভালো করেছে, কিন্তু তার দাম KL-এ বেশি দিয়েছে। β = ৪ সবচেয়ে কম তথ্য রাখে: ১৬টির মধ্যে মাত্র ৮টি মাত্রা কাজ করে। ছোট, “পরিষ্কার” latent চাইলে এটি কাজে লাগতে পারে, কিন্তু likelihood-এ প্রায় ০.০২ bits/dim-এর দাম দিতে হয়। খেয়াল করুন, β = ১-এও ১৬টির মধ্যে ৪–৫টি মাত্রা নিষ্ক্রিয় ছিল, সব seed-এই।
নিষ্ক্রিয় মাত্রার চরম রূপের নাম posterior collapse: প্রায় সব মাত্রায়, প্রায় সব ছবির জন্য q(z | x) prior-এর সমান হয়ে যায়, আর decoder z-কে উপেক্ষা করে। আমাদের MLP decoder z ছাড়া চলতে পারে না, তাই collapse আংশিক থেকেছে। কিন্তু decoder যদি নিজেই আগের pixel বা শব্দ দেখে অনুমান করতে পারে (autoregressive decoder), z ছাড়াই reconstruction ভালো হয় আর KL-এর দাম এড়াতে মডেল z ফেলে দিতে পারে — ভাবনার প্রশ্ন ২-এ এর ইতিহাস।
একটি প্রচলিত প্রতিকার KL annealing (KL warm-up নামেও পরিচিত): শুরুতে β = ০ থেকে ধীরে ধীরে ১-এ তোলা, যাতে encoder আগে কাজের তথ্য রাখতে শেখে, তারপর KL-এর চাপ আসে। আরেকটি free bits: প্রতিটি মাত্রায় একটি ন্যূনতম KL পর্যন্ত দণ্ড না দেওয়া। MNIST-এ, ১৬ মাত্রায়, প্রথম ১০ epoch-এ β-কে ০.১ থেকে ১-এ রৈখিকভাবে তুলে একটি run চালানো হয়েছে:
$ python vae.py mnist 16 1 42 50 10 # শেষে warm-up epoch
mnist_z16_b1_s42_w10 params=1079600 torch=2.15.0+cpu
epoch beta_eff val_rec val_kl val_negelbo val_bpd sec
1 0.10 94.05 47.19 141.24 0.2599 2
2 0.20 71.25 42.01 113.26 0.2084 5
5 0.50 59.93 32.80 92.73 0.1706 12
10 1.00 58.79 27.29 86.09 0.1584 25
20 1.00 56.10 27.23 83.33 0.1533 51
46 1.00 53.82 27.21 81.03 0.1491 121
50 1.00 53.59 27.72 81.31 0.1496 133
{"data": "mnist", "z": 16, "beta": 1.0, "seed": 42, "warmup": 10, "epochs": 50, "best_epoch": 46, …, "test_rec": 53.32564375, "test_kl": 27.162065625, "test_negelbo": 80.487709375, "test_elbo_bpd": 0.14811124893855182, "test_iwae200_nats": 73.34008026123047, "test_iwae200_bpd": 0.13495837152004242, …, "active_units": 16, …}
var_mu 0.979 0.874 0.941 1.082 0.937 1.073 1.053 0.976 0.845 1.020 1.030 0.974 0.843 0.901 0.907 0.849
প্রথম epoch-এ β মাত্র ০.১, তাই KL ৪৭.১৯ পর্যন্ত উঠেছে (warm-up ছাড়া একই epoch-এ ছিল ১৫.৪১): encoder শুরুতেই অনেক তথ্য রাখতে শিখেছে। β = ১-এ পৌঁছানোর পরে KL কমে প্রায় ২৭-এ থিতু হয়, কিন্তু কোনো মাত্রা নিষ্ক্রিয় হয়নি — ১৬টির ১৬টিই সক্রিয়, warm-up ছাড়া তিন seed-এ ছিল ১২টি করে। Test-এ −ELBO ৮০.৪৯ (warm-up ছাড়া ৮১.৭৫–৮২.০৬), importance-sampled অনুমান ০.১৩৫০ bits/dim (০.১৩৯০–০.১৩৯৩-এর জায়গায়)। তবে এটি একটি seed-এর একটি run; সিদ্ধান্তের আগে কয়েকটি seed আর warm-up-এর দৈর্ঘ্য বদলে দেখা দরকার। আর MNIST-এর মতো সহজ ডেটায় MLP decoder-এ সমস্যাটা ছোট; শক্তিশালী autoregressive decoder-এ (text-এ যেমন) প্রভাব অনেক বড় হতে পারে।
৮ · VAE-র পরিবার — conditional VAE আর VQ-VAE
মূল VAE থেকে অনেক রূপ এসেছে। দুটি এই মডিউলের জন্য জরুরি।
Conditional VAE
লেবেল বা অন্য শর্ত y encoder আর decoder দুজনকেই দেওয়া হয়: q(z | x, y), p(x | z, y). তখন নমুনা তোলার সময় বলে দেওয়া যায় “একটি ৭ দাও” বা “একটি জুতা দাও”, আর z ধরে বাকিটা — লেখার ধরন, আকার। Conditional VAE-এর প্রচলিত উৎস Sohn, Lee আর Yan-এর ২০১৫-র NeurIPS প্রবন্ধ। অনুশীলন ৪-এ কোডের পরিবর্তনটি আছে।
VQ-VAE
Latent continuous Gaussian নয়, বরং একটি শেখা তালিকার (codebook) সবচেয়ে কাছের vector. Encoder-এর output-কে সবচেয়ে কাছের codebook vector দিয়ে বদলে দেওয়াকে বলে vector quantization; ফলে প্রতিটি ছবি হয়ে যায় কয়েকটি পূর্ণসংখ্যার একটি grid, অনেকটা token-এর মতো।
VQ-VAE (van den Oord, Vinyals আর Kavukcuoglu, ২০১৭; প্রবন্ধের সারাংশ অনুযায়ী) সাধারণ VAE থেকে দুই জায়গায় আলাদা: কোড discrete, আর prior স্থির N(০, I) নয়, পরে আলাদা করে শেখা (যেমন কোডগুলোর ওপর একটি autoregressive মডেল)। “সবচেয়ে কাছেরটি বাছা” কাজটির অবকলন নেই, তাই training-এ সাধারণত straight-through estimator ব্যবহার হয়: forward-এ quantized vector, backward-এ gradient যেন quantization ছিলই না এমনভাবে সোজা encoder-এ পাঠানো — পর্ব ৮ (PyTorch autograd)-এ round করার উদাহরণে এই কৌশল দেখেছেন। সঙ্গে দুটি বাড়তি loss পদ codebook vector আর encoder output-কে পরস্পরের কাছে টানে। প্রবন্ধের সারাংশে এটাও বলা আছে যে এই নকশা শক্তিশালী autoregressive decoder-এর সঙ্গেও posterior collapse এড়াতে সাহায্য করে।
কেন এটি পর্ব ৮০ (Latent diffusion আর guidance)-এর সেতু? আজকের অনেক ছবি-বানানো ব্যবস্থা দুই ধাপে কাজ করে: একটি autoencoder ছবিকে অনেক ছোট latent-এ চাপে, আর আরেকটি মডেল সেই latent-এ নমুনা তোলা শেখে। Rombach ও সহকর্মীদের latent diffusion প্রবন্ধের সারাংশ বলে, তাঁরা diffusion চালিয়েছেন “pretrained autoencoder”-এর latent space-এ; সেই autoencoder-এর নিয়ন্ত্রক হিসেবে একটি হালকা KL পদ বা vector quantization ব্যবহারের কথা প্রবন্ধে আছে বলে জানা যায়। বিস্তারিত পর্ব ৮০-তে।
আরও গভীরে: Burda, Grosse আর Salakhutdinov-এর IWAE (ICLR ২০১৬) শুধু মূল্যায়নের জন্য নয়, training-এর লক্ষ্য হিসেবেও K-নমুনার সীমা ব্যবহার করে আর active unit-এর সংজ্ঞা দেয় (Covx(Eq[zu]) > ০.০১, যা এই পর্বে ব্যবহার হয়েছে)। Alemi ও সহকর্মীদের “Fixing a Broken ELBO” (ICML ২০১৮) ELBO-কে rate (KL) আর distortion (reconstruction)-এর ভাগ হিসেবে পড়ে, যা β-VAE-র trade-off-কে তথ্যতত্ত্বের ভাষা দেয়। আরও নমনীয় q-এর জন্য দেখুন q-এর ওপর normalizing flow আর hierarchical VAE.
পরীক্ষা ও ইন্টারভিউতে:
- VAE-র loss-এর দুই পদ কী? −Eq[log p(x | z)] (reconstruction) + KL(q(z | x) ‖ p(z)); একসাথে −ELBO.
- log p(x) আর ELBO-র পার্থক্য কী? KL(q(z | x) ‖ p(z | x)) ≥ ০; q আসল posterior হলে শূন্য।
- μ = ০.৮, σ = ০.৫-এর এক মাত্রার KL কত? ½(০.৬৪ + ০.২৫ − ১ + ১.৩৮৬৩) ≈ ০.৬৩৮ nats.
- Reparameterization trick কেন লাগে? নমুনা তোলার কাজ অবকলনযোগ্য নয়; z = μ + σ·ε লিখলে এলোমেলোতা ε-তে যায় আর gradient μ, σ-তে পৌঁছায়।
- Posterior collapse কী আর একটি প্রতিকার? q সব x-এ prior-এর মতো, z উপেক্ষিত; KL annealing বা free bits.
৯ · AI-এর যুগে — VAE-র কোড আর derivation যাচাই
AI সহকারী একটি VAE কয়েক সেকেন্ডে লিখে দেয়, আর সেই কোড প্রায়ই চলে — যা বিপদের অংশ, কারণ এখানকার ভুলগুলো চুপচাপ ভুল সংখ্যা দেয়, কোনো error নয়। চেনা ভুলগুলো: KL-এর চিহ্ন উল্টো বা ½ বাদ; log σ2-কে σ ভেবে বসানো; reconstruction-কে pixel-এর ওপর গড় (mean) আর KL-কে যোগফল (sum) ধরা, ফলে KL-এর ওজন অজান্তে ৭৮৪ গুণ বেড়ে যাওয়া; sigmoid-এর পরে আবার binary_cross_entropy_with_logits; test-এ model.eval() আর torch.no_grad() ভুলে যাওয়া; ELBO-কে সরাসরি log-likelihood বলে প্রকাশ করা; একটি seed-এর ফলকে সাধারণ সত্য বলা; আর derivation-এ বিশ্বাসযোগ্য কিন্তু ভুল একটি ধাপ বা বানানো উদ্ধৃতি। সমাধান: version-এর অফিসিয়াল docs, ছোট batch-এ যাচাই, আর প্রতিটি সূত্র খেলনা উদাহরণে হাতে মেলানো।
নিচের prompt কোডের সঙ্গে যাচাইয়ের প্রমাণও চেয়ে নেয়:
AI-এর লেখা VAE হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১VAE-র ছবি প্রায়ই ঝাপসা দেখায়, GAN-এর ছবি ঝকঝকে। এর আসল কারণ কী, আর সমাধানের চেষ্টাগুলো কী শিখিয়েছে?
তিনটি কারণ একসাথে কাজ করে, আর তিনটিই এই পর্বের সূত্রে লুকিয়ে আছে। প্রথমটি decoder-এর গঠন। আমাদের decoder প্রতিটি pixel-কে আলাদা, স্বাধীন Bernoulli ধরে (রঙিন ছবিতে সাধারণত স্বাধীন Gaussian ধরা হয়)। একটি z থেকে কয়েক রকম ছবি সম্ভব হলে — ধরুন একটি “৭” হালকা বা গাঢ়, একটু বাঁয়ে বা ডানে — decoder সবগুলোর গড় probability দেয়, কারণ pixel-গুলোর মধ্যে সমন্বয় রাখার কোনো উপায় তার নেই। গড় ছবি মানেই ঝাপসা কিনারা। Gaussian decoder-এ log-likelihood mean squared error-এর মতো আচরণ করে, যা অনিশ্চয়তায় গড়ের দিকে যায়।
দ্বিতীয়টি maximum likelihood-এর প্রকৃতি। পর্ব ৭৪-এ দেখেছেন, KL(p̂ ‖ q) কমানো mode-covering: মডেলকে সব ডেটা-অঞ্চলে কিছু probability রাখতেই হয়, ফলে মাঝখানের অসম্ভব অঞ্চলেও কিছু ছড়ায়। GAN এই চাপ থেকে মুক্ত, তাই সে কয়েকটি ধরন বাদ দিয়েও ঝকঝকে ছবি দিতে পারে — পর্ব ৭৭-এ এর দাম, mode collapse, দেখবেন। তৃতীয়টি KL পদ: q(z | x)-কে prior-এর দিকে টানলে আলাদা ছবির কোড একটু মিশে যায়, আর decoder মেশানো জায়গার জন্য আবার গড় ছবিই দেয়।
সমাধানের চেষ্টাগুলো এই তিন কারণকেই আক্রমণ করেছে। Decoder-কে autoregressive করা (PixelVAE জাতীয় কাজ) pixel-এর সমন্বয় ফেরায়, কিন্তু তাতে posterior collapse-এর ঝুঁকি আসে (প্রশ্ন ২)। Hierarchical VAE অনেক স্তরের latent দিয়ে q আর prior-কে নমনীয় করে; ২০২০-এর NVAE-এর মতো কাজ মুখের ছবিতেও বেশ তীক্ষ্ণ নমুনা দেখিয়েছে বলে রিপোর্ট আছে। আর সবচেয়ে প্রভাবশালী পথটি হলো কাজ ভাগ করা: VAE বা VQ-VAE শুধু ছবিকে একটি ছোট latent-এ চাপবে আর ফেরাবে, আর নমুনা তোলার কঠিন কাজ করবে আরেকটি শক্তিশালী মডেল (autoregressive বা diffusion) সেই latent-এর ওপর — পর্ব ৮০ ঠিক এই নকশা।
মূল উপলব্ধি: ঝাপসা ভাব VAE-র কোনো দুর্ঘটনা নয়; স্বাধীন pixel-এর decoder, mode-covering likelihood আর KL-এর টান মিলে গড় ছবির দিকে ঠেলে, আর প্রতিটি সমাধান এর একটি করে কারণ সরায়।
প্র ০২Posterior collapse কী, text-এর VAE-তে কেন এত বেশি দেখা যায়, আর প্রতিকারগুলো কতটা কাজ করে?
Posterior collapse মানে encoder-এর q(z | x) সব x-এর জন্য prior-এর মতো হয়ে যাওয়া: KL প্রায় শূন্য, আর decoder z-কে উপেক্ষা করে নিজের মতো কাজ চালায়। তখন VAE আর latent variable model থাকে না, একটি সাধারণ decoder-মডেলে পরিণত হয়। ধরা পড়ে KL আর active মাত্রা গুনলে, শুধু loss দেখে নয়।
২০১৫ সালে arXiv-এ আসা (আর ২০১৬-র CoNLL-এ প্রকাশিত) Bowman ও সহকর্মীদের “Generating Sentences from a Continuous Space” প্রবন্ধ বাক্যের জন্য LSTM decoder-ওয়ালা VAE চালাতে গিয়ে এই সমস্যার মুখোমুখি হয়। কারণটা যুক্তিতে সহজ। LSTM decoder আগের শব্দগুলো দেখে পরের শব্দ বেশ ভালো অনুমান করতে পারে, অর্থাৎ z ছাড়াই reconstruction ভালো। আর z-এ তথ্য রাখলে KL-এর দাম দিতে হয়। Training-এর শুরুতে encoder এলোমেলো, তার দেওয়া z decoder-এর কাছে শুধু noise; তাই সবচেয়ে সস্তা সমাধান হলো z উপেক্ষা করা আর KL শূন্যে নামানো। একবার এই অবস্থায় গেলে বের হওয়া কঠিন।
প্রতিকারগুলো এই হিসাব বদলায়। KL annealing (সেই প্রবন্ধে ব্যবহৃত বলে পরিচিত; এই পর্বের সেকশন ৭-এ মাপা) শুরুতে KL-এর ওজন শূন্য রেখে ধীরে বাড়ায়, যাতে encoder আগে কাজের তথ্য রাখতে শেখে। Decoder-এর ইনপুটে কিছু শব্দ লুকিয়ে রাখা (word dropout) decoder-কে z-এর ওপর নির্ভর করতে বাধ্য করে। Kingma ও সহকর্মীদের ২০১৬-র কাজে আসা free bits প্রতিটি latent মাত্রায় একটি ন্যূনতম KL “বিনা দণ্ডে” দেয়। আর ২০১৭-র VQ-VAE প্রবন্ধের সারাংশেই বলা আছে যে discrete কোড শক্তিশালী autoregressive decoder-এর সঙ্গেও posterior collapse এড়াতে সাহায্য করে। এগুলোর কোনোটিই সর্বজনীন সমাধান নয়; প্রতিটি নতুন hyperparameter আনে, আর ফল ডেটা ও decoder-এর ওপর নির্ভর করে। আমাদের MLP decoder-এ পুরো collapse হয়নি, কিন্তু আংশিক রূপ দেখা গেছে: ১৬টির কয়েকটি মাত্রা নিষ্ক্রিয়।
মূল উপলব্ধি: Posterior collapse হয় যখন decoder z ছাড়াই যথেষ্ট ভালো আর z ব্যবহারের দাম বেশি; তাই প্রতিকার হয় দাম কমানো (annealing, free bits) নয়তো decoder-কে z-নির্ভর করা।
প্র ০৩ELBO দিয়ে কি দুটি VAE বা একটি VAE আর একটি autoregressive মডেলকে তুলনা করা চলে?
সাবধানে, আর সাধারণত না। ELBO = log p(x) − KL(q ‖ posterior). দুটি VAE-র ELBO আলাদা হলে পার্থক্যটি দুই জায়গা থেকে আসতে পারে: মডেল নিজে (log p(x)) ভালো, নাকি শুধু encoder posterior-এর আরও কাছে পৌঁছেছে। একটি ভালো generative মডেলের সঙ্গে দুর্বল encoder থাকলে তার ELBO খারাপ দেখাবে, যদিও আসল likelihood ভালো। আর autoregressive মডেল (MADE, PixelCNN) হুবহু log p(x) দেয়; তার পাশে VAE-র ELBO বসালে VAE-কে পরিকল্পিতভাবে খারাপ দেখানো হয়।
এর প্রচলিত উত্তর importance sampling. Burda, Grosse আর Salakhutdinov ২০১৬ সালের ICLR-এর প্রবন্ধে (importance weighted autoencoder, IWAE) দেখান, K টি নমুনার গড় weight-এর log একটি নিম্নসীমা, যা K = ১-এ ELBO আর K বাড়লে log p(x)-এর দিকে একঘেয়ে ভাবে ওঠে। মূল্যায়নের জন্য তাই বড় K (শত বা হাজার) প্রচলিত। সেকশন ৫-এর run-এ এটি সরাসরি দেখা গেছে: MNIST-এর ১৬-মাত্রার মডেলে ১,০০০টি test ছবিতে ELBO প্রায় −৮১.৭, K = ১০-এ −৭৭.৪১, K = ১০০-এ −৭৫.৬২, K = ১,০০০-এ −৭৪.৪৫ nats. অর্থাৎ শুধু অনুমানের পদ্ধতি বদলে একই মডেল প্রায় ৭ nats “ভালো” হয়ে গেল। ELBO আর এই অনুমানের ফাঁক মানে encoder posterior থেকে কতটা দূরে, যাকে কখনো amortization gap আর approximation gap-এ ভাগ করে দেখা হয়।
তারপরও দুটি সতর্কতা। প্রথমত, বড় K-এর অনুমানও নিম্নসীমা, তাই VAE-কে সামান্য খারাপ দেখায়; এটি রক্ষণশীল ভুল। দ্বিতীয়ত, likelihood নিজেই সব কাজের মাপকাঠি নয়। Alemi ও সহকর্মীদের “Fixing a Broken ELBO” (২০১৮) দেখায় যে একই ELBO-তে খুব আলাদা মডেল থাকতে পারে — কেউ z-এ অনেক তথ্য রাখে (KL বেশি, reconstruction ভালো), কেউ প্রায় কিছুই না। Representation দরকার হলে প্রথমটি, শুধু density দরকার হলে দুটিই সমান। তাই VAE-র প্রতিবেদনে reconstruction আর KL আলাদা করে লেখা ভালো অভ্যাস, যা এই পর্বের টেবিলে করা হয়েছে।
মূল উপলব্ধি: ELBO শেখানোর জন্য, তুলনার জন্য নয়; তুলনায় বড় K-এর importance-sampled অনুমান, একই ডেটা আর ভাগে, আর সঙ্গে reconstruction ও KL আলাদা করে।
প্র ০৪β-VAE কি সত্যিই “disentangled” latent দেয় — প্রতিটি মাত্রা একটি অর্থবহ বৈশিষ্ট্য?
Higgins ও সহকর্মীরা ২০১৭ সালের ICLR-এ β-VAE প্রস্তাব করেন: loss-এ KL-কে β > ১ দিয়ে গুণ করা। তাঁদের যুক্তি ছিল, KL-এর চাপ বাড়লে q-কে prior-এর মতো স্বাধীন মাত্রায় ভাগ হতে হয়, আর latent-এর প্রতিটি মাত্রা ডেটার একটি আলাদা “কারণ” ধরতে চায় — যেমন একটি মাত্রা আকার, আরেকটি ঘোরানো, আরেকটি অবস্থান। কৃত্রিম, নিয়ন্ত্রিত ডেটাসেটে (যেখানে কারণগুলো জানা) তাঁরা এমন আচরণ দেখান, আর একে বলেন disentanglement. Representation learning-এ ধারণাটি খুব জনপ্রিয় হয়, কারণ ব্যাখ্যাযোগ্য latent দিয়ে নিয়ন্ত্রিত পরিবর্তন করা যায়।
তারপর একটি গুরুত্বপূর্ণ সংশোধন আসে। Locatello ও সহকর্মীরা ২০১৯ সালের ICML-এ বড় মাপের পরীক্ষা আর একটি তাত্ত্বিক ফল দেখান: মডেল বা ডেটায় কোনো অনুমান (inductive bias) বা কিছু তত্ত্বাবধান ছাড়া, label ছাড়া disentanglement নির্ভরযোগ্যভাবে শেখা মৌলিকভাবে অসম্ভব; আর তাঁদের পরীক্ষায় ভালো ফল seed আর hyperparameter-এর ওপর খুব নির্ভর করত। অর্থাৎ β বাড়ালেই অর্থবহ অক্ষ পাওয়া যায়, এমন নিশ্চয়তা নেই।
আমাদের সেকশন ৭-এর মাপ এর একটি সহজ দিক দেখায়: MNIST-এ ১৬টি মাত্রার মধ্যে β = ০.৫-এ ১৫টি, β = ১-এ ১২টি, β = ৪-এ মাত্র ৮টি সক্রিয় ছিল (seed ৪২)। β বেশি দিলে মডেল কম মাত্রা ব্যবহার করে, যা কখনো ব্যাখ্যায় সাহায্য করে, কিন্তু reconstruction আর likelihood-এর দামে। ২-মাত্রার latent মানচিত্রেও দেখবেন, অক্ষ দুটি কোনো একক বৈশিষ্ট্য (যেমন শুধু হেলানো) নয়, অঙ্কের পরিচয় আর লেখার ধরন মিশে আছে। বাজারঘরের পণ্যের “রং”, “আকার” আলাদা নিয়ন্ত্রণে label বা conditional VAE বেশি নির্ভরযোগ্য।
মূল উপলব্ধি: β-VAE তথ্যের বাজেট কমায়, অর্থ নিশ্চিত করে না; অর্থবহ latent চাইলে তত্ত্বাবধান বা নকশায় অনুমান দরকার, আর ফল একাধিক seed-এ যাচাই করতে হয়।
প্র ০৫Diffusion আর GAN-এর যুগে VAE কোথায় টিকে আছে? ইতিহাস থেকে কী বোঝা যায়?
VAE-র জন্ম দুটি প্রায় একই সময়ের কাজে। Kingma আর Welling-এর “Auto-Encoding Variational Bayes” arXiv-এ আসে ২০১৩-র ডিসেম্বরে (ICLR ২০১৪), আর Rezende, Mohamed ও Wierstra প্রায় একই সময়ে স্বাধীনভাবে একই ধারণা প্রকাশ করেন (ICML ২০১৪)। দুটির মূল অবদান একই: reparameterization দিয়ে encoder আর decoder একসাথে, সাধারণ backpropagation-এ শেখানো। এর আগে এমন মডেলে সাধারণত EM বা MCMC লাগত, যা বড় network-এ ধীর। মেশিন লার্নিং কোর্সের পর্ব ৪৮-এর VI-কে deep learning-এর স্কেলে নিয়ে আসা — এটাই ছিল বিপ্লব।
নমুনার মানে VAE পরে GAN আর তারপর diffusion-এর পেছনে পড়ে যায় (প্রশ্ন ১)। কিন্তু তিনটি জায়গায় এটি আজও কেন্দ্রে। প্রথমত, compression-এর ধাপ হিসেবে: Rombach ও সহকর্মীদের latent diffusion প্রবন্ধ (CVPR ২০২২)-এর সারাংশ অনুযায়ী diffusion চালানো হয় “pretrained autoencoder”-এর latent space-এ; প্রবন্ধের বর্ণনায় সেই autoencoder একটি হালকা KL পদ বা vector quantization দিয়ে নিয়ন্ত্রিত, অর্থাৎ VAE পরিবারের আত্মীয় (পর্ব ৮০)। দ্বিতীয়ত, discrete token হিসেবে: VQ-VAE (van den Oord ও সহকর্মী, ২০১৭)-এর ধারণা ছবিকে token-এ ভাঙে, যার ওপর transformer ভাষার মতো চলতে পারে। তৃতীয়ত, যেখানে অনিশ্চয়তা বা মসৃণ latent দরকার: anomaly detection, বৈজ্ঞানিক ডেটা, আর অণুর নকশার মতো ক্ষেত্রে latent-এ খোঁজ চালানোর গবেষণা।
ইতিহাস থেকে একটি শিক্ষা: generative modeling-এ পরিবারগুলো একে অন্যকে সরিয়ে দেয় না, বরং জুড়ে যায়; প্রতিটি পরিবার যে অংশে সবচেয়ে ভালো, সেখানে বসে। তবে কোন বাণিজ্যিক ব্যবস্থা ঠিক কী ব্যবহার করে, তা তাদের নথি ছাড়া বলা যায় না।
মূল উপলব্ধি: VAE আজ প্রায়ই চূড়ান্ত নমুনাদাতা নয়, কিন্তু latent space বানানো আর অনিশ্চয়তাসহ representation-এর জন্য সে এখনো একটি মৌলিক অংশ।
অনুশীলন
হাতে হিসাব, আউটপুট অনুমান, একটি দাবির সমালোচনা আর একটি কোড পরিবর্তন।
-
হাতে হিসাব: (ক) দুই মাত্রার latent-এ encoder দিল μ = (১.০, ০), σ = (০.৫, ১). মোট KL(q ‖ N(০, I)) কত nats? (খ) ০/১ করা ২৮×২৮ MNIST-এ একটি VAE-র test-এ গড় reconstruction পদ (BCE যোগফল) ৫৮.২৮ nats আর গড় KL ২৩.৭৮ nats. −ELBO আর তার bits/dim কত? (গ) একই মডেলের importance-sampled অনুমান (K = ২০০) −৭৫.৫৩ nats. ELBO আর আসল log p(x)-এর ফাঁক সম্পর্কে কী বলা যায়?
(ক) মাত্রা ১: ½(১ + ০.২৫ − ১ − ln ০.২৫) = ½(০.২৫ + ১.৩৮৬৩) = ০.৮১৮১. মাত্রা ২: μ = ০, σ = ১, তাই ½(০ + ১ − ১ − ০) = ০. মোট ০.৮১৮১ nats.
(খ) −ELBO = ৫৮.২৮ + ২৩.৭৮ = ৮২.০৬ nats. Bits/dim = ৮২.০৬ ÷ ০.৬৯৩১ ÷ ৭৮৪ ≈ ০.১৫১০.
(গ) Importance-sampled অনুমানের গড়ও log p(x)-এর একটি নিম্নসীমা, তাই log p(x) ≥ প্রায় −৭৫.৫৩. ফলে ELBO-র ফাঁক, অর্থাৎ গড় KL(q(z | x) ‖ p(z | x)), অন্তত প্রায় ৮২.০৬ − ৭৫.৫৩ = ৬.৫৩ nats (প্রতি ছবিতে), আর K বাড়ালে আরও বড় দেখা যেতে পারে। এই সংখ্যাগুলো সেকশন ৫-এর সত্যিকারের run-এর।
-
আউটপুট অনুমান: নিচের কোড কী ছাপবে? (NumPy-র সাধারণ ছাপার নিয়ম ধরুন।)
import numpy as np mu = np.array([0.0, 1.0]) lv = np.array([0.0, np.log(0.25)]) # log σ² eps = np.array([1.0, -2.0]) z = mu + np.exp(0.5 * lv) * eps kl = 0.5 * (mu**2 + np.exp(lv) - 1 - lv) print(z, kl.round(4), round(float(kl.sum()), 4))σ = elv/2 = (১, ০.৫). z = (০ + ১ × ১, ১ + ০.৫ × (−২)) = (১, ০). KL: প্রথম মাত্রা ½(০ + ১ − ১ − ০) = ০; দ্বিতীয় ½(১ + ০.২৫ − ১ + ১.৩৮৬৩) = ০.৮১৮১. আউটপুট:
[1. 0.] [0. 0.8181] 0.8181. খেয়াল করুন, দ্বিতীয় মাত্রায় ε = −২ হওয়ায় z ঠিক ০-তে পড়েছে, কিন্তু KL তাতে বদলায় না: KL নির্ভর করে শুধু μ আর σ-এর ওপর, তোলা নমুনার ওপর নয়। -
দাবির সমালোচনা: একটি দল লিখেছে: “আমাদের VAE-র test ELBO ০.১৫১ bits/dim, MADE-এর ০.১২৭; তাই VAE অনেক খারাপ। β = ৪ মডেলের KL সবচেয়ে কম, তাই সেটিই সেরা encoder. নমুনা ঝাপসা বলে আমরা decoder-এর জায়গায় বড় একটি PixelCNN বসাব; একটি seed-এই যথেষ্ট।” প্রতিটি অংশের সমস্যা আর সঠিক পদ্ধতি লিখুন।
- ELBO বনাম likelihood: ELBO শুধু নিম্নসীমা; MADE-এর সংখ্যা হুবহু likelihood. ন্যায্য তুলনায় VAE-র জন্য importance-sampled অনুমান (বড় K) লাগে, একই ডেটা, একই ০/১ নিয়ম আর একই test ভাগে। সেকশন ৫-এর run-এ MNIST-এ এতে ব্যবধান প্রায় ০.০২৩ থেকে কমে প্রায় ০.০১১ bits/dim হয়েছে।
- KL কম মানেই ভালো নয়: KL কম মানে z-এ কম তথ্য; β = ৪-এ reconstruction খারাপ হয় আর active মাত্রা কমে। “সেরা” ঠিক হয় কাজ দিয়ে: likelihood, reconstruction, নাকি downstream probe-এর accuracy.
- শক্তিশালী autoregressive decoder: এতে posterior collapse-এর ঝুঁকি, z উপেক্ষিত হতে পারে। করলে KL annealing বা free bits, আর active মাত্রা গুনে দেখা।
- একটি seed: অন্তত তিনটি seed, val দিয়ে বাছাই, test একবার, পরিসরসহ।
-
কোড পরিবর্তন: সেকশন ৫-এর
VAEclass-কে conditional VAE বানান, যাতে নমুনা তোলার সময় কোন সংখ্যা (০–৯) চাই তা বলে দেওয়া যায়। কোন লাইন বদলাবেন, আর ELBO কেমন হবে?লেবেল y-কে one-hot (১০টি সংখ্যা) করে encoder আর decoder দুজনের ইনপুটের সঙ্গে জুড়ে দিন। Encoder-এর প্রথম layer
nn.Linear(D + 10, 512), decoder-এর প্রথম layernn.Linear(z + 10, 256); forward-এh = self.enc(torch.cat([x, y1h], 1))আরlogits = self.dec(torch.cat([z, y1h], 1)). লক্ষ্য হয় শর্তসাপেক্ষ ELBO: log p(x | y) ≥ Eq(z|x,y)[log p(x | z, y)] − KL(q(z | x, y) ‖ p(z)). Prior একই N(০, I) রাখা যায় (Sohn ও সহকর্মীদের ২০১৫-র রূপে prior-ও y-নির্ভর হতে পারে)। নমুনা: z তুলুন N(০, I) থেকে, y = ৭ বসান, decoder চালান। এখন z-কে আর অঙ্কের পরিচয় বহন করতে হয় না, তাই তা মূলত লেখার ধরন (হেলানো, মোটা-পাতলা) ধরে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি VAE-র এক training ধাপ সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Encoder একটি batch-এর প্রতিটি ছবি থেকে μ আর log σ² দেয়
- N(০, I) থেকে ε তুলে z = μ + σ·ε বানানো হয়
- Decoder z থেকে প্রতিটি pixel-এর logit দেয়
- প্রতি ছবিতে reconstruction (BCE যোগফল) আর KL হিসাব করে loss = rec + β·KL-এর batch-গড় নেওয়া হয়
- Backpropagation-এ gradient decoder, z হয়ে encoder পর্যন্ত যায়, আর optimizer weight বদলায়
একটি VAE-তে যেকোনো encoder q(z | x)-এর জন্য ELBO ≤ log p(x) কেন সত্য?
- Reconstruction পদ সবসময় ঋণাত্মক
- KL(q ‖ prior) সবসময় ধনাত্মক
- log p(x) − ELBO = KL(q(z | x) ‖ p(z | x)), যা কখনো ঋণাত্মক নয়
- Decoder-এ sigmoid ব্যবহার হয় বলে
এক মাত্রার latent-এ encoder দিল μ = ১, σ = ১. KL(N(μ, σ²) ‖ N(০, ১)) কত nats?
- ০.৫
- ১
- ০
- ১.৫
VAE-তে z = μ + σ·ε (ε ~ N(০, I)) লেখার আসল উদ্দেশ্য কী?
- z-এর বণ্টন বদলে আরও চওড়া করা
- এলোমেলোতা ε-তে সরিয়ে loss থেকে μ আর σ পর্যন্ত gradient পৌঁছানো
- KL পদ হিসাব করার জন্য নমুনা দরকার বলে
- Test-এর সময় এলোমেলোতা পুরো বন্ধ করা
একটি VAE-তে loss = reconstruction + β·KL. β = ১ থেকে β = ৪ করলে সাধারণত কী দেখা যায়?
- KL বাড়ে, reconstruction ভালো হয়
- কিছুই বদলায় না, কারণ β শুধু একটি স্কেল
- β = ১-এর ELBO উন্নত হয়
- KL কমে, reconstruction খারাপ হয়, active latent মাত্রা কমে
Text-এর জন্য একটি VAE-তে শক্তিশালী autoregressive decoder (যেমন LSTM) বসানো হলো। Training-এর পরে প্রায় সব বাক্যে KL(q ‖ prior) প্রায় শূন্য। এর সবচেয়ে সম্ভাব্য ব্যাখ্যা কী?
- Encoder নিখুঁত posterior শিখে ফেলেছে
- Learning rate খুব কম ছিল, মডেল কিছুই শেখেনি
- Posterior collapse: decoder z ছাড়াই কাজ সারছে, তাই z উপেক্ষিত
- Prior হিসেবে N(০, I) নেওয়াটাই ভুল
একটি VAE-র test-এ ELBO −৮২.১ nats আর K = ২০০ নমুনার importance-sampled অনুমান −৭৫.৫ nats. কোন বক্তব্যটি ঠিক?
- মডেলের log p(x) হলো −৮২.১ nats
- log p(x) প্রায় −৭৫.৫ nats বা তার চেয়ে কিছু বেশি; ELBO-র ফাঁক অন্তত প্রায় ৬.৬ nats
- log p(x) −৮২.১-এর চেয়েও কম হতে পারে
- দুটি আলাদা মডেলের ফল, তুলনা চলে না
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- VAE = prior p(z), decoder p(x | z), encoder q(z | x); আসল posterior অজানা, কারণ p(x)-এর integral হিসাব করা যায় না।
- log p(x) = ELBO + KL(q ‖ posterior), আর ELBO = reconstruction − KL(q ‖ prior).
- Gaussian KL প্রতি মাত্রায় ½(μ² + σ² − ১ − ln σ²); মাত্রাগুলোর KL যোগ হয়।
- z = μ + σ·ε: এলোমেলোতা ε-তে, gradient μ আর σ-তে; score-function পথের চেয়ে ওঠানামা অনেক কম।
- β বাড়ালে KL কমে, reconstruction খারাপ হয়, active মাত্রা কমে; likelihood মাপতে ELBO নয়, importance sampling.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৭৭ · GAN — generator, discriminator আর প্রশিক্ষণের অস্থিরতাপরবর্তী পর্বLikelihood ছাড়া শেখা, minimax খেলা আর mode collapse.
- ডিপ লার্নিং, NLP আর Generative AI · কোর্স হোম৯১টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ৭৪ · Generative modeling-এর মানচিত্রআবার দেখুনBits per dimension আর একই ডেটায় MADE ও অন্যান্য মডেলের ফল।
- মেশিন লার্নিং কোর্সের পর্ব ৪৮ · Variational inferenceভিত্তিELBO, KL-এর দুই দিক, CAVI আর black-box VI.
- AI-এর ভিত্তি কোর্সের পর্ব ৩৮ · Entropy আর informationভিত্তিEntropy, cross-entropy আর KL divergence-এর মূল ধারণা।