Part 04 · LLM Internals
Embeddings and Positional Encoding
প্রতিটা token id একটা vector হয়ে যায় যেটা তার অর্থ বহন করে, সাথে sentence এ সে কোথায় বসে আছে তার একটা signal, কারণ attention নিজে থেকে ক্রম বোঝে না।
⏱ 1-2 ঘণ্টা · Station 25 / 92
জিনিসটা কী, কেন দরকার
Tokenizer তোমার text কে 2, 4 এর মতো id বানিয়েছে। কিন্তু id 2 id 1 এর চেয়ে "বড়" না, আর সংখ্যা 2 থেকে বোঝার উপায় নেই যে "order" শব্দটা "rider" এর চেয়ে "parcel" এর বেশি কাছে। Id শুধু row নম্বর। Transformer কিছু করার আগে প্রতিটা id কে এমন একটা vector বানাতে হয় যেটা অর্থ বহন করে, মানে একটা embedding।
আরেকটা ফাঁকও আছে। Attention (Part 3) প্রতিটা শব্দকে অন্য প্রতিটা শব্দের সাথে তুলনা করে, কিন্তু কোনটা আগে এসেছে জানে না: "rider is late" আর "late is rider" তার কাছে একদম একই। তাই model একটা positional encoding ও যোগ করে, প্রতিটা token কোথায় বসে আছে তার একটা signal। এই দুইটা vector, অর্থ আর অবস্থান, model এর বাকি অংশ শুধু এগুলোই দেখে।
ভেতরে কীভাবে চলে
Embedding table। একটা matrix, vocabulary র প্রতিটা token এর জন্য একটা row আর d টা column (GPT-2 small এ d = 768, বড় model এ কয়েক হাজার)। Token id 2 এর embedding মানে row 2 নেওয়া। ব্যস: একটা lookup, অঙ্কের হিসাবে একটা one-hot vector কে table দিয়ে গুণ করার সমান। Row গুলো random দিয়ে শুরু হয় আর অন্য যেকোনো weight এর মতো training এ শেখা হয়, তাই যেসব token একই রকম জায়গায় ব্যবহার হয় তাদের row কাছাকাছি হয়ে যায়। এভাবেই "order" গিয়ে "parcel" এর কাছে বসে।
Attention এর position কেন লাগে। Token যে ক্রমেই আসুক, self-attention একই weight হিসাব করে; input এলোমেলো করলে প্রতিটা token একই output পায়, শুধু এলোমেলো ক্রমে। কাউকে model কে বলতে হবে "এই token টা তৃতীয়"।
Sinusoidal encoding (2017 এর আসল transformer): position p এর জন্য আলাদা আলাদা frequency র sine আর cosine ঢেউয়ের একটা নির্দিষ্ট vector, embedding এর সাথে যোগ হয়। প্রতিটা position একটা অনন্য pattern পায়, আর কাছাকাছি position কাছাকাছি pattern।
শেখা position (GPT-2, BERT): দ্বিতীয় একটা table, প্রতিটা position এর জন্য একটা শেখা row, একটা সর্বোচ্চ দৈর্ঘ্য পর্যন্ত। সহজ, কিন্তু ওই সর্বোচ্চের পরের position সম্পর্কে model কিছুই জানে না।
RoPE, rotary position embedding (Llama, Mistral, Qwen আর এখনকার বেশিরভাগ open model): একটা vector যোগ না করে, query আর key vector কে তাদের position এর সমানুপাতিক একটা কোণে ঘুরিয়ে দাও। তখন position m এ ঘোরানো query আর position n এ ঘোরানো key এর dot product শুধু নির্ভর করে m − n এর উপর, মানে দুইটার দূরত্বের উপর। Attention স্বাভাবিকভাবেই relative হয়ে যায়: "আমার দুই শব্দ আগেরটা" position 5 এও যা, position 5,000 এও তাই, যেটা লম্বা context এ সাহায্য করে।
50,000 token এর vocabulary, embedding size 4,096 টা সংখ্যা, 16-bit float এ রাখা। Embedding table কত বড়?
ছবিতে দেখো
Code এ চারটা check
PyTorch এ চারটা ছোট check: embedding lookup আর one-hot গুণ একই; position ছাড়া attention "rider is late" আর "late is rider" আলাদা করতে পারে না; sinusoidal encoding যোগ করলে পারে; আর RoPE এর score শুধু position এর দূরত্বের উপর নির্ভর করে।
import math
import torch
torch.manual_seed(0)
vocab = ["<pad>", "rider", "order", "late", "is", "the"]
emb = torch.nn.Embedding(len(vocab), 4) # a table: one learnable 4-number row per token id
# 1) An embedding lookup is just picking a row. It equals one-hot @ table.
ids = torch.tensor([5, 2, 4, 3]) # "the order is late"
looked_up = emb(ids)
one_hot = torch.nn.functional.one_hot(ids, len(vocab)).float()
print("table shape:", tuple(emb.weight.shape), "-> sentence shape:", tuple(looked_up.shape))
print("lookup == one_hot @ table:", torch.allclose(looked_up, one_hot @ emb.weight))
# 2) Without positions, attention can't tell word order apart.
def attend(x):
w = torch.softmax(x @ x.T / 2, dim=-1) # every token attends to every token
return w @ x
a = attend(emb(torch.tensor([1, 4, 3]))) # "rider is late"
b = attend(emb(torch.tensor([3, 4, 1]))) # "late is rider"
print("\nwithout positions, 'rider' gets the same vector in both orders:",
torch.allclose(a[0], b[2], atol=1e-6))
# 3) Sinusoidal positional encoding (the original transformer): a fixed wave pattern per position.
def sinusoid(n_pos, d):
pe = torch.zeros(n_pos, d)
pos = torch.arange(n_pos).unsqueeze(1)
freq = torch.exp(-math.log(10000.0) * torch.arange(0, d, 2) / d)
pe[:, 0::2], pe[:, 1::2] = torch.sin(pos * freq), torch.cos(pos * freq)
return pe
pe = sinusoid(3, 4)
print("\npositional encodings (positions 0, 1, 2):")
for p, row in enumerate(pe):
print(f" pos {p}: {[round(v, 3) for v in row.tolist()]}")
a = attend(emb(torch.tensor([1, 4, 3])) + pe)
b = attend(emb(torch.tensor([3, 4, 1])) + pe)
print("with positions added, 'rider' differs between the orders:", not torch.allclose(a[0], b[2], atol=1e-6))
# 4) RoPE: rotate query and key by an angle proportional to position.
# Their dot product then depends only on the DISTANCE between the two positions.
def rope(v, pos, theta=0.5):
c, s = math.cos(pos * theta), math.sin(pos * theta)
return torch.tensor([v[0] * c - v[1] * s, v[0] * s + v[1] * c])
q, k = torch.tensor([1.0, 0.5]), torch.tensor([0.3, 0.9])
print("\nRoPE score q·k for different absolute positions:")
for m, n in [(2, 0), (7, 5), (40, 38), (5, 2)]:
print(f" q at {m:>2}, k at {n:>2} (distance {m - n}): {rope(q, m) @ rope(k, n):.4f}")
Output:
table shape: (6, 4) -> sentence shape: (4, 4)
lookup == one_hot @ table: True
without positions, 'rider' gets the same vector in both orders: True
positional encodings (positions 0, 1, 2):
pos 0: [0.0, 1.0, 0.0, 1.0]
pos 1: [0.841, 0.54, 0.01, 1.0]
pos 2: [0.909, -0.416, 0.02, 1.0]
with positions added, 'rider' differs between the orders: True
RoPE score q·k for different absolute positions:
q at 2, k at 0 (distance 2): 1.0363
q at 7, k at 5 (distance 2): 1.0363
q at 40, k at 38 (distance 2): 1.0363
q at 5, k at 2 (distance 3): 0.8012
কী দেখলাম:
- (6, 4) এর একটা table 4 token এর বাক্যকে (4, 4) এর matrix বানায়: প্রতি token এ 4 সংখ্যার একটা row। Lookup আর one-hot গুণ হুবহু একই result দেয়, এজন্যই framework এটা lookup হিসেবে বানায়: একই answer, অনেক সস্তা।
- Position ছাড়া "rider" একই output vector পায়, বাক্য "rider is late" হোক বা "late is rider"। Model আক্ষরিক অর্থেই ক্রম দেখতে পায় না।
- Sinusoidal vector প্রতিটা position এ আলাদা (প্রথম দুইটা সংখ্যা দ্রুত দোলে, শেষ দুইটা প্রায় নড়ে না), আর যোগ করার পরে দুই ক্রমে "rider" আলাদা output পায়।
- RoPE: 2 এ query আর 0 তে key, 7 আর 5, 40 আর 38: সবগুলো ঠিক 1.0363 পায়, কারণ প্রতিবার দূরত্ব 2। দূরত্ব 3 দিলে আলাদা score। Absolute position হারিয়ে গেছে; শুধু relative দূরত্ব গোনায়।
Production এ যা মাথায় রাখবে
- Context length আসলে position এর সমস্যা। 4,096 position এ train করা model শেখেনি position 20,000 দেখতে কেমন। Long-context model গুলো scaling এর কৌশল দিয়ে RoPE বাড়ায় আর লম্বা text এ আরো train করে; model এর লেখা window এর বেশি সে সামলাবে ধরে নিও না।
- Embedding table memory খায়। বড় vocabulary (বহুভাষার জন্য) table কে বিশাল করে, আর weight tie না করলে output layer এর জন্য আবার একই সাইজ।
- এগুলো ভেতরের embedding। LLM এর ভেতরের token embedding আর Part 5 এ search এর জন্য যে sentence embedding ব্যবহার করবে, এক জিনিস না; ওগুলো আসে এমন model থেকে যেগুলো পুরো text তুলনাযোগ্য করতে বিশেষভাবে train করা।
- Tokenizer বদলালে model ভাঙে। Embedding এর row গুলো token id এর সাথে বাঁধা; আলাদা tokenizer মানে আলাদা table, তাই tokenizer আর model সবসময় একসাথে ship হয়।
এক কথায়
প্রতিটা token id embedding table এর একটা শেখা row তোলে (তার অর্থ), আর একটা positional signal যোগ বা প্রয়োগ হয় (তার জায়গা); শুধু attention ক্রম দেখতে পায় না, আর RoPE attention কে token গুলোর absolute position এর বদলে তাদের দূরত্বের উপর নির্ভরশীল করে।
নিজে করে দেখো
Code দিয়ে check করো RoPE একটা vector এর length অপরিবর্তিত রাখে (q আর rope(q, 10) এর norm মেলাও)। তারপর এক বাক্যে বলো এই ধর্মটা কেন কাজের।
Answer
Norm দুইটা সমান: ঘোরানো একটা vector কে না টেনেই দিক বদলায় (cos² + sin² = 1)। কাজের কারণ হলো, position শুধু query আর key এর মধ্যের কোণ বদলায়, তাদের সাইজ না, তাই position কোনো token কে attention এ "জোরে" করতে পারে না; দুইটা token কতটা মেলে সেটা এখনো আসে তাদের content থেকে।
Station stamp
Chapter শেষ করো
পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।
Pro নিয়ে XP পাও →