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

Gaussian process — kernel দিয়ে অনিশ্চয়তাসহ অনুমান

Gaussian Processes — Kernel-based Prediction with Uncertainty, Marginal Likelihood and Kernel Composition

Taught in Bangla, this lesson treats a Gaussian process as a prior over functions, draws samples with Cholesky and jitter, and derives the posterior mean and variance from the conditional Gaussian. In-browser runs match a hand-written GP with GaussianProcessRegressor and KernelRidge, learn hyperparameters by log marginal likelihood, and forecast weekly orders with composed kernels.

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

  • Gaussian process-কে function-এর ওপর বণ্টন হিসেবে বোঝা, আর Cholesky ও jitter দিয়ে prior থেকে function টানা
  • Conditional Gaussian সূত্র থেকে posterior-এর গড় আর variance বের করা, দুটি বিন্দুতে হাতে আর SciPy দিয়ে
  • নিজের GP-কে GaussianProcessRegressor আর KernelRidge-এর সঙ্গে মেলানো, আর তফাতটা বলা
  • Log marginal likelihood-এর দুই ভাগ ব্যাখ্যা করা আর তা দিয়ে kernel-এর hyperparameter শেখা
  • Kernel জোড়া দিয়ে সাপ্তাহিক ছাঁদ ধরা, extrapolation-এর আচরণ পড়া, আর O(n³)-এর সীমা ও তার প্রতিকার জানা

পর্ব ৩০-এ kernel ridge regression-এর অনুমান k(x*, X)(K + λI)−1y দেখেছেন, আর সেখানে বলা হয়েছিল এটি একটি Gaussian process-এর গড়ও। এই পর্বে সেই কথাটি খুলে দেখা হবে। লাভ একটাই, কিন্তু বড়: প্রতিটি অনুমানের সঙ্গে একটি সৎ অনিশ্চয়তা, যা ডেটার কাছে কম আর ডেটা থেকে দূরে বেশি। ভিত্তি তিনটি: পর্ব ১৯-এর conditional Gaussian, পর্ব ২৯-এর kernel, আর পর্ব ১০-এর Cholesky. Bayesian ভাবনার মূল কথা (prior, likelihood, posterior) AI-এর ভিত্তি কোর্সের পর্ব ৫০-এ আছে। পর্ব ৪৫ (Bayesian linear regression — posterior আর predictive অনিশ্চয়তা) weight-এর ওপর prior রাখে; এখানে prior বসবে সরাসরি function-এর ওপর। চারটি চালানো যায় এমন ব্লক পাতাতেই চলে (ব্রাউজারের Python, NumPy, SciPy, scikit-learn আর matplotlib); প্রথমবার scikit-learn নামতে প্রায় দশ সেকেন্ড লাগে।

১ · Function-এর ওপর বণ্টন — Gaussian process কী

আবহাওয়া অফিস বলে “কাল ঢাকায় সর্বোচ্চ ৩২ ডিগ্রি, ±২”. এক দিনের জন্য একটি সংখ্যা আর তার অনিশ্চয়তা। এখন পুরো সপ্তাহের পূর্বাভাস ভাবুন: সোমবার গরম হলে মঙ্গলবারও সম্ভবত গরম, কারণ পাশাপাশি দিনের তাপমাত্রা একসঙ্গে ওঠানামা করে। দূরের দুটি দিনের মধ্যে সেই টান দুর্বল। Gaussian process ঠিক এই ধারণা: অনেকগুলো বিন্দুর মান একসঙ্গে অনিশ্চিত, আর কাছের বিন্দুগুলো একে অন্যের সঙ্গে বাঁধা।

পর্ব ১৯-এ multivariate Gaussian দেখেছেন: কয়েকটি সংখ্যার যৌথ বণ্টন, যা একটি গড় vector আর একটি covariance matrix দিয়ে পুরো বর্ণনা করা যায়। এখন সংখ্যাগুলো হোক একটি অজানা function f-এর মান: f(x1), f(x2), …, f(xn). একটি Gaussian process হলো এমন একটি নিয়ম, যা যেকোনো সসীম সংখ্যক x-এর জন্য এই মানগুলোর একটি multivariate Gaussian দেয়। লেখা হয়:

f ~ GP(m(x), k(x, x′))

এখানে m(x) হলো গড় function (প্রায়ই ০ বা ডেটার গড়), আর k(x, x′) হলো covariance function, অর্থাৎ kernel: Cov(f(x), f(x′)) = k(x, x′). n টি বিন্দু বেছে নিলে তাদের covariance matrix K-এর (i, j) ঘর k(xi, xj), যা পর্ব ২৯-এর Gram matrix. Kernel বৈধ (positive semidefinite) বলে যেকোনো বাছাইয়ে K একটি বৈধ covariance. তাই GP-কে ভাবা যায় “অসীম মাত্রার Gaussian” হিসেবে, যার প্রতিটি সসীম টুকরো একটি সাধারণ Gaussian.

এই পর্বের প্রধান kernel হলো RBF kernel (squared exponential নামেও পরিচিত):

k(x, x′) = σf² · exp(−(x − x′)² / (২ℓ²))

দুটি সংখ্যা এর আচরণ ঠিক করে। σf² (signal variance) বলে f গড় থেকে সাধারণত কতটা সরে: প্রতিটি বিন্দুতে f-এর SD σf. আর length-scale ℓ বলে কত দূর পর্যন্ত দুটি বিন্দুর মান একসঙ্গে চলে। x আর x′-এর দূরত্ব ℓ হলে correlation e−০.৫ ≈ ০.৬১, দূরত্ব ২ℓ হলে ০.১৪, ৩ℓ হলে ০.০১১. পর্ব ৩০-এর γ আর এই ℓ একই জিনিস, শুধু ভিন্ন রূপে: γ = ১/(২ℓ²). RBF একটি stationary kernel: মান নির্ভর করে শুধু x − x′-এর ওপর, x কোথায় তার ওপর নয়।

মূল ধারণা

Linear regression-এ prior বসে কয়েকটি weight-এর ওপর (পর্ব ৪৫)। GP-তে prior বসে সরাসরি function-এর ওপর: “কেমন function সম্ভব”, তা kernel দিয়ে বলা হয়। কোনো সূত্রের আকার (সরলরেখা, বহুপদী) আগে থেকে বাছতে হয় না; শুধু মসৃণতা, মাপ আর ছাঁদ (যেমন সাপ্তাহিক পুনরাবৃত্তি) বলতে হয়। এই কারণে GP-কে non-parametric বলা হয়: ডেটা বাড়লে model-এর কার্যকর জটিলতাও বাড়ে।

২ · Prior থেকে function টানা — length-scale, Cholesky আর jitter

একটি prior বুঝতে সবচেয়ে ভালো উপায় তা থেকে কয়েকটি নমুনা টেনে দেখা। ধাপগুলো সরল। ০ থেকে ১০ km-এর মধ্যে ২০০টি x-এর একটি grid নিন। তাদের ২০০ × ২০০ covariance matrix K বানান। তারপর পর্ব ১০-এর কৌশল: K = LLᵀ (Cholesky), আর z যদি ২০০টি স্বাধীন standard normal সংখ্যা হয়, তাহলে f = Lz-এর covariance LLᵀ = K. প্রতিটি z থেকে একটি পুরো function পাওয়া যায়।

একটি সমস্যা আছে। ঘন grid-এ পাশাপাশি বিন্দুর মান প্রায় এক, তাই K-এর অনেক eigenvalue শূন্যের অতি কাছে, আর floating-point-এর গোলমালে কিছু সামান্য ঋণাত্মকও হয়ে যায়। তখন Cholesky ব্যর্থ হয়। প্রতিকার jitter: কর্ণে খুব ছোট একটি সংখ্যা যোগ করা, যেমন ১০−১০. এটি function-কে প্রায় বদলায় না, কিন্তু matrix-কে সংখ্যাগতভাবে positive definite রাখে। নিচের ব্লক তিনটি length-scale-এ একই z দিয়ে চারটি করে function টানে, প্রতিটি K-এর ক্ষুদ্রতম eigenvalue ছাপায়, আর দেখে সবচেয়ে ছোট কোন jitter-এ Cholesky চলে। এখানে scipy.linalg.cholesky নেওয়া হয়েছে, কারণ ব্রাউজারের NumPy-তে np.linalg.cholesky ব্যর্থ হলে error না দিয়ে চুপচাপ NaN ফেরায়। ছবির লেখা ইংরেজিতে, কারণ পাতার matplotlib বাংলা অক্ষর ঠিকমতো আঁকে না।

import numpy as np
import matplotlib.pyplot as plt
from scipy.linalg import cholesky, eigvalsh, LinAlgError

def rbf(a, b, ell, sf=1.0):          # sf^2 exp(-(a-b)^2 / (2 ell^2))
    return sf**2 * np.exp(-(a[:, None] - b[None, :]) ** 2 / (2 * ell**2))

x = np.linspace(0, 10, 200)          # 200টি বিন্দুর grid (km)
rng = np.random.default_rng(46)
z = rng.standard_normal((200, 4))    # একই z, তিনটি length-scale-এ
fig, ax = plt.subplots(1, 3, figsize=(10, 3), sharey=True)
fig.subplots_adjust(bottom=0.18, wspace=0.08)
print("ell  corr(1 km)  crossings  min eig     jitter")
for i, ell in enumerate([0.3, 1.0, 3.0]):
    K = rbf(x, x, ell)
    for jit in [0.0, 1e-10, 1e-8, 1e-6]:   # সবচেয়ে ছোট কোনটিতে চলে
        try:
            L = cholesky(K + jit * np.eye(200), lower=True)
            break
        except LinAlgError:
            continue
    f = L @ z                        # f ~ N(0, K): চারটি function
    cross = np.mean(np.sum(np.diff(np.sign(f), axis=0) != 0, axis=0))
    print(f"{ell:3.1f} {np.exp(-1 / (2 * ell**2)):10.4f} {cross:9.1f}",
          f"{eigvalsh(K)[0]:10.1e} {jit:8.0e}")
    ax[i].plot(x, f, lw=1.2)
    ax[i].set_title(f"length-scale = {ell}")
    ax[i].set_xlabel("x (km)")
ax[0].set_ylabel("f(x) sampled from the prior")
plt.show()

আসল আউটপুট (ব্রাউজারের Python; ছবিটি ব্লকের নিচে আঁকা হয়):

ell  corr(1 km)  crossings  min eig     jitter
0.3     0.0039      11.2   -4.4e-15    1e-10
1.0     0.6065       3.8   -1.7e-14    1e-10
3.0     0.9460       1.0   -6.6e-14    1e-10

Correlation: ১ km দূরের দুটি বিন্দুর correlation exp(−১/(২ℓ²)): ℓ = ০.৩-এ মাত্র ০.০০৩৯, ℓ = ১-এ ০.৬০৬৫, ℓ = ৩-এ ০.৯৪৬০. আঁকাবাঁকা: প্রতিটি function ১০ km-এ গড়ে ১১.২, ৩.৮ আর ১.০ বার শূন্য পার হয়েছে। RBF prior-এ প্রত্যাশিত হার প্রতি একক দৈর্ঘ্যে ১/(πℓ) (Rice-এর সূত্র), অর্থাৎ ১০ km-এ ১০.৬, ৩.২ আর ১.১; মাত্র চারটি নমুনার গড়ও তার কাছাকাছি। ℓ তিন গুণ হলে ঢেউ প্রায় তিন ভাগের এক ভাগ। Eigenvalue: গণিতে K positive semidefinite, তাই কোনো eigenvalue ঋণাত্মক হওয়ার কথা নয়; কিন্তু floating-point-এ ক্ষুদ্রতমটি −৪.৪ × ১০−১৫ থেকে −৬.৬ × ১০−১৪ (এই মানগুলো যন্ত্রভেদে একটু আলাদা হয়)। তাই jitter ০ দিয়ে তিনটিতেই Cholesky ব্যর্থ, আর ১০−১০ যথেষ্ট। ছবিতে একই চারটি z তিনটি panel-এ: বাঁয়ে ঘন ঢেউ, ডানে প্রায় সরল বক্ররেখা। পার্থক্য শুধু kernel-এ।

৩ · Posterior — conditional Gaussian থেকে GP regression

এবার ডেটা আসে। ধরুন n টি অর্ডারের দূরত্ব X আর ডেলিভারির সময় y জানা, আর প্রতিটি মাপে স্বাধীন Gaussian noise আছে, যার variance σn²: yi = f(xi) + εi. নতুন একটি দূরত্ব x*-এ f* = f(x*) জানতে চাই। GP-র সংজ্ঞা বলে y আর f* একসঙ্গে Gaussian (গড় ০ ধরে):

[y, f*] ~ N(০, [[K + σn²I, k*], [k*ᵀ, k**]])

এখানে K হলো training বিন্দুগুলোর n × n kernel matrix, k* হলো x*-এর সঙ্গে প্রতিটি training বিন্দুর kernel মানের vector, আর k** = k(x*, x*). পর্ব ১৯-এর conditional distribution-এর সূত্র (μa + ΣabΣbb−1(xb − μb), আর Σaa − ΣabΣbb−1Σba) সরাসরি বসালে কোনো নতুন optimization ছাড়াই posterior পাওয়া যায়:

গড় μ* = k*ᵀ(K + σn²I)−1y
variance σ*² = k** − k*ᵀ(K + σn²I)−1k*

দুটি সূত্র পড়তে শিখুন। গড়টি training-এর y-গুলোর একটি ওজনদার যোগফল, আর ওজন আসে kernel থেকে: কাছের অর্ডারের কথা বেশি শোনা হয়। Variance-এ k** হলো prior-এর অনিশ্চয়তা, আর বিয়োগের অংশটি হলো “ডেটা দেখে যতটা অনিশ্চয়তা কমল”। x* সব training বিন্দু থেকে দূরে হলে k* ≈ ০, তাই গড় ≈ ০ (prior-এর গড়) আর variance ≈ k** (prior-এর variance-এর সমান)। আর একটি খুঁটিনাটি: σ*² হলো f*-এর variance; একটি নতুন পর্যবেক্ষণ y*-এর variance তার সঙ্গে σn² যোগ করে। প্রথমটিকে বলে epistemic uncertainty (model-এর না-জানা), দ্বিতীয়টিকে aleatoric uncertainty (ডেটার নিজস্ব এলোমেলোতা)। নিচের stepper দুটি training বিন্দুতে পুরো হিসাব হাতে করে।

x = 0, 1; y = 3, 1; σn² = 0.1; prior গড় 0k(a, b) = 2^−(a − b)²

            
  1. Kernel আর K. σf = ১ আর ℓ ≈ ০.৮৪৯ নিলে RBF হয় ২-এর ঘাত, তাই হাতে হিসাব সহজ। দুটি বিন্দু ১ দূরে, তাদের covariance ০.৫.
  2. Noise যোগ। কর্ণে σn² = ০.১ যোগ করে K + σn²I. Determinant ০.৯৬, আর ২ × ২ matrix-এর inverse সরাসরি লেখা যায়।
  3. v = (K + σn²I)−1y. ফল [২.৯১৬৭, −০.৪১৬৭]. এটি ঠিক kernel ridge-এর dual coefficient α, যখন λ = σn².
  4. গড়। x* = ০.৫ দুটি বিন্দু থেকে সমান দূরে, তাই k*-এর দুটি মান সমান, ০.৮৪০৯. গড় ২.১০২২: ৩ আর ১-এর মাঝামাঝি, কিন্তু প্রথম বিন্দুর দিকে একটু বেশি টানা, কারণ v-তে প্রথম বিন্দুর ওজন বড়।
  5. Variance. Prior-এর variance ১ থেকে ০.৮৮৩৯ বাদ যায়, থাকে ০.১১৬১, SD ০.৩৪০৮. ৯৫% পরিসর ২.১০ ± ০.৬৭. নতুন একটি পর্যবেক্ষণের variance-এ noise যোগ হয়: ০.২১৬১.
  6. দূরের বিন্দু। x* = ৩-এ kernel মান প্রায় শূন্য। গড় −০.০২০৩ (প্রায় prior-এর ০), variance ০.৯৯৫৬ (প্রায় prior-এর ১)। ডেটা যেখানে কিছু বলে না, GP সেখানে prior-এ ফিরে যায়।

পুরো ছবিটা এমন দাঁড়ায়: ডেটার কাছে পরিসর সরু, দুটি বিন্দুর মাঝের বড় ফাঁকে চওড়া, আর ডেটার বাইরে prior-এর পরিসরে মিলিয়ে যায়। নিচের ছবিটি পাঁচটি বিন্দুর একটি সত্যিকারের GP posterior থেকে আঁকা।

GP posterior: গড় আর অনিশ্চয়তার পরিসর

কালো বিন্দু ডেটা; নীল রেখা posterior গড়; নীল ছায়া পরিসর; ধূসর দাগ prior-এর গড়

Gaussian process posterior mean and uncertainty band ডেটার কাছে: সরু ফাঁকে: চওড়া দূরে: prior গড় m x (km) → posterior গড় μ*(x) μ* ± ১.৯৬σ* (৯৫% পরিসর) prior-এর গড়, ডেটা থেকে দূরে ফেরা
ছবি ৪৬.১ — একই kernel, একই noise; পরিসরের চওড়া হওয়া শুধু ডেটা থেকে দূরত্বের ফল। লক্ষ করুন, ছায়ার প্রস্থ y-এর মানের ওপর নির্ভর করে না, শুধু x-গুলো কোথায় তার ওপর।

শেষ বাক্যটি একটি গুরুত্বপূর্ণ সীমা। σ*²-এর সূত্রে y নেই। অর্থাৎ hyperparameter স্থির থাকলে GP-র অনিশ্চয়তা শুধু “এখানে ডেটা কত ঘন” বলে, “এখানে ডেটা কত এলোমেলো” নয়। দামি এলাকায় ডেলিভারির সময় বেশি ওঠানামা করলেও সাধারণ GP সব জায়গায় এক σn ধরে। এখন length-scale-এর প্রভাব দেখুন: দুটি বিন্দু x = ০ আর ২, দুটিরই y = ১, σf = ১, σn² = ০.০১; প্রশ্ন মাঝের x = ১-এ আর দূরের x = ৫-এ অনুমান কী।

x = 0, 2; y = 1, 1; σf = 1; σn² = 0.01; অনুমান: গড় (SD)
ℓ
corr, দূরত্ব 0.5 / 1 / 2
x* = 1 (মাঝে)
x* = 5 (দূরে)
  1. ℓ = ০.৩. ১ km দূরে correlation মাত্র ০.০০৩৯. মাঝের x = ১ দুটি বিন্দুর কোনোটির কথা শোনে না: গড় প্রায় ০ (prior), SD প্রায় ১. Function-টি দুটি বিন্দুতে ১ ছুঁয়ে মাঝে prior-এ নেমে যায়।
  2. ℓ = ১. ১ km-এ correlation ০.৬১. মাঝের গড় ১.০৫৯ (দুটি বিন্দুর টান যোগ হয়ে ১-এর সামান্য ওপরে), SD ০.৫৯৮. দূরের x = ৫ তখনও prior-এ।
  3. ℓ = ৩. ২ km দূরেও correlation ০.৮০. মাঝে গড় ১.০৪৫, SD মাত্র ০.১০৮: model প্রায় নিশ্চিত যে function মসৃণভাবে দুটি বিন্দু জুড়েছে। এমনকি x = ৫-এ গড় ০.৪৭৩, SD ০.৭০২.
  4. তুলনা। ℓ বড় হলে প্রতিটি বিন্দুর প্রভাব দূরে ছড়ায়, পরিসর সরু হয়, আর extrapolation-এ ডেটার প্রভাব বেশি দূর যায়। ঠিক ℓ কোনটি, তা ডেটা থেকে শেখার উপায় অংশ ৫-এ।

৪ · নিজে বানানো GP — Cholesky, scikit-learn আর kernel ridge

বাস্তবে (K + σn²I)−1 কখনো সরাসরি বানানো হয় না। Rasmussen আর Williams-এর বই Gaussian Processes for Machine Learning (২০০৬)-এর Algorithm 2.1 যে পথ দেখায়, তা এই: একবার Cholesky, K + σn²I = LLᵀ (প্রায় n³/৬ গুণ, আর সমান সংখ্যক যোগ); তারপর দুটি ত্রিভুজাকার সমাধানে α = (K + σn²I)−1y (প্রতিটি প্রায় n² কাজ)। Variance-এর জন্য প্রতিটি test বিন্দুতে আরও একটি ত্রিভুজাকার সমাধান। SciPy-তে cho_factor আর cho_solve ঠিক এই কাজ করে। scikit-learn-এর GaussianProcessRegressor-ও ভেতরে একই algorithm চালায় (তার source code-এ এই Algorithm 2.1-এর উল্লেখ আছে)।

নিচের module দুই ধরনের কাল্পনিক বাজারঘর ডেটা seed দিয়ে বানায়: hub থেকে দূরত্ব বনাম ডেলিভারির মিনিট, আর দিনে অর্ডারের সংখ্যা। আসল ধারা model জানে না।

# বাজারঘর (কাল্পনিক দোকান): দুই ধরনের seeded ডেটা
import numpy as np

def eta_curve(km):                       # আসল ধারা (model জানে না)
    return 25 + 18 * np.log1p(km) + 4 * np.sin(km / 1.3)

def deliveries(n=7, seed=46):            # hub থেকে km -> ডেলিভারির মিনিট
    rng = np.random.default_rng(seed)
    km = np.sort(rng.uniform(0.3, 7, n)).round(2)
    mins = eta_curve(km) + rng.normal(0, 2, n)
    return km.reshape(-1, 1), mins.round(1)

def daily_orders(days=70, seed=46):      # দিনে অর্ডারের সংখ্যা, সাপ্তাহিক ছাঁদ
    rng = np.random.default_rng(seed)
    t = np.arange(days)
    week = np.array([0, 4, 6, 3, 5, 10, -22])   # দিন 0 = শনিবার; বৃহস্পতি বেশি, শুক্র কম
    slow = 10 * np.sin(2 * np.pi * t / 45)        # ধীর ওঠানামা
    y = 140 + slow + week[t % 7] + rng.normal(0, 4, days)
    return t.reshape(-1, 1).astype(float), y.round()

দ্বিতীয় ব্লক মাত্র ৭টি অর্ডারে GP চালায়, hyperparameter হাতে বেঁধে: σf = ১০ মিনিট, ℓ = ২ km, σn = ২ মিনিট। Prior-এর গড় হিসেবে training-এর y-এর গড় নেওয়া হয়েছে (y থেকে বাদ দিয়ে, শেষে ফেরত যোগ)। তারপর একই হিসাব GaussianProcessRegressor(optimizer=None)-এ, যাতে scikit-learn hyperparameter না বদলায়; এখানে alpha মানে কর্ণে যোগ করা noise variance σn². আর KernelRidge-এ: তার RBF-এ σf² নেই, তাই λ = σn²/σf² আর γ = ১/(২ℓ²) দিলে একই গড় আসার কথা।

import numpy as np
import matplotlib.pyplot as plt
from scipy.linalg import cho_factor, cho_solve
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import ConstantKernel as C, RBF
from sklearn.kernel_ridge import KernelRidge
from bazar_gp import deliveries

X, y = deliveries()                  # 7টি অর্ডার: km -> মিনিট
ym = y.mean(); yc = y - ym           # prior-এর গড় = training গড়
sf, ell, sn = 10.0, 2.0, 2.0         # amplitude, length-scale, noise sd
k = lambda A, B: sf**2 * np.exp(-(A - B.T) ** 2 / (2 * ell**2))
xs = np.linspace(0, 12, 241).reshape(-1, 1)

c = cho_factor(k(X, X) + sn**2 * np.eye(len(y)))   # K + sn^2 I = L L^T
alpha = cho_solve(c, yc)
Ks = k(xs, X)
mean = ym + Ks @ alpha               # posterior গড়
var = sf**2 - np.sum(Ks * cho_solve(c, Ks.T).T, axis=1)
sd = np.sqrt(np.maximum(var, 0))
gp = GaussianProcessRegressor(C(sf**2) * RBF(ell), alpha=sn**2,
                              optimizer=None).fit(X, yc)
m2, s2 = gp.predict(xs, return_std=True)
krr = KernelRidge(alpha=sn**2 / sf**2, kernel="rbf",
                  gamma=1 / (2 * ell**2)).fit(X, yc)
print("max |mean - GP|  =", f"{np.max(np.abs(mean - ym - m2)):.1e}")
print("max |sd - GP|    =", f"{np.max(np.abs(sd - s2)):.1e}")
print("max |mean - KRR| =", f"{np.max(np.abs(mean - ym - krr.predict(xs))):.1e}")
for x0 in [0.85, 3.3, 9.0, 12.0]:
    i = int(round(x0 / 0.05))
    print(f"x = {x0:5.2f} km: mean {mean[i]:5.1f}, 95% +/- {1.96 * sd[i]:4.1f}")
plt.fill_between(xs[:, 0], mean - 1.96*sd, mean + 1.96*sd, alpha=0.25, label="95% band")
plt.plot(xs, mean, label="posterior mean"); plt.plot(X, y, "ko", label="orders")
plt.axhline(ym, ls=":", c="gray", label="prior mean"); plt.legend()
plt.xlabel("distance from hub (km)"); plt.ylabel("delivery time (min)"); plt.show()

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

max |mean - GP|  = 2.3e-14
max |sd - GP|    = 1.4e-14
max |mean - KRR| = 2.1e-14
x =  0.85 km: mean  38.8, 95% +/-  2.7
x =  3.30 km: mean  54.6, 95% +/-  4.2
x =  9.00 km: mean  53.2, 95% +/- 16.6
x = 12.00 km: mean  49.7, 95% +/- 19.6

তিনটি মিল: নিজের গড় আর SD scikit-learn-এর GP-র সঙ্গে ১০−১৪-এর ঘরে মেলে, আর গড় KernelRidge-এর সঙ্গেও (λ = σn²/σf² = ০.০৪): পর্ব ৩০-এর দাবি সংখ্যায় প্রমাণিত। পরিসরের আচরণ: ০.৮৫ km-এ দুটি অর্ডার কাছাকাছি, পরিসর ±২.৭ মিনিট; ২.১৩ আর ৪.৪৭ km-এর মাঝের ফাঁকে ৩.৩ km-এ ±৪.২; শেষ অর্ডার (৬.৫৬ km) পেরিয়ে ৯ km-এ ±১৬.৬; আর ১২ km-এ ±১৯.৬, যা হুবহু prior-এর ১.৯৬ × σf = ১.৯৬ × ১০. সেখানে গড় ৪৯.৭, প্রায় training গড় ৪৯.৪: GP prior-এ ফিরছে। ০.৮৫ km-এর ±২.৭ noise-এর নিজের ±৩.৯-এর (১.৯৬ × ২) চেয়েও কম, কারণ এটি f-এর পরিসর; নতুন একটি অর্ডারের পরিসরে σn² যোগ করতে হবে। ছবিতে ডান দিকের চওড়া ছায়া আর ধূসর prior গড়ের দিকে নেমে আসা নীল রেখা দেখুন: বাস্তবে দূরের অর্ডারে সময় বাড়ারই কথা, কিন্তু RBF kernel সেটা জানে না।

scikit-learn-এর তিনটি ফাঁদ: (১) GaussianProcessRegressor-এর alpha-র default ১০−১০, শুধু jitter; noise না বললে GP প্রতিটি বিন্দু হুবহু ছুঁতে চায়। Noise দিন alpha-তে, বা kernel-এ WhiteKernel যোগ করে শিখতে দিন, কিন্তু দুটি একসঙ্গে না ভেবে দেবেন না। (২) normalize_y-এর default False, অর্থাৎ prior-এর গড় ০; মিনিটে মাপা y-তে ডেটা থেকে দূরে অনুমান শূন্যে নামবে, পর্ব ৩০-এর KernelRidge-এর মতোই। (৩) kernel না দিলে default ConstantKernel(1.0) * RBF(1.0), যার bounds "fixed", তাই fit কিছুই শেখে না; আর নিজে kernel দিলে fit তার hyperparameter বদলায়, তাই optimizer=None না দিলে আপনার দেওয়া মান শুধু শুরুর বিন্দু।

৫ · Hyperparameter শেখা — log marginal likelihood

σf, ℓ আর σn কীভাবে বাছবেন? Cross-validation চলে, কিন্তু GP একটি সম্ভাবনার model বলে আরেকটি স্বাভাবিক পথ আছে: hyperparameter θ দিলে ডেটা y কতটা সম্ভাব্য, সেটি মাপা। f-কে “integrate out” করলে, অর্থাৎ সব সম্ভাব্য function-এর ওপর গড় নিলে, y নিজেই একটি Gaussian: y ~ N(০, Ky), যেখানে Ky = K + σn²I. এর log density হলো log marginal likelihood:

log p(y | X, θ) = −½ yᵀKy−1y − ½ log|Ky| − (n/২) log ২π

প্রথম পদ data-fit: ডেটা kernel-এর বলা “আকারের” সঙ্গে কতটা মেলে। দ্বিতীয় পদ complexity-র শাস্তি: |Ky| (determinant) বড় মানে prior অনেক ধরনের ডেটা সম্ভব বলে, আর তখন যেকোনো একটি নির্দিষ্ট ডেটার সম্ভাবনা কম। ছোট ℓ-এর model প্রায় সবকিছু ব্যাখ্যা করতে পারে, তাই প্রতিটির ভাগে সম্ভাবনা কম পড়ে; বড় ℓ-এর model কম ধরনের ডেটা সম্ভব বলে, কিন্তু ডেটা না মিললে data-fit-এ বড় শাস্তি খায়। এই টানাপোড়েন একটি স্বয়ংক্রিয় Occam's razor, যা David MacKay ১৯৯২ সালের “Bayesian interpolation” প্রবন্ধে স্পষ্ট করে দেখান। Cholesky থাকলে দুটিই সস্তা: yᵀKy−1y = yᵀα, আর log|Ky| = ২ Σ log Lii (log-determinant, overflow ছাড়া)। নিচের stepper তিনটি বিন্দুতে দুটি পদ হাতে মেলায়।

x = 0, 1, 2; y = 1, 0.5, −0.5; σf = 1; σn² = 0.05; ধ্রুবক −(3/2) ln 2π = −2.7568
ℓ
k(0,1), k(0,2)
|Ky|
data-fit −½yᵀKy⁻¹y
complexity −½log|Ky|
মোট (ধ্রুবকসহ)
  1. ℓ = ০.৩. তিনটি বিন্দু কার্যত স্বাধীন, Ky প্রায় ১.০৫ × I, determinant ১.১৫৭৬. Data-fit −০.৭১৩৪, complexity −০.০৭৩২: “যা খুশি হতে পারত” model-এর দাম।
  2. ℓ = ০.৭. পাশাপাশি বিন্দুর covariance ০.৩৬. Data-fit সামান্য ভালো (−০.৬৭১০), কারণ y-গুলো সত্যিই ধীরে নামছে; complexity-ও উঠে +০.০৫৮৯. মোট −৩.৩৬৮৯.
  3. ℓ = ১.৫. Data-fit কমে −১.০৭১৪, কারণ মসৃণ model ১ থেকে −০.৫-এর পতন একটু কম সম্ভাব্য মনে করে। কিন্তু complexity লাফিয়ে +০.৯১৩৬, তাই মোট −২.৯১৪৬, চারটির মধ্যে সর্বোচ্চ।
  4. ℓ = ৪. তিনটি বিন্দু প্রায় পুরো বাঁধা (০.৯৭, ০.৮৮)। Complexity আরও ভালো (+১.৮৪১০), কিন্তু প্রায়-সমতল function দিয়ে দেড় এককের পতন ব্যাখ্যা করা কঠিন: data-fit −৩.৭৮৯৮-এ ধসে, মোট −৪.৭০৫৭.
  5. তুলনা। Complexity-র পদ সবসময় সরল (বড় ℓ) model চায়, data-fit বলে “ডেটা মেলাতে হবে”। মোট সর্বোচ্চ মাঝামাঝি ℓ = ১.৫-এ। কোনো validation set ছাড়াই, একই training ডেটা থেকে।

θ-এর ওপর এই পরিমাণ সর্বোচ্চ করাকে বলে empirical Bayes বা type-II maximum likelihood: f-এর ওপর পুরো Bayesian গড়, কিন্তু hyperparameter-এর জন্য একটি বিন্দু-অনুমান। scikit-learn এর gradient বন্ধ রূপে গুনে পর্ব ১৫-এর L-BFGS চালায়, θ রাখে লগ-স্কেলে (kernel_.theta), আর প্রতিটি hyperparameter-এর একটি সীমা (bounds) দিতে হয়। Likelihood-এর তলে একাধিক চূড়া থাকতে পারে, তাই n_restarts_optimizer দিয়ে কয়েকটি এলোমেলো শুরু থেকে চালানো ভালো। তৃতীয় ব্লক ৩০টি অর্ডারে আগে ℓ-এর grid-এ দুটি পদ নিজে গোনে, তারপর ConstantKernel * RBF + WhiteKernel শেখায়। WhiteKernel কর্ণে একটি শেখার যোগ্য noise variance যোগ করে।

import numpy as np
from scipy.linalg import cho_factor, cho_solve
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import ConstantKernel as C
from sklearn.gaussian_process.kernels import RBF, WhiteKernel
from bazar_gp import deliveries

X, y = deliveries(n=30, seed=47)     # 30টি অর্ডার
yc = y - y.mean(); n = len(y)

def lml_parts(sf, ell, sn):          # log p(y | X, theta) দুই ভাগে
    K = sf**2 * np.exp(-(X - X.T) ** 2 / (2 * ell**2)) + sn**2 * np.eye(n)
    c = cho_factor(K, lower=True)
    fit = -0.5 * yc @ cho_solve(c, yc)       # data-fit: -1/2 y^T K^-1 y
    comp = -np.sum(np.log(np.diag(c[0])))    # complexity: -1/2 log|K|
    return fit, comp, fit + comp - n / 2 * np.log(2 * np.pi)

print("  ell  data-fit  complexity    total")
for ell in [0.2, 0.5, 1, 2, 4, 8]:
    f, cpx, tot = lml_parts(10.0, ell, 2.0)
    print(f"{ell:5}  {f:8.1f}  {cpx:10.1f}  {tot:7.1f}")

kernel = (C(100.0, (1.0, 1e4)) * RBF(2.0, (0.1, 50.0))
          + WhiteKernel(1.0, (1e-2, 1e2)))
gp = GaussianProcessRegressor(kernel, n_restarts_optimizer=3,
                              random_state=46).fit(X, yc)
p = np.exp(gp.kernel_.theta)         # theta লগ-স্কেলে রাখা
sf, ell, sn = np.sqrt(p[0]), p[1], np.sqrt(p[2])
print("\nlearned:", gp.kernel_)
print(f"sigma_f = {sf:.2f}, ell = {ell:.2f} km, noise sd = {sn:.2f} min")
print(f"sklearn LML = {gp.log_marginal_likelihood_value_:.3f}",
      f"| scratch = {lml_parts(sf, ell, sn)[2]:.3f}")

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

  ell  data-fit  complexity    total
  0.2      -5.9       -53.3    -86.8
  0.5      -7.9       -42.5    -78.0
    1      -8.6       -35.4    -71.6
    2     -10.0       -30.6    -68.1
    4     -15.4       -27.7    -70.7
    8     -25.9       -26.2    -79.6

learned: 11**2 * RBF(length_scale=2.45) + WhiteKernel(noise_level=2.57)
sigma_f = 10.96, ell = 2.45 km, noise sd = 1.60 min
sklearn LML = -66.876 | scratch = -66.876

দুই পদের টান: ℓ ০.২ থেকে ৮-এ বাড়লে data-fit −৫.৯ থেকে −২৫.৯-এ নামে (মসৃণ model ডেটা কম মেলায়), আর complexity −৫৩.৩ থেকে −২৬.২-এ ওঠে (সরল model কম শাস্তি পায়)। মোট সর্বোচ্চ ℓ = ২-এ (−৬৮.১), দুই প্রান্তেই খারাপ। শেখা মান: L-BFGS তিনটি hyperparameter একসঙ্গে বদলে পেয়েছে σf = ১০.৯৬, ℓ = ২.৪৫ km, noise variance ২.৫৭, অর্থাৎ SD ১.৬০ মিনিট। LML −৬৬.৮৭৬, grid-এর সেরার চেয়ে ভালো, কারণ এবার σf আর σn-ও শেখা। নিজের সূত্র scikit-learn-এর মানের সঙ্গে তিন দশমিক পর্যন্ত মেলে। সতর্কতা: ডেটার আসল noise SD ২, শেখা মান ১.৬০: ৩০টি বিন্দুর একটি seed থেকে noise অনুমান এলোমেলো, আর আসল ধারাটি নিজে কোনো GP থেকে আসেনি। scikit-learn σf² দেখায় 11**2 আকারে, গোল করে। ব্লকটি কোনো warning ছাড়া চলে, কারণ সীমাগুলো y-এর এককে (মিনিট) যুক্তিসংগত।

৬ · Kernel জোড়া — সাপ্তাহিক অর্ডার আর ডেটার বাইরের অনুমান

Kernel-এর একটি সুন্দর গুণ: দুটি বৈধ kernel-এর যোগফল আর গুণফলও বৈধ kernel. যোগ মানে “f দুটি স্বাধীন অংশের যোগ”, গুণ মানে “দুটি শর্তই একসঙ্গে”। এই kernel composition দিয়ে ডেটার জানা গঠন model-এ বলা যায়। scikit-learn-এ কয়েকটি মূল উপাদান:

Kernelসূত্র (d = ∣x − x′∣)কী বলে
RBF(ℓ)exp(−d²/(২ℓ²))মসৃণ, ℓ-এর মাপে ধীর পরিবর্তন
ExpSineSquared(ℓ, p)exp(−২ sin²(πd/p)/ℓ²)প্রতি p দূরত্বে হুবহু পুনরাবৃত্তি (periodic kernel)
WhiteKernel(σ²)x = x′ হলে σ², নইলে ০প্রতিটি পর্যবেক্ষণে স্বাধীন noise
ConstantKernel(c)cগুণ করলে মাপ (σf²), যোগ করলে ধ্রুব স্তর
DotProduct(σ0)σ0² + x·x′সরলরেখা; non-stationary, Bayesian linear regression-এর সমান
Matern(ℓ, ν)(বহু-পদের সূত্র)RBF-এর চেয়ে কম মসৃণ; ν = ১.৫, ২.৫ প্রচলিত

বাজারঘরের দৈনিক অর্ডারে তিনটি অংশ দেখা যায়: সপ্তাহের ছাঁদ (বৃহস্পতিবার বেশি, শুক্রবার অনেক কম), কয়েক সপ্তাহের ধীর ওঠানামা, আর প্রতিদিনের এলোমেলোতা। তাই kernel হবে periodic (p = ৭ দিন, স্থির) + RBF (ধীর অংশ) + WhiteKernel (noise), প্রতিটির সামনে একটি মাপ। normalize_y=True দিলে scikit-learn y-কে গড় ০ আর SD ১-এ আনে, তাই মাপের সীমা ১-এর আশপাশে দেওয়া হয়েছে। Bounds-এর মাপ ভুল হলে শেখা মান সীমায় আটকে যায় আর scikit-learn একটি ConvergenceWarning দেয়, যা চাপা না দিয়ে সীমা ঠিক করাই উচিত (এই ব্লকের সীমা এভাবেই ঠিক করা হয়েছে)। ১০ সপ্তাহের ডেটা দিয়ে শিখে আরও ৮ সপ্তাহ সামনে অনুমান করা হবে। শেষে একটি আলাদা অংশে n বাড়িয়ে শেখা kernel-এর n × n matrix-এর Cholesky-র সময় মাপা হবে।

import time
import numpy as np
import matplotlib.pyplot as plt
from scipy.linalg import cho_factor
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import ExpSineSquared, RBF, WhiteKernel
from sklearn.gaussian_process.kernels import ConstantKernel as C
from bazar_gp import daily_orders

t, y = daily_orders(days=70)         # 10 সপ্তাহের দৈনিক অর্ডার
kernel = (C(1.0, (1e-2, 1e2)) * ExpSineSquared(1.0, 7.0, (0.01, 10), "fixed")
          + C(1.0, (1e-2, 1e2)) * RBF(20.0, (5, 200))
          + WhiteKernel(0.1, (1e-3, 1)))      # y scale করা, তাই মাপ ~1
gp = GaussianProcessRegressor(kernel, normalize_y=True, n_restarts_optimizer=2,
                              random_state=46).fit(t, y)
print("learned:", gp.kernel_)
tf = np.arange(0, 126).reshape(-1, 1).astype(float)   # আরও 8 সপ্তাহ
m, s = gp.predict(tf, return_std=True)
wd = "Sat Sun Mon Tue Wed Thu Fri".split()      # দিন 0 = শনিবার
for d in [68, 69, 75, 76, 124, 125]:
    print(f"day {d:3d} ({wd[d % 7]}): {m[d]:6.1f} +/- {1.96 * s[d]:4.1f}")
plt.figure(figsize=(9, 3.4)); plt.subplots_adjust(bottom=0.16)
plt.fill_between(tf[:, 0], m - 1.96*s, m + 1.96*s, alpha=0.25, label="95% band")
plt.plot(tf, m, lw=1, label="GP mean"); plt.plot(t, y, "k.", label="orders")
plt.axvline(69.5, ls="--", c="gray"); plt.axhline(y.mean(), ls=":", c="gray")
plt.xlabel("day"); plt.ylabel("orders per day"); plt.legend(fontsize=8); plt.show()

print("\n    n  Cholesky ms")        # O(n^3): n দ্বিগুণ হলে প্রায় 8 গুণ
rng = np.random.default_rng(0)
for n in [250, 500, 1000, 2000]:
    xx = np.sort(rng.uniform(0, 100, n)).reshape(-1, 1)
    K = gp.kernel_(xx)               # শেখা kernel-এর n x n matrix
    t0 = time.perf_counter(); cho_factor(K)
    print(f"{n:5d} {(time.perf_counter() - t0) * 1e3:11.1f}")

আসল আউটপুট (ব্রাউজারের Python; সময় আপনার যন্ত্রে আলাদা হবে):

learned: 0.826**2 * ExpSineSquared(length_scale=0.0512, periodicity=7) + 0.49**2 * RBF(length_scale=7.06) + WhiteKernel(noise_level=0.113)
day  68 (Thu):  150.2 +/-  8.8
day  69 (Fri):  118.3 +/-  9.0
day  75 (Thu):  150.4 +/- 12.1
day  76 (Fri):  119.4 +/- 12.6
day 124 (Thu):  152.4 +/- 14.6
day 125 (Fri):  121.0 +/- 14.6

    n  Cholesky ms
  250         0.9
  500         5.0
 1000        36.2
 2000       265.6

শেখা kernel (scale করা y-এর এককে; y-এর SD প্রায় ১১.৮): সাপ্তাহিক অংশের মাপ ০.৮২৬ (প্রায় ৯.৭ অর্ডার), ধীর অংশের ০.৪৯ (প্রায় ৫.৮ অর্ডার) আর ℓ = ৭.০৬ দিন, noise variance ০.১১৩, অর্থাৎ SD প্রায় ০.৩৩৬ × ১১.৮ ≈ ৪.০ অর্ডার: ডেটা বানানোর সময়ের ৪-এর সঙ্গে প্রায় হুবহু। Periodic অংশের ℓ = ০.০৫১২ খুব ছোট; দিনগুলো পূর্ণসংখ্যা বলে এর মানে “একই বারের দিনগুলো পুরো বাঁধা, ভিন্ন বারের দিন প্রায় স্বাধীন”: প্রতিটি বার নিজের মাত্রা পায়। সামনের অনুমান: বৃহস্পতিবার প্রায় ১৫০, শুক্রবার প্রায় ১১৮ থেকে ১২১, ৮ সপ্তাহ পরেও। সাপ্তাহিক ছাঁদ থেকে যায়, কিন্তু ধীর ঢেউয়ের তথ্য মিলিয়ে যায়: দিন ১২৪-এর ১৫২.৪ মোটামুটি ডেটার গড় (১৪২.৮) আর বৃহস্পতিবারের বাড়তি অংশের যোগফল। পরিসর ±৮.৮ থেকে চওড়া হয়ে ±১৪.৬-এ থামে, কারণ RBF অংশের পুরো prior variance ফিরে এসেছে। সময়: n দ্বিগুণ হলে Cholesky-র সময় ৫.৬, ৭.২ আর ৭.৩ গুণ, n³-এর ৮ গুণের দিকে এগোচ্ছে; n = ২,০০০-এর matrix-ই float64-এ ৩২ MB. সংখ্যাগুলো ব্রাউজারের একটি run-এর; ডেস্কটপের অপ্টিমাইজড LAPACK অনেক দ্রুত, কিন্তু অনুপাতের প্রবণতা একই।

৭ · দাম, বিস্তার আর সীমা — O(n³), sparse GP আর classification

দাম। Exact GP-তে n × n matrix রাখতে মেমরি O(n²), Cholesky-তে সময় O(n³) (time complexity), আর hyperparameter শেখার প্রতিটি ধাপে আবার Cholesky. Fit হয়ে গেলে প্রতিটি নতুন বিন্দুর গড় O(n), variance O(n²). তাই কয়েক হাজার বিন্দু পর্যন্ত exact GP আরামে চলে, কয়েক লাখে নয়।

Sparse GP. প্রধান প্রতিকার inducing points: m টি (m ≪ n) প্রতিনিধি বিন্দু Z, আর পুরো function-এর তথ্য তাদের মধ্য দিয়ে চালানো। তখন খরচ নামে প্রায় O(nm²)-এ। এটি পর্ব ৩১-এর Nyström ধারণার আত্মীয়: K ≈ KnmKmm−1Kmn. কয়েকটি মাইলফলক: Snelson আর Ghahramani-র FITC (NIPS ২০০৫), Quiñonero-Candela আর Rasmussen-এর একীভূত দৃষ্টি (JMLR ২০০৫), Titsias-এর variational inducing points (AISTATS ২০০৯), আর Hensman, Fusi ও Lawrence-এর stochastic রূপ (UAI ২০১৩), যা mini-batch-এ লাখো বিন্দুতে চলে। এসব sparse GP scikit-learn-এ নেই; GPyTorch বা GPflow-এর মতো library-তে পাওয়া যায়। Variational অংশটি পর্ব ৪৮ (Variational inference-এর ধারণা — ELBO আর approximation)-এর ভাষায় লেখা।

Classification. Label ০/১ হলে likelihood আর Gaussian নয় (যেমন logistic), তাই posterior-এর বন্ধ রূপ থাকে না। scikit-learn-এর GaussianProcessClassifier একটি latent function-এর posterior-কে তার শীর্ষবিন্দুর চারপাশে একটি Gaussian দিয়ে আনুমানিক করে, যাকে বলে Laplace approximation (GPML বইয়ের Algorithm 3.1, 3.2 আর 5.1; ধারণাটি Williams আর Barber-এর ১৯৯৮ সালের প্রবন্ধের)। বহু শ্রেণিতে এটি one-vs-rest বা one-vs-one চালায়। বাজারঘরে “এই অর্ডার ফেরত আসবে কি না”-র মতো প্রশ্নে এটি অনিশ্চয়তাসহ সম্ভাবনা দেয়, তবে পর্ব ২৬-এর মতো calibration যাচাই করতেই হবে।

সীমা। (১) Stationary kernel সব জায়গায় একই মসৃণতা আর একই noise ধরে। (২) RBF খুব মসৃণ; Michael Stein তাঁর ১৯৯৯ সালের বই Interpolation of Spatial Data-তে বাস্তব ডেটার জন্য Matérn kernel পছন্দ করার যুক্তি দেন। (৩) Extrapolation শুধু kernel যা বলে তা-ই: RBF বলে “গড়ে ফেরো”, periodic বলে “চিরকাল একই ছাঁদ”; কোনোটিই বাস্তব জগতের নিশ্চয়তা নয়। (৪) পরিসর তখনই বিশ্বাসযোগ্য, যখন model ঠিক; আলাদা ডেটায় coverage মেপে দেখুন, আর model-নির্ভরতা ছাড়া পরিসর চাইলে পর্ব ৫৮ (Conformal prediction — নিশ্চয়তাসহ অনুমানের পরিসর) দেখবেন।

আরও গভীরে: মূল পাঠ্য Carl Rasmussen আর Christopher Williams-এর Gaussian Processes for Machine Learning (MIT Press, ২০০৬), যা লেখকদের ওয়েবসাইটে বিনামূল্যে পাওয়া যায়; অধ্যায় ২ (regression), ৪ (kernel) আর ৫ (marginal likelihood) এই পর্বের ভিত্তি, আর অধ্যায় ৫-এর Mauna Loa CO₂ উদাহরণ kernel জোড়ার ধ্রুপদি নমুনা, যা scikit-learn-এর gallery-তেও আছে। বড় ডেটায় Gardner ও সহকর্মীদের GPyTorch (NeurIPS ২০১৮) Cholesky-র বদলে conjugate gradient আর GPU ব্যবহার করে। আর Radford Neal ১৯৯০-এর দশকে দেখান, এলোমেলো weight-এর এক স্তরের neural network-এর প্রস্থ অসীম করলে তা একটি GP হয়ে যায়।

পরীক্ষা ও ইন্টারভিউতে: (১) GP posterior-এর গড় আর variance? μ* = k*ᵀ(K + σn²I)−1y, σ*² = k** − k*ᵀ(K + σn²I)−1k*. (২) GP আর kernel ridge-এর সম্পর্ক? λ = σn² হলে গড় হুবহু এক; GP বাড়তি variance দেয়। (৩) Length-scale বড় হলে? Function মসৃণ, প্রভাব দূরে ছড়ায়, পরিসর সরু। (৪) Log marginal likelihood-এর দুই পদ? −½yᵀKy−1y (data-fit) আর −½log|Ky| (complexity), সঙ্গে ধ্রুবক −(n/২)log ২π. (৫) ডেটা থেকে দূরে GP কী অনুমান করে? Stationary kernel-এ prior-এর গড় আর prior-এর variance.

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

“ডেলিভারির সময়ের জন্য Gaussian process বানাও, অনিশ্চয়তাসহ” বললে AI assistant দ্রুত কোড দেয়, আর ছবিতে সুন্দর নীল ছায়াও আসে। কিন্তু এই বিষয়ের ভুল প্রায়ই ছবিতে ধরা পড়ে না।

AI প্রায়ই যা করেকেন সমস্যাকীভাবে ধরবেন
alpha আর WhiteKernel দুটিতেই noise দেয়, বা কোনোটিতেই নাnoise দুবার গোনা হয়, বা GP প্রতিটি বিন্দু হুবহু ছুঁয়ে overfit করেgp.kernel_ ছাপুন; noise কোথায় আছে দেখুন
Bounds না দিয়ে, ConvergenceWarning চাপা দেয়hyperparameter সীমায় আটকে থাকে, ফল অর্থহীনwarning পড়ুন; সীমা y আর x-এর এককে ঠিক করুন
np.linalg.inv(K) @ y লেখেধীর আর অস্থির; log|K| সরাসরি নিলে overflowcho_factor/cho_solve, আর ২Σlog Lii
return_std-কে নতুন পর্যবেক্ষণের পরিসর বলেnoise alpha-তে থাকলে std শুধু f-এর; পরিসর কম দেখায়σn² যোগ করুন; আলাদা ডেটায় coverage মাপুন
X scale না করে বহু feature-এ এক ℓ দেয়বড় এককের feature দূরত্ব দখল করেscale করুন বা প্রতি feature-এ আলাদা ℓ (anisotropic RBF)
Extrapolation-কে “পূর্বাভাস” বলে চালায়ডেটার বাইরে শুধু kernel-এর অনুমানkernel কী বলে ভাবুন; সামনের সময়ের ডেটায় backtest

AI-কে কোডের সঙ্গে অনুমান আর যাচাইও চাইতে বলুন:

আমার কাছে বাজারঘরের ১০ সপ্তাহের দৈনিক অর্ডারের সংখ্যা আছে (৭০টি সারি), সাপ্তাহিক ছাঁদসহ। ১. scikit-learn-এর GaussianProcessRegressor দিয়ে periodic + RBF + WhiteKernel বানাও; প্রতিটি hyperparameter-এর bounds কেন এই মানে, y-এর scale দেখে ব্যাখ্যা করো। ২. noise কোথায় রাখছ (alpha না WhiteKernel) আর কেন, স্পষ্ট লেখো; return_std কোন variance দেয়, তাও। ৩. শেষ ২ সপ্তাহ সরিয়ে রেখে বাকিতে fit করো, তারপর ওই ২ সপ্তাহে ৯৫% পরিসরের coverage আর MAE ছাপাও; seasonal naive (গত সপ্তাহের একই দিন) baseline-এর সঙ্গে তুলনা করো। ৪. কোনো ConvergenceWarning এলে চাপা দিয়ো না; কারণ আর সমাধান লেখো। ৫. তুমি যে parameter নাম আর default বলছ, সেগুলো কোন scikit-learn version-এর docs অনুযায়ী, তা লেখো।

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

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

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

প্র ০১খনির kriging থেকে ML-এর Gaussian process: পরিসংখ্যান আর ML কেন ১৯৯০-এর দশকে এসে “function-এর ওপর prior”-এ আগ্রহী হলো?

GP-র গণিত ML-এর চেয়ে পুরোনো। ১৯৫০-এর দশকে দক্ষিণ আফ্রিকার খনি প্রকৌশলী Danie Krige বোরহোলের নমুনা থেকে আশপাশের আকরিকের মান অনুমানের পদ্ধতি নিয়ে কাজ করেন, আর ১৯৬০-এর দশকে Georges Matheron তা আনুষ্ঠানিক রূপ দিয়ে নাম দেন kriging. Kriging-এর অনুমান আর তার “kriging variance” এই পর্বের posterior গড় আর variance-এর সমান রূপ। পরিসংখ্যানে Anthony O'Hagan ১৯৭৮ সালে Journal of the Royal Statistical Society-তে একটি প্রবন্ধে curve fitting-এ Gaussian process prior ব্যবহারের প্রস্তাব দেন।

ML-এ মোড় আসে neural network থেকে। ১৯৯০-এর দশকে Radford Neal Bayesian neural network নিয়ে কাজ করতে গিয়ে দেখান, এলোমেলো weight-এর এক লুকানো স্তরের network-এর প্রস্থ অসীম করলে, উপযুক্ত prior-এ, আউটপুট একটি Gaussian process হয়ে যায় (তাঁর ১৯৯৬ সালের বই Bayesian Learning for Neural Networks-এ এর বিবরণ আছে)। প্রশ্ন উঠল: অসীম network-এর জটিল sampling না করে সরাসরি GP দিয়েই কাজ করা যায় না? Christopher Williams আর Carl Rasmussen NIPS ১৯৯৫ সম্মেলনের প্রবন্ধে (প্রকাশ ১৯৯৬) ঠিক তা করেন: GP regression, marginal likelihood দিয়ে hyperparameter, আর benchmark-এ neural network-এর সঙ্গে তুলনা।

আকর্ষণটা বোঝা কঠিন নয়। Neural network-এ weight-এর prior বলা সহজ, কিন্তু তা function-এর ওপর কী বলে, বোঝা কঠিন। GP-তে সরাসরি বলা যায় “function মসৃণ, এই মাপে বদলায়, সপ্তাহে পুনরাবৃত্তি করে”। আর posterior বন্ধ রূপে আসে, কোনো sampling ছাড়া। দাম হলো n³, যা তখনকার ছোট benchmark-এ সমস্যা ছিল না।

শিক্ষাটি দুই দিকের। একই গণিত তিনটি সম্প্রদায়ে আলাদা নামে বেঁচে ছিল, আর ML পুরোনো সাহিত্য থেকে ধার করে লাভবান হয়েছে, যেমন ভূ-পরিসংখ্যানের Matérn kernel. আবার ML-এর দৃষ্টি (marginal likelihood দিয়ে kernel শেখা, বড় ডেটার approximation) পুরোনো ক্ষেত্রেও ফিরে গেছে।

মূল উপলব্ধি: GP হলো kriging-এর গণিত আর Bayesian ভাবনার মিলন; ML এটি গ্রহণ করে কারণ prior সরাসরি function-এর ওপর বলা যায় আর posterior বন্ধ রূপে আসে।

প্র ০২Marginal likelihood-কে “স্বয়ংক্রিয় Occam's razor” বলা হয়। এটি কতটা ভরসাযোগ্য, আর cross-validation-এর তুলনায় কখন ব্যর্থ হয়?

David MacKay ১৯৯২ সালে Neural Computation-এ “Bayesian interpolation” প্রবন্ধে ব্যাখ্যা করেন, কেন evidence (marginal likelihood) জটিল model-কে নিজে থেকেই শাস্তি দেয়। একটি model-এর সম্ভাবনার মোট পরিমাণ ১, সব সম্ভাব্য ডেটাসেটে ছড়ানো। নমনীয় model অনেক ধরনের ডেটা ব্যাখ্যা করতে পারে, তাই প্রতিটির ভাগে কম পড়ে; সরল model অল্প কিছুতে ঘন। ডেটা সরল model-এর এলাকায় পড়লে সরল model জেতে। এই পর্বের −½log|Ky| পদটি এই হিসাবের সংখ্যাগত রূপ, আর তৃতীয় ব্লকে তা ℓ-এর grid-এ স্পষ্ট দেখা গেছে।

কিন্তু ভরসার সীমা আছে। প্রথম, এটি model ঠিক ধরে নেয়। Noise যদি Gaussian না হয়, বা কিছু এলাকায় বেশি হয়, তাহলে marginal likelihood ভুল কারণে ভুল ℓ বা σn বাছতে পারে; তৃতীয় ব্লকে আসল noise SD ২ হলেও শেখা মান ১.৬০, শুধু ৩০টি বিন্দু থেকে। দ্বিতীয়, likelihood-এর তলে একাধিক চূড়া থাকে: একটি ব্যাখ্যা “ছোট ℓ, কম noise”, আরেকটি “বড় ℓ, বেশি noise” বলে। GPML বইয়ের অধ্যায় ৫-এ এমন দুই-চূড়ার উদাহরণ আছে। এ কারণে কয়েকটি শুরুর বিন্দু থেকে optimizer চালাতে হয়। তৃতীয়, hyperparameter বেশি হলে (যেমন প্রতি feature-এ আলাদা ℓ) marginal likelihood নিজেই overfit করতে পারে।

Cross-validation-এর সুবিধা হলো এটি সরাসরি অনুমানের মান মাপে, model ঠিক কি না তার ওপর কম নির্ভর করে। GP-তে leave-one-out-এর বন্ধ রূপ আছে, তাই CV-ও সস্তা (S. Sundararajan আর S. Sathiya Keerthi ২০০১ সালে এর ভিত্তিতে hyperparameter বাছাই প্রস্তাব করেন)। GPML বইয়ের অধ্যায় ৫-এর আলোচনার সারকথা মোটামুটি এই: model ভুল হলে CV বেশি টেকসই হতে পারে, আর model ঠিক হলে marginal likelihood সাধারণত বেশি স্থির। দুটির মধ্যে বিতর্ক এখনো চলে, আর বাস্তবে দুটিই চালিয়ে দেখা নিরাপদ।

মূল উপলব্ধি: Marginal likelihood একটি সুন্দর, সস্তা, validation set-ছাড়া বাছাই, কিন্তু তা model-এর অনুমানগুলোর মতোই ভালো; আলাদা ডেটায় coverage আর ভুল মেপে তার বাছাই যাচাই করুন।

প্র ০৩GP-র ৯৫% পরিসর কি সত্যিই ৯৫%? কোন অবস্থায় এটি আত্মবিশ্বাসী কিন্তু ভুল, আর কীভাবে যাচাই করবেন?

GP-র পরিসর একটি Bayesian credible interval: “model আর prior সত্য হলে f এই পরিসরে আছে ৯৫% সম্ভাবনায়”। শর্তটাই আসল কথা। Model সঠিক হলে, আর hyperparameter ঠিক থাকলে, নতুন ডেটায় প্রায় ৯৫% বিন্দু পরিসরে পড়ার কথা। বাস্তবে কয়েকটি জায়গায় এটি ভাঙে।

প্রথম, variance-এর সূত্রে y নেই। তাই ডেটার কোনো এলাকায় noise বেশি হলে (ধরুন বর্ষায় দূরের এলাকায় ডেলিভারির সময় বেশি ছড়ানো) GP সেটা দেখতেই পায় না; সব জায়গায় এক σn. সেই এলাকায় পরিসর সংকীর্ণ হবে, coverage ৯৫%-এর অনেক নিচে নামবে, আর শান্ত এলাকায় পরিসর অকারণে চওড়া। দ্বিতীয়, hyperparameter একটি বিন্দু-অনুমান। অল্প ডেটায় ℓ বা σf-এর অনিশ্চয়তা বড়, কিন্তু empirical Bayes তা উপেক্ষা করে, তাই পরিসর একটু আশাবাদী হয়। পুরো Bayesian পথে hyperparameter-এর ওপরও prior রেখে sampling করা যায় (পর্ব ৪৭, MCMC — Metropolis-Hastings আর Gibbs sampling), কিন্তু খরচ বাড়ে। তৃতীয়, extrapolation: periodic kernel বলে ছাঁদ চিরকাল থাকবে; ঈদের সপ্তাহ বা নতুন প্রতিযোগী এলে সেই প্রতিশ্রুতি ভাঙে, আর পরিসর তা জানে না।

যাচাইয়ের পথ সরল কিন্তু শৃঙ্খলার। Training-এ না-ছোঁয়া ডেটায় (সময়ের ডেটায় ভবিষ্যতের অংশে) গুনুন কত ভাগ বিন্দু ৯৫% পরিসরে পড়ল; বিভিন্ন এলাকা বা দিনের ধরন ধরে আলাদা করে গুনুন। পর্ব ২৬-এর reliability diagram-এর মতো, বিভিন্ন স্তরে (৫০%, ৮০%, ৯৫%) coverage আঁকা যায়। Coverage না মিললে উপায়: অন্য kernel (Matérn, কিছু feature-নির্ভর noise), y রূপান্তর (log), বা model-নির্ভরতা ছাড়া পরিসরের জন্য conformal prediction (পর্ব ৫৮)।

মূল উপলব্ধি: GP-র পরিসর model-এর ভেতরের সততা, বাইরের নিশ্চয়তা নয়; ৯৫% দাবি করলে আলাদা ডেটায় ৯৫% মেপে দেখান।

প্র ০৪O(n³)-এর দেয়াল ভাঙতে sparse GP কীভাবে কাজ করে, আর তার দামে কী হারায়?

চতুর্থ ব্লকে n দ্বিগুণ হলে Cholesky-র সময় মোটামুটি কয়েক গুণ বেড়েছে; বড় n-এ সেটি আট গুণের দিকে যায়। ১০ লাখ বিন্দুর n × n matrix float64-এ প্রায় ৮ TB, রাখাই অসম্ভব। তাই ২০০০-এর দশক থেকে GP গবেষণার একটি বড় ধারা approximation.

মূল কৌশল inducing points. m টি বিন্দু Z বেছে নিন (ধরুন ২০০টি), আর ধরে নিন f-এর সব তথ্য u = f(Z)-এর মধ্য দিয়ে যায়। তখন K-এর জায়গায় বসে একটি rank-m approximation KnmKmm−1Kmn, যা পর্ব ৩১-এর Nyström-এর সঙ্গে হুবহু মেলে, আর খরচ O(nm²). Joaquin Quiñonero-Candela আর Carl Rasmussen ২০০৫ সালে JMLR-এ দেখান, তখনকার অনেক পদ্ধতি (SoR, DTC, FITC) আসলে একই কাঠামোর ভিন্ন approximation. Edward Snelson আর Zoubin Ghahramani-র FITC Z-কেও hyperparameter হিসেবে শেখায়। Michalis Titsias ২০০৯ সালে একটি variational দৃষ্টি দেন: Z বাছাই একটি ELBO সর্বোচ্চ করার কাজ, ফলে approximation overfit করে না আর Z বাড়ালে exact GP-র কাছে যায়। James Hensman ও সহকর্মীরা ২০১৩ সালে এটিকে mini-batch stochastic optimization-এ আনেন।

হারানোর জিনিসও আছে। m টি বিন্দু যদি ডেটার সূক্ষ্ম কাঠামো ধরতে না পারে (ছোট ℓ, বহু মাত্রা), তাহলে গড় মসৃণ হয়ে যায় আর পরিসর ভুলভাবে চওড়া বা সরু হয়; কিছু পুরোনো approximation (যেমন SoR) ডেটা থেকে দূরে অস্বাভাবিক কম variance দেয়। অন্য পথও আছে: Gardner ও সহকর্মীদের GPyTorch exact GP-কেই conjugate gradient আর GPU দিয়ে কয়েক লাখ বিন্দুতে নিয়ে যায়, আর সময়ের ডেটায় state-space রূপে কিছু kernel n-এর সঙ্গে রৈখিক খরচে চলে।

মূল উপলব্ধি: Sparse GP n³-কে nm²-এ নামায় কয়েকটি প্রতিনিধি বিন্দুর মধ্য দিয়ে তথ্য চালিয়ে; m বাছাই হলো খরচ আর বিশ্বস্ত অনিশ্চয়তার মধ্যে দরকষাকষি।

প্র ০৫সাপ্তাহিক অর্ডারের forecast-এ GP, seasonal naive আর ARIMA: কোনটি কখন? আর “kernel বাছাই মানেই অনুমান বেছে নেওয়া” কথাটির মানে কী?

চতুর্থ ব্লকের kernel তিনটি বিশ্বাস লিখে দিয়েছে: সপ্তাহের ছাঁদ হুবহু পুনরাবৃত্ত হয়, কয়েক সপ্তাহের ধীর ঢেউ আছে যা সময়ের সঙ্গে মিলিয়ে যায়, আর প্রতিদিনের noise স্বাধীন। ফলাফলে শুক্রবারের ধস ৮ সপ্তাহ পরেও আছে, আর ধীর অংশ ডেটার গড়ে ফিরেছে। এটি model-এর “আবিষ্কার” নয়, kernel-এর সরাসরি পরিণতি। প্রবৃদ্ধি (trend) আছে বলে মনে হলে DotProduct যোগ করলে forecast সরলরেখায় উঠতে থাকবে; ছাঁদ ধীরে বদলায় মনে হলে periodic-কে একটি দীর্ঘ RBF দিয়ে গুণ করলে (“প্রায়-periodic”) ছাঁদ দূর ভবিষ্যতে ঝাপসা হবে। প্রতিটি পছন্দ একটি অনুমান, আর ভবিষ্যতের অনুমান মূলত সেই অনুমানই।

অন্য পদ্ধতির সঙ্গে তুলনা করলে ছবিটা পরিষ্কার হয়। Seasonal naive (“গত সপ্তাহের একই দিন”) কোনো parameter শেখে না, তবু সাপ্তাহিক ডেটায় প্রায়ই শক্ত baseline; GP সেটিকে না হারালে জটিলতার কোনো যুক্তি নেই। ARIMA আর exponential smoothing (পর্ব ৫১, Time series forecasting — decomposition, ARIMA আর exponential smoothing) লম্বা series-এ দ্রুত আর পরিচিত। আবার কিছু ARIMA model নির্দিষ্ট kernel-এর GP-র সমান হিসেবে লেখা যায়; পার্থক্য মূলত parameterization আর গণনার পথে। GP-র শক্তি অনিয়মিত সময়ে মাপা ডেটা, অনুপস্থিত দিন, অল্প ডেটা আর অনিশ্চয়তার পরিসর; দুর্বলতা লম্বা series-এ n³ খরচ আর বহু series একসঙ্গে সামলানো। শত শত দোকানের বিক্রির জন্য ML forecasting (পর্ব ৫২) প্রায়ই বেশি ব্যবহারিক।

কোনটি ভালো, তা ঠিক হবে backtest-এ: ভবিষ্যতের কয়েক সপ্তাহ তালাবদ্ধ রেখে প্রতিটি পদ্ধতির ভুল আর পরিসরের coverage মাপা, কয়েকটি ভিন্ন কাটার বিন্দুতে।

মূল উপলব্ধি: GP-তে kernel বাছাই মানে ভবিষ্যৎ সম্পর্কে বিশ্বাস লিখে দেওয়া; সেই বিশ্বাস সরল baseline আর সময়-ভিত্তিক backtest-এর সামনে যাচাই করুন।

অনুশীলন

হিসাবগুলো খাতায় করুন, তারপর উত্তর মেলান। চাইলে পাতার ব্লকগুলোতে সংখ্যা বদলে যাচাই করুন।

  1. পূর্ণ numerical: দুটি training বিন্দু x = ০ আর ২, y = ২ আর −১; kernel k(a, b) = ২−(a − b)² (σf = ১), σn² = ০.৫, prior গড় ০. (ক) K + σn²I আর তার inverse লিখুন। (খ) x* = ১-এ posterior গড় আর variance বের করুন। (গ) ৯৫% পরিসর কত? (ঘ) Log marginal likelihood-এর data-fit আর complexity পদ গুনুন (ln ব্যবহার করুন)।

    (ক) k(০, ২) = ২−৪ = ০.০৬২৫, তাই K + σn²I = [[১.৫, ০.০৬২৫], [০.০৬২৫, ১.৫]]; determinant ২.২৫ − ০.০০৩৯ = ২.২৪৬১; inverse = (১/২.২৪৬১)[[১.৫, −০.০৬২৫], [−০.০৬২৫, ১.৫]] = [[০.৬৬৭৮, −০.০২৭৮], [−০.০২৭৮, ০.৬৬৭৮]]. (খ) v = (K + σn²I)−1y = [১.৩৬৩৫, −০.৭২৩৫]. x* = ১ দুটি বিন্দু থেকে ১ দূরে, k* = [০.৫, ০.৫]; গড় = ০.৫ × (১.৩৬৩৫ − ০.৭২৩৫) = ০.৩২০০. w = (K + σn²I)−1k* = [০.৩২, ০.৩২], k*ᵀw = ০.৩২, তাই variance = ১ − ০.৩২ = ০.৬৮, SD ০.৮২৪৬. (গ) ০.৩২ ± ১.৯৬ × ০.৮২৪৬ = ০.৩২ ± ১.৬২, অর্থাৎ [−১.৩০, ১.৯৪]; নতুন পর্যবেক্ষণের variance ০.৬৮ + ০.৫ = ১.১৮. গড় ছোট, কারণ দুটি y-এর চিহ্ন উল্টো আর noise বড়। (ঘ) yᵀv = ২ × ১.৩৬৩৫ + ০.৭২৩৫ = ৩.৪৫০৪, data-fit = −১.৭২৫২; ln ২.২৪৬১ = ০.৮০৯২, complexity = −০.৪০৪৬; ধ্রুবক −(২/২)ln ২π = −১.৮৩৭৯; মোট −৩.৯৬৭৭. সংখ্যাগুলো একটি scratch script-এ যাচাই করা।

  2. সমালোচনা ও নকশা: একজন বিশ্লেষক লিখেছেন: “দূরত্ব (km) আর অর্ডারের মূল্য (টাকা) দুটি feature-এ GaussianProcessRegressor() default দিয়ে ডেলিভারির সময় অনুমান করেছি। Training-এ R² = ১.০০! পরিসর সব জায়গায় খুব সরু, তাই model খুব নিশ্চিত। ৩০ km দূরের নতুন জেলায়ও এটি দিয়ে সময় বলে দেব।” কী কী ভুল, আর আপনার নকশা কী?

    সমস্যা: (১) Default alpha=1e-10 আর noise-এর কোনো kernel নেই: GP প্রতিটি বিন্দু হুবহু ছোঁয়, তাই training R² = ১ অর্থহীন, এটি overfitting-এর লক্ষণ। (২) Training বিন্দুতে পরিসর সরু হবেই, কারণ সেখানে noise শূন্য ধরা; তা model-এর নিশ্চয়তা নয়। (৩) km আর টাকার মাপ আলাদা (কয়েক বনাম কয়েক হাজার), অথচ default RBF-এ এক ℓ: টাকার feature দূরত্ব দখল করবে। (৪) normalize_y=False, তাই ডেটা থেকে দূরে অনুমান ০ মিনিটের দিকে যাবে। (৫) ৩০ km ডেটার বাইরে: stationary kernel সেখানে prior-এ ফেরে, অর্থাৎ ডেটা কিছুই বলছে না। নকশা: আগে train/test ভাগ; X scale (Pipeline-এ) বা প্রতি feature-এ আলাদা ℓ (RBF(length_scale=[1.0, 1.0])); ConstantKernel * RBF + WhiteKernel, যুক্তিসংগত bounds, normalize_y=True, কয়েকটি restart; test-এ MAE আর ৯৫% পরিসরের coverage; একটি সরল baseline (linear model); আর নতুন জেলার জন্য সেখান থেকে কিছু ডেটা সংগ্রহ, তার আগে অনুমান না দেওয়া বা পরিসরসহ স্পষ্ট সতর্কতা।

  3. আউটপুট অনুমান করুন: চালানোর আগে তিনটি লাইন লিখুন (দশমিকের পর ৪ ঘর)।
    import numpy as np
    X = np.array([0.0, 1.0]); y = np.array([3.0, 1.0])
    k = lambda a, b: 2.0 ** (-(a[:, None] - b[None, :]) ** 2)
    A = k(X, X) + 0.1 * np.eye(2)
    ks = k(np.array([0.0]), X)[0]
    v = np.linalg.solve(A, y); w = np.linalg.solve(A, ks)
    print(np.round(v, 4))
    print(round(ks @ v, 4), round(1 - ks @ w, 4))
    print(round(-0.5 * y @ v, 4))

    ডেস্কটপে সত্যিকারের run (Python 3.14, NumPy 2.5):

    [ 2.9167 -0.4167]
    2.7083 0.0885
    -4.1667

    প্রথম লাইন GP-র stepper-এর v (kernel ridge-এর α)। দ্বিতীয় লাইনে x* = ০, একটি training বিন্দুতেই: গড় ২.৭০৮৩, y = ৩ নয়, কারণ noise ০.১ ধরা আছে, তাই GP বিন্দুটিকে হুবহু ছোঁয় না, পাশের y = ১-এর দিকে একটু টানে। Variance ০.০৮৮৫, noise-এর ০.১-এর চেয়েও কম: দুটি পর্যবেক্ষণ মিলিয়ে f(০) সম্পর্কে যা জানা যায়, তা একটি মাপের চেয়ে নিশ্চিত। শেষ লাইন log marginal likelihood-এর data-fit পদ, −½ × (৩ × ২.৯১৬৭ − ১ × ০.৪১৬৭) = −৪.১৬৬৭.

  4. কোডিং: একটি function লিখুন gp_posterior(X, y, Xs, sf, ell, sn), যা scipy.linalg.cho_factor/cho_solve দিয়ে posterior গড় আর f-এর SD ফেরত দেয় (y-এর গড় prior গড় হিসেবে), আর log_ml(X, y, sf, ell, sn), যা log marginal likelihood দেয়। বাজারঘরের ৩০টি অর্ডারে (deliveries(n=30, seed=47)) ℓ = ০.৫, ১, ২, ৪-এর মধ্যে log_ml দিয়ে সেরাটি বাছুন (σf = ১০, σn = ২), আর ৮ km-এ গড় ও ৯৫% পরিসর ছাপুন।
    import numpy as np
    from scipy.linalg import cho_factor, cho_solve
    from bazar_gp import deliveries
    
    def kern(A, B, sf, ell):
        return sf**2 * np.exp(-(A - B.T) ** 2 / (2 * ell**2))
    
    def gp_posterior(X, y, Xs, sf, ell, sn):
        ym = y.mean()
        c = cho_factor(kern(X, X, sf, ell) + sn**2 * np.eye(len(y)))
        Ks = kern(Xs, X, sf, ell)
        mean = ym + Ks @ cho_solve(c, y - ym)
        var = sf**2 - np.sum(Ks * cho_solve(c, Ks.T).T, axis=1)
        return mean, np.sqrt(np.maximum(var, 0))
    
    def log_ml(X, y, sf, ell, sn):
        yc = y - y.mean()
        c = cho_factor(kern(X, X, sf, ell) + sn**2 * np.eye(len(y)), lower=True)
        return (-0.5 * yc @ cho_solve(c, yc) - np.sum(np.log(np.diag(c[0])))
                - len(y) / 2 * np.log(2 * np.pi))
    
    X, y = deliveries(n=30, seed=47)
    scores = {ell: log_ml(X, y, 10.0, ell, 2.0) for ell in [0.5, 1, 2, 4]}
    best = max(scores, key=scores.get)
    m, s = gp_posterior(X, y, np.array([[8.0]]), 10.0, best, 2.0)
    print({k: round(float(v), 1) for k, v in scores.items()}, "best:", best)
    print(f"8 km: {m[0]:.1f} +/- {1.96 * s[0]:.1f} min")

    ডেস্কটপে সত্যিকারের run (Python 3.14, NumPy 2.5, SciPy 1.18) ছাপে:

    {0.5: -78.0, 1: -71.6, 2: -68.1, 4: -70.7} best: 2
    8 km: 57.6 +/- 8.2 min

    Log marginal likelihood তৃতীয় ব্লকের grid-এর সঙ্গে মেলে, আর সেরা ℓ = ২. ৮ km ডেটার সীমার (৬.৭৮ km) বাইরে, তাই পরিসর (±৮.২) ডেটার ভেতরের চেয়ে চওড়া। float(v) লেখা হয়েছে, কারণ NumPy 2-এ round করা NumPy সংখ্যা dict-এর ভেতরে np.float64(-78.0) আকারে ছাপে। (A - B.T) শুধু এক feature-এ কাজ করে; বহু feature-এ বর্গ-দূরত্বের সূত্র বা scikit-learn-এর kernel object লাগবে।

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

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

ক্রমCholesky দিয়ে Gaussian process regression নিজে চালানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Kernel আর তার hyperparameter (σf, ℓ, σn) বাছাই করা
  2. Training বিন্দুগুলোর kernel matrix K বানিয়ে কর্ণে σn² যোগ করা
  3. K + σn²I-এর Cholesky factor L বের করা
  4. দুটি ত্রিভুজাকার সমাধানে α = (K + σn²I)−1y পাওয়া
  5. নতুন বিন্দুর k* গুনে গড় k*ᵀα বের করা
  6. আরেকটি ত্রিভুজাকার সমাধানে variance k** − k*ᵀ(K + σn²I)−1k* বের করা

RBF kernel k(x, x′) = exp(−(x − x′)²/(২ℓ²))-এ ℓ = ১. দুটি বিন্দু ২ একক দূরে হলে তাদের f-মানের correlation প্রায় কত?

  • ০.৬১
  • ০.১৪
  • ০.০১১
  • ০.৫

একটি GP regression-এ (RBF kernel, prior গড় ০, σf² = ৪) একটি test বিন্দু সব training বিন্দু থেকে বহু ℓ দূরে। Posterior গড় আর f-এর variance প্রায় কত?

  • গড় সবচেয়ে কাছের training y, variance ০
  • গড় ০, variance ০
  • গড় ০ (prior), variance ৪ (prior)
  • গড় আর variance দুটিই অসীমের দিকে

Kernel ridge regression (λ) আর GP regression (noise variance σn²) একই kernel-এ চালানো হলো। কোন কথাটি ঠিক?

  • λ = σn² হলে অনুমানের গড় হুবহু এক, কিন্তু শুধু GP অনিশ্চয়তা দেয়
  • দুটির গড় কখনো এক হয় না
  • KRR sparse, GP dense
  • GP kernel ব্যবহার করে না, KRR করে

GP-র log marginal likelihood −½yᵀKy−1y − ½log|Ky| − (n/২)log ২π-এ length-scale ℓ অনেক বাড়ালে সাধারণত কোন পদটি বাড়ে (ভালো হয়)?

  • data-fit পদ −½yᵀKy−1y
  • −(n/২)log ২π
  • তিনটি পদই সমানভাবে
  • complexity পদ −½log|Ky|

দৈনিক অর্ডারে GP-র kernel: ExpSineSquared(periodicity = ৭) + RBF(ℓ = ৭ দিন) + WhiteKernel. ডেটা শেষ হওয়ার ৮ সপ্তাহ পরের অনুমান সম্পর্কে কোনটি ঠিক?

  • পুরো অনুমান একটি সমতল রেখা হয়ে যায়
  • সাপ্তাহিক ছাঁদ থাকে, ধীর অংশ গড়ে ফেরে, পরিসর চওড়া হয়
  • পরিসর ডেটার ভেতরের চেয়ে সরু হয়
  • শেষ সপ্তাহের ঠিক একই সংখ্যা হুবহু পুনরাবৃত্ত হয়, পরিসরও এক থাকে

n = ৪,০০০ বিন্দুতে exact GP-র Cholesky-তে ১ সেকেন্ড লাগলে n = ৮,০০০-এ প্রায় কত লাগবে (একই যন্ত্র, বড় n-এ)?

  • ২ সেকেন্ড
  • ৪ সেকেন্ড
  • ৮ সেকেন্ড
  • ১৬ সেকেন্ড

শব্দকোষ

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

মনে রাখুন

  1. সংজ্ঞা: GP মানে যেকোনো কয়েকটি x-এ f-এর মান যৌথভাবে Gaussian, covariance kernel থেকে। RBF-এর σf মাপ আর ℓ মসৃণতা ঠিক করে; prior থেকে নমুনা Cholesky + jitter দিয়ে।
  2. Posterior: μ* = k*ᵀ(K + σn²I)−1y, σ*² = k** − k*ᵀ(K + σn²I)−1k*; নতুন পর্যবেক্ষণে + σn². গড় = kernel ridge (λ = σn²)।
  3. আচরণ: ডেটার কাছে পরিসর সরু, ফাঁকে চওড়া, দূরে prior-এ ফেরা; variance-এ y নেই।
  4. Hyperparameter: log marginal likelihood = data-fit + complexity + ধ্রুবক; L-BFGS, bounds আর কয়েকটি restart; warning চাপা নয়।
  5. Kernel আর দাম: যোগ ও গুণে kernel জোড়া (periodic + RBF + White); extrapolation kernel-এর অনুমান মাত্র। Exact GP O(n³); বড় n-এ inducing point.

পরের পর্বে: GP-তে posterior বন্ধ রূপে এসেছে, কারণ সবকিছু Gaussian. বেশিরভাগ model-এ তা হয় না। পর্ব ৪৭ (MCMC — Metropolis-Hastings আর Gibbs sampling) দেখাবে, posterior-এর সূত্র না জেনেও তা থেকে কীভাবে নমুনা টানা যায়।

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

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