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

Transformer block — residual, LayerNorm আর feed-forward

The Transformer Block — Residual Connections, LayerNorm, Feed-Forward, Pre-Norm vs Post-Norm, Cross-Attention and Weight Tying

Taught in Bangla, this lesson assembles a Transformer block from attention, a feed-forward network, residual connections and LayerNorm, printing every shape in in-page NumPy. It matches a hand-written block to nn.TransformerEncoderLayer, measures pre-norm versus post-norm training with and without warmup over several seeds, shows how residuals and LayerNorm keep gradients alive through 32 blocks, counts parameters, MACs and memory by hand, and covers cross-attention and weight tying.

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

  • একটি pre-norm block-এর forward pass, প্রতিটি array-এর shape সহ
  • Pre-norm আর post-norm-এর ক্রম, আর warmup-সহ ও ছাড়া training-এ পার্থক্য মাপা
  • এক block-এর parameter, MAC আর মেমরি হাতে হিসাব করা; depth বনাম width
  • Residual আর LayerNorm gradient-কে কীভাবে বাঁচায়, তা সংখ্যায় দেখা
  • নিজের block-কে nn.TransformerEncoderLayer-এর সঙ্গে মেলানো; cross-attention আর weight tying

Query, key আর value দিয়ে attention এসেছে পর্ব ৩২ (Attention — query, key, value আর alignment)-এ। তার matrix রূপ আর multi-head-এর হিসাব পর্ব ৩৩ (Self-attention আর multi-head attention)-এ, আর order-এর তথ্য যোগ করা পর্ব ৩৪ (Positional encoding)-এ। সেগুলো এখানে আবার শেখানো হবে না। LayerNorm-এর সূত্র আছে পর্ব ১৪-এ, warmup পর্ব ১৫-এ, dropout পর্ব ১৬-এ, residual-এর প্রথম গণিত পর্ব ১৭-এ, GELU পর্ব ৩-এ। Block-এর একটি সরল ছবি দেখেছেন AI-এর ভিত্তি কোর্সের পর্ব ৭০-এ। এবার সেই ছবির প্রতিটি তির আর বাক্স খুলে দেখা হবে।

১ · একটি block-এ চারটি কাজ — কে কী করে

বাজারঘরের একটি মিটিংয়ের কথা ভাবুন। বিক্রয়, গুদাম, ডেলিভারি আর হিসাব বিভাগের চারজন বসেছেন। প্রথমে সবাই আলোচনা করে একে অন্যের কাছ থেকে দরকারি তথ্য নেন। এটি attention. তারপর প্রত্যেকে নিজের ডেস্কে ফিরে নিজের মতো হিসাব করেন। এটি feed-forward. কেউ নিজের আগের খাতা ফেলে দেন না, নতুন নোট শুধু তাতে যোগ করেন। এটি residual. আর সভায় ঢোকার আগে সবার সংখ্যা একই এককে আনা হয়, কেউ হাজারে, কেউ কোটিতে বলেন না। এটি LayerNorm. এমন মিটিং পরপর অনেকবার হলে একটি Transformer হয়।

একটি Transformer block ঠিক দুটি sublayer দিয়ে তৈরি: multi-head attention আর একটি ছোট feed-forward network (FFN). প্রতিটির চারপাশে থাকে residual connection আর LayerNorm. প্রথম sublayer শুধু token-দের মধ্যে তথ্য সরায়; দ্বিতীয়টি প্রতিটি token-এ আলাদা হিসাব করে। একটি অন্যটির অভাব পূরণ করে।

আধুনিক pre-norm রূপে একটি encoder block এই দুটি লাইনেই শেষ:

x ← x + Attn(LN(x))
x ← x + FFN(LN(x))

RNN-এর সঙ্গে তুলনা করলে একটি সুবিধা চোখে পড়ে। RNN-এ প্রতিটি ধাপ আগের ধাপের জন্য অপেক্ষা করে (পর্ব ২৮), আর এখানে একটি block-এর সব token একসঙ্গে হিসাব হয়। দুই দূরের token-এর মধ্যে তথ্য যেতে অনেক ধাপ লাগে না, এক attention-ই যথেষ্ট। দাম হলো T² হিসাব, আর order নিজে থেকে আসে না, যা পর্ব ৩৪-এর বিষয়।

এখানে x-এর আকার (B, T, d): B হলো batch-এ কয়টি উদাহরণ, T কয়টি token, d প্রতিটি token-এর vector-এর মাপ। LN হলো LayerNorm, Attn হলো multi-head attention. লক্ষ্য করুন, আকার বদলায় না। এই একটি গুণের জন্যই block স্তূপের মতো সাজানো যায়: একটি block-এর output হুবহু পরেরটির input.

Pre-norm encoder block আর decoder block

ধূসর পথ residual: পুরোনো মান সরাসরি ⊕-এ যায়। কমলা পথে encoder-এর output decoder-এর cross-attention-এ পৌঁছায়। ছবিতে dropout আর শেষের LayerNorm দেখানো হয়নি।

Pre-norm encoder and decoder blocks with residual paths and cross-attention x (B, T, d) y (B, T′, d) LayerNormSelf-attention +LayerNorm FFN+ memory LayerNormMasked attention +LayerNorm Cross-attention+ LayerNormFFN + (B, T′, d) K, V Q Encoder-এ ২টি sublayer, decoder-এ ৩টি Q decoder থেকে, K আর V encoder থেকে

ইতিহাস: Vaswani ও সহকর্মীদের ২০১৭ সালের “Attention Is All You Need” প্রবন্ধে encoder আর decoder-এ ছয়টি করে block ছিল, d = ৫১২, h = ৮, FFN-এর মাঝের মাপ ২০৪৮. সেখানে LayerNorm বসে residual যোগের পরে। Residual এসেছিল ২০১৫ সালের ResNet থেকে (He ও সহকর্মীরা), আর LayerNorm ২০১৬ সালের Ba ও সহকর্মীদের কাজ থেকে। মানে block-এর উপাদানগুলো আগেই ছিল; নতুন ছিল RNN বাদ দিয়ে এভাবে এগুলো সাজানো।

২ · ভেতরের তিনটি উপাদান — LayerNorm, FFN আর dropout

LayerNorm. প্রতিটি token-এর d মাপের vector নিজের গড় বিয়োগ করে, নিজের standard deviation দিয়ে ভাগ হয়, তারপর শেখা γ দিয়ে গুণ আর β যোগ হয়। Batch-এর অন্য উদাহরণ বা অন্য token এখানে আসে না। তাই ১ আকারের batch-এও এটি একইভাবে চলে, আর padding-এর দৈর্ঘ্য এর ওপর প্রভাব ফেলে না। এ কারণেই sequence-এ BatchNorm-এর বদলে এটি চলে। Parameter: γ আর β মিলে 2d. নতুন অনেক model এর বদলে RMSNorm নেয়, যাতে গড় বিয়োগ বা β নেই (পর্ব ১৪).

FFN. দুটি linear স্তর আর মাঝে একটি activation:

FFN(x) = W2 · GELU(W1x + b1) + b2

W1 d থেকে r·d-তে নেয়, W2 ফিরিয়ে আনে। r হলো expansion factor, প্রচলিত মান ৪. একে “position-wise” বলা হয়, কারণ T-এর প্রতিটি ঘরে একই weight আলাদাভাবে চলে। তাহলে T যাই হোক, FFN-এর parameter বদলায় না।

FFN কেন দরকার? Attention-এর output হলো V-গুলোর ভারিত যোগফল। Weight ঠিক থাকলে তা V-এর ওপর linear. Softmax-এর non-linearity শুধু কাকে কতটা নেওয়া হবে, সেখানে। একটি token-এর ভেতরের feature মিশিয়ে নতুন feature বানানোর কাজটি মূলত FFN করে। আর চওড়া মাঝের স্তরের কারণে parameter-এর বড় অংশও এখানেই থাকে; একটু পরে হিসাব করে দেখবেন।

Activation. মূল প্রবন্ধে ReLU, পরে GPT-ধারার অনেক model-এ GELU. আরেকটি রূপ SwiGLU (Shazeer ২০২০-এর GLU রূপগুলোর পরীক্ষা; রিপোর্ট অনুযায়ী Llama-ধারায় ব্যবহৃত): SwiGLU(x) = W3(SiLU(W1x) ⊙ W2x), এখানে ⊙ মানে ঘর ধরে গুণ। Matrix তিনটি, তাই মাঝের মাপ 8d/3 রাখলে parameter প্রায় একই থাকে: 3·d·(8d/3) = 8d² আর 2·d·4d = 8d². কোনটি কোন কাজে ভালো, তা নিয়ে সর্বজনীন রায় নেই।

Dropout কোথায়. মূল প্রবন্ধে প্রতিটি sublayer-এর output-এ, residual যোগের ঠিক আগে, আর embedding ও positional encoding-এর যোগফলে। PyTorch-এর nn.MultiheadAttention-এ attention weight-এও dropout দেওয়া যায়। FFN-এর মাঝের স্তরের পরেও PyTorch-এর layer একটি বসায়। লক্ষ্য করুন, residual পথের ওপর কখনো dropout নেই: পুরোনো মান অক্ষত থাকে, বাদ পড়তে পারে শুধু নতুন অংশ। ছোট dataset-এ dropout সাহায্য করে; রিপোর্ট অনুযায়ী অনেক বড় LLM training-এ dropout খুব কম বা শূন্য রাখা হয়, কারণ সেখানে ডেটাই প্রচুর। এটি উদাহরণ ধরে নিন, নিয়ম নয়।

৩ · NumPy-তে একটি block — প্রতিটি shape ছাপিয়ে

এবার পাতার ভেতরেই একটি pre-norm block চালান। Model কিছু শেখেনি, weight এলোমেলো; দেখার বিষয় শুধু কোন array কোন আকারে কোথায় যায়। কোড ছোট রাখতে linear স্তরের bias বাদ, আর LayerNorm-এর শেখা γ = ১, β = ০ ধরা হয়েছে। প্রথমবার চালাতে Pyodide আর SciPy নামার জন্য কয়েক সেকেন্ড লাগতে পারে। পরের ব্লকটি এই ফাংশনগুলোই আবার ব্যবহার করবে।

import numpy as np
from scipy.special import erf
rng = np.random.default_rng(42)
B, T, d, h, r = 2, 5, 16, 4, 4
dk, TRACE = d // h, True
def show(name, a):
    if TRACE: print(f"{name:24}{a.shape}")
def ln(x):                                  # LayerNorm, γ=1, β=0
    return (x - x.mean(-1, keepdims=True)) / (x.std(-1, keepdims=True) + 1e-5)
def gelu(z): return 0.5 * z * (1 + erf(z / np.sqrt(2)))
def init(rng, s):                           # s = weight-এর standard deviation
    return dict(qkv=rng.normal(0, s, (d, 3*d)), o=rng.normal(0, s, (d, d)),
                f1=rng.normal(0, s, (d, r*d)), f2=rng.normal(0, s, (r*d, d)))
def mha(z, w):
    n, t = z.shape[:2]
    qkv = z @ w["qkv"];                                 show("z @ W_qkv", qkv)
    Q, K, V = [a.reshape(n, t, h, dk).transpose(0, 2, 1, 3)
               for a in np.split(qkv, 3, -1)];          show("Q, K, V (B,h,T,dk)", Q)
    s = Q @ K.transpose(0, 1, 3, 2) / np.sqrt(dk);      show("scores (B,h,T,T)", s)
    a = np.exp(s - s.max(-1, keepdims=True))
    a = a / a.sum(-1, keepdims=True);                   show("weights (B,h,T,T)", a)
    c = (a @ V).transpose(0, 2, 1, 3).reshape(n, t, d); show("concat (B,T,d)", c)
    return c @ w["o"]
def block(x, w, res=True, norm=True):       # এক pre-norm block
    N = ln if norm else (lambda a: a)
    a = mha(N(x), w);                                   show("attention output", a)
    x = x + a if res else a;                            show("x + attn", x)
    u = gelu(N(x) @ w["f1"]);                           show("FFN hidden (B,T,r*d)", u)
    f = u @ w["f2"];                                    show("FFN output", f)
    return x + f if res else f
x = rng.normal(size=(B, T, d));                         show("input x", x)
y = block(x, init(rng, 1 / np.sqrt(d)));                show("output", y)
print("LN(x): mean", ln(x)[0, 0].mean().round(4), "std", ln(x)[0, 0].std().round(4))
print("|x| =", np.linalg.norm(x).round(2), " |y - x| =", np.linalg.norm(y - x).round(2))

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

input x                 (2, 5, 16)
z @ W_qkv               (2, 5, 48)
Q, K, V (B,h,T,dk)      (2, 4, 5, 4)
scores (B,h,T,T)        (2, 4, 5, 5)
weights (B,h,T,T)       (2, 4, 5, 5)
concat (B,T,d)          (2, 5, 16)
attention output        (2, 5, 16)
x + attn                (2, 5, 16)
FFN hidden (B,T,r*d)    (2, 5, 64)
FFN output              (2, 5, 16)
output                  (2, 5, 16)
LN(x): mean 0.0 std 1.0
|x| = 10.93  |y - x| = 19.93

পড়ার সময় তিনটি জায়গায় থামুন। প্রথমত, z @ W_qkv-এ d থেকে 3d হয়, তারপর তিন ভাগ হয়ে h টি মাথায় dk মাপে যায়। দ্বিতীয়ত, scores-এর আকার (B, h, T, T): এই T² অংশটিই attention-কে T বাড়লে ব্যয়বহুল করে। তৃতীয়ত, FFN-এর মাঝে একবার (B, T, r·d) হয়ে আকার আবার (B, T, d)-তে ফেরে। Input আর output-এর আকার হুবহু এক। শেষের লাইনে LN-এর পর প্রতিটি token-এর গড় প্রায় ০, std প্রায় ১, আর residual থাকায় output হলো x-এর সঙ্গে একটি বদল (y − x).

৪ · PyTorch-এ নিজের block — nn.TransformerEncoderLayer-এর সঙ্গে মিলিয়ে

নিজের লেখা কোড ঠিক কি না, তা জানার সবচেয়ে কড়া উপায় হলো একই weight বসিয়ে একটি পরীক্ষিত implementation-এর সঙ্গে সংখ্যা মেলানো। নিচের class-গুলো পর্ব ৯-এর ধাঁচে লেখা। এটি সত্যিকারের run, CPU-তে; torch 2.15.0, Python 3.14, লেখার সময়ের স্থিতিশীল সংস্করণ।

import torch, torch.nn as nn

class MHA(nn.Module):
    def __init__(self, d, h):
        super().__init__()
        self.h, self.dk = h, d // h
        self.qkv = nn.Linear(d, 3 * d)           # d থেকে 3d, পরে ৩ ভাগ
        self.out = nn.Linear(d, d)

    def forward(self, y, m=None, mask=None):     # m=None মানে self-attention
        m = y if m is None else m
        B, T, S, d = y.shape[0], y.shape[1], m.shape[1], y.shape[2]
        Q = self.qkv(y).chunk(3, -1)[0]          # Q আসে y থেকে
        _, K, V = self.qkv(m).chunk(3, -1)       # K, V আসে m থেকে
        Q = Q.view(B, T, self.h, self.dk).transpose(1, 2)
        K = K.view(B, S, self.h, self.dk).transpose(1, 2)
        V = V.view(B, S, self.h, self.dk).transpose(1, 2)
        s = Q @ K.transpose(-2, -1) / self.dk ** 0.5
        if mask is not None:                     # True মানে দেখা নিষেধ
            s = s.masked_fill(mask, float("-inf"))
        z = (s.softmax(-1) @ V).transpose(1, 2).reshape(B, T, d)
        return self.out(z)

class Block(nn.Module):                          # pre-norm encoder block
    def __init__(self, d, h, r=4):
        super().__init__()
        self.n1, self.n2 = nn.LayerNorm(d), nn.LayerNorm(d)
        self.att = MHA(d, h)
        self.ffn = nn.Sequential(nn.Linear(d, r * d), nn.GELU(),
                                 nn.Linear(r * d, d))

    def forward(self, x, mask=None):
        x = x + self.att(self.n1(x), mask=mask)
        return x + self.ffn(self.n2(x))

এবার PyTorch-এর layer বানিয়ে তার weight এই class-এ কপি করা। nn.MultiheadAttention তিনটি projection একটিই matrix in_proj_weight-এ রাখে, আকার (3d, d); ঠিক সে কারণে আমাদের qkv-ও এক টুকরো।

ref = nn.TransformerEncoderLayer(d, h, r * d, dropout=0.0, activation="gelu",
                                 batch_first=True, norm_first=True).eval()
mine = Block(d, h, r).eval()

def copy_mha(a, b):
    a.qkv.weight.copy_(b.in_proj_weight); a.qkv.bias.copy_(b.in_proj_bias)
    a.out.load_state_dict(b.out_proj.state_dict())

with torch.no_grad():
    copy_mha(mine.att, ref.self_attn)
    mine.n1.load_state_dict(ref.norm1.state_dict())
    mine.n2.load_state_dict(ref.norm2.state_dict())
    mine.ffn[0].load_state_dict(ref.linear1.state_dict())
    mine.ffn[2].load_state_dict(ref.linear2.state_dict())
~/bazarghar-dl$ uv run python match.py
torch 2.15.0+cpu | parameter: 49984 49984
shape: (8, 12, 64) (8, 12, 64)
max |diff| float32, mask ছাড়া : 0.000e+00
max |diff| float32, causal    : 4.768e-07
max |diff| float64, mask ছাড়া : 0.000e+00
max |diff| float64, causal    : 8.882e-16
decoder: parameter 66752 66752, max |diff| 4.768e-07
dropout=0.1, train(): একই input দুই বার, পার্থক্য 1.019
dropout=0.1, eval() : একই input দুই বার, পার্থক্য 0.000

দেখানো কোডটি যা চলেছে তার সংক্ষেপ; পরীক্ষার লাইন আর decoder-এর অংশ এখানে বাদ। Parameter দুই দিকেই ৪৯,৯৮৪. Mask ছাড়া float32-এ পার্থক্য ঠিক শূন্য, সম্ভবত কারণ এই ইনপুটে একই হিসাব একই ক্রমে হয়েছে। Causal mask-সহ float32-এ পার্থক্য প্রায় ৪.৮ × ১০−৭, যা float64-এ নেমে দাঁড়ায় প্রায় ৮.৯ × ১০−১৬. মানে গণিত এক; বাকিটুকু শুধু দশমিকের গোলকরণ। একই কৌশলে decoder block-ও মিলেছে।

শেষ দুই লাইন দেখায় কেন dropout=0.0 ধরে তুলনা করতে হয়। train()-এ dropout চালু থাকলে একই input-এ দুই বারের output-ই আলাদা হয়, তখন “মেলানো” অর্থহীন। eval()-এ dropout বন্ধ, পার্থক্য শূন্য।

Default-গুলো সাবধানে দেখুন. এই সংস্করণে সত্যিকারের ছাপানো মান: dim_feedforward=2048, dropout=0.1, activation ReLU, batch_first=False, norm_first=False. অর্থাৎ কিছু না বললে আপনি post-norm পাবেন, আর input-এর প্রথম মাত্রা ধরা হবে T, B নয়। আরেকটি ফাঁদ: nn.TransformerEncoder(layer, 2)-এ norm না দিলে শেষের LayerNorm থাকে না (আমাদের run-এ enc.norm ছাপল None). Pre-norm stack-এ শেষে একটি LayerNorm নিজে যোগ করা প্রচলিত, কারণ শেষ block-এর output কখনো normalize হয়নি। আর norm_first=True হলে এই stack একটি UserWarning দেয় যে nested tensor-এর গতি-সুবিধা এখানে কাজ করবে না; এটি ভুল নয়, শুধু একটি সতর্কবার্তা।

৫ · Pre-norm বনাম post-norm — ক্রম আর training-এর স্থিরতা

একই তিনটি কাজ, ক্রম আলাদা। Post-norm: LN(x + f(x)), মূল ২০১৭ নকশা। Pre-norm: x + f(LN(x)). দেখতে ছোট বদল, কিন্তু x-এর পথ পুরো আলাদা হয়ে যায়। নিচের ছোট উদাহরণে d = ২, x = [১, ৩], আর sublayer হিসেবে একটি কাল্পনিক f, যা দুই ঘর অদলবদল করে অর্ধেক করে: f([a, b]) = [b/২, a/২].

প্রতি ধাপে দুই নকশা পাশাপাশি
ধাপ
Pre
Post
  1. শুরু. দুই নকশায় একই input x = [১, ৩].
  2. প্রথম কাজ. Pre-norm আগে LN নেয়: গড় ২, std ১, তাই [−১, ১]. Post-norm সরাসরি f চালায়: [১.৫, ০.৫].
  3. দ্বিতীয় কাজ. Pre-norm-এ f পেল normalize করা মান, দিল [০.৫, −০.৫]. Post-norm যোগ করে পেল [২.৫, ৩.৫].
  4. তৃতীয় কাজ. Pre-norm মূল x-এ যোগ করে শেষ: [১.৫, ২.৫]. Post-norm যোগফলের গড় ৩, std ০.৫ দিয়ে LN করে পেল [−১, ১]. আগের মাপ মুছে গেল।
  5. Sublayer প্রায় শূন্য হলে. Pre-norm block হুবহু x ফেরত দেয়, মানে identity. Post-norm দেয় LN(x), যা x নয়। শুরুতে sublayer-এর output ছোট হলে এই পার্থক্যটিই বড়।
  6. পরিষ্কার পথ. Pre-norm stack-এ input থেকে শেষ পর্যন্ত একটি সোজা যোগের পথ থাকে, তাতে কোনো LN নেই। Post-norm-এ প্রতিটি block-এর শেষে সেই পথ একবার করে LN পার হয়।

গণিতে বললে, pre-norm block-এর Jacobian (output-এর সঙ্গে input-এর আংশিক অন্তরকলজের matrix; মেশিন লার্নিং কোর্সের পর্ব ১১) হলো I + F′, আর L টি block-এর গুণফলে একটি বিশুদ্ধ I পদ টিকে থাকে। Post-norm-এ প্রতিটি উৎপাদকের সামনে LN-এর Jacobian-ও বসে। এতে gradient-এর পথ আর পরিষ্কার থাকে না। Xiong ও সহকর্মীরা (২০২০) mean field বিশ্লেষণে দেখান, post-norm-এ শুরুতে output-এর কাছের parameter-এর gradient বড় হতে পারে, যা উঁচু learning rate-এ training অস্থির করে আর warmup তা সামলায়। Pre-norm-এ তাঁরা warmup ছাড়াই তুলনীয় ফল পান।

নিজে মেপে দেখা. কাজটি ছোট: ১৬ অঙ্কের একটি অর্ডার নম্বরের অঙ্কগুলো ছোট থেকে বড় ক্রমে সাজিয়ে লেখা। প্রতিটি output ঘরের জন্য ১০টি অঙ্ক থেকে বাছাই, তাই অনুমান-স্তরের loss ln ১০ ≈ ২.৩০৩. Stack-এ L টি nn.TransformerEncoderLayer, d = ৬৪, h = ৪, r = ৪, dropout ০, B = ৩২, Adam (β = ০.৯, ০.৯৮). Warmup মানে প্রথম ২০০ ধাপে learning rate রৈখিকভাবে বাড়ানো। প্রতি নকশায় ৩টি seed, ৩০০ ধাপ, CPU-তে। Pre-norm stack-এ শেষে একটি LayerNorm আছে।

def batch(g):
    x = torch.randint(0, 10, (32, 16), generator=g)
    return x, x.sort(dim=1).values           # কাজ: অঙ্ক সাজিয়ে লেখা

class Net(nn.Module):
    def __init__(s, d, h, L, pre):
        super().__init__()
        s.tok, s.pos = nn.Embedding(10, d), nn.Embedding(16, d)
        s.blocks = nn.ModuleList(nn.TransformerEncoderLayer(d, h, 4 * d, 0.0,
            "gelu", batch_first=True, norm_first=pre) for _ in range(L))
        s.norm = nn.LayerNorm(d) if pre else nn.Identity()
        s.head = nn.Linear(d, 10)
    def forward(s, x):
        h = s.tok(x) + s.pos(torch.arange(16))
        for b in s.blocks: h = b(h)
        return s.head(s.norm(h))
# লুপে: for pg in opt.param_groups: pg["lr"] = lr * min(1, (t + 1) / warm)
~/bazarghar-dl$ uv run python prepost.py 24 0.003 3 300
--- pre   warm=  0 | L=24 lr=0.003 | 3 seed
step    0 loss  2.446 [ 2.419, 2.466] acc 0.104 [0.083,0.137]
step   25 loss  1.019 [ 0.978, 1.060] acc 0.577 [0.566,0.592]
step   50 loss  0.666 [ 0.624, 0.737] acc 0.735 [0.702,0.757]
step  100 loss  0.395 [ 0.320, 0.448] acc 0.837 [0.811,0.875]
step  200 loss  0.119 [ 0.095, 0.163] acc 0.960 [0.940,0.974]
step  300 loss  0.132 [ 0.073, 0.216] acc 0.954 [0.922,0.975]
  max grad norm, first 200 steps: 4.8 5.4 6.9
…
--- post  warm=  0 | L=24 lr=0.003 | 3 seed
step    0 loss  2.479 [ 2.393, 2.540] acc 0.103 [0.089,0.130]
step   25 loss  2.317 [ 2.303, 2.340] acc 0.104 [0.099,0.112]
step   50 loss  2.310 [ 2.306, 2.317] acc 0.100 [0.099,0.103]
step  100 loss  2.310 [ 2.305, 2.313] acc 0.100 [0.095,0.107]
step  200 loss  2.310 [ 2.308, 2.311] acc 0.101 [0.095,0.107]
step  300 loss  2.308 [ 2.306, 2.311] acc 0.094 [0.084,0.103]
  max grad norm, first 200 steps: 4.9 8.0 3.3
…
…

এটি সত্যিকারের run; ছাপা অংশ ছেঁটে শুধু দুটি warmup-হীন নকশা রাখা হয়েছে, বাকিগুলো নিচের টেবিলে। ব্র্যাকেটে তিন seed-এর সর্বনিম্ন আর সর্বোচ্চ। দৃশ্যটি পরিষ্কার: L = ২৪ আর learning rate ৩×১০−৩-এ post-norm তিনটি seed-এই ২৫ ধাপের মধ্যে ২.৩১-এ আটকে গেল, যা ln ১০-এর একদম কাছে। Accuracy ০.১০, মানে অনুমানের স্তর। Model ৩০০ ধাপ ধরে কিছুই শিখল না। একই শর্তে pre-norm পৌঁছাল ০.১৩২-তে।

L = ২৪, learning rate ৩×১০−৩ (৩ seed-এর গড়)

নকশাloss, ধাপ ২৫loss, ধাপ ১০০loss, ধাপ ৩০০ (পরিসর)accuracy, ধাপ ৩০০
Pre-norm, warmup ছাড়া1.0190.3950.132 (0.073–0.216)0.954
Pre-norm, warmup1.2800.3740.215 (0.148–0.288)0.916
Post-norm, warmup ছাড়া2.3172.3102.308 (2.306–2.311)0.094
Post-norm, warmup1.3120.5760.420 (0.309–0.486)0.842

L = ৬, learning rate ৩×১০−৩ (৩ seed-এর গড়)

নকশাloss, ধাপ ২৫loss, ধাপ ১০০loss, ধাপ ৩০০ (পরিসর)accuracy, ধাপ ৩০০
Pre-norm, warmup ছাড়া0.7740.1980.090 (0.022–0.126)0.968
Pre-norm, warmup1.7630.4150.085 (0.047–0.123)0.971
Post-norm, warmup ছাড়া0.8230.2400.131 (0.065–0.196)0.955
Post-norm, warmup1.6120.3990.113 (0.049–0.186)0.961

L = ২৪, learning rate ৩×১০−৪ (৩ seed-এর গড়)

নকশাloss, ধাপ ২৫loss, ধাপ ১০০loss, ধাপ ৩০০ (পরিসর)accuracy, ধাপ ৩০০
Pre-norm, warmup ছাড়া1.1260.3500.071 (0.062–0.084)0.982
Pre-norm, warmup2.1940.9860.109 (0.097–0.126)0.976
Post-norm, warmup ছাড়া1.1770.3410.059 (0.045–0.079)0.986
Post-norm, warmup2.2060.8460.114 (0.073–0.142)0.970

তিনটি টেবিল একসঙ্গে পড়লে ফলটি “pre-norm ভালো, post-norm খারাপ”-এর চেয়ে সূক্ষ্ম। প্রথমত, warmup post-norm-কে বাঁচাল: একই গভীরতায় আটকে না গিয়ে ০.৪২০-তে এল, যদিও pre-norm-এর পেছনে। দ্বিতীয়ত, L = ৬-এ চারটি নকশাই ভালো শিখল; সমস্যা গভীরতার সঙ্গে আসে। তৃতীয়ত, learning rate দশ ভাগের এক ভাগ করলে L = ২৪-এও post-norm warmup ছাড়া শিখল, আর এই একটি run-এ ধাপ ৩০০-তে তার গড় loss সবচেয়ে কম (০.০৫৯), যদিও পরিসর pre-norm-এর সঙ্গে মিশে যায়। অর্থাৎ post-norm “ভুল” নয়; তার নিরাপদ learning rate-এর সীমা এখানে নিচে। Pre-norm এই সীমাটি বাড়িয়ে দেয়। আর warmup pre-norm-কে এখানে সাহায্য করেনি (ধাপ ৩০০-তে L = ২৪, ৩×১০−৩-এ ০.১৩২ বনাম ০.২১৫), কারণ ৩০০ ধাপের মধ্যে ২০০ ধাপ কম গতিতে গেছে।

সৎ সীমা: এটি একটি ছোট কৃত্রিম কাজ, তিনটি seed, ৩০০ ধাপ, CPU-তে এক run; dropout নেই, weight-এর init PyTorch-এর default. এই সংখ্যা দিয়ে কোনো সাধারণ নিয়ম প্রমাণ হয় না। “আটকে যাওয়া”-র সঠিক কারণও এই run থেকে বলা যায় না: প্রথম ২০০ ধাপে সর্বোচ্চ gradient norm আটকে যাওয়া নকশায় ৩.৩ থেকে ৮.০, যা warmup-সহ post-norm-এর (১০.১ থেকে ১৩.৯) চেয়ে কম। তাই শুধু gradient-এর মাপ দেখে নির্ণয় করবেন না; loss-ও দেখুন। এমন আটকে যাওয়া ধরার পথ পাবেন পর্ব ১৮-এ।

৬ · Block কেন ভালো শেখে — gradient সংখ্যায় দেখা

লম্বা লাইনে কানে কানে খবর পাঠানোর খেলার কথা ভাবুন। ৩২ জন পার হলে প্রথম কথাটি আর থাকে না। এবার সবার পাশ দিয়ে একটি সোজা পাইপ বসান, যেখানে আসল কথাটি সরাসরি যায়, আর প্রত্যেকে শুধু তার সংশোধনটুকু যোগ করেন। Residual এই পাইপ। আর LayerNorm হলো প্রতিজনকে একই স্বরে কথা বলতে বলা, যাতে সংশোধন জমে জমে চিৎকারে পরিণত না হয়।

এখন আগের NumPy ফাংশনগুলো দিয়েই ৩২ block-এর একটি stack বানানো হবে, দুই ধরনের weight std নিয়ে: ০.২৫ (d = ১৬-এ ১/√d) আর ০.১. চারটি রূপ পাশাপাশি: দুটিই আছে, শুধু LN, শুধু residual, কোনোটিই নেই। শেষের output-কে একটি স্থির এলোমেলো vector-এ প্রজেক্ট করে একটি সংখ্যা (loss-এর বিকল্প) নেওয়া হবে। k-তম block-এর পরের activation-এর ওপর সেই সংখ্যার gradient norm মাপা হবে finite difference দিয়ে: প্রতিটি উপাদানে ±ε বসিয়ে বাকি block-গুলো আবার চালানো। PyTorch পাতায় চলে না, তাই এই পথ। সবগুলো উপাদান একটি বড় batch-এ রাখায় এটি দ্রুত চলে। ডেস্কটপে একই রকম block-এ (T = ৬, L = ১৬) এই পদ্ধতির ফল PyTorch autograd-এর সঙ্গে পাঁচ significant অঙ্ক পর্যন্ত মিলেছে (যেমন ১১.৫৭৯ দুই দিকেই)।

import numpy as np
import matplotlib.pyplot as plt
TRACE, L = False, 32
ks = list(range(0, L + 1, 8))

def gnorm(W, c, k, a, res, norm):           # k-তম activation-এর gradient norm
    n, e = a.size, 1e-5 * max(1.0, np.abs(a).max())
    p = np.tile(a.reshape(-1), (2 * n, 1)); i = np.arange(n)
    p[i, i] += e; p[n + i, i] -= e
    y = p.reshape(2 * n, 4, d)
    for w in W[k:]: y = block(y, w, res, norm)
    v = ((ln(y) if norm else y) * c).sum((1, 2))
    return np.linalg.norm((v[:n] - v[n:]) / (2 * e))

fig, axes = plt.subplots(1, 2, figsize=(8, 3.4), sharey=True)
for ax, s in zip(axes, (0.25, 0.1)):        # 0.25 = 1/sqrt(16)
    rng = np.random.default_rng(42)
    W = [init(rng, s) for _ in range(L)]
    x0, c = rng.normal(size=(1, 4, d)), rng.normal(size=(4, d))
    print(f"\nweight std = {s}; k = {ks}")
    for name, res, norm in [("residual + LN", 1, 1), ("LN only", 0, 1),
                            ("residual only", 1, 0), ("neither", 0, 0)]:
        a, g = x0, []
        for k in range(L + 1):
            if k in ks: g.append(gnorm(W, c, k, a, res, norm))
            if k < L: a = block(a, W[k], res, norm)
        print(f"{name:14}" + "".join(f"{v:9.1e}" for v in g))
        ax.semilogy(ks, g, marker="o", label=name)
    ax.set_title(f"weight std = {s}")
    ax.set_xlabel("activation after block k (0 = input)")
axes[0].set_ylabel("gradient norm (log)"); axes[0].legend(fontsize=8)
plt.show()

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

weight std = 0.25; k = [0, 8, 16, 24, 32]
residual + LN   1.3e+01  1.7e+00  1.1e+00  8.9e-01  7.8e-01
LN only         1.5e-01  3.0e-01  5.2e+00  1.3e+01  5.1e+00
residual only   2.8e+14  5.1e+09  5.0e+06  9.5e+03  7.8e+00
neither         2.1e+04  7.2e+02  1.9e+02  6.5e+01  7.8e+00

weight std = 0.1; k = [0, 8, 16, 24, 32]
residual + LN   8.7e+00  6.8e+00  5.5e+00  5.0e+00  4.7e+00
LN only         5.5e+00  2.7e+00  4.5e+00  4.6e+01  4.2e+01
residual only   1.7e+01  1.4e+01  1.2e+01  9.4e+00  7.8e+00
neither         2.9e-51  5.8e-39  1.1e-25  4.7e-13  7.8e+00

দুই ছবিতে k = ০ হলো input, k = ৩২ শেষ। ডানের ছবি (std = ০.১) আগে দেখুন। “neither” সারিতে শেষে gradient প্রায় ৮, কিন্তু input-এ পৌঁছে ১০−৫০-এর ঘরের নিচে: এটিই vanishing gradient, কারণ প্রতি block-এ একটি ১-এর চেয়ে ছোট গুণক। শুধু residual যোগ করলেই সেটি ১০-এর ঘরে ফিরে আসে। এবার বাঁয়ের ছবি (std = ০.২৫): “residual only”-তে input-এর gradient প্রায় ২.৮ × ১০১৪, এটি exploding gradient, কারণ এখন প্রতি block-এ I + F′-এর মান ১-এর বেশি, আর activation নিজেও ফুলে ওঠে। অর্থাৎ residual একা যথেষ্ট নয়। “residual + LN” দুই init-এই শান্ত থাকে, ০.৭৮ থেকে ১৩-এর মধ্যে।

এক seed দেখে সিদ্ধান্ত না নিতে ডেস্কটপে একই সেটিংয়ে ৫টি seed চালানো হয়েছে; input-এ (k = ০) পরিসর: std = ০.১-এ residual + LN ৭.২ থেকে ৮.৬, LN only ০.০১৩ থেকে ৫.৯, residual only ১৫ থেকে ২২, neither ১০−৫৪ থেকে ১০−৫২. std = ০.২৫-এ residual + LN ৬.০ থেকে ১৪, LN only ০.০১ থেকে ৬.০, residual only প্রায় ১০১২ থেকে ১০১৪, আর neither ০.১৩ থেকে প্রায় ২.৬ × ১০৫, মানে ছয় ঘরের বেশি ছড়ানো। লক্ষণীয় হলো স্থিরতা: দুটি একসঙ্গে থাকলে seed আর init বদলালেও মাপ প্রায় এক ঘরেই থাকে। এটিই block-এর নকশার মূল গুণ।

এই পরিমাপ থেকে একটি প্রচলিত init কৌশলের যুক্তিও বোঝা যায়। Residual শাখা শুরুতে যত ছোট, block তত identity-র কাছে, আর সিগন্যাল তত অক্ষত। GPT-2 প্রতিবেদনে residual স্তরের weight-কে স্তরের সংখ্যার ১/√N দিয়ে ছোট করার কথা আছে; ReZero আর LayerScale একই লক্ষ্যে শেখা যায় এমন একটি ছোট গুণক বসায়। সব কৌশলের মূল কথা এক: network শুরু করুক প্রায় “কিছু না করে”, তারপর দরকারি বদলটুকু শিখুক।

সীমা: এই সংখ্যা শুধু শুরুর মুহূর্তের (training হয়নি), একটি seed-এর, ছোট d-এর। Residual না থাকলেও সবসময় মৃত্যু হয় না, যেমন LN only-এর সারি বলছে। আর “gradient বড় থাকলেই ভালো শেখে” এমনও নয়; তা দেখাতে আগের অংশের training run দরকার ছিল। পর্ব ১৭-এ এই যুক্তির MLP রূপ এসেছে, আর ResNet-এ পর্ব ২২-এ।

৭ · Parameter, MAC আর মেমরি — হাতে হিসাব

পরীক্ষা আর ইন্টারভিউতে প্রায়ই প্রশ্ন আসে: এই model-এ parameter কত? উত্তর মুখস্থ না করে একবার গুনে নিন। নিচে d = ৬৪, h = ৪, r = ৪, আর সব linear স্তরে bias আছে (PyTorch-এর default).

একটি linear স্তর a→b: a·b + b (bias)
অংশ
হিসাব
জমা
  1. Q, K, V. তিনটি projection প্রতিটি d → d; একসঙ্গে ধরলে 64 → 192. Bias-সহ ১২,৪৮০.
  2. Attention-এর শেষ স্তর. জোড়া দেওয়া ফল d থেকে d-তে: ৪,১৬০. পুরো attention ১৬,৬৪০, মানে 4(d² + d).
  3. FFN-এর প্রথম স্তর. 64 → 256, bias ২৫৬টি: ১৬,৬৪০.
  4. FFN-এর দ্বিতীয় স্তর. 256 → 64, bias ৬৪টি: ১৬,৪৪৮. পুরো FFN ৩৩,০৮৮, attention-এর প্রায় দ্বিগুণ।
  5. LayerNorm. দুটি, প্রতিটিতে 2d: মোট ২৫৬. প্রায় নগণ্য। মোট ৪৯,৯৮৪, যা PyTorch-এর গোনার সঙ্গে হুবহু মিলেছে।
  6. h-এর ভূমিকা. h শুধু একই matrix-এর output-কে ভাগ করে; নতুন weight আনে না। তাই h = ৪ বা ৮, মোট একই। শর্ত শুধু একটি: d-কে h দিয়ে ভাগ করা যেতে হবে।

সাধারণ সূত্র, r = ৪ আর bias ধরে: attention 4(d²+d), FFN 2rd² + rd + d, LayerNorm 4d. বড় d-তে মোট প্রায় 12d². T, B বা h কোথাও নেই। এই সূত্রগুলো নিচের ব্লকে একটি টেবিল বানায়। সঙ্গে MAC আর মেমরিও আছে।

MAC প্রতি token, এক block-এ: Q, K, V 3d²; স্কোর h·T·dk = T·d; weighted sum আরও T·d; output projection d²; FFN 2rd². যোগফল (4 + 2r)d² + 2Td, এখানে softmax আর LayerNorm-এর ছোট খরচ ধরা হয়নি। পুরো sequence-এর জন্য T দিয়ে গুণ করুন। দুটি অংশ সমান হয় যখন T = (4 + 2r)d/2, মানে r = ৪-এ T = ৬d. এর নিচের T-তে খরচের বেশি অংশ T² নয়, projection আর FFN-এর।

def parts(d, r):
    att = 3 * (d*d + d) + (d*d + d)
    ffn = (d * r*d + r*d) + (r*d * d + d)
    return att, ffn, 2 * (d + d)

print("  d  h  r  L      att      ffn    LN    block      all L")
for d, h, r, L in [(64,4,4,2), (128,4,4,4), (256,8,4,6), (512,8,4,6), (768,12,4,12)]:
    a, f, n = parts(d, r); b = a + f + n
    print(f"{d:3d} {h:2d} {r:2d} {L:2d} {a:8,d} {f:8,d} {n:5,d} {b:8,d} {L*b:10,d}")

print("\nএকই d = 512, r = 4, আলাদা h:",
      [(h, sum(parts(512, 4))) for h in (1, 8, 16)])

mac = lambda d, r, T: ((4 + 2*r) * d*d, 2 * T * d)
print("\nMAC প্রতি token, d = 256, r = 4")
for T in (64, 512, 1536, 4096):
    p, q = mac(256, 4, T)
    print(f"T = {T:4d}: rest {p:9,d}  attention {q:9,d}  share {q/(p+q):.0%}")

print("\nDepth বনাম width, T = 128, r = 4 (প্রায় একই budget)")
for d, L in [(128, 16), (256, 4)]:
    P = L * sum(parts(d, 4)); M = L * sum(mac(d, 4, 128))
    print(f"d = {d}, L = {L:2d}: parameter {P:9,d}  MAC/token {M:9,d}")

B, h, T, d, r = 8, 8, 512, 256, 4
mb = lambda n: f"{n * 4 / 1e6:6.1f} MB"           # float32 = 4 byte
print(f"\nমেমরি, B={B} h={h} T={T} d={d}, এক block, float32")
print("parameter          ", mb(sum(parts(d, r))))
print("Q, K, V            ", mb(3 * B * T * d))
print("attention weights  ", mb(B * h * T * T))
print("FFN hidden         ", mb(B * T * r * d))

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

  d  h  r  L      att      ffn    LN    block      all L
 64  4  4  2   16,640   33,088   256   49,984     99,968
128  4  4  4   66,048  131,712   512  198,272    793,088
256  8  4  6  263,168  525,568 1,024  789,760  4,738,560
512  8  4  6 1,050,624 2,099,712 2,048 3,152,384 18,914,304
768 12  4 12 2,362,368 4,722,432 3,072 7,087,872 85,054,464

একই d = 512, r = 4, আলাদা h: [(1, 3152384), (8, 3152384), (16, 3152384)]

MAC প্রতি token, d = 256, r = 4
T =   64: rest   786,432  attention    32,768  share 4%
T =  512: rest   786,432  attention   262,144  share 25%
T = 1536: rest   786,432  attention   786,432  share 50%
T = 4096: rest   786,432  attention 2,097,152  share 73%

Depth বনাম width, T = 128, r = 4 (প্রায় একই budget)
d = 128, L = 16: parameter 3,172,352  MAC/token 3,670,016
d = 256, L =  4: parameter 3,159,040  MAC/token 3,407,872

মেমরি, B=8 h=8 T=512 d=256, এক block, float32
parameter              3.2 MB
Q, K, V               12.6 MB
attention weights     67.1 MB
FFN hidden            16.8 MB

d = ৫১২, h = ৮, r = ৪-এর এক block-এ ৩১,৫২,৩৮৪, যা nn.TransformerEncoderLayer(512, 8, 2048)-এর গোনার সঙ্গে আমাদের run-এ হুবহু মিলেছে। FFN সেখানে attention-এর প্রায় দ্বিগুণ। d দ্বিগুণ হলে parameter প্রায় চার গুণ, কিন্তু L দ্বিগুণ হলে দ্বিগুণ। তাই d = ১২৮, L = ১৬ আর d = ২৫৬, L = ৪-এর budget প্রায় সমান। MAC-ও কাছাকাছি।

মেমরিতে একটি বড় শিক্ষা: T = ৫১২-এ attention weight-এর matrix একাই প্রায় ৬৭ MB, parameter-এর (৩.২ MB) প্রায় ২১ গুণ। Training-এ gradient আর Adam-এর দুটি buffer মিলে parameter-এর মোট প্রায় চার গুণ জায়গা লাগে, কিন্তু OOM-এর কারণ প্রায়ই T². এটি সরল হিসাব: scaled_dot_product_attention-এর কোনো backend সম্পূর্ণ matrix না-ও রাখতে পারে, যার ধারণা পর্ব ৩৭ (দক্ষ attention)-এ আসবে।

পুরো model-এর একটি হিসাব. ২০১৭ সালের প্রবন্ধের মাপে ধরুন: d = ৫১২, ৬টি encoder আর ৬টি decoder block, প্রবন্ধের ইংরেজি–জার্মান কাজের প্রায় ৩৭,০০০ token-এর ভাগ করা vocabulary, আর embedding ও head একই matrix. Encoder ৬ × ৩১,৫২,৩৮৪ = ১,৮৯,১৪,৩০৪. Decoder ৬ × ৪২,০৪,০৩২ = ২,৫২,২৪,১৯২. Embedding ৩৭,০০০ × ৫১২ = ১,৮৯,৪৪,০০০. মোট প্রায় ৬.৩ কোটি। এটি আমাদের সূত্রের হিসাব, প্রবন্ধের জানানো সংখ্যা নয়; তবে এই পদ্ধতিতেই যেকোনো model-এর আকার অনুমান করা যায়।

Depth বনাম width. L বাড়ালে তথ্য আরও বার token-দের মধ্যে ঘোরে, এবং একটি হিসাবের ওপর আরেকটি হিসাব গড়ে ওঠে। d বাড়ালে প্রতি token-এ বেশি তথ্য ধরে, কিন্তু parameter বর্গে বাড়ে। কোনো একক নিয়ম নেই। Stochastic depth (পর্ব ১৬) বা residual-এর ছোট শুরু খুব গভীর stack-এ সাহায্য করে; আবার gradient checkpointing মেমরি বাঁচায়, সময় নেয়। নিজের কাজে কয়েকটি ছোট কনফিগারেশন validation-এ তুলনা করাই সৎ পথ।

৮ · Decoder block আর cross-attention

Decoder block-এ encoder-এর দুটির পাশে একটি তৃতীয় sublayer যুক্ত হয়। প্রথম sublayer হলো causal mask-সহ self-attention, যাতে কোনো ঘর পরের token না দেখে। তৃতীয়টি FFN. মাঝেরটি cross-attention, যা পর্ব ৩০-এর fixed context-এর সমস্যার সমাধান: এখন decoder-এর প্রতিটি ঘর encoder-এর পুরো output, মানে memory, দেখতে পারে। নিচে d = ২, এক head, projection বাদ (Q, K, V সরাসরি বসানো), Tsrc = ৩, Ttgt = ২.

Scores = Q·Kᵀ/√2, পরে সারি ধরে softmax
ধাপ
মান
  1. Memory. Encoder-এর শেষ output: ৩টি token, প্রতিটি ২ মাপের। আকার (Tsrc, d) = (৩, ২).
  2. K আর V. দুটিই memory থেকে আসে। Encoder থেকে এই দুটিই নেওয়া হয়, Q নয়। সাধারণত প্রতিটি decoder block একই memory পড়ে।
  3. Q. আসে decoder-এর নিজের অবস্থা থেকে। এটি প্রশ্ন: “এই ঘর লিখতে input-এর কোথায় তাকাব?”
  4. Scores. Q·Kᵀ, তারপর √২ দিয়ে ভাগ। সারি Ttgt, কলাম Tsrc: আকার (২, ৩).
  5. Weights. প্রতি সারিতে softmax: decoder-এর প্রতিটি ঘর encoder-এর ৩টি ঘরের ওপর একটি probability ভাগ পেল। কলাম ধরে যোগ ১ হওয়ার শর্ত নেই।
  6. ফল. Weights·V: আকার (২, ২). Ttgt-ই রইল। Tsrc শুধু “কতগুলোর মধ্যে বাছাই” ঠিক করে, তাই output-এর আকারে আসে না।

বাস্তবে দুটি কথা মনে রাখা দরকার। প্রথমত, encoder একবার চলে আর memory বানিয়ে রাখে; decoder প্রতিটি নতুন token লেখার সময় সেই একই memory পড়ে। দ্বিতীয়ত, input-এ padding থাকলে memory-র সেই ঘরগুলোও বাদ দিতে হয়। তাই cross-attention-এ causal mask নয়, লাগে padding mask (PyTorch-এ memory_key_padding_mask). পর্ব ৩০-এর padding-এর সমস্যাটি এখানে অন্য রূপে ফিরে আসে: এই mask ভুলে গেলে কোনো error হয় না, শুধু decoder pad-এর দিকেও তাকায়।

সত্যিকারের run-এ মাপগুলো এবার বড় করে দেখুন। নিচে আমাদের DecBlock pre-norm, তিন sublayer, আর উপরের MHA-ই ব্যবহার করে। Q একটি input থেকে, K আর V অন্যটি থেকে নেওয়ার জন্য শুধু m দেওয়া হয়।

def forward(self, y, mem, causal):
    y = y + self.sa(self.n1(y), mask=causal)   # masked self-attention
    y = y + self.ca(self.n2(y), m=mem)         # Q: y, K ও V: mem
    return y + self.ffn(self.n3(y))
~/bazarghar-dl$ uv run python encdec.py
src (2, 9, 64) -> memory (2, 9, 64)
tgt (2, 5, 64)
  masked self-attn-এর পরে (2, 5, 64)
  cross-attn-এর পরে       (2, 5, 64)
  FFN-এর পরে              (2, 5, 64)
parameter: encoder block 49984 | decoder block 66752
cross weights (B, h, T_tgt, T_src): (2, 4, 5, 9)
এক সারির যোগফল: 1.0
T_src =  3 -> decoder output (2, 5, 64) | parameter 66752
T_src = 40 -> decoder output (2, 5, 64) | parameter 66752
d=512, h=8, ffn=2048: 3152384 4204032

Encoder block ৪৯,৯৮৪ থেকে decoder block ৬৬,৭৫২: পার্থক্য ১৬,৭৬৮, মানে একটি attention (১৬,৬৪০) আর একটি LayerNorm (১২৮). নিজের decoder block-কেও nn.TransformerDecoderLayer-এর সঙ্গে মেলানো হয়েছে; পার্থক্য সর্বোচ্চ ৪.৮ × ১০−৭. Cross weight-এর আকার (২, ৪, ৫, ৯), মানে B, h, Ttgt, Tsrc, আর এক সারির যোগ ১. Tsrc ৩ বা ৪০ যাই হোক, output (২, ৫, ৬৪) আর parameter একই। মূল ২০১৭ মাপে (d = ৫১২) এক encoder block ৩১,৫২,৩৮৪, এক decoder block ৪২,০৪,০৩২.

একটি কথা মনে রাখুন: GPT-ধারার model কোনো encoder ব্যবহার করে না। তার block-এ শুধু masked self-attention আর FFN থাকে, মানে আকারে encoder block-এর মতো, তবে causal mask-সহ। পর্ব ৩৬ (নিজে Transformer লেখা)-এ আপনি এই রূপটিই লিখবেন।

৯ · Weight tying — একই matrix দুই জায়গায়

Language model-এর শুরুতে embedding matrix (V, d), শেষে LM head (d → V). দুটি একই আকারের। Embedding বলে “এই token-এর vector কী”, head বলে “এই vector কোন token-এর কাছাকাছি”। তাই একই matrix দুই কাজে লাগানো স্বাভাবিক চিন্তা। একে বলে weight tying. PyTorch-এ এক লাইন: head.weight = tok.weight; এখানে V মানে vocabulary-র আকার, আগের attention-এর V নয়।

~/bazarghar-dl$ uv run python tying.py
V    d  L     untied       tied     saved     %
   100   64  2    129,280    122,880     6,400   5.0
  5000  128  4  2,106,112  1,466,112   640,000  30.4
 32000  256  6 21,188,608 12,996,608 8,192,000  38.7
 50000  768 12 162,052,608 123,652,608 38,400,000  23.7
same object: True
state_dict keys has both: True True
named_parameters has head.weight: False
wrong count over state_dict: 2,106,112 | right: 1,466,112
one grad tensor shared: torch.Size([5000, 128]) True

Bias ছাড়া head-এ সাশ্রয় ঠিক V·d: ৫,০০০ × ১২৮ = ৬,৪০,০০০. ছোট d আর ছোট stack-এ এই ভাগ বড় (এই সারিগুলোতে ৩৮.৭% পর্যন্ত), বড় stack-এ তুলনায় কমে (২৪%). ছোট vocabulary-তে (V = ১০০) মাত্র ৫%. লক্ষ্য করুন তিনটি জিনিস। state_dict()-এ দুটি নামই আছে, কিন্তু named_parameters()-এ একটি। তাই state_dict দিয়ে গুনলে ভুল বড় সংখ্যা আসে (২১,০৬,১১২ বনাম ১৪,৬৬,১১২). আর gradient-ও একটিই tensor: দুই ব্যবহারের gradient এতে যোগ হয়। Weight সেভ করার সময় ফরম্যাট দুই নাম কীভাবে সামলায়, তা নিজের টুলের ডকুমেন্টেশনে দেখে নেওয়া ভালো।

আরও গভীরে: Tying-এর যুক্তি ও ফল এসেছে Press ও Wolf (২০১৭) আর Inan ও সহকর্মীদের (২০১৭) কাজে; মূল Transformer-ও এক ধরনের ভাগাভাগি করে। Nguyen ও Salazar (২০১৯) দেখান pre-norm আর ছোট init-এ warmup ছাড়া training সম্ভব; Xiong ও সহকর্মীরা (২০২০) এর mean field ব্যাখ্যা দেন। ReZero (Bachlechner ও সহকর্মীরা, ২০২০) আর LayerScale (Touvron ও সহকর্মীরা, ২০২১) residual শাখায় ছোট শেখা গুণক বসায়। RMSNorm আর SwiGLU কোন model-এ আছে, তা প্রতিটি model-এর নিজের config থেকে নিশ্চিত হয়ে নিন, কারণ রিপোর্ট আর সংস্করণ বদলায়।

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

  • Pre-norm আর post-norm-এর সূত্র? x + f(LN(x)) আর LN(x + f(x)).
  • d = ৫১২, h = ৮, r = ৪-এর encoder block-এ parameter? ৩১,৫২,৩৮৪; h = ১৬ হলেও একই।
  • Cross-attention-এ Q, K, V কোথা থেকে? Q decoder থেকে; K, V encoder-এর memory থেকে।
  • Weight tying কত বাঁচায়? V·d, যদি head-এ bias না থাকে।
  • Residual-এ gradient বাঁচে কেন? Jacobian I + F′; গুণফলে একটি I পদ টিকে থাকে।

১০ · AI-এর যুগে — block-এর কোড যাচাই

AI assistant একটি Transformer block দ্রুত লিখে দেয়, আর কোডটি প্রায় সবসময় চলে। ভুলগুলো error দেয় না; তাই সেগুলো ধরতে হয় মেপে।

যে ভুল প্রায়ই দেখা যায়ফলকীভাবে ধরবেন
Pre-norm বলে post-norm কোড, বা norm_first বাদগভীর stack warmup ছাড়া আটকে যেতে পারেForward-এর লাইন সূত্রের সঙ্গে মেলানো
batch_first বাদB আর T অদলবদল, error ছাড়াই ভুল ফলBatch-এর এক উদাহরণ একা চালিয়ে তুলনা করা
Pre-norm stack-এ শেষের LayerNorm নেইHead normalize না করা মান পায়মডিউলের তালিকা ছাপিয়ে দেখা
Cross-attention-এ Q আর K/V উল্টোOutput-এর দৈর্ঘ্য TsrcTsrc ≠ Ttgt নিয়ে shape দেখা
Mask-এর অর্থ উল্টো (True মানে “নিষেধ”, না “দেখা”)Decoder ভবিষ্যৎ দেখে; training দারুণ, ব্যবহারে খারাপপরের token বদলালে আগের output বদলায় কি না পরীক্ষা করা
Parameter state_dict দিয়ে গোনাTying থাকলে সংখ্যা বেশিmodel.parameters() আর হাতে-গোনা সূত্রে মেলানো
একটি seed দেখে “এই নকশা ভালো” দাবিকাকতালীয় ফলকয়েকটি seed, আর পরিসর দেখানো

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

আমি PyTorch-এ একটি pre-norm Transformer block লিখছি (d=64, h=4, r=4, GELU, batch_first). ১. Block আর MHA class দাও, nn.MultiheadAttention ছাড়াই। ২. তোমার block-এ nn.TransformerEncoderLayer-এর weight কপি করার কোড দাও (norm_first=True, batch_first=True, dropout=0, activation="gelu"), আর সর্বোচ্চ পার্থক্য ছাপাও। ৩. Parameter হাতে-গোনা সূত্র দাও, আর model.parameters()-এর সঙ্গে assert করো। ৪. Causal mask ঠিক আছে কি না, তা পরের token বদলে পরীক্ষা করার কোড লেখো। ৫. এক batch overfit করার কোড দাও; পরে warmup-সহ ও ছাড়া ৩টি seed-এ তুলনা করো। ৬. যেসব argument তুমি এই PyTorch সংস্করণের docs-এ যাচাই করোনি, তা স্পষ্ট করে বলো।

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

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

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

প্র ০১মূল Transformer post-norm ছিল। তাহলে অনেক নতুন model pre-norm নিল কেন? Post-norm কি ভুল ছিল?

“ভুল” শব্দটি ঠিক নয়। ২০১৭ সালের প্রবন্ধে post-norm-এর সঙ্গে warmup আর পরে নেমে আসা একটি learning rate schedule ছিল, ৪,০০০ ধাপের warmup সহ। আর সেই সমন্বয়ে অনুবাদের কাজে ফল চমৎকার এসেছিল। সমস্যাটি ধরা পড়ে যখন model আরও গভীর হয়, আর সবাই একই নকশা অন্য কাজে আনেন। তখন warmup আর learning rate সাবধানে খুঁজতে হয়, আর ভুল হলে training আটকে যায়।

২০১৯ সালে Nguyen ও Salazar-এর “Transformers without Tears” দেখায়, pre-norm আর ছোট শুরুর weight-এ warmup ছাড়াই বড় learning rate-এ শেখা যায়। একই সময়ে OpenAI-এর GPT-2 প্রতিবেদনে (Radford ও সহকর্মীরা, ২০১৯) LayerNorm-কে sub-block-এর input-এ সরানোর কথা আসে, শেষ block-এর পরে একটি অতিরিক্ত LayerNorm সহ। Xiong ও সহকর্মীরা (২০২০) mean field দিয়ে তত্ত্ব দেন: post-norm-এ শুরুতে output-এর কাছের parameter-এর gradient বড় হতে পারে।

তবু পুরো গল্প একমুখী নয়। Pre-norm-এ residual পথে কোনো স্বাভাবিকীকরণ নেই, তাই activation-এর মান গভীরতার সঙ্গে বাড়তে পারে; এ কারণে শেষে একটি LayerNorm লাগে। আর কিছু কাজে post-norm-এর চূড়ান্ত ফল ভালো বা সমান বলে রিপোর্ট আছে। কিন্তু সাধারণভাবে এই সিদ্ধান্ত কোনো একক প্রমাণে হয়নি। আমাদের ছোট run-এও ফল এক কাজ আর ছোট আকারের, তাই সিদ্ধান্তে পৌঁছানোর জন্য যথেষ্ট নয়। বড় model-এ training-এর অস্থিরতা খুব দামি, আর pre-norm সেই ঝুঁকি কমায় বলে নতুন কাজে এটি প্রচলিত হয়েছে।

মূল উপলব্ধি: একটি নকশা শুধু চূড়ান্ত ফলে নয়, ঝুঁকি আর tuning-এর খরচেও বিচার হয়; pre-norm-এর জনপ্রিয়তার বড় অংশ সেখান থেকে।

প্র ০২FFN-এর মাঝের স্তর ৪ গুণ চওড়া কেন? আর FFN পুরো বাদ দিলে কী হবে?

“৪” কোনো তত্ত্ব থেকে আসেনি। ২০১৭ সালের প্রবন্ধে d = ৫১২ আর dff = ২০৪৮ ছিল, আর পরের অনেক কাজ এই অনুপাত রেখে দিয়েছে। একই নকশার ভেতরে থেকে ভালো ফল পাওয়া গেলে অনুপাত বদলানোর চাপ কম থাকে, আর একে সেই অভ্যাস হিসেবেই পড়ুন। SwiGLU-তে ৮/৩ আসে একটি হিসাব থেকে: তিনটি matrix হওয়ায় parameter প্রায় একই রাখার চেষ্টা।

r কমালে কী হয়? আমাদের সূত্রে FFN-এর parameter 2rd². r = ২ হলে FFN অর্ধেক, block-এর প্রায় ৩৩% parameter কমে। ফল কতটা পড়বে, সেটি কাজ আর ডেটার ওপর নির্ভর করে। কিছু কাজে ছোট r-ও পরীক্ষা করা হয়েছে।

FFN বাদ দিলে সমস্যা গভীর। Attention-এর weight ঠিক থাকলে output V-এর ওপর linear, তারপর একটি linear projection. Softmax-এর non-linearity থাকলেও সেটি শুধু “কাকে কতটা” ঠিক করে। একটি token-এর vector-এর উপাদানগুলো এখানে সরাসরি একে অন্যের সঙ্গে জটিলভাবে মেশে না। FFN সেই কাজটি করে। আবার উল্টো দিকেও একটি ইঙ্গিত আছে: MLP-Mixer (Tolstikhin ও সহকর্মীরা, ২০২১) attention ছাড়াই ছবির কাজে ভালো ফল দেখিয়েছে। Token মেশানো আর feature মেশানোর দুটি ভূমিকা অনেক রূপে করা যায়; attention শুধু একটি ভালো রূপ, তবে ভাষার জন্য খুব উপযোগী, কারণ এটি দৈর্ঘ্য আর সূচকের ওপর নির্ভর না করে দূরের token থেকে তথ্য আনে।

তাই r বেছে নেওয়ার সময় আগে পরিচিত মানটি নিন, তারপর নিজের ডেটায় একটি ছোট আর একটি বড় মান পরীক্ষা করে দেখুন।

মূল উপলব্ধি: Block-এর দুটি ভূমিকা আলাদা: token-দের মধ্যে আদান-প্রদান আর token-এর ভেতরে হিসাব। সংখ্যাগুলো প্রায়ই অভ্যাস, ভূমিকাগুলো নকশার মূল।

প্র ০৩Weight tying এত parameter বাঁচায়, তবু কিছু বড় model কেন এটি ব্যবহার করে না?

Tying-এর পেছনে দুটি যুক্তি। একটি সাশ্রয়: V·d parameter কমে। অন্যটি ধারণাগত: Press ও Wolf (২০১৭) আর Inan ও সহকর্মীরা (২০১৭) দেখান, ভাষা model-এ এতে ফল ভালো হতে পারে, কারণ যে matrix token পড়ে সেই matrix-ই token লেখে, আর কম ব্যবহৃত token-ও দুই পথের gradient থেকে শেখে। ছোট ডেটায় এটি বেশ কাজের। মূল Transformer প্রবন্ধেও দুই embedding আর pre-softmax linear-এর মধ্যে একই weight ভাগ করা হয়, আর embedding-এ √d দিয়ে গুণ করা হয়।

আমাদের run-এ দেখলেন, সাশ্রয়ের ভাগ V আর d-এর সঙ্গে L-এর ওপরও নির্ভর করে। Block-এর parameter প্রায় 12Ld², আর tying বাঁচায় Vd. untied model-এ embedding আর head দুটিই থাকে, তাই মোটের মধ্যে সাশ্রয়ের ভাগ প্রায় V/(12Ld + 2V). V = ৫০,০০০, d = ৭৬৮, L = ১২ হলেই ২৪%, কিন্তু খুব বড় d আর L-এ এই ভাগ কমে আসে। অর্থাৎ বিশাল model-এ সাশ্রয়ের যুক্তি দুর্বল।

বড় model-এ বিষয়টি পরীক্ষার ওপর ছেড়ে দেওয়া হয়। Hugging Face-এর config-এ tie_word_embeddings নামে একটি setting আছে, আর বিভিন্ন model এখানে আলাদা মান রাখে; তাই কোনো model সম্পর্কে অনুমান না করে তার config পড়ে দেখুন। আরেকটি ব্যবহারিক দিক আছে। Tie না করলে embedding আর head আলাদা হতে পারে, আর ডেটা যত বেশি, তত এই স্বাধীনতার দাম কম মনে হতে পারে। Tying-এ আবার সেভ করা, শেয়ার করা আর quantize করার সময় দুই নামের ঝামেলা সামলাতে হয়।

তাই নিজের ছোট model-এ tying দিয়ে শুরু করা যুক্তিসঙ্গত, আর পরে validation-এ দুটি রূপ তুলনা করে দেখা ভালো।

মূল উপলব্ধি: Tying একই সঙ্গে সাশ্রয় আর একটি ধারণা (inductive bias); তার মূল্য ডেটা আর model-এর আকারের ওপর নির্ভর করে, সর্বজনীন নিয়ম নয়।

প্র ০৪একই budget-এ গভীর আর সরু, না অগভীর আর চওড়া? ইতিহাস কী বলে?

আমাদের টেবিলে d = ১২৮, L = ১৬ আর d = ২৫৬, L = ৪-এর parameter প্রায় সমান, MAC-ও কাছাকাছি। আচরণ কিন্তু এক নয়। গভীরে একটি attention-এর ফল পরের attention-এর ইনপুট হয়, তাই “তিন ধাপে সম্পর্ক” ধরতে সুবিধা হয়। চওড়ায় প্রতিটি token অনেক বেশি তথ্য বহন করতে পারে, h-ও বাড়ানো যায়, আর attention weight-এর মেমরি L কম থাকায় কম লাগে। সবচেয়ে বড় পার্থক্য খরচের ধরনে: d বদলালে checkpoint-এর আকার বর্গে বাড়ে, L বদলালে সরলরেখায়।

রিপোর্ট অনুযায়ী GPT-2 পরিবার দুটি একসঙ্গে বাড়িয়েছে: সবচেয়ে ছোট রূপে ১২ স্তর আর d = ৭৬৮, সবচেয়ে বড় রূপে ৪৮ স্তর আর d = ১৬০০. অর্থাৎ বাস্তবে একটি বেছে নেওয়া হয়নি। Kaplan ও সহকর্মীদের (২০২০) scaling law প্রবন্ধে ভাষা model-এর loss মূলত মোট parameter-এর সঙ্গে সম্পর্কিত বলে দেখা যায়, আকৃতির প্রভাব তুলনায় কম। তবে সেটি একটি পরিবারের ওপর একটি পর্যবেক্ষণ, প্রমাণিত সার্বজনীন নিয়ম নয়।

গভীরতার একটি সীমা আছে gradient আর সময়ে: প্রতিটি নতুন block আরও একবার পুরো attention চালায়। আবার চওড়ায় সীমা হলো d-এর বর্গে বাড়া parameter, আর ছোট ডেটায় overfitting. বাস্তবে যা ভালো কাজ করে, তা হলো কয়েকটি ছোট কনফিগারেশন validation ডেটায় তুলনা করে দেখা। একজন নতুন practitioner-এর জন্য নিয়মটি সরল: আগে পরিচিত অনুপাত নিন, তারপর সময় আর মেমরির সীমায় মেলান।

মূল উপলব্ধি: Parameter-এর সংখ্যা একা স্থাপত্যের গুণ বলে না; depth আর width খরচের আর শেখার ধরন বদলায়, তাই উত্তর আসে পরীক্ষা থেকে।

প্র ০৫Residual না থাকলে Transformer কি একেবারেই শেখে না? LayerNorm ছাড়াও কি চলে?

“একেবারেই না” বলা ভুল হবে। আমাদের পাতার পরিমাপে LayerNorm থাকলে residual ছাড়াও gradient পুরো মরে যায়নি (input-এ ০.০১ থেকে ৬), যদিও সেটি কেবল শুরুর মুহূর্ত। He ও সহকর্মীদের ২০১৫ সালের ResNet দেখিয়েছিল গভীর plain CNN-এ training error-ই বেড়ে যেতে পারে, আর ২০১৬ সালের পরের প্রবন্ধে তাঁরা পরিষ্কার identity পথের গুরুত্ব দেখান। Veit ও সহকর্মীরা (২০১৬) একটি ব্যাখ্যা দেন: residual network অনেকগুলো অপেক্ষাকৃত ছোট পথের সমষ্টির মতো আচরণ করে। সেই দৃষ্টিতে gradient বেশিরভাগ ছোট পথ দিয়ে যায়।

আবার LayerNorm ছাড়া residual-ও আমাদের পরিমাপে বিপজ্জনক: weight-এর std ০.২৫ হলে gradient বিস্ফোরিত হলো। কিন্তু weight ছোট হলে সেই একই সারি শান্ত হয়ে যায় (std = ০.১-এ L = ৩২-তেও ১৫ থেকে ২২). মানে init আর স্থাপত্য একসঙ্গে কাজ করে। এই চিন্তা থেকেই এসেছে Fixup (Zhang ও সহকর্মীরা, ২০১৯), যা CNN-এ normalization ছাড়া ভালো init দিয়ে শেখা দেখায়। আর ReZero (২০২০) ও LayerScale (২০২১) residual শাখায় ছোট শেখা গুণক বসায়, যাতে শুরুতে প্রতিটি block প্রায় identity থাকে।

GPT-2 প্রতিবেদনেও এই ধারণার একটি রূপ আছে: residual স্তরের weight-কে স্তরের সংখ্যার ১/√N দিয়ে ছোট করা। আর আধুনিক model-এ কিছু LayerNorm RMSNorm-এ বদলে গেছে, কিন্তু কোনো না কোনো স্বাভাবিকীকরণ প্রায় সবসময় থাকে। ফলে প্রশ্নটির সৎ উত্তর হলো: residual আর normalization একে অপরের পরিপূরক। একটি gradient-এর পথ দেয়, অন্যটি মানের মাপ সামলায়।

নিজের কোডে তাই এদের একটি বাদ দিলে সেটি জেনে-বুঝে করুন, আর শেখার গতি ও loss মেপে দেখুন।

মূল উপলব্ধি: ভালো training কোনো একক কৌশলের ফল নয়; residual, normalization, init আর learning rate মিলে একটি ভারসাম্য তৈরি করে।

অনুশীলন

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

  1. পূর্ণ numerical: d = ১২৮, h = ৮, r = ৪, সব linear-এ bias. (ক) এক encoder block-এর parameter কত? (খ) এক decoder block? (গ) V = ৮,০০০ আর bias-হীন head-এ tying কত বাঁচায়? (ঘ) T = ৬৪-এ প্রতি token-এ MAC কত?

    (ক) d² + d = ১৬,৫১২; attention ৪ × ১৬,৫১২ = ৬৬,০৪৮. FFN: ২·৪·১৬,৩৮৪ + ৪·১২৮ + ১২৮ = ১,৩১,০৭২ + ৬৪০ = ১,৩১,৭১২. LayerNorm ৪·১২৮ = ৫১২. মোট ১,৯৮,২৭২. (খ) ২ × ৬৬,০৪৮ + ১,৩১,৭১২ + ৩ × ২৫৬ = ২,৬৪,৫৭৬. (গ) ৮,০০০ × ১২৮ = ১০,২৪,০০০. (ঘ) ১২d² + 2Td = ১,৯৬,৬০৮ + ১৬,৩৮৪ = ২,১২,৯৯২.

  2. Output অনুমান করুন: x-এর আকার (৩, ১০, ৮), h = ২, r = ৪, আর d = ৮. নিচের প্রতিটি array-এর আকার লিখুন: x @ W_qkv, Q (ভাগের পর), scores, FFN-এর মাঝের স্তর, output. দ্বিতীয় প্রশ্ন: x = [১, ৩] আর sublayer সবসময় ০ দিলে pre-norm আর post-norm block কী ফেরত দেবে?

    x @ W_qkv: (৩, ১০, ২৪). Q: (৩, ২, ১০, ৪). Scores: (৩, ২, ১০, ১০). FFN-এর মাঝে: (৩, ১০, ৩২). Output: (৩, ১০, ৮). দ্বিতীয় প্রশ্ন: pre-norm [১, ৩], মানে x; post-norm LN([১, ৩]) = [−১, ১].

  3. নকশা ও সমালোচনা: একজন সহকর্মী বাজারঘরের রিভিউ ক্লাসিফায়ারের জন্য লিখেছেন: nn.TransformerEncoderLayer(128, 8) দিয়ে ১২ স্তর, input আকার (B, T, d), warmup নেই, learning rate ৩×১০−৩, আর একটি seed-এ accuracy ৯০% পেয়ে বলছেন “এই নকশা সেরা”. কী কী সমস্যা দেখছেন?

    এক, default batch_first=False, তাই (B, T, d) ইনপুটে B-কে sequence ধরা হবে: কোনো error ছাড়াই ভুল। দুই, default norm_first=False মানে post-norm; গভীর stack আর উঁচু learning rate-এ warmup ছাড়া ঝুঁকি আছে। হয় pre-norm নিন, নয় warmup দিন। তিন, padding mask-এর কথা নেই। চার, FFN default ২০৪৮, d = ১২৮-এ r = ১৬: parameter-এর বেশিরভাগ FFN-এ যাবে, সম্ভবত অজান্তে। পাঁচ, একটি seed আর কোনো baseline নেই; TF-IDF আর logistic regression-এর মতো সহজ baseline ও কয়েকটি seed লাগবে।

  4. কোডিং: FFN-এর বদলে bias ছাড়া একটি SwiGLU লিখুন: W1, W2: d → m; W3: m → d. d = ৬৪, m = ১৭০ হলে parameter কত? r = ৪-এর bias-হীন FFN-এর সঙ্গে তুলনা করুন, আর একটি (২, ৫, ৬৪) ইনপুটে shape যাচাই করুন।

    ৩ × ৬৪ × ১৭০ = ৩২,৬৪০. Bias-হীন r = ৪ FFN: ২ × ৪ × ৬৪² = ৩২,৭৬৮. প্রায় সমান। কোড: self.w1 = nn.Linear(d, m, bias=False), একইভাবে w2 আর w3, আর forward-এ return self.w3(F.silu(self.w1(x)) * self.w2(x)). Output (২, ৫, ৬৪), আর sum(p.numel() for p in ffn.parameters()) ৩২,৬৪০ দেবে। আসল ব্যবহারের আগে এক batch overfit করে দেখুন।

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

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

ক্রমএকটি pre-norm decoder block-এর ভেতরের কাজগুলো সঠিক ক্রমে সাজান।

  1. LayerNorm, তারপর causal mask-সহ self-attention, আর residual যোগ
  2. LayerNorm, তারপর memory থেকে K ও V নিয়ে cross-attention, আর residual যোগ
  3. LayerNorm, তারপর প্রতিটি token-এ FFN, আর residual যোগ
  4. Output পরের decoder block-এ পাঠানো

d = ৫১২, r = ৪, সব linear-এ bias থাকা একটি Transformer encoder block-এ h = ৮ থেকে h = ১৬ করা হলো। Parameter কীভাবে বদলাবে?

  • দ্বিগুণ হবে
  • অর্ধেক হবে
  • একই থাকবে: ৩১,৫২,৩৮৪
  • শুধু output projection-এর অংশ বাড়বে

একটি cross-attention-এ B = ২, h = ৪, Ttgt = ৫, Tsrc = ৯. Attention weight-এর আকার কী?

  • (২, ৪, ৯, ৫)
  • (২, ৪, ৫, ৯)
  • (২, ৪, ৫, ৫)
  • (২, ৪, ৯, ৯)

Sublayer f-এর output শূন্যের কাছাকাছি হলে কোন বাক্যটি ঠিক?

  • Pre-norm block প্রায় identity, post-norm block নয়
  • Post-norm block প্রায় identity, pre-norm block নয়
  • দুটিই প্রায় identity
  • কোনোটিই identity নয়

একটি language model-এ V = ১০,০০০, d = ২৫৬, আর LM head-এ bias নেই। Embedding আর head tie করলে parameter কত কমবে?

  • ১০,০০০
  • ৬৫,৫৩৬
  • ৫১,২০,০০০
  • ২৫,৬০,০০০

একটি Transformer encoder layer d = ৬৪, dropout = ০.১ দিয়ে বানানো হয়েছে, আর নিজের block-এর সঙ্গে সংখ্যা মেলানো হচ্ছে। Weight কপির পরেও output মিলছে না। প্রথমে কী দেখা উচিত?

  • Head-এর সংখ্যা কমিয়ে দেখা
  • Model eval() অবস্থায় আছে কি না, বা dropout = ০ করা হয়েছে কি না
  • Learning rate কমানো
  • Weight tying বন্ধ করা

d = ২৫৬, r = ৪ একটি block-এ প্রতি token-এ MAC প্রায় (4 + 2r)d² + 2Td. কোন T-তে attention-এর 2Td অংশ বাকি অংশের সমান হয়?

  • T = ২৫৬
  • T = ৭৬৮
  • T = ১,৫৩৬
  • T = ৩,০৭২

শব্দকোষ

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

মনে রাখুন

  1. কাঠামো: x ← x + Attn(LN(x)); x ← x + FFN(LN(x)). আকার (B, T, d) বদলায় না। Attention token-দের মধ্যে, FFN token-এর ভেতরে।
  2. ক্রম: post-norm LN(x + f(x)), pre-norm x + f(LN(x)). Pre-norm-এ সোজা যোগের পথ থাকে, আর stack-এর শেষে একটি LayerNorm লাগে।
  3. গোনা: attention 4(d²+d), FFN 2rd²+rd+d, LN 4d; h নেই। MAC প্রতি token (4+2r)d²+2Td.
  4. Decoder: masked self-attention, cross-attention (Q decoder, K ও V encoder), FFN. Output-এর দৈর্ঘ্য Ttgt.
  5. যাচাই: dropout = ০, eval(), copied weight দিয়ে পরীক্ষিত layer-এর সঙ্গে সংখ্যা মেলানো; default (batch_first, norm_first) দেখা; parameters() দিয়ে গোনা।

পরের পর্বে: পর্ব ৩৬ (নিজে Transformer লেখা — ছোট বাংলা language model)-এ এই block সাজিয়ে একটি পুরো language model লিখে, train করে, তার সৎ মূল্যায়ন করা হবে।

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

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