এই পর্বে যা শিখবেন
- Markov property কী, আর কখন এটি যুক্তিসংগত, কখন বিপজ্জনক অনুমান
- Transition matrix লেখা, n ধাপের সম্ভাবনা হাতে আর
np.linalg.matrix_powerদিয়ে, absorbing state আর শোষিত হওয়ার গড় সময় - Stationary distribution তিন পথে বের করা, ergodicity-র সহজ মানে, আর PageRank-এর random surfer
- বাংলা লেখায় bigram model বানিয়ে seed দিয়ে নতুন লেখা তৈরি, আর তার সীমা
- Hidden Markov model-এর তিন প্রশ্ন; forward algorithm, Viterbi হাতে ও কোডে, log-space, Baum–Welch-এর ধারণা আর hmmlearn
আগের তিনটি পর্বে (পর্ব ৫০ Bayes-এর উপপাদ্য, ৫১ Naive Bayes, ৫২ Bayesian network) অনিশ্চয়তা ছিল এক মুহূর্তের ছবি: প্রমাণ এল, বিশ্বাস বদলাল। এই পর্বে নতুন একটি মাত্রা যোগ হচ্ছে: সময়. এমন ক্রম নিয়ে যুক্তির সবচেয়ে পুরোনো আর এখনো কাজের হাতিয়ার হলো Markov chain আর তার লুকানো রূপ, hidden Markov model. সম্ভাবনার মৌলিক নিয়ম (conditional probability, total probability) আছে পর্ব ৩৫-এ, আর matrix গুণ ও eigenvalue পর্ব ১৭-এ; এখানে শুধু দরকারটুকু মনে করিয়ে দেওয়া হবে। এই পর্বের সব সম্ভাবনা বাজারঘর (এই কোর্সের কাল্পনিক অনলাইন দোকান) নিয়ে বানানো, কোনো আসল ডেটা নয়।
১ · ক্রম আর Markov property — “ভবিষ্যৎ শুধু বর্তমানের ওপর”
অনেক ডেটা আসে ক্রম (sequence) হয়ে: প্রতি মাসে একজন ক্রেতার অবস্থা, প্রতি ঘণ্টায় রাস্তার অবস্থা, একটি বাক্যের শব্দগুলো। এমন ক্রমের পুরো সম্ভাবনা লিখতে পর্ব ৩৫-এর chain rule লাগে:
P(X1, X2, …, XT) = P(X1) · P(X2 | X1) · P(X3 | X1, X2) ⋯ P(XT | X1, …, XT−1)
এখানে Xt মানে সময় t-তে অবস্থা, আর “|” মানে “জানা থাকলে”। সমস্যা হলো শেষের পদগুলো: ১২ মাসের ইতিহাসে প্রতিটি মাসে ৪টি সম্ভাব্য অবস্থা থাকলে ইতিহাসের রকম 411, ৪১ লাখের বেশি। প্রতিটির জন্য আলাদা সম্ভাবনা মাপার মতো ডেটা কারও কাছে নেই।
১৯০৬ সালের দিকে রুশ গণিতবিদ Andrey Markov একটি সাহসী সরলীকরণ নিয়ে কাজ শুরু করেন, যা আজ তাঁর নামে পরিচিত। Markov property বলে:
P(Xt+1 | X1, …, Xt) = P(Xt+1 | Xt)
কথায়: বর্তমান অবস্থা জানলে অতীত আর কোনো বাড়তি খবর দেয় না। তখন পুরো ক্রমের সম্ভাবনা হয়ে যায় P(X1) আর পাশাপাশি জোড়ার সম্ভাবনার গুণফল, আর মাপতে হয় শুধু “এই অবস্থা থেকে ওই অবস্থায়” যাওয়ার সম্ভাবনা। এমন অনুমান মেনে চলা ক্রমের মডেলকে বলে Markov chain.
লুডু খেলার কথা ভাবুন। আপনার গুটি এখন ২৩ নম্বর ঘরে। পরের চালে কোথায় যাবে, তা নির্ভর করে শুধু এই ঘর আর ছক্কার ওপর; গুটিটি আগের চালে ১৭-তে ছিল না ২০-তে, তাতে কিছু আসে যায় না। এটিই Markov property.
কখন অনুমানটি যুক্তিসংগত, কখন নয়
| পরিস্থিতি | Markov অনুমান | কেন |
|---|---|---|
| লুডু, সাপ-লুডু, ছক্কার খেলা | ঠিক খাটে | নিয়মই এমন: পরের ঘর শুধু এখনকার ঘর আর ছক্কায় ঠিক হয় |
| ক্রেতার মাসিক অবস্থা | মোটামুটি খাটে | অবস্থাটি যদি যথেষ্ট তথ্য ধরে (শেষ কেনার সময়, কতবার কেনেন), তবে কাজ চলে; ঈদের মাসে সম্ভাবনা বদলে গেলে সাবধান |
| লম্বা বাক্যের অর্থ | দুর্বল | বাক্যের শুরুর কর্তা শেষের ক্রিয়া ঠিক করে; শুধু আগের শব্দ দেখলে তা হারায় |
লক্ষ করুন, “Markov কি না” অনেকটা নির্ভর করে আপনি অবস্থা কীভাবে সংজ্ঞায়িত করছেন তার ওপর। অবস্থায় যত বেশি তথ্য রাখবেন (যেমন “গত দুই মাসের অবস্থা” একসঙ্গে), অনুমান তত সত্যের কাছে যাবে, কিন্তু অবস্থার সংখ্যা তত বাড়বে আর মাপতে তত বেশি ডেটা লাগবে। এ ছাড়া এই পর্বে ধরে নেওয়া হবে যে স্থানান্তরের সম্ভাবনা সময়ের সঙ্গে বদলায় না (time-homogeneous chain).
Markov ১৯০৬ সালের প্রবন্ধে দেখান যে পরস্পর-নির্ভর ঘটনার ক্রমেও বড় সংখ্যার নিয়ম (law of large numbers) খাটতে পারে; প্রচলিত বর্ণনা অনুযায়ী এটি ছিল সমসাময়িক গণিতবিদ Pavel Nekrasov-এর একটি দাবির জবাব, যে এই নিয়মের জন্য স্বাধীনতা জরুরি। ১৯১৩ সালে তিনি Pushkin-এর পদ্যোপন্যাস “Eugene Onegin”-এর প্রথম প্রায় ২০,০০০ অক্ষর হাতে গুনে দেখান যে একটি স্বরবর্ণের পরে আরেকটি স্বরবর্ণ আসার সম্ভাবনা ব্যঞ্জনবর্ণের পরে আসার সম্ভাবনা থেকে অনেক আলাদা। এটিকে প্রায়ই লেখার ওপর Markov chain-এর প্রথম প্রয়োগ বলা হয়।
২ · বাজারঘরের ক্রেতার Markov chain — ছবি আর transition matrix
বাজারঘরের ব্যবস্থাপক প্রতি মাসের শেষে প্রত্যেক ক্রেতাকে চারটি দলের একটিতে রাখেন: Active (এ মাসে একাধিকবার কিনেছেন), Occasional (একবার কিনেছেন), Dormant (কেনেননি, কিন্তু অ্যাকাউন্ট আছে) আর Churned (তিন মাস কোনো সাড়া নেই, মানে ছেড়ে গেছেন বলে ধরা)। “ফিরে আসুন” ছাড়ের অফার পেয়ে কিছু Churned ক্রেতা আবার ফেরেন। নিচের সম্ভাবনাগুলো উদাহরণের জন্য বানানো।
একই তথ্য একটি টেবিলে লিখলে তাকে বলে transition matrix P. সারি মানে “এই মাসের অবস্থা”, কলাম মানে “পরের মাসের অবস্থা”, আর ঘর Pij = P(Xt+1 = j | Xt = i).
| এ মাসে ↓ / পরের মাসে → | Active | Occasional | Dormant | Churned | সারির যোগ |
|---|---|---|---|---|---|
| Active | 0.70 | 0.20 | 0.08 | 0.02 | 1.00 |
| Occasional | 0.30 | 0.40 | 0.25 | 0.05 | 1.00 |
| Dormant | 0.10 | 0.20 | 0.50 | 0.20 | 1.00 |
| Churned | 0.10 | 0.05 | 0.05 | 0.80 | 1.00 |
প্রতিটি সারির যোগ ঠিক 1, আর প্রতিটি ঘর 0 থেকে 1-এর মধ্যে। কারণ এ মাসে Active একজন ক্রেতা পরের মাসে চারটির কোনো একটিতে থাকবেনই। কলামের যোগ 1 হওয়ার কোনো দরকার নেই। এমন matrix-কে row-stochastic matrix বলে। কোড লিখে সবার আগে P.sum(axis=1) দেখে নিন; অনেক বই আর library উল্টো রীতি (কলাম = এখনকার অবস্থা) ব্যবহার করে, আর এই গোলমাল খুব সাধারণ ভুল।
এক ধাপ মানে vector × matrix
শুধু একজন ক্রেতা নয়, “কোন অবস্থায় থাকার সম্ভাবনা কত”, এটিকে একটি সারি-vector xt দিয়ে লেখা যায়। আজ ক্রেতাটি নিশ্চিতভাবে Active, তাই x0 = [1, 0, 0, 0]. পরের মাসের vector:
xt+1 = xt P, মানে xt+1[j] = ∑i xt[i] · Pij
এটি পর্ব ৩৫-এর total probability: পরের মাসে j-তে পৌঁছানোর সব পথ (প্রতিটি সম্ভাব্য এখনকার অবস্থা i থেকে) যোগ। প্রথম মাসে x1 = [1, 0, 0, 0] P = P-এর প্রথম সারি = [0.70, 0.20, 0.08, 0.02]. দ্বিতীয় মাসে কী হয়, ধাপে ধাপে দেখুন। এটি পর্ব ১৭-এর matrix-vector গুণ, শুধু vector বাঁ দিকে।
1x1 = [0.70, 0.20, 0.08, 0.02]2for j in range(4):3 x2[j] = sum(x1[i] * P[i, j])4print(x2, x2.sum())
| কলাম j | |
| চারটি পথ | |
| x2[j] | |
| চলতি যোগ |
- শুরুর vector. এক মাস পরে ক্রেতাটি Active-এ 0.70, Occasional-এ 0.20, Dormant-এ 0.08, Churned-এ 0.02 সম্ভাবনায় আছেন। এটিই P-এর Active সারি।
- Active কলাম. চারটি পথ: Active থেকে Active (0.70 × 0.70 = 0.49), Occasional থেকে (0.20 × 0.30 = 0.06), Dormant থেকে (0.08 × 0.10 = 0.008), Churned থেকে (0.02 × 0.10 = 0.002). যোগ 0.56.
- Occasional কলাম. 0.70 × 0.20 + 0.20 × 0.40 + 0.08 × 0.20 + 0.02 × 0.05 = 0.14 + 0.08 + 0.016 + 0.001 = 0.237.
- Dormant কলাম. 0.056 + 0.05 + 0.04 + 0.001 = 0.147.
- Churned কলাম. 0.014 + 0.01 + 0.016 + 0.016 = 0.056. দুই মাসে ছেড়ে যাওয়ার সম্ভাবনা ২% থেকে প্রায় ৬% হলো।
- যাচাই. চারটির যোগ 0.56 + 0.237 + 0.147 + 0.056 = 1.000. প্রতিটি ধাপে সম্ভাবনা শুধু অবস্থাগুলোর মধ্যে সরে, হারায় না।
৩ · n ধাপের সম্ভাবনা — matrix power আর absorbing state
দুই ধাপ পরের vector x2 = x1 P = x0 P P = x0 P2. সাধারণভাবে xn = x0 Pn, আর Pn-এর ঘর (i, j) মানে “i থেকে শুরু করে ঠিক n ধাপ পরে j-তে থাকার সম্ভাবনা”। এই সম্পর্কটির নাম Chapman–Kolmogorov সমীকরণ: Pm+n = Pm Pn.
হাতে ২ ধাপ: একটি ঘর
Dormant একজন ক্রেতা ২ মাস পরে Active হবেন, তার সম্ভাবনা (P2)D,A. মাঝের মাসে তিনি চারটির যেকোনোটিতে থাকতে পারেন:
- D → A → A: 0.10 × 0.70 = 0.070
- D → O → A: 0.20 × 0.30 = 0.060
- D → D → A: 0.50 × 0.10 = 0.050
- D → C → A: 0.20 × 0.10 = 0.020
যোগ 0.200. মানে আজ ঘুমিয়ে থাকা ক্রেতার ২ মাস পরে আবার নিয়মিত হওয়ার সম্ভাবনা ২০%, যদিও সরাসরি এক মাসে মাত্র ১০%. পুরো P2 (লেখকের যন্ত্রে NumPy দিয়ে হিসাব করা):
Active Occas. Dormant Churned
Active 0.5600 0.2370 0.1470 0.0560
Occasional 0.3600 0.2725 0.2515 0.1160
Dormant 0.2000 0.2100 0.3180 0.2720
Churned 0.1700 0.0900 0.0855 0.6545
প্রথম সারিটি stepper-এর x2, আর Dormant সারির প্রথম ঘর হাতের 0.200. P2-এর প্রতিটি সারির যোগও 1: দুটি stochastic matrix-এর গুণফলও stochastic. NumPy-তে Pn লেখা হয় np.linalg.matrix_power(P, n) দিয়ে; P ** n লিখলে প্রতিটি ঘরের আলাদা ঘাত হয়, যা একেবারে ভুল উত্তর দেয় (পর্ব ১৭-এর * বনাম @-এর ফাঁদ)।
Absorbing state — অফার বন্ধ হলে
ধরুন বাজারঘর “ফিরে আসুন” অফার বন্ধ করল, আর Churned ক্রেতা আর ফেরেন না। তখন Churned সারি হয় [0, 0, 0, 1]: একবার ঢুকলে আর বের হওয়া যায় না। এমন অবস্থাকে বলে absorbing state, আর বাকি অবস্থাগুলোকে transient (ক্ষণস্থায়ী)। যেকোনো transient অবস্থা থেকে absorbing-এ পৌঁছানোর পথ থাকলে শেষ পর্যন্ত সব সম্ভাবনা সেখানেই জমে: লেখকের যন্ত্রে হিসাবে এই নতুন chain-এ Active থেকে শুরু করে ২৪ মাস পরে Churned-এ থাকার সম্ভাবনা প্রায় 0.82.
কত মাস লাগবে গড়ে? transient অংশের ৩ × ৩ matrix Q নিয়ে (Active, Occasional, Dormant-এর মধ্যে স্থানান্তর) N = (I − Q)−1 বানাতে হয়, যাকে fundamental matrix বলে; এখানে I হলো identity matrix আর −1 মানে inverse. N-এর ঘর (i, j) হলো i থেকে শুরু করে শোষিত হওয়ার আগে j-তে গড়ে কত মাস কাটে। প্রতিটি সারির যোগই শোষিত হওয়ার গড় সময়। এই উদাহরণে তা Active থেকে প্রায় ১৫.২৬ মাস, Occasional থেকে ১৩.৬৮, Dormant থেকে ১০.৫৩ (অনুশীলন ৪-এ কোড আর simulation দিয়ে যাচাই)।
৪ · Stationary distribution — তিন পথে একই উত্তর
অফারসহ আসল chain-এ ফিরে আসি। Active থেকে শুরু করলে প্রথম মাসগুলোতে vector দ্রুত বদলায়, কিন্তু অনেক মাস পরে তা একটি নির্দিষ্ট vector-এর কাছে স্থির হয়, আর শুরু কোথা থেকে হয়েছিল তা প্রায় ভুলে যায়। সেই vector π-কে বলে stationary distribution: এমন একটি distribution, যাকে P দিয়ে গুণ করলে বদলায় না।
π P = π, ∑i πi = 1
ঢাকার একটি বড় মার্কেটের চারটি তলার কথা ভাবুন। প্রতি ঘণ্টায় কিছু মানুষ ওপরে যান, কিছু নিচে নামেন, কেউ কেউ বেরিয়ে যান আর নতুন কেউ ঢোকেন। ব্যক্তিরা ঘুরছেন, কিন্তু প্রতিটি তলায় মোট ভিড়ের ভাগ মোটামুটি একই থাকে। Stationary distribution ব্যক্তির অবস্থা নয়, এই ভারসাম্যের ভাগ।
তিনটি পথ
- বারবার গুণ (power iteration): যেকোনো x0 থেকে x ← x P অনেকবার। চার অবস্থার এই ছোট chain-এ ২০০ বার যথেষ্টের চেয়ে বেশি।
- Eigenvector: π P = π মানে π হলো P-এর বাঁ দিকের eigenvector, যার eigenvalue 1. NumPy-র
np.linalg.eigডান দিকের eigenvector দেয় (A v = λ v), তাই P-এর transpose নিতে হয়: PT πT = πT. তারপর eigenvector-কে তার যোগ দিয়ে ভাগ করে distribution বানাতে হয়, কারণ eigenvector-এর দৈর্ঘ্য বা চিহ্ন নির্দিষ্ট নয়। - Simulation: একজন কাল্পনিক ক্রেতাকে অনেক মাস ধরে random ভাবে চালিয়ে দেখা, কত ভাগ মাস তিনি কোন অবস্থায় কাটালেন। এটি পর্ব ৩৬-এর Monte Carlo.
নিচের কোড প্রথমে অংশ ২ আর ৩-এর সংখ্যা মেলায়, তারপর তিন পথে π বের করে, আর শেষে simulation-এর প্রথম ১২ মাসের পথ (অক্ষরের প্রথম অক্ষর: A, O, D, C) দেখায়। প্রথমবার চালালে ব্রাউজারে NumPy নামতে কয়েক সেকেন্ড লাগতে পারে; SciPy থাকা ব্লকে আরও কয়েক সেকেন্ড।
import numpy as np
from bisect import bisect_right
np.set_printoptions(precision=4, suppress=True)
states = ["Active", "Occasional", "Dormant", "Churned"]
P = np.array([[0.70, 0.20, 0.08, 0.02], # সারি = এই মাস
[0.30, 0.40, 0.25, 0.05], # কলাম = পরের মাস
[0.10, 0.20, 0.50, 0.20],
[0.10, 0.05, 0.05, 0.80]])
print("সারির যোগ:", P.sum(axis=1))
x0 = np.array([1.0, 0, 0, 0]) # ক্রেতা এখন Active
print("১ মাস পরে:", x0 @ P)
print("২ মাস পরে:", x0 @ np.linalg.matrix_power(P, 2))
for n in (6, 12, 50):
print(f"P^{n}-এর Active সারি:", np.linalg.matrix_power(P, n)[0])
x = x0.copy() # পথ ১: বারবার গুণ
for _ in range(200):
x = x @ P
print("বারবার গুণ :", x)
w, V = np.linalg.eig(P.T) # পথ ২: πP = π
v = V[:, np.argmin(np.abs(w - 1))].real
print("eigenvalue :", np.sort(w.real)[::-1])
print("eigenvector :", v / v.sum())
rng = np.random.default_rng(42) # পথ ৩: simulation
cum = P.cumsum(axis=1).tolist()
s, counts, path = 0, np.zeros(4), []
for t, u in enumerate(rng.random(20_000)):
s = min(bisect_right(cum[s], u), 3)
counts[s] += 1
if t < 12:
path.append(states[s][0])
print("প্রথম ১২ মাস:", " ".join(path))
print("simulation :", counts / counts.sum())আসল আউটপুট:
সারির যোগ: [1. 1. 1. 1.]
১ মাস পরে: [0.7 0.2 0.08 0.02]
২ মাস পরে: [0.56 0.237 0.147 0.056]
P^6-এর Active সারি: [0.3909 0.2201 0.1961 0.1929]
P^12-এর Active সারি: [0.3553 0.2031 0.1855 0.2561]
P^50-এর Active সারি: [0.3499 0.1998 0.1827 0.2676]
বারবার গুণ : [0.3499 0.1998 0.1827 0.2676]
eigenvalue : [1. 0.7251 0.4979 0.177 ]
eigenvector : [0.3499 0.1998 0.1827 0.2676]
প্রথম ১২ মাস: O O D D A D D D O O O D
simulation : [0.3513 0.2042 0.1834 0.2611]
প্রথম তিন লাইন অংশ ২-এর stepper আর অংশ ৩-এর হাতের হিসাব মেলায়। এরপর দেখুন Pn-এর Active সারি কীভাবে বদলায়: ৬ মাসে Churned-এর সম্ভাবনা 0.19, ১২ মাসে 0.26, আর ৫০ মাসে 0.2676-এ স্থির। বারবার গুণ আর eigenvector হুবহু একই π = [0.3499, 0.1998, 0.1827, 0.2676] দিয়েছে। eigenvalue-গুলোর মধ্যে সবচেয়ে বড়টি ঠিক 1, বাকিগুলো 1-এর চেয়ে ছোট; দ্বিতীয় বড়টি (0.7251) বলে vector কত দ্রুত π-এর কাছে আসে: শুরুর পার্থক্য প্রতি মাসে মোটামুটি ওই গুণিতকে ছোট হয়, তাই ১২ মাসে প্রায় 0.725112 ≈ 0.02 গুণ।
একজন ব্যক্তির পথ খুবই এলোমেলো, কিন্তু ২০,০০০ মাসের গড় [0.3513, 0.2042, 0.1834, 0.2611], eigenvector-এর খুব কাছে, পুরোপুরি এক নয়, কারণ নমুনা সীমিত আর পরপর মাসগুলো পরস্পর-নির্ভর। ব্যবসার অর্থ: এই সম্ভাবনা চলতে থাকলে দীর্ঘমেয়াদে প্রায় ২৭% ক্রেতা যেকোনো মাসে Churned অবস্থায় থাকবেন। কোডে bisect_right একটি uniform সংখ্যাকে সারির cumulative sum-এর সঙ্গে তুলনা করে পরের অবস্থা বাছে।
Ergodicity — কখন π একটিই, আর সবাই সেখানে পৌঁছায়
সব chain-এ এমন সুন্দর ফল আসে না। সসীম অবস্থার একটি chain-কে ergodic বলা হয়, সহজ কথায়, যখন দুটি শর্ত মেলে:
- সব অবস্থা থেকে সব অবস্থায় পৌঁছানো যায় (কয়েক ধাপে হলেও); একে irreducible বলে। Absorbing Churned থাকলে এটি ভাঙে: সেখান থেকে আর কোথাও যাওয়া যায় না।
- কোনো বাঁধা চক্র নেই; একে aperiodic বলে। উদাহরণ: P = [[0, 1], [1, 0]] হলে অবস্থা প্রতিবার উল্টায়: A, B, A, B… π = [0.5, 0.5] আছে, কিন্তু Active থেকে শুরু করা vector কখনো সেখানে স্থির হয় না, শুধু দোলে।
এই দুটি শর্ত মানলে π একটিই, সব শুরু থেকেই xn তার দিকে যায়, আর একটি লম্বা simulation-এ প্রতিটি অবস্থায় কাটানো সময়ের ভাগও π-এর কাছে যায়। বাজারঘরের chain-এর সব ঘর ধনাত্মক, তাই দুটি শর্তই মেলে।
আরও গভীরে: “ধনাত্মক stochastic matrix-এর সবচেয়ে বড় eigenvalue ঠিক 1, আর তার eigenvector-এর সব ঘর ধনাত্মক”, এই নিশ্চয়তা আসে Perron–Frobenius উপপাদ্য থেকে। আরেকটি সুন্দর ফল: অবস্থা i-তে ফিরে আসার গড় সময় 1/πi (Kac-এর ফল নামে পরিচিত); এখানে Active থাকা কোনো মাস থেকে গুনে পরের বার Active হতে গড়ে 1/0.3499 ≈ 2.86 মাস লাগে (পরের মাসেই Active থাকলে সেটি ১ মাস ধরা হয়)।
৫ · PageRank — বিখ্যাত একটি Markov chain
Stationary distribution-এর সবচেয়ে বিখ্যাত প্রয়োগগুলোর একটি এসেছে web search থেকে। ১৯৯৮ সালে Sergey Brin আর Lawrence Page তাঁদের প্রবন্ধ “The Anatomy of a Large-Scale Hypertextual Web Search Engine”-এ (WWW7 সম্মেলন) PageRank বর্ণনা করেন। ধারণাটি হলো একজন random surfer: তিনি একটি পাতায় আছেন, পাতার link-গুলোর একটিতে random ভাবে চাপেন, তারপর আবার, এভাবে চলতে থাকেন। মাঝে মাঝে বিরক্ত হয়ে তিনি link ছেড়ে একেবারে যেকোনো একটি পাতায় চলে যান। অনেকক্ষণ পরে তিনি কোন পাতায় কত ভাগ সময় কাটান, সেটিই সেই পাতার PageRank. মানে এটি একটি random walk-এর stationary distribution.
একটি ছোট বানানো উদাহরণ: বাজারঘরের সাইটের চারটি পাতা। home থেকে offers আর blog-এ link; offers থেকে শুধু home-এ; blog থেকে home, offers আর help-এ; help থেকে offers-এ। link-গুলোর মধ্যে সম্ভাবনা সমান ভাগ হয়: home-এর দুটি link, তাই প্রতিটি ½; blog-এর তিনটি, প্রতিটি ⅓. এভাবে পাওয়া matrix M. তারপর damping factor d যোগ করা হয়:
G = d · M + (1 − d) / n
এখানে n পাতার সংখ্যা (৪), আর d সম্ভাবনায় surfer link ধরে চলেন, 1 − d সম্ভাবনায় যেকোনো পাতায় লাফ দেন। Brin আর Page লিখেছিলেন যে তাঁরা সাধারণত d = 0.85 নিতেন। প্রবন্ধে সূত্রটি একটু ভিন্নভাবে লেখা (n দিয়ে ভাগ ছাড়া), তবে সেখানেই বলা আছে যে PageRank-গুলো পাতার ওপর একটি সম্ভাবনার distribution হিসেবে দেখা যায়; এখানে সেই সম্ভাবনার রূপটি নেওয়া হয়েছে। লাফের অংশটি বদ্ধ পাতার দল আর চক্রের সমস্যা সারায়। G-এর সব ঘর ধনাত্মক, তাই অংশ ৪-এর ergodicity মেলে।
import numpy as np
np.set_printoptions(precision=4, suppress=True)
pages = ["home", "offers", "blog", "help"]
links = {0: [1, 2], 1: [0], 2: [0, 1, 3], 3: [1]} # কোন পাতা কোথায় link দেয়
n = len(pages)
M = np.zeros((n, n))
for i, outs in links.items():
M[i, outs] = 1 / len(outs) # link-গুলোর মধ্যে সমান ভাগ
def pagerank(d, tol=1e-10):
G = d * M + (1 - d) / n # damping: (1-d) সম্ভাবনায় লাফ
r = np.full(n, 1 / n)
for k in range(1, 500):
new = r @ G
if np.abs(new - r).sum() < tol:
return new, k, G
r = new
r, k, G = pagerank(0.85)
print("G (d = 0.85):\n", G)
print("সারির যোগ:", G.sum(axis=1))
print(f"power iteration, {k} ধাপে:", r)
w, V = np.linalg.eig(G.T)
v = V[:, np.argmin(np.abs(w - 1))].real
print("eigenvector দিয়ে: ", v / v.sum())
print("ক্রম:", [pages[i] for i in np.argsort(-r)])
for d in (0.5, 0.99):
r2, k2, _ = pagerank(d)
print(f"d = {d}: {r2} ({k2} ধাপ)")আসল আউটপুট:
G (d = 0.85):
[[0.0375 0.4625 0.4625 0.0375]
[0.8875 0.0375 0.0375 0.0375]
[0.3208 0.3208 0.0375 0.3208]
[0.0375 0.8875 0.0375 0.0375]]
সারির যোগ: [1. 1. 1. 1.]
power iteration, 66 ধাপে: [0.3763 0.3328 0.1974 0.0934]
eigenvector দিয়ে: [0.3763 0.3328 0.1974 0.0934]
ক্রম: ['home', 'offers', 'blog', 'help']
d = 0.5: [0.3182 0.3182 0.2045 0.1591] (26 ধাপ)
d = 0.99: [0.3985 0.3334 0.1997 0.0684] (120 ধাপ)
G-এর প্রতিটি ঘর হাতে মেলানো যায়: home-এর সারিতে offers আর blog-এ 0.85 × ½ + 0.15/4 = 0.425 + 0.0375 = 0.4625, আর link না থাকা ঘরে শুধু লাফের 0.0375. Power iteration ৬৬ ধাপে থেমেছে, আর eigenvector দিয়ে একই সংখ্যা এসেছে। home সবচেয়ে ওপরে, কারণ তিনটি পাতার দুটি (offers, blog) সেখানে link দেয়, আর offers-এর একমাত্র link home-এ, ফলে offers-এর পুরো “ভোট” home পায়। help-এ link দেয় শুধু blog, আর blog-এর ভোট তিন ভাগ হয়, তাই help সবচেয়ে নিচে। শুধু link-এর সংখ্যা নয়, কে link দিচ্ছে আর তার কতগুলো link, তা-ও গুরুত্বপূর্ণ।
d কমালে (0.5) লাফ বেশি, সব পাতা সমানের দিকে যায়, আর চলা দ্রুত থামে (২৬ ধাপ)। d প্রায় 1 হলে link-কাঠামো প্রাধান্য পায়, কিন্তু থামতে বেশি ধাপ লাগে (১২০), কারণ দ্বিতীয় বড় eigenvalue-এর মান d-এর বেশি হতে পারে না, আর d বড় হলে সেটি 1-এর কাছে যেতে পারে।
আজকের search engine সম্পর্কে দাবি নয়। এখানে শুধু ১৯৯৮ সালের প্রকাশিত ধারণাটি দেখানো হলো। কোনো search engine আজ পাতা কীভাবে সাজায়, তা প্রকাশ্য নয় আর বহু সংকেত মিলিয়ে করা হয় বলে ধরা হয়; “PageRank-ই আজকের র্যাঙ্কিং” বলা ঠিক নয়। যে পাতার কোনো link নেই (dangling page), তার সারি ফাঁকা থাকে; সাধারণ সমাধান হলো সেই সারিকে সমান 1/n দিয়ে ভরা। এখানকার ছোট উদাহরণে এমন পাতা রাখা হয়নি।
৬ · n-gram language model — বাংলা লেখার Markov chain
অবস্থা যদি শব্দ হয়? তাহলে Markov chain একটি language model: এখনকার শব্দ দেখে পরের শব্দের সম্ভাবনা দেয়। শুধু আগের একটি শব্দ দেখলে তাকে bigram model বলে; আগের দুটি দেখলে trigram; সাধারণভাবে আগের n − 1টি শব্দ দেখলে n-gram model. Bigram-এ সম্ভাবনা মাপা হয় সরাসরি গুনে:
P(b | a) = count(a, b) / count(a, যেকোনো পরের শব্দ)
এটি আসলে maximum likelihood-এর অনুমান। Transition matrix-এর প্রতিটি সারি একটি শব্দ, প্রতিটি কলাম তার পরের সম্ভাব্য শব্দ। নতুন লেখা তৈরি করতে একটি শুরুর শব্দ (seed word) দিয়ে, প্রতিটি ধাপে সারির distribution থেকে পরের শব্দ sample করতে হয়, ঠিক অংশ ৪-এর simulation-এর মতো।
Claude Shannon তাঁর ১৯৪৮ সালের প্রবন্ধে (পর্ব ৩৮-এ যার কথা এসেছে) ইংরেজির “approximation” তৈরি করে দেখান: প্রথমে অক্ষর ধরে, পরে শব্দ ধরে, যেখানে প্রতিটি শব্দ আগের শব্দের ওপর নির্ভর করে বাছা হয়। তাঁর তৈরি শব্দের ক্রম পাশাপাশি বেশ স্বাভাবিক শোনায়, কিন্তু পুরো বাক্যে অর্থ নেই।
নিচের অনুচ্ছেদটি এই পর্বের জন্য লেখা, কোথাও থেকে নেওয়া নয়। শব্দ ভাঙতে একটি সরল regex ব্যবহার হয়েছে: ফাঁকা জায়গা, দাঁড়ি (।) আর কমা দিয়ে ভাঙা, আর দাঁড়ি-কমাকেও আলাদা token রাখা, যাতে model বাক্যের শেষ “শেখে”। বাংলা লেখার tokenization-এর পুরো আলোচনা পর্ব ৬৭-এ। কোডটি গণনা দেখায়, কয়েকটি সম্ভাবনা হিসাব করে, তারপর একই seed word “সুমি” আর তিনটি ভিন্ন random seed দিয়ে নতুন লেখা বানায়।
import re
from collections import Counter, defaultdict
import numpy as np
text = ("সকালে বাজারঘরের গুদামে আলো জ্বলে। সুমি আপা চা নিয়ে অর্ডারের খাতা "
"খোলেন। খাতা দেখে তিনি প্যাকেট সাজান। প্যাকেট সাজানো হলে কুরিয়ার "
"ভাই আসেন। কুরিয়ার ভাই প্যাকেট নিয়ে শহরের পথে যান। পথে জ্যাম "
"থাকলে ডেলিভারি দেরি হয়। দেরি হলে ক্রেতা ফোন করেন। ফোন পেয়ে "
"সুমি আপা খাতা দেখেন আর হাসিমুখে বলেন, প্যাকেট পথে আছে। বিকেলে "
"আবার নতুন অর্ডার আসে, আর সুমি আপা আবার চা নিয়ে খাতা খোলেন।")
tokens = re.findall(r"[^।,\s]+|[।,]", text) # শব্দ আর দাঁড়ি-কমা আলাদা token
nxt = defaultdict(Counter)
for a, b in zip(tokens, tokens[1:]):
nxt[a][b] += 1 # bigram গণনা
print("token:", len(tokens), "| আলাদা:", len(set(tokens)))
print("'প্যাকেট'-এর পরে:", dict(nxt["প্যাকেট"]))
def prob(b, a): # P(b | a)
return nxt[a][b] / sum(nxt[a].values())
print("P(সাজান | প্যাকেট) =", round(prob("সাজান", "প্যাকেট"), 3))
print("P(খাতা দেখে তিনি) =", round(prob("দেখে", "খাতা") * prob("তিনি", "দেখে"), 3))
print("P(প্যাকেট দেরি) =", prob("দেরি", "প্যাকেট"))
def generate(word, n, seed):
rng = np.random.default_rng(seed)
out = [word]
for _ in range(n):
words = list(nxt[word])
counts = np.array(list(nxt[word].values()), dtype=float)
word = str(rng.choice(words, p=counts / counts.sum()))
out.append(word)
return " ".join(out).replace(" ।", "।").replace(" ,", ",")
for seed in (1, 2, 3):
print(f"seed {seed}:", generate("সুমি", 22, seed))আসল আউটপুট:
token: 77 | আলাদা: 45
'প্যাকেট'-এর পরে: {'সাজান': 1, 'সাজানো': 1, 'নিয়ে': 1, 'পথে': 1}
P(সাজান | প্যাকেট) = 0.25
P(খাতা দেখে তিনি) = 0.25
P(প্যাকেট দেরি) = 0.0
seed 1: সুমি আপা আবার নতুন অর্ডার আসে, আর হাসিমুখে বলেন, আর সুমি আপা আবার নতুন অর্ডার আসে, প্যাকেট সাজানো হলে কুরিয়ার
seed 2: সুমি আপা চা নিয়ে অর্ডারের খাতা দেখে তিনি প্যাকেট সাজানো হলে ক্রেতা ফোন করেন। কুরিয়ার ভাই প্যাকেট নিয়ে শহরের পথে জ্যাম থাকলে
seed 3: সুমি আপা চা নিয়ে শহরের পথে জ্যাম থাকলে ডেলিভারি দেরি হয়। পথে জ্যাম থাকলে ডেলিভারি দেরি হয়। দেরি হয়। বিকেলে
৭৭টি token, যার মধ্যে আলাদা ৪৫টি। “প্যাকেট”-এর পরে চারটি ভিন্ন শব্দ একবার করে এসেছে, তাই প্রতিটির সম্ভাবনা ¼. “খাতা”-র পরে তিনটি শব্দ (খোলেন দুবার, দেখে একবার, দেখেন একবার; মোট চারবার) আসায় P(দেখে | খাতা) = ¼, আর “দেখে”-র পরে সবসময় “তিনি”, তাই পুরো জোড়ার সম্ভাবনা ¼ × 1 = 0.25. কিন্তু “প্যাকেট দেরি” কখনো দেখা যায়নি, তাই সম্ভাবনা ঠিক শূন্য। এ কারণেই আসল n-gram model-এ smoothing লাগে, যেমন Laplace smoothing (পর্ব ৫১-এ Naive Bayes-এর জন্য)।
তৈরি লেখাগুলো দেখুন। seed 2-এর “অর্ডারের খাতা দেখে তিনি প্যাকেট সাজানো হলে ক্রেতা ফোন করেন” প্রতিটি পাশাপাশি জোড়ায় স্বাভাবিক, কারণ প্রতিটি জোড়া মূল লেখায় আছে। কিন্তু পুরো বাক্যটি অর্থহীন: প্যাকেট সাজানোর সঙ্গে ক্রেতার ফোনের সম্পর্ক নেই। seed 3 একই অংশ বারবার বলছে (“দেরি হয়। দেরি হয়।”), কারণ model-এর কোনো স্মৃতি নেই যে এটি একটু আগেই বলা হয়েছে। স্থানীয়ভাবে সাবলীল, সামগ্রিকভাবে অর্থহীন: Markov property-র সরাসরি ফল। একই seed আবার চালালে হুবহু একই লেখা আসবে।
আধুনিক language model-এর সঙ্গে সম্পর্ক। আজকের বড় language model-ও একভাবে “আগের token দেখে পরের token-এর সম্ভাবনা” দেয়, আর লেখা তৈরিতে একইভাবে sample করে। পার্থক্য হলো তারা শুধু আগের একটি শব্দ নয়, হাজারো token-এর প্রসঙ্গ একসঙ্গে দেখে, আর গোনা টেবিলের বদলে neural network দিয়ে সম্ভাবনা হিসাব করে, তাই না-দেখা জোড়াতেও যুক্তিসংগত সম্ভাবনা দিতে পারে। কীভাবে, আর কোথায় সীমা, তা পর্ব ৭০-এ (LLM কীভাবে কাজ করে — token, পরের শব্দের অনুমান আর সীমা)।
৭ · Hidden Markov model — লুকানো ট্রাফিক, দেখা যায় শুধু ডেলিভারির সময়
বাজারঘরের একজন রাইডার প্রতি ঘণ্টায় একটি করে ডেলিভারি দেন। ডেলিভারির সময় তিন ভাগে লেখা হয়: fast, normal, slow. সময়টি আসলে নির্ভর করে রাস্তার অবস্থার ওপর: Free (ফাঁকা), Heavy (ভারী), Jam (জ্যাম)। কিন্তু অফিসে বসে কেউ রাস্তা দেখতে পান না; দেখা যায় শুধু ডেলিভারির সময়। রাস্তার অবস্থা নিজে একটি Markov chain (জ্যাম থাকলে পরের ঘণ্টাতেও জ্যাম থাকার সম্ভাবনা বেশি), আর প্রতিটি অবস্থা একটি সম্ভাবনায় একটি ফল “নির্গত” করে। এই কাঠামোকে বলে hidden Markov model (HMM).
ডাক্তারের রোগনির্ণয়ের কথা ভাবুন। ভেতরের আসল অবস্থা ডাক্তার সরাসরি দেখেন না; দেখেন জ্বর, নাড়ি, পরীক্ষার ফল, আর তা থেকে অবস্থার ধারাবাহিকতা অনুমান করেন। HMM ঠিক এই যুক্তিকে সংখ্যায় লেখে: লুকানো কারণ একটি chain ধরে চলে, আর আমরা দেখি শুধু তার ছাপ।
একটি HMM-এ তিন সেট সম্ভাবনা লাগে (এখানে সব বানানো):
| সম্ভাবনা | Free | Heavy | Jam |
|---|---|---|---|
| শুরু π (প্রথম ঘণ্টার অবস্থা) | 0.5 | 0.3 | 0.2 |
| Transition A: Free থেকে → | 0.6 | 0.3 | 0.1 |
| Heavy থেকে → | 0.3 | 0.4 | 0.3 |
| Jam থেকে → | 0.1 | 0.4 | 0.5 |
| Emission B: অবস্থা ↓ / দেখা ফল → | fast | normal | slow |
|---|---|---|---|
| Free | 0.7 | 0.2 | 0.1 |
| Heavy | 0.2 | 0.5 | 0.3 |
| Jam | 0.1 | 0.3 | 0.6 |
B-এর ঘর হলো emission probability, P(দেখা ফল | লুকানো অবস্থা)। A আর B দুটিরই প্রতিটি সারির যোগ 1. HMM-এর দুটি অনুমান: (১) লুকানো অবস্থা Markov property মানে; (২) এখনকার দেখা ফল শুধু এখনকার লুকানো অবস্থার ওপর নির্ভর করে, আগের ফল বা অবস্থার ওপর নয়। তাহলে একটি নির্দিষ্ট লুকানো পথ Z আর দেখা ক্রম O-এর যৌথ সম্ভাবনা সরল গুণফল:
P(Z, O) = πz1 Bz1(o1) · Az1 z2 Bz2(o2) ⋯ Az(T−1) zT BzT(oT)
উদাহরণ: তিন ঘণ্টায় দেখা গেল fast, slow, slow. পথ Free, Jam, Jam ধরলে: 0.5 × 0.7 × 0.1 × 0.6 × 0.5 × 0.6 = 0.0063. এটি পরে Viterbi-র উত্তর হিসেবে ফিরে আসবে।
HMM-এর তিনটি মূল প্রশ্ন
Lawrence Rabiner-এর ১৯৮৯ সালের বহুল পঠিত tutorial (“A Tutorial on Hidden Markov Models and Selected Applications in Speech Recognition”, Proceedings of the IEEE) HMM-এর কাজকে তিনটি প্রশ্নে সাজায়, আর এই ভাগটি আজও পাঠ্যবইয়ে প্রচলিত:
১ · Evaluation
model দেওয়া আছে; দেখা ক্রমটি আসার সম্ভাবনা P(O) কত? উত্তর: forward algorithm. কোন model ডেটার সঙ্গে বেশি মেলে, তুলনায় কাজে লাগে।
২ · Decoding
দেখা ক্রমের পেছনে সবচেয়ে সম্ভাব্য লুকানো পথ কোনটি? উত্তর: Viterbi algorithm.
৩ · Learning
শুধু দেখা ক্রম থেকে π, A, B কীভাবে শেখা যায়? উত্তর: Baum–Welch (EM).
৮ · Forward algorithm আর Viterbi — trellis ভরে ভরে
P(O) বের করার সরাসরি পথ হলো সব লুকানো পথের P(Z, O) যোগ করা। তিন ঘণ্টায় 33 = 27টি পথ, কিন্তু ১০০ ঘণ্টায় 3100, যা কোনো কম্পিউটারে গোনা সম্ভব নয়। Forward algorithm মূল কৌশলটি নেয় dynamic programming থেকে: প্রতিটি সময় আর অবস্থার জন্য একটি মাত্র সংখ্যা রাখা,
αt(j) = P(o1, …, ot, Zt = j)
মানে “প্রথম t-টি ফল দেখা গেছে আর সময় t-তে অবস্থা j”. আগের কলাম থেকে পরেরটি:
α1(j) = πj Bj(o1), αt+1(j) = [∑i αt(i) Aij] · Bj(ot+1), P(O) = ∑j αT(j)
বন্ধনীর ভেতরটি অংশ ২-এর vector × matrix, শুধু তারপর দেখা ফলের সম্ভাবনা দিয়ে গুণ। সময় × অবস্থার এই ছকটিকে বলে trellis (জালির মতো দেখতে বলে)। খরচ N অবস্থা আর T সময়ে মোটামুটি N2T, সূচকীয় NT নয়। দেখা ক্রম fast, slow, slow-এর জন্য trellis ভরুন:
1alpha = pi * B[:, fast]2for o in [slow, slow]:3 alpha = (alpha @ A) * B[:, o]4print(alpha.sum())
| ধাপ | |
| হিসাব | |
| α Free | |
| α Heavy | |
| α Jam |
- প্রথম কলাম. শুরুর সম্ভাবনা × fast-এর emission: Free 0.5 × 0.7 = 0.35, Heavy 0.3 × 0.2 = 0.06, Jam 0.2 × 0.1 = 0.02.
- t = 2, Free. তিনটি আগের অবস্থা থেকে Free-তে আসার সম্ভাবনা যোগ: 0.21 + 0.018 + 0.002 = 0.23. তারপর slow দেখার সম্ভাবনা, Free-তে মাত্র 0.1: α = 0.023.
- t = 2, Heavy. 0.105 + 0.024 + 0.008 = 0.137, × 0.3 = 0.0411.
- t = 2, Jam. 0.035 + 0.018 + 0.010 = 0.063, × 0.6 = 0.0378. slow দেখার পর Free-র সংখ্যা সবচেয়ে ছোট হয়ে গেল।
- t = 3, তিনটি একসঙ্গে. α2 @ A = [0.02991, 0.03846, 0.03353], তারপর slow-এর emission [0.1, 0.3, 0.6] দিয়ে ঘরে ঘরে গুণ: [0.002991, 0.011538, 0.020118].
- P(O). শেষ কলামের যোগ 0.034647. সব ২৭টি পথের P(Z, O) যোগ করলেও ঠিক এটিই আসে, কিন্তু এখানে লেগেছে মাত্র কয়েকটি গুণ আর যোগ।
Viterbi — যোগের বদলে সর্বোচ্চ, সঙ্গে পেছনের তীর
Decoding প্রশ্নে চাই একটি পথ, যোগফল নয়। Viterbi algorithm forward-এর হুবহু কাঠামো নেয়, শুধু ∑-এর জায়গায় max বসায়:
δt+1(j) = maxi [δt(i) Aij] · Bj(ot+1), ψt+1(j) = argmaxi [δt(i) Aij]
δt(j) হলো “সময় t-তে j-তে শেষ হওয়া সেরা একক পথের সম্ভাবনা”, আর ψ হলো backpointer: সেই সেরা পথ আগের ধাপে কোন অবস্থা থেকে এসেছিল। শেষে সবচেয়ে বড় δ থেকে শুরু করে তীর ধরে পেছনে হাঁটলে পুরো পথ পাওয়া যায়। Andrew Viterbi ১৯৬৭ সালে এটি প্রকাশ করেন error-correcting code-এর decoding-এর জন্য; পরে এটি HMM-এর প্রধান হাতিয়ার হয়ে ওঠে।
- প্রথম কলাম. δ1 = α1 = [0.35, 0.06, 0.02]; প্রথম ধাপে যোগ বা max-এর পার্থক্য নেই।
- t = 2. Free-তে আসার তিন প্রার্থী 0.35 × 0.6 = 0.21, 0.06 × 0.3 = 0.018, 0.02 × 0.1 = 0.002; সেরা Free থেকে, × 0.1 = 0.021. Heavy-র সেরা 0.35 × 0.3 = 0.105 (Free থেকে), × 0.3 = 0.0315. Jam-এর সেরা 0.35 × 0.1 = 0.035 (Free থেকে), × 0.6 = 0.021. তিনটি তীরই Free-তে ফেরে।
- t = 3, Free. 0.021 × 0.6 = 0.0126 (Free থেকে), 0.0315 × 0.3 = 0.00945 (Heavy), 0.021 × 0.1 = 0.0021 (Jam). সেরা Free; × 0.1 = 0.00126.
- t = 3, Heavy. 0.0063 (F), 0.0126 (H), 0.0084 (J). সেরা Heavy; × 0.3 = 0.00378.
- t = 3, Jam. 0.0021 (F), 0.00945 (H), 0.0105 (J). সেরা Jam, যদিও Heavy খুব কাছে; × 0.6 = 0.0063.
- পেছনে হাঁটা. শেষ কলামের সর্বোচ্চ 0.0063, Jam-এ। তার তীর Jam-এ (t = 2), আর সেখান থেকে তীর Free-তে (t = 1). পথ Free → Jam → Jam, সম্ভাবনা 0.0063, অংশ ৭-এর হাতের গুণফল।
এবার দুটি algorithm কোডে, প্রতিটি কলামের মান ছাপিয়ে। শেষ অংশে ১০০০ ঘণ্টার একটি random ক্রমে সরাসরি গুণ বনাম log-space: সরাসরি গুণে সংখ্যাগুলো এত ছোট হয় যে float64-এ underflow হয়ে 0.0 হয়ে যায় (পর্ব ৩৫ আর পর্ব ৩৮-এর সমস্যা)। সমাধান হলো log α রাখা; কিন্তু forward-এ যোগ আছে, আর log-এর যোগ সরাসরি করা যায় না। তাই log-sum-exp কৌশল লাগে: log(ea + eb) = m + log(ea−m + eb−m), যেখানে m = max(a, b); SciPy-তে scipy.special.logsumexp. Viterbi-তে কাজটি আরও সহজ: max আর গুণ log-এ হয়ে যায় max আর যোগ।
import numpy as np
from scipy.special import logsumexp
np.set_printoptions(precision=6, suppress=True)
S = ["Free", "Heavy", "Jam"] # লুকানো অবস্থা
O = {"fast": 0, "normal": 1, "slow": 2} # দেখা যায় এমন ফল
pi = np.array([0.5, 0.3, 0.2])
A = np.array([[0.6, 0.3, 0.1], [0.3, 0.4, 0.3], [0.1, 0.4, 0.5]])
B = np.array([[0.7, 0.2, 0.1], [0.2, 0.5, 0.3], [0.1, 0.3, 0.6]])
obs = [O[o] for o in ["fast", "slow", "slow"]]
alpha = pi * B[:, obs[0]] # forward
print("alpha1:", alpha)
for t, o in enumerate(obs[1:], start=2):
alpha = (alpha @ A) * B[:, o]
print(f"alpha{t}:", alpha)
print("P(O) =", round(alpha.sum(), 6))
delta, back = pi * B[:, obs[0]], [] # Viterbi
for o in obs[1:]:
scores = delta[:, None] * A # [i, j]: i থেকে j-তে
back.append(scores.argmax(axis=0))
delta = scores.max(axis=0) * B[:, o]
print("delta:", delta, "| পেছনের তীর:", [S[i] for i in back[-1]])
path = [int(delta.argmax())]
for bp in reversed(back):
path.insert(0, int(bp[path[0]]))
print("Viterbi path:", [S[i] for i in path], "| P =", round(delta.max(), 6))
rng = np.random.default_rng(0) # লম্বা ক্রম: underflow
seq = rng.integers(0, 3, size=1000)
a, la = pi * B[:, seq[0]], np.log(pi * B[:, seq[0]])
for o in seq[1:]:
a = (a @ A) * B[:, o]
la = logsumexp(la[:, None] + np.log(A), axis=0) + np.log(B[:, o])
print("১০০০ ধাপ, সরাসরি:", a.sum(), "| log-space:", round(logsumexp(la), 2))আসল আউটপুট:
alpha1: [0.35 0.06 0.02]
alpha2: [0.023 0.0411 0.0378]
alpha3: [0.002991 0.011538 0.020118]
P(O) = 0.034647
delta: [0.021 0.0315 0.021 ] | পেছনের তীর: ['Free', 'Free', 'Free']
delta: [0.00126 0.00378 0.0063 ] | পেছনের তীর: ['Free', 'Heavy', 'Jam']
Viterbi path: ['Free', 'Jam', 'Jam'] | P = 0.0063
১০০০ ধাপ, সরাসরি: 0.0 | log-space: -1114.76
forward-এর তিনটি কলাম আর P(O) = 0.034647 প্রথম stepper-এর সঙ্গে হুবহু এক। Viterbi-তে scores[i, j] = delta[i] * A[i, j] একটি ৩ × ৩ ছক (পর্ব ১৫-এর broadcasting), যার প্রতিটি কলামের max নতুন δ আর argmax পেছনের তীর। তীরগুলো দ্বিতীয় stepper-এর মতোই: প্রথমবার সব Free, দ্বিতীয়বার Free, Heavy, Jam. পথ Free → Jam → Jam, P = 0.0063.
শেষ লাইনটি সংখ্যার সীমা দেখায়। ১০০০ ধাপে আসল log P(O) প্রায় −1114.76, মানে P(O) ≈ e−1115, যা float64-এর সবচেয়ে ছোট ধনাত্মক মানের (প্রায় e−745) চেয়েও অনেক ছোট, তাই সরাসরি গুণে চুপচাপ 0.0. কোনো error আসেনি, যা আরও বিপজ্জনক। log-space একই হিসাব নিরাপদে করে। আরেকটি পথ scaling: প্রতিটি ধাপে α-কে তার যোগ দিয়ে ভাগ করে ভাগফলগুলোর log যোগ রাখা।
Viterbi-র পথ আর “প্রতি ঘণ্টায় সবচেয়ে সম্ভাব্য অবস্থা” এক জিনিস নয়। Viterbi পুরো পথটিকে একসঙ্গে সর্বোচ্চ করে। প্রতিটি ঘণ্টা আলাদাভাবে দেখলে (forward-backward দিয়ে posterior, নিচের hmmlearn run-এ predict_proba) দ্বিতীয় ঘণ্টায় Jam 0.469, Heavy 0.392: এখানে দুটি উত্তর মিলেছে, কিন্তু সবসময় মেলে না; প্রতি ঘণ্টার সেরা অবস্থা জোড়া দিলে এমন পথও আসতে পারে, যার সম্ভাবনা শূন্য (যেমন A-তে নিষিদ্ধ একটি স্থানান্তর)। আর সেরা পথটি P(O)-এর মাত্র প্রায় ১৮%; দ্বিতীয় সেরা Free → Heavy → Jam (0.00567)-ও প্রায় সমান সম্ভাব্য। “সবচেয়ে সম্ভাব্য” মানে “নিশ্চিত” নয়।
৯ · শেখা, hmmlearn-এর সত্যিকারের run আর HMM-এর জায়গা
Baum–Welch — শুধু দেখা ক্রম থেকে শেখা (ধারণা)
এতক্ষণ π, A, B জানা ধরে নিয়েছি। বাস্তবে বাজারঘরের কাছে থাকে শুধু হাজারো ডেলিভারির সময়, রাস্তার আসল অবস্থা কোথাও লেখা নেই। লুকানো অবস্থা জানা থাকলে কাজটি সহজ হতো: শুধু গুনে ভাগ করা, অংশ ৬-এর bigram-এর মতো। জানা নেই বলে Baum–Welch algorithm একটি চক্র চালায়, যা expectation–maximization (EM)-এর একটি রূপ:
- শুরু: π, A, B-এর একটি অনুমান (প্রায়ই random).
- E-ধাপ: এখনকার model দিয়ে forward আর backward (পেছন থেকে একই ধরনের হিসাব) চালিয়ে প্রতিটি ঘণ্টায় প্রতিটি অবস্থায় থাকার, আর প্রতিটি স্থানান্তর ঘটার প্রত্যাশিত সংখ্যা বের করা। পূর্ণ গণনার বদলে “আংশিক গণনা”।
- M-ধাপ: সেই প্রত্যাশিত গণনা দিয়ে নতুন π, A, B, ঠিক যেন গুনে ভাগ করা হচ্ছে।
- আবার: likelihood আর উল্লেখযোগ্যভাবে না বাড়া পর্যন্ত।
প্রতিটি চক্রে likelihood কমে না, এটি প্রমাণিত; কিন্তু চক্রটি একটি local maximum-এ আটকে যেতে পারে (পর্ব ৪৪-এর hill climbing-এর মতো সমস্যা)। পদ্ধতিটি Leonard Baum আর সহকর্মীদের ১৯৬০-এর দশকের শেষ আর ১৯৭০ সালের প্রবন্ধগুলো থেকে এসেছে; EM-এর সাধারণ কাঠামো ১৯৭৭ সালে Dempster, Laird আর Rubin প্রকাশ করেন। নিচের run-এ এই সমস্যাগুলো চোখে দেখা যাবে।
hmmlearn — সত্যিকারের run
HMM-এর একটি পরিচিত Python library হলো hmmlearn, যা scikit-learn-এর মতো fit/predict ধরনের API দেয়। এটি ব্রাউজারের Python-এ নেই, তাই লেখকের যন্ত্রে একটি আলাদা venv-এ চালানো হয়েছে (সত্যিকারের run, Python 3.14.4, uv 0.11.28; লেখার সময়ে PyPI-র সর্বশেষ hmmlearn 0.3.3). দুটি বিষয় লক্ষ করুন। প্রথমত, এর README-তে প্রকল্পটিকে “limited-maintenance mode”-এ বলা আছে। দ্বিতীয়ত, ক্রমিক চিহ্নের (fast, normal, slow-এর মতো) HMM-এর class এখন CategoricalHMM. পুরোনো টিউটোরিয়ালে এই কাজে MultinomialHMM লেখা থাকে, কিন্তু সেই নাম এখন একটি ভিন্ন model-এর (আসল multinomial distribution); বানালেই এই সতর্কবার্তা আসে:
MultinomialHMM has undergone major changes. The previous version was implementing a CategoricalHMM (a special case of MultinomialHMM). This new implementation follows the standard definition for a Multinomial distribution …
প্রথমে প্রকল্প তৈরি:
$ cd ~/bazarghar-ai
$ uv venv --python 3.14
$ uv pip install hmmlearn
…
+ hmmlearn==0.3.3
+ numpy==2.5.4
+ scikit-learn==1.9.1
+ scipy==1.18.1
…
import numpy as np
from hmmlearn import hmm
import hmmlearn
np.set_printoptions(precision=4, suppress=True)
print("hmmlearn", hmmlearn.__version__, "| numpy", np.__version__)
model = hmm.CategoricalHMM(n_components=3) # Free, Heavy, Jam
model.startprob_ = np.array([0.5, 0.3, 0.2])
model.transmat_ = np.array([[0.6, 0.3, 0.1],
[0.3, 0.4, 0.3],
[0.1, 0.4, 0.5]])
model.emissionprob_ = np.array([[0.7, 0.2, 0.1], # fast, normal, slow
[0.2, 0.5, 0.3],
[0.1, 0.3, 0.6]])
X = np.array([[0], [2], [2]]) # fast, slow, slow
logp = model.score(X)
print("log P(O) =", round(logp, 4), "| P(O) =", round(np.exp(logp), 6))
vlogp, states = model.decode(X, algorithm="viterbi")
names = np.array(["Free", "Heavy", "Jam"])
print("Viterbi:", names[states], "| P(path, O) =", round(np.exp(vlogp), 6))
print("posterior:\n", model.predict_proba(X))
# Baum-Welch: শুধু observation থেকে parameter শেখা
Xs, Zs = model.sample(3000, random_state=7)
best = None
for seed in range(6):
m = hmm.CategoricalHMM(n_components=3, n_iter=500, tol=1e-4,
random_state=seed).fit(Xs)
ll = m.score(Xs)
print(f"seed {seed}: log-lik {ll:.1f}, iter {m.monitor_.iter}")
if best is None or ll > best.score(Xs):
best = m
print("true model log-lik:", round(model.score(Xs), 1))
order = np.argsort(-best.emissionprob_[:, 0]) # fast-বেশি অবস্থা আগে
print("learned transmat (reordered):\n", best.transmat_[order][:, order])
print("learned emissionprob (reordered):\n", best.emissionprob_[order])$ python traffic_hmm.py
hmmlearn 0.3.3 | numpy 2.5.4
log P(O) = -3.3625 | P(O) = 0.034647
Viterbi: ['Free' 'Jam' 'Jam'] | P(path, O) = 0.0063
posterior:
[[0.6879 0.2135 0.0985]
[0.1394 0.3915 0.4691]
[0.0863 0.333 0.5807]]
seed 0: log-lik -3289.7, iter 500
seed 1: log-lik -3289.5, iter 500
seed 2: log-lik -3259.1, iter 500
seed 3: log-lik -3258.3, iter 500
seed 4: log-lik -3256.9, iter 500
seed 5: log-lik -3259.3, iter 500
true model log-lik: -3263.0
learned transmat (reordered):
[[0.5497 0.2916 0.1587]
[0.0007 0.8612 0.1382]
[0.9997 0.0002 0.0001]]
learned emissionprob (reordered):
[[0.8382 0.1024 0.0594]
[0.1682 0.4178 0.414 ]
[0.0407 0.5917 0.3676]]
প্রথম অংশ মিলেছে। score log-likelihood দেয়, log-space-এ হিসাব করা: −3.3625, যার exp 0.034647, আমাদের forward-এর সংখ্যা। decode একই Viterbi পথ Free → Jam → Jam আর 0.0063 দিয়েছে। predict_proba প্রতিটি ঘণ্টার posterior: ইনপুট একটি কলামের 2D array, প্রতিটি সারি একটি সময়।
শেখার অংশে তিনটি শিক্ষা। (১) Local maximum: একই ৩০০০ ঘণ্টার ডেটায় ছয়টি random শুরু দুই দলে ভাগ হয়েছে: দুটি প্রায় −3289.6-এ আটকে গেছে, চারটি −3259-এর কাছে। তাই কয়েকটি শুরু চালিয়ে সেরাটি নেওয়াই রীতি। (২) ধীর convergence: সব run ৫০০ চক্রের সীমায় পৌঁছেছে, tol-এ নয়; এটি লেখকের যন্ত্রের একটি run-এর ফল, সাধারণ নিয়ম নয়। (৩) ভালো likelihood, ভিন্ন parameter: সেরা model-এর log-likelihood (−3256.9) সত্যিকারের model-এর (−3263.0) চেয়েও বেশি, কারণ শেখা model এই নির্দিষ্ট নমুনার সঙ্গে খাপ খাইয়ে নেয়। অথচ তার matrix আসলটির মতো নয়: তৃতীয় অবস্থা প্রায় সবসময় (0.9997) প্রথম অবস্থায় ফেরে। অনেক ভিন্ন parameter প্রায় একই likelihood দিতে পারে। আরও আছে label switching: শেখা অবস্থা ০, ১, ২-এর নাম “Free, Heavy, Jam” কেউ দেয় না, তাই এখানে fast-এর emission ধরে সাজাতে হয়েছে।
HMM কোথায় ব্যবহার হয়েছে, আর আজ কোথায় দাঁড়িয়ে
- Speech recognition: ১৯৭০-এর দশক থেকে কয়েক দশক ধরে অনেক বড় speech system-এর কেন্দ্রে ছিল HMM: লুকানো অবস্থা শব্দের ধ্বনি-অংশ, দেখা ফল শব্দতরঙ্গের feature.
- Part-of-speech tagging: লুকানো অবস্থা ব্যাকরণের পদ (বিশেষ্য, ক্রিয়া…), দেখা ফল শব্দ।
- Gene finding আর জীববিজ্ঞান: DNA-র ক্রমে লুকানো অবস্থা “gene-এর অংশ কি না”, আর প্রোটিন পরিবার খোঁজায় profile HMM; ১৯৯০-এর দশকের কয়েকটি পরিচিত gene-খোঁজার টুল HMM-ধরনের model ব্যবহার করত বলে উল্লেখ আছে।
২০১০-এর দশক থেকে অনেক কাজে, বিশেষ করে speech আর ভাষায়, neural sequence model (recurrent network, পরে Transformer) বড় অংশে HMM-কে সরিয়ে দিয়েছে, কারণ তারা দীর্ঘ প্রসঙ্গ আর জটিল feature সরাসরি শিখতে পারে। তবু HMM পুরো হারিয়ে যায়নি: অল্প ডেটায়, যেখানে ব্যাখ্যাযোগ্যতা আর সম্ভাবনার পরিষ্কার মানে দরকার, বা জীববিজ্ঞানের কিছু কাজে এটি এখনো ব্যবহৃত হয় বলে দেখা যায়। কোনটি কোথায় “সেরা”, তা কাজ আর ডেটার ওপর নির্ভর করে।
আরও গভীরে: Forward আর Viterbi একই কাঠামোর দুটি রূপ: শুধু (যোগ, গুণ) জোড়াকে (max, গুণ) দিয়ে বদলানো হয়েছে; গণিতে এগুলোকে ভিন্ন semiring বলে, আর এই দৃষ্টিভঙ্গি থেকে parsing আর graph-এর অনেক algorithm একসঙ্গে বোঝা যায়। HMM হলো পর্ব ৫২-এর Bayesian network-এর একটি বিশেষ রূপ (প্রতিটি সময়ে একটি লুকানো আর একটি দেখা node); সাধারণ রূপটির নাম dynamic Bayesian network. লুকানো অবস্থা ধারাবাহিক সংখ্যা আর সব normal হলে একই ধারণা থেকে Kalman filter আসে।
পরীক্ষা ও ইন্টারভিউতে (বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এ ধরনের প্রশ্ন সাধারণ):
- P = [[0.9, 0.1], [0.5, 0.5]], অবস্থা ১ থেকে ২ ধাপে আবার ১-এ ফেরার সম্ভাবনা? 0.9 × 0.9 + 0.1 × 0.5 = 0.86.
- দুই অবস্থার chain-এ P(1→2) = a, P(2→1) = b হলে stationary distribution? π = [b/(a + b), a/(a + b)]; π P = π থেকে এক লাইনে।
- Stationary distribution কোন eigenvector? PT-এর eigenvalue 1-এর eigenvector, যোগ 1 করতে normalize করে।
- Forward আর Viterbi-র পার্থক্য? একই trellis; forward পূর্বসূরিদের যোগ নেয় (P(O)), Viterbi সর্বোচ্চ নেয় আর backpointer রাখে (সেরা পথ)। দুটিরই খরচ O(N2T).
- লম্বা ক্রমে forward-এ সমস্যা কী, আর সমাধান? underflow; log-space-এ log-sum-exp বা প্রতি ধাপে scaling.
১০ · AI-এর যুগে — AI সহকারীর লেখা Markov আর HMM কোড যাচাই
“একটি HMM বানিয়ে দাও” বা “stationary distribution বের করো” লিখলে AI সহকারী দ্রুত কোড দেয়, কিন্তু ভুলগুলো প্রায়ই নিঃশব্দ। AI সহকারীরা প্রায়ই যা করে:
- সারি আর কলামের রীতি গুলিয়ে ফেলা:
P @ xলিখে ধরে নেওয়া যে এটি পরের distribution, অথচ সারি-stochastic P-তে লাগেx @ P; eigenvector-এP.Tভুলে যাওয়া, বা eigenvector normalize না করা (ঋণাত্মক “সম্ভাবনা”)। - পুরোনো বা বানানো API: discrete HMM-এ পুরোনো অর্থে
MultinomialHMM;np.matrix_power-এর মতো অস্তিত্বহীন নাম (আসলটিnp.linalg.matrix_power);P ** nদিয়ে matrix power. - লম্বা ক্রমে সরাসরি গুণ: log-space বা scaling ছাড়া forward, ফলে 0.0 বা
-inf, তারপর warning চাপা দেওয়া। - শেখা model নিয়ে অতিরিক্ত দাবি: একটি random শুরুর Baum–Welch-কে “আসল” parameter বলা, শেখা অবস্থাকে নিজে থেকে “Jam” নাম দেওয়া, train ডেটার likelihood দিয়েই model-এর মান বিচার।
প্রম্পটে রীতি, version আর যাচাইয়ের উপায় নিজেই বলে দিন। নিচের প্রম্পটে আসল ক্রেতার ডেটা নেই, শুধু বানানো ছোট উদাহরণ।
AI-এর কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১Markov কেন একটি কবিতার বইয়ের অক্ষর গুনতে বসেছিলেন, আর তাতে সম্ভাবনা তত্ত্বের কী লাভ হলো?
বিশ শতকের শুরুতে সম্ভাবনা তত্ত্বের সবচেয়ে বড় ফলগুলো, যেমন বড় সংখ্যার নিয়ম (অনেকবার পরীক্ষা করলে গড় একটি নির্দিষ্ট মানের দিকে যায়) আর central limit theorem, প্রমাণ করা হতো স্বাধীন ঘটনার জন্য: মুদ্রা ছোড়া, ছক্কা। প্রচলিত ইতিহাস অনুযায়ী মস্কোর গণিতবিদ Pavel Nekrasov যুক্তি দেন যে বড় সংখ্যার নিয়ম খাটতে হলে স্বাধীনতা লাগবে, আর Andrey Markov এই দাবির কঠোর সমালোচক ছিলেন।
১৯০৬ সালের প্রবন্ধে Markov দেখান যে এমন ক্রমেও বড় সংখ্যার নিয়ম খাটতে পারে, যেখানে প্রতিটি ঘটনা আগেরটির ওপর নির্ভর করে, যদি নির্ভরতাটি একটি নির্দিষ্ট সরল ধরনের হয়। এটিই আজকের Markov chain. তত্ত্বটি যে শুধু খাতা-কলমের নয়, তা দেখাতে তিনি ১৯১৩ সালে Pushkin-এর “Eugene Onegin”-এর প্রথম প্রায় ২০,০০০ অক্ষর হাতে গুনে স্বরবর্ণ আর ব্যঞ্জনবর্ণের ক্রম বিশ্লেষণ করেন। তিনি পান যে স্বরবর্ণের পরে স্বরবর্ণ আসার হার আর ব্যঞ্জনবর্ণের পরে স্বরবর্ণ আসার হার স্পষ্টভাবে আলাদা, মানে অক্ষরগুলো স্বাধীন নয়; আবার দুই অবস্থার chain দিয়ে সামগ্রিক হার ভালোভাবে ব্যাখ্যা করা যায়।
এই পরীক্ষাটি নির্ভরশীল ঘটনার সম্ভাবনা তত্ত্বের দরজা খোলে, যা পরে Kolmogorov-সহ অনেকে সাধারণ রূপ দেন; আর লেখাকে সম্ভাবনার ক্রম হিসেবে দেখার ধারণা Shannon-এর ১৯৪৮ সালের n-gram পরীক্ষা হয়ে আজকের language model পর্যন্ত গেছে। তবে এই গল্পের কিছু দিক, বিশেষ করে Nekrasov-এর সঙ্গে বিতর্কের দার্শনিক দিক, ইতিহাসবিদরা বিভিন্নভাবে বর্ণনা করেন; তাই বিস্তারিত উদ্ধৃতি দেওয়ার আগে মূল সূত্র দেখা ভালো।
মূল উপলব্ধি: স্বাধীনতা ছাড়াও নিয়মিত আচরণ সম্ভব, এটি প্রমাণ করতে গিয়েই Markov chain জন্মায়, আর একটি কবিতার অক্ষর গোনা দেখায় যে তত্ত্বটি আসল ক্রমে খাটে।
প্র ০২PageRank-এর random surfer কেন একটি শক্তিশালী ধারণা ছিল, আর এর দুর্বলতা কোথায়?
১৯৯০-এর দশকের মাঝামাঝি web দ্রুত বড় হচ্ছিল, আর অনেক search পদ্ধতি মূলত পাতার লেখার সঙ্গে প্রশ্নের শব্দ মেলাত। এতে ঠকানো সহজ ছিল। Brin আর Page-এর প্রবন্ধ আরেকটি উৎস ব্যবহার করে: web-এর link-কাঠামো। এই বৃত্তাকার সংজ্ঞাটি (গুরুত্বপূর্ণ পাতা মানে যাদের দিকে গুরুত্বপূর্ণ পাতা link দেয়) গণিতে একটি eigenvector সমস্যা, আর random surfer তার সুন্দর সম্ভাবনামূলক ব্যাখ্যা।
শক্তির দিকগুলো: প্রথমত, একটি পাতা নিজের লেখা বদলে নিজের PageRank বাড়াতে পারে না; অন্যদের link লাগে। দ্বিতীয়ত, হিসাবটি প্রশ্নের ওপর নির্ভর করে না, তাই আগে থেকে একবার করে রাখা যায়। তৃতীয়ত, power iteration বিশাল কিন্তু sparse matrix-এও চলে, কারণ প্রতিটি ধাপ শুধু link ধরে সংখ্যা সরানো।
দুর্বলতাও আছে। link কেনা-বেচা বা “link farm” (অনেকগুলো নকল পাতা, যারা একে অপরকে link দেয়) দিয়ে PageRank কৃত্রিমভাবে বাড়ানোর চেষ্টা হয়েছে, আর পরবর্তী গবেষণায় এমন কারসাজি ধরার পদ্ধতি (যেমন বিশ্বস্ত পাতা থেকে শুরু করা রূপ) প্রস্তাব করা হয়েছে। random surfer সব link-কে সমান ধরে, অথচ আসল মানুষ পাতার ওপরের বা প্রাসঙ্গিক link বেশি চাপেন। আর একটি পাতা “জনপ্রিয়” মানেই তা সঠিক বা প্রশ্নের সঙ্গে প্রাসঙ্গিক নয়। এ কারণে বাস্তব search system বহু সংকেত মেলায় বলে ধরা হয়; কোনটি কতটা ওজন পায়, তা প্রকাশ্য নয়, তাই এ নিয়ে কোনো নির্দিষ্ট দাবি এখানে করা হচ্ছে না।
মূল উপলব্ধি: PageRank গুরুত্বের একটি বৃত্তাকার সংজ্ঞাকে একটি Markov chain-এর stationary distribution বানিয়ে সমাধানযোগ্য করে; কিন্তু link-জনপ্রিয়তা সত্যতা বা প্রাসঙ্গিকতার সমান নয়।
প্র ০৩n বাড়ালে n-gram model কি একসময় নিখুঁত লেখা দেবে? আধুনিক language model এখানে কী বদলেছে?
Bigram-এ লেখা স্থানীয়ভাবে স্বাভাবিক কিন্তু সামগ্রিকভাবে অর্থহীন, তাই স্বাভাবিক প্রশ্ন: আগের দুই, তিন বা দশটি শব্দ দেখলে কী হয়? Trigram বা 4-gram-এ লেখা অবশ্যই বেশি সাবলীল হয়। কিন্তু একটি বড় সমস্যা দ্রুত বাড়ে: sparsity. শব্দভান্ডারে V-টি শব্দ থাকলে সম্ভাব্য n-gram-এর সংখ্যা Vn. কোনো লেখার সংগ্রহে তার অতি ক্ষুদ্র অংশই দেখা যায়। অংশ ৬-এর “প্যাকেট দেরি”-র মতো না-দেখা জোড়ার সম্ভাবনা শূন্য হয়ে যায়।
n-gram যুগের গবেষণার বড় অংশ তাই ছিল smoothing আর backoff: না-দেখা ঘটনায় কিছু সম্ভাবনা সরানো, আর লম্বা প্রসঙ্গ না পেলে ছোট প্রসঙ্গে ফিরে যাওয়া। Kneser–Ney smoothing (১৯৯৫ সালের প্রবন্ধ) এর মধ্যে বিশেষ সফল বলে পরিচিত।
Neural language model দুটি জায়গায় মৌলিক পরিবর্তন আনে। প্রথমত, প্রতিটি শব্দকে একটি vector (embedding, পর্ব ৩৩-এ পরিচয়) দিয়ে প্রকাশ করা, ফলে “ঢাকা” আর “চট্টগ্রাম”-এর মতো কাছাকাছি শব্দের অভিজ্ঞতা ভাগ হয়, আর না-দেখা জোড়াতেও যুক্তিসংগত সম্ভাবনা আসে। দ্বিতীয়ত, প্রসঙ্গ আর নির্দিষ্ট n-এ বাঁধা নয়: Transformer-ধরনের model হাজারো token পেছনে দেখতে পারে। তবু লেখা তৈরির মূল চক্রটি একই: পরের token-এর distribution থেকে sample, তারপর আবার। তাই এমন model-কেও একটি অনেক বড় অবস্থার ওপর Markov-ধরনের প্রক্রিয়া হিসেবে দেখা যায়, যেখানে “অবস্থা” হলো পুরো প্রসঙ্গ; এই দৃষ্টিভঙ্গি কতটা কাজের, তা নিয়ে গবেষকদের মধ্যে ভিন্ন মত আছে।
যা বদলায়নি: সাবলীলতা সত্যের নিশ্চয়তা নয়। n-gram যেমন “দেরি হয়। দেরি হয়।” বলে কোনো দ্বিধা ছাড়া, তেমনি বড় model-ও আত্মবিশ্বাসী ভুল তথ্য দিতে পারে। বিস্তারিত পর্ব ৭০-এ।
মূল উপলব্ধি: n বাড়ালে sparsity বাড়ে, তাই শুধু গোনা দিয়ে নিখুঁত লেখা আসে না; neural model শব্দের মিল আর দীর্ঘ প্রসঙ্গ ধরে, কিন্তু পরের token sample করার মূল চক্র আর সত্যতার সীমা একই থাকে।
প্র ০৪Speech recognition-এ HMM কেন কয়েক দশক টিকে ছিল, আর কেন শেষে neural model তার জায়গা নিল?
মুখের কথা একটি ক্রম: কয়েক মিলিসেকেন্ড পরপর শব্দতরঙ্গ থেকে কিছু সংখ্যা (feature) বের করা হয়, আর প্রশ্ন হলো এর পেছনে কোন ধ্বনি আর শব্দের ক্রম আছে। এটি প্রায় হুবহু HMM-এর decoding প্রশ্ন: লুকানো অবস্থা ধ্বনির অংশ, দেখা ফল শব্দতরঙ্গের feature. ১৯৭০-এর দশকে IBM-এ Frederick Jelinek-এর দল আর Carnegie Mellon-এ James Baker-এর মতো গবেষকেরা এই সম্ভাবনাভিত্তিক পথে কাজ করেন বলে উল্লেখ পাওয়া যায়, আর ১৯৮০-এর দশকে পদ্ধতিটি প্রধান ধারায় পরিণত হয়। Rabiner-এর ১৯৮৯ সালের tutorial এই জ্ঞান একসঙ্গে গুছিয়ে দেয় আর বহু বছর এই ক্ষেত্রের প্রবেশপথ হিসেবে পড়া হয়েছে।
HMM-এর সাফল্যের কয়েকটি স্পষ্ট কারণ আছে। Viterbi আর forward-এর খরচ ক্রমের দৈর্ঘ্যের সঙ্গে সরলরৈখিকভাবে বাড়ে, তাই লম্বা কথাও সামলানো যায়। Baum–Welch দিয়ে হাতে লেবেল না দেওয়া ডেটা থেকেও parameter শেখা যায়। আর কাঠামোটি modular: ধ্বনির model, শব্দের উচ্চারণ-অভিধান আর অংশ ৬-এর মতো n-gram language model আলাদাভাবে বানিয়ে একটি বড় graph-এ জোড়া যেত।
দুর্বলতা ছিল HMM-এর অনুমানগুলোতেই। প্রতিটি feature শুধু এখনকার অবস্থার ওপর নির্ভর করে, এটি কথার বাস্তবতার সঙ্গে মেলে না; আর অবস্থায় কত সময় থাকা হবে, তার সরল জ্যামিতিক distribution-ও অবাস্তব। ২০১০-এর দশকের শুরুতে গবেষকেরা emission-এর GMM-এর জায়গায় deep neural network বসিয়ে বড় উন্নতি দেখান; বেশ কয়েকটি গবেষণা দলের ২০১২ সালের একটি যৌথ প্রবন্ধ এই পরিবর্তনের একটি পরিচিত মাইলফলক। প্রথমে এগুলো ছিল “hybrid”: HMM কাঠামো থাকল, শুধু ভেতরের একটি অংশ neural. পরে end-to-end model (সরাসরি শব্দতরঙ্গ থেকে লেখা) অনেক ক্ষেত্রে HMM-কে পুরোপুরি সরিয়ে দেয়।
মূল উপলব্ধি: HMM দক্ষ algorithm, লেবেলহীন শেখা আর modular নকশার জন্য দীর্ঘদিন সেরা ছিল; তার সরল অনুমানগুলো যত বড় বাধা হয়ে দাঁড়াল, ডেটা আর গণনা বাড়ার সঙ্গে neural model তত এগিয়ে গেল।
প্র ০৫বাজারঘরের ক্রেতার আচরণে Markov অনুমান ভাঙলে কী হয়, আর কীভাবে বোঝা যায় ও সামলানো যায়?
অংশ ২-এর chain বলে, এ মাসে Dormant দুজন ক্রেতার পরের মাসের সম্ভাবনা একই, একজন এক মাস ধরে Dormant আর অন্যজন আট মাস ধরে হলেও। বাস্তবে এটি প্রায়ই ভুল: যত বেশি দিন কেউ কেনেননি, তাঁর ফেরার সম্ভাবনা সাধারণত তত কমে। একে “duration dependence” বলা যায়। আবার দুজন Active ক্রেতার একজন তিন বছরের পুরোনো, অন্যজন গত মাসে প্রথম কিনেছেন; তাঁদের আচরণ আলাদা হওয়া স্বাভাবিক। তৃতীয় সমস্যা সময়ের সঙ্গে বদল: ঈদ, পহেলা বৈশাখ বা বড় ছাড়ের মাসে স্থানান্তরের সম্ভাবনা বদলায়, অথচ আমাদের model ধরে নিয়েছে P প্রতি মাসে একই।
অনুমান ভাঙলে model-এর ভবিষ্যদ্বাণী পদ্ধতিগতভাবে ভুল হয়। যেমন দীর্ঘদিনের Dormant ক্রেতাদের ফেরার সম্ভাবনা বেশি দেখাবে, ফলে অফারের বাজেট ভুল জায়গায় যাবে। যাচাইয়ের একটি সরল উপায়: ইতিহাস থেকে “আগের মাসেও Dormant, এ মাসেও Dormant” আর “আগের মাসে Active, এ মাসে Dormant”, এই দুই দলের পরের মাসের হার আলাদা করে গুনুন। Markov অনুমান সত্য হলে দুটি হার প্রায় সমান হওয়ার কথা; বড় পার্থক্য থাকলে (আর নমুনা যথেষ্ট বড় হলে, পর্ব ৩৭-এর পরীক্ষার মতো যাচাই করে) অনুমানটি দুর্বল।
সামলানোর কয়েকটি পথ আছে, প্রতিটির দাম আছে। প্রথম, অবস্থাকে সমৃদ্ধ করা: “Dormant-১ মাস”, “Dormant-২+ মাস” আলাদা অবস্থা, বা দ্বিতীয় ক্রমের chain, যেখানে অবস্থা হলো শেষ দুই মাসের জোড়া। এতে অবস্থার সংখ্যা দ্রুত বাড়ে (৪ থেকে ১৬), আর প্রতিটি ঘর মাপতে বেশি ডেটা লাগে। দ্বিতীয়, semi-Markov model, যেখানে অবস্থায় কতদিন থাকা হবে তার আলাদা distribution থাকে। তৃতীয়, মাস অনুযায়ী আলাদা P, বা ক্রেতার বৈশিষ্ট্য দিয়ে সম্ভাবনা হিসাব করা একটি classifier (যেমন পর্ব ৫৯-এর logistic regression).
সরল model ভুল হলেও প্রায়ই কাজের, যদি জানেন কোথায় ভুল। কোনটি বাছবেন, তা নির্ভর করে ডেটার পরিমাণ, সিদ্ধান্তের ঝুঁকি আর ব্যাখ্যার প্রয়োজনের ওপর।
মূল উপলব্ধি: Markov অনুমান আসলে “অবস্থাটি যথেষ্ট তথ্য ধরে” এই দাবি; ডেটা দিয়ে তা যাচাই করুন, আর ভাঙলে অবস্থা সমৃদ্ধ করার খরচ আর লাভ মেপে পথ বাছুন।
অনুশীলন
প্রথমটি হাতে হিসাবের, দ্বিতীয়টি আউটপুট অনুমানের, তৃতীয়টি নকশা ও সমালোচনার, শেষটি কোড লিখে শোষিত হওয়ার সময় বের করার।
-
হাতে হিসাব: একটি এলাকার আবহাওয়া দুই অবস্থার Markov chain: Rain আর Dry. P(Rain → Rain) = 0.6, P(Rain → Dry) = 0.4, P(Dry → Rain) = 0.2, P(Dry → Dry) = 0.8. (ক) আজ Rain হলে দুই দিন পরে Rain-এর সম্ভাবনা। (খ) Stationary distribution. (গ) আবহাওয়া লুকানো, রাইডারের ডেলিভারি দেখা যায়: শুরু π = (Rain 0.4, Dry 0.6), P(late | Rain) = 0.7, P(late | Dry) = 0.2. পরপর দুই দিন late দেখা গেলে forward দিয়ে P(late, late), আর Viterbi দিয়ে সবচেয়ে সম্ভাব্য আবহাওয়ার পথ।
(ক) মাঝের দিনের দুই সম্ভাবনা যোগ: 0.6 × 0.6 + 0.4 × 0.2 = 0.36 + 0.08 = 0.44. পুরো P2 = [[0.44, 0.56], [0.28, 0.72]].
(খ) πR = 0.2 / (0.4 + 0.2) = ⅓, πD = ⅔. যাচাই: ⅓ × 0.6 + ⅔ × 0.2 = 0.2 + 0.133 = ⅓.
(গ) α1 = [0.4 × 0.7, 0.6 × 0.2] = [0.28, 0.12]. α2(Rain) = (0.28 × 0.6 + 0.12 × 0.2) × 0.7 = 0.192 × 0.7 = 0.1344; α2(Dry) = (0.28 × 0.4 + 0.12 × 0.8) × 0.2 = 0.208 × 0.2 = 0.0416. P(late, late) = 0.176. Viterbi: δ2(Rain) = max(0.168, 0.024) × 0.7 = 0.1176 (Rain থেকে), δ2(Dry) = max(0.112, 0.096) × 0.2 = 0.0224 (Rain থেকে)। সেরা পথ Rain → Rain, সম্ভাবনা 0.1176. লেখকের যন্ত্রে NumPy দিয়ে মেলানো:
[0.28 0.12] [0.1344 0.0416] 0.176. -
আউটপুট অনুমান করুন: চালানোর আগে প্রতিটি লাইনের আউটপুট লিখুন, তারপর যেকোনো ব্লকে বসিয়ে মিলিয়ে দেখুন।
import numpy as np P = np.array([[0.9, 0.1], [0.5, 0.5]]) print(P.sum(axis=1)) print(np.array([1.0, 0.0]) @ P @ P) print(np.linalg.matrix_power(P, 2)[1]) print(P @ np.array([1.0, 0.0]))[1. 1.] [0.86 0.14] [0.7 0.3] [0.9 0.5]প্রথম লাইন: দুটি সারিরই যোগ 1, বৈধ transition matrix. দ্বিতীয়: অবস্থা ১ থেকে দুই ধাপ, 0.9 × 0.9 + 0.1 × 0.5 = 0.86 আর 0.9 × 0.1 + 0.1 × 0.5 = 0.14. তৃতীয়: P2-এর দ্বিতীয় সারি, 0.5 × 0.9 + 0.5 × 0.5 = 0.7 আর 0.3. চতুর্থটি ফাঁদ: matrix-কে ডান দিক থেকে vector দিয়ে গুণ করলে আসে P-এর প্রথম কলাম [0.9, 0.5], যার যোগ 1.4, তাই এটি কোনো distribution নয়। সারি-stochastic রীতিতে পরের distribution হলো
x @ P(লেখকের যন্ত্রে মেলানো, NumPy 2.5.4). -
নকশা ও সমালোচনা: একজন সহকর্মী (বা একটি AI সহকারী) বাজারঘরের রাইডারদের এক মাসের ডেলিভারির সময় থেকে ট্রাফিকের HMM বানানোর জন্য নিচের পরিকল্পনা দিয়েছেন। অন্তত পাঁচটি সমস্যা চিহ্নিত করে একটি ভালো পরিকল্পনা বলুন।
১. hmm.MultinomialHMM(n_components=3) দিয়ে সব ডেটায় fit, একবার, random_state ছাড়া। ২. শেখা অবস্থা ০, ১, ২-কে সরাসরি Free, Heavy, Jam বলা। ৩. ১০,০০০ ধাপের forward নিজে লেখা, সম্ভাবনা সরাসরি গুণ করে। ৪. প্রতি ঘণ্টায় alpha-র সবচেয়ে বড় অবস্থা জুড়ে "সেরা পথ" বলা। ৫. Train ডেটার log-likelihood দেখে বলা, model খুব ভালো।- ভুল class: hmmlearn 0.3-এ ক্রমিক চিহ্নের জন্য
CategoricalHMM;MultinomialHMMএখন আসল multinomial model আর সতর্কবার্তা দেয়। - একটি শুরু, seed ছাড়া: Baum–Welch local maximum-এ আটকায়, আর ফল পুনরুৎপাদন করা যায় না; কয়েকটি
random_stateদিয়ে চালিয়ে সেরাটি নেওয়া দরকার। - নামকরণ অনুমান: label switching-এর কারণে অবস্থা ০ মানে Free হবে, এমন নিশ্চয়তা নেই; emission দেখে (fast-এর সম্ভাবনা) আর সম্ভব হলে কিছু জানা দিনের সঙ্গে মিলিয়ে নাম দিতে হবে।
- Underflow: ১০,০০০ ধাপে সরাসরি গুণ 0.0 দেয়; log-space বা scaling লাগবে।
- ভুল decoding: forward-এর alpha (বা posterior) প্রতি ঘণ্টায় আলাদা সেরা অবস্থা দেয়, পুরো পথের সেরা নয়; পথের জন্য Viterbi.
- Train ডেটায় বিচার: likelihood আলাদা দিনের ডেটায় মাপতে হবে, আর তুলনার জন্য একটি সরল baseline (যেমন লুকানো অবস্থা ছাড়া শুধু ফলের হার) লাগবে।
ভালো পরিকল্পনা: দিন ধরে train আর validation ভাগ (এক রাইডারের দিন একসঙ্গে,
lengthsদিয়ে আলাদা ক্রম জানিয়ে);CategoricalHMM, ৫-১০টি seed; validation-এscore; emission দেখে অবস্থার নাম;decodeদিয়ে পথ; আর লেখার আগে সব matrix-এর সারির যোগ যাচাই। - ভুল class: hmmlearn 0.3-এ ক্রমিক চিহ্নের জন্য
-
কোড লিখে শোষণের সময়: “ফিরে আসুন” অফার বন্ধ, তাই Churned সারি [0, 0, 0, 1]; বাকি সারি অংশ ২-এর মতো। NumPy দিয়ে fundamental matrix N = (I − Q)−1 আর প্রতিটি transient অবস্থা থেকে Churned হওয়ার গড় মাস বের করুন, তারপর ৫,০০০ জন Active ক্রেতার simulation দিয়ে Active-এর সংখ্যাটি যাচাই করুন (seed 42).
import numpy as np np.set_printoptions(precision=2, suppress=True) P = np.array([[0.70, 0.20, 0.08, 0.02], [0.30, 0.40, 0.25, 0.05], [0.10, 0.20, 0.50, 0.20], [0.00, 0.00, 0.00, 1.00]]) # Churned absorbing Q = P[:3, :3] # শুধু transient অবস্থা N = np.linalg.inv(np.eye(3) - Q) # fundamental matrix print("N:\n", N) print("গড় মাস (A, O, D থেকে):", N.sum(axis=1)) rng = np.random.default_rng(42) cum = P.cumsum(axis=1) months = [] for _ in range(5000): s, m = 0, 0 while s != 3: s = min(int(np.searchsorted(cum[s], rng.random(), side="right")), 3) m += 1 months.append(m) print("simulation, Active থেকে:", np.mean(months))লেখকের যন্ত্রে সত্যিকারের run (Python 3.14.4, NumPy 2.5.4):
N: [[8.22 3.82 3.22] [5.76 4.67 3.26] [3.95 2.63 3.95]] গড় মাস (A, O, D থেকে): [15.26 13.68 10.53] simulation, Active থেকে: 15.1634Active থেকে শুরু করা ক্রেতা ছেড়ে যাওয়ার আগে গড়ে ৮.২২ মাস Active, ৩.৮২ মাস Occasional আর ৩.২২ মাস Dormant থাকেন; যোগ ১৫.২৬. Simulation 15.16 দিয়েছে, কাছাকাছি; পার্থক্যটি নমুনার এলোমেলোতার। সংখ্যাগুলো বানানো সম্ভাবনা থেকে, কোনো আসল দোকানের নয়।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি HMM-এ Viterbi দিয়ে সবচেয়ে সম্ভাব্য লুকানো পথ বের করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 📋 π, A, B দেওয়া আছে, আর প্রতিটি সারির যোগ 1 কি না যাচাই
- 🟢 প্রথম কলাম: δ1(j) = πj × Bj(o1)
- 🔁 প্রতিটি পরের সময়ে প্রতিটি অবস্থার জন্য সেরা পূর্বসূরি খুঁজে δ আর backpointer লেখা
- 🏁 শেষ কলামে সবচেয়ে বড় δ বাছা
- ⬅️ backpointer ধরে পেছনে হেঁটে পুরো পথ তৈরি
একটি Markov chain-এর transition matrix P = [[0.7, 0.3], [0.4, 0.6]] (সারি = এখনকার অবস্থা)। অবস্থা ১ থেকে শুরু করলে ঠিক ২ ধাপ পরে অবস্থা ১-এ থাকার সম্ভাবনা কত?
- 0.49
- 0.61
- 0.70
- 0.12
NumPy-তে একটি সারি-stochastic matrix P-এর stationary distribution বের করতে কোন কাজটি ঠিক?
P ** 100-এর যেকোনো সারি- np.linalg.eig(P)-এর eigenvalue 1-এর eigenvector
- np.linalg.eig(P.T)-এর eigenvalue 0-এর eigenvector
- np.linalg.eig(P.T)-এর eigenvalue 1-এর eigenvector, তার যোগ দিয়ে ভাগ করে
একটি ছোট web-এ ৪টি পাতা, damping factor d = 0.85. PageRank-এর G = d · M + (1 − d)/n সূত্রে কোনো পাতা থেকে এমন পাতায় স্থানান্তরের সম্ভাবনা কত, যেখানে কোনো link নেই?
- 0.0375
- 0
- 0.15
- 0.25
একটি bigram model ছোট একটি বাংলা অনুচ্ছেদে প্রশিক্ষিত, smoothing ছাড়া। একটি নতুন বাক্যে এমন দুটি পাশাপাশি শব্দ আছে, যা প্রশিক্ষণের লেখায় কখনো পাশাপাশি আসেনি। model পুরো বাক্যটিকে কত সম্ভাবনা দেবে?
- অন্য জোড়াগুলোর গুণফল, অচেনা জোড়াটি বাদ দিয়ে
- প্রতিটি অচেনা জোড়ার জন্য 1/শব্দভান্ডারের আকার
- ঠিক 0
- model একটি error দেবে
একটি HMM-এ শুরু π = (0.5, 0.5), প্রথম দেখা ফলের emission সম্ভাবনা দুই অবস্থায় যথাক্রমে 0.8 আর 0.2. transition A = [[0.9, 0.1], [0.1, 0.9]], আর দ্বিতীয় ফলের emission যথাক্রমে 0.8 আর 0.2. forward algorithm-এ α2-এর প্রথম ঘর কত?
- 0.36
- 0.296
- 0.288
- 0.37
১০০০ ধাপের একটি observation ক্রমে forward algorithm সম্ভাবনা সরাসরি গুণ করে চালানো হলো, আর ফল এল 0.0. সবচেয়ে সম্ভাব্য কারণ আর ঠিক সমাধান কোনটি?
- ক্রমটি model-এর কাছে অসম্ভব; model বদলাতে হবে
- float32 ব্যবহার করা
- forward-এর বদলে Viterbi চালানো
- underflow; log-space-এ log-sum-exp বা প্রতি ধাপে scaling
Rabiner-এর tutorial অনুযায়ী HMM-এর তিনটি মূল প্রশ্নের সঙ্গে algorithm-এর কোন মিলটি ঠিক?
- Evaluation: Viterbi; Decoding: Baum–Welch; Learning: forward
- Evaluation: Baum–Welch; Decoding: forward; Learning: Viterbi
- Evaluation: forward; Decoding: Viterbi; Learning: Baum–Welch
- Evaluation: forward; Decoding: Baum–Welch; Learning: Viterbi
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Markov property: বর্তমান জানলে অতীত বাড়তি খবর দেয় না। এটি একটি অনুমান, যার সত্যতা নির্ভর করে অবস্থা কতটা তথ্য ধরে তার ওপর; ডেটা দিয়ে যাচাই করুন।
- Transition matrix-এর সারির যোগ 1; পরের distribution
x @ P, n ধাপেx @ np.linalg.matrix_power(P, n); দুই ধাপে মাঝের সব অবস্থার যোগ। Absorbing state-এ শোষণের গড় সময় N = (I − Q)−1-এর সারির যোগ। - Stationary distribution π P = π: বারবার গুণ, PT-এর eigenvalue 1-এর eigenvector আর simulation একই উত্তর দেয়; ergodic (সব অবস্থায় পৌঁছানো যায়, বাঁধা চক্র নেই) হলে π একটিই। PageRank = damping-সহ random surfer-এর π.
- n-gram model একটি Markov chain: গুনে ভাগ করা সম্ভাবনা, seed দিয়ে sample; স্থানীয়ভাবে সাবলীল, সামগ্রিকভাবে অর্থহীন; না-দেখা জোড়া শূন্য, তাই smoothing.
- HMM: লুকানো chain, দেখা emission; forward (যোগ) দেয় P(O), Viterbi (max + backpointer) দেয় সেরা পথ, Baum–Welch শেখে কিন্তু local maximum আর label switching-এর ঝুঁকিসহ; লম্বা ক্রমে log-space.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫৪ · অনিশ্চয়তায় সিদ্ধান্ত — expected utility আর MDP-এর পরিচয়পরবর্তী পর্বMarkov chain-এ কাজ আর পুরস্কার যোগ: value iteration আর policy.
- AI-এর ভিত্তি আর Python for AI · কোর্স হোম৭৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ১৭ · NumPy-তে linear algebra — vector, matrix, dot product আর normঝালাইmatrix গুণ, @ বনাম *, আর eigenvalue-র পরিচয়।
- পর্ব ৩৫ · AI-এর জন্য সম্ভাবনা — event, conditional আর independenceঝালাইconditional probability, total probability আর underflow.
- DSA · Dynamic programming-এর ভিত্তিপাশের ট্র্যাকforward আর Viterbi যে কৌশলে দাঁড়িয়ে: ছোট উপ-সমস্যার উত্তর জমিয়ে রাখা।
- DSA · Graphপাশের ট্র্যাকPageRank-এর link-কাঠামো আর state diagram-এর ভিত্তি।