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

Positional encoding — sinusoidal, learned আর RoPE

Positional Encoding — Learned Embeddings, Sinusoids, Relative Positions, RoPE, ALiBi and a Length Extrapolation Test

Taught in Bangla, this lesson shows that self-attention loses word order without positions, then compares learned position embeddings, sinusoidal encodings computed by hand, relative positions, RoPE and ALiBi. In-page NumPy verifies that RoPE scores depend only on the offset, and a real PyTorch run trains on short sequences and tests on 128 positions, where RoPE wins one task and loses another, so no single method is best everywhere.

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

  • Self-attention-এ ক্রম কেন হারায়, আর position যোগ করলে কী বদলায়, সংখ্যা দিয়ে
  • Learned embedding-এর সীমা, আর সারি কেন নতুন ঘরে শেখে না
  • Sinusoidal সূত্র হাতে হিসাব, আর dot product কেন শুধু দূরত্বের ফাংশন
  • RoPE হাতে ঘুরিয়ে আর NumPy-তে লিখে “শুধু ব্যবধান” ধর্মটি যাচাই করা
  • ALiBi-র ধারণা, কোন model কী ব্যবহার করে, আর দৈর্ঘ্যের পরীক্ষা নিজে সাজানো

পর্ব ৩৩-এ (Self-attention আর multi-head attention) দেখেছেন, Q, K আর V একই input থেকে আসে, আর score হয় dot product দিয়ে। সেখানে একটি প্রশ্ন খোলা রয়ে গেছে: ক্রমের খবর model পাবে কোথা থেকে? RNN-এ (পর্ব ২৮) ক্রমটি হিসাবের ধরনেই থাকত, কারণ token একটির পর একটি ঢুকত। এই পর্বে সেই খবর আলাদা করে ঢোকানোর পদ্ধতিগুলো দেখবেন। Dot product আর embedding-এর ভিত্তি AI-এর ভিত্তি কোর্সের পর্ব ৩৩ আর পর্ব ৬৯-এ আছে। ঘূর্ণনের matrix-এর জ্যামিতি আছে মেশিন লার্নিং কোর্সের পর্ব ৭-এ।

১ · ক্রম কোথায় হারায় — একই শব্দ, উল্টো অর্থ

বাজারঘরের দুটি review নিন: “ডেলিভারি ভালো, দাম খারাপ” আর “ডেলিভারি খারাপ, দাম ভালো”. শব্দ একই, শুধু জায়গা আলাদা, আর অর্থ প্রায় উল্টো। Self-attention-এর সূত্রে কিন্তু কোথাও ঘরের নম্বর নেই। প্রতিটি token বাকিদের সঙ্গে শুধু vector মেলায়।

পরীক্ষার হলে খাতা জমা নিলেন, কিন্তু রোল নম্বর ছাড়া। কোন খাতায় কী লেখা, সব ঠিকই আছে। কিন্তু কে আগে বসেছিল, কে পরে, সেই খবর আর নেই। খাতাগুলো যেভাবেই সাজান, প্রতিটি খাতার ভেতরের লেখা বদলায় না। Position ছাড়া self-attention-এর অবস্থা এমনই।

এই ধর্মের নাম permutation equivariance: input-এর সাজানো বদলালে output-ও একইভাবে সাজানো বদলায়, মান নয়। তাই সব ঘরের গড় নিলে (mean pooling) ফল একদম এক হয়। নিচের কোডে দুই review-এর হিসাব করা হয়েছে, প্রথমে position ছাড়া, পরে প্রতিটি ঘরে একটি আলাদা vector যোগ করে। প্রথমবার চালালে Python আর NumPy লোড হতে কয়েক সেকেন্ড লাগতে পারে।

import numpy as np
rng = np.random.default_rng(34)
d = 8
vocab = ["ডেলিভারি", "ভালো", "দাম", "খারাপ"]
E = {w: rng.normal(size=d) for w in vocab}     # token embedding
WQ, WK, WV = rng.normal(size=(3, d, d)) / np.sqrt(d)

def self_attn(X):                              # X: (T, d)
    Q, K, V = X @ WQ, X @ WK, X @ WV
    S = Q @ K.T / np.sqrt(d)
    S = S - S.max(axis=-1, keepdims=True)      # stable softmax
    A = np.exp(S) / np.exp(S).sum(axis=-1, keepdims=True)
    return A @ V

a = ["ডেলিভারি", "ভালো", "দাম", "খারাপ"]
b = ["ডেলিভারি", "খারাপ", "দাম", "ভালো"]
Xa = np.array([E[w] for w in a])
Xb = np.array([E[w] for w in b])
P = rng.normal(size=(4, d))                    # প্রতিটি ঘরের নিজের vector

Ya, Yb = self_attn(Xa), self_attn(Xb)
print("position ছাড়া:")
print("  'ভালো'-র output-এর পার্থক্য:", np.abs(Ya[1] - Yb[3]).max().round(12))
print("  দুই বাক্যের গড় vector-এর পার্থক্য:", np.abs(Ya.mean(0) - Yb.mean(0)).max().round(12))

Pa, Pb = self_attn(Xa + P), self_attn(Xb + P)
print("position-সহ:")
print("  'ভালো'-র output-এর পার্থক্য:", np.abs(Pa[1] - Pb[3]).max().round(4))
print("  দুই বাক্যের গড় vector-এর পার্থক্য:", np.abs(Pa.mean(0) - Pb.mean(0)).max().round(4))

আউটপুট (পাতায় চালানো):

position ছাড়া:
  'ভালো'-র output-এর পার্থক্য: 0.0
  দুই বাক্যের গড় vector-এর পার্থক্য: 0.0
position-সহ:
  'ভালো'-র output-এর পার্থক্য: 1.991
  দুই বাক্যের গড় vector-এর পার্থক্য: 0.3723

Position ছাড়া “ভালো” শব্দটি ঘর ১-এ থাকুক বা ঘর ৩-এ, তার output একই (এখানে ১২ ঘর দশমিকে গোল করে শূন্য)। গড় নিলে দুই review আলাদা করার কোনো উপায়ই থাকে না। ঘরের vector যোগ করতেই দুটোই বদলে গেছে। Weight এখানে এলোমেলো, কিছুই শেখানো হয়নি: এটি কাঠামোর ধর্ম, training-এর ফল নয়। পর্ব ৩৩-এ একই কথা সাধারণ permutation দিয়ে দেখেছেন; এখানে দেখলেন এর ব্যবসায়িক খরচ।

Position ঢোকানোর জায়গা আছে তিনটি, আর পদ্ধতিগুলো মূলত এই জায়গাতেই আলাদা। নিচের ছবিটি এই পর্বের মানচিত্র।

Position কোথায় ঢোকে

Where each method injects position into attention

Where learned, sinusoidal, RoPE and ALiBi act token id → embedding Q, K, V projection score = Q·Kᵀ softmax V-এর ভারিত গড় Learned, sinusoidalembedding-এ যোগ RoPEQ আর K-কে ঘোরানো ALiBiscore থেকে বিয়োগ
ছবি ৩৪.১ — একই attention, position ঢোকার তিনটি জায়গা।

২ · Learned embedding — সবচেয়ে সোজা উপায়, আর তার দেয়াল

সবচেয়ে সরল চিন্তা: token-এর জন্য যেমন nn.Embedding টেবিল (পর্ব ২৭), ঘরের নম্বরের জন্যও তেমন একটি টেবিল। একে বলে learned position embedding. ঘরের নম্বরটি হলো absolute position, শুরু থেকে গোনা:

xt = E(tokent) + P(t), যেখানে P একটি (max_len, d) shape-এর শেখার যোগ্য matrix.

এই পদ্ধতি চলে এবং বহু পরিচিত model-এ ব্যবহার হয়েছে। Hugging Face-এর GPT-2 নথি বলে, এই model absolute position embedding ব্যবহার করে, n_positions-এর default 1024, আর input ডানদিকে pad করতে হয়। কারণ বাঁয়ে padding দিলে আসল token-গুলো ভুল ঘরের সারি পায় (পর্ব ১১-এর padding মনে করুন)।

দুর্বলতা দুটি। টেবিলের মাপের বাইরে গেলে সরাসরি error আসে, যাকে max length-এর দেয়াল বলা যায়। আর যে সারি training-এ কখনো ব্যবহার হয়নি, সেটি কিছুই শেখে না। নিচের খেলনা হিসাবে দ্বিতীয়টি হাতে ধরে দেখুন।

P টেবিল আর একটি খেলনা loss: ∑(P[t] − c[t])², lr = 0.1
অবস্থা
P[0]
P[1]
P[2]
P[3]
P[4]
P[5]
  1. শুরু. পাঁচটি সারি, সব এলোমেলো মানে। ধরা যাক training-এর সব sequence-এর দৈর্ঘ্য ৩।
  2. কোন সারি লাগে. T = ৩ হলে শুধু P[0], P[1], P[2] হিসাবে ঢোকে। বাকি দুই সারি loss-এর সূত্রেই নেই, তাই তাদের gradient শূন্য।
  3. এক ধাপ. লক্ষ্য c = [1, 0], [0, 1], [−1, 0]. P[0]-এর প্রথম ঘর: ০.১০ − ০.১·২·(০.১০ − ১) = ০.২৮. P[3] আর P[4] একটুও নড়েনি।
  4. ২০ ধাপ. প্রথম তিন সারি লক্ষ্যের প্রায় কাছে। শেষ দুই সারি এখনো শুরুর মানেই, যেন training হয়ইনি।
  5. T = ৫. কোনো error নেই! কিন্তু ঘর ৩ আর ৪ পাচ্ছে শুরুর এলোমেলো মান। এটিই বিপজ্জনক দিক: model চুপচাপ চলে, আর আপনি ধরতে পারেন না।
  6. T = ৬. P[5] বলে কিছুই নেই, তাই IndexError. অন্তত এখানে ভুলটি স্পষ্ট। Sinusoidal সূত্র এখানে একটি মান দিতে পারত, কিন্তু সেটিও model-এর অদেখা মান। পরের অংশগুলোতে তার ফল দেখবেন।

দেয়ালটি সত্যিকারের PyTorch-এ এমন দেখায়। ছোট model-টি পরে অংশ ৮-এ দেখবেন; এখানে max_len=16 দিয়ে প্রথমে ১৬, তারপর ১৭ ঘরের input দেওয়া হলো (সত্যিকারের run, CPU-তে; traceback ছোট করে দেখানো):

~/bazarghar-dl$ uv run python run_error.py
T=16: torch.Size([1, 16, 10])
Traceback (most recent call last):
  …
    return torch.embedding(weight, input, padding_idx, scale_grad_by_freq, sparse)
IndexError: index out of range in self

আরেকটি ফাঁদ আসে batch বানানোর সময়। Absolute position-এ ঘরের নম্বর গোনা হয় টেবিলের বাঁ থেকে। ধরা যাক “ভালো পণ্য” দুই token, আর batch-এর দৈর্ঘ্য ৪. ডানে pad করলে “ভালো” পায় ঘর ০, “পণ্য” পায় ঘর ১. বাঁয়ে pad করে নম্বর ঠিক না করলে তারা পায় ঘর ২ আর ৩, যদিও বাক্যের শুরু সেটিই। তাই generation-এ বাঁয়ে pad করলে attention mask থেকে আলাদা করে position নম্বর হিসাব করতে হয়। এই বিষয় পর্ব ৩৭-এ আবার আসবে।

এই মেসেজে “position” শব্দটিই নেই। তাই দৈর্ঘ্য একটু বাড়তেই index out of range in self এলে আগে দেখুন T কি position টেবিলের চেয়ে বড়। Token id-র সীমা ছাড়ালেও একই মেসেজ আসে, তাই দুটোই যাচাই করতে হয়।

৩ · Sinusoidal encoding — ঘড়ির কাঁটা থেকে সূত্র

২০১৭ সালের Transformer paper-এ (Vaswani ও সহলেখকেরা) শেখার বদলে একটি সূত্র ব্যবহার করা হয়, যার নাম sinusoidal encoding. ধারণাটি ঘড়ি থেকে সহজে আসে।

দেয়ালঘড়ির সেকেন্ডের কাঁটা দ্রুত ঘোরে, মিনিটের কাঁটা ধীরে, ঘণ্টার কাঁটা আরও ধীরে। শুধু সেকেন্ডের কাঁটা দেখে সময় বলা যায় না, কারণ সে প্রতি মিনিটে ফিরে আসে। কিন্তু তিনটি কাঁটা মিলিয়ে দেখলে দিনের প্রতিটি মুহূর্ত আলাদা চেনা যায়। Sinusoidal encoding-এ প্রতিটি ঘরের vector হলো অনেকগুলো কাঁটার অবস্থান।

একটি কাঁটার অবস্থান বৃত্তের একটি বিন্দু, যা দুটি সংখ্যায় লেখা হয়: sin আর cos. শুধু sin নিলে চলে না, কারণ sin(a) = sin(π − a), মানে দুটি আলাদা কোণ একই মান দেয়। জোড়ায় নিলে এই দ্ব্যর্থতা থাকে না। ঘর t-এর জন্য, জোড়া n (n = ০, ১, …, d/২ − ১)-এ:

PE(t, 2n) = sin(ωn t),   PE(t, 2n+1) = cos(ωn t),   ωn = 1 / 100002n/d

এখানে d হলো embedding-এর মাত্রা, আর ωn হলো কাঁটাটি প্রতি ঘরে কত রেডিয়ান ঘোরে। একটি কাঁটার wavelength হলো 2π/ωn, মানে সে কত ঘর পরে নিজের জায়গায় ফেরে। Paper অনুযায়ী এই মান 2π থেকে 10000·2π পর্যন্ত জ্যামিতিকভাবে বাড়ে। এই vector-টি token embedding-এর সঙ্গে যোগ হয়। এবার ঘর ৩-এর মান হাতে বের করুন।

চার ঘরের vector, দুই জোড়া sin আর cos
হিসাব
PE(3)
  1. প্রস্তুতি. d = ৪ মানে দুই জোড়া, দুটি কাঁটা। কোণ মাপা হয় রেডিয়ানে, ডিগ্রিতে নয়। Calculator-এ এই ভুলটিই সবচেয়ে বেশি হয়।
  2. দ্রুত কাঁটা. n = ০-তে ω = ১, তাই ঘর ৩-এ কোণ ৩ রেডিয়ান।
  3. প্রথম জোড়া. sin ৩ = ০.১৪১১, cos ৩ = −০.৯৯০০. এরা vector-এর ঘর ০ আর ১-এ বসে।
  4. ধীর কাঁটা. n = ১-এ 100002/4 = ১০০, তাই ω = ০.০১. ঘর ৩-এ কোণ মাত্র ০.০৩।
  5. দ্বিতীয় জোড়া. sin ০.০৩ ≈ ০.০৩০০, cos ০.০৩ ≈ ০.৯৯৯৬. ধীর কাঁটাটি এখনো শুরুর কাছেই।
  6. যাচাই. প্রতিটি জোড়ায় sin² + cos² = ১, তাই দৈর্ঘ্য² = d/২ = ২, যেকোনো ঘরে। পরীক্ষার খাতায় হিসাব মেলানোর সহজ উপায় এটি।
import numpy as np
import matplotlib.pyplot as plt

def sinusoidal(T, d, base=10000.0):
    pos = np.arange(T)[:, None]                # (T, 1)
    w = 1.0 / base ** (np.arange(0, d, 2) / d) # (d/2,)
    pe = np.zeros((T, d))
    pe[:, 0::2] = np.sin(pos * w)
    pe[:, 1::2] = np.cos(pos * w)
    return pe

np.set_printoptions(precision=4, suppress=True, linewidth=60)
print("PE(3), d=4 :", sinusoidal(4, 4)[3])
print("PE(4), d=4 :", sinusoidal(5, 4)[4])

PE = sinusoidal(64, 32)
w = 1.0 / 10000 ** (np.arange(0, 32, 2) / 32)
print("shape:", PE.shape)
print("wavelength of 3 pairs:", (2 * np.pi / w)[[0, 4, 15]].round(1))
norms = np.linalg.norm(PE, axis=1)
print("row norm min, max:", norms.min().round(6), norms.max().round(6))

fig, ax = plt.subplots(figsize=(6.2, 3.2))
im = ax.imshow(PE.T, aspect="auto", cmap="RdBu_r", vmin=-1, vmax=1)
ax.set_xlabel("position t")
ax.set_ylabel("dimension")
ax.set_title("Sinusoidal encoding, d = 32")
fig.colorbar(im, ax=ax)
plt.show()

আউটপুট:

PE(3), d=4 : [ 0.1411 -0.99    0.03    0.9996]
PE(4), d=4 : [-0.7568 -0.6536  0.04    0.9992]
shape: (64, 32)
wavelength of 3 pairs: [    6.3    62.8 35332.9]
row norm min, max: 4.0 4.0

হাতের হিসাব মিলেছে। ছবিতে ওপরের দিকের (নিচু নম্বরের) মাত্রাগুলোতে রং দ্রুত বদলায়, নিচের দিকে প্রায় স্থির: এরা দ্রুত আর ধীর কাঁটা। প্রতিটি সারির দৈর্ঘ্য √(d/২) = ৪, তাই কোনো ঘর অন্যদের চেয়ে “বড়” নয়। ঘর ৩ থেকে ৪-এ দ্রুত কাঁটা অনেকটা বদলেছে, ধীর কাঁটা সামান্য।

10000 কেন? সংখ্যাটির পেছনে কোনো গভীর গণিত আছে বলে paper দাবি করে না; এটি একটি নকশার পছন্দ। সবচেয়ে ধীর কাঁটার wavelength, d বড় হলে, প্রায় 2π·10000 ≈ ৬২,৮০০ ঘর, যা সেই সময়ের অনুবাদের বাক্যের চেয়ে অনেক লম্বা। তাই সাধারণ দৈর্ঘ্যে দুটি ঘরের vector একই হওয়ার ভয় থাকে না। Base অনেক ছোট হলে ধীর কাঁটাও দ্রুত ফিরে আসত। অনেক বড় হলে ধীর কাঁটাগুলো প্রায় নড়তই না, আর সেই মাত্রাগুলো কম তথ্য দিত। Paper-এ token embedding-কে আগে √d দিয়ে গুণ করে তারপর PE যোগ করার কথাও আছে, যাতে দুটির মাপ কাছাকাছি থাকে। অনেক কোডে এই গুণটি বাদ পড়ে, তাই পড়ার সময় মিলিয়ে নিন।

৪ · Dot product আর দূরত্ব — সূত্রটির লুকানো গুণ

দুই ঘর s আর t-এর vector-এর dot product নিন। cos a·cos b + sin a·sin b = cos(a − b) সূত্র জোড়ায় জোড়ায় বসালে:

PE(s) · PE(t) = ∑n cos(ωn(t − s))

ডান দিকে s বা t আলাদা করে নেই, আছে শুধু t − s, মানে relative position. অর্থাৎ ঘর ২০ আর ৩০-এর মিল, ঘর ৫০০ আর ৫১০-এর মিলের সমান। আরেকটি গুণও আছে: k ঘর সরানো একটি linear কাজ, PE(t + k) = Mk·PE(t), যেখানে Mk শুধু k-এর ওপর নির্ভর করে। Paper-এ এই ধর্মটিই সূত্র বেছে নেওয়ার একটি কারণ হিসেবে বলা হয়েছে। নিচে দুটোই মিলিয়ে দেখুন।

import matplotlib.pyplot as plt

PE = sinusoidal(600, 64)
dot = lambda s, t: PE[s] @ PE[t] / 32          # 32 = d/2, তাই সর্বোচ্চ 1
print("similarity (20, 30)  :", round(dot(20, 30), 6))
print("similarity (100, 110):", round(dot(100, 110), 6))
print("similarity (500, 510):", round(dot(500, 510), 6))

k = 7
w = 1.0 / 10000 ** (np.arange(0, 64, 2) / 64)
M = np.zeros((64, 64))
for n, wn in enumerate(w):                     # প্রতি জোড়ায় একটি 2x2 ঘূর্ণন
    c, s = np.cos(wn * k), np.sin(wn * k)
    M[2*n:2*n+2, 2*n:2*n+2] = [[c, s], [-s, c]]
print("max |PE(t+7) - M PE(t)|:", np.abs(PE[7:] - PE[:-7] @ M.T).max().round(12))

fig, ax = plt.subplots(figsize=(6.2, 3.0))
for d in (16, 64):
    P = sinusoidal(301, d)
    ax.plot(P @ P[0] / (d / 2), label=f"d = {d}")
ax.set_xlabel("distance k")
ax.set_ylabel("similarity")
ax.set_title("PE(t) and PE(t + k): similarity vs distance")
ax.legend()
plt.show()

আউটপুট:

similarity (20, 30)  : 0.657863
similarity (100, 110): 0.657863
similarity (500, 510): 0.657863
max |PE(t+7) - M PE(t)|: 0.0

তিনটি মান হুবহু এক, আর সরানোর সূত্র মিলেছে (১২ ঘর দশমিকে)। ছবিতে k = ০-তে similarity ১, কাছের দূরত্বে দ্রুত কমে (d = ৬৪-তে k = ১০-এ ০.৬৫৮), তারপর ঢেউ তুলে ধীরে নামে (k = ৩০০-এ ০.২৯২)। সম্পর্কটি একটানা কমে না: d = ১৬-তে k = ১০-এ ০.৪৫৬ কিন্তু k = ২০-এ ০.৭২২. তাই similarity দেখে দূরত্ব পড়া যায় না, শুধু কাছের ঘর মোটামুটি বেশি মেলে। কম কাঁটায় গড় কম মসৃণ, তাই d = ১৬-এর ঢেউ বড়, আর সে শূন্যের নিচেও নামে। লক্ষ্য করুন, এই নিখুঁত সম্পর্ক শুধু PE-এর নিজেদের মধ্যে। Token embedding যোগ হলে আর শেখা projection বসলে attention score আর শুধু দূরত্বের ফাংশন থাকে না। RoPE ঠিক এই ফাঁকটিই বন্ধ করে।

৫ · Relative position — “কোন ঘরে” নয়, “কত দূরে”

ট্রেনে “আমার সিট ক-৪২” হলো absolute. “আমার তিন সিট পেছনে যিনি বসেছেন” হলো relative. ব্যাকরণের অনেক নিয়ম দ্বিতীয় ধরনের: বিশেষণ বিশেষ্যের ঠিক আগে বসে, সেটি বাক্যের ৩ নম্বর ঘরে হোক বা ৩০০ নম্বরে।

২০১৮ সালে Shaw, Uszkoreit আর Vaswani self-attention-এ সরাসরি দূরত্ব ঢোকানোর একটি পদ্ধতি দেন। প্রতিটি ব্যবধান (j − i) একটি নির্দিষ্ট সীমায় কেটে তার জন্য শেখার যোগ্য vector রাখা হয়, আর সেটি score-এর হিসাবে যোগ হয়। সীমার বাইরের দূরত্বগুলো একই সারি পায়, তাই লম্বা input-এ error আসে না। ধরুন সীমা ২: তাহলে ব্যবধান −৭ আর −২ দুটোই −২ হয়ে যায়, আর টেবিলে থাকে মাত্র ৫টি সারি (−২, −১, ০, ১, ২)। লম্বা input-এ সারির সংখ্যা বাড়ে না। দাম হলো, সীমার বাইরের দূরত্বগুলো model আলাদা করতে পারে না। পরে T5-এর মতো model score-এ দূরত্বভিত্তিক একটি bias যোগ করার সহজ রূপ ব্যবহার করে।

এদের খরচ একটি: score-এর হিসাবের ভেতরে অতিরিক্ত term ঢোকে। প্রতিটি (i, j) জোড়ার জন্য টেবিল থেকে মান আনতে হয়, আর দ্রুত attention কোডের সঙ্গে মেলাতে কিছুটা বাড়তি কাজ লাগে। পরের অংশের RoPE একই ধরনের ফল দেওয়ার চেষ্টা করে শুধু Q আর K বদলে, score-এর সূত্র না ছুঁয়ে।

৬ · RoPE — Q আর K-কে ঘুরিয়ে দেওয়া

RoPE (Rotary Position Embedding) প্রস্তাব করা হয় Su ও সহলেখকদের ২০২১ সালের RoFormer paper-এ। মূল চিন্তা স্কুলের জ্যামিতি: দুটি vector-কে একই কোণে ঘোরালে তাদের dot product বদলায় না। Q আর K-এর প্রতিটি মাত্রা-জোড়াকে ঘোরানো হয়, ঘর t-এর জন্য tθ কোণে, rotation matrix R(a) দিয়ে:

R(iθ)q · R(jθ)k = qᵀ R(iθ)ᵀ R(jθ) k = q · R((j − i)θ) k

এখানে q আর k হলো একটি জোড়ার দুটি করে সংখ্যা, i আর j হলো দুই ঘরের নম্বর। ঘোরানো matrix-এর ধর্ম R(a)ᵀ R(b) = R(b − a) ব্যবহার হয়েছে। ফল: একই q আর k হলে score শুধু j − i-এর ওপর নির্ভর করে। প্রতিটি জোড়ার নিজের θn থাকে, যা sinusoidal-এর ωn-এর মতোই গঠিত। V ঘোরানো হয় না। কোনো নতুন parameter-ও লাগে না।

দুজনে সমান ঘুরলে মাঝের কোণ একই থাকে

Same offset, same angle between q and k (θ = 0.5 rad, values from the stepper below)

Rotating q and k by the same extra angle keeps the angle between them qk qk i = 1, j = 3i = 2, j = 4 কোণ 12.3°কোণ 12.3°
ছবি ৩৪.২ — দিক বদলেছে, মাঝের কোণ বদলায়নি, তাই score একই।

এবার একটি জোড়া হাতে ঘোরান। সংখ্যাগুলো ছোট রাখতে ধরা হলো q = (1, 2), k = (3, 1), আর θ = ০.৫ রেডিয়ান।

R(a)(x, y) = (x·cos a − y·sin a, x·sin a + y·cos a)
হিসাব
ফল
  1. ঘোরানোর আগে. সাধারণ dot product ৫. এতে ঘরের কোনো খবর নেই।
  2. Q ঘোরানো. ঘর i = ১, তাই কোণ ১·০.৫ = ০.৫. q′-এর দৈর্ঘ্য এখনো √৫ = ২.২৩৬১, কারণ ঘূর্ণন দৈর্ঘ্য বদলায় না।
  3. K ঘোরানো. ঘর j = ৩, তাই কোণ ৩·০.৫ = ১.৫. দৈর্ঘ্য এখনো √১০.
  4. Score. ০.০৬৩৮ + ৬.৮৪৫০ ≈ ৬.৯০৮৯. ৫ থেকে বদলেছে, মানে position-এর প্রভাব score-এ ঢুকেছে।
  5. এক ঘর সরানো. i = ২, j = ৪ করলে q′ আর k′ দুটিই অন্য জায়গায়, কিন্তু ২.৪৬৫৫ + ৪.৪৪৩৩ ≈ ৬.৯০৮৯. ব্যবধান ২ একই, score-ও একই।
  6. শর্টকাট. শুধু k-কে (j − i)θ = ১.০ কোণে ঘোরালেও একই ৬.৯০৮৯. আর ব্যবধান ৩ হলে ৫.৩৪১২, মানে দূরত্ব বদলালে score বদলায়।

এবার পুরো Q আর K-তে। “একই q আর k” শর্তটি পরীক্ষায় রাখতে একই token সব ঘরে বসানো হয়েছে। শেষে তুলনার জন্য যোগ করা absolute position, আর আলাদা আলাদা token-ও দেখা হয়েছে।

import numpy as np

def rope(x, base=10000.0):                     # x: (T, d), d জোড় সংখ্যা
    T, d = x.shape
    w = 1.0 / base ** (np.arange(0, d, 2) / d)
    a = np.arange(T)[:, None] * w              # (T, d/2)
    c, s = np.cos(a), np.sin(a)
    out = np.empty_like(x)
    out[:, 0::2] = x[:, 0::2] * c - x[:, 1::2] * s
    out[:, 1::2] = x[:, 0::2] * s + x[:, 1::2] * c
    return out

rng = np.random.default_rng(34)
T, d = 16, 8
q0, k0 = rng.normal(size=(2, d))               # একই token সব ঘরে
Q, K = np.tile(q0, (T, 1)), np.tile(k0, (T, 1))

S = rope(Q) @ rope(K).T                        # (T, T) score
for i, j in [(1, 4), (5, 8), (10, 13), (1, 5), (5, 9)]:
    print(f"score({i:2d},{j:2d}), offset {j-i}: {S[i, j]: .6f}")

spread = max(np.ptp(np.diag(S, k)) for k in range(-T + 1, T))
print("largest spread on any offset, RoPE:", spread.round(12))
print("norm kept:", np.allclose(np.linalg.norm(rope(Q), axis=1), np.linalg.norm(q0)))

pos = rng.normal(size=(T, d))                  # যোগ করা absolute position
A = (Q + pos) @ (K + pos).T
print("largest spread, added positions:", round(max(np.ptp(np.diag(A, k)) for k in range(-T + 1, T)), 4))

Qr, Kr = rng.normal(size=(2, T, d))            # আলাদা আলাদা token
Sr = rope(Qr) @ rope(Kr).T
print("different tokens at each slot:", round(np.ptp(np.diag(Sr, 3)), 4))

আউটপুট:

score( 1, 4), offset 3:  0.359317
score( 5, 8), offset 3:  0.359317
score(10,13), offset 3:  0.359317
score( 1, 5), offset 4:  0.708208
score( 5, 9), offset 4:  0.708208
largest spread on any offset, RoPE: 0.0
norm kept: True
largest spread, added positions: 20.8149
different tokens at each slot: 11.2301

(i, j) আর (i + s, j + s) একই score দিয়েছে, আর সব ব্যবধান মিলিয়ে সবচেয়ে বড় পার্থক্য শূন্য (১২ ঘর দশমিকে)। Position যোগ করলে একই ব্যবধানেও score অনেক ছড়িয়ে যায় (২০.৮১৪৯), কারণ q·pos(j)-এর মতো term আসে। শেষ লাইনটি জরুরি: ঘরে ঘরে token আলাদা হলে score অবশ্যই আলাদা (১১.২৩০১)। ধর্মটি হলো “একই token-জোড়ার score ব্যবধান ছাড়া আর কিছুতে নির্ভর করে না”, এর বেশি নয়।

আরও গভীরে: বাস্তব কোডে RoPE প্রতিটি head-এ আলাদা করে বসে, আর পুরো 2×2 matrix না বানিয়ে উপরের মতো cos আর sin-এর গুণ দিয়ে হিসাব হয়, তাই খরচ O(T·d). Su ও সহলেখকেরা এটিকে complex সংখ্যার গুণ হিসেবেও লেখেন। Score-এর সূত্র না বদলানোয় এটি দ্রুত attention কোডের সঙ্গে সহজে মেলে (পর্ব ৩৭, দক্ষ attention-এ আরও দেখবেন)। KV cache থাকলে নতুন token-এর ঘরের নম্বর আগের দৈর্ঘ্য থেকে গুনতে হয়, নইলে ব্যবধান ভুল হয়ে যায়।

৭ · ALiBi, আর কোন model কী ব্যবহার করে

ALiBi (Press, Smith ও Lewis, ২০২১) সবচেয়ে সরল চিন্তা: কোনো position vector নয়, softmax-এর আগে score থেকে দূরত্বের সমানুপাতিক একটি মান বিয়োগ:

scoreij ← scoreij − m·|i − j|

এখানে m একটি ঢাল, যা প্রতিটি head-এ আলাদা আর শেখা হয় না। ফলে দূরের token-এ attention কমে। কাছের দোকানের কথা আগে মনে পড়ার মতো একটি পক্ষপাত, আর সেটিই এর দুর্বলতাও হতে পারে যখন উত্তর অনেক দূরে থাকে। Paper-টির সারাংশ অনুযায়ী, ১০২৪ token-এ train করা একটি model ২০৪৮ token-এ চালিয়ে ২০৪৮-এ train করা sinusoidal model-এর সমান ফল পাওয়া গিয়েছিল। এটি তাঁদের নির্দিষ্ট পরীক্ষার ফল, সাধারণ নিয়ম নয়।

কোন model কী ব্যবহার করে, সেটি নিজে যাচাই করে নেওয়াই ভালো। নিচের তথ্য ২০২৬ সালের নভেম্বরে পড়া সরকারি নথি থেকে, আর নথি বদলাতে পারে:

Modelপদ্ধতিনথিতে যা লেখা
GPT-2Learned absoluteHugging Face নথি: absolute position embedding, ডানে pad, n_positions default 1024
LlamaRoPEHugging Face নথি: absolute-এর বদলে rotary; config-এ rope_parameters
BLOOMALiBiModel card: ALiBi positional encodings

সাম্প্রতিক অনেক open language model-এর config-এ RoPE-সংক্রান্ত ঘর দেখা যায়, তবে এটি কোনো সার্বজনীন নিয়ম নয়। যে model ব্যবহার করবেন, তার config.json আর model card দেখে নিন। Llama config-এও max_position_embeddings আছে: RoPE-তে টেবিল না থাকলেও একটি ঘোষিত সীমা থাকতে পারে। সেই সীমা ছাড়িয়ে গেলে কী হবে, তা নির্ভর করে ওই সংস্করণের কোডের ওপর, তাই চালিয়ে দেখতে হয়। ছবির জন্য position কীভাবে আলাদা, তা পর্ব ৩৮-এ (Vision Transformer) আসবে।

পুরো পর্বের তুলনা এক জায়গায়। Parameter-এর হিসাবে L হলো max length, d হলো embedding-এর মাত্রা, c হলো ব্যবধান কাটার সীমা, আর dh হলো head-এর মাত্রা, h হলো head-এর সংখ্যা।

পদ্ধতিকোথায় বসেশেখার যোগ্য parameterসীমার বাইরে
LearnedEmbedding-এ যোগL × dIndexError
SinusoidalEmbedding-এ যোগ০মান পাওয়া যায়, ফল অনিশ্চিত
Relative টেবিলScore-এর হিসাবে(2c + 1) × dh (ধরন অনুযায়ী বদলায়)সীমার সারিতেই থাকে
RoPEQ আর K-তে০মান পাওয়া যায়, ফল মাপতে হয়
ALiBiScore থেকে বিয়োগ০ (মূল রূপে)মান পাওয়া যায়, দূরের দিকে attention কমে

একটি উদাহরণে L = ১০২৪ আর d = ৭৬৮ হলে learned টেবিলে ১০২৪ × ৭৬৮ = ৭,৮৬,৪৩২টি parameter. বড় model-এর মোট parameter-এর তুলনায় এটি সামান্য, তাই parameter বাঁচানো পদ্ধতি বাছাইয়ের প্রধান কারণ সাধারণত নয়।

৮ · সত্যিকারের run — ছোটতে শেখা, লম্বায় পরীক্ষা

এতক্ষণ যা দেখলেন তা সূত্রের ধর্ম। Model আসলে নতুন দৈর্ঘ্যে কেমন করে, তার নাম length extrapolation, আর সেটি মেপেই জানতে হয়। পরীক্ষাটি বাজারঘরের অর্ডার নম্বরের মতো এলোমেলো অঙ্কের সারিতে, দুটি কাজ:

  • back3 (relative নিয়ম): প্রতিটি ঘরে বলতে হবে ৩ ঘর আগের অঙ্ক। প্রথম ৩ ঘর ignore_index দিয়ে বাদ।
  • first2 (absolute নিয়ম): প্রতিটি ঘরে বলতে হবে index 2-এর অঙ্ক, মানে শুরু থেকে নির্দিষ্ট একটি ঘর।

Model: PyTorch 2.15.0, Python 3.14-এ, GPU-তে। ২টি pre-norm block, d = ৬৪, ৪টি head, AdamW, ১,৫০০ ধাপ। Training-এর দৈর্ঘ্য T = ৮ থেকে ১৬, পরীক্ষা ১৬ থেকে ১২৮ পর্যন্ত। learned-এর টেবিল ১২৮ সারির, যাতে error না এসে আসল আচরণ দেখা যায়। Block-এর বিস্তারিত পর্ব ৩৫-এ আসবে; এখানে শুধু position-এর অংশটি দেখুন (আসল ফাইল থেকে নেওয়া অংশ, বাকি কোড ছোট করতে বাদ):

def rope(x, base=10000.0):                     # x: (B, heads, T, d_head)
    T, dh = x.shape[-2:]
    w = 1.0 / base ** (torch.arange(0, dh, 2, device=x.device).float() / dh)
    a = torch.arange(T, device=x.device).float()[:, None] * w   # (T, dh/2)
    c, s = a.cos(), a.sin()
    x0, x1 = x[..., 0::2], x[..., 1::2]
    return torch.stack([x0 * c - x1 * s, x0 * s + x1 * c], -1).flatten(-2)

class Attn(nn.Module):
    def forward(self, x):                      # x: (B, T, d)
        B, T, d = x.shape
        q, k, v = (t.view(B, T, self.h, self.dh).transpose(1, 2)
                   for t in self.qkv(x).chunk(3, -1))
        if self.use_rope:
            q, k = rope(q), rope(k)            # শুধু Q আর K ঘোরে, V নয়
        y = F.scaled_dot_product_attention(q, k, v)
        return self.out(y.transpose(1, 2).reshape(B, T, d))

def batch(task, B, T, gen, device):
    ids = torch.randint(0, 10, (B, T), generator=gen)
    if task == "back3":
        y = ids.roll(3, 1)
        y[:, :3] = -100
    elif task == "first2":
        y = ids[:, 2:3].expand(B, T).clone()
    return ids.to(device), y.to(device)

একটি হিসাব আগে পরিষ্কার করা দরকার: T > ১৬ কলামে শুধু নতুন ঘরগুলো (index ১৬ বা তার পরে) গোনা হয়েছে, কারণ প্রশ্নটি নতুন ঘর নিয়েই। প্রতিটি কলামে তিন seed-এর গড়, নিচের সারিতে সর্বনিম্ন-সর্বোচ্চ। অঙ্ক ১০টি, তাই এলোমেলো অনুমানে প্রায় ০.১০ আসার কথা। (সত্যিকারের run, version-এর প্রথম লাইনটি বাদ।)

~/bazarghar-dl$ uv run python run_compare.py back3
kind        T=16    T=32    T=64    T=128   (mean accuracy, min-max over seeds)
none       0.234   0.195   0.160   0.143
  range    0.23-0.24 0.19-0.20 0.16-0.16 0.14-0.14
learned    1.000   0.132   0.124   0.114
  range    1.00-1.00 0.13-0.14 0.12-0.13 0.11-0.11
sinusoidal 1.000   0.131   0.136   0.117
  range    1.00-1.00 0.11-0.17 0.13-0.14 0.11-0.12
rope       1.000   0.824   0.669   0.591
  range    1.00-1.00 0.78-0.89 0.62-0.73 0.57-0.63
total 178 s

~/bazarghar-dl$ uv run python run_compare.py first2
kind        T=16    T=32    T=64    T=128   (mean accuracy, min-max over seeds)
none       0.240   0.165   0.148   0.105
  range    0.24-0.24 0.17-0.17 0.14-0.15 0.10-0.11
learned    1.000   1.000   1.000   1.000
  range    1.00-1.00 1.00-1.00 1.00-1.00 1.00-1.00
sinusoidal 1.000   1.000   1.000   0.620
  range    1.00-1.00 1.00-1.00 1.00-1.00 0.30-1.00
rope       1.000   0.532   0.114   0.114
  range    1.00-1.00 0.40-0.64 0.08-0.13 0.11-0.12
total 354 s

(সময়টি অন্য কাজের সঙ্গে ভাগ করা GPU-তে, তুলনার যোগ্য নয়।) ফলগুলো একটি একটি করে পড়ুন:

যা দেখা গেলব্যাখ্যা
none কোনো দৈর্ঘ্যেই শেখেনি (T = ১৬-তে প্রায় ০.২৪, T = ১২৮-এ ০.১০৫ থেকে ০.১৪৩)Position ছাড়া এই ক্রমের নিয়ম শেখা যায়নি। ০.১০-এর চেয়ে কিছুটা বেশি হওয়ার সম্ভাব্য কারণ, উত্তরটি সারির ভেতরের অঙ্কগুলোর একটি।
back3: learned ১.০০০ থেকে ০.১৩২নতুন ঘরের সারি শেখেনি, ঠিক স্টেপারের মতো।
back3: sinusoidal-ও ১.০০০ থেকে ০.১৩১সূত্র মান দেয়, শেখা দেয় না। Network এই vector-গুলো আগে দেখেনি।
back3: rope ০.৮২৪, ০.৬৬৯, ০.৫৯১সবচেয়ে ভালো, কিন্তু নিখুঁত নয়। নিচে কারণ মাপা হয়েছে।
first2: learned ১.০০০ সব জায়গায়অবাক করার মতো! উত্তর থাকে index 2-এ, যার সারি শেখা হয়েছে; তাই আমাদের ব্যাখ্যা, নতুন ঘরের সারি এখানে তেমন বাধা হয়নি।
first2: sinusoidal T = ১২৮-এ ০.৬২০ (০.৩০ থেকে ১.০০)এক seed-এ নিখুঁত, আরেকটিতে ০.৩০. একটি seed দেখে সিদ্ধান্ত নিলে ভুল হতো।
first2: rope ০.৫৩২, ০.১১৪, ০.১১৪Absolute নিয়মে RoPE ভেঙে পড়ে। T = ১৬-তে ১.০০০ কেন, সেটি নিচে।

RoPE back3-এ ০.৫৯১-এ নামল কেন, নতুন ঘরের জন্য, নাকি অন্য কিছু? “কারণ” অনুমান না করে তিনটি মাপ নেওয়া হলো:

~/bazarghar-dl$ uv run python run_diag.py
(1) accuracy by position band, mean of 3 seeds
T     pos 3-15   pos 16+
16    1.000
32    0.855   0.824
64    0.829   0.669
128   0.656   0.591
(2) attention mass of the last token, block 1, mean of heads and seeds
T     offset=3   offset<=15  offset>15
16    0.336      1.000       0.000
32    0.181      0.482       0.518
64    0.104      0.267       0.733
128   0.061      0.144       0.856
(3) same rope model, trained on T=8..64 instead of 8..16
T=16   mean 1.000  min 1.000  max 1.000
T=32   mean 1.000  min 1.000  max 1.000
T=64   mean 1.000  min 1.000  max 1.000
T=128  mean 1.000  min 1.000  max 1.000

(১) পুরোনো ঘর ৩–১৫-ও T = ১২৮-এ ০.৬৫৬-এ নেমেছে। তাই সমস্যা নতুন ঘর নয়। (২) প্রথম block-এ শেষ token-এর attention-এর ০.৮৫৬ অংশ চলে যাচ্ছে ১৫ ঘরের চেয়ে দূরের token-এ, যে ব্যবধান training-এ কখনো আসেনি। RoPE ব্যবধানের ফাংশন ঠিকই, কিন্তু অদেখা ব্যবধানে network কী করবে তা training ঠিক করে দেয়নি। (৩) Training-এ T = ৬৪ পর্যন্ত দিতেই একই RoPE model T = ১২৮-এও ১.০০০ করেছে। এই তিনটি পরীক্ষা একসঙ্গে এই ব্যাখ্যাকেই সমর্থন করে: অদেখা দূরত্বই বাধা, ঘরের নম্বর নয়।

আর first2-তে T = ১৬-তে RoPE ১.০০০ পেল কীভাবে, যেখানে ঘরের নম্বর সে সরাসরি দেখে না? অনুমান ছিল, দুই স্তর লাগবে:

~/bazarghar-dl$ uv run python run_layers.py
rope, layers=1, first2, T=16: 0.943 0.922 0.931
rope, layers=2, first2, T=16: 1.000 1.000 1.000

অনুমানটি পুরো মেলেনি: এক স্তরেই প্রায় ০.৯৩. একটি সম্ভাব্য ব্যাখ্যা হলো, sequence-এর শুরুর সীমাটি নিজেই একটি সংকেত, কারণ ঘর ২-এর বাঁয়ে দুটির বেশি token নেই। এই run তা প্রমাণ করে না; আমরা শুধু ফলটি জানাচ্ছি।

মূল ধারণা

এই একটি স্থানীয় পরীক্ষায় কোনো পদ্ধতি সব কাজে সেরা হয়নি। কাজটি relative হলে RoPE এগিয়ে, absolute হলে পিছিয়ে। ছোট model আর অঙ্কের খেলনা কাজের ফল বড় language model-এ একইভাবে খাটবে, এমন দাবি করা যায় না। খাটে পদ্ধতিটি: নিজের কাজে ছোটতে train করুন, লম্বায় মাপুন, কয়েকটি seed-এ, আর “নতুন ঘর” আলাদা করে গুনুন।

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

  • Transformer-এ position কেন লাগে? Position ছাড়া self-attention permutation equivariant, তাই ক্রম আলাদা করতে পারে না।
  • PE(3) বের করুন, d = 4. [sin 3, cos 3, sin 0.03, cos 0.03] = [০.১৪১১, −০.৯৯০০, ০.০৩০০, ০.৯৯৯৬]; দৈর্ঘ্য² = ২.
  • Learned আর sinusoidal-এর পার্থক্য? প্রথমটিতে (max_len × d) parameter আর কড়া সীমা; দ্বিতীয়টিতে parameter নেই, মান সব ঘরে পাওয়া যায়, কিন্তু accuracy-র নিশ্চয়তা নেই।
  • RoPE কোন কোন vector ঘোরায়? শুধু Q আর K; V নয়। একই token-জোড়ার score শুধু j − i-এর ফাংশন হয়।
  • ALiBi কোথায় বসে? Softmax-এর আগে score থেকে m·|i − j| বিয়োগ করে; কোনো position vector যোগ করে না।

৯ · AI-এর যুগে — position-এর কোড যাচাই

AI সহকারী RoPE বা PE-র কোড মুহূর্তেই লিখে দেয়। ভুল থাকলেও কোড সাধারণত চলে, কারণ ভুল shape-এর চেয়ে ভুল গণিত বেশি হয়। এই ধরনের ভুল চোখে পড়ে না, তাই সংখ্যা দিয়ে মেলাতে হয়।

AI-এর সাধারণ ভুলকীভাবে ধরবেন
RoPE V-তেও বসানো, অথবা শুধু K-তেএকই token-এ (i, j) আর (i + s, j + s)-এর score মেলান
Multi-head-এ ভাগ করার আগে পুরো d-তে RoPE, অথচ দাবি head-প্রতিঘোরানোর আগে আর পরে shape আর θ-এর সংখ্যা ছাপান
KV cache-এ ঘরের নম্বর প্রতিবার ০ থেকে শুরুCache-সহ আর cache-ছাড়া logits মিলিয়ে দেখুন
পুরোনো transformers-এর argument (যেমন পুরোনো RoPE config নাম)নিজের version-এর নথি পড়ুন, config ছাপান
“Sinusoidal যেকোনো দৈর্ঘ্যে কাজ করে” জাতীয় দাবিছোটতে train করে লম্বায় মাপুন, একাধিক seed-এ
Paper-এর নাম, বছর বা ফল বানিয়ে বলাarXiv-এ গিয়ে মূল সারাংশ পড়ুন

নিচের prompt-এ কোড লিখিয়ে নেওয়ার সঙ্গে যাচাইও চাওয়া হয়েছে। AI-এর দেওয়া পরীক্ষাটিও নিজে চালিয়ে দেখবেন; তার দেওয়া আউটপুট বিশ্বাস করে নয়।

আমি PyTorch 2.x-এ একটি ছোট Transformer-এ RoPE যোগ করতে চাই। বাংলায় বোঝাও। ১. Multi-head-এ ভাগ করার পরে Q আর K-তে RoPE বসানোর কোড দাও, প্রতিটি লাইনে tensor-এর shape লিখে। ২. V কেন ঘোরে না, দুই লাইনে বলো। ৩. একটি ছোট test দাও: একই token সব ঘরে বসিয়ে score(i, j) আর score(i+s, j+s) মেলাবে। ৪. KV cache থাকলে ঘরের নম্বর কীভাবে গুনতে হবে, দেখাও। ৫. কোনো paper-এর কথা বললে লেখক আর বছর দাও, আর নিশ্চিত না হলে সেটা বলো। আউটপুট বানিয়ে দিও না।

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

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

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

প্র ০১২০১৭-র paper sinusoidal বেছে নিয়েছিল লম্বা sequence-এ কাজ করার আশায়। তাহলে পরের অনেক model আবার learned-এ গেল কেন?

Vaswani ও সহলেখকেরা দুটোই চেষ্টা করেছিলেন, আর তাঁদের পরীক্ষায় ফল প্রায় একই ছিল। তাঁরা সূত্রটি বেছে নেন এই অনুমানে যে এটি হয়তো training-এর চেয়ে লম্বা sequence-এ কাজে লাগতে পারে। লক্ষ্য করুন, ভাষাটি অনুমানের, প্রমাণের নয়।

পরে BERT আর GPT-পরিবারের model learned ব্যবহার করে, কারণ বাস্তবে সুবিধাটি বড় ছিল না। এসব model নির্দিষ্ট একটি context length-এ train হতো, আর সেই সীমার ভেতরেই চলত। সীমার ভেতরে learned টেবিল প্রতিটি ঘরের জন্য যা দরকার তাই শিখতে পারে, আর সূত্রের মতো কোনো আগে-ঠিক-করা আকৃতি মানতে হয় না। আমাদের ছোট পরীক্ষাতেও sinusoidal নতুন ঘরে back3-এ ০.১৩১, learned ০.১৩২: মান পাওয়া গেছে, কাজ হয়নি।

কেন? Network শেখে নির্দিষ্ট কিছু vector-এর ওপর একটি ফাংশন। Index ১৭-এর sinusoidal vector আগের ১৬টির কাছাকাছি হলেও, ধীর কাঁটাগুলো এমন মানে পৌঁছায় যা training-এ আসেনি। তাই এটি একটি নতুন input, আর শেখা ফাংশন সেখানে কী করবে তার নিশ্চয়তা নেই। ঠিক যেমন একটি regression মডেল নিজের দেখা সীমার বাইরে অনুমান করলে ভুল করতে পারে। এই সমস্যার ওপরই পরে relative পদ্ধতিগুলো দাঁড়ায়: দূরত্ব একই থাকলে ঘর বদলালেও input একই দেখায়।

তবে এর মানে এই নয় যে sinusoidal ফেলনা। পরামিতি শূন্য, কোডে সহজ, আর অনেক ছোট কাজে বা শেখানোর জন্য ভালো শুরু। Sequence-এর দৈর্ঘ্য training আর ব্যবহারে একই থাকলে পার্থক্যও অনেক সময় কম।

মূল উপলব্ধি: “এই সূত্র যেকোনো দৈর্ঘ্যে মান দেয়” আর “model যেকোনো দৈর্ঘ্যে ঠিক কাজ করে” দুটি আলাদা বাক্য। প্রথমটি গণিত, দ্বিতীয়টি পরীক্ষার বিষয়।

প্র ০২Position vector token embedding-এর সঙ্গে যোগ করলে দুটো মিশে যায় না? Concatenate করলেই তো পরিষ্কার হতো?

মিশে যায়, আর এই আশঙ্কাটি যৌক্তিক। x = E + P থেকে আলাদা করে E আর P ফেরত পাওয়ার কোনো সরল সূত্র নেই। তবু যোগই চালু, কয়েকটি কারণে।

প্রথম কারণ উচ্চ মাত্রার জ্যামিতি। মাত্রা বড় হলে এলোমেলো vector-গুলো প্রায় পরস্পর লম্ব হয়। তাই E-এর টেবিল আর P-এর টেবিল মোটামুটি আলাদা দিকে থাকতে পারে, আর পরের শেখা projection (WQ ইত্যাদি) যে দিকটি দরকার সেটি বেছে নিতে পারে। AI-এর ভিত্তি কোর্সের vector-এর পর্বে dot product নিয়ে যা দেখেছেন, এই যুক্তি তার ওপরই দাঁড়ানো। এটি একটি যুক্তিসঙ্গত ব্যাখ্যা, কোনো কড়া প্রমাণ নয়।

দ্বিতীয় কারণ খরচ। d = ৬৪-এর সঙ্গে আরও ৬৪ মাত্রা জুড়লে input ১২৮ হয়। তখন Q, K আর V-এর projection-এর parameter বাড়ে, অথবা আগে একটি কমানোর স্তর লাগে। ধরা যাক প্রতিটি projection d × d: তাহলে ৬৪² = ৪,০৯৬ থেকে ১২৮ × ৬৪ = ৮,১৯২ হয়, মানে দ্বিগুণ, আর শুধু একটি জিনিস রাখার জন্য। তৃতীয়ত, ২০১৭-র paper আর পরের অনেক কাজে যোগই ভালো ফল দিয়েছে, তাই পরিবর্তনের চাপ তৈরি হয়নি।

অন্যদিকে যোগের একটি সত্যিকারের দাম আছে, যা অংশ ৬-এর কোডে দেখেছেন: score-এ q·pos(j)-এর মতো মিশ্র term আসে, যা একই দূরত্বেও score বদলায়। সংখ্যায় স্প্রেড ছিল ২০.৮১৪৯, যেখানে RoPE-তে শূন্য। RoPE আর ALiBi এই সমস্যা এড়ায় embedding-কে একদম না ছুঁয়ে। এদের সুবিধা তাই শুধু লম্বা sequence নয়, token-এর অর্থ আর position-এর হিসাব আলাদা থাকাও।

মূল উপলব্ধি: প্রথম দেখায় “অশুদ্ধ” মনে হওয়া নকশাও কাজ করতে পারে, যদি পরের স্তর আলাদা করে নেওয়ার ক্ষমতা রাখে। কিন্তু সেই মিশ্রণের দাম মাপা যায়, আর পরের পদ্ধতিগুলো সেটিই কমানোর চেষ্টা।

প্র ০৩Shaw-এর relative টেবিলও “দূরত্ব” দেখে, RoPE-ও। তাহলে অনেক language model RoPE-র দিকে কেন গেল?

দুটোই একই ধারণার দুটি প্রয়োগ, তাই পার্থক্য প্রকৌশলের। Shaw ও সহলেখকদের ২০১৮ সালের পদ্ধতিতে ব্যবধানকে একটি সীমায় কাটা হয়, আর প্রতিটি মানের জন্য শেখার যোগ্য vector থাকে। তাঁরা WMT ২০১৪ অনুবাদে BLEU-তে উন্নতি রিপোর্ট করেন। সমস্যা হলো হিসাবের ভেতরের term: প্রতিটি (i, j)-এর জন্য টেবিল থেকে আলাদা vector, তাই attention কোডটি আর কেবল “QKᵀ, softmax, V” থাকে না।

RoPE-র প্রথম সুবিধা: কোনো নতুন parameter নেই, আর কাজটি আগেই হয়ে যায়, Q আর K-তে। তারপর একদম সাধারণ scaled dot-product attention চলে। তাই hardware-এর জন্য তৈরি দ্রুত attention কোড অপরিবর্তিত থাকে। অনেক বড় model-এ এটি বড় ব্যবহারিক বিষয়। দ্বিতীয় সুবিধা: নানা ব্যবধানের জন্য টেবিলের সীমা বেছে নিতে হয় না। সূত্র প্রতিটি ব্যবধানে আলাদা মান দেয়। তৃতীয়ত, Su ও সহলেখকেরা লম্বা টেক্সটের classification-এ RoFormer-এর ভালো ফল রিপোর্ট করেন, আর Hugging Face-এর Llama নথিও এটিকে লম্বা sequence ভালোভাবে সামলানোর উদ্দেশ্যে absolute-এর বিকল্প হিসেবে বর্ণনা করে।

তবু এটিকে “সবচেয়ে ভালো” বলা ঠিক নয়। BLOOM বেছে নিয়েছে ALiBi. আর Kazemnejad ও সহলেখকদের ২০২৩ সালের কাজ reasoning আর গণিতের কাজে দেখিয়েছে, ALiBi, Rotary আর absolute তিনটিই লম্বা context-এ দুর্বল হতে পারে। আমাদের পরীক্ষাতেও absolute নিয়মে RoPE ০.১১৪-এ নেমেছে। কোন পদ্ধতি কেন ব্যাপক হলো, তা বলতে গেলে নকল করার সহজতা, জনপ্রিয় code base আর পরীক্ষার ফল সবই মিশে আছে, আর একক কারণ দেখানো কঠিন। তাই “সবাই এটা ব্যবহার করে” যুক্তিটি নিজের কাজের প্রমাণ হিসেবে যথেষ্ট নয়।

মূল উপলব্ধি: একটি পদ্ধতি শুধু accuracy-র জোরে জেতে না। কোড সরল থাকে কি না, আগের অপ্টিমাইজেশন কাজে লাগে কি না, এসবও সিদ্ধান্তে ভারী হয়।

প্র ০৪একটি গবেষণায় position encoding একদম ছাড়া decoder সবচেয়ে ভালো করেছে। তাহলে position কি আসলেই দরকার?

প্রশ্নের গবেষণাটি হলো Kazemnejad ও সহলেখকদের ২০২৩ সালের কাজ। সারাংশ অনুযায়ী, decoder-only Transformer-এ তাঁরা absolute, T5-ধাঁচের relative, ALiBi আর Rotary পদ্ধতির সঙ্গে “কোনো position নয়” তুলনা করেন, reasoning আর গণিতের কাজে, ছোট context থেকে বড় context-এ। সেখানে “কোনো position নয়” সবচেয়ে ভালো করেছে।

এই ফল প্রথমে অংশ ১-এর সঙ্গে সাংঘর্ষিক মনে হয়। সমাধান একটি ছোট শব্দে: decoder. Decoder-এ causal mask থাকে, মানে প্রতিটি token শুধু আগের token দেখে (পর্ব ৩৩)। ঘর t কতগুলো token দেখতে পারে, সেটিই ঘরের নম্বরের একটি পরোক্ষ খবর। তাই mask ক্রম ভেঙে দেয়, আর permutation equivariance আর পুরো খাটে না। লেখকেরাও তাত্ত্বিকভাবে দেখান, এমন model absolute আর relative দুটোই প্রকাশ করতে পারে।

আমাদের পরীক্ষা ছিল causal mask ছাড়া, এবং সেখানে none কোনো কাজ শেখেনি। তাই দুটি ফল একসঙ্গে সত্য হতে পারে: mask-ছাড়া encoder-এ position লাগে, আর mask-সহ decoder-এ কিছু কাজে explicit position ছাড়াই চলতে পারে। একই কারণে মাল্টিহেড বা স্তরের সংখ্যা নয়, mask আছে কি না, সেটিই এখানে প্রধান প্রশ্ন। আবার এটিকে “position বাদ দিন” হিসেবে পড়াও ঠিক হবে না। তাঁদের ফল নির্দিষ্ট কাজের, আর সাধারণ ভাষার ওপর train করা বড় model-এর জন্য এটি সরাসরি প্রমাণ নয়। জনপ্রিয় বড় model-গুলো এখনও explicit position রাখে, কারণ তাদের নিজেদের পরীক্ষায় সেটি কাজে লেগেছে বলে ধরা যায়।

নিজের কাজে যাচাই করা সহজ: একই model position-সহ আর position-ছাড়া train করুন, একই seed-এ, একই দৈর্ঘ্যে, আর নতুন ঘর আলাদা করে মাপুন। তারপর সিদ্ধান্ত নিন।

মূল উপলব্ধি: কোনো ফল পড়ার সময় মডেলের কাঠামো, কাজের ধরন আর দৈর্ঘ্যের সীমা একসঙ্গে পড়ুন। একটি শব্দ, এখানে “decoder”, পুরো সিদ্ধান্ত বদলে দিতে পারে।

প্র ০৫বাজারঘরের model ২৫৬ token-এ train হয়েছে, এখন ১,০২৪ token-এর policy নথি পড়াতে হবে। প্রকৌশলী হিসেবে আপনি কী করবেন?

প্রথমে জানা দরকার model কী ব্যবহার করে, কারণ ঝুঁকিগুলো আলাদা। Learned হলে আর টেবিল ২৫৬ সারির হলে সরাসরি error আসবে, যা অন্তত সৎ। টেবিল বড় করে নতুন সারি এলোমেলো রাখলে আরও খারাপ: কোনো error নেই, ফল চুপচাপ খারাপ, যেমন স্টেপারে দেখেছেন। Sinusoidal বা RoPE হলে কোড চলবে, আর ঠিক এখানেই আত্মতুষ্টির বিপদ।

দ্বিতীয় ধাপ মাপা। নিজের লেবেল করা কিছু লম্বা নথিতে ফল মাপুন, আর ছোট নথির ফলের সঙ্গে তুলনা করুন। নথির শুরু, মাঝ আর শেষের তথ্য নিয়ে আলাদা প্রশ্ন রাখুন, কারণ আমাদের run দেখিয়েছে পুরোনো ঘরও (৩–১৫) লম্বা input-এ ০.৬৫৬-এ নেমে আসতে পারে।

তৃতীয় ধাপ বিকল্প। (ক) নথি টুকরো করে আলাদা আলাদা পড়ানো, তারপর ফল জোড়া: সরল ও সস্তা, কিন্তু দুই টুকরোর মধ্যের সম্পর্ক হারায়। (খ) ১,০২৪ পর্যন্ত দৈর্ঘ্যে আবার train বা fine-tune: আমাদের ছোট পরীক্ষায় লম্বা training-এ RoPE ১.০০০-এ ফিরেছে, যদিও বড় model-এ এর খরচ অনেক। (গ) Learned টেবিল interpolate করা: Vision Transformer-এ এই কৌশল প্রচলিত, তবে টেক্সটে এর ফল নিজে মাপা দরকার। (ঘ) শুরু থেকেই লম্বা context-এ train করা অন্য model নেওয়া।

খরচের কথাও মাথায় রাখুন। Attention-এর সময় আর মেমরি T-এর সঙ্গে দ্রুত বাড়ে (পর্ব ৩৭ · দক্ষ attention), আর দৈর্ঘ্য চারগুণ হলে শুধু score matrix ১৬ গুণ বড়। সিদ্ধান্তটি তাই একটি ডেটার মাপ আর খরচের হিসাব, কোনো পছন্দের পদ্ধতির নাম নয়। ফল জানিয়ে দেওয়ার সময়ে সীমাটিও লিখে রাখুন, যাতে ব্যবহারকারী জানেন কতটা লম্বা নথিতে এটি পরীক্ষিত।

মূল উপলব্ধি: কোড চলা আর ফল ঠিক থাকা আলাদা। দৈর্ঘ্য বাড়ানো একটি distribution shift, আর তাকে সেভাবেই মেপে ঠিক করতে হয়।

অনুশীলন

প্রথমে নিজে করুন, তারপর উত্তর মেলান।

  1. হাতে হিসাব: d = ৪ ধরে PE(2) বের করুন, আর দৈর্ঘ্য² মিলিয়ে দেখুন।

    n = ০: ω = ১, কোণ ২. sin ২ = ০.৯০৯৩, cos ২ = −০.৪১৬১. n = ১: ω = ০.০১, কোণ ০.০২. sin ০.০২ ≈ ০.০২০০, cos ০.০২ ≈ ০.৯৯৯৮.

    PE(2) = [০.৯০৯৩, −০.৪১৬১, ০.০২০০, ০.৯৯৯৮]. দৈর্ঘ্য² = (০.৮২৬৮ + ০.১৭৩২) + (০.০০০৪ + ০.৯৯৯৬) = ২, যা d/২ এর সমান।

  2. আউটপুট অনুমান করুন: অংশ ৬-এর rope ফাংশনে x = np.array([[1., 0.], [1., 0.]]) দিলে (T = ২, d = ২) ফল কী হবে? আগে হাতে লিখুন, তারপর চালিয়ে মিলিয়ে দেখুন।

    d = ২-তে একটিই জোড়া, ω = ১. ঘর ০-এ কোণ ০, তাই সারিটি বদলায় না: [1, 0]. ঘর ১-এ কোণ ১ রেডিয়ান: (1·cos 1 − 0, 1·sin 1 + 0) = [০.৫৪০৩, ০.৮৪১৫].

    দুই সারির দৈর্ঘ্যই ১. প্রথম ঘর কখনো ঘোরে না, এটি মনে রাখলে KV cache-এর ভুলও ধরা সহজ হয়।

  3. ALiBi হাতে আর কোডে: T = ৪, সব score ০, m = ১ ধরে query ঘর ০-এর attention weight বের করুন। তারপর NumPy-তে কয়েক লাইনে লিখুন।

    Score হয় [0, −1, −2, −3]. e-এর ঘাতে [১, ০.৩৬৭৯, ০.১৩৫৩, ০.০৪৯৮], যোগফল ১.৫৫৩০. Weight ≈ [০.৬৪৩৯, ০.২৩৬৯, ০.০৮৭১, ০.০৩২১].

    import numpy as np
    T, m = 4, 1.0
    i, j = np.arange(T)[:, None], np.arange(T)[None, :]
    S = np.zeros((T, T)) - m * np.abs(i - j)
    A = np.exp(S) / np.exp(S).sum(-1, keepdims=True)
    print(A[0].round(4))

    দূরত্ব বাড়লে weight দ্রুত কমে। m বড় করলে model আরও কাছের দিকে ঝোঁকে। উত্তরটি দূরে থাকলে এই পক্ষপাত ক্ষতি করতে পারে।

  4. নকশা সমালোচনা: একজন সহকর্মী বলছেন: “আমাদের classifier-এর learned টেবিল ২৫৬ সারির। ১,০২৪ সারির করে দিই, কোড চলবে, ঝামেলা শেষ। Training-এর দরকার নেই।” এই পরিকল্পনার সমস্যা কোথায়, আর আপনি কী বলবেন?

    কোড চলবে, আর সেটিই ফাঁদ। ২৫৭ নম্বর থেকে সারিগুলো শেখা হয়নি, তাই নতুন ঘরের token এলোমেলো vector পাবে। আমাদের run-এ relative কাজে নতুন ঘরে accuracy ১.০০০ থেকে ০.১৩২-এ নেমেছিল। পুরোনো checkpoint লোড করতেও টেবিলের মাপ না মেলায় সমস্যা হতে পারে।

    প্রস্তাব: আগে লম্বা নথির ছোট একটি লেবেল করা সেটে মাপুন; নথি টুকরো করা, টেবিল interpolate করে আবার fine-tune করা, আর লম্বা দৈর্ঘ্যে নতুন train, এই তিনটি একই সেটে তুলনা করুন। ফল তিন seed-এ দেখে সিদ্ধান্ত নিন।

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

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

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

  1. Token থেকে embedding, কোনো position যোগ ছাড়াই
  2. Embedding থেকে Q, K আর V তৈরি করে head-এ ভাগ করা
  3. ঘরের নম্বর অনুযায়ী শুধু Q আর K ঘোরানো
  4. Q·Kᵀ দিয়ে score, তারপর softmax
  5. Softmax-এর weight দিয়ে V-এর ভারিত গড়

Causal mask আর position encoding ছাড়া একটি self-attention স্তরে input token-গুলোর ক্রম বদলালে কী হয়?

  • ত্রুটি দেখিয়ে থেমে যায়
  • Output-গুলোও একইভাবে সাজানো বদলায়, মান একই থাকে
  • প্রতিটি token-এর output-এর মান পুরো বদলে যায়
  • Output সবসময় শূন্য হয়

d = 4 ধরে sinusoidal encoding-এ PE(3)-এর চারটি মান (sin, cos, sin, cos) কোনটি?

  • [0.0523, 0.9986, 0.0005, 1.0000]
  • [0.1411, -0.9900, 0.1411, -0.9900]
  • [0.1411, -0.9900, 0.0300, 0.9996]
  • [-0.9900, 0.1411, 0.9996, 0.0300]

max_len = 128-এর একটি learned position টেবিল শুধু T ≤ 16-এর sequence-এ train হলো। T = 64-এর input দিলে সাধারণত কী হয়?

  • IndexError আসে
  • টেবিল নিজে থেকে বড় হয়ে যায়
  • Index 16-এর পরের সারিগুলোও training-এ শিখে গেছে, তাই সব ঠিক থাকে
  • Error আসে না, কিন্তু নতুন ঘরগুলো শুরুর এলোমেলো সারি পায়

RoPE-তে q আর k-কে যথাক্রমে iθ আর jθ কোণে ঘোরালে, একই q আর k-এর জন্য dot product কীসের ওপর নির্ভর করে?

  • শুধু j − i-এর ওপর (q আর k ছাড়া)
  • শুধু i-এর ওপর
  • শুধু i + j-এর ওপর
  • i বা j কোনোটির ওপরই নয়

নিচের কোনটি RoPE আর ALiBi-র সঠিক তুলনা?

  • দুটিই token embedding-এ যোগ হয়
  • RoPE Q আর K ঘোরায়, ALiBi score থেকে দূরত্বের সমানুপাতিক মান বিয়োগ করে
  • ALiBi Q আর K ঘোরায়, RoPE score থেকে দূরত্ব বিয়োগ করে
  • দুটিই প্রতিটি ঘরের জন্য শেখার যোগ্য টেবিল রাখে

T = 8–16-এ train করা একটি RoPE model “৩ ঘর আগের অঙ্ক” কাজে T = 128-এ পুরোনো ঘরেও accuracy হারাল। প্রথম block-এর attention weight দেখা গেল বেশিরভাগ চলে যাচ্ছে 15-এর চেয়ে দূরের token-এ। সবচেয়ে যুক্তিসঙ্গত পরবর্তী পরীক্ষা কোনটি?

  • RoPE-এর base 10000 বদলে 100 করে আবার একই training
  • T = 256-এ পরীক্ষা করে দেখা, আরও লম্বায় ঠিক হয়ে যায় কি না
  • Training-এ দৈর্ঘ্য বাড়িয়ে (যেমন T = 8–64) আবার একই পরীক্ষা করা
  • RoPE V-তেও বসানো

শব্দকোষ

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

মনে রাখুন

  1. Mask বা position ছাড়া self-attention ক্রম আলাদা করতে পারে না; দুই review-এর গড় vector হুবহু এক।
  2. Learned: সহজ, কিন্তু সীমার বাইরে error, আর অদেখা সারি এলোমেলো।
  3. Sinusoidal: সূত্রে মান, PE(s)·PE(t) শুধু t − s-এর ফাংশন; তবু নতুন ঘরে accuracy-র নিশ্চয়তা নেই।
  4. RoPE: শুধু Q আর K ঘোরে; একই token-জোড়ার score শুধু ব্যবধানে নির্ভর করে। ALiBi score থেকে দূরত্ব বিয়োগ করে।
  5. সেরা পদ্ধতি কাজের ওপর নির্ভর করে। ছোটতে train, লম্বায় মাপ, নতুন ঘর আলাদা, কয়েকটি seed.

পরের পর্বে: Attention আর position হাতে এল। এবার এগুলোকে residual, LayerNorm আর feed-forward network-এর সঙ্গে জুড়ে একটি পুরো Transformer block বানাবেন, আর প্রতিটি অংশের parameter গুনবেন।

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

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