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

Latent diffusion আর guidance — text-to-image, CFG আর sampler

Latent Diffusion and Guidance — Why Diffuse in a Latent Space, Cross-Attention Text Conditioning, Classifier-Free Guidance, Samplers and One Real Text-to-Image Call

Taught in Bangla, this lesson computes the cost of a denoising step in pixel versus latent space, explains the KL-regularised autoencoder, and shows how class labels and text enter through cross-attention. It derives classifier guidance and classifier-free guidance, sweeps the guidance scale on a small latent model trained on MNIST, covers samplers, negative prompts, img2img and inpainting, and traces a real Stable Diffusion v1.5 text-to-image call in a desktop run on the GPU, with in-page Python.

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

  • পিক্সেল বনাম latent space-এ একটি denoising ধাপের খরচ হাতে হিসাব করা
  • KL-regularised autoencoder, f, latent channel আর scaling factor-এর ভূমিকা
  • শ্রেণির লেবেল আর text দিয়ে conditioning, আর cross-attention-এ Q, K, V কোথা থেকে আসে
  • Classifier guidance আর classifier-free guidance-এর সূত্র, আর guidance scale-এর মিল বনাম বৈচিত্র্যের আপস
  • Sampler, ধাপের সংখ্যা, negative prompt, img2img আর inpainting; একটি text-to-image call ধাপে ধাপে

পর্ব ৭৯ (Diffusion model — DDPM, noise schedule আর denoising)-এ diffusion model ছবি বানায় সরাসরি পিক্সেলে, noise থেকে ধাপে ধাপে noise সরিয়ে। এই পর্বে দুটি ধারণা: ছোট latent space-এ diffusion চালানো, আর guidance দিয়ে নমুনাকে একটি শর্তের (শ্রেণি বা text) দিকে ঠেলা। Autoencoder পর্ব ৭৫-এ (Autoencoder), attention-এর Q, K, V পর্ব ৩৩-এ (Self-attention), আর KL divergence AI-এর ভিত্তি কোর্সের পর্ব ৩৮-এ; এখানে শুধু এক লাইনে মনে করিয়ে দেওয়া হবে। PyTorch আর diffusers পাতায় চলে না, তাই তাদের সব আউটপুট ডেস্কটপের সত্যিকারের run থেকে; পাতায় চালানো যায় এমন চারটি ব্লক NumPy, pandas আর matplotlib দিয়ে (প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে)।

১ · কেন latent space-এ diffusion — পিক্সেলের বদলে সংক্ষিপ্ত রূপ

ঢাকার একজন স্থপতি প্রতিটি ইট সরিয়ে নকশা বদলান না। তিনি কাগজে ছোট মাপের নকশা আঁকেন, সেখানে দেয়াল আর জানালা বারবার সরান, আর শেষে একবারই রাজমিস্ত্রি দিয়ে পুরো বাড়ি বানান। নকশায় ইটের রং নেই, কিন্তু কোথায় ঘর, কোথায় দরজা, সব আছে। Latent diffusion ঠিক এই কাজ করে: diffusion-এর বহু ধাপের ভারী কাজটি হয় ছোট “নকশায়” (latent), আর পুরো মাপের ছবি বানায় একটি decoder, শেষে মাত্র একবার।

Diffusion model প্রতিটি ধাপে পুরো ছবির ওপর একটি বড় network (সাধারণত U-Net) চালায়, আর একটি ছবি বানাতে লাগে কয়েক ডজন থেকে কয়েকশো ধাপ। অথচ পিক্সেলের বড় অংশ চোখে প্রায় অদৃশ্য খুঁটিনাটি: পাশাপাশি পিক্সেল প্রায় একই রকম। Latent diffusion-এর মূল ধারণা হলো কাজটি দুই ভাগে ভাগ করা। প্রথমে একটি autoencoder ছবিকে একটি ছোট latent space-এ চেপে আনে আর সেখান থেকে ফিরিয়ে আনতে শেখে; তারপর diffusion model শেখে শুধু সেই latent-এর বণ্টন। Diffusion-এর মূল কাঠামো (forward noising, ε ভবিষ্যদ্বাণী, DDPM আর DDIM, time embedding-সহ U-Net) পর্ব ৭৯-এর বিষয়; এখানে সেটি জানা ধরে নিচ্ছি।

খরচ কতটা কমে, তা হিসাব করা সহজ। Stable Diffusion v1-এর model card বলে তার autoencoder ছবিকে f = ৮ গুণ ছোট করে আর latent-এ ৪টি channel রাখে: H × W × 3 ছবি → H/8 × W/8 × 4 latent. নিচের stepper-এ একটি denoising ধাপের খরচ দুই জায়গায় পাশাপাশি দেখুন। (FLOP মানে floating-point operation; একটি গুণ আর একটি যোগ মিলে ২ FLOP ধরা হয়েছে।)

f = 8, latent channel = 4, U-Net-এর প্রথম স্তরে 320 channel
ধাপ
পিক্সেল
latent
অনুপাত
  1. পিক্সেল ছবি. ৫১২ × ৫১২ রঙিন ছবিতে ৭,৮৬,৪৩২টি সংখ্যা, আর ২,৬২,১৪৪টি অবস্থান (প্রতিটি অবস্থানে ৩টি রং)।
  2. Latent. Encoder প্রতি দিকে ৮ গুণ ছোট করে: ৬৪ × ৬৪, আর প্রতিটি অবস্থানে ৪টি সংখ্যা। মোট ১৬,৩৮৪, অর্থাৎ ৪৮ গুণ কম সংখ্যা।
  3. Conv layer. একটি ৩ × ৩ convolution-এর খরচ অবস্থানের সংখ্যার সমানুপাতিক। ৩২০ channel থেকে ৩২০ channel-এ পিক্সেলে ৪৮৩.২ GFLOP, latent-এ ৭.৫৫ GFLOP: ৬৪ গুণ কম।
  4. Attention. Self-attention-এর খরচ অবস্থানের সংখ্যার বর্গের সমানুপাতিক। তাই সাশ্রয় ৬৪² = ৪,০৯৬ গুণ। পুরো মাপে একটি head-এর score matrix-ই fp16-এ ১২৮ GiB, যা কোনো সাধারণ GPU-তে আঁটে না; latent-এ ৩২ MiB.
  5. পুরো sampling. ৫০ ধাপ, আর guidance-এর জন্য প্রতি ধাপে U-Net দুবার (অংশ ৪): মোট ১০০টি U-Net call. প্রতিটি call-এর সাশ্রয় ১০০ বার কাজে লাগে; latent থেকে ছবি বানানোর decoder চলে শেষে একবারই।
  6. আমাদের ছোট model. MNIST-এর ২৮ × ২৮ × ১ ছবিকে অংশ ২-এর autoencoder ৭ × ৭ × ৪-এ আনে: সংখ্যা মাত্র ৪ গুণ কম, কিন্তু অবস্থান ১৬ গুণ কম।

একই হিসাব পাতায় চালিয়ে দেখুন। ব্লকটি conv আর attention-এর FLOP দুই জায়গায় গোনে আর ৫০ ধাপে যোগ করে।

import numpy as np

def conv_flops(h, w, cin, cout, k=3):      # একটি k×k conv layer: গুণ + যোগ = 2 FLOP
    return 2 * h * w * cin * cout * k * k

def attn_flops(h, w, d):                   # self-attention: QKᵀ আর (softmax)·V, n = h·w
    n = h * w
    return 2 * (2 * n * n * d)

spaces = {"pixel 512x512x3": (512, 512, 3), "latent 64x64x4": (64, 64, 4),
          "MNIST pixel 28x28x1": (28, 28, 1), "MNIST latent 7x7x4": (7, 7, 4)}
for name, (h, w, c) in spaces.items():
    print(f"{name:20s}: {h * w * c:>9,} সংখ্যা | {h * w:>7,} অবস্থান")

px, lt = (512, 512), (64, 64)
r_num = (512 * 512 * 3) / (64 * 64 * 4)
r_conv = conv_flops(*px, 320, 320) / conv_flops(*lt, 320, 320)
r_attn = attn_flops(*px, 320) / attn_flops(*lt, 320)
print(f"\nসংখ্যার অনুপাত {r_num:.0f}x | conv-এর FLOP অনুপাত {r_conv:.0f}x | attention-এর {r_attn:,.0f}x")

for name, hw in [("pixel", px), ("latent", lt)]:
    c = conv_flops(*hw, 320, 320) * 50 / 1e12
    a = attn_flops(*hw, 320) * 50 / 1e12
    print(f"{name:6s}: 50 ধাপে একটি conv layer {c:8.2f} TFLOP | একটি attention layer {a:10.2f} TFLOP")

n_px, n_lt = 512 * 512, 64 * 64            # একটি head-এর n × n score matrix, fp16 = 2 byte
print(f"\nscore matrix (fp16): পিক্সেলে {n_px ** 2 * 2 / 2**30:.0f} GiB | latent-এ {n_lt ** 2 * 2 / 2**20:.0f} MiB")

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

pixel 512x512x3     :   786,432 সংখ্যা | 262,144 অবস্থান
latent 64x64x4      :    16,384 সংখ্যা |   4,096 অবস্থান
MNIST pixel 28x28x1 :       784 সংখ্যা |     784 অবস্থান
MNIST latent 7x7x4  :       196 সংখ্যা |      49 অবস্থান

সংখ্যার অনুপাত 48x | conv-এর FLOP অনুপাত 64x | attention-এর 4,096x
pixel : 50 ধাপে একটি conv layer    24.16 TFLOP | একটি attention layer    4398.05 TFLOP
latent: 50 ধাপে একটি conv layer     0.38 TFLOP | একটি attention layer       1.07 TFLOP

score matrix (fp16): পিক্সেলে 128 GiB | latent-এ 32 MiB

প্রথম চার সারি stepper-এর মাপগুলো মেলায়। তারপর তিনটি অনুপাত: সংখ্যায় ৪৮ গুণ, conv-এ ৬৪ গুণ, attention-এ ৪,০৯৬ গুণ। ৫০ ধাপে একটি attention layer পিক্সেলে প্রায় ৪,৪০০ TFLOP, latent-এ ১.০৭. তাই latent model ৬৪ × ৬৪-এও attention রাখতে পারে। শেষ লাইনটি মেমরির দিক: একটি score matrix-ই পিক্সেলে ১২৮ GiB. এগুলো একটি layer-এর খাতা-কলমের হিসাব; মাপা সময় অংশ ৫ আর ৭-এ।

ইতিহাস: Latent diffusion প্রস্তাব করেন Robin Rombach, Andreas Blattmann, Dominik Lorenz, Patrick Esser আর Björn Ommer, “High-Resolution Image Synthesis with Latent Diffusion Models” প্রবন্ধে (arXiv ডিসেম্বর ২০২১, CVPR ২০২২)। সারাংশ বলে, pixel-space diffusion optimize করতে প্রায়ই “hundreds of GPU days” লাগে। Stable Diffusion এই কাজের ওপর গড়া; diffusers-এর ডকুমেন্টেশন একে CompVis, Stability AI, Runway আর LAION-এর গবেষক ও প্রকৌশলীদের তৈরি বলে উল্লেখ করে (লেখার সময়ে, নভেম্বর ২০২৬; পরিবর্তন হতে পারে)।

২ · Autoencoder ধাপ — KL-regularised latent, f আর scaling factor

Autoencoder-কে দুটি বিপরীত চাহিদা মেটাতে হয়। এক, decoder যেন latent থেকে ছবি প্রায় হুবহু ফেরত আনে, নইলে diffusion যত ভালোই শিখুক, শেষ ছবি ঝাপসা হবে। দুই, latent space যেন “ভদ্র” হয়: মান যেন অসীমে না ছড়ায় আর কাছাকাছি latent যেন কাছাকাছি ছবি দেয়, কারণ diffusion এই space-এ Gaussian noise যোগ করবে আর সরাবে। পর্ব ৭৫-এর সাধারণ autoencoder-এ দেখেছেন, এলোমেলো latent decode করলে অর্থহীন ছবি আসে; সাধারণ AE-র latent নিজে ভালো sampling space নয়। পর্ব ৭৬ (Variational autoencoder)-এর KL divergence পদ latent-কে N(0, I)-এর দিকে টেনে space গুছিয়ে রাখে।

Latent diffusion-এর প্রবন্ধ এখানে একটি মাঝামাঝি পথ নেয়। পুরো VAE-র মতো শক্ত KL নয় (তাতে ছবি ঝাপসা হয়), বরং খুব ছোট ওজনের একটি KL পদ, যাকে প্রবন্ধ “KL-reg” বলে; বিকল্প হিসেবে VQ-VAE-ধরনের codebook (“VQ-reg”) নিয়েও পরীক্ষা আছে। ছবি ধারালো রাখতে autoencoder-কে শুধু পিক্সেলের MSE দিয়ে নয়, একটি perceptual loss আর একটি patch-ভিত্তিক adversarial loss দিয়েও train করা হয়েছিল বলে প্রবন্ধ লেখে (adversarial loss-এর খেলা পর্ব ৭৭-এ দেখেছেন)। ফলে autoencoder সামলায় খুঁটিনাটি, আর diffusion শেখে অর্থপূর্ণ গঠন।

আরেকটি ছোট কিন্তু জরুরি বিষয়: scaling factor. Encoder-এর latent-এর মান যেকোনো পরিসরে থাকতে পারে, অথচ diffusion-এর noise schedule ধরে নেয় ডেটার variance প্রায় ১. তাই latent-কে একটি স্থির সংখ্যা দিয়ে গুণ করে নেওয়া হয়। Stable Diffusion v1.5-এ scaling_factor = 0.18215 (অংশ ৭-এর run-এ ছাপা): diffusion-এর আগে গুণ, decode-এর আগে ভাগ। এই ভাগ ভুলে গেলে ছবি অদ্ভুত রঙের দাগে ভরে যায়।

নিজের ছোট model-এর জন্য MNIST-এ একটি ছোট KL-regularised convolutional autoencoder train করা হলো: ২৮ × ২৮ × ১ → ৭ × ৭ × ৪ (f = ৪), stride-2 convolution দিয়ে নামা আর transposed convolution দিয়ে ওঠা, KL ওজন খুব ছোট। এখানে MNIST নেওয়া হয়েছে, কারণ অঙ্কের ১০টি শ্রেণি guidance মাপার পরিষ্কার মাপকাঠি দেয় (MNIST: LeCun, Cortes আর Burges; লাইসেন্স সাধারণত CC BY-SA 3.0 বলে উল্লেখ করা হয়, তবু ব্যবহারের আগে নিজে যাচাই করুন)। সত্যিকারের run (ডেস্কটপ, GPU-তে; torch 2.15.0, Python 3.14.4):

~/bazarghar-dl$ uv run python train_ae.py
AE params 200,329 | latent 7x7x4 = 196 numbers (pixels 784) | KL weight 0.0001
epoch 1: train rec(sum/img)  11.39 | KL/img    88.8 | test MSE/pixel 0.00266
epoch 2: train rec(sum/img)   4.28 | KL/img    83.9 | test MSE/pixel 0.00249
epoch 3: train rec(sum/img)   3.95 | KL/img    79.7 | test MSE/pixel 0.00237
epoch 4: train rec(sum/img)   3.79 | KL/img    77.6 | test MSE/pixel 0.00241
epoch 5: train rec(sum/img)   3.67 | KL/img    76.2 | test MSE/pixel 0.00228
epoch 6: train rec(sum/img)   3.56 | KL/img    75.2 | test MSE/pixel 0.00234
epoch 7: train rec(sum/img)   3.50 | KL/img    74.5 | test MSE/pixel 0.00226
epoch 8: train rec(sum/img)   3.44 | KL/img    73.9 | test MSE/pixel 0.00227
latent mean 0.0085 std 0.5204 -> scale factor 1/std = 1.9216
time 25.8 s (GPU) | peak GPU memory 3307 MiB

কয়েকটি জিনিস লক্ষ্য করুন। ৭ × ৭ × ৪ = ১৯৬টি সংখ্যা থেকে test ছবি ফেরত আসে প্রতি পিক্সেলে গড় বর্গ-ত্রুটি (MSE) প্রায় ০.০০২৩ নিয়ে ([০, ১] পরিসরে), অর্থাৎ গড়ে প্রায় ০.০৫ পিক্সেল-মানের তফাত: চোখে প্রায় হুবহু। KL পদ প্রতি ছবিতে প্রায় ৭৪ nat-এ থেমেছে, শূন্যের কাছে নয়: ওজন ছোট বলে latent N(0, I) হয়নি, শুধু লাগাম টানা হয়েছে। Latent-এর standard deviation ০.৫২০৪, তাই এখানে scaling factor ১/০.৫২০৪ ≈ ১.৯২; Stable Diffusion-এর ০.১৮২১৫ ঠিক একই কাজ করে, শুধু তার latent-এর ছড়ানো বেশি বলে সংখ্যাটি ১-এর নিচে। পুরো train set আগেভাগে একবার latent-এ এনে রাখা হলো; এরপর diffusion আর পিক্সেল দেখে না।

৩ · Conditioning — শ্রেণির লেবেল থেকে text, আর cross-attention

“একটি লাল মগ” লিখলে model লাল মগ বানাবে কীভাবে? Diffusion-এর denoiser εθ(xt, t)-কে একটি তৃতীয় input দেওয়া হয়: শর্ত c. তখন model শেখে εθ(xt, t, c), অর্থাৎ conditional generation. শর্ত সবচেয়ে সহজ রূপে একটি শ্রেণির লেবেল (“অঙ্ক ৭”); এটিকে একটি embedding vector বানিয়ে time embedding-এর সঙ্গে যোগ করে দিলেই চলে। আমাদের MNIST model ঠিক এটাই করে।

Text-এ শব্দের ক্রম আর সম্পর্ক আছে, একটি vector-এ চাপলে তা হারায়। তাই latent diffusion text-কে প্রথমে একটি text encoder দিয়ে token-প্রতি একটি করে vector-এর সারিতে রূপান্তর করে, আর U-Net-এর ভেতরে cross-attention layer দিয়ে ছবির প্রতিটি অবস্থানকে সেই সারির দিকে “তাকাতে” দেয়। পর্ব ৩৩-এর self-attention-এর সঙ্গে একমাত্র পার্থক্য হলো query আর key/value আসে দুই আলাদা উৎস থেকে:

Q = WQ·φ(zt),  K = WK·τ(y),  V = WV·τ(y),  Attention = softmax(QKT/√d)·V

এখানে φ(zt) U-Net-এর feature map (প্রতিটি অবস্থান একটি token), τ(y) text encoder-এর output, d query/key-এর মাত্রা। ছবির প্রতিটি অবস্থান prompt-এর প্রাসঙ্গিক শব্দের তথ্য নিজের feature-এ মেশায়। Stable Diffusion v1-এর text encoder হলো OpenAI-এর CLIP ViT-L/14-এর text অংশ, যা train-এর সময় স্থির (frozen) রাখা হয়েছিল বলে model card জানায়। CLIP-এর বিস্তারিত পর্ব ৮২ (Multimodal model — CLIP, image captioning আর vision-language model)-এ আসবে।

Latent diffusion-এর পুরো পথ — prompt থেকে ছবি

Text encoder শর্ত দেয়, U-Net latent-এ বারবার noise সরায়, decoder শেষে একবার ছবি বানায়

Latent diffusion pipeline: text encoder, U-Net with cross-attention, VAE decoder prompt “a red mug …” tokenizer 77টি token id text encoder CLIP, frozen τ(y): 77 × 768 noise zT 64 × 64 × 4 U-Net εθ cross-attention + time embedding t N ধাপ (sampler) z0: 64 × 64 × 4 VAE decoder ছবি 512 × 512 × 3 VAE encoder শুধু training আর img2img-এ K, V
ছবি ৮০.১ — বাম কলাম প্রতি prompt-এ একবার চলে, ডান কলামের U-Net চলে প্রতি ধাপে (guidance থাকলে দুবার), আর decoder চলে শেষে একবার। Encoder লাগে শুধু training-এ বা কোনো ছবি থেকে শুরু করলে (img2img).

Prompt একবারই encode হয়; প্রতি ধাপের প্রতিটি cross-attention layer সেই একই K, V ব্যবহার করে। Stable Diffusion v1.5-এর U-Net config-এ cross-attention-ওয়ালা block আছে ৬৪ × ৬৪, ৩২ × ৩২ আর ১৬ × ১৬ মাপে (আর মাঝের ৮ × ৮ block-এ), তাই prompt বড় গঠন আর খুঁটিনাটি দুটোতেই প্রভাব ফেলে।

৪ · Guidance — classifier guidance থেকে classifier-free guidance

শুধু শর্ত দিলেই model শর্ত পুরো মানে না। Conditional model-এর নমুনা প্রায়ই prompt-এর সঙ্গে আলগাভাবে মেলে: অংশ ৭-এর run-এ guidance ছাড়া “লাল মগ” চাইলে এসেছে মাটির ওপর একটি হালকা রঙের কাপ। তাই নমুনা নেওয়ার সময় শর্তের দিকে একটু বাড়তি ঠেলা দেওয়া হয়, যার নাম guidance.

Classifier guidance (Dhariwal আর Nichol, “Diffusion Models Beat GANs on Image Synthesis”, NeurIPS ২০২১) প্রথম জনপ্রিয় পথ। Noisy ছবিতে train করা একটি আলাদা classifier-এর gradient ∇x log p(c | xt), একটি scale s দিয়ে গুণ করে, প্রতি ধাপে নমুনাকে শ্রেণির দিকে ঠেলে। সমস্যা: noisy ছবির জন্য আলাদা classifier train করতে হয়, আর text-এর মতো খোলা শর্তে তা কঠিন।

Classifier-free guidance (CFG; Jonathan Ho আর Tim Salimans, NeurIPS ২০২১-এর একটি workshop-এ প্রথম, পরে arXiv ২০২২) আলাদা classifier ছাড়াই একই কাজ করে। Training-এ কিছু নমুনায় (যেমন ১০%) শর্ত মুছে “শূন্য শর্ত” ∅ দেওয়া হয়, তাই একই network শর্তসহ εθ(xt, c) আর শর্তহীন εθ(xt, ∅) দুটোই শেখে। Stable Diffusion v1.5-এর model card-ও বলে, text conditioning ১০% সময় বাদ দেওয়া হয়েছিল। নমুনা নেওয়ার সময় দুটিকে মেলানো হয়:

ε̃ = εθ(xt, ∅) + w · ( εθ(xt, c) − εθ(xt, ∅) )

এখানে w হলো guidance scale. বন্ধনীর ভেতরের পার্থক্যটি হলো “শর্ত দিলে noise-এর অনুমান কোন দিকে সরে”। w = ০ মানে শর্তহীন model, w = ১ মানে সাধারণ conditional model, আর w > ১ মানে সেই দিকে আরও বেশি দূর যাওয়া (extrapolation). diffusers-এর guidance_scale ঠিক এই w; তার ডকুমেন্টেশন বলে guidance চালু হয় guidance_scale > 1 হলে, আর Stable Diffusion pipeline-এ ডিফল্ট মান ৭.৫ (লেখার সময়ে)। কিছু প্রবন্ধ (মূল CFG প্রবন্ধসহ) সূত্রটি (1 + w) দিয়ে লেখে, তাই সেখানে তাদের w = ০ মানে এখানকার w = ১; সংখ্যা মেলানোর আগে কোন রীতি, দেখে নিন।

ε_u = (0.20, −0.50, 0.10), ε_c = (0.50, −0.30, −0.20) · x_t = (0.6, −0.4, 0.2), ᾱ_t = 0.5
ধাপ
ε̃
‖ε̃‖
x̂₀
  1. দুই অনুমান. একই xt আর t-এ network দুবার চলে: একবার শর্ত c দিয়ে, একবার শূন্য শর্ত ∅ দিয়ে।
  2. পার্থক্য. d = εc − εu = (০.৩০, ০.২০, −০.৩০)। এটিই শর্তের “দিক”: prompt থাকায় অনুমান কোথায় সরল।
  3. w = ০. শুধু শর্তহীন অনুমান; prompt-এর কোনো প্রভাব নেই। এখান থেকে পাওয়া পরিষ্কার ছবির অনুমান x̂0 = (০.৬৪৯, −০.০৬৬, ০.১৮৩)।
  4. w = ১. হুবহু শর্তসহ অনুমান; guidance ছাড়া conditional model এটাই দেয়।
  5. w = ৩. একই দিকে তিন গুণ দূরে: (১.১০, ০.১০, −০.৮০)। ε̃-এর দৈর্ঘ্য ০.৬১৬ থেকে বেড়ে ১.৩৬৪, আর x̂0-এর তৃতীয় সংখ্যা ০.৪৮৩ থেকে ১.০৮৩-এ। নমুনা শর্তের দিকে জোরে সরে।
  6. w = ৭. ε̃-এর দৈর্ঘ্য ৩.১৭৮; প্রশিক্ষণের ε-এর প্রতিটি সংখ্যা N(০, ১) থেকে, তাই তিনটি সংখ্যার দৈর্ঘ্য সাধারণত √৩ ≈ ১.৭-এর আশেপাশে। এটি তার অনেক বাইরে। x̂0-এর মান −১.৪৫ থেকে ২.২৮: খুব বড় w-এ ছবি অতি-উজ্জ্বল বা অতি-সম্পৃক্ত (oversaturated) হওয়ার এটাই কারণ।
  7. খরচ. CFG-এর দাম প্রতি ধাপে দুটি U-Net call; ৫০ ধাপে ১০০টি। দুটি input সাধারণত একই batch-এ চলে।

সংখ্যাগুলো একটি ধাপের। পুরো sampling-এ guidance কী করে, তা দেখতে একটি ২-মাত্রার খেলনা নিন, যেখানে নির্ভুল ε সূত্র থেকে হিসাব করা যায়, কোনো network লাগে না। দুই শ্রেণি: “থলে” N((−১, ০), ০.৮²I) আর “মগ” N((১, ০), ০.৮²I), সমান ভাগে। প্রতিটি শ্রেণির জন্য সঠিক εc জানা; শর্তহীন εu হলো দুই শ্রেণির ε-এর ওজনদার গড়, যেখানে ওজন হলো xt দেখে প্রতিটি শ্রেণির posterior সম্ভাবনা। ২,০০০টি নমুনা “মগ” চেয়ে DDIM-এর ৫০ ধাপে বানানো হলো।

import numpy as np

mu = np.array([[-1.0, 0.0], [1.0, 0.0]])   # দুই শ্রেণির কেন্দ্র: 0 = থলে, 1 = মগ
s2 = 0.8 ** 2                              # প্রতিটি শ্রেণির variance
abar = np.cumprod(1 - np.linspace(1e-4, 0.02, 1000))

def eps_class(x, t, c):                    # নির্ভুল ε: শ্রেণি c জানা থাকলে
    a = abar[t]; v = a * s2 + 1 - a
    return (x - np.sqrt(a) * mu[c]) * np.sqrt(1 - a) / v

def eps_uncond(x, t):                      # শ্রেণি না জানলে: দুই শ্রেণির ওজনদার গড়
    a = abar[t]; v = a * s2 + 1 - a
    d = [-((x - np.sqrt(a) * m) ** 2).sum(1) / (2 * v) for m in mu]
    p = np.exp(d[1] - np.logaddexp(d[0], d[1]))[:, None]   # P(শ্রেণি 1 | x_t)
    return (1 - p) * eps_class(x, t, 0) + p * eps_class(x, t, 1)

rng = np.random.default_rng(42)
ts = np.linspace(999, 0, 50).round().astype(int)
for w in [0, 1, 3, 7]:
    x = rng.standard_normal((2000, 2))
    for i, t in enumerate(ts):             # DDIM, 50 ধাপ, সবাই শ্রেণি 1 (মগ) চায়
        eu = eps_uncond(x, t)
        e = eu + w * (eps_class(x, t, 1) - eu)
        a, ap = abar[t], (abar[ts[i + 1]] if i + 1 < len(ts) else 1.0)
        x0 = (x - np.sqrt(1 - a) * e) / np.sqrt(a)
        x = np.sqrt(ap) * x0 + np.sqrt(1 - ap) * e
    print(f"w={w}: x>0 ভাগ {(x[:, 0] > 0).mean():.3f} | x-এর গড় {x[:, 0].mean():+.3f} | std {x.std(0).mean():.3f}")

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

w=0: x>0 ভাগ 0.491 | x-এর গড় -0.036 | std 1.004
w=1: x>0 ভাগ 0.908 | x-এর গড় +1.012 | std 0.771
w=3: x>0 ভাগ 1.000 | x-এর গড় +1.804 | std 0.634
w=7: x>0 ভাগ 1.000 | x-এর গড় +2.628 | std 0.578

w = ০-এ prompt-এর কোনো প্রভাব নেই: অর্ধেক নমুনা (০.৪৯১) মগের দিকে, অর্ধেক থলের দিকে, আর ছড়ানো প্রায় ১.০, পুরো মিশ্রণের মতো। w = ১-এ ৯০.৮% নমুনা মগের দিকে; বাকিগুলো দুই শ্রেণির মিশে-যাওয়া অংশ থেকে, কারণ মগের বণ্টনেরও কিছু অংশ x < ০-এ পড়ে। গড় +১.০১২, মগের আসল কেন্দ্র ১.০-এর প্রায় সমান, আর ছড়ানো ০.৭৭১, আসল ০.৮-এর কাছে: w = ১ সঠিক conditional বণ্টন দেয়। এরপর আসল শিক্ষা। w = ৩-এ সব নমুনা মগের দিকে (১.০০০), কিন্তু গড় +১.৮০৪, আসল কেন্দ্র ছাড়িয়ে গেছে, আর ছড়ানো কমে ০.৬৩৪. w = ৭-এ গড় +২.৬২৮, ছড়ানো ০.৫৭৮. অর্থাৎ guidance নমুনাকে শর্তের সবচেয়ে “স্পষ্ট” রূপের দিকে ঠেলে বণ্টনকে সরু করে, আর বড় w-এ এমন জায়গায় নেয় যেখানে আসল ডেটা প্রায় নেই: শর্তের সঙ্গে মিল আর বৈচিত্র্যের আপস।

কেন কাজ করে, এক লাইনে: ε আর score (∇ log p)-এর সম্পর্ক ধরলে, εc − εu সমানুপাতিক −∇ log p(c | xt)-এর, অর্থাৎ একটি “অন্তর্নিহিত classifier”-এর gradient. তাই CFG আসলে classifier guidance-ই, শুধু classifier-টি একই network-এর দুই রূপের পার্থক্য থেকে আসে। স্বজ্ঞাতভাবে নমুনা আসে মোটামুটি p(x | c)·p(c | x)w−1 থেকে, যা w > ১-এ শ্রেণির “নিশ্চিত” অংশকে বেশি ওজন দেয় (পুরো sampling পথে এটি হুবহু সঠিক নয়)।

৫ · নিজের ছোট latent diffusion — MNIST-এ CFG-এর সত্যিকারের sweep

এবার অংশ ২-এর autoencoder-এর latent-এ একটি শ্রেণি-শর্তযুক্ত diffusion model train করা হলো, আর guidance scale বদলে মাপা হলো। নকশা যতটা সম্ভব ছোট আর পরিষ্কার:

  • Denoiser: ৭ × ৭ × ৪ latent-এর ওপর ৪টি residual block (১২৮ channel, GroupNorm, SiLU), কোনো downsampling নেই; time embedding আর শ্রেণির embedding যোগ করে প্রতিটি block-এ দেওয়া। মোট ১২,৯২,৮০৪টি parameter.
  • CFG-এর জন্য training: শ্রেণি ০ থেকে ৯, আর একটি বাড়তি লেবেল ১০ মানে “শর্ত নেই”; প্রতিটি batch-এ এলোমেলোভাবে ১০% নমুনার লেবেল ১০ করে দেওয়া (condition dropout).
  • Diffusion: T = ১,০০০, β রৈখিকভাবে ১০−৪ থেকে ০.০২, ε-ভবিষ্যদ্বাণীর MSE loss, AdamW (lr ৩ × ১০−৪), batch ১২৮, ২০ epoch, seed ১. Sampling: DDIM, ৫০ ধাপ, η = ০ (নির্ধারিত পথ)।
  • বিচারক: আলাদা train করা একটি ছোট CNN digit classifier (৪,২১,৬৪২ parameter, ৩ epoch, MNIST test accuracy ০.৯৮৭২, CPU-তে ৫০.৮ সেকেন্ড)। প্রতিটি w-এ প্রতিটি শ্রেণির ১০০টি, মোট ১,০০০টি নমুনা; একই noise (seed ১২৩) সব w-এ, যাতে শুধু w-এর প্রভাব দেখা যায়।
  • মাপকাঠি: class accuracy (চাওয়া শ্রেণি আর classifier-এর অনুমান মিলল কি না), গড় confidence, আর diversity: প্রতিটি শ্রেণির ১০০টি নমুনায় প্রতিটি পিক্সেলের standard deviation-এর গড় (০–২৫৫ মাপে), দশ শ্রেণির গড়।

সত্যিকারের run (ডেস্কটপ, GPU-তে; torch 2.15.0, Python 3.14.4; training log-এর মাঝের epoch বাদ দিয়ে সংক্ষেপিত):

~/bazarghar-dl$ uv run python train_dm.py latent 20
latent: input (4, 7, 7) | params 1,292,804 | resume at epoch 0
epoch 1: loss 0.1887 | 5.3 s
epoch 2: loss 0.1230 | 4.9 s
epoch 3: loss 0.1127 | 5.0 s
…
epoch 9: loss 0.0967 | 5.9 s
epoch 10: loss 0.0952 | 5.0 s
…
epoch 19: loss 0.0891 | 4.9 s
epoch 20: loss 0.0891 | 4.9 s
total train time 101.4 s (GPU) | peak GPU memory 153 MiB
~/bazarghar-dl$ uv run python evaluate.py latent cfg
real test 1000: acc 0.985 conf 0.984 diversity 37.7
latent: 20 epochs, train time 101.4 s
w=0.0  steps=50  | class acc 0.088 | conf 0.834 | diversity  50.0 | 1.26 s for 1000 images
w=1.0  steps=50  | class acc 0.721 | conf 0.909 | diversity  43.3 | 1.02 s for 1000 images
w=3.0  steps=50  | class acc 0.995 | conf 0.997 | diversity  33.1 | 1.98 s for 1000 images
w=7.0  steps=50  | class acc 1.000 | conf 1.000 | diversity  26.9 | 2.00 s for 1000 images
peak GPU memory 310 MiB

চারটি সারি পাশাপাশি পড়ুন। w = ০: শর্তহীন model; class accuracy ০.০৮৮, অর্থাৎ দশ শ্রেণির আন্দাজের (০.১০) কাছাকাছি, যেমন হওয়ার কথা। Diversity ৫০.০, আসল ছবির ৩৭.৭-এর চেয়ে বেশি, কারণ এখানে একটি “শ্রেণি”-র ১০০টি ছবি আসলে নানা অঙ্কের মিশ্রণ। w = ১: guidance ছাড়া conditional model; accuracy মাত্র ০.৭২১. ছোট model শর্তকে পুরো মানতে শেখেনি। w = ৩: accuracy ০.৯৯৫, confidence ০.৯৯৭, আসল test ছবির (০.৯৮৫, ০.৯৮৪) চেয়েও বেশি “স্পষ্ট”; কিন্তু diversity ৩৩.১, আসলের ৩৭.৭-এর নিচে। w = ৭: accuracy ১.০০০, কিন্তু diversity ২৬.৯: একই অঙ্কের ছবিগুলো একে অপরের মতো হয়ে যাচ্ছে। অংশ ৪-এর খেলনার একই ছবি, এবার আসল network-এ। সময়ের দিকে, w = ১-এ ১,০০০টি ছবি ১.০২ সেকেন্ডে, আর w = ৩ ও ৭-এ প্রায় ২.০ সেকেন্ডে: CFG-এর দুই শাখা প্রায় দ্বিগুণ কাজ। (w = ০-এর প্রথম সারিতে ১.২৬ সেকেন্ডে GPU-র প্রথমবারের গরম হওয়ার সময়ও মিশে আছে।)

পিক্সেল বনাম latent. তুলনার জন্য একই diffusion নিয়ম, একই লেবেল-ড্রপ আর একই ২০ epoch দিয়ে সরাসরি ২৮ × ২৮ পিক্সেলে একটি ছোট U-Net (২৮ → ১৪ → ৭ → ১৪ → ২৮, ১৯,৮২,৩৩৭ parameter) train করা হলো। সংক্ষেপিত সত্যিকারের run:

~/bazarghar-dl$ uv run python train_dm.py pixel 10   # দুই ভাগে, মাঝে checkpoint
pixel: input (1, 28, 28) | params 1,982,337 | resume at epoch 0
epoch 1: loss 0.0688 | 25.7 s
epoch 2: loss 0.0310 | 24.5 s
…
pixel: input (1, 28, 28) | params 1,982,337 | resume at epoch 10
…
epoch 20: loss 0.0219 | 24.7 s
total train time 496.7 s (GPU) | peak GPU memory 963 MiB
~/bazarghar-dl$ uv run python evaluate.py pixel cfg   # 250টি করে চার ভাগে
pixel: 20 epochs, train time 496.7 s
w=0.0  steps=50  | class acc 0.111 | conf 0.865 | diversity  47.1 | 7.19 s for 1000 images
w=1.0  steps=50  | class acc 0.940 | conf 0.970 | diversity  38.8 | 6.97 s for 1000 images
w=3.0  steps=50  | class acc 1.000 | conf 0.999 | diversity  33.6 | 13.84 s for 1000 images
w=7.0  steps=50  | class acc 1.000 | conf 1.000 | diversity  30.5 | 14.06 s for 1000 images
peak GPU memory 1118 MiB

ফলটি সৎভাবে পড়ুন। খরচে latent এগিয়ে: ২০ epoch-এ training ১০১ বনাম ৪৯৭ সেকেন্ড (প্রায় ৫ গুণ), আর w = ৩-এ ১,০০০টি ছবি ১.৯৮ বনাম ১৩.৮৪ সেকেন্ড (প্রায় ৭ গুণ; পিক্সেল model মেমরির জন্য চার ভাগে চলেছে)। মানে পিক্সেল এগিয়ে: guidance ছাড়া (w = ১) accuracy ০.৯৪০ বনাম ০.৭২১, আর w = ৩-এ ১.০০০ বনাম ০.৯৯৫. এই ছোট পরীক্ষায় latent model-এর দাম হলো autoencoder-এর হারানো তথ্য আর একটি ছোট denoiser; guidance সেই ঘাটতির বড় অংশ পূরণ করেছে। দুই model-এ একই প্রবণতা: w বাড়লে accuracy বাড়ে, diversity কমে। একটি seed-এর একটি run, তাই সংখ্যাগুলো সাধারণ নিয়ম নয়।

নিচের ব্লক একই run-এর আসল নমুনা আঁকে: প্রতিটি w-এর জন্য ০ থেকে ৯ প্রতিটি অঙ্কের প্রথম নমুনা, একই শুরুর noise থেকে। ছবি ছোট রাখতে প্রতিটি পিক্সেল ১৬টি ধূসর স্তরে (৪ bit) রেখে base64 করে পাতায় বসানো হয়েছে (প্রায় ২১ KB). Classifier-এর সংখ্যাগুলো desktop run থেকে নেওয়া।

import base64
import numpy as np, pandas as pd, matplotlib.pyplot as plt

raw = np.frombuffer(base64.b64decode(open("mnist_cfg.b64").read()), np.uint8)
px = np.stack([raw >> 4, raw & 15], axis=1).reshape(40, 28, 28).astype(int) * 17   # 4 bit → 0..255
ws = [0, 1, 3, 7]
res = pd.DataFrame({"w": ws, "acc": [0.088, 0.721, 0.995, 1.000], "conf": [0.834, 0.909, 0.997, 1.000],
                    "div": [50.0, 43.3, 33.1, 26.9]})       # desktop run: 1,000 samples per w
print(res.to_string(index=False))
print("real test 1000: acc 0.985 | conf 0.984 | div 37.7")
for i, w in enumerate(ws):
    g = px[i * 10:(i + 1) * 10]
    print(f"w={w}: কালির ভাগ {(g > 128).mean():.3f} | গড় উজ্জ্বলতা {g.mean():5.1f}")

fig = plt.figure(figsize=(8, 5.2))
for i in range(40):
    ax = fig.add_subplot(5, 10, i + 1)
    ax.imshow(px[i], cmap="gray", vmin=0, vmax=255); ax.set_xticks([]); ax.set_yticks([])
    if i % 10 == 0: ax.set_ylabel(f"w={ws[i // 10]}", fontsize=8)
ax = fig.add_axes([0.12, 0.05, 0.8, 0.17])
ax.plot(ws, res["acc"], "o-", label="class accuracy")
ax.plot(ws, res["div"] / 50, "s--", label="diversity / 50")
ax.axhline(37.7 / 50, color="gray", lw=0.8, ls=":", label="real test diversity / 50")
ax.set_xlabel("guidance scale w"); ax.legend(fontsize=7, loc="lower right")
fig.suptitle("Latent diffusion on MNIST: same noise, columns = digits 0..9 (seed 123)")
plt.show()

আউটপুট (পাতার Python-এ চালানো; নিচে নমুনা আর একটি ছোট লেখচিত্র আঁকা হয়):

w   acc  conf  div
 0 0.088 0.834 50.0
 1 0.721 0.909 43.3
 3 0.995 0.997 33.1
 7 1.000 1.000 26.9
real test 1000: acc 0.985 | conf 0.984 | div 37.7
w=0: কালির ভাগ 0.159 | গড় উজ্জ্বলতা  40.3
w=1: কালির ভাগ 0.155 | গড় উজ্জ্বলতা  39.5
w=3: কালির ভাগ 0.163 | গড় উজ্জ্বলতা  41.3
w=7: কালির ভাগ 0.176 | গড় উজ্জ্বলতা  44.3

চিত্রের প্রতিটি সারি একটি w, প্রতিটি কলাম একটি অঙ্ক (০ থেকে ৯), আর একই কলামে চারটি ছবি একই শুরুর noise থেকে। w = ০-এর সারিতে চাওয়া অঙ্কের সঙ্গে ছবির প্রায় কোনো সম্পর্ক নেই (classifier দেখেছে ৮, ৮, ৮, ৭, ১, ৭, ৬, ০, ০, ৪)। w = ১-এ দশটির সাতটি ঠিক (০, ১, ২, ৪, ৫, ৬, ৯), কিন্তু “৩” চাইলে এসেছে ৭-এর মতো আকৃতি আর “৭” চাইলে ৯. w = ৩ আর ৭-এ দশটিই ঠিক। একই কলামে noise একই, শুধু শর্তের ঠেলা বাড়ছে। কালির ভাগ ০.১৫৫ থেকে w = ৭-এ ০.১৭৬: রেখা মোটা আর একঘেয়ে হচ্ছে। নিচের লেখচিত্রে accuracy ওঠে আর diversity নামে; ধূসর ডট লাইনটি আসল test ছবির diversity, যার নিচে w = ৩ আর ৭ দুটোই।

৬ · Sampler আর ধাপের সংখ্যা — এবং negative prompt, img2img, inpainting

একটি train করা diffusion model থেকে নমুনা নেওয়ার পথ একটাই নয়। Model শুধু ε অনুমান করে; xT থেকে x0-এ কোন পথে, কত বড় লাফে যাবেন, তা ঠিক করে sampler (diffusers-এ এর নাম scheduler). একই model-এ sampler বদলাতে নতুন training লাগে না। diffusers-এর ডকুমেন্টেশনে (লেখার সময়ে) পরিচিত কয়েকটি:

  • DDIM (DDIMScheduler; Song, Meng আর Ermon, ICLR ২০২১): পর্ব ৭৯-এর নির্ধারিত sampler; ধাপের একটি উপ-সেট বেছে লাফ দেয়।
  • DPM-Solver (DPMSolverMultistepScheduler; Lu ও সহকর্মীরা, NeurIPS ২০২২, আর পরে DPM-Solver++): diffusion-এর ODE-কে একটি বিশেষ উচ্চ-ক্রমের পদ্ধতিতে সমাধান করে; প্রবন্ধগুলো প্রায় ১০ থেকে ২০ ধাপে ভালো নমুনার দাবি করে।
  • Euler (EulerDiscreteScheduler): Karras ও সহকর্মীদের “Elucidating the Design Space of Diffusion-Based Generative Models” (NeurIPS ২০২২)-এর কাঠামো থেকে আসা সরল প্রথম-ক্রমের ODE ধাপ।

ধাপ কমালে সময় প্রায় সমানুপাতে কমে, কারণ প্রতিটি ধাপে U-Net-এর একই খরচ। প্রশ্ন হলো মান কতটা পড়ে। নিজের latent model-এ w = ৩ রেখে DDIM-এর ধাপ বদলে দেখা হলো (একই ১,০০০টি noise, একই classifier; সত্যিকারের run):

~/bazarghar-dl$ uv run python evaluate.py latent steps
w=3.0  steps=10  | class acc 0.994 | conf 0.997 | diversity  31.2 | 0.70 s for 1000 images
w=3.0  steps=25  | class acc 0.995 | conf 0.997 | diversity  32.6 | 1.06 s for 1000 images
w=3.0  steps=50  | class acc 0.995 | conf 0.997 | diversity  33.1 | 2.09 s for 1000 images
w=3.0  steps=100 | class acc 0.995 | conf 0.997 | diversity  33.3 | 4.15 s for 1000 images
peak GPU memory 310 MiB

১০ থেকে ১০০ ধাপে সময় ০.৭০ থেকে ৪.১৫ সেকেন্ড, প্রায় রৈখিক; অথচ class accuracy ০.৯৯৪ থেকে ০.৯৯৫, প্রায় অপরিবর্তিত। Diversity সামান্য বাড়ে (৩১.২ → ৩৩.৩): খুব কম ধাপে ছবিগুলো একটু বেশি মসৃণ আর একরকম। এই সহজ ডেটায় ১০ ধাপই যথেষ্ট; বড় রঙিন ছবিতে তা নয় (অংশ ৭)। ধাপ আর sampler নিজের মাপকাঠিতে মাপুন।

একই কাঠামোয় আরও কয়েকটি পরিচিত কৌশল আসে, সবই sampling-এর পথ বদলে, model বদলে নয়:

  • Negative prompt: CFG-এর শর্তহীন শাখার খালি prompt-এর বদলে একটি অবাঞ্ছিত বর্ণনা (যেমন “blurry, watermark”) বসানো; নমুনা সেদিক থেকে দূরে ঠেলা খায়। বিস্তারিত ভাবনার প্রশ্ন ৩-এ।
  • img2img: শুদ্ধ noise থেকে নয়, একটি বিদ্যমান ছবির latent-এ কিছুটা noise যোগ করে মাঝের একটি ধাপ থেকে denoise শুরু করা (SDEdit, Meng ও সহকর্মীরা, ICLR ২০২২-এর ধারণা)। diffusers-এ strength ঠিক করে কতটা noise: কম হলে মূল ছবি বেশি টিকে থাকে।
  • Inpainting: একটি mask দেওয়া অংশ নতুন করে বানানো, বাকিটা রেখে। বিশেষ inpainting checkpoint mask আর ঢাকা ছবির latent বাড়তি input হিসেবে নেয়।
  • LoRA: পর্ব ৫৫-এর LoRA diffusion-এর U-Net-এর attention layer-এ বসিয়ে কয়েকটি ছবি থেকে নতুন ধরন বা বস্তু শেখানো যায়; diffusers-এর pipeline-এ load_lora_weights আছে।

৭ · Stable Diffusion-এর ভেতরে — একটি সত্যিকারের text-to-image call

এবার একটি আসল, বড় latent diffusion model. বেছে নেওয়া হয়েছে Hugging Face-এর stable-diffusion-v1-5/stable-diffusion-v1-5 (revision 451f4fe), কারণ লেখার সময়ে (নভেম্বর ২০২৬; পরিবর্তন হতে পারে) এটি token ছাড়া ডাউনলোড করা যায় (gated নয়), তার লাইসেন্স CreativeML OpenRAIL-M (ব্যবহারের নিষেধাজ্ঞাসহ একটি খোলা লাইসেন্স), আর fp16 রূপে তিনটি অংশ মিলে প্রায় ২.১ GB: U-Net ১.৭২ GB, text encoder ২৪৬ MB, VAE ১৬৭ MB. Model card জানায়: এটি একটি latent diffusion model, text encoder স্থির CLIP ViT-L/14, autoencoder-এর f = ৮ আর latent-এ ৪ channel, LAION-এর ইংরেজি ক্যাপশনের ছবিতে train করা, আর ৫১২ × ৫১২ মাপে চূড়ান্ত প্রশিক্ষণ। Card একে “intended for research purposes only” বলে। তাই prompt শুধু বাজারঘরের তিনটি সাধারণ পণ্যের: একটি লাল সিরামিক মগ, একটি পাটের ব্যাগ আর এক জোড়া চামড়ার স্যান্ডেল; কোনো মানুষ, লোগো বা ব্র্যান্ড নয়। ডাউনলোড ছোট রাখতে safety checker নামানো হয়নি; তার বদলে প্রতিটি ছবি চোখে দেখে যাচাই করা হয়েছে।

প্রথমে একটি call হাতে ভাঙা হলো: tokenizer, text encoder, U-Net-এর প্রথম cross-attention layer-এ একটি hook দিয়ে query আর key-এর আকার, sampler, আর decoder. সত্যিকারের run (ডেস্কটপ, GPU-তে, fp16; progress bar আর library-র সতর্কবার্তা বাদ দিয়ে):

~/bazarghar-dl$ uv run python sd_run.py
torch 2.15.0+cu132 | diffusers 0.42.1 | transformers 5.22.2
params: unet 859.5M | text_encoder 123.1M | vae 83.7M
vae scaling_factor 0.18215 | latent_channels 4 | unet sample_size 64 | cross_attention_dim 768
default scheduler: PNDMScheduler
[1] tokenizer: input_ids (1, 77) | real tokens incl. start/end 13 | first ids [49406, 320, 736, 15112, 9722, 525, 320, 9057, 2175, 267, 4306, 1125, 49407]
    tokens: ['<|startoftext|>', 'a</w>', 'red</w>', 'ceramic</w>', 'mug</w>', 'on</w>', 'a</w>', 'wooden</w>', 'table</w>', ',</w>', 'product</w>', 'photo</w>', '<|endoftext|>']
[2] text encoder: last_hidden_state (1, 77, 768) torch.float16
[3] cross-attention (first down block): query in (2, 4096, 320) -> (2, 4096, 320) | key in (2, 77, 768) -> (2, 77, 320)
[4] sampler: 30 steps x 2 (CFG batch) U-Net calls -> latents (1, 4, 64, 64) | 3.91 s
[5] VAE decoder: (1, 4, 64, 64) -> image (1, 3, 512, 512) | 0.15 s
    peak GPU memory 2667 MiB
prompt: "a red ceramic mug on a wooden table, product photo" · 30 ধাপ, w = 7.5, seed 7
ধাপ
আকার
মাপা
  1. Prompt. ইংরেজিতে একটি সাধারণ পণ্যের বর্ণনা; model card বলে model প্রধানত ইংরেজি ক্যাপশনে train করা।
  2. Tokenizer. CLIP-এর BPE tokenizer বাক্যটিকে ১৩টি token-এ ভাঙে (শুরু ও শেষের চিহ্নসহ), তারপর padding দিয়ে ঠিক ৭৭ দৈর্ঘ্যে আনে। ৭৭-এর বেশি হলে বাকিটা কেটে যায়।
  3. Text encoder. ৭৭টি token-এর প্রতিটির জন্য একটি ৭৬৮-মাত্রার vector. CFG-এর জন্য খালি prompt-ও encode হয়, তাই পরের ধাপগুলোতে batch ২.
  4. শুরুর noise. পিক্সেল নয়, latent-এর মাপে: ৪ × ৬৪ × ৬৪ = ১৬,৩৮৪টি Gaussian সংখ্যা। একই seed দিলে একই ছবি।
  5. Cross-attention. প্রথম block-এ ৬৪ × ৬৪ = ৪,০৯৬টি অবস্থানের প্রতিটি থেকে ৩২০-মাত্রার query; text-এর ৭৬৮-মাত্রার vector থেকে to_k ৩২০-মাত্রার key বানায়। ছবির প্রতিটি অবস্থান ৭৭টি token-এর ওপর attention নেয়।
  6. Sampler loop. ৩০ ধাপ (ডিফল্ট PNDM scheduler-এ diffusers-এর source অনুযায়ী আসলে ৩১টি timestep), প্রতি ধাপে শর্তসহ আর শর্তহীন দুটি input একই batch-এ: মোট প্রায় ৬২টি U-Net চালনা, ৩.৯১ সেকেন্ড। প্রতি ধাপে ε̃ = εu + ৭.৫·(εc − εu).
  7. VAE decoder. শেষ latent-কে ০.১৮২১৫ দিয়ে ভাগ করে decoder ৫১২ × ৫১২ × ৩ ছবি বানায়, মাত্র ০.১৫ সেকেন্ডে। পুরো call-এ সর্বোচ্চ GPU মেমরি ২,৬৬৭ MiB.

এরপর একই seed (৭) রেখে guidance scale আর sampler বদলানো হলো (একই run-এর বাকি অংশ, সংক্ষেপিত: peak মেমরির কলাম এক লাইনে আনা; prompt 0 মগ, 1 পাটের ব্যাগ, 2 স্যান্ডেল, আর শেষ সারিতে স্যান্ডেলের সঙ্গে negative prompt):

… (warm-up call omitted)
prompt 0 | pndm  | steps  30 | guidance  1.0 |  2.32 s | p0_pndm_s30_g1.0
prompt 0 | pndm  | steps  30 | guidance  3.0 |  3.93 s | p0_pndm_s30_g3.0
prompt 0 | pndm  | steps  30 | guidance  7.5 |  3.91 s | p0_pndm_s30_g7.5
prompt 0 | pndm  | steps  30 | guidance 12.0 |  3.91 s | p0_pndm_s30_g12.0
prompt 0 | ddim  | steps  10 | guidance  7.5 |  1.38 s | p0_ddim_s10_g7.5
prompt 0 | ddim  | steps  25 | guidance  7.5 |  3.20 s | p0_ddim_s25_g7.5
prompt 0 | ddim  | steps  50 | guidance  7.5 |  6.27 s | p0_ddim_s50_g7.5
prompt 0 | dpm   | steps  10 | guidance  7.5 |  1.39 s | p0_dpm_s10_g7.5
prompt 0 | dpm   | steps  25 | guidance  7.5 |  3.19 s | p0_dpm_s25_g7.5
prompt 0 | dpm   | steps  50 | guidance  7.5 |  6.25 s | p0_dpm_s50_g7.5
prompt 0 | euler | steps  10 | guidance  7.5 |  1.37 s | p0_euler_s10_g7.5
prompt 0 | euler | steps  25 | guidance  7.5 |  3.21 s | p0_euler_s25_g7.5
prompt 0 | euler | steps  50 | guidance  7.5 |  6.23 s | p0_euler_s50_g7.5
prompt 1 | dpm   | steps  25 | guidance  7.5 |  3.19 s | p1_dpm_s25_g7.5
prompt 2 | dpm   | steps  25 | guidance  7.5 |  3.19 s | p2_dpm_s25_g7.5
prompt 2 | dpm   | steps  25 | guidance  7.5 |  3.19 s | p2_dpm_neg
(every run: peak 2669 MiB)

একটি seed, একটি run; সাধারণ নিয়ম নয়। সময়: guidance ১.০-এ ৩০ ধাপ ২.৩২ সেকেন্ড, কিন্তু ৩, ৭.৫ বা ১২-এ প্রায় ৩.৯ সেকেন্ড: কারণ guidance_scale ১-এর বেশি হলেই শর্তহীন শাখা চলে। তিন sampler-এর সময় শুধু ধাপের সংখ্যায় বাঁধা। Guidance-এর ছবি: w = ১-এ কোনো লাল মগ নেই: ওপর থেকে দেখা মাটির ওপর একটি হালকা রঙের কাপ, ভেতরে লাল কিছু, চারপাশে সবুজ পাতা; prompt-এর সঙ্গে মিল আলগা। w = ৩-এ কাঠের টেবিলে একটি লাল মগ, রং একটু নরম। w = ৭.৫-এ চকচকে উজ্জ্বল লাল মগ, কাঠের দানা স্পষ্ট। w = ১২-এ লাল আরও গাঢ় আর পটভূমি প্রায় সমতল গাঢ় বাদামি, কাঠের দানা প্রায় হারিয়ে গেছে: অতি-সম্পৃক্ত ছবির শুরু। Sampler: ১০ ধাপে DDIM-এর মগ কাত হয়ে ফ্রেমের বাইরে আংশিক কাটা আর ঝাপসা, Euler-এর পটভূমি ঝাপসা, কিন্তু DPM-Solver ১০ ধাপেই ২৫ বা ৫০ ধাপের প্রায় একই রকম পরিষ্কার মগ দিয়েছে; পাটের ব্যাগ আর স্যান্ডেলের ছবি বিশ্বাসযোগ্য। স্যান্ডেলে “blurry, text, watermark” negative prompt যোগ করলে একই seed-এ স্যান্ডেলের নকশা আর রাখার ভঙ্গি বদলে গেছে, যদিও আগের ছবিতে লেখা বা watermark ছিল না। CLIP score মাপা হয়নি (আরেকটি বড় model নামাতে হতো); সেটি পর্ব ৮১-এর বিষয়।

নিচের ব্লক এই run-এর পাঁচটি ছবির খুব ছোট রূপ (২৮ × ২৮ পিক্সেল, প্রতিটি রং ৪ bit) আঁকে, আর সময়ের টেবিল থেকে ধাপপ্রতি সময় হিসাব করে। এত ছোট ছবিতে বিশদ দেখা যায় না, শুধু রং আর গঠনের মোটা ধারণা।

import base64
import numpy as np, pandas as pd, matplotlib.pyplot as plt

raw = np.frombuffer(base64.b64decode(open("sd_thumbs.b64").read()), np.uint8)
px = np.stack([raw >> 4, raw & 15], axis=1).reshape(5, 28, 28, 3).astype(int) * 17
names = ["mug w=1", "mug w=3", "mug w=7.5", "mug w=12", "jute bag w=7.5"]
for n, im in zip(names, px):
    sat = (im.max(axis=2) - im.min(axis=2)).mean()        # রঙের তীব্রতা
    rgb = im.mean(axis=(0, 1)).round().astype(int)
    print(f"{n:15s}: গড় RGB {rgb} | saturation {sat:5.1f}")

# desktop GPU run: 512x512, guidance 7.5, seconds per image
t = pd.DataFrame({"sampler": ["ddim"] * 3 + ["dpm"] * 3 + ["euler"] * 3,
                  "steps": [10, 25, 50] * 3,
                  "sec": [1.38, 3.20, 6.27, 1.39, 3.19, 6.25, 1.37, 3.21, 6.23]})
t["ms/step"] = (t["sec"] / t["steps"] * 1000).round(0)
print(t.pivot(index="steps", columns="sampler", values="sec"))
print("গড় ms/ধাপ:", t.groupby("sampler")["ms/step"].mean().round(0).to_dict())

fig = plt.figure(figsize=(8, 4.2))
for i in range(5):
    ax = fig.add_subplot(2, 5, i + 1); ax.imshow(px[i].astype(np.uint8))
    ax.set_title(names[i], fontsize=8); ax.axis("off")
ax = fig.add_subplot(2, 1, 2)
for s, g in t.groupby("sampler"):
    ax.plot(g["steps"], g["sec"], "o-", label=s)
ax.set_xlabel("sampling steps"); ax.set_ylabel("seconds per image"); ax.legend(fontsize=8)
fig.suptitle("Stable Diffusion v1.5, real run (seed 7), 28x28 thumbnails")
plt.show()

আউটপুট (পাতার Python-এ চালানো; নিচে পাঁচটি ছোট ছবি আর একটি লেখচিত্র আঁকা হয়):

mug w=1        : গড় RGB [107 113 103] | saturation  15.8
mug w=3        : গড় RGB [119 101 101] | saturation  27.6
mug w=7.5      : গড় RGB [121  91  93] | saturation  31.7
mug w=12       : গড় RGB [129  90  86] | saturation  44.6
jute bag w=7.5 : গড় RGB [153 143 133] | saturation  22.5
sampler  ddim   dpm  euler
steps                     
10       1.38  1.39   1.37
25       3.20  3.19   3.21
50       6.27  6.25   6.23
গড় ms/ধাপ: {'ddim': 130.0, 'dpm': 131.0, 'euler': 130.0}

প্রথম পাঁচ সারি ছোট ছবিগুলোর রঙের মাপ। “saturation” এখানে প্রতিটি পিক্সেলে সবচেয়ে বড় আর সবচেয়ে ছোট রঙের মানের পার্থক্যের গড়: ধূসর ছবিতে ০-এর কাছে, তীব্র রঙে বড়। মগের চারটি ছবিতে w বাড়ার সঙ্গে এটি ১৫.৮ → ২৭.৬ → ৩১.৭ → ৪৪.৬ বাড়ে, আর গড় সবুজ ১১৩ থেকে ৯০-এ নামে: চোখে দেখা “লাল আরও গাঢ়, ছবি অতি-সম্পৃক্ত” কথাটি সংখ্যাতেও আসে। এটি একটি seed-এর একটি prompt, তাই প্রবণতা দেখায়, প্রমাণ নয়। টেবিলে তিন sampler-এর সময় প্রায় হুবহু এক, আর ধাপপ্রতি গড় প্রায় ১৩০ ms: sampler বদলালে ধাপপ্রতি খরচ বদলায় না, বদলায় কত কম ধাপে ভালো ছবি আসে। লেখচিত্রে তিনটি রেখা প্রায় একে অপরের ওপর পড়ে আর সরলরেখা।

আরও গভীরে: মূল প্রবন্ধ Rombach ও সহকর্মীদের “High-Resolution Image Synthesis with Latent Diffusion Models” (CVPR ২০২২); সেখানে autoencoder-এর loss আর বিভিন্ন f-এর তুলনা আছে। Guidance-এর জন্য Dhariwal আর Nichol (NeurIPS ২০২১) আর Ho আর Salimans-এর “Classifier-Free Diffusion Guidance” (arXiv ২০২২)। দ্রুত sampler-এর জন্য DPM-Solver (NeurIPS ২০২২) আর Karras ও সহকর্মীদের EDM প্রবন্ধ (NeurIPS ২০২২)। পরের প্রজন্মে U-Net-এর জায়গায় transformer (DiT, ICCV ২০২৩) আর rectified flow (Stable Diffusion 3, ২০২৪) এসেছে; latent আর CFG সেখানেও আছে।

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

  • Latent diffusion কেন সস্তা? f = ৮, ৪ channel-এ ৫১২ × ৫১২ × ৩ → ৬৪ × ৬৪ × ৪: সংখ্যায় ৪৮ গুণ, conv-এ ৬৪ গুণ, attention-এ ৪,০৯৬ গুণ কম, প্রতিটি ধাপে।
  • CFG-এর সূত্র? ε̃ = εu + w·(εc − εu); training-এ প্রায় ১০% শর্ত মুছে একই network দুই অনুমান শেখে; প্রতি ধাপে দুটি call.
  • Guidance বাড়ালে? শর্তের সঙ্গে মিল বাড়ে, বৈচিত্র্য কমে, খুব বড় w-এ অতি-সম্পৃক্ত ছবি।
  • Text কোথায় ঢোকে? U-Net-এর cross-attention-এ: Q ছবির feature থেকে, K আর V text encoder-এর output থেকে।
  • Scaling factor কী? Latent-এর variance প্রায় ১ করার ধ্রুবক (SD v1-এ ০.১৮২১৫); encode-এর পরে গুণ, decode-এর আগে ভাগ।

৮ · AI-এর যুগে — AI-এর লেখা diffusion কোড যাচাই

“diffusers দিয়ে একটি text-to-image script লিখে দাও” বললে AI assistant কয়েক সেকেন্ডে কোড দেয়। এই ক্ষেত্রের ভুলগুলো প্রায় সবই চুপচাপ: ছবি আসে, কিন্তু রং ভুল, guidance উল্টো, বা লাইসেন্স না পড়েই model ব্যবহার। নিচের জায়গাগুলো আলাদা করে দেখুন।

AI প্রায়ই যা করেফলকীভাবে ধরবেন
পুরোনো বা বানানো argument (যেমন পুরোনো version-এর নাম, অস্তিত্বহীন scheduler)Error, বা চুপচাপ উপেক্ষিত argumentনিজের version-এর diffusers ডকুমেন্টেশন; pipe.__call__-এর signature ছাপিয়ে দেখা
নিজের latent decode-এর আগে scaling_factor দিয়ে ভাগ ভুলে যাওয়াফ্যাকাশে বা দাগযুক্ত ছবিvae.config.scaling_factor ছাপানো; একটি আসল ছবি encode-decode করে ফেরত আসে কি না দেখা
CFG-এর সূত্রে দুই শাখা উল্টে ফেলা, বা chunk-এর ক্রম cat-এর সঙ্গে না মেলাw বাড়ালে prompt থেকে দূরে সরেw = ০ আর ১-এ ফল শর্তহীন আর শর্তসহ অনুমানের সঙ্গে হুবহু মেলে কি না (অনুশীলন ৪)
Sampling-এর সময় torch.no_grad() বা model.eval() বাদমেমরি ফুরিয়ে যায়, GroupNorm/Dropout-এর আচরণ বদলায়Peak memory মাপা; একই seed-এ দুবার চালিয়ে ফল মেলে কি না
“সেরা” guidance বা ধাপের সংখ্যা একটি সুন্দর ছবি দেখিয়ে দাবিভাগ্যবান seed-কে নিয়ম ভাবাকয়েকটি seed, কয়েকটি w, একটি বাইরের মাপকাঠি (classifier, পর্ব ৮১-এর metric)
লাইসেন্স না দেখে যেকোনো checkpoint, বা gated model-এর জন্য token চাওয়াআইনি ঝুঁকি, গোপন তথ্য ফাঁসModel card আর লাইসেন্স নিজে পড়া; token কখনো কোডে বা চ্যাটে না রাখা

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

আমি diffusers দিয়ে একটি Stable Diffusion 1.x-ধরনের pipeline চালাচ্ছি (fp16, GPU). আমার diffusers-এর version: [এখানে লিখুন]। ১. model card-এ লাইসেন্স কোথায় দেখব আর কোন শর্তগুলো পড়া জরুরি, বলো; লাইসেন্স সম্পর্কে নিশ্চিত না হলে তা স্পষ্ট বলো। ২. একটি pipeline call-কে হাতে ভেঙে দেখাও: tokenizer, text encoder, latent noise, CFG-সহ sampler loop, scaling factor দিয়ে ভাগ, VAE decode; প্রতিটি ধাপে tensor-এর shape ছাপাও। ৩. guidance_scale ১, ৩, ৭.৫-এ একই seed-এ ছবি বানানোর আর সময় ও peak memory মাপার কোড দাও। ৪. যে argument বা function তোমার নিশ্চিত জানা নেই, তা চিহ্নিত করো; আমি নিজের version-এর docs-এ মেলাব। ৫. কোনো মানুষ, লোগো বা ব্র্যান্ডের ছবি বানানোর উদাহরণ দেবে না; prompt শুধু সাধারণ পণ্যের।

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

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

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

প্র ০১Latent space-এ diffusion চালালে খরচ বহু গুণ কমে। তাহলে এর দাম কী, আর কোথায় সেই দাম চোখে পড়ে?

দামটি দেওয়া হয় autoencoder-এ। Diffusion যত নিখুঁতই হোক, শেষ ছবি decoder যা ফেরত আনতে পারে তার চেয়ে ভালো হতে পারে না। Stable Diffusion v1-এর model card নিজেই সীমাবদ্ধতার তালিকায় লেখে যে autoencoding অংশটি lossy, অর্থাৎ কিছু তথ্য হারায়। কোথায় হারায়, তা বোঝা সহজ: f = ৮ মানে ৮ × ৮ = ৬৪টি পিক্সেলের একটি টুকরো (প্রতিটিতে ৩টি রং, মোট ১৯২টি সংখ্যা) মাত্র ৪টি latent সংখ্যায় চাপা হয়। বড়, মসৃণ গঠন এতে টিকে থাকে, কিন্তু খুব ছোট আর তীক্ষ্ণ খুঁটিনাটি, যেমন পণ্যের লেবেলে ছোট লেখা, দূরের মুখ, আঙুল বা সূক্ষ্ম নকশা, প্রায়ই ঘোলা বা বিকৃত হয়। Model card পাঠযোগ্য লেখা বানাতে না পারাকেও সীমাবদ্ধতা বলে; autoencoder তার একটি কারণ হতে পারে।

Rombach ও সহকর্মীরা প্রবন্ধে বিভিন্ন f (১ থেকে ৩২) নিয়ে পরীক্ষা করে দেখান যে খুব ছোট f-এ (যেমন ১ বা ২) training ধীর, আর খুব বড় f-এ (৩২) তথ্য এত হারায় যে মান পড়ে যায়; মাঝামাঝি f = ৪ থেকে ১৬ ভালো ভারসাম্য দিয়েছিল বলে তাঁরা রিপোর্ট করেন। পরের প্রজন্মের কিছু model latent-এ বেশি channel নিয়েছে বলে রিপোর্ট আছে (যেমন Stable Diffusion 3-এর প্রবন্ধে ১৬ channel).

দ্বিতীয় দাম হলো জটিলতা। এখন দুটি model, দুটি training, আর তাদের মধ্যে একটি চুক্তি (scaling factor, latent-এর আকার)। Autoencoder বদলালে পুরো diffusion model আবার train করতে হয়, কারণ latent-এর “ভাষা” বদলে যায়।

মূল উপলব্ধি: Latent diffusion খরচ কমায় ছবির খুঁটিনাটি একটি আলাদা autoencoder-কে দিয়ে; শেষ মানের সীমা তাই decoder ঠিক করে, আর ছোট লেখা বা সূক্ষ্ম বিশদে সেই সীমা সবচেয়ে আগে চোখে পড়ে।

প্র ০২Guidance scale বাড়ালে prompt-এর সঙ্গে মিল বাড়ে। তাহলে সবসময় বড় w নিলেই হয় না কেন?

কারণ guidance বিনা মূল্যে আসে না; সে বৈচিত্র্য আর স্বাভাবিকতা কিনে নেয়। Ho আর Salimans-এর মূল CFG প্রবন্ধ ImageNet-এ guidance-এর মাত্রা বদলে দেখায় যে Inception Score (মান বা “স্পষ্টতা”) বাড়তে থাকে, কিন্তু FID (আসল ছবির বণ্টনের সঙ্গে মিল, যাতে বৈচিত্র্যও ধরা পড়ে) একটি ছোট guidance-এ সবচেয়ে ভালো হয়ে তারপর খারাপ হতে থাকে। এটি GAN-এর truncation trick-এর মতো আপস: প্রতিটি ছবি স্পষ্ট হয়, কিন্তু সব ছবি একরকম।

এই পর্বের দুই পরীক্ষাতেই এটি সংখ্যায় দেখা গেছে। ২-মাত্রার খেলনায় w = ৭-এ নমুনার গড় আসল কেন্দ্র ১.০ ছাড়িয়ে ২.৬৩-এ চলে গেছে আর ছড়ানো ০.৮ থেকে ০.৫৮-এ নেমেছে। MNIST-এর latent model-এ w = ৭-এ classifier-এর accuracy ১.০০০, কিন্তু শ্রেণির ভেতরের বৈচিত্র্য ২৬.৯, আসল test ছবির ৩৭.৭-এর অনেক নিচে; ছবিতে রেখাগুলো মোটা আর একঘেয়ে। বড় model-এ খুব বড় w-এ রং অতি-সম্পৃক্ত আর ছবি “পোড়া” দেখায়, কারণ x̂0 প্রশিক্ষণের পরিসরের বাইরে যায়।

এর সমাধানে কয়েকটি কৌশল প্রস্তাবিত হয়েছে। Imagen-এর প্রবন্ধ (Saharia ও সহকর্মীরা, ২০২২) “dynamic thresholding” প্রস্তাব করে, যা প্রতি ধাপে x̂0-এর চরম মানগুলো কেটে আনে। Lin ও সহকর্মীদের “Common Diffusion Noise Schedules and Sample Steps are Flawed” (২০২৩) guidance-এর পরে ε̃-এর মাপ আবার আগের মাপে আনার একটি “guidance rescale” প্রস্তাব করে; diffusers-এ এটি guidance_rescale নামে আছে। ব্যবহারিক পথ হলো কয়েকটি w-এ একই seed চালিয়ে চোখ আর metric দুটো দিয়েই দেখা।

মূল উপলব্ধি: Guidance scale হলো মিল আর বৈচিত্র্যের মধ্যে একটি নব; বড় w প্রতিটি ছবিকে prompt-এর কাছে আনে কিন্তু বণ্টনকে সরু আর অস্বাভাবিক করে, তাই সঠিক মান মেপে বের করতে হয়।

প্র ০৩Negative prompt আসলে কী করে? এটি কি একটি নিরাপত্তা ফিল্টার?

Negative prompt-এর গণিত CFG-এর সূত্রেই লুকানো। সাধারণ CFG-এ শর্তহীন অনুমান εθ(xt, ∅) আসে একটি খালি prompt (“”)-এর encoding থেকে। Negative prompt দিলে সেই খালি prompt-এর জায়গায় বসে negative text-এর encoding: ε̃ = εneg + w·(εc − εneg). তাই নমুনা শুধু prompt-এর দিকে নয়, negative prompt থেকে দূরেও ঠেলা খায়। diffusers-এর ডকুমেন্টেশনও বলে negative prompt উপেক্ষিত হয় যখন guidance ব্যবহার হয় না (guidance_scale < 1), যা এই ব্যাখ্যার সঙ্গে মেলে: guidance না থাকলে শর্তহীন শাখাই নেই।

এ কারণে negative prompt-এর প্রভাব অনুমান করা কঠিন। এটি “এই জিনিস থাকবে না” এমন কোনো নিয়ম নয়; এটি শুধু একটি দিক, যার দিকে ঠেলা কমে। “blurry, text, watermark” লিখলে ছবি কিছুটা কম ঝাপসা হতে পারে, কিন্তু নিশ্চয়তা নেই, আর অন্য কিছুও বদলে যায়। অংশ ৭-এর run-এ একই seed-এ negative prompt যোগ করলে ছবির গঠন বদলেছে, শুধু অবাঞ্ছিত অংশ নয়।

তাই negative prompt কোনোভাবেই নিরাপত্তা ব্যবস্থা নয়। ক্ষতিকর বা অনুপযুক্ত ছবি আটকানোর জন্য ব্যবস্থা লাগে আলাদা স্তরে: prompt-এর নীতি-যাচাই, output-এর ওপর classifier (যেমন diffusers-এর safety checker, যার সঙ্গে ব্যবহারের কথা Stable Diffusion-এর model card বলে), মানুষের পর্যালোচনা, আর provenance বা watermark (পর্ব ৮১-এ)। এই পর্বের সত্যিকারের run-এ safety checker বন্ধ রাখা হয়েছিল ডাউনলোডের আকার কমাতে, আর তার ক্ষতিপূরণ হিসেবে prompt শুধু নিরীহ পণ্যের ছবিতে সীমিত রাখা হয়েছিল এবং প্রতিটি ছবি চোখে দেখা হয়েছিল; বাস্তব কোনো সেবায় এমন শর্টকাট চলে না।

মূল উপলব্ধি: Negative prompt হলো CFG-এর শর্তহীন শাখার জায়গায় একটি অবাঞ্ছিত prompt বসানো; এটি একটি নরম দিক-নির্দেশ, কোনো নিয়ম বা নিরাপত্তা ফিল্টার নয়।

প্র ০৪Text-to-image model-এ text encoder কতটা গুরুত্বপূর্ণ? বাংলা prompt-এ ফল খারাপ হয় কেন?

Text encoder হলো model-এর “কান”: prompt-এর যা সে আলাদা করে ধরতে পারে না, U-Net তা কখনো জানবে না। Google-এর Imagen প্রবন্ধ (Saharia ও সহকর্মীরা, ২০২২) রিপোর্ট করে যে শুধু text নিয়ে pretrain করা বড় একটি language model (T5-XXL)-এর encoder ব্যবহার করলে, আর সেই text encoder-এর আকার বাড়ালে, ছবি আর লেখার মিল যতটা বাড়ে, image diffusion model-এর আকার বাড়ালে ততটা বাড়ে না। Stable Diffusion v1 নেয় CLIP-এর text অংশ, যা ছবি-ক্যাপশনের জোড়া থেকে contrastive পদ্ধতিতে শেখা; এটি বস্তু আর ধরন ধরে ভালো, কিন্তু গণনা (“তিনটি মগ”), বাম-ডান অবস্থান আর জটিল সম্পর্কে দুর্বল বলে প্রায়ই দেখা যায়। Model card নিজেও “compositionality” কঠিন বলে সীমাবদ্ধতার তালিকায় রাখে।

CLIP-এর tokenizer-এর আরেকটি সীমা: সর্বোচ্চ ৭৭টি token (শুরু আর শেষের চিহ্নসহ)। এর বেশি লিখলে বাকিটা কেটে যায়; অংশ ৭-এর run-এ tokenizer ঠিক ৭৭ দৈর্ঘ্যের সারি দেয়।

বাংলার ক্ষেত্রে সমস্যা দুই স্তরে। প্রথম, ডেটা: model card বলে training ডেটা প্রধানত ইংরেজি ক্যাপশনের (LAION-2B(en)), আর model ইংরেজি ছাড়া অন্য ভাষার prompt-এ খারাপ করে বলে সীমাবদ্ধতার তালিকায় লেখে। দ্বিতীয়, tokenizer: ইংরেজি-কেন্দ্রিক BPE vocabulary-তে বাংলা লেখা প্রায়ই অনেক ছোট ছোট টুকরোয় ভাঙে (পর্ব ৪০-এ token fertility দেখেছেন), তাই একই বাক্য অনেক বেশি token খায় আর প্রতিটি টুকরোর অর্থ দুর্বল। ব্যবহারিক সমাধান হলো prompt ইংরেজিতে অনুবাদ করে দেওয়া, বা বহুভাষিক text encoder-ওয়ালা model খোঁজা; কোনো model বাংলায় ভালো দাবি করলে নিজের কয়েকটি prompt-এ মেপে দেখুন।

মূল উপলব্ধি: Text-to-image-এ prompt বোঝার সীমা ঠিক করে text encoder আর তার tokenizer; ইংরেজি-কেন্দ্রিক ডেটা আর ৭৭ token-এর সীমা বাংলা ও দীর্ঘ prompt-এ ফল খারাপ করে।

প্র ০৫একটি open-weight text-to-image model দিয়ে বাজারঘরের পণ্যের ছবি বানাতে চাইলে লাইসেন্স, ডেটা আর দায়িত্বের দিক থেকে কী কী ভাববেন?

প্রথমে লাইসেন্স পড়ুন, model card-এর সারাংশ নয়। Stable Diffusion v1.5-এর লাইসেন্স CreativeML OpenRAIL-M: ওজন খোলা, বাণিজ্যিক ব্যবহারও নিষিদ্ধ নয়, কিন্তু লাইসেন্সে কিছু ব্যবহারের ওপর নিষেধাজ্ঞা (use restrictions) আছে, যেমন আইন ভঙ্গ, শিশুদের ক্ষতি, কারও ক্ষতির উদ্দেশ্যে মিথ্যা তথ্য ছড়ানো বা কাউকে হয়রানি; আর এই শর্তগুলো পরের ব্যবহারকারীদের ওপরও চাপাতে হয়। একই সঙ্গে model card নিজেই model-কে “intended for research purposes only” বলে বর্ণনা করে। এই দুই কথার টানাপোড়েন নিয়ে আইনি পরামর্শ ছাড়া কোনো সিদ্ধান্ত নেবেন না। আর কিছু model অ-বাণিজ্যিক লাইসেন্সে আসে বা ডাউনলোডের আগে শর্ত মানতে বলে (gated).

দ্বিতীয়, ডেটা আর কপিরাইট। Model card জানায় training ডেটা LAION-এর ইন্টারনেট থেকে সংগৃহীত ছবি-ক্যাপশন, যাতে কপিরাইটযুক্ত ছবি আর প্রাপ্তবয়স্ক সামগ্রীও ছিল, আর ডেটায় বারবার থাকা ছবি model কিছুটা মুখস্থ করতে পারে বলেও লেখে। Carlini ও সহকর্মীদের “Extracting Training Data from Diffusion Models” (USENIX Security ২০২৩) দেখিয়েছিল যে কিছু ক্ষেত্রে প্রশিক্ষণের ছবির প্রায় হুবহু নকল বের করা যায়, বিশেষত বহুবার থাকা ছবির। তাই ব্র্যান্ড, লোগো বা নির্দিষ্ট শিল্পীর নামে prompt দেবেন না।

তৃতীয়, গ্রাহকের প্রতি সততা। AI-তে বানানো পাটের ব্যাগের ছবি আসল পণ্যের চেয়ে ভালো দেখালে সেটি বিভ্রান্তিকর বিজ্ঞাপন। ব্যবহার করলে বিজ্ঞাপন বা ব্যানারের ধারণা-চিত্রে সীমিত রাখুন, আসল পণ্যের জায়গায় নয়, আর স্পষ্ট লিখুন ছবিটি AI দিয়ে তৈরি। মানুষের ছবি, বিশেষ করে বাস্তব কোনো মানুষের মুখ, কখনোই বানাবেন না। বাংলাদেশের প্রাসঙ্গিক আইন এখানে যাচাই করা হয়নি; ব্যবসায় ব্যবহারের আগে আইনজ্ঞের পরামর্শ নিন। Watermark আর provenance-এর মতো প্রযুক্তিগত দিক পর্ব ৮১ (Generative model-এর মূল্যায়ন আর নিরাপত্তা — FID, CLIP score, deepfake আর watermark)-এর বিষয়।

মূল উপলব্ধি: Open weight মানে দায়মুক্তি নয়; লাইসেন্সের ব্যবহার-নিষেধ, প্রশিক্ষণ ডেটার কপিরাইট আর মুখস্থ করার ঝুঁকি, আর গ্রাহকের প্রতি সততা, তিনটিই আগে যাচাই করে তারপর ব্যবহার।

অনুশীলন

প্রথমে খাতায় করুন, তারপর উত্তর মেলান।

  1. পূর্ণ numerical: একটি latent diffusion model-এর autoencoder-এ f = ৮ আর latent channel ৪. (ক) ৭৬৮ × ৭৬৮ × ৩ ছবির latent-এর আকার কত, আর সংখ্যায় কত গুণ ছোট? অবস্থানের সংখ্যা কত গুণ কমে, আর self-attention-এর খরচ মোটামুটি কত গুণ? (খ) একটি ধাপে দুটি latent সংখ্যার শর্তহীন অনুমান εu = (০.৪, −০.১) আর শর্তসহ εc = (০.১, ০.৩)। w = ৫-এ CFG-এর ε̃ কত? (গ) ৩০ ধাপের sampling-এ w = ৫ হলে U-Net কতবার চলে, আর w = ১ হলে? (ঘ) ৬৪০ × ৪৪৮ মাপের ছবি চাইলে sampler কোন আকারের noise দিয়ে শুরু করবে?

    (ক) ৭৬৮/৮ = ৯৬, তাই latent ৯৬ × ৯৬ × ৪ = ৩৬,৮৬৪টি সংখ্যা; ছবিতে ৭৬৮ × ৭৬৮ × ৩ = ১৭,৬৯,৪৭২, অনুপাত ৪৮ গুণ (f² × ৩/৪ = ৬৪ × ০.৭৫)। অবস্থান ৫,৮৯,৮২৪ থেকে ৯,২১৬: ৬৪ গুণ কম; attention-এর খরচ অবস্থানের বর্গে, তাই প্রায় ৪,০৯৬ গুণ। (খ) d = εc − εu = (−০.৩, ০.৪); ε̃ = (০.৪ − ১.৫, −০.১ + ২.০) = (−১.১, ১.৯)। (গ) w = ৫-এ প্রতি ধাপে দুবার: ৬০; w = ১-এ শুধু শর্তসহ শাখা: ৩০ (diffusers-এ guidance_scale ১-এর বেশি না হলে শর্তহীন শাখা চালানো হয় না)। (ঘ) প্রতি দিক ৮ দিয়ে ভাগ: ৮০ × ৫৬ × ৪ (PyTorch-এর ক্রমে ৪ × ৫৬ × ৮০)। এজন্যই ছবির মাপ ৮-এর গুণিতক হতে হয়।

  2. সমালোচনা ও নকশা: বাজারঘরের একজন সহকর্মী পণ্যের ব্যানারের জন্য একটি text-to-image script লিখেছেন আর বলছেন, “দ্রুত করতে DDIM-এর ৫ ধাপ রেখেছি, prompt-এর সঙ্গে মিল বাড়াতে guidance_scale ২০ দিয়েছি, নিজের latent-গুলো সরাসরি vae.decode(latents)-এ দিচ্ছি, আর negative prompt-এ ‘nsfw’ লিখে দিয়েছি তাই safety checker লাগবে না। Model বাংলা বোঝে, তাই prompt বাংলায় লিখছি।” ছবিগুলো অতি-উজ্জ্বল আর অদ্ভুত রঙের দাগে ভরা। কোথায় কোথায় সমস্যা, আর কী বদলাবেন?

    (১) Scaling factor ভুলে যাওয়া: diffusion-এর latent decoder-এ দেওয়ার আগে vae.config.scaling_factor (SD v1.5-এ ০.১৮২১৫) দিয়ে ভাগ করতে হয়; না করলে decoder ভুল মাপের মান পায়, তাই রঙের দাগ। (২) Guidance ২০: অনেক বড়; ε̃-এর মাপ ফুলে x̂0 পরিসরের বাইরে যায়, ছবি অতি-সম্পৃক্ত হয় আর বৈচিত্র্য কমে (অংশ ৪ আর ৫)। (৩) ৫ ধাপ DDIM: খুব কম; DPM-Solver-এর মতো উন্নত sampler-এ ২০ থেকে ২৫ ধাপ, বা DDIM-এ ৩০ থেকে ৫০ ধাপ দিয়ে শুরু করে মাপুন (অংশ ৬)। (৪) Negative prompt নিরাপত্তা নয়: এটি শুধু একটি দিক থেকে সরে যাওয়ার নরম ঠেলা; output-এর ওপর আলাদা ফিল্টার আর মানুষের পর্যালোচনা লাগে। (৫) বাংলা prompt: model card বলে model প্রধানত ইংরেজি ক্যাপশনে train করা আর অন্য ভাষায় খারাপ করে; ইংরেজিতে অনুবাদ করুন, আর ৭৭ token-এর সীমা মনে রাখুন। সঙ্গে লাইসেন্স পড়ুন আর ছবিকে AI-তৈরি বলে চিহ্নিত করুন।

  3. আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন নিচের কোড কী ছাপবে।
    import numpy as np
    eu = np.array([0.1, -0.2]); ec = np.array([0.3, -0.6])
    for w in [0, 1, 2.5]:
        print(w, np.round(eu + w * (ec - eu), 2))
    print(64 * 64 * 4, 512 * 512 * 3 // (64 * 64 * 4))
    print(2 * 30, 768 // 8)
    0 [ 0.1 -0.2]
    1 [ 0.3 -0.6]
    2.5 [ 0.6 -1.2]
    16384 48
    60 96

    (সত্যিকারের run, NumPy 2.5.4.) d = εc − εu = (০.২, −০.৪)। w = ০-এ শুধু εu, w = ১-এ হুবহু εc, আর w = ২.৫-এ ০.১ + ২.৫ × ০.২ = ০.৬ আর −০.২ − ২.৫ × ০.৪ = −১.২. Array-এর ধনাত্মক সংখ্যার আগে NumPy একটি ফাঁকা জায়গা রাখে। চতুর্থ লাইন ৬৪ × ৬৪ × ৪ latent-এর ১৬,৩৮৪টি সংখ্যা আর ৪৮ গুণের অনুপাত। শেষ লাইনে ৩০ ধাপে CFG-এর ৬০টি U-Net call আর ৭৬৮ মাপের ছবির latent-এর পাশ ৯৬.

  4. কোড লিখুন: PyTorch-এ একটি function cfg_eps(net, x, t, y, w, null_id=10) লিখুন। net(x, t, y) একটি শ্রেণি-শর্তযুক্ত denoiser, যেখানে লেবেল ১০ মানে “শর্ত নেই”। Function-টি এক batch-এ শর্তসহ আর শর্তহীন দুই শাখা চালিয়ে CFG-এর ε̃ ফেরত দেবে, আর gradient রাখবে না।
    import torch
    
    @torch.no_grad()
    def cfg_eps(net, x, t, y, w, null_id=10):
        """Classifier-free guidance: ε̃ = ε_u + w·(ε_c − ε_u), এক batch-এ দুই শাখা।"""
        y_null = torch.full_like(y, null_id)
        e = net(torch.cat([x, x]), torch.cat([t, t]), torch.cat([y, y_null]))
        e_c, e_u = e.chunk(2)
        return e_u + w * (e_c - e_u)

    যাচাই (সত্যিকারের run, torch 2.15.0, CPU-তে; অংশ ৫-এর train করা latent denoiser, ৮টি এলোমেলো latent, t = ৫০০): batch-এ একসঙ্গে চালানো আর দুই শাখা আলাদা চালানোর ফল মেলে, পার্থক্য শুধু float-এর গোলমাল।

    w=0.0: shape (8, 4, 7, 7) | max |batched − separate| = 1.19e-06 | mean |ε̃| = 0.797
    w=1.0: shape (8, 4, 7, 7) | max |batched − separate| = 1.07e-06 | mean |ε̃| = 0.811
    w=3.0: shape (8, 4, 7, 7) | max |batched − separate| = 4.53e-06 | mean |ε̃| = 0.859
    w=0 equals uncond: True | w=1 equals cond: True

    দুটি জিনিস লক্ষ্য করুন: chunk(2)-এর ক্রম cat-এর ক্রমের সঙ্গে মিলতে হবে (এখানে আগে শর্তসহ), আর batch দ্বিগুণ হয় বলে মেমরিও প্রায় দ্বিগুণ লাগে।

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

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

ক্রমLatent diffusion-এ একটি text-to-image call-এর কাজগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Tokenizer prompt-কে নির্দিষ্ট দৈর্ঘ্যের token id-এর সারিতে ভাঙে
  2. Text encoder token-গুলোকে vector-এর সারিতে রূপান্তর করে
  3. Latent আকারের Gaussian noise দিয়ে শুরু
  4. প্রতি ধাপে U-Net cross-attention দিয়ে text দেখে শর্তসহ ও শর্তহীন ε অনুমান করে
  5. CFG-এর সূত্রে দুই অনুমান মিলিয়ে sampler latent হালনাগাদ করে
  6. শেষ latent-কে scaling factor দিয়ে ভাগ করে VAE decoder ছবি বানায়

একটি latent diffusion model-এর autoencoder প্রতি দিকে ৮ গুণ ছোট করে আর ৪টি channel রাখে। ৫১২ × ৫১২ × ৩ ছবির latent-এ সংখ্যার পরিমাণ ছবির তুলনায় কত গুণ কম?

  • ৮ গুণ
  • ৪৮ গুণ
  • ৬৪ গুণ
  • ৪,০৯৬ গুণ

Classifier-free guidance-এ ε̃ = εu + w·(εc − εu). কোনো ধাপে εu = ০.২ আর εc = ০.৫ হলে w = ৩-এ ε̃ কত?

  • ০.৫
  • ১.৫
  • ১.১
  • ০.৯

একটি শ্রেণি-শর্তযুক্ত diffusion model-এ guidance scale ১ থেকে ৭-এ বাড়ালে সাধারণত কী দেখা যায়?

  • শ্রেণির সঙ্গে মিল বাড়ে, বৈচিত্র্য কমে
  • মিল আর বৈচিত্র্য দুটোই বাড়ে
  • ধাপ কম লাগে, তাই sampling দ্রুত হয়
  • w = ১-এর পরে আর কিছু বদলায় না

Stable Diffusion-এর U-Net-এ text-এর তথ্য ছবির latent-এ কীভাবে ঢোকে?

  • Text-এর token latent-এর সঙ্গে channel হিসেবে জোড়া হয়
  • শুধু শেষে VAE decoder text দেখে
  • Text encoder প্রতি ধাপে latent-এর মান সরাসরি বদলায়
  • Cross-attention: query ছবির feature থেকে, key ও value text থেকে

কেউ diffusion-এর শেষ latent সরাসরি VAE decoder-এ দিলেন, scaling factor (০.১৮২১৫) দিয়ে ভাগ না করে। সাধারণত কী হবে?

  • Decoder একটি shape error দেবে
  • ছবি চলে আসে, কিন্তু রং আর বিশদ বিকৃত হয়
  • কোনো পার্থক্য হবে না, কারণ এটি শুধু একটি ধ্রুবক
  • Sampler-কে দ্বিগুণ ধাপ চালাতে হবে

Text-to-image model-এ negative prompt দেওয়া হলে classifier-free guidance-এর সূত্রে আসলে কী বদলায়?

  • Guidance scale নিজে থেকে কমে যায়
  • Negative prompt মূল prompt-এর শেষে জুড়ে দেওয়া হয়
  • শর্তহীন শাখার খালি prompt-এর জায়গায় negative prompt বসে
  • তৈরি ছবি থেকে negative prompt-এর বস্তু কেটে ফেলা হয়

শব্দকোষ

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

মনে রাখুন

  1. Latent-এ diffusion: f = ৮, ৪ channel-এ ৫১২ × ৫১২ × ৩ → ৬৪ × ৬৪ × ৪: সংখ্যায় ৪৮ গুণ, conv-এ ৬৪ গুণ, attention-এ ৪,০৯৬ গুণ সাশ্রয়, প্রতি ধাপে; decoder চলে শেষে একবার।
  2. Autoencoder: খুব ছোট KL (বা VQ) দিয়ে গোছানো latent, perceptual আর adversarial loss-এ ধারালো ছবি; latent-কে scaling factor দিয়ে গুণ, decode-এর আগে ভাগ।
  3. Conditioning: শ্রেণি লেবেল embedding হিসেবে; text cross-attention দিয়ে, query ছবি থেকে, key/value text encoder থেকে।
  4. CFG: ε̃ = εu + w·(εc − εu); training-এ ১০% শর্ত মোছা; w = ০ শর্তহীন, ১ সাধারণ, বড় w-এ মিল বাড়ে, বৈচিত্র্য কমে; প্রতি ধাপে দুটি call.
  5. Sampler আর মাপ: DDIM, DPM-Solver, Euler একই model-এ ভিন্ন পথ; ধাপ কমালে সময় প্রায় সমানুপাতে কমে, মান মেপে দেখতে হয়; negative prompt নিরাপত্তা নয়।

পরের পর্বে: এই পর্বে মান মাপা হয়েছে একটি নিজের classifier দিয়ে আর চোখে দেখে। পর্ব ৮১ (Generative model-এর মূল্যায়ন আর নিরাপত্তা — FID, CLIP score, deepfake আর watermark)-এ আসবে generative model মাপার প্রচলিত metric, prompt আর ছবির মিল মাপার CLIP score, আর deepfake, watermark ও provenance-এর প্রতিরক্ষামূলক দিক।

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

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