সরাসরি পর্বে যান
পর্ব ৭৯ · ৯১-এর মধ্যে · মডিউল ১২

Diffusion model — DDPM, noise schedule আর denoising

Diffusion Models — Forward Noising, Noise Schedules, DDPM and DDIM Sampling

Taught in Bangla, this lesson derives the closed-form forward noising step, computes ᾱt and SNR for linear and cosine schedules, and explains the ε-prediction loss. It works one DDPM reverse step and one DDIM step in numbers, builds a small U-Net with time embedding and EMA, and trains it on Fashion-MNIST in a real desktop run on the GPU, comparing classifier-feature FID and sampling time for 1000-step DDPM and fewer-step DDIM, with in-page Python and NumPy.

এই পর্বে যা শিখবেন

  • Forward process-এর সংজ্ঞা আর closed-form q(xt | x0) কেন সম্ভব
  • Linear আর cosine noise schedule-এ ᾱt আর SNR হাতে হিসাব, আর schedule বাছাইয়ের যুক্তি
  • ε-prediction-এর সরল loss আর training-এর পুরো ধাপ
  • DDPM-এর এক reverse ধাপ আর DDIM-এর এক ধাপ সংখ্যায়, আর কম ধাপে sampling-এর দাম
  • Time embedding-সহ ছোট U-Net, EMA, আর Fashion-MNIST-এ নিজের DDPM শেখানো ও মাপা

এই মডিউলে এর আগে আরও কয়েক ধরনের generative model আসছে: পর্ব ৭৪ (Generative modeling-এর মানচিত্র) পুরো ছবি দেয়, পর্ব ৭৬ (Variational autoencoder) latent আর ELBO দেখায়, আর পর্ব ৭৭ (GAN) দুই network-এর প্রতিযোগিতা। Diffusion model এদের থেকে আলাদা একটি সরল চালাকি নেয়: ছবি বানানো কঠিন, কিন্তু ছবি নষ্ট করা খুব সহজ; তাই নষ্ট করার প্রতিটি ছোট ধাপ উল্টাতে শেখা যায় কি না। এই পর্বের তিনটি ব্লক ব্রাউজারের Python আর NumPy-তে চলে (প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে), আর একটি ব্লক ডেস্কটপের সত্যিকারের run-এর ছবি আঁকে। PyTorch-এর সব সংখ্যা ডেস্কটপে GPU-তে চালানো run থেকে।

১ · মূল ধারণা — noise মেশানো সহজ, ফেরানো শেখা

পুকুরে এক ফোঁটা লাল রং ফেলুন। কয়েক মিনিটে রং ছড়িয়ে পুরো পানি হালকা গোলাপি; কোথায় ফোঁটা পড়েছিল, আর বোঝা যায় না। রসায়নে একে বলে diffusion: অণুগুলো এলোমেলো ধাক্কায় ছড়িয়ে পড়ে। উল্টোটা, গোলাপি পানি থেকে ফোঁটাটি আবার এক জায়গায় জড়ো করা, প্রকৃতিতে নিজে থেকে ঘটে না। কিন্তু যদি প্রতিটি ছোট মুহূর্তের জন্য জানা থাকত “এই অবস্থা থেকে এক মুহূর্ত আগে অণুগুলো কোথায় ছিল”, তাহলে ছবিটি ধাপে ধাপে পিছিয়ে নেওয়া যেত। Diffusion model ঠিক এই “এক ধাপ পিছিয়ে যাওয়া” শেখে, ছবির pixel নিয়ে।

ধরুন বাজারঘরের পণ্যের একটি ছোট ধূসর ছবি x0 (এখানে ২৮ × ২৮ pixel, মান −১ থেকে ১)। প্রতিটি ধাপে ছবিটি একটু ছোট করে (√(1 − βt) দিয়ে গুণ) আর সামান্য Gaussian noise যোগ করুন, যার variance βt. βt ছোট সংখ্যা, যেমন ০.০০০১ থেকে ০.০২. হাজার ধাপ পরে xT কার্যত একটি standard normal noise, মূল ছবির কোনো চিহ্ন নেই। এটাই forward process; এতে কোনো parameter নেই, কিছু শেখারও নেই।

দুই দিকের শিকল

Forward process q adds noise; the learned reverse process pθ removes it step by step

Forward noising and learned reverse denoising chain q: noise যোগ (শেখার কিছু নেই) → ← pθ: network noise সরায় (শেখা হয়) x₀ x₁ xₜ … x_T training: x₀ থেকে xₜ এক লাফে
ছবি ৭৯.১ — কমলা তীর: প্রতিটি ধাপে একটু noise, শেষে শুধু noise. সবুজ তীর: network প্রতিটি ধাপে “এখানে কতটা noise আছে” অনুমান করে তা সরায়। বেগুনি ড্যাশ: training-এ মাঝের ধাপগুলো না চালিয়ে সূত্র দিয়ে সরাসরি xt বানানো যায়।

Reverse দিকটাই আসল কাজ। শুরু হয় একটি নতুন এলোমেলো noise xT থেকে, যা কোনো আসল ছবি থেকে আসেনি। প্রতিটি ধাপে একটি network দেখে “এই noisy ছবিতে noise-এর অংশ কোনটা”, আর সেই অনুমানের একটু সরিয়ে xt−1 বানায়। হাজার ধাপ পরে একটি নতুন ছবি, যা training ডেটার মতো দেখায় কিন্তু কোনো একটির কপি হওয়ার কথা নয় (কপি হয়ে যাওয়ার ঝুঁকি পর্ব ৮১-এর বিষয়)। একটি মূল সুবিধা: প্রতিটি ধাপ ছোট, তাই প্রতিটি উল্টো ধাপকেও প্রায় Gaussian ধরা যায়, আর network-কে শুধু তার গড়ের দিক শিখতে হয়। ২০১৫ সালে Sohl-Dickstein ও সহকর্মীরা non-equilibrium thermodynamics থেকে এই ধারণা প্রস্তাব করেন; ২০২০ সালে Ho, Jain আর Abbeel-এর DDPM প্রবন্ধ একে সরল loss আর ভালো ছবির মান দিয়ে জনপ্রিয় করে।

২ · Forward process — এক লাফে যেকোনো ধাপে

এক ধাপের নিয়ম লিখি। Normal distribution-এর চিহ্ন N(গড়, variance) দিয়ে:

q(xt | xt−1) = N(√(1 − βt)·xt−1, βt·I)

অর্থাৎ xt = √(1 − βt)·xt−1 + √βt·εt, যেখানে εt ~ N(0, I) প্রতিটি pixel-এ আলাদা standard normal noise, আর I মানে সব pixel-এর noise স্বাধীন। পরের অবস্থা শুধু এখনকার অবস্থার ওপর নির্ভর করে, তাই এটি একটি Markov chain. √(1 − β) দিয়ে ছোট করার কারণ: x-এর variance ১ হলে নতুন variance (1 − β)·১ + β = ১, তাই ধাপে ধাপে মান ফুলে ওঠে না। এজন্য একে “variance preserving” বলা হয়।

এখন সুবিধাটি। αt = 1 − βt আর ᾱt = α1·α2·…·αt (শুরু থেকে t পর্যন্ত গুণফল) লিখলে দুটি ধাপ মিলিয়ে দেখা যায়: x2 = √(α2α1)·x0 + (দুটি স্বাধীন Gaussian noise-এর একটি যোগফল)। স্বাধীন Gaussian-এর যোগফল আবার Gaussian, আর variance যোগ হয়: α2(1 − α1) + (1 − α2) = 1 − α1α2. একইভাবে আরোহ (induction) দিয়ে:

q(xt | x0) = N(√ᾱt·x0, (1 − ᾱt)·I)  ⇒  xt = √ᾱt·x0 + √(1 − ᾱt)·ε

এটি training-কে সস্তা করে: t = ৭৩৭-এর noisy ছবি চাইলে ৭৩৭ ধাপ চালাতে হয় না, একটি গুণ আর একটি যোগ। √ᾱt হলো মূল ছবির কতটা “সংকেত” টিকে আছে, √(1 − ᾱt) হলো noise-এর ভাগ, আর দুটির বর্গের যোগফল সবসময় ১. Gaussian-এর এই যোগের নিয়ম মেশিন লার্নিং কোর্সের পর্ব ১৯-এ আর sampling-এর মূল কথা AI-এর ভিত্তি কোর্সের পর্ব ৩৬-এ আছে।

৩ · Noise schedule — linear বনাম cosine, আর SNR

β1, …, βT-এর তালিকাকে বলে noise schedule. DDPM প্রবন্ধ T = ১০০০ আর β-কে ০.০০০১ থেকে ০.০২ পর্যন্ত সমান ফাঁকে (linear) নিয়েছিল। Nichol আর Dhariwal (২০২১) দেখান, ছোট ছবিতে এই schedule শেষের দিকের অনেক ধাপ নষ্ট করে, কারণ ছবি অনেক আগেই প্রায় পুরো noise হয়ে যায়। তাঁদের cosine schedule সরাসরি ᾱ ঠিক করে দেয়:

ᾱt = f(t) ÷ f(0),   f(t) = cos2( ((t/T + s) ÷ (1 + s)) · π/2 ),   s = 0.008

ছোট s যোগ করা হয় যাতে t = ০-এর কাছে β খুব ছোট না হয়ে যায়; আর বাস্তবে βt = 1 − ᾱt/ᾱt−1-কে ০.৯৯৯-এ কেটে দেওয়া হয়, যাতে শেষ ধাপে ভাগে সমস্যা না হয়। দুই schedule তুলনা করার সবচেয়ে কাজের মাপ হলো signal-to-noise ratio:

SNR(t) = ᾱt ÷ (1 − ᾱt)

রেডিওর অভিজ্ঞতা মনে করুন: SNR বেশি মানে গান পরিষ্কার, কম মানে শুধু ঝিরঝির। SNR = ১ মানে সংকেত আর noise সমান শক্তিশালী (ᾱ = ০.৫)। Linear schedule-এর জন্য হাতে একটি ভালো আসন্ন মান আছে: ছোট β-তে ln(1 − β) ≈ −β, তাই ᾱt ≈ e−Σβ, আর সমান ফাঁকের β-এর যোগফল Σβ = t·β1 + (βT − β1)·t(t − 1) ÷ (2(T − 1)).

Linear: ᾱ ≈ e^(−Σβ) · Cosine: কোণ = (t/T + s)/(1 + s) × 90°, ᾱ = cos²(কোণ) ÷ 0.99984
ধাপ
linear Σβ → ᾱ
linear SNR
cosine ᾱ
cosine SNR
  1. t = ১০০. দুটিতেই ছবি প্রায় অক্ষত। Linear-এ ᾱ ≈ ০.৮৯৭, cosine-এ ০.৯৭২; e−Σβ আসন্ন মান আসলের সঙ্গে চার দশমিক পর্যন্ত প্রায় মেলে।
  2. t = ২৫০. Linear-এ SNR ইতিমধ্যে ১.১০, মানে সংকেত আর noise প্রায় সমান; পুরো যাত্রার এক-চতুর্থাংশেই। Cosine-এ SNR এখনও ৫.৫৪.
  3. t = ৫০০. মাঝপথে linear-এ ᾱ মাত্র ০.০৭৯, ছবির ছায়া প্রায় নেই; cosine-এ ᾱ ≈ ০.৪৯৪, SNR প্রায় ১. Cosine ঠিক এটাই চায়: মাঝপথে সমান ভাগ।
  4. t = ৭৫০. Linear-এ SNR ০.০০৩৪: এখান থেকে শেষ পর্যন্ত আড়াইশো ধাপ প্রায় খাঁটি noise-এর ওপর, শেখার মতো সংকেত সামান্য। Cosine-এ তখনও ০.১৬৯.
  5. t = ১০০০. দুটিতেই প্রায় শূন্য। Linear-এ ᾱ ≈ ০.০০০০৪, পুরো শূন্য নয়, তাই শেষ ধাপেও ছবির একটি অতি ক্ষীণ ছাপ থাকে; বড় ছবির model-এ এই খুঁটিনাটি সমস্যা তৈরি করতে পারে বলে পরে আলোচনা হয়েছে (অংশ ৯).

নিচের ব্লক একই হিসাব NumPy-তে করে আর দুটি ছবি আঁকে: দুই schedule-এর ᾱt, আর একটি ৬৪ pixel-এর সারি (একটি জামার কাঁধ বরাবর একটি লাইন, পটভূমি −১, কাপড় ০.৮) কয়েকটি t-তে। সব t-তে একই ε ব্যবহার করা হয়েছে, যাতে শুধু ᾱ-এর প্রভাব চোখে পড়ে।

import numpy as np
import matplotlib.pyplot as plt

T, s = 1000, 0.008
beta = np.linspace(1e-4, 0.02, T)                  # DDPM-এর linear schedule
ab_lin = np.cumprod(1 - beta)                      # ᾱ_t = (1−β_1)(1−β_2)…(1−β_t)
f = np.cos((np.arange(T + 1) / T + s) / (1 + s) * np.pi / 2) ** 2
ab_cos = (f / f[0])[1:]                            # cosine schedule-এর ᾱ_t, t = 1..T
print("   t | linear: abar      SNR  | cosine: abar      SNR")
for t in [1, 100, 250, 500, 750, 900, 1000]:
    a, c = ab_lin[t - 1], ab_cos[t - 1]
    print(f"{t:4d} | {a:12.5f} {a/(1-a):9.3g} | {c:12.5f} {c/(1-c):9.3g}")
half = lambda ab: int(np.argmin(np.abs(ab - 0.5))) + 1
print("SNR = 1 (abar = 0.5) at t =", half(ab_lin), "(linear) and", half(ab_cos), "(cosine)")

rng = np.random.default_rng(7)                     # একটি সারির ছোট "ছবি": 64 pixel, [-1, 1]
x0 = np.where((np.arange(64) > 15) & (np.arange(64) < 48), 0.8, -1.0)
eps = rng.standard_normal(64)                      # সব t-তে একই ε, তুলনা সহজ করতে
fig, (a1, a2) = plt.subplots(1, 2, figsize=(10, 3.4))
a1.plot(ab_lin, label="linear"); a1.plot(ab_cos, label="cosine")
a1.set_xlabel("t"); a1.set_ylabel("alpha-bar"); a1.legend(); a1.set_title("Signal kept")
for k, t in enumerate([1, 100, 250, 500, 1000]):
    a = ab_lin[t - 1]
    xt = np.sqrt(a) * x0 + np.sqrt(1 - a) * eps    # closed form: q(x_t | x_0)
    a2.plot(xt - 3.2 * k, lw=1, label=f"t={t}")
a2.set_yticks([]); a2.set_title("One row noised (linear)"); a2.legend(fontsize=7, loc="lower left")
plt.show()

আউটপুট (পাতার Python-এ চালানো; সঙ্গে দুটি ছবি):

   t | linear: abar      SNR  | cosine: abar      SNR
   1 |      0.99990     1e+04 |      0.99996  2.42e+04
 100 |      0.89702      8.71 |      0.97209      34.8
 250 |      0.52409       1.1 |      0.84701      5.54
 500 |      0.07859    0.0853 |      0.49384     0.976
 750 |      0.00335   0.00336 |      0.14427     0.169
 900 |      0.00028  0.000275 |      0.02409    0.0247
1000 |      0.00004  4.04e-05 |      0.00000  3.75e-33
SNR = 1 (abar = 0.5) at t = 259 (linear) and 496 (cosine)

টেবিলটি stepper-এর সংখ্যাই, আরও কয়েকটি t-সহ। Linear-এ SNR ১-এ নামে t = ২৫৯-এ, অর্থাৎ যাত্রার প্রথম চার ভাগের এক ভাগেই; cosine-এ t = ৪৯৬-এ, প্রায় ঠিক মাঝপথে। t = ৯০০-এ linear-এর SNR প্রায় ০.০০০৩, cosine-এর প্রায় ০.০২৫. বাম ছবিতে linear-এর ᾱ বক্ররেখা শুরুতেই খাড়া নেমে অর্ধেক পথে প্রায় শূন্যে লেগে থাকে, cosine প্রায় সমান ঢালে নামে। ডান ছবিতে একই সারি: t = ১০০-এ জামার কাঁধের ধাপ পরিষ্কার, t = ২৫০-এ noise-এর মধ্যে আকারটা এখনও চেনা যায়, t = ৫০০-এ প্রায় হারিয়ে গেছে। শেষ সারিতে cosine-এর SNR একটি অতি ছোট সংখ্যা দেখায়: সূত্রে ᾱT ঠিক শূন্য হওয়ার কথা, যা আসে float-এর গোলকরণে; বাস্তবে β-কে ০.৯৯৯-এ কেটে দেওয়ায় ᾱT প্রায় 2.4 × 10−9 হয়।

৪ · Training-এর লক্ষ্য — noise অনুমান করা

Reverse ধাপ q(xt−1 | xt) সরাসরি জানা নেই, কারণ তার জন্য পুরো ডেটার distribution জানতে হয়। কিন্তু x0 জানা থাকলে q(xt−1 | xt, x0) একটি সুনির্দিষ্ট Gaussian, Bayes-এর নিয়মে হিসাব করা যায়। DDPM-এর পুরো derivation একটি variational bound থেকে আসে, যা পর্ব ৭৬ (Variational autoencoder)-এর ELBO-র মতো, আর মেশিন লার্নিং কোর্সের পর্ব ৪৮-এর ধারণার ওপর দাঁড়ানো। Bound-এর প্রতিটি পদ দুটি Gaussian-এর KL divergence, আর সেটি শেষ পর্যন্ত দুটি গড়ের দূরত্বে নামে। Ho ও সহকর্মীরা দেখান, গড়টিকে noise-এর অনুমান দিয়ে লিখলে আর পদগুলোর ওজন বাদ দিলে loss অসাধারণ সরল হয়:

Lsimple = Ex₀, t, ε [ ‖ ε − εθ( √ᾱt·x0 + √(1 − ᾱt)·ε, t ) ‖2 ]

এখানে εθ হলো network (θ তার weight), যে noisy ছবি আর ধাপ t দেখে noise অনুমান করে; ‖·‖2 মানে সব pixel-এর বর্গের যোগ, অর্থাৎ একটি সাধারণ mean squared error. Training-এর একটি ধাপ তাই:

  1. ডেটা থেকে একটি ছবি x0 (batch-এ অনেকগুলো) নিন।
  2. ১ থেকে T-এর মধ্যে সমান সম্ভাবনায় একটি t বাছুন।
  3. ছবির মাপের একটি noise ε ~ N(0, I) টানুন।
  4. xt = √ᾱt·x0 + √(1 − ᾱt)·ε বানান।
  5. Loss = (ε − εθ(xt, t))2-এর গড়; gradient নিয়ে optimizer-এর একটি ধাপ।

এটি পর্ব ১০-এর training loop-ই, শুধু লেবেল নিজেরাই বানানো: “উত্তর” হলো যে noise আমরা নিজেরা মিশিয়েছি। কোনো adversary নেই, যেমন GAN-এ থাকে, তাই training সাধারণত স্থির; loss একটি সরল regression. কেন ছবি নয়, noise অনুমান? গাণিতিকভাবে দুটি সমতুল্য: ε̂ জানলে x̂0 = (xt − √(1 − ᾱt)·ε̂) ÷ √ᾱt. কিন্তু loss-এর ওজন আলাদা দাঁড়ায়, আর DDPM প্রবন্ধে ε-prediction-এ ছবির মান ভালো এসেছিল। পরে x0-prediction আর দুটির মিশ্রণ (“v-prediction”, Salimans আর Ho, ২০২২)-ও প্রচলিত হয়েছে।

Denoising আর score: সংখ্যাতত্ত্বে একটি পুরোনো ফল (Tweedie-র সূত্র) বলে, Gaussian noise-এর জন্য সেরা denoiser আর noisy ডেটার score, অর্থাৎ ∇x log p(xt), একই তথ্য বহন করে: εθ(xt, t) ≈ −√(1 − ᾱt)·∇x log p(xt). তাই noise অনুমান শেখা মানে প্রতিটি noise-স্তরে “কোন দিকে গেলে ডেটা বেশি সম্ভাব্য” শেখা। এই score অবশ্য মেশিন লার্নিং কোর্সের “score function” (parameter-এর সাপেক্ষে log-likelihood-এর derivative) থেকে আলাদা: এখানে derivative ডেটা x-এর সাপেক্ষে। অংশ ৬-এর ব্লক এই সম্পর্কটিই ব্যবহার করে।

৫ · DDPM-এর reverse ধাপ — গড়ের দিকে একটু, তারপর নতুন noise

Sampling-এ xT ~ N(0, I) থেকে শুরু করে t = T, T − 1, …, ১ পর্যন্ত প্রতিটি ধাপে:

xt−1 = (1 ÷ √αt) · ( xt − (βt ÷ √(1 − ᾱt)) · εθ(xt, t) ) + σt·z,   z ~ N(0, I)

প্রথম অংশ হলো উল্টো ধাপের গড়: network-এর অনুমিত noise-এর একটি ছোট ভাগ বাদ দিয়ে, তারপর √αt দিয়ে ভাগ করে আগের মাপে ফেরানো। দ্বিতীয় অংশ নতুন noise; DDPM প্রবন্ধে σt2 = βt আর আরেকটি পছন্দ β̃t = βt(1 − ᾱt−1) ÷ (1 − ᾱt), দুটিতে প্রায় একই ফল আসার কথা বলা হয়েছে। শেষ ধাপে (t = ১) noise যোগ করা হয় না। প্রতিটি ধাপে নতুন noise টেনে এগোনোকে বলে ancestral sampling. নিচে একটি pixel-এর একটি ধাপ, সংখ্যায়।

β = 0.01004, α = 0.98996, ᾱ = 0.07859 (√ᾱ = 0.2803, √(1 − ᾱ) = 0.9599); x_t = 0.60, ε̂ = 0.90, z = −0.50
ধাপ
হিসাব
মানে
  1. অনুমান. Network xt = ০.৬০ আর t = ৫০০ দেখে বলে, noise-এর অংশ ε̂ = ০.৯০. মাঝপথে √(1 − ᾱ) ≈ ০.৯৬, তাই xt-এর প্রায় পুরোটাই noise হতে পারে।
  2. x̂0 দেখে নেওয়া. একই অনুমান থেকে “পরিষ্কার ছবি” −০.৯৪১৪: pixel-টি সম্ভবত পটভূমির। DDPM-এর সূত্রে x̂0 সরাসরি লাগে না, কিন্তু বাস্তব কোডে একে প্রায়ই [−১, ১]-এ কেটে নেওয়া হয়।
  3. সরানোর ভাগ. β ÷ √(1 − ᾱ) ≈ ০.০১০৫: অনুমিত noise-এর মাত্র প্রায় এক শতাংশ সরানো হবে। হাজার ছোট ধাপের এক ধাপ।
  4. গড়. ০.৬০ − ০.০০৯৪ = ০.৫৯০৬, তারপর √α দিয়ে ভাগ (মাপ ফেরানো): μ ≈ ০.৫৯৩৬.
  5. নতুন noise. σ = √β ≈ ০.১০০২, z = −০.৫০, তাই x৪৯৯ ≈ ০.৫৪৩৫. এই এলোমেলো অংশই DDPM-কে বৈচিত্র্য দেয়, আর একই শুরু থেকে দুবার আলাদা ছবি আসে।
import numpy as np

T = 1000
beta = np.linspace(1e-4, 0.02, T); alpha = 1 - beta; abar = np.cumprod(alpha)
t = 500                                    # বইয়ের t = 500, array-তে index 499
b, al, a, a_prev = beta[t - 1], alpha[t - 1], abar[t - 1], abar[t - 2]
x_t, eps_hat, z = 0.60, 0.90, -0.50        # একটি pixel, network-এর অনুমান, নতুন noise
print(f"beta={b:.5f}  alpha={al:.5f}  abar={a:.5f}  abar_prev={a_prev:.5f}")

x0_hat = (x_t - np.sqrt(1 - a) * eps_hat) / np.sqrt(a)
print(f"predicted x0 = {x0_hat:.4f}")
mean = (x_t - b / np.sqrt(1 - a) * eps_hat) / np.sqrt(al)
x_prev = mean + np.sqrt(b) * z             # DDPM: sigma_t^2 = beta_t
print(f"DDPM: mean = {mean:.4f}, sigma = {np.sqrt(b):.4f}, x_499 = {x_prev:.4f}")
same = np.sqrt(a_prev) * b / (1 - a) * x0_hat + np.sqrt(al) * (1 - a_prev) / (1 - a) * x_t
print(f"same mean from the x0 form: {same:.4f}")

for s in (499, 480):                       # DDIM (eta = 0): কোনো নতুন noise নেই
    a_s = abar[s - 1]
    x_s = np.sqrt(a_s) * x0_hat + np.sqrt(1 - a_s) * eps_hat
    print(f"DDIM to t={s}: abar_s = {a_s:.5f}, x_{s} = {x_s:.4f}")

আউটপুট (পাতার Python-এ চালানো):

beta=0.01004  alpha=0.98996  abar=0.07859  abar_prev=0.07938
predicted x0 = -0.9414
DDPM: mean = 0.5936, sigma = 0.1002, x_499 = 0.5435
same mean from the x0 form: 0.5936
DDIM to t=499: abar_s = 0.07938, x_499 = 0.5983
DDIM to t=480: abar_s = 0.09579, x_480 = 0.5644

প্রথম লাইন stepper-এর মাথার সংখ্যাগুলো। x̂0 = −০.৯৪১৪, গড় ০.৫৯৩৬ আর নতুন noise-সহ ০.৫৪৩৫: দুটি stepper-এর হিসাব হুবহু মিলল। চতুর্থ লাইনটি একটি সূত্র-পরীক্ষা: উল্টো ধাপের গড়কে x̂0 আর xt-এর ভারযুক্ত গড় হিসেবেও লেখা যায় (√ᾱt−1βt ÷ (1 − ᾱt) আর √αt(1 − ᾱt−1) ÷ (1 − ᾱt) ওজনে), আর দুই রূপ একই ০.৫৯৩৬ দেয়। এই দ্বিতীয় রূপ থেকেই বোঝা যায়, মাঝপথে x̂0-এর ওজন খুব কম (প্রায় ০.০০৩), তাই একটি ধাপে ছবি প্রায় নড়ে না। শেষ দুই লাইন DDIM: t = ৪৯৯-এ ০.৫৯৮৩ আর এক লাফে t = ৪৮০-এ ০.৫৬৪৪, কোনো z ছাড়া। একই ব্লক বারবার চালালেও ফল বদলাবে না, কারণ এখানে z হাতে বসানো; আসল DDPM-এ z প্রতিবার নতুন।

৬ · DDIM — একই network, noise ছাড়া, বড় লাফে

DDPM-এর বড় দাম সময়: একটি ছবির জন্য network-কে ১০০০ বার চালাতে হয়। Song, Meng আর Ermon-এর DDIM (arXiv-এ ২০২০, ICLR ২০২১) একটি গুরুত্বপূর্ণ পর্যবেক্ষণ থেকে আসে: training-এর loss শুধু q(xt | x0)-এর ওপর নির্ভর করে, ধাপগুলো কীভাবে শিকলে জোড়া তার ওপর নয়। তাই একই প্রান্তিক distribution দেয় এমন অন্য (non-Markovian) forward process ধরা যায়, আর একই প্রশিক্ষিত network দিয়ে অন্যভাবে sample করা যায়। Retraining লাগে না। η = ০ নিলে DDIM-এর ধাপ:

xs = √ᾱs · x̂0 + √(1 − ᾱs) · ε̂,   যেখানে s < t আর x̂0 = (xt − √(1 − ᾱt)·ε̂) ÷ √ᾱt

পড়ে দেখুন: অনুমিত পরিষ্কার ছবি x̂0 নিয়ে, তাকে আবার s ধাপের noise-স্তরে বসানো, আর noise হিসেবে নতুন z নয়, একই ε̂. কোনো এলোমেলো অংশ নেই, তাই xT ঠিক থাকলে ছবিও ঠিক; একে বলে deterministic sampling. আর s = t − ১ হতে হবে এমন নয়: ১০০০ থেকে ৯৮০, ৯৬০, … এভাবে ৫০ ধাপে নামা যায়। সাধারণ রূপে একটি η প্যারামিটার আছে: η = ১-এ DDPM-ধাঁচের noise, η = ০-এ DDIM. এই পর্বের দুটি কোডেই সেই সাধারণ রূপ ব্যবহার করা হয়েছে, আর η = ১-এর σ মানে উপরের β̃.

x_t = 0.60 (t = 500), ε̂ = 0.90, x̂₀ = −0.9414; x_s = √ᾱ_s · x̂₀ + √(1 − ᾱ_s) · ε̂
ধাপ
হিসাব
মানে
  1. শুরু. একই network-এর একই উত্তর: ε̂ = ০.৯০, তাই x̂0 = −০.৯৪১৪. DDIM নতুন কিছু শেখে না, শুধু এই দুটি সংখ্যা অন্যভাবে জোড়ে।
  2. এক ধাপ পিছনে. x̂0-কে t = ৪৯৯-এর noise-স্তরে বসানো হলো, noise হিসেবে আবার ε̂: ফল ০.৫৯৮৩.
  3. তুলনা. DDPM-এর গড় ছিল ০.৫৯৩৬, তার ওপর নতুন noise দিয়ে ০.৫৪৩৫; DDIM কোনো noise যোগ করে না, আর একই xt থেকে প্রতিবার ০.৫৯৮৩. দুটি গড় কাছাকাছি, কিন্তু সমান নয়, কারণ দুটি আলাদা পথ।
  4. বড় লাফ. একই সূত্রে সরাসরি t = ৪৮০: ᾱs = ০.০৯৫৭৯, ফল ০.৫৬৪৪. মাঝের ১৯টি ধাপ চালাতে হয়নি।
  5. খরচ. ২০ করে নামলে ১০০০-এর বদলে ৫০টি network call. লাফ খুব বড় হলে (যেমন ৫ ধাপে) প্রতিটি লাফে x̂0-এর ভুল বড় হয়ে জমে; নিচের ব্লক আর অংশ ৮-এর run সেটাই দেখায়।

এবার একটি পুরো sampler. দুই মাত্রার একটি খেলনা ডেটা: একটি বৃত্তের ওপর ৮টি ছোট Gaussian (প্রতিটির standard deviation ০.০৮). এখানে network শেখাতে হয় না, কারণ এই ডেটার noisy রূপ q(xt) নিজেও ৮টি Gaussian-এর মিশ্রণ, যার score সূত্রে লেখা যায়; তাই ε̂ হলো নিখুঁত denoiser (অংশ ৪-এর নোটের সূত্র)। ফলে নিচে যে পার্থক্য দেখবেন, তা শুধু sampler-এর ধাপের সংখ্যা আর ধরন থেকে, network-এর ভুল থেকে নয়। মাপ দুটি: প্রতিটি নমুনা থেকে নিকটতম কেন্দ্রের গড় দূরত্ব (আসল ডেটায় তাত্ত্বিক মান ০.০৮ × √(π/2) ≈ ০.১০০), আর প্রতিটি “mode”-এ নমুনার ভাগ (সমান হলে ১/৮ = ০.১২৫)।

import numpy as np
import matplotlib.pyplot as plt

T = 1000
abar = np.cumprod(1 - np.linspace(1e-4, 0.02, T))   # linear schedule-এর ᾱ_t
K, s0 = 8, 0.08                                     # 8 টি ছোট Gaussian, বৃত্তের ওপর
mu = 2 * np.c_[np.cos(2 * np.pi * np.arange(K) / K), np.sin(2 * np.pi * np.arange(K) / K)]

def eps_hat(x, t):            # নিখুঁত denoiser: noisy mixture-এর score থেকে ε
    a = abar[t]; v = a * s0**2 + 1 - a              # t-তে প্রতিটি component-এর variance
    d = x[:, None, :] - np.sqrt(a) * mu[None]       # (N, K, 2)
    logw = -(d**2).sum(-1) / (2 * v)
    w = np.exp(logw - logw.max(1, keepdims=True)); w /= w.sum(1, keepdims=True)
    return np.sqrt(1 - a) * (w[..., None] * d).sum(1) / v   # ε = −√(1−ᾱ)·score

def sample(n, steps, eta, rng):   # eta=1: DDPM-ধাঁচের (noise যোগ), eta=0: DDIM (নির্ধারিত)
    ts = np.linspace(T - 1, 0, steps).round().astype(int); x = rng.standard_normal((n, 2))
    for t, s in zip(ts, list(ts[1:]) + [-1]):
        e = eps_hat(x, t); x0 = (x - np.sqrt(1 - abar[t]) * e) / np.sqrt(abar[t])
        if s < 0: x = x0; break
        sig = eta * np.sqrt((1 - abar[s]) / (1 - abar[t]) * (1 - abar[t] / abar[s]))
        x = np.sqrt(abar[s]) * x0 + np.sqrt(1 - abar[s] - sig**2) * e + sig * rng.standard_normal(x.shape)
    return x

rng = np.random.default_rng(0)
real = mu[rng.integers(0, K, 800)] + s0 * rng.standard_normal((800, 2))
runs = {"real data": real, "DDPM 1000": sample(800, 1000, 1, rng), "DDPM 50": sample(800, 50, 1, rng),
        "DDIM 50": sample(800, 50, 0, rng), "DDIM 5": sample(800, 5, 0, rng)}
fig, axs = plt.subplots(1, 5, figsize=(12, 2.8), sharex=True, sharey=True)
for ax, (name, x) in zip(axs, runs.items()):
    dist = np.linalg.norm(x[:, None] - mu[None], axis=-1)
    share = np.bincount(dist.argmin(1), minlength=K) / len(x)
    print(f"{name:9s}: mean dist to nearest centre {dist.min(1).mean():.3f}, mode share {share.min():.3f}-{share.max():.3f}")
    ax.scatter(x[:, 0], x[:, 1], s=2); ax.scatter(*mu.T, c="red", s=10); ax.set_title(name)
plt.show()

আউটপুট (পাতার Python-এ চালানো; সঙ্গে পাঁচটি ছবি):

real data: mean dist to nearest centre 0.100, mode share 0.105-0.140
DDPM 1000: mean dist to nearest centre 0.092, mode share 0.107-0.140
DDPM 50  : mean dist to nearest centre 0.067, mode share 0.111-0.149
DDIM 50  : mean dist to nearest centre 0.081, mode share 0.117-0.135
DDIM 5   : mean dist to nearest centre 0.601, mode share 0.111-0.140

আসল ডেটায় নিকটতম কেন্দ্রের গড় দূরত্ব ০.১০০, তত্ত্বের সঙ্গে মিলে। নিখুঁত denoiser আর ১০০০ ধাপের DDPM ০.০৯২ দেয়, প্রায় ঠিক মাপের ছড়ানো। ৫০ ধাপে DDPM-ধাঁচের (η = ১) sampler নমুনাকে কেন্দ্রের দিকে বেশি টেনে আনে (০.০৬৭), আর DDIM ৫০ ধাপে ০.০৮১, আসলের কাছাকাছি। ৫ ধাপের DDIM-এ বড় লাফের ভুল জমে: গড় দূরত্ব ০.৬০১, ছবিতে নমুনা কেন্দ্র থেকে অনেক দূরে, বৃত্ত বরাবর ছড়ানো। তবু সব sampler-এ আটটি mode-এর ভাগ ০.১০৫ থেকে ০.১৪৯-এর মধ্যে, কোনো mode বাদ পড়েনি; GAN-এর mode collapse-এর সঙ্গে এই পার্থক্যটি ভাবনার প্রশ্ন ৩-এ আসবে। মনে রাখুন: এখানে denoiser নিখুঁত; আসল network-এর ভুল যোগ হলে কম ধাপে ক্ষতি আরও বেশি হতে পারে, যা অংশ ৮-এর run দেখায়।

৭ · Denoiser network — time embedding-সহ U-Net

εθ-এর কাজ: ২৮ × ২৮ noisy ছবি নিয়ে ২৮ × ২৮ noise অনুমান। Input আর output-এর মাপ সমান, যেখানে বড় প্রসঙ্গ (“এটা জুতো না ব্যাগ”) আর pixel-স্তরের খুঁটিনাটি দুটিই লাগে। এটি ঠিক পর্ব ২৫-এর U-Net-এর সমস্যা, তাই DDPM থেকে শুরু করে বেশিরভাগ ছবির diffusion model-এ U-Net ব্যবহার হয়েছে। পার্থক্য একটি: একই network-কে হাজারটি আলাদা noise-স্তরে কাজ করতে হয়, তাই তাকে জানাতে হয় এখন কোন t. এজন্য time embedding: t-কে পর্ব ৩৪-এর sinusoidal সূত্রে একটি vector বানানো, একটি ছোট MLP দিয়ে বদলানো, আর প্রতিটি residual block-এ প্রতিটি channel-এ একটি করে সংখ্যা হিসেবে যোগ করা। ফলে network “t = ৯০০, প্রায় সবই noise” আর “t = ২০, শুধু হালকা দানা সরাও” আলাদা আচরণ করতে পারে।

এই পর্বের desktop run-এর network ছোট, ২৮ × ২৮-এর জন্য বানানো (condensed বিবরণ):

অংশমাপ (channel × H × W)কী আছে
Input conv + res block32 × 28 × 28GroupNorm, SiLU, 3 × 3 conv, time embedding যোগ
Down (stride 2) + res block64 × 14 × 14Skip হিসেবে রেখে দেওয়া হয়
Bottleneck128 × 7 × 7Res block, ৪৯টি অবস্থানে self-attention, res block
Up (ConvTranspose) + skip জোড়া64 × 14 × 14 → 32 × 28 × 28Encoder-এর একই মাপের map channel ধরে জোড়া
Output1 × 28 × 28GroupNorm, SiLU, 3 × 3 conv: অনুমিত ε

মোট ১২,৪৭,৫২১টি parameter. Normalization-এ BatchNorm নয়, GroupNorm (৮টি দল), কারণ এটি batch-এর ওপর নির্ভর করে না, আর একই batch-এ নানা t-এর ছবি থাকায় batch-এর পরিসংখ্যান অর্থহীন হতো (পর্ব ১৪)। আরেকটি প্রায় অপরিহার্য কৌশল EMA: training-এর weight-এর একটি চলমান গড় আলাদা কপিতে রাখা (এখানে ০.৯৯৯), আর sampling করা সেই কপি দিয়ে। Diffusion-এর loss-এ noise অনেক, তাই শেষ ধাপের weight একটু এলোমেলো; গড়টি মসৃণ, আর ছবির মান সাধারণত ভালো হয়। DDPM প্রবন্ধেও EMA ছিল। পরে কিছু model U-Net-এর বদলে Transformer দিয়ে ε অনুমান করেছে (যেমন DiT, Peebles আর Xie, ২০২৩); সেই দিক আর text দিয়ে নিয়ন্ত্রণ পর্ব ৮০ (Latent diffusion আর guidance)-এর বিষয়।

৮ · সত্যিকারের run — Fashion-MNIST-এ নিজের DDPM

এবার সব মিলিয়ে একটি আসল model. ডেটা Fashion-MNIST (Zalando Research, MIT লাইসেন্স): ৬০,০০০টি ২৮ × ২৮ ধূসর ছবি, ১০ ধরনের পোশাক, জুতো আর ব্যাগ। এটি বাজারঘরের পণ্যের ছবির বদলি হিসেবে ব্যবহার হচ্ছে; আসল দোকানের ছবি নয়। ডেস্কটপে Python 3.14, torch 2.15.0 (CUDA build), GPU-তে। অংশ ৭-এর U-Net, Adam (lr 2 × 10−4), batch ১২৮, EMA ০.৯৯৯, seed ০. GPU অন্য কাজের সঙ্গে ভাগ করা, তাই প্রতিটি run-এ ১৩ মিনিটের সময়সীমা, প্রতি epoch-এ checkpoint. একই বাজেটে দুটি model: linear আর cosine schedule. সত্যিকারের run, output সংক্ষেপিত (মাঝের epoch বাদ):

~/bazarghar-dl$ uv run python train.py linear 13 60
params 1247521
epoch   1  loss 0.1019  15.4s
epoch   2  loss 0.0571  14.8s
epoch   5  loss 0.0463  14.8s
epoch  10  loss 0.0418  15.3s
epoch  20  loss 0.0390  15.1s
epoch  40  loss 0.0373  15.5s
epoch  51  loss 0.0367  15.6s
peak GPU memory allocated: 629 MiB
run wall time: 13.0 min, total epochs 51

~/bazarghar-dl$ uv run python train.py cosine 13 60
params 1247521
epoch   1  loss 0.1560  15.0s
epoch   2  loss 0.0979  14.5s
epoch   5  loss 0.0821  14.6s
epoch  10  loss 0.0747  14.7s
epoch  20  loss 0.0703  15.0s
epoch  40  loss 0.0676  15.1s
epoch  51  loss 0.0665  14.9s
peak GPU memory allocated: 629 MiB
run wall time: 12.7 min, total epochs 51

প্রতি epoch প্রায় ১৫ সেকেন্ড, ১৩ মিনিটে দুটি model-ই ৫১ epoch পেয়েছে. Loss প্রথম epoch-এ দ্রুত নামে, তারপর খুব ধীরে; diffusion-এ এটি স্বাভাবিক, কারণ বড় t-তে noise অনুমান প্রায় সহজ আর ছোট t-তে প্রায় অসম্ভব (খুব অল্প noise, আর সেটি কোনটা বোঝা কঠিন), তাই loss কখনো শূন্যের কাছে যায় না। দুই schedule-এর loss সরাসরি তুলনীয় নয়: schedule বদলালে কোন noise-স্তর কত ঘন ঘন আসে তা বদলায়, তাই একই মানের model-এর loss-ও আলাদা হয়। Training-এ peak GPU memory মাত্র ৬২৯ MiB, আর ১০০০টি ছবি একসঙ্গে sample করতে প্রায় ২ GiB: model ছোট।

মান মাপতে ছবি দেখাই যথেষ্ট নয়। একই ডেটায় একটি ছোট CNN classifier আলাদা করে শেখানো হলো (test accuracy ০.৯০৬৭)। তার শেষের আগের স্তরের ১২৮-মাত্রার feature দিয়ে generated ছবি আর ১০,০০০টি test ছবির মধ্যে Fréchet distance মাপা হলো, যাকে এখানে “FID(clf)” বলছি। এটি প্রচলিত Inception-ভিত্তিক FID নয়, তাই অন্য কোথাও প্রকাশিত FID-এর সঙ্গে তুলনা চলে না; শুধু এই পর্বের model-গুলোর মধ্যে তুলনার জন্য। কম মানে ভালো। একই classifier প্রতিটি নমুনার শ্রেণিও বলে, তাই ১০টি শ্রেণির ভাগ (সমান হলে ০.১) দেখায় কোনো ধরন বাদ পড়ছে কি না। প্রতিটি sampler-এ ১০০০টি নমুনা, seed ১২৩, x̂0 প্রতিটি ধাপে [−১, ১]-এ কাটা।

~/bazarghar-dl$ uv run python evaluate.py linear
classifier test accuracy 0.9067
reference: 1000 real train images vs test set  FID(clf) 4.235  class share 0.078-0.135
== linear schedule, EMA weights after 51 epochs, 1000 samples each ==
sampler      steps   time(s)  FID(clf)  class share min-max  max-prob
DDPM         1000      83.6     5.867   0.079-0.146         0.876
DDPM          250      20.3     7.570   0.078-0.141         0.864
DDPM           50       3.9    10.779   0.075-0.133         0.861
DDIM          250      20.6     6.833   0.076-0.136         0.860
DDIM           50       4.2     8.554   0.081-0.128         0.855
DDIM           20       1.7    14.524   0.084-0.124         0.842
DDIM           10       0.8    31.624   0.061-0.124         0.819

~/bazarghar-dl$ uv run python evaluate.py cosine
== cosine schedule, EMA weights after 51 epochs, 1000 samples each ==
sampler      steps   time(s)  FID(clf)  class share min-max  max-prob
DDPM         1000      80.4     8.053   0.063-0.134         0.861
DDPM          250      20.1     5.876   0.076-0.121         0.866
DDPM           50       4.0    13.427   0.057-0.122         0.851
DDIM          250      20.2     7.617   0.085-0.136         0.851
DDIM           50       4.1     9.939   0.081-0.138         0.843
DDIM           20       1.6    16.153   0.077-0.141         0.830
DDIM           10       0.8    32.878   0.062-0.145         0.808
peak GPU memory allocated: 1948 MiB

~/bazarghar-dl$ uv run python seeds.py linear,cosine
seed sweep: FID(clf), 1000 samples per seed
linear DDPM 1000: 6.602  5.745  5.489  | mean 5.946
linear DDPM  250: 7.167  5.762  8.370  | mean 7.100
linear DDIM   50: 10.502  10.061  10.509  | mean 10.357
cosine DDPM 1000: 4.954  5.757  6.267  | mean 5.659
cosine DDPM  250: 5.076  5.692  7.483  | mean 6.083
cosine DDIM   50: 12.957  12.216  12.056  | mean 12.410
peak GPU memory allocated: 1948 MiB

প্রথমে মাপকাঠি: training সেটের ১০০০টি আসল ছবিও test সেটের সঙ্গে FID(clf) ৪.২৩৫ দেয়, শূন্য নয়। ১০০০টি নমুনা থেকে mean আর covariance অনুমানের ভুলই এই পক্ষপাত (পর্ব ৮১-এ বিস্তারিত)। তাই এই মাপে ৪-এর কাছাকাছি মানে প্রায় আসল ছবির সমান।

এবার পাঁচটি পর্যবেক্ষণ। এক, ধাপ কমালে সময় প্রায় সমানুপাতে কমে: ১০০০ ধাপে ৮৩.৬ সেকেন্ড, ৫০ ধাপে প্রায় ৪ সেকেন্ড, ১০ ধাপে ০.৮ সেকেন্ড (১০০০টি ছবির জন্য, GPU-তে)। খরচের প্রায় সবটাই network call. দুই, মান পড়ে, তবে ধীরে নয়, হঠাৎ: linear model-এ DDIM ২৫০ ধাপে ৬.৮৩, ৫০-এ ৮.৫৫, ২০-এ ১৪.৫২, ১০-এ ৩১.৬২. তিন, কম ধাপে DDIM DDPM-এর চেয়ে ভালো: ৫০ ধাপে linear-এ ৮.৫৫ বনাম ১০.৭৮, cosine-এ ৯.৯৪ বনাম ১৩.৪৩, খেলনা ব্লকের মতোই। চার, শ্রেণির ভাগ সব sampler-এ মোটামুটি ০.০৬ থেকে ০.১৫-এর মধ্যে, কোনো ধরন হারায়নি; তবে ১০ ধাপে classifier-এর গড় নিশ্চয়তা (max-prob) ০.৮৭৬ থেকে ০.৮১৯-এ নামে, ছবি ঝাপসা হলে যা প্রত্যাশিত।

পাঁচ, এবং সবচেয়ে জরুরি: একটি seed-এর সংখ্যা দিয়ে সিদ্ধান্ত নেওয়া বিপজ্জনক। Seed ১২৩-এ cosine-এর ১০০০ ধাপ (৮.০৫) linear-এর (৫.৮৭) চেয়ে খারাপ দেখায়, অথচ আরও তিনটি seed-এ cosine ৪.৯৫ থেকে ৬.২৭ আর linear ৫.৪৯ থেকে ৬.৬০; দুটির পরিসর মিলে যায়। একই model-এর DDPM ২৫০ ধাপও seed ভেদে ৫.৭৬ থেকে ৮.৩৭. তাই এই বাজেটে দুই schedule-এর ১০০০ ধাপের মানে কোনো নির্ভরযোগ্য পার্থক্য দেখা যায়নি। DDIM ৫০ ধাপে অবশ্য cosine model চারটি seed-এই খারাপ (৯.৯৪ থেকে ১২.৯৬, linear-এ ৮.৫৫ থেকে ১০.৫১); কিন্তু প্রতিটি schedule-এ একটিমাত্র training run, তাই এটি schedule-এর গুণ নাকি এই নির্দিষ্ট run-এর, বলা যায় না। এগুলো একটি ছোট model-এর একটি local run-এর সংখ্যা, সাধারণ ফল নয়।

নিচের ব্লক run-এর আসল নমুনা আঁকে। প্রতিটি সারিতে প্রথম ৬টি ছবি (৪৮টি ছবি ৪-bit-এ, অর্থাৎ ১৬টি ধূসর স্তরে, base64 করে পাতায় বসানো, আকারে প্রায় ২৫ KB-এর)। শেষ দুই সারি একটি নমুনার যাত্রা: xt আর সেই ধাপের অনুমিত x̂0, t = ৯৯৯, ৭৪৯, ৪৯৯, ২৪৯, ৯৯ আর ০-তে। সঙ্গে দুই run-এর epoch-প্রতি loss.

import base64, json
import numpy as np
import matplotlib.pyplot as plt

raw = np.frombuffer(base64.b64decode(open("samples.b64").read()), np.uint8)
q = np.stack([raw >> 4, raw & 15], 1).reshape(-1, 28, 28)   # 4-bit থেকে খোলা
imgs = q.astype(float) / 15                                  # 0 = কালো, 1 = সাদা
names = ["lin DDPM 1000", "lin DDPM 50", "lin DDIM 50", "lin DDIM 10",
         "cos DDPM 1000", "cos DDIM 50", "x_t, t=999..0", "x0-hat"]
print("images:", imgs.shape, "| rows:", len(names), "x 6")
fig, axs = plt.subplots(8, 6, figsize=(6, 8.6))
for k, ax in enumerate(axs.flat):
    ax.imshow(imgs[k], cmap="gray", vmin=0, vmax=1); ax.set_xticks([]); ax.set_yticks([])
    if k % 6 == 0: ax.set_ylabel(names[k // 6], rotation=0, ha="right", va="center", fontsize=7)
fig.subplots_adjust(left=0.24, right=0.98, top=0.95, bottom=0.02, hspace=0.15, wspace=0.08)
fig.suptitle("Real samples: Fashion-MNIST DDPM, seed 123, EMA, 51 epochs", fontsize=8)
plt.show()

loss = json.loads(open("loss.json").read())
for name, v in loss.items():
    print(f"{name:6s}: epoch 1 {v[0]:.4f} | epoch 10 {v[9]:.4f} | epoch {len(v)} {v[-1]:.4f}")
fig, ax = plt.subplots(figsize=(6, 3))
for name, v in loss.items():
    ax.plot(range(1, len(v) + 1), v, label=name)
ax.set_yscale("log"); ax.set_xlabel("epoch"); ax.set_ylabel("mean eps MSE (train)")
ax.set_title("Training loss per epoch (one run each)"); ax.legend()
plt.show()

আউটপুট (পাতার Python-এ চালানো; সঙ্গে নমুনার grid আর loss-এর ছবি):

images: (48, 28, 28) | rows: 8 x 6
linear: epoch 1 0.1019 | epoch 10 0.0418 | epoch 51 0.0367
cosine: epoch 1 0.1560 | epoch 10 0.0747 | epoch 51 0.0665

ছবিগুলো run-এর আসল output, seed ১২৩, EMA weight, ৫১ epoch; পাতায় শুধু ৪-bit-এ ছোট করা। প্রথম সারিতে (linear, DDPM ১০০০) জামা, স্নিকার আর লম্বা হাতার পোশাক চেনা যায়; প্রথম ছবিটি ব্যাগ আর জুতোর একটি অদ্ভুত মিশ্রণ, ছোট model-এর একটি সাধারণ ব্যর্থতা। এবার DDIM-এর তিনটি সারি পাশাপাশি দেখুন: linear DDIM ৫০, linear DDIM ১০ আর cosine DDIM ৫০-এ একই অবস্থানে প্রায় একই পণ্য (হিল জুতো, স্নিকার, ব্যাগ, প্যান্ট)। কারণ DDIM নির্ধারিত: একই শুরুর noise xT একই ছবিতে যায়, ধাপের সংখ্যা বদলালেও মোটামুটি একই ছবি, শুধু ১০ ধাপে খুঁটিনাটি কম। আরও মজার, আলাদাভাবে শেখানো দুটি model-ও একই xT-কে প্রায় একই ছবিতে নিয়েছে; এমন মিল আগের গবেষণাতেও লক্ষ করা হয়েছে, তবে এখানে এটি একটি ছোট পর্যবেক্ষণ মাত্র। DDPM-এর সারিগুলো আলাদা, কারণ প্রতিটি ধাপের নতুন noise পথ বদলে দেয়।

শেষ দুই সারি একটি নমুনার যাত্রা (linear, DDPM ১০০০, প্রথম ছবিটি)। t = ৯৯৯-এ xt খাঁটি noise, আর x̂0 একটি ঝাপসা দাগ: এত noise-এ network-এর সেরা অনুমান সব ছবির একটি গড়ের মতো। t = ৪৯৯-এ x̂0-এ মোটা আকার এসে গেছে, যদিও xt-এ চোখে কিছু বোঝা যায় না। t = ২৪৯-এর পরে শুধু খুঁটিনাটি বদলায়। অর্থাৎ বড় t-তে আকার, ছোট t-তে texture ঠিক হয়। Loss-এর ছবিতে দুটি run-ই প্রথম কয়েক epoch-এ দ্রুত নামে, তারপর প্রায় সমতল; cosine-এর loss বেশি দেখালেও ওপরের যুক্তিতে (schedule বদলালে কোন noise-স্তর কত ঘন ঘন আসে তা বদলায়) তা খারাপ model বোঝায় না।

৯ · বড় ছবিতে কী বদলায় — সীমা আর সাধারণ ভুল

ধীর sampling

প্রতিটি ছবিতে অনেকবার network চালানো diffusion-এর মূল খরচ। DDIM ছাড়াও উচ্চ-ক্রমের ODE solver (যেমন DPM-Solver), distillation আর consistency model-এর মতো পদ্ধতি ধাপ কমানোর চেষ্টা করে; কয়েকটির নাম পর্ব ৮০-তে আসবে। প্রতিটির দাবি নিজের ডেটায় মাপা দরকার।

Schedule আর ছবির মাপ

একই β বড় ছবিতে কম “ক্ষতি” করে, কারণ পাশের pixel-গুলো মিলে noise গড় করে ফেলে; তাই বড় ছবিতে schedule সরিয়ে নেওয়ার প্রস্তাব আছে। Linear schedule-এ শেষ ধাপের SNR পুরো শূন্য নয়; Lin ও সহকর্মীরা (২০২৪) দেখান, এতে training আর sampling-এর শুরুর বিন্দু মেলে না, আর কিছু model খুব উজ্জ্বল বা খুব অন্ধকার ছবি বানাতে পারে না বলে রিপোর্ট আছে।

সাধারণ বাগ

ᾱ-এর index এক ঘর সরে যাওয়া (t ১ থেকে না ০ থেকে); কম ধাপে sampling-এ মূল β ব্যবহার করা (লাফের জন্য β নতুন করে হিসাব করতে হয়: 1 − ᾱt/ᾱs); ছবিকে [০, ১]-এ রেখে noise-কে N(0, 1) ধরা (মাপ মেলে না); EMA ভুলে raw weight দিয়ে sample; আর model.eval() আর torch.no_grad() ছাড়া sampling.

আরও গভীরে: Song আর Ermon (২০১৯) score matching দিয়ে নানা noise-স্তরে ∇x log p শিখে Langevin dynamics দিয়ে sample করেন; Hyvärinen (২০০৫)-এর score matching আর Vincent (২০১১)-এর denoising score matching এর তাত্ত্বিক ভিত্তি। Song ও সহকর্মীরা (ICLR ২০২১) DDPM আর score-ভিত্তিক model-কে একটি stochastic differential equation-এর দুই রূপ হিসেবে মেলান, আর দেখান যে একই score থেকে একটি নির্ধারিত “probability flow ODE”-ও পাওয়া যায়; DDIM-কে তার একটি সরল discretisation হিসেবে দেখা যায়। Karras ও সহকর্মীরা (২০২২, “EDM”) schedule, network-এর input-output মাপ আর sampler আলাদা করে বিশ্লেষণ করেন। বিস্তারিত সূত্র নিজে মূল প্রবন্ধে মিলিয়ে নিন।

পরীক্ষা ও ইন্টারভিউতে:

  • β = [0.1, 0.2] হলে ᾱ2 আর SNR? ০.৯ × ০.৮ = ০.৭২; SNR = ০.৭২ ÷ ০.২৮ ≈ ২.৫৭.
  • ᾱt = ০.৬৪, x0 = ০.৫, ε = ১-এ xt? ০.৮ × ০.৫ + ০.৬ × ১ = ১.০.
  • DDPM-এর network কী অনুমান করে, loss কী? যোগ করা noise ε; loss = ‖ε − εθ(xt, t)‖2-এর গড়, t সমান সম্ভাবনায়।
  • DDIM কেন কম ধাপে চলে, retraining লাগে কি? loss শুধু q(xt | x0)-এর ওপর নির্ভর করে, তাই একই network দিয়ে noise ছাড়া, বড় লাফের পথ নেওয়া যায়; retraining লাগে না।
  • U-Net-এ t কীভাবে ঢোকে? Sinusoidal embedding → MLP → প্রতিটি residual block-এ channel-প্রতি যোগ।

১০ · AI-এর যুগে — diffusion কোডে AI-এর ভুল ধরা

Diffusion-এর কোড ছোট, তাই AI assistant সহজেই পুরো DDPM লিখে দেয়। বিপদ হলো, ভুল থাকলেও কোড চলে আর loss কমে; শুধু ছবি ঝাপসা, ধূসর বা অদ্ভুত আসে, আর কারণ খুঁজে পাওয়া কঠিন। নিচের ভুলগুলো সবচেয়ে বেশি দেখা যায়:

AI প্রায়ই যা করেকেন সমস্যাকীভাবে ধরবেন
ᾱt আর ᾱt−1-এর index গুলিয়ে ফেলে, বা t ০ থেকে না ১ থেকে, ঠিক রাখে নাপ্রতিটি ধাপে সামান্য ভুল মাপ, হাজার ধাপে জমেStepper-এর মতো একটি pixel-এ এক ধাপ হাতে মিলিয়ে দেখুন
কম ধাপের sampling-এ মূল βt ব্যবহার করেলাফের জন্য noise-এর মাপ ভুল, ছবি noisy থাকেη-রূপের সাধারণ সূত্র বা 1 − ᾱt/ᾱs দিয়ে নতুন β
ছবি [০, ১]-এ রাখেnoise-এর তুলনায় সংকেত ছোট, schedule-এর SNR হিসাব মেলে নাডেটা [−১, ১]-এ আছে কি না প্রিন্ট করে দেখুন
পুরোনো বা বানানো library API (যেমন diffusers-এর scheduler-এর ভুল argument)error বা নীরবে অন্য আচরণনিজের version-এর docs আর signature দেখুন
EMA, eval(), no_grad() বাদ দেয়খারাপ ছবি, বেশি memorySampling function-এর প্রথম তিন লাইন পড়ুন
একটি ভালো seed-এর ছবি বা কম নমুনার FID দিয়ে “GAN-এর চেয়ে ভালো” দাবিFID নমুনার সংখ্যায় পক্ষপাতদুষ্ট, একটি run ভাগ্যের ব্যাপারএকই সংখ্যক নমুনা, একই reference, কয়েকটি seed (পর্ব ৮১)

AI-কে কোডের সঙ্গে যাচাইয়ের উপায়ও চাইতে বলুন:

আমি PyTorch-এ ২৮ × ২৮ ধূসর ছবির জন্য একটি ছোট DDPM লিখছি। আমার torch-এর version নিচে দিলাম। ১. β, α, ᾱ-এর array কোন index থেকে শুরু, তা স্পষ্ট লেখো, আর t = 1 ও t = T-এ কোন মান ব্যবহার হয় দেখাও। ২. Training step আর DDPM sampling step-এর প্রতিটি লাইনের পাশে সূত্রটি মন্তব্যে লেখো। ৩. DDIM (η = 0) দিয়ে ৫০ ধাপে sampling-এর কোড দাও, আর লাফের জন্য β কীভাবে হিসাব হচ্ছে বুঝিয়ে বলো। ৪. একটি pixel-এর এক ধাপ হাতে মেলানোর ছোট test দাও (x_t, ε̂, ᾱ দিয়ে)। ৫. EMA, model.eval() আর torch.no_grad() কোথায় আছে দেখাও। ৬. কোনো API সম্পর্কে নিশ্চিত না হলে বলো; বানিয়ে লিখো না।

AI-এর দেওয়া diffusion কোড — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Thermodynamics থেকে ছবি — ২০১৫ সালের diffusion-এর ধারণা কেন ২০২০ সালের আগে জনপ্রিয় হয়নি?

Jascha Sohl-Dickstein, Eric Weiss, Niru Maheswaranathan আর Surya Ganguli-র “Deep Unsupervised Learning using Nonequilibrium Thermodynamics” (ICML ২০১৫) প্রবন্ধে মূল কাঠামো প্রায় সবই ছিল: ধাপে ধাপে noise মেশানোর একটি Markov chain, উল্টো দিকের শেখা chain, আর একটি variational bound দিয়ে training. অনুপ্রেরণা এসেছিল পরিসংখ্যানগত পদার্থবিজ্ঞান থেকে, যেখানে ধীরে ধীরে ছড়িয়ে পড়া একটি প্রক্রিয়া আর তার উল্টো প্রক্রিয়া নিয়ে কাজ আছে। কিন্তু সেই সময়ের ছবির মান GAN-এর তুলনায় পিছিয়ে ছিল, আর তখন সবার মনোযোগ ছিল GAN আর VAE-তে।

২০২০ সালে Jonathan Ho, Ajay Jain আর Pieter Abbeel-এর DDPM কয়েকটি আপাত ছোট সিদ্ধান্ত মেলাল। প্রথমত, network-কে উল্টো ধাপের গড় নয়, যোগ করা noise অনুমান করানো। দ্বিতীয়ত, bound-এর পদগুলোর ওজন বাদ দিয়ে সরল MSE loss, যা তাত্ত্বিকভাবে “ঠিক” bound নয়, কিন্তু ছবির মানে ভালো কাজ করেছে। তৃতীয়ত, তখনকার ভালো network নকশা: residual block, GroupNorm আর self-attention-সহ U-Net. প্রবন্ধটি denoising score matching আর Langevin dynamics-এর সঙ্গে এই সরল loss-এর সম্পর্কও দেখায়, আর একই সময়ে Yang Song আর Stefano Ermon-এর score-ভিত্তিক কাজ (২০১৯) অন্য দিক থেকে কাছাকাছি জায়গায় পৌঁছেছিল।

আরও একটি কারণ ছিল compute. হাজার ধাপের sampling আর বড় U-Net-এর training ২০১৫-এর তুলনায় ২০২০-এ অনেক সহজলভ্য GPU-তে চলে। এরপর Nichol আর Dhariwal-এর উন্নতি (২০২১) আর “Diffusion Models Beat GANs on Image Synthesis” (Dhariwal আর Nichol, ২০২১) প্রবন্ধ দেখায়, ImageNet-এর মতো কঠিন ডেটায়ও diffusion ভালো FID দিতে পারে; তবে সেই তুলনা নির্দিষ্ট মাপ আর সেটিং-এ, সব ক্ষেত্রে নয়।

এই ইতিহাস দেখায়, গবেষণায় একটি ধারণা প্রথমবার প্রকাশ আর তার কার্যকর রূপ আলাদা জিনিস। লক্ষ্যবস্তু (কী অনুমান করব), loss-এর ওজন আর network-এর নকশা, তিনটিই মিলে ফল দেয়; এই পর্বের ছোট run-ও ঠিক এই তিনটি সিদ্ধান্তের ওপর দাঁড়িয়ে।

মূল উপলব্ধি: Diffusion-এর মূল ধারণা ২০১৫-এর, কিন্তু ε-prediction, সরল loss, ভালো U-Net আর সহজলভ্য compute মিলে ২০২০-এর পরে তা কার্যকর হয়েছে।

প্র ০২Score matching আর DDPM — দুটি আলাদা পথ কীভাবে একই জায়গায় পৌঁছায়?

একটি probability density p(x)-এর score হলো ∇x log p(x): প্রতিটি বিন্দুতে কোন দিকে গেলে সম্ভাবনা সবচেয়ে দ্রুত বাড়ে। score জানলে normalising constant না জেনেও sample করা যায়, Langevin dynamics দিয়ে: x ← x + (ধাপ/2)·score + √ধাপ·z. পাহাড়ি রাস্তায় কুয়াশায় হাঁটার মতো: চারপাশ দেখা যায় না, কিন্তু পায়ের নিচে ঢাল বোঝা যায়, আর সেই ঢাল ধরে ওপরে ওঠা যায়, মাঝে মাঝে এলোমেলো পা ফেলে।

সমস্যা হলো score শেখা। Aapo Hyvärinen (২০০৫) দেখান, আসল score না জেনেও একটি সমতুল্য লক্ষ্য দিয়ে score-এর model শেখা যায়; একে বলে score matching. Pascal Vincent (২০১১) দেখান, ডেটায় Gaussian noise মিশিয়ে noise-টা অনুমান করা, অর্থাৎ denoising, আসলে noisy ডেটার score শেখারই সমান; একে বলে denoising score matching. কিন্তু একটি noise-স্তরে শেখা score ডেটা থেকে দূরের ফাঁকা জায়গায় ভুল হয়, কারণ সেখানে training-এর উদাহরণ নেই। Song আর Ermon (২০১৯) এর সমাধানে অনেকগুলো noise-স্তরে একটি network শেখান আর বড় noise থেকে ধীরে ধীরে ছোট noise-এ Langevin চালান।

এখন DDPM-এর দিকে তাকান: সেখানেও অনেক noise-স্তর, একটি network যে noise অনুমান করে, আর বড় noise থেকে ছোটতে নামা। অংশ ৪-এর নোটের সম্পর্ক εθ ≈ −√(1 − ᾱ)·score দুটিকে সরাসরি জোড়ে। Song ও সহকর্মীরা (ICLR ২০২১) দুটিকে একটি stochastic differential equation-এর দুই discretisation হিসেবে দেখান: DDPM একটি “variance preserving”, Song-Ermon একটি “variance exploding” প্রক্রিয়া। একই কাঠামো থেকে noise ছাড়া একটি ODE-ও আসে, যা DDIM-এর ধারণার কাছাকাছি।

এই পর্বের খেলনা ব্লক ঠিক এই সম্পর্ক ব্যবহার করেছে: আট-Gaussian মিশ্রণের score সূত্রে লিখে তাকে ε-এ বদলানো। ব্যবহারিক কথা: “diffusion”, “score-based” আর “flow” নামের অনেক model গাণিতিকভাবে ঘনিষ্ঠ আত্মীয়; নাম নয়, কোন process, কোন লক্ষ্য আর কোন sampler, সেটাই দেখার বিষয়।

মূল উপলব্ধি: Noise অনুমান করা আর noisy ডেটার score শেখা একই কাজ; DDPM আর score-based model একই ধারণার দুটি ঐতিহাসিক পথ।

প্র ০৩Diffusion কেন GAN-এর মতো mode collapse-এ কম ভোগে, অথচ এত ধীর? এই দুইয়ের মধ্যে কি কোনো সম্পর্ক আছে?

পর্ব ৭৭ (GAN)-এ generator একটি discriminator-কে ধোঁকা দিতে শেখে। Generator যদি শুধু কয়েক ধরনের ভালো ছবি বানিয়েই ধোঁকা দিতে পারে, তাহলে বাকি ধরন বাদ পড়ে: mode collapse. Diffusion-এর loss-এ এমন কোনো প্রতিপক্ষ নেই। প্রতিটি training ছবি থেকে noise মেশানো উদাহরণ আসে, আর network প্রতিটির জন্য regression-এর ভুল কমায়; কোনো ধরন বাদ দিলে সেই ছবিগুলোর loss বেশি থাকে। তাই diffusion-এর লক্ষ্য likelihood-ভিত্তিক model-এর মতো “সব ডেটা ঢাকা”, আর এই পর্বের খেলনা ব্লকে আটটি mode-এই প্রায় সমান ভাগ এসেছে।

দাম হলো sampling-এর সময়। একটি জটিল distribution থেকে এক লাফে sample করা কঠিন; diffusion কাজটিকে হাজারটি সহজ ধাপে ভাগ করে, যেখানে প্রতিটি ধাপ প্রায় Gaussian. ধাপ যত ছোট, প্রতিটি ধাপের Gaussian অনুমান তত সঠিক, কিন্তু ধাপ তত বেশি। এই কারণেই DDPM-এ ধাপ কমালে মান পড়ে; DDIM আর উন্নত solver ধাপের পথটিকে মসৃণ করে কিছুটা রক্ষা করে, কিন্তু খেলনা ব্লকের DDIM ৫ ধাপ দেখায় সীমাও আছে।

Xiao, Kreis আর Vahdat (ICLR ২০২২) এই টানাপোড়েনকে “generative learning trilemma” বলেছেন: উচ্চ মান, বৈচিত্র্য (mode coverage) আর দ্রুত sampling, তিনটি একসঙ্গে পাওয়া কঠিন; GAN দ্রুত আর মানসম্পন্ন কিন্তু বৈচিত্র্যে দুর্বল হতে পারে, diffusion মান আর বৈচিত্র্যে ভালো কিন্তু ধীর। এটি একটি পর্যবেক্ষণ, প্রমাণিত নিয়ম নয়, আর পরের গবেষণা (distillation, consistency model, কম ধাপের sampler) ব্যবধান কমিয়েছে।

একটি সূক্ষ্ম কথা: “mode collapse কম” মানে সব সময় ভালো নয়। ডেটা ঢাকতে গিয়ে model training-এর বিরল বা নির্দিষ্ট ছবি মনে রেখে ফেলতে পারে; সেই প্রশ্ন ভাবনার প্রশ্ন ৫-এ।

মূল উপলব্ধি: Diffusion কঠিন sampling-কে অনেক সহজ ধাপে ভাগ করে বৈচিত্র্য আর স্থির training পায়; সেই ধাপের সংখ্যাই তার গতির দাম।

প্র ০৪Noise schedule শুধু একটি hyperparameter, নাকি model-এর নকশার অংশ?

প্রথম দেখায় β-এর তালিকা একটি সাধারণ hyperparameter মনে হয়। কিন্তু schedule ঠিক করে, training-এ network কোন noise-স্তরে কত সময় দেয়। t সমান সম্ভাবনায় বাছাই হয়, তাই linear schedule-এ ২৮ × ২৮ ছবির ক্ষেত্রে প্রায় তিন-চতুর্থাংশ training উদাহরণ এমন t-তে, যেখানে SNR ইতিমধ্যে ১-এর নিচে, আর শেষ এক-চতুর্থাংশে প্রায় শুধু noise (stepper ১-এ t = ৭৫০-এ SNR ০.০০৩৪)। সেখানে network-এর শেখার মতো সংকেত সামান্য।

Nichol আর Dhariwal (২০২১) cosine schedule প্রস্তাব করেন এই যুক্তিতে, আর ছোট ছবিতে (যেমন ৩২ × ৩২, ৬৪ × ৬৪) log-likelihood-এ উন্নতির কথা বলেন। তবে ছবির মাপ বাড়লে উল্টো সমস্যা: একই β-তে বড় ছবি কম নষ্ট হয়, কারণ পাশাপাশি অনেক pixel মিলে কম-কম্পাঙ্কের তথ্য (মোটা আকার) noise-এর মধ্যেও টিকে থাকে। Hoogeboom, Heek আর Salimans-এর “simple diffusion” (২০২৩) সহ কয়েকটি কাজ ছবির মাপ অনুযায়ী schedule সরিয়ে নেওয়ার প্রস্তাব দেয়।

আরেকটি খুঁটিনাটি শেষ বিন্দুর SNR. Linear schedule-এ ᾱT ≈ ০.০০০০৪, পুরো শূন্য নয়। Training-এ xT-এ ছবির একটি ক্ষীণ ছাপ থাকে, অথচ sampling শুরু হয় খাঁটি noise থেকে। Lin, Liu, Li আর Yang (WACV ২০২৪) দেখান, এই অমিলে কিছু বড় model খুব উজ্জ্বল বা খুব অন্ধকার ছবি বানাতে পারে না, আর schedule বদলে শেষ SNR শূন্য করার প্রস্তাব দেন।

Karras ও সহকর্মীদের EDM (২০২২) আরও এগিয়ে বলে: training-এ কোন noise-স্তর কত ঘন ঘন দেখানো হবে আর sampling-এ কোন স্তরগুলোতে থামা হবে, এ দুটি আলাদা সিদ্ধান্ত, আলাদাভাবে ঠিক করা যায়। এই পর্বের desktop run-এ linear আর cosine schedule একই সময় পেয়েছে; পার্থক্য অংশ ৮-এ দেখুন, তবে এক seed-এর একটি ছোট run থেকে সাধারণ সিদ্ধান্ত নেওয়া যায় না।

মূল উপলব্ধি: Schedule ঠিক করে network কোন noise-স্তরে শেখে আর sampling কোথা থেকে শুরু হয়; তাই এটি ছবির মাপ আর কাজ অনুযায়ী নকশার সিদ্ধান্ত।

প্র ০৫নিখুঁত denoiser কি নতুন ছবি বানায়, নাকি training-এর ছবি মুখস্থ ফেরত দেয়?

একটি চিন্তার পরীক্ষা করুন। Training ডেটা যদি N টি নির্দিষ্ট ছবি হয়, তাহলে “empirical distribution”-এর noisy রূপ N টি Gaussian-এর মিশ্রণ, আর তার score সূত্রে লেখা যায়, ঠিক খেলনা ব্লকের মতো, শুধু প্রতিটি component-এর variance শূন্য থেকে শুরু। এই নিখুঁত denoiser দিয়ে sample করলে প্রতিটি নমুনা শেষে কোনো একটি training ছবিতেই গিয়ে পড়ে। অর্থাৎ loss-এর দিক থেকে “সেরা” model পুরোপুরি মুখস্থ করে; নতুন ছবি আসে কারণ আসল network সীমিত, আর তার নকশা (convolution, U-Net) মসৃণ, সাধারণ নিয়ম শিখতে উৎসাহ দেয়।

বাস্তবে এর প্রমাণও এসেছে। Carlini ও সহকর্মীরা (USENIX Security ২০২৩) বড় text-to-image model আর CIFAR-10-এ শেখানো diffusion model থেকে কিছু training ছবির প্রায় হুবহু কপি বের করতে পেরেছেন বলে রিপোর্ট করেন, বিশেষ করে যেসব ছবি ডেটায় বহুবার ছিল। Somepalli ও সহকর্মীরা (CVPR ২০২৩) একই ধরনের “replication” মাপেন। অন্যদিকে Kadkhodaie ও সহকর্মীদের কাজ (ICLR ২০২৪) দেখায়, ডেটা যথেষ্ট বড় হলে আলাদা দুই ভাগে শেখানো দুটি model প্রায় একই ছবি বানায়, যা মুখস্থ নয়, সাধারণীকরণের লক্ষণ হিসেবে ব্যাখ্যা করা হয়।

ব্যবহারিক শিক্ষা কয়েকটি। ডুপ্লিকেট ছবি সরানো, ছোট ডেটায় বেশি সময় training না করা, আর generated ছবিকে training সেটের নিকটতম ছবির সঙ্গে মিলিয়ে দেখা (pixel বা feature দূরত্বে) জরুরি। কারও ব্যক্তিগত ছবি বা কপিরাইট করা কাজ training ডেটায় থাকলে এটি আইন ও নৈতিকতার প্রশ্নও তোলে। মাপার পদ্ধতি আর নিরাপত্তার দিক পর্ব ৮১ (Generative model-এর মূল্যায়ন আর নিরাপত্তা)-এর বিষয়; এই কোর্সের পরে AI ethics-এর একটি আলাদা কোর্সেও এ নিয়ে বিস্তারিত আসবে।

মূল উপলব্ধি: Loss-এর হিসাবে নিখুঁত denoiser মুখস্থ করে; নতুন ছবি আসে network-এর সীমা আর নকশার ঝোঁক থেকে, তাই memorisation মাপা আর ঠেকানো diffusion-এর কাজেরই অংশ।

অনুশীলন

প্রথমে খাতায় করুন, তারপর উত্তর মেলান। পাতার ব্লক বদলে নিজের সংখ্যাও যাচাই করতে পারেন।

  1. পূর্ণ numerical: একটি pixel-এ x0 = ০.৫, ε = −১.০, আর একটি ধাপে ᾱt = ০.৩৬, βt = ০.০২. (ক) xt আর SNR কত? (খ) Network ε̂ = −০.৯ দিলে x̂0 কত? (গ) DDPM-এর গড় μ, আর σt = √βt, z = ১.০ হলে xt−1? (ঘ) DDIM (η = ০) দিয়ে ᾱs = ০.৬৪-এর ধাপে লাফ দিলে xs?

    (ক) xt = √০.৩৬ × ০.৫ + √০.৬৪ × (−১) = ০.৩ − ০.৮ = −০.৫; SNR = ০.৩৬ ÷ ০.৬৪ = ০.৫৬২৫. (খ) x̂0 = (−০.৫ − ০.৮ × (−০.৯)) ÷ ০.৬ = ০.২২ ÷ ০.৬ ≈ ০.৩৬৬৭. আসল x0 ছিল ০.৫: ε-এর ০.১ ভুল x̂0-এ √(1 − ᾱ) ÷ √ᾱ = ০.৮ ÷ ০.৬ ≈ ১.৩৩ গুণ হয়ে ০.১৩৩ ভুল দাঁড়ায়; SNR কম হলে এই বিবর্ধন আরও বড়। (গ) β ÷ √(1 − ᾱ) = ০.০২ ÷ ০.৮ = ০.০২৫; μ = (−০.৫ − ০.০২৫ × (−০.৯)) ÷ √০.৯৮ = −০.৪৭৭৫ ÷ ০.৯৮৯৯৫ ≈ −০.৪৮২৩; σ = √০.০২ ≈ ০.১৪১৪, তাই xt−1 ≈ −০.৪৮২৩ + ০.১৪১৪ = −০.৩৪০৯. (ঘ) xs = ০.৮ × ০.৩৬৬৭ + ০.৬ × (−০.৯) = ০.২৯৩৩ − ০.৫৪ ≈ −০.২৪৬৭. (সব সংখ্যা একটি ছোট NumPy script-এ মিলিয়ে নেওয়া।)

  2. সমালোচনা ও নকশা: একজন সহকর্মী বাজারঘরের পণ্যের ছোট ছবিতে DDPM শেখিয়েছেন (T = ১০০০)। দ্রুত করতে তিনি sampling-এ প্রতি ২০তম t নিয়ে DDPM-এর মূল সূত্রই চালান, মূল βt দিয়ে; raw weight ব্যবহার করেন; আর ১০০টি ছবির FID training সেটের সঙ্গে তুলনা করে লেখেন “আমাদের model GAN-এর চেয়ে ভালো”. কী কী ভুল, আর কী বদলাবেন?

    প্রথম ভুল: ধাপ বাদ দিলে প্রতিটি লাফে noise-এর মাপ বদলায়। t থেকে s = t − ২০-এ যেতে কার্যকর β হলো 1 − ᾱt/ᾱs, মূল βt নয়; মূল β দিলে প্রতিটি লাফে প্রায় ২০ গুণ কম noise সরে, আর ছবি noisy থেকে যায়। সমাধান: β নতুন করে হিসাব (respacing) বা DDIM-এর সাধারণ η-সূত্র। দ্বিতীয়ত, raw weight-এর বদলে EMA কপি দিয়ে sample করুন। তৃতীয়ত, FID ১০০টি নমুনায় খুব পক্ষপাতদুষ্ট আর অনিশ্চিত; reference হওয়া উচিত আলাদা test সেট, একই সংখ্যক নমুনা আর একই feature extractor দিয়ে দুটি model-এর তুলনা, কয়েকটি seed-এ। “GAN-এর চেয়ে ভালো” দাবির জন্য একই ডেটা, একই compute বাজেট আর একই মাপে GAN-ও চালাতে হবে; সঙ্গে sampling-এর সময়ও রিপোর্ট করা দরকার, কারণ diffusion সেখানে পিছিয়ে। শেষে training ছবির নিকটতম প্রতিবেশী দেখে কপি হচ্ছে কি না যাচাই।

  3. আউটপুট অনুমান করুন: নিচের কোড কী ছাপবে?
    import numpy as np
    beta = np.array([0.1, 0.2, 0.3, 0.4])
    abar = np.cumprod(1 - beta)
    print(abar)
    print(np.round(abar / (1 - abar), 3))
    x0, eps = 1.0, 0.5
    print(round(float(np.sqrt(abar[1]) * x0 + np.sqrt(1 - abar[1]) * eps), 4))
    [0.9    0.72   0.504  0.3024]
    [9.    2.571 1.016 0.433]
    1.1131

    cumprod চলমান গুণফল দেয়: ০.৯, ০.৯ × ০.৮ = ০.৭২, ০.৭২ × ০.৭ = ০.৫০৪, ০.৫০৪ × ০.৬ = ০.৩০২৪. SNR = ᾱ ÷ (1 − ᾱ): ৯, ২.৫৭১, ১.০১৬, ০.৪৩৩ (০.৩০২৪ ÷ ০.৬৯৭৬ = ০.৪৩৩৪৯…, তাই তিন দশমিকে ০.৪৩৩)। প্রথম সংখ্যা 9. ছাপে, কারণ array-টি float আর বাকিদের দশমিক আছে। শেষ লাইন: index ১ মানে দ্বিতীয় ধাপ, ᾱ = ০.৭২; √০.৭২ × ১ + √০.২৮ × ০.৫ = ০.৮৪৮৫ + ০.২৬৪৬ = ১.১১৩১. দেখার বিষয়: xt-এর মান ১-এর বেশি হতে পারে, কারণ noise যোগ হয়েছে; ছবির পরিসর [−১, ১] শুধু x0-এর জন্য।

  4. কোড লিখুন: NumPy-তে দুটি function লিখুন: q_sample(x0, t, abar, eps), যা closed form দিয়ে xt দেয় (t ১ থেকে T, array-এ index t − 1), আর ddim_step(x_t, eps_hat, t, s, abar), যা η = ০-এ xs দেয়। তারপর দেখান: সঠিক ε দিলে t থেকে সরাসরি s = ০-এ (ᾱ = ১ ধরে) গেলে মূল x0 হুবহু ফিরে আসে।
    import numpy as np
    
    def q_sample(x0, t, abar, eps):
        a = abar[t - 1]                                   # t = 1..T
        return np.sqrt(a) * x0 + np.sqrt(1 - a) * eps
    
    def ddim_step(x_t, eps_hat, t, s, abar):
        a_t = abar[t - 1]
        a_s = 1.0 if s == 0 else abar[s - 1]              # s = 0: পরিষ্কার ছবি
        x0_hat = (x_t - np.sqrt(1 - a_t) * eps_hat) / np.sqrt(a_t)
        return np.sqrt(a_s) * x0_hat + np.sqrt(1 - a_s) * eps_hat
    
    abar = np.cumprod(1 - np.linspace(1e-4, 0.02, 1000))
    rng = np.random.default_rng(0)
    x0 = rng.uniform(-1, 1, 4); eps = rng.standard_normal(4)
    x_t = q_sample(x0, 600, abar, eps)
    back = ddim_step(x_t, eps, 600, 0, abar)              # সঠিক ε দিলে
    print(np.round(x0, 4), np.round(back, 4), np.allclose(x0, back))

    ডেস্কটপে চালালে শেষ লাইন ছাপে [ 0.2739 -0.4604 -0.9181 -0.9669] [ 0.2739 -0.4604 -0.9181 -0.9669] True. সঠিক ε দিলে x̂0-এর সূত্র forward-এর সূত্রকে হুবহু উল্টায়, তাই t = ৬০০ থেকে এক লাফে মূল ছবি ফিরে আসে। এটি একটি ভালো unit test: কোডে index বা বর্গমূলের ভুল থাকলে allclose False দেবে। আসল sampling-এ ε̂ শুধু একটি অনুমান, তাই এক লাফে x0-এ যাওয়া ঝাপসা গড় ছবি দেয়; সেজন্যই ধাপে ধাপে নামা।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমDDPM-এর একটি training ধাপের কাজগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ডেটা থেকে একটি batch ছবি x0 নেওয়া
  2. প্রতিটি ছবির জন্য ১ থেকে T-এর মধ্যে একটি t সমান সম্ভাবনায় বাছাই
  3. ছবির মাপের noise ε ~ N(0, I) টানা
  4. xt = √ᾱt·x0 + √(1 − ᾱt)·ε বানানো
  5. Network-এর ε̂ আর ε-এর mean squared error হিসাব
  6. Gradient নিয়ে optimizer-এর ধাপ আর EMA কপি হালনাগাদ

একটি diffusion model-এ কোনো ধাপে ᾱt = ০.২৫. Signal-to-noise ratio ᾱ ÷ (1 − ᾱ) কত, আর x0-এর গুণাঙ্ক কত?

  • SNR ০.২৫, গুণাঙ্ক ০.২৫
  • SNR ১/৩, গুণাঙ্ক ০.৫
  • SNR ৩, গুণাঙ্ক ০.৫
  • SNR ১/৩, গুণাঙ্ক ০.৮৬৬

DDPM-এর সরল training loss-এ network-এর output-কে কিসের সঙ্গে তুলনা করা হয়?

  • একটি discriminator-এর আসল/নকল সিদ্ধান্তের সঙ্গে
  • সবসময় মূল পরিষ্কার ছবি x0-এর সঙ্গে
  • x_t বানাতে যে noise ε মেশানো হয়েছিল, তার সঙ্গে
  • ঠিক আগের ধাপের ছবি xt−1-এর সঙ্গে

xt = ০.৫, ᾱt = ০.৬৪, আর network-এর অনুমিত noise ε̂ = ০.৫. অনুমিত পরিষ্কার মান x̂0 = (xt − √(1 − ᾱ)·ε̂) ÷ √ᾱ কত?

  • ০.২৫
  • ০.২
  • ০.৩১২৫
  • ০.৮১২৫

DDIM (η = ০) sampling সম্পর্কে কোন বক্তব্য ঠিক?

  • এর জন্য network-কে নতুন loss দিয়ে আবার শেখাতে হয়
  • প্রতিটি ধাপে DDPM-এর চেয়ে বেশি নতুন noise যোগ করে
  • যেকোনো ধাপ-সংখ্যায় ছবির মান একই থাকে
  • একই xT থেকে সবসময় একই ছবি আসে, আর ধাপ বাদ দিয়ে লাফ দেওয়া যায়

T = ১০০০-এর linear (β ০.০০০১ থেকে ০.০২) আর cosine schedule-এর তুলনায় কোনটি ঠিক?

  • Linear schedule মাঝপথে (t = ৫০০) বেশি সংকেত রাখে
  • Linear-এ ছবি অনেক আগেই প্রায় পুরো noise হয়; cosine সংকেত আরও সমানভাবে কমায়
  • Cosine-এ t = ১-এই ছবি প্রায় পুরো noise
  • Schedule শুধু sampling-এ লাগে, training-এ নয়

Diffusion-এর U-Net-এ time embedding কেন লাগে?

  • Input আর output-এর মাপ সমান রাখতে
  • প্রতিটি pixel-এর অবস্থান network-কে জানাতে
  • একই network যাতে জানে এখন কোন noise-স্তর (t), আর সেই অনুযায়ী আচরণ বদলায়
  • Training দ্রুত করতে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Forward: xt = √ᾱt·x0 + √(1 − ᾱt)·ε, ᾱt = ∏(1 − β); training-এ যেকোনো t-তে এক লাফে।
  2. Schedule: SNR = ᾱ ÷ (1 − ᾱ); linear-এ ছবি দ্রুত noise হয় (t ≈ ২৫৯-এ SNR ১), cosine-এ মাঝপথে (t ≈ ৪৯৬)।
  3. Loss: যোগ করা noise অনুমান, MSE; noise অনুমান আর score শেখা একই কাজ।
  4. Sampling: DDPM গড়ের দিকে ছোট ধাপ + নতুন noise, অনেক ধাপ; DDIM (η = ০) একই network-এ noise ছাড়া বড় লাফ, কিন্তু খুব কম ধাপে মান পড়ে।
  5. Network আর মাপ: Time embedding-সহ U-Net, GroupNorm আর EMA; ছবির মান মাপুন একই নমুনা-সংখ্যা, একই reference আর একাধিক seed দিয়ে, সঙ্গে সময়।

পরের পর্বে: পর্ব ৮০ (Latent diffusion আর guidance — text-to-image, CFG আর sampler): ছবির বদলে ছোট latent space-এ diffusion, text দিয়ে নিয়ন্ত্রণ আর classifier-free guidance.

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান