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

Gaussian mixture আর EM algorithm — নরম clustering

Gaussian Mixtures and the EM Algorithm — Soft Clustering

Taught in Bangla, this lesson treats Gaussian mixtures as soft clustering and shows k-means as a limiting case. It explains why the log of a sum blocks a closed-form MLE, works one EM iteration by hand, writes EM in NumPy that matches scikit-learn's GaussianMixture exactly, and covers covariance types, BIC and AIC, restarts and reg_covar against collapsing components.

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

  • শক্ত আর নরম clustering-এর পার্থক্য, আর k-means কীভাবে GMM-এর একটি সীমা-রূপ
  • Mixture density, লুকানো চলক z, আর Bayes-এর নিয়মে responsibility
  • যোগফলের log কেন সরাসরি MLE আটকায়, আর EM-এর E-step ও M-step-এর সূত্র কোথা থেকে আসে
  • NumPy-তে এক আর দুই মাত্রার EM নিজে লেখা, log-likelihood-এর একটানা বৃদ্ধি দেখা, আর scikit-learn-এর সঙ্গে হুবহু মেলানো
  • Covariance type, BIC/AIC দিয়ে K বাছাই, n_init দিয়ে restart, আর reg_covar দিয়ে ভেঙে পড়া component ঠেকানো

AI-এর ভিত্তি কোর্সের পর্ব ৬৫-এ k-means হাতে আর NumPy-তে, k-means++, elbow আর silhouette এসেছে। সেখানে প্রতিটি বিন্দু ঠিক একটি দলে পড়ে। এই কোর্সের পর্ব ১৮ শিখিয়েছে likelihood সবচেয়ে বড় করে parameter বের করা, আর পর্ব ১৯ শিখিয়েছে multivariate Gaussian-এর density, উপবৃত্তাকার contour আর covariance-এর MLE. এই পর্ব তিনটিকে জোড়া দেয়: কয়েকটি Gaussian মিশিয়ে ডেটার একটি model বানানো, আর লুকানো দল-পরিচয় না জেনেও তার parameter শেখা। চালানো যায় এমন চারটি ব্লক পাতাতেই চলে (ব্রাউজারের Python: NumPy, SciPy, matplotlib আর scikit-learn সহ)। প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে, scikit-learn-এ প্রায় ১০ সেকেন্ড।

১ · শক্ত ভাগ না নরম ভাগ — k-means থেকে Gaussian mixture

ডাক্তার রোগী দেখে সাধারণত বলেন না “এটি নিশ্চিত ডেঙ্গু”। জ্বর, প্লাটিলেট আর ঋতু মিলিয়ে বলেন “ডেঙ্গুর সম্ভাবনা বেশি, টাইফয়েডও বাদ দেওয়া যায় না”, তারপর পরীক্ষা দেন। বাজারঘরের অর্ডারেও একই অবস্থা। সাধারণ দিনের ক্রেতা ৯০০ টাকার আশেপাশে কেনেন, উৎসবের আগের ক্রেতা ২,৪০০ টাকার আশেপাশে। ১,৫০০ টাকার একটি অর্ডার কোন দলের? “নিশ্চিত উৎসবের” বলার চেয়ে “৮৪% উৎসবের, ১৬% সাধারণ দিনের” বলা বেশি সৎ উত্তর।

Unsupervised learning-এ label নেই, তাই দলগুলো ডেটা থেকেই খুঁজতে হয়। দুই ধরনের উত্তর হতে পারে। Hard clustering প্রতিটি বিন্দুকে ঠিক একটি দলে বসায়, যেমন k-means. Soft clustering প্রতিটি বিন্দুর জন্য সব দলের সম্ভাবনা দেয়, যার যোগফল ১. দুই দলের মাঝখানের বিন্দু তখন নিজেই জানিয়ে দেয় যে সে অনিশ্চিত।

k-means চুপচাপ ধরে নেয় যে প্রতিটি দল গোল আর সব দলের ছড়ানো সমান। বাজারঘরে এটি প্রায়ই খাটে না: ছোট বাস্কেটের ছাত্রছাত্রীরা আঁটসাঁট দল, পাইকারি ক্রেতারা ছড়ানো, হেলানো আর সংখ্যায় কম। GMM প্রতিটি দলকে নিজের গড় μk, নিজের covariance matrix Σk আর নিজের অনুপাত πk দেয়। তাই দলগুলো হেলানো উপবৃত্ত, ভিন্ন আকার আর ভিন্ন আকৃতির হতে পারে।

k-means আসলে GMM-এর একটি সীমা। ধরুন সব component-এর covariance একই আর গোল, Σk = εI, আর সব πk সমান। তাহলে বিন্দু xi-এর জন্য component k-এর responsibility (অংশ ২-এ এর সংজ্ঞা আসবে):

rik = exp(−‖xi − μk‖² / 2ε) ÷ ∑j exp(−‖xi − μj‖² / 2ε)

এটি দূরত্বের বর্গের ওপর একটি softmax, আর ε তার “তাপমাত্রা”। ε বড় হলে সব r প্রায় সমান, ভাগ খুব নরম। ε → ০ হলে নিকটতম কেন্দ্রের r → ১ আর বাকিগুলো → ০. Σ আর π স্থির রেখে শুধু গড় হালনাগাদ করলে M-step-এর ওজন দেওয়া গড় হয়ে যায় দলের সাধারণ গড়, আর EM হুবহু k-means-এর “বসানো, তারপর গড় নেওয়া” loop. তাই k-means-কে বলা যায় শক্ত, গোল আর সমান মাপের GMM.

বিষয়k-meansGaussian mixture (EM)
ফলপ্রতিটি বিন্দুর একটি দলপ্রতিটি দলের সম্ভাবনা (responsibility)
দলের আকৃতিগোল, সমান ছড়ানোযেকোনো উপবৃত্ত, ভিন্ন মাপ আর ভিন্ন অনুপাত
যা কমায় বা বাড়ায়দলের ভেতরের দূরত্বের বর্গের যোগফল কমায়log-likelihood বাড়ায়
K বাছাইelbow, silhouetteBIC, AIC, held-out likelihood
বাড়তি লাভসরল আর দ্রুতdensity দেয়, তাই অস্বাভাবিকতা মাপা আর নমুনা বানানো যায়
ঝুঁকিlocal minimumlocal maximum, আর component ভেঙে পড়া

২ · Mixture density আর লুকানো চলক — ডেটা তৈরির গল্প

GMM-এর density হলো K-টি Gaussian-এর ওজন দেওয়া যোগফল:

p(x) = π1N(x; μ1, Σ1) + … + πKN(x; μK, ΣK), πk ≥ ০, ∑ πk = ১

এখানে N(x; μ, Σ) হলো পর্ব ১৯-এর multivariate Gaussian density, আর πk-কে বলে mixing weight. ওজনগুলোর যোগফল ১ বলে p(x)-এর মোট integral-ও ১, অর্থাৎ এটি একটি বৈধ density. সূত্রটিকে ডেটা তৈরির একটি গল্প (generative story) হিসেবে পড়া যায়: প্রতিটি ক্রেতার জন্য প্রথমে একটি পাশা চালা হয়, যার k নম্বর দিক পড়ার সম্ভাবনা πk; ফল z হলো ক্রেতার দল। তারপর সেই দলের Gaussian থেকে x আসে। আমরা x দেখি, z দেখি না। এমন চলককে বলে latent variable, বা লুকানো চলক। GMM তাই একটি generative model, যা থেকে নতুন নমুনাও বানানো যায়।

ডেটা তৈরির গল্প, আর EM-এর উল্টো পথে অনুমান

ওপরে: z বাছাই, তারপর x. নিচে: x থেকে z-এর সম্ভাবনা (E), তারপর parameter (M)

The generative story of a Gaussian mixture and the EM loop ডেটা তৈরির গল্প পাশা: π π = (0.68, 0.32) লুকানো z z = 1 বা 2 দেখা x x ~ N(μ_z, σ_z²) আমরা শুধু x দেখি; z লুকানো EM: উল্টো দিকে অনুমান E-step r = P(z = k │ x) M-step r-ওজনে π, μ, Σ প্রতি পাকে log-likelihood কমে না থামা: পরিবর্তন tol-এর নিচে নামলে
ছবি ২.১ — ওপরের অংশ model-এর অনুমান: π অনুযায়ী দল z বাছাই, তারপর সেই দলের Gaussian থেকে x. নিচের অংশ শেখার পথ: দেখা x থেকে প্রতিটি দলের সম্ভাবনা (E-step), সেই সম্ভাবনাকে ওজন ধরে parameter (M-step), তারপর আবার E-step.

Responsibility আসে Bayes-এর নিয়ম থেকে। z-এর prior P(z = k) = πk, আর likelihood p(x | z = k) = N(x; μk, Σk). তাই x দেখার পর z-এর posterior (AI-এর ভিত্তি কোর্সের পর্ব ৫০):

rik = P(zi = k | xi) = πkN(xi; μk, Σk) ÷ ∑j πjN(xi; μj, Σj)

প্রতিটি সারির r-এর যোগফল ১. লবটি “দল k-এর ক্রেতা হওয়া আর ঠিক এই x দেখা”-র ঘনত্ব, হরটি সব দল মিলিয়ে x দেখার ঘনত্ব, অর্থাৎ p(x). বাজারঘরের উদাহরণে (অংশ ৫-এর fit থেকে): ১,৫০০ টাকার অর্ডারে সাধারণ দিনের দলের ঘনত্ব খুব কম, কারণ সেটি গড় ৮৮১ থেকে প্রায় তিন standard deviation দূরে। উৎসবের দলের ঘনত্ব বেশি, যদিও ওই দলের ওজন মাত্র ০.৩২. ফল r ≈ (০.১৬, ০.৮৪).

দুটি সাধারণ ভুল। (১) Mixture মানে দুটি random variable যোগ করা নয়। X1 + X2-এর বণ্টন একটি Gaussian (গড় μ1 + μ2), কিন্তু mixture-এ প্রতিটি নমুনা একটি দল থেকে আসে, তাই density-তে দুটি চূড়া থাকতে পারে। (২) Component-এর নম্বরের কোনো মানে নেই। দল ১ আর দল ২ অদলবদল করলে likelihood হুবহু একই থাকে। একে বলে label switching. দুটি run-এর “দল ১” তাই আলাদা দলকে বোঝাতে পারে। তুলনার আগে গড় বা ওজন দিয়ে দলগুলো সাজিয়ে নিতে হয় (অংশ ৮-এর ব্লকে argsort ঠিক এই কাজ করে)।

Parameter কতগুলো? d মাত্রায় K-টি full-covariance component-এ গড়ের জন্য Kd-টি, covariance-এর জন্য K·d(d + ১)/২টি, আর ওজনের জন্য K − ১টি সংখ্যা (শেষ ওজনটি বাকিগুলো থেকে আসে)। d = ২, K = ৩ হলে ৬ + ৯ + ২ = ১৭টি। d = ২০ হলে প্রতিটি covariance একাই ২১০টি সংখ্যা চায়। অংশ ৭-এর covariance type এই সংখ্যা কমানোর উপায়।

৩ · সরাসরি MLE কেন কঠিন — যোগফলের log

parameter θ = (π, μ, Σ) শেখার স্বাভাবিক পথ maximum likelihood: n-টি বিন্দুর log-likelihood সবচেয়ে বড় করা।

ℓ(θ) = ∑i = 1..n log ( ∑k = 1..K πkN(xi; μk, Σk) )

একটি Gaussian হলে (পর্ব ১৮) log সরাসরি exp-কে কেটে দেয়, ℓ হয় μ-এর একটি দ্বিঘাত রাশি, আর gradient শূন্য করলেই নমুনার গড়। Mixture-এ log বসে আছে একটি যোগফলের বাইরে। নিচের stepper দেখায় এই একটি পার্থক্য কেন সব বদলে দেয়।

ℓ-কে μ₁-এর সাপেক্ষে সর্বোচ্চ করাNₖ(x) = N(x; μₖ, σₖ²)

            
  1. একটি Gaussian. log নিলে exp কেটে যায়, ℓ হয় μ-এর দ্বিঘাত রাশি, আর ∂ℓ/∂μ = ০ থেকে সরাসরি নমুনার গড়।
  2. Mixture. log এখন π₁N₁ + π₂N₂-এর বাইরে। log(a + b)-কে ভাঙা যায় না, তাই exp ভেতরে আটকে থাকে।
  3. Derivative. Chain rule দিলে প্রতিটি পদের সামনে একটি ভগ্নাংশ আসে, যা ঠিক অংশ ২-এর responsibility ri1. শূন্য করলে μ₁ দেখতে ওজন দেওয়া গড়ের মতো।
  4. বৃত্তাকার নির্ভরতা। সেই ওজন ri1 আবার μ₁-এর ওপর নির্ভর করে। সমীকরণটি μ₁-এর জন্য সরাসরি সমাধান করা যায় না।
  5. z জানা থাকলে। Complete-data log-likelihood-এ log সরাসরি একটি Gaussian-এর ওপর বসে, যোগফল দলে দলে ভাগ হয়, আর প্রতিটি দলের MLE সাধারণ গড় আর covariance.
  6. EM. অজানা zik-এর জায়গায় তার প্রত্যাশা rik বসিয়ে সমাধান করা, তারপর নতুন parameter দিয়ে r আবার হিসাব। যেখানে থামে, সেখানে ধাপ ৩-এর gradient শূন্য হওয়ার শর্ত পূরণ হয়।

আরও দুটি সমস্যা আছে। প্রথমত ℓ concave নয় (পর্ব ১২-এর ভাষায় non-convex সমস্যা)। label switching-এর জন্যই অন্তত K!-টি সমান উঁচু চূড়া থাকে, আর তার বাইরে নিচু চূড়া আর saddle point-ও থাকে। দ্বিতীয়ত, ℓ-এর কোনো সর্বোচ্চ মানই নেই: একটি component-এর গড় একটি বিন্দুর ওপর বসিয়ে তার σ শূন্যের দিকে নিলে ℓ → ∞ (অংশ ৮)। তাই “GMM-এর MLE” বলতে বাস্তবে বোঝায় একটি যুক্তিসংগত local maximum.

ইতিহাস: Karl Pearson ১৮৯৪ সালে Walter Weldon-এর মাপা নেপলসের কাঁকড়ার একটি দেহ-অনুপাতের ডেটায় দুটি normal-এর mixture বসান। তিনি moment পদ্ধতি ব্যবহার করেছিলেন, যার জন্য একটি নবম ঘাতের বহুপদী সমীকরণ হাতে সমাধান করতে হয়েছিল বলে প্রচলিত বিবরণে বলা হয়। প্রশ্ন ছিল কাঁকড়াগুলো একটি না দুটি উপ-প্রজাতি। Mixture model তাই ML-এর চেয়ে অনেক পুরোনো, কিন্তু প্রায় এক শতাব্দী ধরে এর fit করা ছিল কষ্টসাধ্য কাজ।

৪ · EM-এর দুই ধাপ — E-step আর M-step

Expectation–maximization (EM) stepper-এর শেষ ধাপের কৌশলটিকে একটি নিয়মিত loop বানায়। বর্তমান parameter θ(t) থেকে শুরু করে প্রতি পাকে:

E-step (Expectation)

প্রতিটি বিন্দু আর প্রতিটি component-এর জন্য rik = πkN(xi; μk, Σk) ÷ ∑j πjN(xi; μj, Σj), পুরোনো θ দিয়ে। এটি complete-data log-likelihood-এর প্রত্যাশা নেওয়ার সমান, কারণ E[zik] = rik.

M-step (Maximization)

Nk = ∑i rik (দল k-এর “কার্যকর সদস্য সংখ্যা”)। তারপর πk = Nk/n, μk = ∑ rikxi / Nk, আর Σk = ∑ rik(xi − μk)(xi − μk)ᵀ / Nk.

M-step-এর সূত্রগুলো পর্ব ১৯-এর MLE-রই ওজন দেওয়া রূপ: গড় হলো ওজন দেওয়া গড়, covariance হলো ওজন দেওয়া বর্গ-বিচ্যুতির গড় (n − ১ নয়, Nk দিয়ে ভাগ)। πk-এর সূত্রে একটি শর্ত আছে, ∑ πk = ১. পর্ব ১৬-এর Lagrange multiplier দিয়ে ∑k Nk log πk + λ(১ − ∑ πk)-এর derivative শূন্য করলে Nk/πk = λ, আর যোগফলের শর্ত থেকে λ = n. তাই πk = Nk/n, অর্থাৎ দল k-এর ওপর মোট responsibility-র অংশ। M-step-এ শর্তগুলো আপনা-আপনি রক্ষা পায়: π-এর যোগফল ১, আর Σk ওজন দেওয়া outer product-এর যোগফল বলে PSD (পর্ব ১০)।

পরীক্ষায় প্রায়ই এক মাত্রায় একটি পাক হাতে করতে বলা হয়। নিচের stepper চারটি বিন্দুতে তা-ই করে। সংখ্যাগুলো একটি scratch script-এ যাচাই করা।

চারটি বিন্দু, দুটি component; প্রতিটি সংখ্যা চার দশমিকে গোল
ধাপ
হিসাব
সংখ্যায়
মানে
  1. শুরু। চারটি বিন্দু ১, ২, ৪, ৭. শুরুর অনুমান: দুটি দলের গড় ২ আর ৬, দুটিরই σ = ২, ওজন সমান। এই parameter-এ ℓ = −৯.০৫৩৭.
  2. Density. σ = ২ হলে চূড়ার উচ্চতা ১/(২√(২π)) = ০.১৯৯৫. x = ১ গড় ২ থেকে ১ একক দূরে, তাই N₁ = ০.১৯৯৫ × e−১/৮ = ০.১৭৬০; গড় ৬ থেকে ৫ একক দূরে, তাই N₂ = ০.১৯৯৫ × e−২৫/৮ = ০.০০৮৮.
  3. E-step. x = ১-এ r₁ = ০.১৭৬০/(০.১৭৬০ + ০.০০৮৮) = ০.৯৫২৬. x = ৪ দুই গড় থেকে সমান দূরে, তাই ০.৫. প্রতিটি সারিতে r₁ + r₂ = ১.
  4. Nk আর π. N₁ = ০.৯৫২৬ + ০.৮৮০৮ + ০.৫ + ০.০৪৭৪ = ২.৩৮০৮, N₂ = ৪ − ২.৩৮০৮ = ১.৬১৯২. নতুন ওজন π = (০.৫৯৫২, ০.৪০৪৮).
  5. নতুন μ. দল ১-এর লব ০.৯৫২৬·১ + ০.৮৮০৮·২ + ০.৫·৪ + ০.০৪৭৪·৭ = ৫.০৪৬১, ভাগ ২.৩৮০৮ = ২.১১৯৫. দল ২-এর গড় ৫.৫২৯৮: x = ৪-এর অর্ধেক ওজন তাকে ৬ থেকে নিচে টেনেছে।
  6. নতুন σ². দল ১: ∑ r(x − ২.১১৯৫)² = ৪.১০৪২, ভাগ ২.৩৮০৮ = ১.৭২৩৯ (σ ≈ ১.৩১)। দল ২: ৫.৬৮৭৫ / ১.৬১৯২ = ৩.৫১২৫ (σ ≈ ১.৮৭)। এখানে নতুন μ ব্যবহার হয়, পুরোনোটি নয়।
  7. ফল। নতুন parameter-এ ℓ = −৮.৪৯৬৫, আগের −৯.০৫৩৭-এর চেয়ে বেশি। EM-এর প্রতিশ্রুতি ঠিক এটুকুই: ℓ কখনো কমবে না। অংশ ৫ দেখাবে কেন।

কখন থামবেন? সাধারণ নিয়ম: পরপর দুই পাকে ℓ-এর পরিবর্তন একটি ছোট সীমা tol-এর নিচে নামলে। scikit-learn-এর GaussianMixture প্রতি নমুনার গড় log-likelihood দেখে, ডিফল্ট tol=1e-3 আর max_iter=100. এই tol বেশ ঢিলা; সূক্ষ্ম তুলনার সময় (অংশ ৬ আর ৭-এর মতো) একে ছোট করা ভালো।

৫ · NumPy-তে EM — আর log-likelihood কেন কখনো কমে না

এবার বাজারঘরের ৩০০টি কাল্পনিক অর্ডার: ২১০টি সাধারণ দিনের (গড় ৯০০ টাকা, sd ২৫০), ৯০টি উৎসবের (গড় ২,৪০০, sd ৬০০)। Model জানে না কোনটি কোন দলের। শুরুর অনুমান ইচ্ছা করে দূরে রাখা হয়েছে: গড় ৫০০ আর ৩,০০০, দুটিরই sd ৮০০. প্রতিটি লাইনে আগে সেই পাকের শুরুর parameter-এ ℓ, তীরের পরে M-step-এর নতুন parameter.

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm
rng = np.random.default_rng(42)            # কাল্পনিক বাজারঘর: অর্ডারের মূল্য (টাকা)
x = np.concatenate([rng.normal(900, 250, 210),     # সাধারণ দিনের ক্রেতা
                    rng.normal(2400, 600, 90)])    # উৎসবের ক্রেতা
pi = np.array([0.5, 0.5])                  # শুরুর অনুমান, ইচ্ছা করে দূরে
mu = np.array([500.0, 3000.0])
sd = np.array([800.0, 800.0])
lls = []
for it in range(30):
    w = pi * norm.pdf(x[:, None], mu, sd)  # n × 2: πₖ·N(xᵢ; μₖ, σₖ)
    lls.append(np.log(w.sum(axis=1)).sum())          # এই θ-তে log-likelihood
    r = w / w.sum(axis=1, keepdims=True)   # E-step: responsibility
    Nk = r.sum(axis=0)                     # M-step: ওজন দেওয়া গণনা
    pi = Nk / len(x)
    mu = (r * x[:, None]).sum(axis=0) / Nk
    sd = np.sqrt((r * (x[:, None] - mu) ** 2).sum(axis=0) / Nk)
    if it in (0, 1, 2, 4, 9, 29):
        print(f"iter {it + 1:2d}: loglik {lls[-1]:9.2f} -> pi {pi.round(3)}"
              f" mu {mu.round()} sd {sd.round()}")
print("loglik never fell:", bool(np.all(np.diff(lls) >= -1e-9)))
for v in (1200, 1500, 1800):               # মাঝামাঝি অর্ডার: নরম ভাগ
    p = pi * norm.pdf(v, mu, sd)
    print(f"order {v} tk: r = {(p / p.sum()).round(3)}")
g = np.linspace(0, 5000, 400)
fig, ax = plt.subplots(figsize=(6, 3.6), layout="constrained")
ax.hist(x, bins=30, density=True, alpha=0.4, label="orders")
for k in (0, 1):
    ax.plot(g, pi[k] * norm.pdf(g, mu[k], sd[k]), "--", label=f"component {k + 1}")
ax.plot(g, (pi * norm.pdf(g[:, None], mu, sd)).sum(axis=1), "k", label="mixture")
ax.set_xlabel("order value (taka)"); ax.set_ylabel("density")
ax.set_title("Two-component GMM fitted by EM"); ax.legend(fontsize=8)
plt.show()

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

iter  1: loglik  -2536.12 -> pi [0.727 0.273] mu [ 950. 2363.] sd [336. 702.]
iter  2: loglik  -2325.16 -> pi [0.716 0.284] mu [ 914. 2398.] sd [257. 642.]
iter  3: loglik  -2303.21 -> pi [0.705 0.295] mu [ 895. 2388.] sd [228. 618.]
iter  5: loglik  -2297.90 -> pi [0.694 0.306] mu [ 886. 2355.] sd [216. 630.]
iter 10: loglik  -2297.51 -> pi [0.684 0.316] mu [ 882. 2318.] sd [213. 655.]
iter 30: loglik  -2297.50 -> pi [0.681 0.319] mu [ 881. 2306.] sd [212. 663.]
loglik never fell: True
order 1200 tk: r = [0.896 0.104]
order 1500 tk: r = [0.162 0.838]
order 1800 tk: r = [0.001 0.999]

প্রথম পাকেই ℓ −২,৫৩৬.১২ থেকে −২,৩২৫.১৬-এ লাফ দেয়, পঞ্চম পাকে −২,২৯৭.৯০, আর দশম থেকে ত্রিশতম পাকে বাড়ে মাত্র ০.০১. শেষ অনুমান: ওজন (০.৬৮১, ০.৩১৯), গড় (৮৮১, ২,৩০৬) টাকা, sd (২১২, ৬৬৩)। ডেটা বানানো হয়েছিল ওজন ০.৭ আর ০.৩, গড় ৯০০ আর ২,৪০০, sd ২৫০ আর ৬০০ দিয়ে। পার্থক্যের কারণ মাত্র ৩০০টি নমুনার এলোমেলোতা আর দুই দলের লেজের মিশে যাওয়া। loglik never fell: True মানে ৩০টি পাকের প্রতিটিতে ℓ বেড়েছে বা সমান থেকেছে; নিজে লেখা EM-এ এটিই সবচেয়ে সস্তা bug-পরীক্ষা। নরম ভাগ: ১,২০০ টাকার অর্ডার প্রায় ৯০% সাধারণ দিনের, ১,৫০০ টাকায় পাল্লা উল্টে ৮৪% উৎসবের, আর ১,৮০০ টাকা প্রায় নিশ্চিতভাবে উৎসবের। ছবিতে কালো রেখা (mixture) histogram-এর দুটি চূড়াই ধরেছে। ড্যাশ দেওয়া দুটি রেখা π দিয়ে গুণ করা component, আর তাদের যোগফলই কালো রেখা।

কেন ℓ কখনো কমে না: একটি নিম্নসীমা। লুকানো z-এর ওপর যেকোনো বণ্টন q(z) নিন। log একটি concave function, তাই Jensen's inequality (পর্ব ১২) বলে “গড়ের log ≥ log-এর গড়”। এ থেকে প্রতিটি বিন্দুর জন্য:

log p(x | θ) = log ∑z q(z)·[p(x, z | θ) / q(z)] ≥ ∑z q(z) log [p(x, z | θ) / q(z)] = ELBO(q, θ)

ডান দিকের রাশির নাম ELBO, অর্থাৎ evidence lower bound. দুই দিকের ফাঁকটুকু ঠিক KL divergence KL(q ‖ p(z | x, θ)), যা কখনো ঋণাত্মক নয় আর শূন্য হয় শুধু q যখন আসল posterior (AI-এর ভিত্তি কোর্সের পর্ব ৩৮)। এখন EM-কে এভাবে পড়ুন। E-step-এ q-কে posterior বানানো হয়, অর্থাৎ q(zi = k) = rik. KL শূন্য হয়, আর নিম্নসীমা বর্তমান θ-তে ℓ-কে ছুঁয়ে ফেলে। M-step-এ q স্থির রেখে θ বদলে ELBO বাড়ানো হয়। ELBO-র θ-নির্ভর অংশই complete-data log-likelihood-এর প্রত্যাশা, তাই বন্ধ সূত্রগুলো আসে। নতুন θ-তে ℓ ≥ নতুন ELBO ≥ পুরোনো ELBO = পুরোনো ℓ. তাই প্রতি পাকে ℓ বাড়ে বা একই থাকে।

তিনটি ফল মনে রাখুন। (১) EM-এ learning rate নেই, আর ℓ কখনো কমে না। (২) নিশ্চয়তা শুধু stationary বিন্দুর, সাধারণত একটি local maximum (অংশ ৭)। (৩) শেষের দিকে গতি ধীর, বিশেষ করে component-গুলো মিশে থাকলে। ব্লকের ১০ থেকে ৩০ নম্বর পাকে ℓ মাত্র ০.০১ বাড়ে, অথচ μ₂ তখনও ২,৩১৮ থেকে ২,৩০৬-এ সরছে। সমতল চূড়ায় ℓ প্রায় স্থির থাকলেও parameter নড়তে পারে, তাই খুব ঢিলা tol বিপজ্জনক। ELBO-র এই দৃষ্টিভঙ্গি পর্ব ৪৮ (Variational inference-এর ধারণা — ELBO আর approximation)-এর ভিত্তি। সেখানে q-কে আর নিখুঁত posterior বানানো যায় না, একটি সরল পরিবার থেকে বেছে নিতে হয়।

বাস্তব কোডে log-space. এক মাত্রায় সরাসরি density গুণ করা চলে, কিন্তু ২০-৩০ মাত্রায় N(x; μ, Σ) সহজেই ১০−৩০০-এর নিচে নামে, তখন সারির যোগফল শূন্য আর r = ০/০. সমাধান: log πk + log N-এর ওপর log-sum-exp কৌশল, অর্থাৎ সর্বোচ্চ মানটি আলাদা করে বের করে তারপর exp. scikit-learn ভেতরে scipy.special.logsumexp দিয়ে ঠিক এটাই করে।

৬ · দুই মাত্রায় full covariance — নিজের EM বনাম GaussianMixture

বাজারঘরের ৩৬০ জন কাল্পনিক ক্রেতার দুটি feature: গড় বাস্কেটের মূল্য (শত টাকায়) আর মাসে অর্ডারের সংখ্যা। তিনটি লুকানো দল: ছোট বাস্কেটের ১৫০ জন, পরিবারের ১৫০ জন, আর ৬০ জন পাইকারি ক্রেতা, যাঁদের ছড়ানো বেশি আর হেলানো উল্টো দিকে। নিচের ব্লক EM নিজে লেখে (covariance-এর M-step একটি einsum-এ), তারপর scikit-learn-কে একই শুরুর মান দেয়: weights_init, means_init আর precisions_init (covariance-এর inverse দিয়ে)। একই শুরু, একই tol হলে দুটির ফল মেলা উচিত।

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import multivariate_normal as mvn
from sklearn.mixture import GaussianMixture
rng = np.random.default_rng(42)            # (গড় বাস্কেট শত টাকা, মাসে অর্ডার)
X = np.vstack([rng.multivariate_normal([6, 3], [[4, 1], [1, 1]], 150),
               rng.multivariate_normal([15, 7], [[9, 4], [4, 4]], 150),
               rng.multivariate_normal([28, 4.5], [[30, -6], [-6, 3]], 60)])
K, n = 3, len(X); mu = X[rng.choice(n, K, replace=False)]  # শুরু: এলোমেলো ৩ সারি
cov = np.array([np.cov(X.T)] * K); pi = np.full(K, 1 / K); old = -np.inf
init = dict(weights_init=pi.copy(), means_init=mu.copy(), precisions_init=np.linalg.inv(cov))
for it in range(1, 501):
    w = np.column_stack([pi[k] * mvn.pdf(X, mu[k], cov[k]) for k in range(K)])
    ll = np.log(w.sum(axis=1)).mean()      # গড় log-likelihood
    r = w / w.sum(axis=1, keepdims=True)   # E-step
    Nk = r.sum(axis=0); pi = Nk / n        # M-step
    mu = (r.T @ X) / Nk[:, None]
    D = X[:, None, :] - mu                 # n × K × 2
    cov = np.einsum("nk,nki,nkj->kij", r, D, D) / Nk[:, None, None] + 1e-6 * np.eye(2)
    if ll - old < 1e-6: break
    old = ll
gm = GaussianMixture(K, init_params="random_from_data", tol=1e-6, **init).fit(X)
print("iterations:", it, gm.n_iter_, "| loglik/n:", round(ll, 6), round(gm.lower_bound_, 6))
print("weights:", pi.round(3), gm.weights_.round(3))
print("means:\n", mu.round(2), "\n", gm.means_.round(2))
print("max |cov diff|:", f"{np.abs(cov - gm.covariances_).max():.1e}")
fig, ax = plt.subplots(figsize=(6, 4), layout="constrained")
ax.scatter(X[:, 0], X[:, 1], c=gm.predict(X), s=8, cmap="viridis")
t = np.linspace(0, 2 * np.pi, 100)
for k in range(K):                         # Mahalanobis 2-এর উপবৃত্ত (পর্ব ১৯)
    lam, V = np.linalg.eigh(gm.covariances_[k])
    e = gm.means_[k][:, None] + V @ (2 * np.sqrt(lam)[:, None] * [np.cos(t), np.sin(t)])
    ax.plot(e[0], e[1], "r")
ax.set_xlabel("avg basket (100 taka)"); ax.set_ylabel("orders per month")
ax.set_title("GMM, full covariance: 2-sd ellipses"); plt.show()

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

iterations: 33 33 | loglik/n: -4.890552 -4.890552
weights: [0.405 0.432 0.163] [0.405 0.432 0.163]
means:
 [[ 6.02  2.95]
 [14.88  6.96]
 [28.45  4.07]]
 [[ 6.02  2.95]
 [14.88  6.96]
 [28.45  4.07]]
max |cov diff|: 2.8e-14

দুটি পদ্ধতিই ৩৩ পাকে থেমেছে। গড় log-likelihood ছয় দশমিক পর্যন্ত এক (−৪.৮৯০৫৫২), ওজন আর গড় হুবহু এক, আর covariance-এর সবচেয়ে বড় পার্থক্য ২.৮ × ১০−১৪, যা শুধু floating-point গোল করার ফল। হুবহু মেলাতে তিনটি খুঁটিনাটি লেগেছে। (১) একই শুরু। (২) প্রতিটি covariance-এর কর্ণে 1e-6 যোগ, যা scikit-learn-এর ডিফল্ট reg_covar. (৩) থামার একই নিয়ম: scikit-learn প্রতি পাকে আগে E-step-এ ℓ মাপে, তারপর M-step করে, তারপর পরিবর্তন দেখে। তাই নিজের loop-ও M-step-এর পরে থামার শর্ত দেখে, আর lower_bound_ হলো শেষ E-step-এর গড় log-likelihood. আসল অনুপাত ছিল ০.৪১৭, ০.৪১৭ আর ০.১৬৭; অনুমান ০.৪০৫, ০.৪৩২ আর ০.১৬৩. গড়গুলো আসল (৬, ৩), (১৫, ৭) আর (২৮, ৪.৫)-এর কাছে। পাইকারি দলের দ্বিতীয় গড় ৪.০৭, আসল ৪.৫ থেকে একটু দূরে, কারণ মাত্র ৬০টি সারি আর বড় ছড়ানো। Component-এর ক্রম এখানে কাকতালীয়ভাবে বানানোর ক্রমের সঙ্গে মিলেছে।

উপবৃত্তগুলো কোথা থেকে। প্রতিটি component-এর Σk-কে eigh দিয়ে ভাঙলে অক্ষের দিক আসে eigenvector থেকে আর অর্ধ-অক্ষের দৈর্ঘ্য c√λ (পর্ব ১৯-এর অংশ ২)। এখানে c = ২, অর্থাৎ Mahalanobis দূরত্ব ২-এর contour. দুই মাত্রায় এর ভেতরে প্রায় ৮৬.৫% থাকার কথা। ছবিতে পাইকারি দলের উপবৃত্ত সবচেয়ে বড় আর অন্য দুটির উল্টো দিকে হেলানো: বাস্কেট যত বড়, অর্ডার তত কম। k-means এমন আকৃতি ধরতে পারে না।

ব্রাউজারে init_params কেন বদলানো। GaussianMixture-এর ডিফল্ট init_params="kmeans" ভেতরে KMeans চালায়। এই পাতার ব্রাউজার-Python-এ তা একটি অপ্রাসঙ্গিক deprecation warning ছাপে, তাই পাতার সব ব্লকে "random_from_data" দেওয়া হয়েছে। ডেস্কটপে ডিফল্টটি ঠিকঠাক চলে। এই ব্লকে তিনটি *_init দেওয়া আছে বলে init_params-এর প্রভাব নেই, তবু scikit-learn সেই ধাপটি চালায়।

৭ · Covariance type, BIC/AIC দিয়ে K বাছাই, আর restart

Full covariance সবচেয়ে নমনীয়, কিন্তু parameter-ও সবচেয়ে বেশি। covariance_type দিয়ে Σk-এর ওপর গঠন বসানো যায়, যাকে বলে covariance type:

typeΣk-এর গঠনcovariance-এর parameterd = 2, K = 3-এ মোটউপবৃত্তের আকৃতি
fullপ্রতিটির নিজের পূর্ণ matrixK·d(d + 1)/29 + 6 + 2 = 17যেকোনো দিকে হেলানো, ভিন্ন মাপ
tiedসবার একই পূর্ণ matrixd(d + 1)/23 + 6 + 2 = 11সব দলের একই আকৃতি আর হেলানো
diagপ্রতিটির নিজের কর্ণ matrixK·d6 + 6 + 2 = 14অক্ষ-বরাবর, হেলানো নয়
sphericalপ্রতিটির σk²IK3 + 6 + 2 = 11বৃত্ত, মাপ আলাদা

শেষ কলামের “+ 6 + 2” হলো গড়ের Kd = ৬ আর ওজনের K − ১ = ২. এবার K কত? ℓ দিয়ে বাছাই করা যায় না, কারণ component বাড়ালে training ডেটায় ℓ প্রায় সবসময় বাড়ে। তাই parameter-এর জন্য শাস্তি যোগ করা হয়:

BIC = −2ℓ + p·ln n, AIC = −2ℓ + 2p

এখানে ℓ fit-এর সর্বোচ্চ log-likelihood, p parameter-এর সংখ্যা, n সারির সংখ্যা; দুটিই যত কম, তত ভালো। AIC (Hirotugu Akaike, ১৯৭৩–৭৪) ভবিষ্যৎ ডেটায় অনুমানের মান লক্ষ্য করে, BIC (Gideon Schwarz, ১৯৭৮) আসে Bayesian যুক্তি থেকে। n ≥ ৮ হলেই ln n > ২, তাই BIC কম component বেছে নেয়। scikit-learn-এ gm.bic(X) আর gm.aic(X) ঠিক এই সূত্র। নিচে অংশ ৫-এর অর্ডার-ডেটায় K = ১ বনাম K = ২ হাতে তুলনা।

n = 300, ln n = 5.7038; ℓ এসেছে fit-এর শেষে
ধাপ
হিসাব
সংখ্যায়
মানে
  1. শাস্তির একক। n = ৩০০ হলে ln n = ৫.৭০৩৮. প্রতিটি বাড়তি parameter-এর জন্য BIC ৫.৭০ পয়েন্ট শাস্তি দেয়, AIC দেয় মাত্র ২.
  2. K = ১. একটি Gaussian-এর MLE: গড় ১,৩৩৫.৮, sd ৭৮২.৮, ℓ = −২,৪২৪.৫৩. −২ℓ = ৪,৮৪৯.০৬, শাস্তি ১১.৪১, BIC = ৪,৮৬০.৪৭.
  3. K = ২. EM-এর শেষে ℓ = −২,২৯৭.৫০. Parameter ৫টি: দুটি গড়, দুটি sd আর একটি ওজন (অন্যটি ১ − π₁)। −২ℓ = ৪,৫৯৪.৯৯, শাস্তি ২৮.৫২, BIC = ৪,৬২৩.৫১.
  4. তুলনা। BIC-এর পার্থক্য প্রায় ২৩৭. Kass আর Raftery (১৯৯৫)-এর বহুল ব্যবহৃত মাপকাঠিতে ১০-এর বেশি পার্থক্য মানে “খুব শক্ত” প্রমাণ।
  5. AIC. AIC = −২ℓ + ২p: K = ১-এ ৪,৮৫৩.০৭, K = ২-এ ৪,৬০৪.৯৯. এখানে দুই মাপের উত্তর এক, তবে পরের ব্লকে দেখবেন সবসময় তা হয় না।

নিচের ব্লক অংশ ৬-এর ক্রেতা-ডেটায় চারটি covariance type আর K = ১ থেকে ৬ পর্যন্ত সব জোড়া fit করে BIC আর AIC তুলনা করে। তারপর K = ৩-এ শুধু এক শুরু থেকে (n_init=1) ছয়টি ভিন্ন seed চালায়, যাতে local maximum চোখে পড়ে।

import numpy as np
import matplotlib.pyplot as plt
from sklearn.mixture import GaussianMixture
rng = np.random.default_rng(42)            # একই কাল্পনিক ক্রেতা-ডেটা
X = np.vstack([rng.multivariate_normal([6, 3], [[4, 1], [1, 1]], 150),
               rng.multivariate_normal([15, 7], [[9, 4], [4, 4]], 150),
               rng.multivariate_normal([28, 4.5], [[30, -6], [-6, 3]], 60)])
Ks, types = range(1, 7), ["full", "tied", "diag", "spherical"]
opt = dict(tol=1e-5, max_iter=500, random_state=0)
full = [GaussianMixture(k, init_params="random_from_data", n_init=3, **opt).fit(X) for k in Ks]
bic, aic = {}, {}
for t in types:                            # শুরু: full fit-এর গড় (নিচের সতর্কতা)
    fits = [GaussianMixture(k, covariance_type=t, means_init=f.means_, init_params="random",
                            **opt).fit(X)
            for k, f in zip(Ks, full)]
    bic[t] = [g.bic(X) for g in fits]; aic[t] = [g.aic(X) for g in fits]
    print(f"{t:9s} best K: BIC {np.argmin(bic[t]) + 1}, AIC {np.argmin(aic[t]) + 1}"
          f" | BIC at K=3 {bic[t][2]:6.0f}")
print("full BIC K=1..6:", np.round(bic["full"]).astype(int))
print("full AIC K=1..6:", np.round(aic["full"]).astype(int))
for seed in range(6):                      # n_init=1: একেক শুরু, একেক শেষ
    g = GaussianMixture(3, init_params="random_from_data", tol=1e-6,
                        max_iter=500, random_state=seed).fit(X)
    print(f"seed {seed}: loglik/n {g.lower_bound_:.4f}  weights {np.sort(g.weights_).round(3)}")
fig, ax = plt.subplots(figsize=(6, 3.6), layout="constrained")
for t in types:
    ax.plot(Ks, bic[t], "o-", label=t)
ax.set_xlabel("number of components K"); ax.set_ylabel("BIC (lower is better)")
ax.set_title("BIC by covariance type"); ax.legend(fontsize=8); plt.show()

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

full      best K: BIC 3, AIC 6 | BIC at K=3   3621
tied      best K: BIC 6, AIC 6 | BIC at K=3   3754
diag      best K: BIC 5, AIC 6 | BIC at K=3   3716
spherical best K: BIC 6, AIC 6 | BIC at K=3   3901
full BIC K=1..6: [4204 3719 3621 3648 3669 3685]
full AIC K=1..6: [4184 3676 3555 3558 3557 3549]
seed 0: loglik/n -4.8906  weights [0.163 0.405 0.432]
seed 1: loglik/n -4.8906  weights [0.163 0.405 0.432]
seed 2: loglik/n -4.8906  weights [0.163 0.405 0.432]
seed 3: loglik/n -5.0455  weights [0.006 0.208 0.787]
seed 4: loglik/n -4.8906  weights [0.163 0.406 0.431]
seed 5: loglik/n -5.0467  weights [0.037 0.179 0.784]

Full: BIC-এর তালিকা ৪,২০৪ → ৩,৭১৯ → ৩,৬২১ → ৩,৬৪৮ → ৩,৬৬৯ → ৩,৬৮৫, তাই পরিষ্কার সর্বনিম্ন K = ৩, যত দল বানানো হয়েছিল ঠিক ততগুলো। AIC বেছেছে ৬, কিন্তু তার তালিকায় K = ৩-এর পর পরিবর্তন মাত্র কয়েক পয়েন্ট (৩,৫৫৫ থেকে ৩,৫৪৯)। হালকা শাস্তিতে বাড়তি component প্রায় বিনা খরচে ঢুকে পড়ে। অন্য type: tied-এ সব দলের এক আকৃতি, অথচ পাইকারি দল উল্টো দিকে হেলানো, তাই K = ৩-এ BIC ৩,৭৫৪, আর সে ৬টি component চায়। diag (৫টি) আর spherical (৬টি) অক্ষ-বরাবর উপবৃত্ত বা বৃত্ত জোড়া দিয়ে হেলানো দলকে নকল করে। K = ৩-এ চারটির মধ্যে full-এর BIC সবচেয়ে কম। এখানে বেশি K মানে “বেশি দল” নয়, “আকৃতি নকল করার খরচ”। Restart: ছয়টি seed-এর চারটি পৌঁছেছে −৪.৮৯০৬-এ, অংশ ৬-এর একই সমাধানে। Seed ৩ আর ৫ আটকে গেছে −৫.০৪৫৫ আর −৫.০৪৬৭-এ। প্রতি সারিতে পার্থক্য ০.১৫৫, ৩৬০ সারিতে মোট প্রায় ৫৬ log-likelihood একক। Seed ৩-এর একটি component-এর ওজন ০.০০৬, অর্থাৎ মাত্র দুজন ক্রেতা: খারাপ সমাধানের একটি পরিষ্কার লক্ষণ। Seed ৪-এর ওজনে তৃতীয় দশমিকের পার্থক্য শুধু tol-এর জন্য। n_init=6 দিলে scikit-learn নিজেই সবচেয়ে ভালোটি রাখত।

tied আর ফাঁকা শুরুর ফাঁদ (সত্যিকারের run). ব্লকে tied, diag আর spherical-কে full fit-এর গড় দিয়ে শুরু করানো হয়েছে, আর covariance-এর শুরুর হিসাবের জন্য init_params="random" (সব সারিতে এলোমেলো কিন্তু ভরা responsibility দেয়)। কারণ অন্য শুরুতে tied ভেঙে পড়ে। আরেকটি খুঁটিনাটি: means_init দিলেও ডিফল্ট init_params="kmeans" ভেতরে KMeans চালায়, তাই ব্রাউজারে warning আসে। "k-means++" আর "random_from_data" শুরুতে মাত্র K-টি সারিকে responsibility ১ দেয়, বাকি সব সারি ০. scikit-learn-এর tied covariance-এর সূত্র (XᵀX − ∑ Nkμkμkᵀ)/∑ Nk ধরে নেয় যে প্রতিটি সারির responsibility-র যোগফল ১. তাই প্রথম covariance বিশাল হয়, পরের E-step-এ সব r প্রায় সমান হয়, আর তিনটি গড় মিলে যায় সামগ্রিক গড়ে। ডেস্কটপে একই ডেটায় একটি run (Python 3.14.4, লেখার সময়ের সর্বশেষ stable scikit-learn 1.9.1; 1.8.0-তেও হুবহু একই ফল):

~/bazarghar-ml$ python tied_init.py
python 3.14.4 | numpy 2.5.4 | scikit-learn 1.9.1
k-means++        means [[13.5, 4.9], [13.6, 4.9], [13.5, 4.9]]  loglik/n -5.798
random_from_data means [[13.5, 4.9], [13.5, 4.9], [13.5, 4.9]]  loglik/n -5.798
kmeans           means [[16.3, 7.3], [30.7, 3.5], [6.7, 3.2]]  loglik/n -5.124
K=1 for reference means [[13.5, 4.9]]  loglik/n -5.798

প্রথম দুই শুরুতে তিনটি component একটিতে মিশে গেছে, ℓ হুবহু K = ১-এর সমান, অথচ কোনো warning নেই। শিক্ষা: fit-এর পর গড়গুলো আলাদা কি না আর ℓ K = ১-এর চেয়ে ভালো কি না, দেখে নিন।

Local maximum থেকে বাঁচার সাধারণ পথ restart: n_init-টি ভিন্ন শুরু থেকে EM চালিয়ে যে fit-এর ℓ সবচেয়ে বেশি, সেটি রাখা। scikit-learn-এ ডিফল্ট n_init=1, অর্থাৎ restart নেই। BIC বা AIC হিসাব করার আগে প্রতিটি K-তে যথেষ্ট restart দরকার। নইলে একটি খারাপ local maximum একটি ভালো K-কে খারাপ দেখাতে পারে।

৮ · নরম সিদ্ধান্ত, ভেঙে পড়া component আর reg_covar

predict_proba প্রতিটি নতুন ক্রেতার responsibility দেয়, predict শুধু সবচেয়ে বড়টির নম্বর। দুই দলের মাঝের ক্রেতার বেলায় এই পার্থক্য সিদ্ধান্ত বদলায়। ব্লকের দ্বিতীয় অংশ উল্টো দিকের একটি বিপদ দেখায়। বাজারঘরের অর্ডার-তালিকায় ৫টি হুবহু এক দামের (৯,৯৯৯ টাকা) “বান্ডেল” অর্ডার আছে। একটি component সেগুলোর ওপর বসে পড়লে কী হয়?

import numpy as np
from scipy.stats import norm
from sklearn.mixture import GaussianMixture
rng = np.random.default_rng(42)            # একই কাল্পনিক ক্রেতা-ডেটা
X = np.vstack([rng.multivariate_normal([6, 3], [[4, 1], [1, 1]], 150),
               rng.multivariate_normal([15, 7], [[9, 4], [4, 4]], 150),
               rng.multivariate_normal([28, 4.5], [[30, -6], [-6, 3]], 60)])
gm = GaussianMixture(3, init_params="random_from_data", n_init=3, tol=1e-6,
                     max_iter=500, random_state=0).fit(X)
order = np.argsort(gm.means_[:, 0])        # দল: ছোট, মাঝারি, বড় বাস্কেট
new = np.array([[6, 3], [10, 4.5], [11, 5.5], [21, 5.5], [24, 5]])
P = gm.predict_proba(new)[:, order]
for x, p in zip(new, P):
    print(f"customer {x}: soft {p.round(3)}  hard -> group {p.argmax() + 1}")

x = np.r_[rng.normal(900, 250, 40), [9999.0] * 5]   # ৫টি হুবহু এক দামের বান্ডেল
pi, mu, sd = np.array([0.9, 0.1]), np.array([900.0, 8000.0]), np.array([250.0, 1500.0])
for it in range(1, 7):                     # reg ছাড়া নিজের EM
    w = pi * norm.pdf(x[:, None], mu, sd)
    print(f"iter {it}: loglik {np.log(w.sum(axis=1)).sum():9.2f}  sd2 {sd[1]:.3g}")
    r = w / w.sum(axis=1, keepdims=True); Nk = r.sum(axis=0)
    pi, mu = Nk / len(x), (r * x[:, None]).sum(axis=0) / Nk
    sd = np.sqrt((r * (x[:, None] - mu) ** 2).sum(axis=0) / Nk)
    if sd[1] < 1e-6: print(f"sd2 -> {sd[1]:.3g}: density at 9999 -> infinity"); break
Xd = x[:, None]
for reg in (0.0, 1e-6, 100.0):
    try:
        g = GaussianMixture(2, reg_covar=reg, means_init=[[900], [8000]],
                            init_params="random_from_data", random_state=0).fit(Xd)
        sd = np.sqrt(g.covariances_.ravel())
        print(f"reg_covar={reg}: sd {sd[0]:.1f}, {sd[1]:.3g}  loglik {g.score(Xd) * len(x):.1f}")
    except ValueError as e:
        print(f"reg_covar={reg}: ValueError:", str(e)[:60], "...")

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

customer [6. 3.]: soft [0.99 0.01 0.  ]  hard -> group 1
customer [10.   4.5]: soft [0.45 0.55 0.  ]  hard -> group 2
customer [11.   5.5]: soft [0.039 0.96  0.001]  hard -> group 2
customer [21.   5.5]: soft [0.    0.044 0.956]  hard -> group 3
customer [24.  5.]: soft [0. 0. 1.]  hard -> group 3
iter 1: loglik   -343.30  sd2 1.5e+03
iter 2: loglik   -317.75  sd2 24.2
sd2 -> 0: density at 9999 -> infinity
reg_covar=0.0: sd 275.7, 3.64e-12  loglik -172.6
reg_covar=1e-06: sd 275.7, 0.001  loglik -267.3
reg_covar=100.0: sd 275.9, 10  loglik -313.3

নরম উত্তর: দলগুলো গড় বাস্কেট অনুযায়ী সাজানো, তাই নম্বর স্থির। (৬, ৩) ক্রেতা ৯৯% ছোট বাস্কেটের দলে। (১০, ৪.৫) ক্রেতা ০.৪৫ বনাম ০.৫৫, প্রায় মুদ্রা ছোড়া, অথচ শক্ত label বলে শুধু “দল ২”। (২১, ৫.৫) ক্রেতা ৯৫.৬% পাইকারি, আর (২৪, ৫) একেবারে ১.০. ভেঙে পড়া: নিজের EM-এ দ্বিতীয় component-এর sd প্রথম পাকে ১,৫০০, দ্বিতীয় পাকের শুরুতে ২৪.২, আর দ্বিতীয় M-step-এর পরে হুবহু ০. তখন সেই component শুধু পাঁচটি ৯,৯৯৯ টাকার অর্ডার ধরে আছে। পরের E-step-এ শূন্য দিয়ে ভাগ হতো, density অসীম। scikit-learn: reg_covar=0-এ sd ৩.৬৪ × ১০−১২ (floating-point গোলমালের জন্য হুবহু শূন্য নয়), আর ℓ = −১৭২.৬, তিনটির মধ্যে সবচেয়ে বেশি. শুধু likelihood দেখলে সবচেয়ে অর্থহীন model-টিকেই সেরা মনে হবে। ডিফল্ট 1e-6-এ sd-এর নিম্নসীমা √1e-6 = ০.০০১ টাকা, যা টাকার এককে কোনো বাধাই নয় (ℓ = −২৬৭.৩)। reg_covar=100 মানে variance-এ ১০০ টাকা², অর্থাৎ sd অন্তত ১০ টাকা; ℓ কমে −৩১৩.৩, কিন্তু model যুক্তিসংগত। variance হুবহু শূন্য হলে scikit-learn একটি ValueError দেয়, যা reg_covar বাড়াতে বা ডেটা scale করতে বলে; ব্লকের except অংশ সেই বার্তাটি ছাপার জন্য রাখা।

কেন এমন হয়। এক মাত্রায় একটি বিন্দু x-এর ওপর বসা component-এর density N(x; x, σ²) = ১/(σ√(২π)), যা σ → ০ হলে অসীম। বাকি বিন্দুগুলোকে অন্য component সামলায়, তাই মোট ℓ সীমাহীনভাবে বাড়ে। এটি likelihood singularity, আর এই কারণেই GMM-এর “global MLE” অর্থহীন। হুবহু এক মান বারবার থাকলে (বান্ডেলের দাম, এন্ট্রির ডিফল্ট মান) সমস্যা সহজে আসে। d মাত্রায় d-টির কম বিন্দু একটি component-এ পড়লেই তার Σ singular হয়।

প্রতিকার। (১) Covariance regularization: reg_covar প্রতিটি Σk-এর কর্ণে একটি ধ্রুবক যোগ করে, ফলে variance তার নিচে নামতে পারে না। ডিফল্ট 1e-6 মাপা হয় ডেটার এককে, টাকা²-এ। ৯,৯৯৯ টাকার উদাহরণে তা কার্যত কোনো বাধা নয়। তাই আগে feature scale করুন (যেমন StandardScaler), তারপর reg_covar বাছুন। (২) হুবহু এক মানগুলো সত্যিই আলাদা দল, না ভুল, দেখুন। (৩) খুব ছোট ওজনের component (π < ০.০১) সন্দেহ করুন। (৪) Covariance-এর ওপর prior বসালে MAP অনুমান (পর্ব ১৮) singularity এড়ায়।

Bayesian GMM, একটি নোট। scikit-learn-এর BayesianGaussianMixture ওজন, গড় আর covariance-এর ওপর prior বসায় আর fit করে variational inference দিয়ে, EM দিয়ে নয়। ডিফল্ট weight_concentration_prior_type="dirichlet_process". Dirichlet process prior-এর সুবিধা: n_components একটু বেশি দিলে অপ্রয়োজনীয় component-এর ওজন প্রায় শূন্যে নেমে যেতে পারে। তবে ফল prior-এর মানের ওপর নির্ভর করে। এই Bayesian Gaussian mixture-এর পেছনের পদ্ধতি পর্ব ৪৮-এ (Variational inference-এর ধারণা) আসবে।

EM শুধু GMM-এর জন্য নয়। লুকানো চলক জানলে যেখানে fit সহজ, সেখানেই EM খাটে: HMM-এর Baum–Welch (AI-এর ভিত্তি কোর্সের পর্ব ৫৩-এর HMM), missing value-সহ Gaussian, factor analysis আর অনেক topic model. পরের পর্বগুলোতে GMM আবার আসবে: পর্ব ৪০ (Density estimation — histogram, KDE আর mixture model)-এ parametric density হিসেবে, আর পর্ব ৪৩ (Anomaly detection — isolation forest, LOF আর one-class SVM)-এ score_samples দিয়ে কম-ঘনত্বের বিন্দু খোঁজায়।

আরও গভীরে: EM-এর সাধারণ রূপ আর নাম আসে Dempster, Laird আর Rubin-এর ১৯৭৭ সালের প্রবন্ধ থেকে (Journal of the Royal Statistical Society, Series B পত্রিকায়)। C. F. Jeff Wu (১৯৮৩) দেখান যে তাঁদের convergence প্রমাণে একটি ফাঁক ছিল, আর কোন শর্তে EM সত্যিই stationary বিন্দুতে পৌঁছায় তা স্পষ্ট করেন। Neal আর Hinton (১৯৯৮) EM-কে ELBO-র ওপর পালাক্রমে সর্বোচ্চকরণ হিসেবে দেখান, যা থেকে incremental আর online EM-এর পথ খোলে। Xu আর Jordan (১৯৯৬) GMM-এ EM-এর গতিকে gradient পদ্ধতির সঙ্গে তুলনা করেন: component বেশি মিশে থাকলে EM ধীর হয়।

পরীক্ষা ও ইন্টারভিউতে: এই ধরনের প্রশ্ন বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় প্রায়ই আসে।

  • k-means আর GMM-এর সম্পর্ক? Σk = εI আর সমান π নিয়ে ε → ০ করলে responsibility শক্ত হয়, আর EM হয় k-means.
  • GMM-এর MLE-র বন্ধ সূত্র নেই কেন? log একটি যোগফলের বাইরে বসে, তাই gradient-এর শর্তে responsibility আসে, যা নিজেই parameter-এর ওপর নির্ভর করে।
  • EM কী নিশ্চয়তা দেয়? প্রতি পাকে log-likelihood কমে না; শুধু local maximum বা stationary বিন্দু, global নয়।
  • ℓ = −১০০, p = ৫, n = ১০০ হলে BIC? ২০০ + ৫ × ৪.৬০৫ = ২২৩.০; AIC = ২১০.
  • reg_covar কেন লাগে? একটি component একটি বিন্দু বা হুবহু এক মানগুলোতে বসে variance শূন্য হলে likelihood অসীম হয়, আর covariance invertible থাকে না।

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

AI assistant-এর GMM কোড দেখতে বিশ্বাসযোগ্য, কিন্তু ভুল লুকিয়ে থাকে parameter-এর নাম, ডিফল্ট মান আর K বাছাইয়ের যুক্তিতে:

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
সবচেয়ে বড় log-likelihood দেখে K বাছেK বাড়লে training ℓ প্রায় সবসময় বাড়েBIC/AIC বা held-out log-likelihood, কয়েকটি seed-এ
বানানো parameter দেয়, যেমন covariance_type="diagonal" বা GaussianMixture(k=3)আসল নাম "diag" আর n_componentsনিজের version-এর docs পড়ুন, চালিয়ে দেখুন
ধরে নেয় n_init ডিফল্টে কয়েকবার চলেডিফল্ট ১, তাই একটি খারাপ local maximum থেকে যেতে পারেn_init নিজে দিন, seed বদলে lower_bound_ তুলনা করুন
দাবি করে EM global maximum খুঁজে পায়শুধু local maximum বা stationary বিন্দুভিন্ন শুরুতে ফল আর ওজন মিলিয়ে দেখুন
feature scale না করে reg_covar-এর ডিফল্ট রাখে1e-6 ডেটার এককে মাপা; টাকা আর গণনা মিশলে অর্থহীনআগে scale, তারপর reg_covar; ছোট ওজনের component খুঁজুন
দুই run-এর predict নম্বর সরাসরি তুলনা করেLabel switching: নম্বরের কোনো স্থায়ী মানে নেইগড় দিয়ে সাজান, বা adjusted Rand index দিয়ে তুলনা
নিজে লেখা EM-এ সরাসরি density গুণ করেবেশি মাত্রায় underflow, r = ০/০log-sum-exp; scikit-learn-এর সঙ্গে ছোট উদাহরণে মেলান
responsibility-কে ক্রেতার আসল “ধরন”-এর নিশ্চিত সম্ভাবনা বলেModel Gaussian না হলে সম্ভাবনা অতি-আত্মবিশ্বাসীদলগুলোর প্রোফাইল দেখুন, ব্যবসায়িক মানে যাচাই করুন

উত্তরের সঙ্গে যাচাইয়ের কোড চেয়ে নিন; নিজে চালানো অংশটিই আসল পরীক্ষা।

আমার কাছে বাজারঘরের ২,০০০ ক্রেতার ৪টি feature আছে (গড় বাস্কেট, মাসে অর্ডার, ছাড়ের ব্যবহার, ফেরতের হার)। Gaussian mixture দিয়ে নরম segment বানাতে চাই। ১. scikit-learn-এর GaussianMixture দিয়ে কোড লেখো: scaling, চারটি covariance_type, K = 1 থেকে 8, প্রতিটিতে n_init, আর BIC ও AIC-এর টেবিল। আমার scikit-learn version-এ প্রতিটি parameter-এর ডিফল্ট মান কী, সেটিও বলো। ২. NumPy দিয়ে log-space-এ (logsumexp) একটি ছোট EM লেখো, যা seed দেওয়া ছোট ডেটায় GaussianMixture-এর সঙ্গে একই শুরু থেকে মেলে, আর প্রতি পাকে log-likelihood কমে না তা যাচাই করে। ৩. কোনো component ভেঙে পড়লে বা খুব ছোট ওজন পেলে কীভাবে ধরব, আর reg_covar কীভাবে বাছব, বলো। ৪. দুই run-এর segment নম্বর কীভাবে মেলাব (label switching), আর segment-গুলোর ব্যবসায়িক মানে কীভাবে যাচাই করব, বলো। ৫. তোমার উত্তরের কোন অংশে ভুল হওয়ার সম্ভাবনা বেশি, সেটিও জানাও।

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

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

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

প্র ০১Pearson ১৮৯৪ সালে mixture fit করেছিলেন, অথচ EM-এর সাধারণ রূপ এল ১৯৭৭ সালে। এত সময় লাগল কেন, আর ১৯৭৭-এর প্রবন্ধ নতুন কী দিল?

Pearson-এর পদ্ধতি ছিল moment মেলানো। ডেটার প্রথম কয়েকটি moment (গড়, variance, skewness ইত্যাদি) mixture-এর তাত্ত্বিক moment-এর সমান ধরা হয়, তারপর সমীকরণগুলো সমাধান করা হয়। দুই component-এর এক মাত্রার mixture-এ পাঁচটি অজানা, তাই পাঁচটি moment, আর প্রচলিত বিবরণ অনুযায়ী শেষে একটি নবম ঘাতের বহুপদী। কম্পিউটার ছাড়া এটি ছিল বিশাল পরিশ্রম, আর বেশি component বা বেশি মাত্রায় কার্যত অসম্ভব। পরে Fisher-এর maximum likelihood বেশি দক্ষ বলে প্রতিষ্ঠিত হয়, কিন্তু mixture-এ তার বন্ধ সমাধান নেই।

EM-ধরনের পুনরাবৃত্তি ১৯৭৭-এর আগেও আলাদা আলাদা জায়গায় দেখা গিয়েছিল: missing data-র জন্য H. O. Hartley-র ১৯৫৮ সালের কাজ, জেনেটিক্সের gene-counting পদ্ধতি, আর ১৯৭০ সালের আশেপাশে Baum ও সহকর্মীদের HMM-এর অ্যালগরিদম। কিন্তু প্রতিটি ছিল নির্দিষ্ট একটি সমস্যার কৌশল। Dempster, Laird আর Rubin এগুলোকে এক কাঠামোয় আনেন। তাঁরা “incomplete data” ধারণা দেন, যেখানে দেখা ডেটা আর লুকানো অংশ মিলে complete data. তাঁরা E-step আর M-step-এর নাম দেন, আর দেখান যে likelihood কখনো কমে না। ফলে একটি নতুন সমস্যায় EM লেখার রেসিপি সবার হাতে এল: লুকানো চলক খুঁজুন, complete-data likelihood লিখুন, তার প্রত্যাশা নিন, সর্বোচ্চ করুন।

সময় লাগার আরেকটি কারণ কম্পিউটার। EM শত শত পাক চালায়, প্রতিটি পাকে সব ডেটা ছুঁয়ে। ১৯৭০-এর দশকে কম্পিউটার সহজলভ্য হওয়ায় এই ধরনের পুনরাবৃত্তি ব্যবহারিক হয়। প্রবন্ধটি পরিসংখ্যানের সবচেয়ে বেশি উদ্ধৃত কাজগুলোর একটি বলে ব্যাপকভাবে উল্লেখ করা হয়। তবে এর convergence প্রমাণের একটি ফাঁক Wu ১৯৮৩ সালে ঠিক করেন।

মূল উপলব্ধি: Mixture-এর ধারণা পুরোনো; নতুন ছিল লুকানো চলককে কেন্দ্রে রেখে একটি সাধারণ, নিশ্চিতভাবে উন্নতি করা পুনরাবৃত্তি, যা কম্পিউটারের যুগে ব্যবহারিক হয়ে ওঠে।

প্র ০২GMM-এর ℓ সরাসরি gradient ascent বা Adam দিয়েও বাড়ানো যায়। তাহলে EM কেন এত জনপ্রিয়, আর কখন অন্য পথ ভালো?

EM-এর সুবিধাগুলো ব্যবহারিক। প্রথমত, কোনো learning rate নেই। প্রতিটি M-step একটি বন্ধ সূত্র, তাই পর্ব ১৩-এর step size বাছাইয়ের ঝামেলা নেই। দ্বিতীয়ত, শর্তগুলো আপনা-আপনি রক্ষা পায়। π-এর যোগফল ১ থাকে, আর প্রতিটি Σk PSD থাকে। Gradient ascent-এ এর জন্য softmax-parameterisation আর Cholesky-parameterisation লাগে, নইলে একটি ধাপেই covariance অবৈধ হয়ে যেতে পারে। তৃতীয়ত, ℓ কখনো কমে না, তাই debugging সহজ: কোনো পাকে ℓ কমলে কোডে নিশ্চিত ভুল আছে। অংশ ৫-এর ব্লক ঠিক এই পরীক্ষাটি চালায়।

দুর্বলতাও আছে। EM-এর গতি শেষের দিকে রৈখিক, আর হার নির্ভর করে লুকানো চলক কতটা “অনিশ্চিত” তার ওপর। Component-গুলো বেশি মিশে থাকলে responsibility প্রায় সমান থাকে। তখন প্রতিটি পাকে parameter সামান্য সরে, আর শত শত পাক লাগে। Xu আর Jordan (১৯৯৬) দেখান যে EM-এর ধাপকে একটি বিশেষ projection matrix দিয়ে গুণ করা gradient ধাপ হিসেবে লেখা যায়। ভালোভাবে আলাদা দলে এটি প্রায় Newton-এর মতো দ্রুত, মিশে থাকা দলে ধীর। তাই কেউ কেউ শেষের দিকে Newton বা quasi-Newton-এ (পর্ব ১৫) চলে যান, বা EM-কে ত্বরান্বিত করার পদ্ধতি ব্যবহার করেন।

গভীর শিক্ষার যুগে আরেকটি পরিস্থিতি সাধারণ: mixture একটি বড় neural network-এর অংশ। উদাহরণ mixture density network, যেখানে network প্রতিটি ইনপুটের জন্য π, μ, σ দেয়। সেখানে M-step-এর বন্ধ সূত্র থাকে না, তাই পুরো জিনিসটি mini-batch SGD বা Adam দিয়ে শেখানো হয়, log-sum-exp আর softmax ব্যবহার করে। খুব বড় ডেটায় batch EM-এর প্রতিটি পাক সব সারি ছোঁয়, তাই online বা mini-batch EM-ও ব্যবহৃত হয়।

মূল উপলব্ধি: EM-এর জনপ্রিয়তার কারণ নিরাপত্তা আর সরলতা: learning rate নেই, শর্ত ভাঙে না, ℓ কমে না। মিশে থাকা দলে বা neural network-এর ভেতরে gradient-ভিত্তিক পথ বেশি কাজের।

প্র ০৩GMM-এর likelihood-এর সর্বোচ্চ মানই যদি না থাকে, তাহলে “GMM fit করা” বলতে আসলে কী বোঝায়?

অংশ ৮-এ দেখেছি, একটি component একটি ডেটা-বিন্দুর ওপর বসে variance শূন্যের দিকে নিলে ℓ সীমাহীনভাবে বাড়ে। গাণিতিকভাবে তাই সব parameter-এর ওপর ℓ-এর supremum অসীম, আর “global maximum likelihood estimate” সংজ্ঞায়িতই নয়। এটি পরিসংখ্যানবিদদের কাছে বহু দশক ধরে জানা কথা। তবু GMM বাস্তবে কাজ করে, কারণ আমরা আসলে global maximum খুঁজি না। খুঁজি parameter space-এর একটি “যুক্তিসংগত” অঞ্চলে একটি local maximum, যেখানে প্রতিটি component-এর variance বাস্তব ছড়ানোর কাছাকাছি।

তাত্ত্বিক দিক থেকে কিছু ফল এই অবস্থানকে সমর্থন করে। নির্দিষ্ট শর্তে, ডেটা বাড়লে সত্যিকারের parameter-এর কাছে likelihood equation-এর একটি consistent মূল থাকে। অর্থাৎ ভালো local maximum-টি অর্থবহ, যদিও কোথাও আরও উঁচু “অসীম চূড়া” আছে। সমস্যা হলো ব্যবহারিক: EM যেকোনো শুরু থেকে সেই অসীম চূড়ার দিকেও হাঁটতে পারে, বিশেষ করে ছোট n, বেশি মাত্রা, বেশি component বা হুবহু এক মান থাকলে।

তাই “fit করা” আসলে কয়েকটি সিদ্ধান্তের সমষ্টি। প্রথমত, likelihood-কে বাধা দেওয়া: reg_covar-এর মতো variance-এর নিম্নসীমা, বা covariance-এর ওপর prior (inverse-Wishart), যা MAP অনুমানকে সসীম রাখে। দ্বিতীয়ত, শুরু বাছাই: k-means বা k-means++ দিয়ে যুক্তিসংগত শুরু, আর কয়েকটি restart. তৃতীয়ত, ফল যাচাই: কোনো component-এর ওজন খুব ছোট বা variance অস্বাভাবিক ছোট হলে বাতিল। চতুর্থত, held-out ডেটায় মূল্যায়ন: অসীম চূড়ায় বসা model training ডেটায় চমৎকার, কিন্তু নতুন ডেটায় খারাপ, কারণ নতুন বিন্দু ঠিক সেই এক বিন্দুতে পড়ে না।

এখানে overfitting-এর একটি চরম রূপ দেখা যায়। Model ক্ষমতা (একটি component-এর variance শূন্যে নেওয়া) ব্যবহার করে training ডেটার একটি বিন্দু মুখস্থ করে। প্রতিকারও সেই পরিচিত সমাধান: regularization, prior আর validation.

মূল উপলব্ধি: GMM-এর MLE একটি সুসংজ্ঞায়িত বিন্দু নয়, বরং বাধা দেওয়া একটি খোঁজ। reg_covar, prior, ভালো শুরু আর held-out মূল্যায়ন মিলে “ভালো local maximum”-কে অর্থবহ উত্তর বানায়।

প্র ০৪BIC, AIC, held-out likelihood আর ব্যবসার প্রয়োজন — K বাছাইয়ে কোনটিকে কতটা বিশ্বাস করবেন?

দুটি মাপ দুটি ভিন্ন প্রশ্নের উত্তর দেয়। Akaike ১৯৭০-এর দশকের শুরুতে AIC দেন KL divergence-এর যুক্তিতে: কোন model নতুন ডেটার বণ্টনের সবচেয়ে কাছে, তার একটি আসন্ন অনুমান। তাই AIC অনুমান-ক্ষমতায় মনোযোগী, আর সত্যিকারের model তালিকায় না থাকলেও যুক্তিসংগত। Schwarz ১৯৭৮ সালে BIC দেন Bayesian যুক্তিতে: প্রতিটি model-এর marginal likelihood-এর একটি বড়-n আসন্ন রূপ। আসল model তালিকায় থাকলে BIC ডেটা বাড়ার সঙ্গে সেটিকেই বেছে নেয়, যাকে বলে consistency. শাস্তি ln n বলে BIC কম component চায়। অংশ ৭-এর ব্লকে full covariance-এ BIC বেছেছে ৩, AIC ৬. দুটিই নিজের সংজ্ঞায় “ঠিক”।

Mixture model-এ একটি সূক্ষ্মতা আছে। BIC আর AIC-এর প্রচলিত যুক্তি কিছু regularity শর্তের ওপর দাঁড়ানো, আর mixture-এ K কমানোর সীমায় (যেমন একটি ওজন শূন্য, বা দুটি গড় এক) সেই শর্তগুলো ভাঙে। তবু অভিজ্ঞতায় BIC GMM-এ প্রায়ই ভালো কাজ করে। Held-out log-likelihood (cross-validation-এ) কোনো আসন্ন সূত্র ছাড়াই সরাসরি জিজ্ঞেস করে: নতুন ডেটা কতটা ভালো ব্যাখ্যা হয়? এর খরচ বেশি।

তারপর আসে model-এর ভুল আকৃতি। ডেটা Gaussian না হলে, যেমন বাঁকা অর্ডার-মূল্য, একটি আসল দলকে নকল করতে একাধিক Gaussian লাগে। তখন BIC বেশি K বেছে নেবে, আর সেই K “দলের সংখ্যা” নয়, “আকৃতি নকল করার খরচ”। অংশ ৭-এ diag-এর K = ৫ আর spherical-এর K = ৬ ঠিক এই ঘটনা: হেলানো দল ধরতে তারা অনেকগুলো অক্ষ-বরাবর উপবৃত্ত জোড়া দিয়েছে।

শেষে ব্যবসা। বাজারঘরের মার্কেটিং দল হয়তো চারটির বেশি segment-এর জন্য আলাদা অফার বানাতে পারবে না। সৎ পথ: BIC আর AIC-এর বক্ররেখা দেখুন, যেখানে বক্ররেখা প্রায় সমতল সেখানে কয়েকটি K-কে “সমান ভালো” ধরুন, তারপর দলগুলোর প্রোফাইল, স্থিতিশীলতা (ভিন্ন seed ও bootstrap নমুনায়, পর্ব ২১) আর ব্যবহারযোগ্যতা দেখে বাছুন।

মূল উপলব্ধি: AIC অনুমান-ক্ষমতা, BIC model চেনা, held-out likelihood সরাসরি সাধারণীকরণ মাপে; model-এর আকৃতি ভুল হলে কোনোটিই “দলের আসল সংখ্যা” বলে না। শেষ সিদ্ধান্তে স্থিতিশীলতা আর ব্যবহারের প্রয়োজনও ঢোকে।

প্র ০৫বাজারঘর GMM-এর responsibility দিয়ে ক্রেতাদের segment করে অফার পাঠাতে চায়। নরম segment প্রোডাকশনে নেওয়ার আগে কী কী ভাবা দরকার?

প্রথম প্রশ্ন: responsibility কতটা বিশ্বাসযোগ্য? rik হলো model-এর ভেতরের posterior. Model ঠিক হলে এটি সত্যিকারের অনিশ্চয়তা মাপে, কিন্তু Gaussian অনুমান ভাঙলে এটি অতি-আত্মবিশ্বাসী হতে পারে। Gaussian-এর লেজ দ্রুত পাতলা হয়, তাই দূরের একটি বিন্দু প্রায় ১.০ responsibility পায় সবচেয়ে কাছের দলে, যদিও সেটি কোনো দলেরই নয়। অংশ ৮-এর ব্লকে (২৪, ৫) ক্রেতা পাইকারি দলে ১.০ পেয়েছে। এমন বিন্দু ধরতে responsibility-র সঙ্গে মোট density p(x)-ও দেখা দরকার, যা score_samples দেয়। খুব কম p(x) মানে “কোনো দলেই ভালোভাবে পড়ে না”।

দ্বিতীয়: scale আর feature. টাকা, গণনা আর অনুপাত মিশিয়ে scale না করলে বড় এককের feature covariance-এ প্রাধান্য পায়, আর reg_covar-এর অর্থ বদলে যায়।

তৃতীয়: স্থিতিশীলতা আর সময়। প্রতি মাসে নতুন করে fit করলে label switching-এর কারণে “segment ২” আরেকটি দল হয়ে যেতে পারে। তাই আগের মাসের গড় দিয়ে নতুন fit শুরু করা (means_init) বা গড় মিলিয়ে নাম স্থির রাখা দরকার। উৎসবের মৌসুমে ক্রেতাদের আচরণ বদলায়, তাই segment-এর আকার আর গড় নজরে রাখতে হয়। এ বিষয়ে পর্ব ৫৯ (Distribution shift) আর পর্ব ৬৯ (ML system-এর test আর নজরদারি) বিস্তারিত বলবে।

চতুর্থ: ব্যবহার আর ন্যায্যতা। নরম উত্তর ব্যবসায় কাজে লাগে। মাঝের ক্রেতাকে দুই ধরনের অফার মিশিয়ে দেওয়া যায়, বা অনিশ্চিত ক্রেতাদের জন্য A/B test চালানো যায় (পর্ব ৬২)। কিন্তু segment যদি পরোক্ষভাবে এলাকা বা আয়ের সঙ্গে মিলে যায়, আর তার ভিত্তিতে দাম বা সুবিধা আলাদা হয়, তাহলে কিছু ক্রেতা অন্যায্য আচরণ পেতে পারেন। segment-এর প্রোফাইল তাই মানুষ দিয়ে পর্যালোচনা করা দরকার। আর segment-এর নাম (“কম মূল্যবান ক্রেতা”) যেন অপমানজনক না হয়।

মূল উপলব্ধি: Responsibility একটি model-নির্ভর সম্ভাবনা। প্রোডাকশনে তাকে মোট density, scaling, স্থির নাম, নিয়মিত নজরদারি আর মানুষের পর্যালোচনার সঙ্গে ব্যবহার করতে হয়।

অনুশীলন

হিসাবগুলো খাতায় করুন, তারপর উত্তর মেলান। একটির জন্য কোড চালানো ঐচ্ছিক।

  1. হাতের হিসাব: (ক) এক মাত্রায় তিনটি বিন্দু x = (0, 2, 6), দুটি component: μ = (0, 6), σ = (2, 2), π = (0.5, 0.5). একটি E-step আর M-step করে নতুন π, μ বের করুন। (খ) n = ২০০ সারিতে K = ১-এর ℓ = −৫২০ (p = ২) আর K = ২-এর ℓ = −৪৯৫ (p = ৫)। BIC আর AIC দিয়ে কোনটি বাছবেন?

    (ক) σ আর π সমান বলে ri1 = ১/(১ + exp(−[(x − ৬)² − x²]/৮)). x = ০: পার্থক্য ৩৬/৮ = ৪.৫, r1 = ১/(১ + e−৪.৫) = ০.৯৮৯০. x = ২: (১৬ − ৪)/৮ = ১.৫, r1 = ০.৮১৭৬. x = ৬: −৪.৫, r1 = ০.০১১০. তাই N1 = ০.৯৮৯০ + ০.৮১৭৬ + ০.০১১০ = ১.৮১৭৬, N2 = ১.১৮২৪; π = (০.৬০৫৯, ০.৩৯৪১). μ1 = (০ + ২ × ০.৮১৭৬ + ৬ × ০.০১১০)/১.৮১৭৬ = ১.৭০১২/১.৮১৭৬ = ০.৯৩৫৯; μ2 = (২ × ০.১৮২৪ + ৬ × ০.৯৮৯০)/১.১৮২৪ = ৬.২৯৮৮/১.১৮২৪ = ৫.৩২৭১. (খ) ln ২০০ = ৫.২৯৮৩. BIC: K = ১-এ ১,০৪০ + ২ × ৫.২৯৮৩ = ১,০৫০.৬০; K = ২-এ ৯৯০ + ৫ × ৫.২৯৮৩ = ১,০১৬.৪৯. কম বলে K = ২. AIC: ১,০৪৪ বনাম ১,০০০, আবার K = ২.

  2. সমালোচনা ও নকশা: একজন বিশ্লেষক বাজারঘরের ৫০০ ক্রেতার ৩০টি feature (টাকায় খরচ, অর্ডার সংখ্যা, শতাংশে ছাড় ইত্যাদি) নিয়ে scale না করেই GaussianMixture(n_components=10, covariance_type="full") চালালেন। K = ২ থেকে ১০-এর মধ্যে যেটির training log-likelihood সবচেয়ে বেশি, সেটি বাছলেন, আর predict-এর নম্বর দিয়ে প্রতি মাসে segment রিপোর্ট পাঠাতে শুরু করলেন। কোথায় কোথায় সমস্যা, আর কীভাবে নতুন করে সাজাবেন?

    সমস্যা: (১) K বাছাইয়ের মাপ ভুল: training ℓ K-এর সঙ্গে প্রায় সবসময় বাড়ে। (২) Parameter অনেক বেশি: ৩০ মাত্রায় প্রতিটি full covariance ৪৬৫টি সংখ্যা চায়, ১০টিতে ৪,৬৫০, অথচ সারি মাত্র ৫০০. অনেক component-এর Σ প্রায় singular হবে। (৩) Scale নেই: টাকার feature হাজারের ঘরে, শতাংশ এককের ঘরে, তাই covariance আর reg_covar-এর অর্থ feature-ভেদে আলাদা। (৪) Restart নেই, ডিফল্ট n_init = ১. (৫) Label switching: প্রতি মাসে নতুন fit-এ নম্বরের মানে বদলাতে পারে, তাই মাসে মাসে তুলনা অর্থহীন। (৬) নরম তথ্য আর ব্যবসায়িক যাচাই, দুটিই নেই।

    নতুন নকশা: বাঁকা খরচে log, তারপর StandardScaler; দরকার হলে আগে PCA দিয়ে কয়েকটি মাত্রায় নামা। Covariance type আর K = ১ থেকে ৮ BIC/AIC দিয়ে তুলনা, প্রতিটিতে n_init ৫–১০; সঙ্গে held-out log-likelihood. ছোট ওজন বা অস্বাভাবিক ছোট variance-এর component খোঁজা। নাম স্থির রাখতে আগের মাসের গড় দিয়ে শুরু, বা গড় মিলিয়ে নম্বর দেওয়া। রিপোর্টে responsibility আর p(x) রাখা, আর segment-এর প্রোফাইল ব্যবসা দলের সঙ্গে যাচাই।

  3. আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন নিচের কোড কী ছাপবে। (w হলো তিনটি বিন্দু আর দুটি component-এর πkN(xi | k).)
    import numpy as np
    w = np.array([[0.30, 0.10],
                  [0.02, 0.18],
                  [0.05, 0.05]])
    r = w / w.sum(axis=1, keepdims=True)
    print(r.round(2))
    Nk = r.sum(axis=0)
    print(Nk, Nk / 3)
    x = np.array([2.0, 8.0, 5.0])
    print(((r * x[:, None]).sum(axis=0) / Nk).round(3))
    [[0.75 0.25]
     [0.1  0.9 ]
     [0.5  0.5 ]]
    [1.35 1.65] [0.45 0.55]
    [3.556 6.182]

    প্রতিটি সারি নিজের যোগফল দিয়ে ভাগ: ০.৩০/০.৪০ = ০.৭৫, ০.০২/০.২০ = ০.১, আর তৃতীয় সারি সমান বলে ০.৫-০.৫. N = (০.৭৫ + ০.১ + ০.৫, ০.২৫ + ০.৯ + ০.৫) = (১.৩৫, ১.৬৫), যোগফল ৩ = সারির সংখ্যা, আর π = N/৩ = (০.৪৫, ০.৫৫). গড়: দল ১-এ (০.৭৫·২ + ০.১·৮ + ০.৫·৫)/১.৩৫ = ৪.৮/১.৩৫ = ৩.৫৫৬, দল ২-এ (০.২৫·২ + ০.৯·৮ + ০.৫·৫)/১.৬৫ = ১০.২/১.৬৫ = ৬.১৮২. ছাপা ফরম্যাট একটি ডেস্কটপ run-এ যাচাই করা।

  4. Parameter গোনা আর বাছাই: d = ১০ feature, K = ৪ component, n = ৩০০ সারি। (ক) full, tied, diag আর spherical-এ মোট parameter কত (গড়, covariance আর ওজন মিলিয়ে)? (খ) BIC-এর শাস্তি full আর diag-এর মধ্যে কত আলাদা? (গ) কোনটি দিয়ে শুরু করবেন, আর কেন?

    (ক) গড় Kd = ৪০, ওজন K − ১ = ৩, এই ৪৩ সবার জন্য একই। Covariance: full-এ K·d(d + ১)/২ = ৪ × ৫৫ = ২২০, মোট ২৬৩; tied-এ ৫৫, মোট ৯৮; diag-এ K·d = ৪০, মোট ৮৩; spherical-এ K = ৪, মোট ৪৭. (খ) ln ৩০০ = ৫.৭০৩৮. পার্থক্য (২৬৩ − ৮৩) × ৫.৭০৩৮ = ১৮০ × ৫.৭০৩৮ ≈ ১,০২৬.৭ পয়েন্ট। অর্থাৎ full-কে diag-এর চেয়ে ℓ-এ প্রায় ৫১৩ বেশি পেতে হবে, তবেই BIC-এ জিতবে। (গ) প্রতিটি component গড়ে ৭৫টি সারিতে ৫৫টি covariance-সংখ্যা অনুমান করবে, যা অস্থির। শুরু diag বা tied দিয়ে (scale করার পর), শক্ত correlation থাকলে tied বা আগে PCA; তারপর BIC আর held-out likelihood-এ full-এর সঙ্গে তুলনা।

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

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

ক্রমএকটি Gaussian mixture fit করে K বাছাইয়ের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Feature scale করা আর শুরুর মান বাছাই (কয়েকটি restart-এর জন্য)
  2. E-step: বর্তমান parameter দিয়ে প্রতিটি বিন্দুর responsibility
  3. M-step: responsibility-র ওজনে π, μ আর Σ হালনাগাদ
  4. log-likelihood-এর পরিবর্তন tol-এর নিচে নামলে থামা, নইলে আবার E-step
  5. সব restart-এর মধ্যে সবচেয়ে বড় log-likelihood-এর fit রাখা
  6. বিভিন্ন K আর covariance type-এর BIC তুলনা করে model বাছাই

দুই component-এর একটি GMM-এ একটি বিন্দুর জন্য π1N1(x) = 0.06 আর π2N2(x) = 0.02. এই বিন্দুর component 1-এর responsibility কত?

  • 0.06
  • 0.25
  • 0.75
  • 3

Gaussian mixture-এর log-likelihood-এ gradient শূন্য করে সরাসরি বন্ধ সূত্র পাওয়া যায় না। মূল কারণ কোনটি?

  • Covariance matrix-এর inverse নিতে হয়
  • log বসে আছে component-গুলোর যোগফলের বাইরে, তাই ওজনগুলো নিজেই parameter-নির্ভর
  • ডেটা অনেক বেশি
  • mixing weight-গুলোর যোগফল 1 হতে হয়

EM algorithm সম্পর্কে কোন বক্তব্যটি সঠিক?

  • EM সবসময় global maximum খুঁজে পায়
  • EM-এ একটি learning rate বাছতে হয়
  • কিছু পাকে log-likelihood কমা স্বাভাবিক
  • প্রতি পাকে log-likelihood কমে না, তবে local maximum-এ থামতে পারে

n = 100 সারিতে একটি model-এর সর্বোচ্চ log-likelihood ℓ = −100 আর parameter p = 5. BIC কত (ln 100 ≈ 4.605)?

  • প্রায় 223.0
  • 210
  • প্রায় −177.0
  • প্রায় −77.0

কোন শর্তে Gaussian mixture-এর EM ঠিক k-means-এর মতো আচরণ করে?

  • সব component-এর full covariance আর বড় ε
  • সবার covariance একই εI, সমান ওজন, আর ε → 0
  • সবার covariance εI আর ε → ∞
  • diag covariance আর আলাদা ওজন

একটি ডেটায় ১২টি হুবহু এক মান (যেমন 999.0) আছে। reg_covar = 0 দিয়ে GMM চালালে একটি component সেগুলোর ওপর বসে পড়ার সম্ভাবনা আছে। তখন কী ঘটে?

  • ওই component-এর density শূন্যে নামে
  • EM এগুলো আপনা-আপনি উপেক্ষা করে
  • Variance শূন্যের দিকে যায়, log-likelihood সীমাহীনভাবে বাড়ে
  • BIC এই component-কে আপনা-আপনি বাদ দেয়

d = 3 feature আর K = 2 component-এর full-covariance GMM-এ মোট কয়টি parameter (গড়, covariance আর ওজন)?

  • 12
  • 20
  • 25
  • 19

শব্দকোষ

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

মনে রাখুন

  1. p(x) = ∑ πkN(x; μk, Σk); লুকানো z দল বাছে, responsibility rik = P(zi = k | xi) হলো নরম ভাগ। Σk = εI, ε → ০ হলে k-means.
  2. যোগফলের log বন্ধ সূত্র আটকায়; z জানা থাকলে সমস্যা সহজ, তাই EM অজানা z-এর জায়গায় তার প্রত্যাশা বসায়।
  3. E-step: rik. M-step: Nk = ∑ rik, πk = Nk/n, ওজন দেওয়া μk আর Σk. ELBO-র যুক্তিতে ℓ কখনো কমে না, তবে local maximum-এ থামতে পারে; বাস্তব কোডে log-sum-exp.
  4. Covariance type (full, tied, diag, spherical) parameter কমায়; K বাছুন BIC = −2ℓ + p ln n বা AIC দিয়ে, প্রতিটিতে যথেষ্ট n_init দিয়ে।
  5. Component একটি বিন্দুতে বসলে ℓ → ∞; reg_covar (ডেটার এককে), scaling, ছোট ওজনের খোঁজ আর prior দিয়ে ঠেকান; responsibility-র সঙ্গে মোট density-ও দেখুন।

পরের পর্বে: GMM একটি parametric density estimate. পর্ব ৪০ (Density estimation — histogram, KDE আর mixture model) দেখাবে histogram-এর bin-প্রস্থের সমস্যা, kernel density estimation নিজে লেখা, bandwidth বাছাই, আর বাজারঘরের ডেলিভারি সময়ের মতো ধনাত্মক ডেটায় সীমানার সমস্যা।

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

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