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

Word embedding — word2vec, GloVe আর fastText

Word Embeddings — Skip-gram with Negative Sampling Derived, Co-occurrence Factorisation, Subword Vectors for Bangla, and Honest Evaluation on Real Text

Taught in Bangla, this lesson builds skip-gram pairs, derives the negative sampling objective and gradient, and trains SGNS from scratch in in-page NumPy with a gradient check. It reaches similar vectors through co-occurrence, PPMI and SVD, explains GloVe, and shows fastText subword vectors for unseen Bangla word forms. A real desktop gensim run on Bangla Wikipedia compares word2vec and fastText, then evaluates analogies, seed stability and bias honestly.

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

  • একটি বাক্য থেকে skip-gram জোড়া বানানো, আর negative sampling-এর objective ও gradient নিজে বের করা
  • NumPy-তে SGNS লেখা, gradient সংখ্যায় যাচাই, আর training-এর আগে-পরে প্রতিবেশী দেখা
  • Co-occurrence, PPMI আর SVD দিয়ে একই কাজ, আর GloVe-এর ধারণা
  • fastText-এর subword কীভাবে বাংলার রূপভেদে সাহায্য করে, আর কোথায় ভুল পথে নেয়
  • Embedding-এর সৎ মূল্যায়ন, analogy-র ফাঁদ, bias, আর pretrained না নিজের training

AI-এর ভিত্তি কোর্সের পর্ব ৬৯-এ embedding দিয়ে খুঁজেছেন: LSA, একটি তৈরি sentence model, cosine, HNSW. আর পর্ব ২৭-এ nn.Embedding দেখেছেন, যার টেবিল কোনো কাজের loss থেকে শেখে। এই পর্বের প্রশ্ন আলাদা: কোনো label ছাড়া শুধু লেখা থেকে সেই টেবিল কীভাবে শেখা হয়? চারটি ব্লক পাতাতেই চলে (ব্রাউজারের Python, NumPy; matplotlib প্রথমবার নামতে কয়েক সেকেন্ড লাগে)। প্রথম তিনটি একই namespace ভাগ করে।

১ · পাশের শব্দ দেখে অর্থ আন্দাজ — আর এখন “শেখা” কেন

একজন বিদেশি বন্ধু চায়ের দোকানে “ঝালমুড়ি” শব্দটি প্রথম শুনলেন। তিনি অভিধান খোলেননি। তিনি শুনলেন “ঠোঙায় ঝালমুড়ি”, “ঝালমুড়ি আর চা”, “ঝাল কম দিয়ে ঝালমুড়ি”। অল্প দিনেই তিনি বুঝে যান, এটি একটি মুখরোচক নাশতা। “চানাচুর” শব্দেরও পাশে একই শব্দগুলো বসে, তাই দুটিকে কাছাকাছি রাখেন। Embedding শেখা ঠিক এই আন্দাজের যান্ত্রিক রূপ।

এই ভাবনার নাম distributional hypothesis (ইতিহাসসহ AI-এর ভিত্তি কোর্সের পর্ব ৬৯-এ আছে)। সেখানে পুরো লেখা ছিল প্রসঙ্গ, আর ফল ছিল লেখার vector. এখানে প্রসঙ্গ অনেক ছোট: একটি শব্দের দুই পাশের কয়েকটি শব্দ, যাকে বলে context window. আর প্রতিটি শব্দ পায় একটি নিজস্ব vector, অর্থাৎ word embedding.

দুটি পথ আছে। গোনা: কে কার পাশে কতবার বসল, সেই co-occurrence টেবিল বানিয়ে ছোট করা। অনুমান: একটি শব্দ দেখে পাশের শব্দ অনুমান করার ছোট একটি কাজ বানানো, আর সেই কাজের ভেতরের টেবিলকেই embedding হিসেবে নেওয়া। ২০১৩ সালের word2vec দ্বিতীয় পথের সবচেয়ে পরিচিত উদাহরণ। আমরা পরে দেখব, দুই পথ যতটা আলাদা মনে হয়, ততটা নয়।

Bengio ও সহকর্মীদের ২০০৩ সালের neural language model আর Collobert ও Weston-এর ২০০৮ সালের কাজে শেখা word vector আগেই ছিল। ২০১৩ সালে Tomas Mikolov ও সহকর্মীদের দুটি প্রবন্ধ skip-gram আর CBOW দেয়, পরেরটি যোগ করে negative sampling আর ঘন শব্দ ছেঁটে ফেলা। মূল পরিবর্তন ছিল গতি: কোটি কোটি token-এ একটি সাধারণ যন্ত্রেই training সম্ভব হলো। ২০১৪ সালে আসে GloVe (Pennington, Socher, Manning), আর ২০১৬–১৭ সালে fastText-এর subword vector (Bojanowski ও সহকর্মীরা)।

২ · Skip-gram — একটি বাক্য থেকে শেখার জোড়া

Skip-gram-এর কাজ: কেন্দ্রের শব্দ c দেখে প্রসঙ্গের শব্দ o অনুমান করা। CBOW উল্টো: চারপাশের শব্দের গড় দেখে মাঝের শব্দ অনুমান করা। CBOW দ্রুত; skip-gram-কে প্রায়ই বিরল শব্দে ভালো বলা হয়, তবে সেটি ডেটা আর কাজের ওপর নির্ভর করে। Label কেউ দেয় না; লেখা থেকেই জোড়া তৈরি হয়। নিচের stepper-এ window W = ২.

রাইডার পার্সেল আজ বাসায় দিলেন
কেন্দ্র c
Window
নতুন জোড়া
মোট জোড়া
Dot product
  1. প্রথম শব্দ। বাঁ পাশে কিছু নেই, তাই শুধু ডান পাশের দুটি শব্দ। জোড়া: (রাইডার, পার্সেল) আর (রাইডার, আজ)।
  2. দ্বিতীয় শব্দ। বাঁয়ে একটি, ডানে দুটি: তিনটি জোড়া। মোট ৫।
  3. মাঝের শব্দ। দুই পাশে দুটি করে, তাই চারটি জোড়া। এখানে “আজ” কেন্দ্র, যদিও এই শব্দটি বিষয় নিয়ে প্রায় কিছুই বলে না। ঘন শব্দ কেন ছাঁটা হয়, পরের অংশে।
  4. চতুর্থ শব্দ। তিনটি জোড়া। (পার্সেল, বাসায়) আগেই এসেছে; এখন (বাসায়, পার্সেল)। Skip-gram দুটিকে আলাদা জোড়া ধরে।
  5. শেষ শব্দ। দুটি জোড়া। পুরো বাক্যে ১৪টি।
  6. খরচ। Vocabulary V = ৭৩,৭৬০ হলে (অংশ ৭-এর run) সাধারণ softmax-এ প্রতি জোড়ায় V-টি dot product, এই একটি বাক্যেই প্রায় ১০ লাখ। k = ৫ negative নিলে প্রতি জোড়ায় ৬টি, মোট ৮৪টি।

Model-এ দুটি টেবিল থাকে, দুটিই V × d: কেন্দ্র হিসেবে শব্দের vector v, আর প্রসঙ্গ হিসেবে শব্দের vector u. দুই শব্দের মিলের স্কোর শুধু একটি dot product: uo · vc. Skip-gram-এর আদি রূপে P(o | c) = exp(uo · vc) ÷ ∑w exp(uw · vc), অর্থাৎ পর্ব ১৩-এর softmax আর cross-entropy, V-টি class-এ। হরের যোগফলটিই খরচের উৎস।

SGNS-এর গঠন: দুটি টেবিল, কয়েকটি dot product

Hidden layer বা activation নেই; শেখার সবকিছু এই দুই টেবিলে

Skip-gram with negative sampling: two tables and dot products টেবিল V (কেন্দ্র) টেবিল U (প্রসঙ্গ) মাছ পার্সেল = v রাইডার মাছ = u_n পার্সেল রাইডার = u_o σ(u_o · v)আসল জোড়া σ(u_n · v)এলোমেলো ↑ ১-এর দিকে ঠেলা ↓ ০-এর দিকে ঠেলা Training শেষে U ফেলে V রাখাই সাধারণ পছন্দ
ছবি ৪১.১ — একই শব্দের দুই টেবিলে দুটি আলাদা সারি থাকে: “পার্সেল” বাঁয়ে একটি, ডানে আরেকটি।

৩ · Negative sampling — objective, gradient আর নমুনার নিয়ম

কৌশলটি প্রশ্ন বদলে দেয়। “V-টি শব্দের মধ্যে কোনটি প্রসঙ্গ?” নয়; বরং “এই জোড়াটি কি লেখা থেকে এসেছে, না এলোমেলো?” এটি হ্যাঁ/না-র sigmoid প্রশ্ন, ঠিক logistic regression-এর মতো। ধরুন D = ১ মানে আসল জোড়া:

P(D = 1 | c, o) = σ(uo · vc), যেখানে σ(x) = 1 ÷ (1 + e−x), আর 1 − σ(x) = σ(−x)

প্রতিটি আসল জোড়ার জন্য একটি noise distribution Pn থেকে k-টি শব্দ n1…nk তোলা হয়। এদের লেবেল D = ০। Binary cross-entropy লিখলে এক জোড়ার loss দাঁড়ায়:

L = −log σ(uo · vc) − ∑i=1..k log σ(−uni · vc)

এবার gradient. d/dx [−log σ(x)] = −(1 − σ(x)), আর d/dx [−log σ(−x)] = σ(x). পর্ব ৪-এর chain rule লাগালেই:

ParameterGradientSGD-র পরে যা ঘটে
vc−(1 − σ(uo·vc)) uo + ∑ σ(uni·vc) uniuo-র দিকে সরে, negative-এর দিক থেকে সরে
uo−(1 − σ(uo·vc)) vcvc-র দিকে সরে
uniσ(uni·vc) vcvc-র থেকে দূরে সরে

গুণকগুলোই আসল গল্প। Model যদি আসল জোড়ায় ইতিমধ্যে নিশ্চিত হয় (σ ≈ ১), তবে 1 − σ ≈ ০, মানে প্রায় কিছুই বদলায় না। ভুল যত বড়, ধাক্কাও তত বড়। আর প্রতি জোড়ায় বদলায় মাত্র k + ২টি সারি। সংখ্যা দিয়ে একবার দেখা যাক।

v = [0.5, 0.2], u_o = [0.4, −0.1], u_n = [−0.3, 0.6]
ধাপ
হিসাব
  1. Dot product. v = “পার্সেল”, uo = আসল প্রসঙ্গ “রাইডার”, un = এলোমেলো “মাছ”। শুরুতে দুটি স্কোরই প্রায় শূন্য: model এখনো কিছু জানে না।
  2. Sigmoid. দুটি probability-ই প্রায় ০.৫। আমরা চাই প্রথমটি ১-এর দিকে, দ্বিতীয়টি ০-এর দিকে যাক।
  3. Loss. দুই পদ যোগ: ০.৬০৭২ + ০.৬৭৮৩ = ১.২৮৫৫। দুটিই ln 2 ≈ ০.৬৯৩১-এর কাছে, মানে এলোমেলো অনুমান।
  4. Gradient. ১ − ০.৫৪৪৯ = ০.৪৫৫১ আর ০.৪৯২৫: এই দুই গুণকই সব ধাক্কার মাপ। লক্ষ করুন ∂uo আর ∂un দুটিই v-এর সমান্তরাল।
  5. Update. θ ← θ − η·gradient, η = ০.৫। v-এর দ্বিতীয় ঘর ০.২ থেকে ০.০২৯৫-এ নামল, কারণ un-এর দ্বিতীয় ঘর বড় আর v তার থেকে সরছে।
  6. আবার মাপা। আসল জোড়ার স্কোর বাড়ল, এলোমেলো জোড়ার কমল, loss ১.২৮৫৫ থেকে ১.১০৬৯। Training মানে কোটি কোটিবার এই একই কাজ।

PyTorch-এর autograd কি একই কথা বলে? সত্যিকারের run (ডেস্কটপে, CPU-তে; লেখার সময়ে, নভেম্বর ২০২৬: Python 3.12.13, torch 2.15.0; কোড ছাঁটা, পূর্ণ ফাইলে হাতের সূত্র আর update-ও আছে):

v  = torch.tensor([0.5, 0.2], requires_grad=True)    # center word
uo = torch.tensor([0.4, -0.1], requires_grad=True)   # true context word
un = torch.tensor([-0.3, 0.6], requires_grad=True)   # negative sample
loss = -torch.log(torch.sigmoid(uo @ v)) - torch.log(torch.sigmoid(-un @ v))
loss.backward()
torch 2.15.0+cpu | loss 1.2855
autograd  v.grad  tensor([-0.3298,  0.3410])  uo.grad tensor([-0.2276, -0.0910])  un.grad tensor([0.2463, 0.0985])
hand      ∂L/∂v   tensor([-0.3298,  0.3410])  ∂L/∂uo tensor([-0.2276, -0.0910])  ∂L/∂un tensor([0.2463, 0.0985])
after step: v tensor([0.6649, 0.0295]) uo tensor([ 0.5138, -0.0545]) un tensor([-0.4231,  0.5507])
new loss 1.1069

Negative কোথা থেকে, আর ঘন শব্দ কেন ছাঁটা হয়

Mikolov ও সহকর্মীরা পরীক্ষা করে একটি নিয়ম পান: Pn(w) ∝ f(w)3/4, যেখানে f(w) শব্দটির আপেক্ষিক কম্পাঙ্ক (unigram distribution). ¾ ঘাত ঘন শব্দের ভাগ একটু কমায়, বিরলের ভাগ বাড়ায়। ধরুন f = [০.৯, ০.০৯, ০.০১]: ঘাতের পরে ভাগ দিয়ে হয় [০.৮২৫, ০.১৪৭, ০.০২৮]। বিরল শব্দটির ভাগ প্রায় তিন গুণ হলো। এটি অভিজ্ঞতা থেকে পাওয়া নিয়ম, গাণিতিকভাবে প্রমাণিত সর্বোত্তম মান নয়।

আর subsampling: “এবং”, “ও”, “হয়”-এর মতো শব্দ প্রায় সব বাক্যে থাকে, কিন্তু বিষয় নিয়ে কম বলে। প্রবন্ধের সূত্রে প্রতিটি উপস্থিতি 1 − √(t ÷ f(w)) সম্ভাবনায় ফেলে দেওয়া হয়। t = 10−4 আর “এবং”-এর f ≈ ০.০১৬৭ (আমাদের Wikipedia corpus) হলে ফেলার সম্ভাবনা প্রায় ০.৯২। ফলে window-তে দূরের অর্থবহ শব্দও ঢুকতে পারে। (gensim-এর হিসাব সামান্য ভিন্ন সূত্রে হয়, কিন্তু ধারণা এক।)

৪ · পাতাতেই SGNS — NumPy-তে শূন্য থেকে

প্রথম ব্লক একটি ছোট কাল্পনিক corpus বানায়: খাবার, ডেলিভারি আর টাকার তিনটি শব্দ-দল, আর চারটি সাধারণ শব্দ। প্রতিটি বাক্যে একটি দলের ৪টি শব্দ আর ২টি সাধারণ শব্দ, এলোমেলো ক্রমে। বাক্যগুলো ব্যাকরণ মেনে লেখা নয়; এটি শুধু বিষয়ের গঠন রাখে। এত পরিষ্কার দল বাস্তবে থাকে না, তাই এখানকার সুন্দর ফল আসল লেখায় আশা করবেন না; আসল লেখা অংশ ৭-এ। ব্লকটি হাতের gradient-কে সংখ্যায় মিলিয়ে দেখে (gradient checking, পর্ব ৫)।

import numpy as np

GROUPS = ["মাছ ভাত ডাল তরকারি রান্না স্বাদ".split(),
          "রাইডার পার্সেল ঠিকানা ডেলিভারি দেরি বাসা".split(),
          "দাম টাকা ছাড় রিফান্ড বিকাশ বাকি".split()]
COMMON = "আজ খুব আমার ছিল".split()
rng = np.random.default_rng(42)
sents = []
for _ in range(240):                   # একটি দলের 4 শব্দ + 2 সাধারণ শব্দ
    w = list(rng.choice(GROUPS[rng.integers(3)], 4)) + list(rng.choice(COMMON, 2))
    sents.append([str(x) for x in rng.permutation(w)])
vocab = sorted({w for s in sents for w in s})
idx = {w: i for i, w in enumerate(vocab)}
V, d, K = len(vocab), 8, 3
pairs = np.array([(idx[s[i]], idx[s[j]]) for s in sents for i in range(len(s))
                  for j in range(max(0, i-2), min(len(s), i+3)) if j != i])
counts = np.bincount(pairs[:, 0], minlength=V)
pn = counts**0.75 / (counts**0.75).sum()          # noise: unigram^0.75
print("V =", V, "| বাক্য", len(sents), "| জোড়া", len(pairs))
print("প্রথম বাক্য:", " ".join(sents[0]))
print(f"সবচেয়ে ঘন: {vocab[counts.argmax()]} f={counts.max()/counts.sum():.3f} P_n={pn.max():.3f}")

sig = lambda x: 1 / (1 + np.exp(-x))
def loss(v, uo, un): return -np.log(sig(uo @ v)) - np.log(sig(-un @ v)).sum()
r = np.random.default_rng(0)
v, uo, un = r.normal(size=d), r.normal(size=d), r.normal(size=(K, d))
so, sn = sig(uo @ v), sig(un @ v)
hand = {"v_c": -(1-so)*uo + sn @ un, "u_o": -(1-so)*v, "u_n": np.outer(sn, v)}
for name, x in [("v_c", v), ("u_o", uo), ("u_n", un)]:   # central difference
    num = np.zeros_like(x)
    for k in np.ndindex(x.shape):
        old = x[k]; x[k] = old + 1e-5; a = loss(v, uo, un)
        x[k] = old - 1e-5; b = loss(v, uo, un); x[k] = old
        num[k] = (a - b) / 2e-5
    print(f"{name}: max|হাতের − সংখ্যার| = {np.abs(hand[name] - num).max():.1e}")

আসল আউটপুট (ব্রাউজারের Python, NumPy 2.4.6):

V = 22 | বাক্য 240 | জোড়া 4320
প্রথম বাক্য: রান্না ডাল আজ তরকারি ছিল ডাল
সবচেয়ে ঘন: আজ f=0.087 P_n=0.075
v_c: max|হাতের − সংখ্যার| = 3.7e-11
u_o: max|হাতের − সংখ্যার| = 4.1e-11
u_n: max|হাতের − সংখ্যার| = 6.1e-11

২২টি শব্দ আর ৪,৩২০টি জোড়া, কারণ ৬ শব্দের প্রতিটি বাক্যে ১৮টি জোড়া (২৪০ × ১৮)। সবচেয়ে ঘন “আজ”-এর ভাগ ০.০৮৭ থেকে Pn-এ ০.০৭৫-এ নেমেছে: ¾ ঘাতের কাজ এটিই। তিন ধরনের gradient-এই হাতের সূত্র আর central difference-এর পার্থক্য প্রায় 10−11. সূত্রে চিহ্নের ভুল থাকলে এই সংখ্যা ০.১-এর কাছাকাছি আসত। Training চালানোর আগে এই একটি পরীক্ষা অনেক সময় বাঁচায়।

পরের ব্লক training চালায়। প্রতিটি mini-batch-এ একই সূত্র, শুধু vectorized; একই সারি একাধিকবার এলে np.add.at সব বদল যোগ করে। শেষে কেন্দ্র-টেবিলকে PCA দিয়ে ২ মাত্রায় এনে ছবি আঁকে। ছবিতে বাংলা অক্ষর ঠিকমতো আঁকা যায় না, তাই লেবেল index নম্বর; কোন নম্বর কোন শব্দ, তা আউটপুটে আছে।

import numpy as np
import matplotlib.pyplot as plt
def show(E, tag):
    N = E / np.linalg.norm(E, axis=1, keepdims=True)
    for w in ["মাছ", "পার্সেল", "দাম"]:
        sim = N @ N[idx[w]]
        near = [i for i in np.argsort(-sim) if i != idx[w]][:4]
        print(tag, w, "→", ", ".join(f"{vocab[i]} {sim[i]:.2f}" for i in near))
rng = np.random.default_rng(1); lr, B = 0.05, 64
Vc, U = rng.normal(0, 0.1, (V, d)), rng.normal(0, 0.1, (V, d))
show(Vc, "আগে")
for ep in range(30):
    total = 0.0
    for b in np.array_split(rng.permutation(len(pairs)), len(pairs) // B):
        c, o = pairs[b].T
        n = rng.choice(V, (len(b), K), p=pn)          # (B, K)
        so = sig((U[o] * Vc[c]).sum(1))               # (B,)
        sn = sig((U[n] * Vc[c][:, None]).sum(2))      # (B, K)
        total -= np.log(so).sum() + np.log(1 - sn).sum()
        gv = -(1-so)[:, None] * U[o] + (sn[..., None] * U[n]).sum(1)
        np.add.at(U, o, lr * (1-so)[:, None] * Vc[c])
        np.add.at(U, n, -lr * sn[..., None] * Vc[c][:, None])
        np.add.at(Vc, c, -lr * gv)
    if ep in (0, 9, 29): print(f"epoch {ep+1:2d}  প্রতি জোড়ায় loss {total/len(pairs):.3f}")
show(Vc, "পরে")
Z = Vc - Vc.mean(0)
P = Z @ np.linalg.svd(Z, full_matrices=False)[2][:2].T   # PCA
print("তালিকা:", " ".join(f"{i}:{w}" for i, w in enumerate(vocab)))
col = [next((g for g, G in enumerate(GROUPS) if w in G), 3) for w in vocab]
fig, ax = plt.subplots(figsize=(5, 4))
ax.scatter(P[:, 0], P[:, 1], c=col, cmap="tab10", s=30)
for i, (x, y) in enumerate(P): ax.annotate(str(i), (x, y), xytext=(4, 3), textcoords="offset points")
ax.set(xlabel="PC 1", ylabel="PC 2", title="SGNS vectors, 2-D PCA (index labels)")
plt.show()

আসল আউটপুট (ব্রাউজারের Python, NumPy 2.4.6):

আগে মাছ → টাকা 0.57, পার্সেল 0.34, ছাড় 0.33, ডেলিভারি 0.30
আগে পার্সেল → বাসা 0.43, দাম 0.39, রান্না 0.37, আমার 0.36
আগে দাম → বাকি 0.85, ডাল 0.72, বিকাশ 0.57, আজ 0.56
epoch  1  প্রতি জোড়ায় loss 2.383
epoch 10  প্রতি জোড়ায় loss 1.951
epoch 30  প্রতি জোড়ায় loss 1.949
পরে মাছ → ডাল 0.98, তরকারি 0.98, স্বাদ 0.96, রান্না 0.94
পরে পার্সেল → দেরি 0.91, রাইডার 0.87, ডেলিভারি 0.87, ঠিকানা 0.87
পরে দাম → রিফান্ড 0.94, বাকি 0.93, টাকা 0.93, বিকাশ 0.89
তালিকা: 0:আজ 1:আমার 2:খুব 3:ছাড় 4:ছিল 5:টাকা 6:ঠিকানা 7:ডাল 8:ডেলিভারি 9:তরকারি 10:দাম 11:দেরি 12:পার্সেল 13:বাকি 14:বাসা 15:বিকাশ 16:ভাত 17:মাছ 18:রাইডার 19:রান্না 20:রিফান্ড 21:স্বাদ

শুরুতে প্রতিবেশী অর্থহীন: “মাছ”-এর পাশে “টাকা”। ৩০ epoch পরে তিন শব্দের প্রতিবেশী নিজের দল থেকেই। কেউ model-কে দলের কথা বলেনি; শুধু একই বাক্যে বসা থেকে এসেছে। Loss ২.৩৮৩ থেকে ১.৯৪৯-এ নেমে থেমে গেছে, শূন্যে যায়নি। এটি ভুল নয়: দলের ভেতরে কোন শব্দের পাশে কোন শব্দ, তা এখানে এলোমেলো, আর negative-এর মধ্যেও আসল প্রসঙ্গের শব্দ উঠে আসে। তাই একটি সীমার নিচে নামা সম্ভব নয়। ছবিতে তিনটি রঙের দল আলাদা জায়গায়, আর চার সাধারণ শব্দ (০, ১, ২, ৪) তাদের মাঝামাঝি, যেহেতু তারা সব দলের সঙ্গেই বসে।

৫ · গোনার পথ — co-occurrence, PPMI, SVD আর GloVe

এবার একই জোড়াগুলো শুধু গুনি। Xij = শব্দ i-এর window-তে j কতবার এলো। কাঁচা সংখ্যায় “আজ”, “খুব”-এর মতো ঘন শব্দ সবকিছুতে এগিয়ে থাকে। তাই প্রশ্ন বদলানো হয়: দুটি শব্দ স্বাধীন হলে যত আশা করতাম, তার চেয়ে কত বেশি একসঙ্গে এলো? এটিই PMI:

PMI(i, j) = log [ P(i, j) ÷ (P(i) · P(j)) ], আর PPMI = max(PMI, 0)

ঋণাত্মক অংশ ছোট corpus-এ অনির্ভরযোগ্য, তাই PPMI সেগুলো শূন্য করে। তারপর truncated SVD দিয়ে সবচেয়ে বড় k-টি দিক রাখা হয়। SVD আর Eckart–Young-এর ফল মেশিন লার্নিং কোর্সের পর্ব ৯-এ বিস্তারিত আছে। সেখানে আর AI-এর ভিত্তি কোর্সের পর্ব ৬৯-এ matrix ছিল শব্দ × নথি; এখানে শব্দ × শব্দ।

import numpy as np

X = np.zeros((V, V))
np.add.at(X, (pairs[:, 0], pairs[:, 1]), 1)        # co-occurrence গোনা
i, j, m = idx["মাছ"], idx["ভাত"], idx["দাম"]
print("X[মাছ,ভাত] =", int(X[i, j]), "| X[মাছ,দাম] =", int(X[i, m]), "| মোট =", int(X.sum()))
p = X.sum(1) / X.sum()
pmi = np.log(np.where(X > 0, X / X.sum() / np.outer(p, p), 1.0))   # X = 0 হলে 0
ppmi = np.maximum(pmi, 0)
print(f"PMI(মাছ,ভাত) = {pmi[i, j]:.3f} | PPMI(মাছ,দাম) = {ppmi[i, m]:.3f}")
W, S, Vt = np.linalg.svd(ppmi)
print("singular value:", S[:6].round(2))
E = W[:, :3] * np.sqrt(S[:3])                      # প্রথম 3 দিক
show(E, "SVD")
err = np.linalg.norm(ppmi - (W[:, :3] * S[:3]) @ Vt[:3])
print(f"rank-3 ভুল {err:.3f} | সূত্র √(বাকি S² যোগ) {np.sqrt((S[3:]**2).sum()):.3f}")
mask = X >= 5                                      # SGNS-এর dot বনাম PMI − log K
r = np.corrcoef((Vc @ U.T)[mask], pmi[mask] - np.log(K))[0, 1]
print(f"ঘর {mask.sum()} | corr(u·v, PMI − log K) = {r:.3f}")

আসল আউটপুট (ব্রাউজারের Python, NumPy 2.4.6):

X[মাছ,ভাত] = 17 | X[মাছ,দাম] = 0 | মোট = 4320
PMI(মাছ,ভাত) = 1.131 | PPMI(মাছ,দাম) = 0.000
singular value: [6.71 6.17 5.16 1.1  1.07 1.03]
SVD মাছ → স্বাদ 1.00, ডাল 1.00, ভাত 1.00, রান্না 1.00
SVD পার্সেল → ঠিকানা 1.00, বাসা 1.00, দেরি 1.00, ডেলিভারি 1.00
SVD দাম → বিকাশ 1.00, ছাড় 1.00, টাকা 1.00, বাকি 1.00
rank-3 ভুল 2.950 | সূত্র √(বাকি S² যোগ) 2.950
ঘর 268 | corr(u·v, PMI − log K) = 0.735

“মাছ” আর “ভাত” ১৭ বার পাশাপাশি, “মাছ” আর “দাম” একবারও না। PMI ১.১৩১ মানে স্বাধীন হলে যত আশা করতাম, তার প্রায় e1.131 ≈ ৩.১ গুণ। Singular value-এ একটি পরিষ্কার ফাঁক: ৬.৭১, ৬.১৭, ৫.১৬, তারপর ১.১। আমরা ঠিক তিনটি দল রেখেছিলাম, আর SVD তিনটি বড় দিক খুঁজে পেল। প্রতিবেশীর cosine ১.০০ দেখে উচ্ছ্বসিত হবেন না: ৩ মাত্রায় একই দলের শব্দগুলো প্রায় একই দিকে মিশে গেছে। আসল লেখায় এমন ফাঁক সাধারণত থাকে না। Rank-3 ভুল আর বাদ পড়া singular value-এর সূত্র দুটিই ২.৯৫০: Eckart–Young হাতে-কলমে। শেষ লাইনটি কৌতূহলের: ব্লক ২-এর SGNS কোনো দিন এই টেবিল দেখেনি, তবু ২৬৮টি ঘরে তার dot product PMI − log K-এর সঙ্গে ০.৭৩৫ correlation রাখে। হুবহু সমান নয়; ছোট ডেটা, মাত্র ৮ মাত্রা, আর ¾ ঘাতও আছে। তবু দুই পথের আত্মীয়তার ছায়া পরিষ্কার।

GloVe এই দুই পথের মাঝখানে। এটি পুরো corpus একবার গুনে X বানায়, তারপর gradient দিয়ে ছোট vector শেখে, যাতে এই loss কমে:

J = ∑i,j f(Xij) · (wi · w̃j + bi + b̃j − log Xij)²

এখানে w আর w̃ দুই টেবিলের vector, b আর b̃ শব্দপ্রতি একটি করে সংখ্যা, আর f ওজন: ছোট X-এ কম, বড় X-এ সীমিত (প্রবন্ধে xmax = ১০০ আর ঘাত ¾)। X = ০ ঘরগুলো যোগফলে আসেই না। প্রবন্ধে শেষে w + w̃ ব্যবহার করা হয়। আমাদের ব্লকের PPMI-SVD হুবহু GloVe নয়, তবে একই পরিবারের: co-occurrence থেকে factorization. আমরা GloVe নিজে চালাইনি।

আরও গভীরে: Levy আর Goldberg (২০১৪) দেখান, SGNS-এর সর্বোত্তম সমাধানে uo·vc = PMI(c, o) − log k, অর্থাৎ SGNS পরোক্ষে shifted PMI matrix-কে factorize করে; এতে কিছু অনুমান আছে, যেমন dimension যথেষ্ট বড়। Levy, Goldberg আর Dagan (২০১৫) পরে দেখান, window, subsampling, ¾ ঘাতের মতো সেটিং গোনার পদ্ধতিতে আনলে পার্থক্যের অনেকটা কমে যায়। Baroni ও সহকর্মীরা (২০১৪) আগে “অনুমান” পথকে এগিয়ে রেখেছিলেন। তাই “কোন পথ সেরা” প্রশ্নে সম্প্রদায় একমত নয়; কাজ আর সেটিং অনুযায়ী ফল বদলায়।

৬ · fastText — শব্দের ভেতরের টুকরো

word2vec আর GloVe-এ প্রতিটি শব্দ একটি অখণ্ড প্রতীক। ইংরেজিতে এটি কিছুটা চলে। বাংলায় একই শব্দের বহু রূপ: বাজার, বাজারে, বাজারের, বাজারটি, বাজারগুলোর, বাজারেও। প্রতিটি আলাদা প্রতীক, প্রতিটির ডেটা ভাগ হয়ে যায়, আর যে রূপ training-এ আসেনি, তার কোনো vector-ই নেই। এই out-of-vocabulary (OOV) সমস্যার মাপ পর্ব ৩৯ (NLP-এর পথ) দেখায়, আর টুকরো করে পড়ার আরেক পথ পর্ব ৪০ (Tokenization) দেখায়।

চিঠিতে “মালিবাগের” শব্দটি নতুন দেখলেন। “মালি”, “বাগ”, “-এর” আপনি চেনেন, তাই আন্দাজ করেন এটি কোনো জায়গার নামের সম্বন্ধ রূপ। আপনি শব্দের টুকরো দিয়ে আন্দাজ করলেন। আবার “মালিবাগ” আসলে “মালি”-র বাগান নাও হতে পারে। fastText-এর জোর আর দুর্বলতা দুটিই এই আন্দাজে।

fastText-এর একটি পরিবর্তনই সব বদলায়: কেন্দ্রের শব্দের স্কোর আর একটি vector থেকে আসে না। শব্দের দুই পাশে সীমানার চিহ্ন < > বসিয়ে ৩ থেকে ৬ দৈর্ঘ্যের সব character n-gram নেওয়া হয়, আর স্কোর = ∑g zg · uo, যেখানে zg প্রতিটি n-gram-এর vector. Loss, negative sampling, gradient সবই আগের মতো, শুধু gradient ভাগ হয়ে সব zg-তে যায়। গুরুত্বপূর্ণ বিশদ: এই “character” হলো Unicode code point, তাই “বা” দুটি character (ব + া), আর n-gram আপনার পড়ার অক্ষর (grapheme cluster) ভেঙে ফেলতে পারে।

Training-এ কখনো না-দেখা একটি শব্দ (সংখ্যা: অংশ ৭-এর সত্যিকারের run)
ধাপ
ফল
  1. word2vec-এর উত্তর। টেবিলে সারি নেই, তাই কোনো vector নেই। বাজারঘর আমাদের কাল্পনিক দোকান; নামটি লেখায় প্রায় আসেই না।
  2. Code point. পাইথনে len("বাজারঘরের") হয় ৯, কারণ া আর ে আলাদা code point. সীমানার চিহ্নসহ ১১।
  3. ৩-gram. ৯টি টুকরো। “াজা” আর “ারঘ” স্বরচিহ্ন দিয়ে শুরু, যা পড়ার অক্ষর ভেঙে দেয়; fastText-এর তাতে আপত্তি নেই। শেষের “ের>” টুকরোটি সম্বন্ধ রূপের চিহ্ন বহন করে।
  4. সব n-gram. ১১ চিহ্নে ৩০টি টুকরো। ৭৩,৭৬০ শব্দের vocabulary-তে ১৭টি দেখা গেছে। “রঘর”, “জারঘ”-এর মতো সন্ধিস্থলের টুকরোগুলো অদেখা।
  5. Vector. টুকরোগুলো hash হয়ে একটি নির্দিষ্ট bucket-এ যায়, তাই অদেখা টুকরোও কোনো vector পায়। সেটি অন্য টুকরোর সঙ্গে ভাগ করা হতে পারে (hash collision). আমাদের ৩,৮৪,৪৯৩টি n-gram আর ৫ লাখ bucket-এ প্রায় ৫৪% bucket-এ অন্তত একটি n-gram পড়ার কথা, মানে অদেখা টুকরোর প্রায় অর্ধেক অন্য টুকরোর vector ধার করে। ৩০টির গড় হাতে নিয়ে gensim-এর ফলের সঙ্গে মেলানো হয়েছে।
  6. কার কাছে। “বাজার”-এর রূপগুলোর খুব কাছে, “নদী” থেকে দূরে। ভালো। কিন্তু “ঘর”-এর দিক (০.৬৩৪) অনেক দুর্বল: শব্দটি দুটি অর্থের জোড় হলেও vector প্রধানত “বাজার”-এর দিকে। এটি অর্থের হিসাব নয়, টুকরোর গড়।

পাতাতেই ছোট আকারে দেখা যাক। এই ব্লকে আলাদা একটি কাল্পনিক corpus: ৯টি মূল শব্দ, প্রতিটির তিনটি বিভক্তি-সহ রূপ। “টির” ইচ্ছা করে নেই, আর “বাজারঘর” তো নেই-ই। Update লিখতে হয়েছে এক জোড়া এক জোড়া করে, তাই epoch কম।

import numpy as np

STEMS = ["বাজার দোকান হাট", "ঘর কক্ষ মহল", "মাছ ডাল ভাত"]
SUF = ["", "ের", "ে", "টি"]                       # "টির" ইচ্ছা করে নেই
rng = np.random.default_rng(3)
sents = [[str(s) + str(rng.choice(SUF)) for s in rng.choice(STEMS[rng.integers(3)].split(), 5)]
         for _ in range(300)]
vocab = sorted({w for s in sents for w in s}); idx = {w: i for i, w in enumerate(vocab)}
def grams(w, lo=3, hi=4):
    b = "<" + w + ">"
    return [b] + [b[i:i+n] for n in range(lo, hi+1) for i in range(len(b)-n+1)]
G = {}
sub = [np.array([G.setdefault(g, len(G)) for g in grams(w)]) for w in vocab]
d, K, lr = 8, 3, 0.05
Z, U = rng.normal(0, 0.1, (len(G), d)), rng.normal(0, 0.1, (len(vocab), d))
sig = lambda x: 1 / (1 + np.exp(-x))
print("শব্দ", len(vocab), "| n-gram", len(G), "| ঘরের:", " ".join(grams("ঘরের")[1:]))
for ep in range(4):
    for s in sents:
        for i, w in enumerate(s):
            ids = sub[idx[w]]
            for j in range(max(0, i-2), min(len(s), i+3)):
                if j == i: continue
                t = np.array([idx[s[j]], *rng.integers(len(vocab), size=K)])
                e = np.r_[1.0, np.zeros(K)] - sig(U[t] @ Z[ids].sum(0))
                U[t] += lr * np.outer(e, Z[ids].sum(0))
                Z[ids] += lr * (e @ U[t]) / len(ids)
def vec(w):
    hit = [G[g] for g in grams(w) if g in G]
    return Z[hit].sum(0), f"{len(hit)}/{len(grams(w))}"
A = np.array([vec(w)[0] for w in vocab]); A /= np.linalg.norm(A, axis=1, keepdims=True)
for w in ["বাজারঘরের", "মাছটির", "কম্পিউটার"]:
    v, hit = vec(w); v = v / np.linalg.norm(v)
    near = ", ".join(f"{vocab[i]} {A[i] @ v:.2f}" for i in np.argsort(-(A @ v))[:4])
    print(w, "OOV" if w not in idx else "দেখা", "| n-gram", hit, "→", near)

আসল আউটপুট (ব্রাউজারের Python, NumPy 2.4.6):

শব্দ 36 | n-gram 188 | ঘরের: <ঘর ঘরে রের ের> <ঘরে ঘরের রের>
বাজারঘরের OOV | n-gram 12/18 → বাজারের 0.98, বাজারে 0.98, বাজারটি 0.98, বাজার 0.97
মাছটির OOV | n-gram 7/12 → মাছটি 0.99, মাছে 0.99, মাছ 0.97, ভাত 0.97
কম্পিউটার OOV | n-gram 1/18 → বাজার 0.70, বাজারে 0.66, বাজারটি 0.65, বাজারের 0.63

“বাজারঘরের”-এর ১৮টি টুকরোর ১২টি চেনা, আর সে “বাজার”-এর রূপগুলোর কাছে। “মাছটির” একইভাবে “মাছটি”, “মাছে”-র কাছে, যদিও “টির” কোথাও ছিল না। মূল শব্দের প্রতিটি রূপ আলাদা করে না শিখে বিভক্তি পার হওয়া গেল। শেষ লাইনটিই সতর্কবার্তা। “কম্পিউটার”-এর ১৮টির মাত্র ১টি (“ার>”) মেলে, তবু সে “বাজার”-এর কাছে ০.৭০ পায়, কারণ মাত্র একটি টুকরোর দিকই তার পুরো vector. fastText অচেনা শব্দেও আত্মবিশ্বাসী একটি vector দেয়, আর উঁচু cosine মানেই অর্থের মিল নয়। আমাদের ব্লক অদেখা টুকরো ফেলে দেয়; আসল fastText-এ hash-এর কারণে সেগুলোও কোনো vector পায়।

৭ · আসল বাংলা লেখায় — word2vec বনাম fastText

ডেটা আর লাইসেন্স: বাংলা Wikipedia-র ২০২৩ সালের ১ নভেম্বরের snapshot, Hugging Face-এর wikimedia/wikipedia dataset, config 20231101.bn; dataset card অনুযায়ী লাইসেন্স CC BY-SA 3.0 আর GFDL. আমরা দুটি parquet ফাইলের প্রথমটি নিয়েছি (৭১,৫৩৫টি নিবন্ধ), NFC করে শুধু বাংলা অক্ষরের শব্দ রেখেছি (AI-এর ভিত্তি কোর্সের পর্ব ৬৭), আর seed দিয়ে ৩৩% বাক্য নিয়েছি। Wikipedia-র লেখা বিশ্বকোষীয়, আর বাংলা Wikipedia-তে স্থান-নামের ছোট নিবন্ধ অনেক, যার ছাপ প্রতিবেশীতে দেখবেন। রিভিউ বা কথ্য লেখায় ফল আলাদা হবে।

সত্যিকারের run, ডেস্কটপে CPU-তে (লেখার সময়ে, নভেম্বর ২০২৬: gensim 4.4.0, Python 3.12.13). Python 3.14-এ uv gensim-এর খুব পুরোনো 0.10.1 বেছে নিয়েছিল, তাই Python 3.12 ব্যবহার করা হয়েছে; AI-এর দেওয়া install নির্দেশ চালিয়েও version মিলিয়ে নিন। কোড ছাঁটা:

common = dict(vector_size=100, window=5, min_count=5, sg=1, negative=5,
              ns_exponent=0.75, sample=1e-4, epochs=3, workers=4)
w2v = Word2Vec(sents, seed=1, **common)
ft = FastText(sents, seed=1, min_n=3, max_n=6, bucket=500_000, **common)
sentences 653,841  tokens 8,094,405
candidates 1412 held out 300
word2vec seed 1: vocab 73,760  20 s on CPU
word2vec seed 2: vocab 73,760  19 s on CPU
fastText seed 1: vocab 73,760  40 s on CPU

ছয়টি শব্দ পরীক্ষা করা হয়েছিল; “নদী” বাদে পাঁচটির প্রথম ৬ প্রতিবেশী, cosine সহ:

ঢাকা
  word2vec: রাজশাহী 0.85, চট্টগ্রাম 0.82, বরিশাল 0.77, জাহাঙ্গীরনগর 0.77, ময়মনসিংহ 0.76, খুলনা 0.74
  fastText: ঢাকাই 0.91, ঢাকাসহ 0.89, রাজশাহী 0.87, চট্টগ্রাম 0.85, চট্টগ্রামসহ 0.85, ঢাকাস্থ 0.85
ভালো
  word2vec: ভাল 0.89, খারাপ 0.75, পেতেন 0.72, অবিশ্বাস্য 0.71, আশানুরূপ 0.71, দারুণ 0.71
  fastText: ভালোই 0.91, ভাল 0.90, ভালই 0.89, ভালোর 0.86, ভালভ 0.81, ভালোভাবেই 0.78
ডাক্তার
  word2vec: নার্স 0.88, সেবিকা 0.85, গৃহিণী 0.84, অনাথ 0.82, পেশায় 0.82, সহপাঠী 0.81
  fastText: ডাক্তারী 0.95, ডাক্তারি 0.91, ডাক্তারের 0.90, ডাক্তাররা 0.87, ওক্তার 0.87, আক্তার 0.82
বাজার
  word2vec: হাট 0.84, বাজারগুলো 0.74, নলচিরা 0.69, বাজারের 0.69, শান্তিরহাট 0.67, লালখান 0.66
  fastText: বাজারও 0.94, বাজারদর 0.92, বউবাজার 0.90, হাটবাজার 0.90, বাজারমুখী 0.90, বৌবাজার 0.89
মাছ
  word2vec: ইলিশ 0.81, চিংড়ি 0.80, মাছের 0.77, শুঁটকি 0.76, জলজ 0.75, শামুক 0.75
  fastText: মাছও 0.93, মাছে 0.89, মাছেরা 0.89, মাছধরা 0.86, মাছি 0.83, মাছের 0.82

একটি স্পষ্ট প্যাটার্ন। word2vec পাশাপাশি জিনিস আনে: ঢাকার পাশে অন্য বিভাগীয় শহর, মাছের পাশে ইলিশ আর চিংড়ি। fastText আগে আনে একই শব্দের রূপ আর বানানের আত্মীয়: “বাজারদর”, “মাছি”, “ভালভ”, এমনকি “ডাক্তার”-এর পাশে নামের অংশ “আক্তার”, যার সঙ্গে মিল শুধু “ক্তার” টুকরোয়। আবার word2vec-এর ফলও নিখুঁত নয়: “ভালো”-র পাশে “খারাপ” ০.৭৫, কারণ দুটি একই প্রসঙ্গে বসে, আর “ডাক্তার”-এর পাশে “গৃহিণী”, “অনাথ” এসেছে সম্ভবত জীবনী নিবন্ধের বাক্য থেকে।

OOV-তে পরীক্ষা

বাজারঘরের: word2vec KeyError; fastText: বাজারের 0.95, বাজার 0.93, বাজারটির 0.92, বাজারও 0.92, চকবাজারের 0.91
নদীগুলোতেও: word2vec KeyError; fastText: নদীগুলোতে 0.98, নদীগুলো 0.91, উপনদীগুলো 0.91, নদীগুলোর 0.89, পাহাড়গুলো 0.86
শিক্ষকদেরকেও: word2vec KeyError; fastText: শিক্ষকদের 0.95, শিক্ষকগণ 0.94, শিক্ষকরা 0.93, শিক্ষকেরা 0.92, শিক্ষককে 0.92
ডাক্তারবাবুদের: word2vec KeyError; fastText: ডাক্তাররা 0.88, ডাক্তারের 0.87, সহপাঠীদের 0.87, পেশাজীবীদের 0.87, ডাক্তারী 0.86
মাছগুলোকে: word2vec KeyError; fastText: বলগুলোকে 0.94, এগুলোকে 0.93, গুলোকে 0.92, অংশগুলোকে 0.92, সেগুলোকে 0.91
ঢাকাবাসীদের: word2vec KeyError; fastText: দালালদের 0.91, জঙ্গীদের 0.90, বাসদের 0.90, হাজীদের 0.89, আসামীদের 0.89

প্রথম চারটি চমৎকার। শেষ দুটি শিক্ষণীয়: “মাছগুলোকে”-র প্রতিবেশীরা সবাই “-গুলোকে”, মাছের কোনো চিহ্ন নেই; “ঢাকাবাসীদের”-এর প্রতিবেশীরা “-দের”-এ শেষ হওয়া মানুষের দল। বিভক্তির টুকরো মূল শব্দকে ঢেকে দিতে পারে। হাতে বাছা উদাহরণে ভুল ধারণা হতে পারে, তাই বড় পরীক্ষা: training-এর আগে ৩০০টি বিভক্তি-সহ রূপ (যেমন “সূত্রটি”, “পরমাণুতে”) লেখা থেকে পুরো সরিয়ে দিলাম, তারপর জিজ্ঞেস করলাম, মূল শব্দ fastText-এর প্রথম ১০-এ আসে কি?

sample: ['পরমাণুতে=পরমাণু+তে', 'খানকে=খান+কে', 'বছরকে=বছর+কে', 'সূত্রটি=সূত্র+টি', 'আরবরা=আরব+রা', 'উপজাতিরা=উপজাতি+রা', 'যেটিতে=যেটি+তে', 'এশিয়াতে=এশিয়া+তে']
n=298  stem at rank 1: 0.050  stem in top-10: 0.477
{'কে': '36/78', 'টি': '38/56', 'তে': '23/52', 'দের': '16/48', 'রা': '24/32', 'টির': '5/19', 'গুলো': '0/10', 'গুলোর': '0/3'}

৪৭.৭%-এ মূল শব্দ প্রথম ১০-এ, প্রথম স্থানে মাত্র ৫%. word2vec এই রূপগুলোতে কিছুই দিতে পারে না, তাই fastText-এর লাভ বাস্তব। কিন্তু ফলাফল বিভক্তিভেদে অনেক আলাদা: “-রা”-তে ২৪/৩২, “-গুলো”-তে ০/১০, কারণ দীর্ঘ বিভক্তিতে টুকরোর বড় অংশই বিভক্তির। সীমা: রূপগুলো একটি শেষাংশের নিয়ম দিয়ে বাছা, তাই “খানকে”-র মতো নামও ঢুকেছে; corpus বা seed বদলালে সংখ্যা কিছুটা বদলাবে। একটি run, একটি corpus.

৮ · সৎ মূল্যায়ন — analogy, seed আর bias

Embedding ভালো হলো কি না, তা বলার সবচেয়ে সৎ উপায় হলো যে কাজে লাগবে, সেই কাজে মাপা। তার আগে কয়েকটি সস্তা পরীক্ষা আছে, প্রতিটির সীমাসহ।

Analogy: বিখ্যাত, কিন্তু সাবধান

Word analogy পরীক্ষা: a : b :: c : ?-এর উত্তর ধরা হয় cos(x, b + c − a) সবচেয়ে বড় যে x-এর। প্রমিত বাংলা analogy সেটের খোঁজ আমরা করিনি, তাই নিজের একটি ছোট তালিকা বানিয়েছি। এটি কোনো benchmark নয়। প্রতিটি শ্রেণিতে জোড়াগুলোর সব ক্রমবিন্যাস প্রশ্ন:

শ্রেণিপ্রশ্নword2vec top-1word2vec top-5fastText top-1প্রশ্নের শব্দ বাদ না দিলে উত্তর প্রশ্নেরই শব্দ (word2vec)
রাজধানী : দেশ১৫৬১২ (৭.৭%)৪১২০ (১২.৮%)৯৩ (৫৯.৬%)
পরিবার (বাবা : মা)৫৬১২ (২১.৪%)১৯৭ (১২.৫%)৪২ (৭৫%)
বিপরীত (দিন : রাত)৪২১৩ (৩১%)২৩২ (৪.৮%)৪১ (৯৭.৬%)
সম্বন্ধ রূপ (নদী : নদীর)৫৬২০ (৩৫.৭%)৩২১৪ (২৫%)৫৪ (৯৬.৪%)
মোট৩১০৫৭ (১৮.৪%)১১৫ (৩৭.১%)৪৩ (১৩.৯%)২৩০ (৭৪.২%)
ঢাকা:বাংলাদেশ :: দিল্লি:? -> তামিলনাড়ু 0.66, নেপাল 0.66, ভারত 0.66, ভারতের 0.65
ছেলে:মেয়ে :: রাজা:? -> রাজার 0.77, রানী 0.77, রানি 0.76, রাণী 0.76
পুরুষ:নারী :: রাজা:? -> রাজপুত্র 0.74, সামন্ত 0.72, সীতারাম 0.72, হরিশচন্দ্র 0.71
ভালো:খারাপ :: বড়:? -> মারাত্মক 0.72, সুনামির 0.63, ভয়ঙ্কর 0.62, ছোট 0.62
ভাই:বোন :: বাবা:? -> মা 0.85, মাতা 0.81, পিতামাতা 0.79, পিতা 0.78
নদী:নদীর :: গ্রাম:? -> ব্লকের 0.67, পঞ্চায়েত 0.65, পঞ্চায়েতের 0.64, মৌজা 0.63

“রাজা”-র গল্পটি এখানে প্রায় কাজ করে, কিন্তু “ছেলে:মেয়ে” জোড়ায়; “পুরুষ:নারী” জোড়ায় “রানী” প্রথম চারে নেই। একটি উদাহরণ বেছে দেখালে যেকোনো দাবিই সত্য মনে হয়। সবচেয়ে বড় ফাঁদটি শেষ কলামে: প্রচলিত হিসাব a, b, c-কে উত্তর থেকে বাদ দেয়। বাদ না দিলে ৭৪%-এ সবচেয়ে কাছের শব্দ প্রশ্নেরই একটি শব্দ, অর্থাৎ b + c − a প্রায়ই a, b বা c-র পাশেই থাকে, আর “সম্পর্কের দিক” সংখ্যাটি নিজে যতটা মনে হয় ততটা শক্ত নয়। Linzen (২০১৬) আর Nissim ও সহকর্মীরা (২০২০) এই সমস্যা নিয়ে লিখেছেন।

Seed: প্রতিবেশীও পাল্টায়

একই ডেটা, একই সেটিং, শুধু seed আলাদা (আর একাধিক thread-ও ফল বদলায়)। ৩০০টি বাছাই শব্দে:

n=300 mean overlap 0.544  min 0.0 max 1.0
cos(ঢাকা, বাংলাদেশ) seeds: 0.567 0.602

দুই run-এ প্রথম ১০ প্রতিবেশীর গড়ে মাত্র ৫৪.৪% মেলে। তাই “এই শব্দের প্রতিবেশী দেখে সমাজের চিন্তা বুঝলাম” ধরনের দাবির আগে একাধিক seed দেখা দরকার; Antoniak আর Mimno (২০১৮) ঠিক এই অস্থিরতা নিয়ে সতর্ক করেন।

Bias: যা মাপা গেল, যা গেল না

লেখা থেকে শেখা vector লেখার সামাজিক ধরনও শেখে। Bolukbasi ও সহকর্মীরা (২০১৬) ইংরেজি সংবাদের word2vec-এ পেশা আর লিঙ্গের সম্পর্ক দেখান, আর Caliskan ও সহকর্মীরা (২০১৭) মানুষের ভেতরে-থাকা পক্ষপাতের পরীক্ষার ধাঁচে মাপেন। আমরা বাংলায় একটি সরল পরীক্ষা করলাম: পাঁচটি জোড়া (পুরুষ−নারী, ছেলে−মেয়ে…) থেকে একটি দিক বানিয়ে পেশার শব্দ সেই দিকে ফেলা। ধনাত্মক মানে “পুরুষ” দিকে।

seed1 অভিনেত্রী -0.24 গায়িকা -0.21 গায়ক -0.12 অভিনেতা -0.12 শিক্ষক -0.06 রাঁধুনি -0.06 গৃহিণী -0.01 বিজ্ঞানী +0.02 নার্স +0.05 ডাক্তার +0.08 সৈনিক +0.09 প্রকৌশলী +0.10 ড্রাইভার +0.12
seed2 অভিনেত্রী -0.26 গায়িকা -0.18 গায়ক -0.14 অভিনেতা -0.10 শিক্ষক -0.08 রাঁধুনি -0.05 গৃহিণী -0.01 ড্রাইভার +0.08 বিজ্ঞানী +0.08 ডাক্তার +0.08 সৈনিক +0.09 নার্স +0.11 প্রকৌশলী +0.14

এখান থেকে বড় কোনো সিদ্ধান্ত টানা উচিত নয়। “অভিনেতা” আর “গায়ক” পুরুষবাচক শব্দ, অথচ দুটিই ঋণাত্মক দিকে; “নার্স” ধনাত্মক। মানে দিকটি লিঙ্গ ছাড়াও বিষয় (চলচ্চিত্র, গান) ধরছে, মান ছোট, আর একটি বিশ্বকোষের অংশ থেকে সমাজ সম্পর্কে কিছু বলা যায় না। তবু উল্টো সিদ্ধান্তও ভুল হবে: “এখানে bias নেই”। আপনার কাজে embedding-এর সিদ্ধান্ত মানুষের ওপর পড়লে দলভিত্তিক ফল আলাদা করে মাপুন; মাপা আর প্রতিকারের পদ্ধতি মেশিন লার্নিং কোর্সের পর্ব ৬০-এ। Gonen আর Goldberg (২০১৯) দেখান, একটি দিক সরালেই embedding bias মুছে যায় না।

Pretrained, না নিজের training?

পরিস্থিতিসাধারণ পছন্দকারণ
ডেটা কম, সাধারণ লেখাPretrained vectorকয়েক হাজার রিভিউতে ভালো vector শেখা কঠিন
নিজস্ব শব্দভান্ডার (পণ্যের নাম, Banglish)নিজের training, ডেটা যথেষ্ট হলেবিশ্বকোষে এই শব্দ নেই; Banglish-এর হরফও আলাদা
অনেক রূপভেদ, OOV বেশিfastTextঅদেখা রূপে vector দেয়; বানানের ভুল-আত্মীয়ের দাম মেনে
বাক্যের অর্থ দিয়ে খোঁজা, RAGSentence embedding (পর্ব ৬৭)শব্দের গড় ক্রম আর নঞর্থক অর্থ হারায়
একই শব্দের নানা অর্থContextual embedding (পর্ব ৪৪)এখানে প্রতিটি শব্দের একটিই vector

একটি পরিচিত pretrained সম্পদ হলো Grave ও সহকর্মীদের (২০১৮) ১৫৭ ভাষার fastText vector, যাতে বাংলাও আছে; fastText-এর সাইট অনুযায়ী লাইসেন্স CC BY-SA 3.0, আর ফাইলগুলো অনেক GB-র। আমরা এটি চালাইনি, তাই মান নিয়ে কিছু বলছি না। আর একটি কথা: বড় ভাষা model-এর যুগে static word vector কি অকেজো? না। ছোট, দ্রুত, ব্যাখ্যা করা সহজ, আর একটি ফোনেও চলে। পর্ব ৪২ (Deep learning দিয়ে text classification) দেখাবে আসল কাজে এই ধরনের baseline কতদূর যায়।

৯ · AI-এর যুগে — embedding-এর কোড আর দাবি যাচাই

AI assistant word2vec-এর কোড চট করে লিখে দেয়, কিন্তু এই কয়েকটি ভুল প্রায়ই পাওয়া যায়: পুরোনো gensim 3 argument (size, iter; এখন vector_size, epochs), model.most_similar (এখন model.wv.most_similar), negative-এর ¾ ঘাত ভুলে যাওয়া, gradient-এ চিহ্নের ভুল, NFC না করা, আর “king − man + woman” দেখিয়ে সব প্রমাণ হয়েছে বলা।

এই ধাঁচে চাইলে উত্তর যাচাই করা সহজ হয়:

আমি gensim 4.4 দিয়ে বাংলা লেখায় (আগে NFC করা) skip-gram আর fastText train করব। শুধু বর্তমান argument ব্যবহার করো, আর কোনটি কেন, এক লাইনে বলো। সঙ্গে দাও: ১) OOV শব্দে দুই model-এর আচরণের ছোট পরীক্ষা ২) দুই seed-এ top-10 প্রতিবেশীর মিল ৩) আমার কাজের (রিভিউ শ্রেণিবিভাগ) মাপ, TF-IDF baseline-এর সঙ্গে। Analogy-র ফল দিলে প্রশ্নের শব্দ বাদ দেওয়া হলো কি না স্পষ্ট লেখো। কোন সংখ্যা তুমি চালিয়ে পাওনি, তা আলাদা করে বলো।

যাচাইয়ের চেকলিস্ট

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

  • Negative sampling কেন? V-মুখী softmax-এর বদলে k + ১টি sigmoid, তাই প্রতি জোড়ায় খরচ V-এর ওপর নির্ভর করে না।
  • Skip-gram বনাম CBOW? প্রথমটি কেন্দ্র থেকে প্রসঙ্গ, দ্বিতীয়টি প্রসঙ্গের গড় থেকে কেন্দ্র; CBOW দ্রুত।
  • ¾ ঘাত কেন? ঘন শব্দের ভাগ কমায়, বিরলের বাড়ায়; এটি পরীক্ষায় পাওয়া নিয়ম।
  • fastText OOV-তে কীভাবে vector দেয়? n-gram-এর vector-এর গড়; সীমানা চিহ্ন আর hash bucket সহ।
  • GloVe কী factorize করে? ওজনসহ least squares-এ log co-occurrence.

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

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

প্র ০১“king − man + woman = queen” কেন এত বিখ্যাত হলো, আর এটি আসলে কতটা প্রমাণ করে?

২০১৩ সালে Mikolov, Yih আর Zweig দেখান, শেখা vector-এর বিয়োগ-যোগে কিছু সম্পর্ক ধরা পড়ে। ছবিটি মনে রাখার মতো: অর্থ যেন একটি জ্যামিতি, আর “রাজকীয়তা” বা “লিঙ্গ” যেন একটি দিক। প্রচার আর পাঠ্যবইয়ে এটি দ্রুত ছড়ায়।

গল্পের নিচে একটি কারিগরি শর্ত লুকিয়ে থাকে: প্রচলিত হিসাবে a, b, c উত্তর হতে পারে না। Linzen (২০১৬) দেখান, এই বাদ দেওয়া আর b ও c-র নিজস্ব কাছাকাছি হওয়া ফলের বড় অংশ ব্যাখ্যা করতে পারে। Nissim, van Noord আর van der Goot (২০২০) “Fair is Better than Sensational” প্রবন্ধে একই কথা বলেন, বিশেষত “পক্ষপাত দেখানো” analogy নিয়ে: “man is to doctor as woman is to ?”-এর উত্তর “doctor” হতে পারত, কিন্তু নিয়মই তা নিষিদ্ধ করে রেখেছে।

আমাদের বাংলা run একই ছবি দেয়। শর্ত তুলে দিলে ৩১০টির ৭৪%-এ উত্তর প্রশ্নেরই শব্দ। শর্তসহ top-1 ১৮.৪%, আর “ছেলে:মেয়ে :: রাজা”-তে “রানী” দ্বিতীয়, “রাজার”-এর পরে। এর মানে এই নয় যে দিক বলে কিছু নেই। সম্বন্ধ রূপে ৩৫.৭% আর রাজধানীতে ৭.৭%-এর পার্থক্য দেখায়, কিছু সম্পর্ক বেশি নিয়মিত ধরা পড়ে; সম্ভবত সম্বন্ধ রূপ খুব নিয়মিত প্রসঙ্গে বসে বলে। আর আমাদের প্রশ্নের তালিকা ছোট, হাতে বানানো, আর প্রতিটি উত্তরের ভিন্ন বানানকে (রানী, রানি, রাণী) ভুল গোনা হয়েছে, তাই বাংলায় সংখ্যাটি একটু কমই দেখায়।

ব্যবহারের দিক থেকে analogy স্কোর দিয়ে embedding বাছাই করা ঝুঁকিপূর্ণ। যে model analogy-তে ভালো, সে আপনার শ্রেণিবিভাগ বা খোঁজে ভালো হবে, এমন নিশ্চয়তা নেই। অর্থ যে একটি পরিষ্কার জ্যামিতি, এই দাবিও এত সহজ নয়।

মূল উপলব্ধি: বিখ্যাত উদাহরণ একটি চিন্তার ছবি, মূল্যায়ন নয়। শর্তগুলো খুলে, পুরো তালিকায়, আর আসল কাজে মেপে দেখুন।

প্র ০২“গোনা” বনাম “অনুমান”: word2vec কি সত্যিই পুরোনো গোনার পদ্ধতিকে হারিয়েছিল?

২০১৪ সালে Baroni, Dinu আর Kruszewski-র প্রবন্ধের নামই ছিল “Don't count, predict!”। তাঁদের পরীক্ষায় অনুমান-ভিত্তিক vector বেশিরভাগ কাজে গোনা-ভিত্তিকের চেয়ে ভালো করে। তখনকার সাধারণ ধারণাও তাই দাঁড়ায়: neural পদ্ধতি নতুন কিছু শিখছে।

কিন্তু একই বছর Levy আর Goldberg দেখান, SGNS-এর সর্বোত্তম সমাধানে u·v প্রায় PMI − log k-এর সমান। অর্থাৎ যে যন্ত্রটি “অনুমান” করছে, সে পরোক্ষে একটি গোনার টেবিলকে ছোট করছে। পরের বছর Levy, Goldberg আর Dagan আরও এগোন: word2vec-এর সাথে আসা কৌশলগুলো (subsampling, ¾ ঘাত, ছোট window-র ওজন) গোনার পদ্ধতিতে যোগ করলে পার্থক্যের অনেকটা কমে। পার্থক্যের একটি বড় অংশ ছিল সেটিংয়ে, দর্শনে নয়।

আমাদের ব্লক ৩-এ এই সম্পর্কের একটি ছোট ছায়া দেখেছেন: co-occurrence ≥ ৫ এমন ঘরে SGNS-এর dot আর PMI − log K-এর সম্পর্ক। সেটি ছোট কৃত্রিম corpus, আর আমাদের Pn-এ ¾ ঘাত আছে, তাই হুবহু মেলার কথা নয়।

তাহলে কি দুটি একই? ব্যবহারে না। গোনার পথে V × V matrix রাখতে হয় (sparse হলেও বড়), আর SVD-র খরচ আছে। SGNS প্রতি জোড়ায় ধাপে ধাপে চলে, নতুন লেখা এলে চালিয়ে যাওয়া সহজ। GloVe একবার গুনে পরে দ্রুত চলে, তবে গোনার সময় মেমরি লাগে। আবার SVD প্রায় নির্ধারিত ফল দেয়, যেখানে SGNS-এর ফল seed-এ বদলায়। আর পরের ধাপের কাজে (পর্ব ৪৪) পুরো প্রশ্নটিই বদলে যায়, কারণ এক শব্দের এক vector ধারণাটিই ছেড়ে দেওয়া হয়।

মূল উপলব্ধি: নতুন পদ্ধতির জয় দেখলে জিজ্ঞেস করুন, পুরোনো পদ্ধতিকে কি একই যত্নে সাজানো হয়েছিল? অনেক “অগ্রগতি” পরীক্ষার ন্যায্যতার প্রশ্নেও আংশিক কমে যায়।

প্র ০৩SGNS-এ দুটি টেবিল কেন? একই টেবিল দিয়ে চললে কী সমস্যা হতো?

এক টেবিল হলে স্কোর হতো vc · vo. প্রথম সমস্যা নিজের সঙ্গে স্কোরে: v · v সবসময় অঋণাত্মক, আর vector বড় হলে অনেক বড়। Goldberg আর Levy (২০১৪) তাঁদের ব্যাখ্যামূলক নোটে এই কথাটি বলেন: model তখন একটি শব্দকে তার নিজের প্রসঙ্গে খুব সম্ভাব্য বলতে চায়, অথচ লেখায় একই শব্দ পরপর কমই বসে।

দ্বিতীয় ব্যাপারটি অর্থের। ধরুন “ইলিশ” আর “চিংড়ি”। এরা একই বাক্যে কম বসতে পারে, কিন্তু দুজনেরই পাশে “মাছ”, “বাজার”, “কেজি”। দুই টেবিলে দুজনের v একই রকম হয়, কারণ দুজনকেই একই u-গুলোর দিকে ঠেলা হয়। তাই “একই ধরনের প্রসঙ্গ” থেকে কাছাকাছি আসে, “পাশাপাশি বসা” থেকে নয়। এক টেবিলে দুটি ধারণা বেশি মিশে যায়। v-v cosine এবং v-u dot product তাই দুটি আলাদা প্রশ্নের উত্তর দিতে পারে: একই ধরনের শব্দ, আর পাশে বসা শব্দ।

তৃতীয়ত, শেষে কী রাখবেন? সাধারণত কেন্দ্রের টেবিল (v-গুলো) রাখা হয়। GloVe-তে w + w̃ যোগ করা হয়, কারণ সেখানে X প্রতিসম হলে দুই টেবিলের ভূমিকা সমান; পার্থক্য আসে শুধু শুরুর এলোমেলো মান থেকে, আর যোগে একটু উন্নতি পাওয়ার কথা প্রবন্ধে বলা হয়েছে। fastText-এ z শুধু কেন্দ্রের দিকে, আর u থাকে শব্দপ্রতি। কেন? কারণ অদেখা শব্দের দরকার পড়ে কেন্দ্রের দিকেই; আর প্রসঙ্গের দিকেও n-gram দিলে খরচ সম্ভবত অনেক বাড়ত।

আর পর্ব ৩৬-এর weight tying? সেখানে token embedding আর output layer একই টেবিল, কিন্তু মাঝে একটি পুরো Transformer আছে, তাই “নিজের সঙ্গে স্কোর” সমস্যাটি সেভাবে আসে না: output মাঝের state-এর সঙ্গে গুণ হয়, সরাসরি একই শব্দের vector-এর সঙ্গে নয়।

মূল উপলব্ধি: নকশার ছোট সিদ্ধান্তের পেছনে একটি নির্দিষ্ট গাণিতিক কারণ থাকে। “এটি না থাকলে কী ভাঙত?” প্রশ্নটি আপনাকে সেই কারণে পৌঁছে দেবে।

প্র ০৪বাংলায় কি fastText সবসময় word2vec-এর চেয়ে ভালো? বাংলার রূপতত্ত্বের জন্য আরও ভালো কিছু কি আছে?

Bojanowski ও সহকর্মীদের প্রবন্ধের মূল দাবিই ছিল যে সমৃদ্ধ morphology-র ভাষায় আর ছোট ডেটায় subword বেশি সাহায্য করে। বাংলা সেই ধরনের ভাষা: বিভক্তি, নির্দেশক (-টি, -টা, -গুলো), জোর দেওয়া (-ও, -ই), আর সমাস। আমাদের run-এ “নদীগুলোতেও”, “শিক্ষকদেরকেও” সুন্দর ফল দিল, যেখানে word2vec-এ কিছুই নেই।

কিন্তু “সবসময়” ভুল। আমাদের একই run-এ “বিপরীত” analogy-তে word2vec ৩১%, fastText ৪.৮%; “ডাক্তার”-এর পাশে “আক্তার”; “মাছগুলোকে”-র পাশে “বলগুলোকে”। n-gram বানান দেখে, অর্থ নয়। বাংলায় শব্দের শেষের টুকরো বেশি ঘন, তাই সেগুলো ছোট মূল শব্দকে ঢেকে দিতে পারে। আবার বানানের মিল সবসময় আত্মীয়তা নয়: “বাজারি”-র কাছে “বাজার” যুক্তিসঙ্গত, কিন্তু “কলমি”-র প্রতিবেশীতে আমাদের run কলমি শাকের কিছুই দেয়নি (ঝরকা, গুইসাপ…)।

তাহলে বিকল্প কী? এক, stemming বা morphological analyzer দিয়ে আগে মূল শব্দ বের করা (AI-এর ভিত্তি কোর্সের পর্ব ৬৭), কিন্তু বাংলায় ভালো সরঞ্জাম সীমিত আর অতিরিক্ত ছাঁটার ঝুঁকি আছে। দুই, min_n বাড়ানো, যাতে ছোট শেষাংশ কম প্রভাব ফেলে; এটি পরীক্ষা করে দেখার বিষয়, আমরা চালাইনি। তিন, code point-এর বদলে grapheme ধরে n-gram; ধারণাটি যুক্তিসঙ্গত, কিন্তু কতটা লাভ, তা আপনার ডেটায় মাপতে হবে। চার, পর্ব ৪০-এর subword tokenizer আর পরের contextual model, যেখানে প্রসঙ্গ আর subword দুটিই একসঙ্গে কাজ করে।

আর Banglish? “bazar” আর “বাজার”-এর একটি n-gram-ও মেলে না, তাই fastText এখানে হরফের সীমা পেরোতে পারে না; দুটি একই corpus-এ একই প্রসঙ্গে বসলে তবেই কাছে আসতে পারে।

মূল উপলব্ধি: OOV কমানো আর অর্থ ধরা দুটি আলাদা লক্ষ্য। fastText প্রথমটিতে শক্তিশালী, দ্বিতীয়টিতে তার দাম আছে; আপনার ডেটায় কোনটি বেশি জরুরি, তা মেপে ঠিক করুন।

প্র ০৫Embedding-এর bias কে দেখবে, আর সেই “দিক” মুছে দিলেই কি সমস্যা শেষ?

Bolukbasi ও সহকর্মীদের ২০১৬ সালের প্রবন্ধের নাম “Man is to Computer Programmer as Woman is to Homemaker?”। তাঁরা ইংরেজি সংবাদের word2vec-এ একটি লিঙ্গের দিক খুঁজে পান, পেশার শব্দ সেই দিকে ফেলে দেখান, আর সেই দিকটি সরানোর একটি পদ্ধতি দেন। একই সময়ে Caliskan, Bryson আর Narayanan ২০১৭ সালে Science-এ দেখান, vector-এর দূরত্বে মানুষের মধ্যে মাপা অনেক সংশ্লিষ্টতার ছাপ পাওয়া যায়।

তারপর দুটি সংশোধন আসে। Gonen আর Goldberg (২০১৯) “Lipstick on a Pig”-এ দেখান, দিকটি সরানোর পরও পক্ষপাতের শব্দগুলো গুচ্ছ হয়ে কাছাকাছি থাকে, আর একটি classifier তাদের আগের দিক আন্দাজ করতে পারে। অর্থাৎ পক্ষপাত একটি দিকে নয়, পুরো জ্যামিতিতে ছড়ানো। আর Nissim ও সহকর্মীরা দেখান, কিছু বিখ্যাত উদাহরণ analogy-র নিয়মের ফলও ছিল।

আমাদের বাংলা পরীক্ষাটি তাই সতর্ক থাকার উদাহরণ। “নার্স” ধনাত্মক এলো, “গায়ক” ঋণাত্মক। কেউ যদি শুধু “ডাক্তার +০.০৮”, “গৃহিণী −০.০১” বেছে দেখান, একটি গল্প বানানো যায়; কেউ “নার্স” দেখিয়ে উল্টো গল্প। ১৩টি শব্দ, একটি corpus-এর অংশ, এত ছোট মান। ডেটার ধরনও প্রশ্ন: বাংলা Wikipedia কে লিখেছেন, কাদের নিয়ে লেখা, সেটি বাংলাদেশের সব মানুষের ভাষার প্রতিনিধি কি না, আমরা জানি না।

দায়িত্ব কার? আমরা যা বলতে পারি: vector-এর বাইরে আসল সিদ্ধান্তে কী ঘটে, সেখানে মাপা বেশি কাজের। ধরুন বাজারঘর চাকরির আবেদন বাছছে না, কিন্তু রিভিউ ছাঁকছে: কোনো উপভাষা বা নাম দেখে কি ভুল বেশি হয়? এটি দলভিত্তিক মাপ (মেশিন লার্নিং কোর্সের পর্ব ৬০), আর কাজের ধরন অনুযায়ী মানুষের তদারকি। AI নৈতিকতা নিয়ে আরও গভীর আলোচনা পরের একটি কোর্সে আসবে।

মূল উপলব্ধি: bias vector-এর একটি ছোট দাগ নয় যে মুছে দিলেই হলো। ডেটার উৎস, মাপার পদ্ধতি আর সিদ্ধান্তের ক্ষতি, তিনটি একসঙ্গে দেখতে হয়।

অনুশীলন

হাতে হিসাব করে তারপর মিলিয়ে নিন।

  1. একটি update হাতে: v = [1, 0], uo = [0.5, 0.5], un = [−1, 0.5], η = 0.1. L, ∂L/∂v আর নতুন v বের করুন।

    uo·v = 0.5, un·v = −1. σ(0.5) = 0.6225, σ(−1) = 0.2689.

    L = −ln 0.6225 − ln(1 − 0.2689) = 0.4741 + 0.3133 ≈ 0.7873.

    ∂L/∂v = −0.3775·[0.5, 0.5] + 0.2689·[−1, 0.5] = [−0.1888, −0.1888] + [−0.2689, 0.1345] ≈ [−0.4577, −0.0543].

    নতুন v = [1, 0] − 0.1·[−0.4577, −0.0543] = [1.0458, 0.0054].

  2. ¾ ঘাত আর subsampling: (ক) f = [0.5, 0.4, 0.1] হলে Pn কত? (খ) t = 10−4 আর f = 0.01 হলে প্রবন্ধের সূত্রে ফেলার সম্ভাবনা কত? f = 10−4 হলে?

    (ক) 0.50.75 = 0.5946, 0.40.75 = 0.5030, 0.10.75 = 0.1778; যোগ 1.2754. ভাগ করে [0.466, 0.394, 0.139]. বিরল শব্দের ভাগ 0.1 থেকে প্রায় 0.14.

    (খ) 1 − √(10−4 ÷ 0.01) = 1 − 0.1 = 0.9. দ্বিতীয়টিতে 1 − √1 = 0: এই কম্পাঙ্কের নিচে কোনো শব্দ ফেলা হয় না।

  3. আউটপুট অনুমান করুন: W = ২ আর ৬ শব্দের বাক্যে কতটি skip-gram জোড়া? আর এই কোড কী ছাপবে?
    w = "ঘরের"
    b = "<" + w + ">"
    print(len(w), [b[i:i+3] for i in range(len(b)-2)])

    জোড়া: ২ + ৩ + ৪ + ৪ + ৩ + ২ = ১৮.

    কোড ছাপবে 4 ['<ঘর', 'ঘরে', 'রের', 'ের>']. “ঘরের” চারটি code point (ঘ, র, ে, র), কারণ ে আলাদা code point; সীমানাসহ ৬ চিহ্নে ৪টি ৩-gram.

  4. সমালোচনা করুন: একজন সহকর্মী লিখেছেন: “বাজারঘরের ৫,০০০ রিভিউতে word2vec train করলাম। ‘ভালো’-র প্রতিবেশী ‘সুন্দর’, আর রাজা-রানীর analogy মিলেছে। তাই model অর্থ বোঝে, এটি দিয়ে রিভিউ খোঁজা চালু করি।” কী কী প্রশ্ন করবেন?

    মূল্যায়ন: দুটি বেছে নেওয়া উদাহরণ কোনো মাপ নয়; রিভিউ খোঁজার recall@k আর MRR (AI-এর ভিত্তি কোর্সের পর্ব ৬৯) কোথায়? TF-IDF baseline কত? ডেটা: ৫,০০০ রিভিউ খুবই কম; রাজা-রানী সেখানে এলো কীভাবে? অস্থিরতা: দুটি seed-এ প্রতিবেশী কতটা মেলে? OOV: নতুন পণ্যের নাম আর বানানভেদে কী হবে? fastText বা sentence model কি ভালো? মনোভাব: “ভালো”-র পাশে “খারাপ” আসে কি? রিভিউতে এটি বড় সমস্যা। ভাষা: “অর্থ বোঝে” দাবি নয়; “প্রসঙ্গে মিল পেয়েছে” বলাই ঠিক। ব্যক্তিগত তথ্য: রিভিউতে নাম বা ফোন নম্বর আছে কি?

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

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

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

  1. লেখা NFC করে token-এ ভাগ করা, vocabulary বানানো
  2. ঘন শব্দ subsampling করে window থেকে (c, o) জোড়া বানানো
  3. প্রতি জোড়ায় f3/4 নিয়মে k-টি negative তোলা
  4. σ(u·v) থেকে loss আর gradient হিসাব করা
  5. vc, uo আর negative-এর সারিগুলো update করা
  6. শেষে V রেখে প্রতিবেশী দেখা আর আসল কাজে মাপা

Vocabulary V = ১,০০,০০০ আর k = ৫ হলে একটি skip-gram জোড়ায় negative sampling কয়টি dot product হিসাব করে?

  • ১,০০,০০০
  • ৬
  • ৫
  • ১০১০

SGNS-এ loss L = −log σ(uo·v) − log σ(−un·v) হলে ∂L/∂un কত?

  • −(1 − σ(uo·v)) v
  • −σ(un·v) v
  • σ(un·v) v
  • σ(uo·v) un

Negative sampling-এ unigram কম্পাঙ্কের ওপর 3/4 ঘাত দিলে কী হয়?

  • ঘন শব্দের ভাগ কিছুটা কমে, বিরল শব্দের ভাগ বাড়ে
  • ঘন শব্দের ভাগ আরও বাড়ে
  • ভাগ অপরিবর্তিত থাকে
  • সব শব্দের ভাগ সমান হয়

Training-এ কখনো না-আসা “বাজারঘরের” শব্দে gensim-এর FastText আর Word2Vec কী করে?

  • দুটিই একই vector দেয়
  • দুটিই শূন্য vector দেয়
  • FastText KeyError দেয়, Word2Vec n-gram গড় করে
  • FastText n-gram-এর গড় দেয়, Word2Vec KeyError দেয়

GloVe-র loss কোন রাশির কাছাকাছি wi·w̃j + bi + b̃j-কে আনতে চায়?

  • Xij
  • log Xij
  • PPMI(i, j)
  • σ(uj·vi)

একটি প্রতিবেদনে শুধু “king − man + woman ≈ queen” দেখিয়ে বলা হলো embedding ভালো। সবচেয়ে সঠিক প্রতিক্রিয়া কোনটি?

  • এক উদাহরণে মিলেছে, তাই model অর্থ বুঝেছে
  • Analogy পুরোপুরি অর্থহীন, তাই embedding-ও অকেজো
  • শর্ত (প্রশ্নের শব্দ বাদ কি না), পুরো তালিকায় হার আর আসল কাজে baseline-এর সঙ্গে মাপ চাওয়া
  • Vector-এর মাত্রা দ্বিগুণ করে আবার একই উদাহরণ দেখা

শব্দকোষ

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

মনে রাখুন

  1. SGNS: L = −log σ(uo·vc) − ∑ log σ(−un·vc); প্রতি জোড়ায় k + ১টি dot product, negative আসে f3/4 থেকে।
  2. গোনার পথ (PPMI + SVD, GloVe-র log X) আর অনুমানের পথ (SGNS ≈ PMI − log k) একই পরিবারের; সেটিং প্রায়ই দর্শনের সমান গুরুত্বপূর্ণ।
  3. fastText-এ শব্দ = n-gram-এর গড়: OOV-তে vector পায়, কিন্তু বানানের আত্মীয়রাও কাছে আসে (“ডাক্তার”–“আক্তার”)।
  4. মূল্যায়ন: প্রতিবেশী আর analogy শুধু সংকেত। শর্ত, seed আর আসল কাজ দেখুন; একটি উদাহরণে কিছু প্রমাণ হয় না।
  5. প্রতিটি শব্দের একটিই vector. “কাল”-এর দুই অর্থ এখানে মিশে যায়, যা পরের contextual model সমাধান করতে চায়।

পরের পর্বে: এই vector কাজে লাগানো: বাংলা রিভিউয়ের শ্রেণিবিভাগে TF-IDF baseline, bag-of-embeddings, CNN, BiLSTM আর ছোট Transformer এক ডেটায় মুখোমুখি (পর্ব ৪২)।

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

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