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

Vector আর similarity — dot product, cosine আর embedding-এর ধারণা

Vectors and Similarity — Norms, the Dot Product, Cosine Similarity, Distances and the Idea of Embeddings

Taught in Bangla, this lesson reads a vector as a list, an arrow and a feature row, then works through norms, unit vectors, the dot product, angles and cosine similarity by hand and in NumPy. It shows why Euclidean and cosine pick different neighbours, how unscaled columns distort distance, distance concentration in high dimensions, and a toy embedding similarity matrix with top-k search.

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

  • একটি vector-কে সংখ্যার তালিকা, ২-মাত্রার তীর আর feature-এর সারি হিসেবে পড়া; যোগ আর scalar গুণের ছবি
  • L2, L1 আর L∞ norm, unit vector আর normalisation, হাতে হিসাব করে NumPy দিয়ে মেলানো
  • Dot product-এর দুই রূপ, কোণ বের করা, আর cosine similarity কেন দৈর্ঘ্য উপেক্ষা করে
  • Euclidean, Manhattan আর cosine দূরত্ব; scaling না করলে কী ভুল হয়, আর অনেক মাত্রায় দূরত্বের অদ্ভুত আচরণ
  • Embedding-এর ধারণা, similarity matrix আর top-3 সবচেয়ে কাছের পণ্য; “king − man + woman” দাবির আসল চেহারা

এই পর্ব দিয়ে শুরু হলো মডিউল ৬, AI-এর গণিত। লক্ষ্য গণিতের পরীক্ষা পাস করা নয়, বরং AI-এর ভেতরে যে কয়েকটি ধারণা বারবার ফিরে আসে, সেগুলোর পেছনের ছবিটি পরিষ্কার করা। প্রতিটি ধারণায় আমরা একই ক্রম মানব: আগে একটি ছবি বা ছোট সংখ্যার উদাহরণ হাতে হিসাব, তারপর সূত্র, তারপর NumPy দিয়ে যাচাই, শেষে AI-তে কোথায় লাগে। NumPy-তে vector, @ আর np.linalg.norm লেখার হাতের কাজ আপনি পর্ব ১৭-এ দেখেছেন, আর broadcasting পর্ব ১৫-এ; এখানে সেই হিসাবগুলোর অর্থ আর জ্যামিতি দেখব। কোড পাতাতেই চলবে (ব্রাউজারের ভেতরে Python, Pyodide); প্রথমবার চালাতে কয়েক সেকেন্ড লাগতে পারে।

১ · Vector-এর তিন চেহারা — সংখ্যার তালিকা, তীর আর feature-এর সারি

ক্রিকেটের স্কোরকার্ডে একজন ব্যাটারের লাইনটি ভাবুন: রান ৪৫, বল ৩২, চার ৫, ছক্কা ১. চারটি সংখ্যা, আর প্রতিটির জায়গা নির্দিষ্ট; “৩২” যদি রানের ঘরে চলে যায়, অর্থই বদলে যায়। ক্রম মেনে সাজানো এমন কয়েকটি সংখ্যাকে গণিতে বলে vector. কয়টি সংখ্যা আছে, সেটি vector-এর মাত্রা (dimension): এখানে ৪-মাত্রার vector.

একই vector-কে তিনভাবে দেখা যায়, আর তিনটি চেহারাই এই পর্বে কাজে লাগবে।

১. সংখ্যার তালিকা

a = [3, 1]. কম্পিউটারের চোখে vector শুধু এটুকুই: NumPy-র একটি ১-মাত্রার array. যোগ, গুণ, দৈর্ঘ্য সবই এই সংখ্যাগুলোর ওপর হিসাব।

২. তীর

২ বা ৩ মাত্রায় vector-কে শূন্য বিন্দু থেকে একটি তীর হিসেবে আঁকা যায়: [3, 1] মানে ডানে ৩ ঘর, ওপরে ১ ঘর। তীরের আছে দিক আর দৈর্ঘ্য; similarity বোঝার মূল চাবি এই দুটি।

৩. feature-এর সারি

ডেটার একটি সারি, যেখানে প্রতিটি ঘর একটি feature. বাজারঘরের ক্রেতা রিনা = [12, 850, 5]: বছরে ১২টি অর্ডার, গড়ে ৮৫০ টাকা খরচ, শেষ অর্ডার ৫ দিন আগে।

তৃতীয় চেহারাটিই AI-তে সবচেয়ে বেশি দেখা যায়। একজন ক্রেতা, একটি পণ্য, একটি ছবি, এমনকি একটি বাংলা রিভিউকেও সংখ্যার একটি সারিতে বদলে ফেলা গেলে কম্পিউটার তাদের তুলনা করতে পারে। এমন সারিকে বলে feature vector. রিনার vector-এ তিনটি সংখ্যা, তাই এটি ৩-মাত্রার জগতের একটি বিন্দু বা তীর। ৩ মাত্রা আমরা কল্পনা করতে পারি; ৩০০ মাত্রা পারি না, কিন্তু হিসাবের নিয়ম একই থাকে। এই পর্বের কৌশল তাই সহজ: ছবি আঁকব ২ মাত্রায়, বিশ্বাস করব নিয়মগুলো যেকোনো মাত্রায় খাটে, আর NumPy দিয়ে যাচাই করব।

মূল ধারণা

একটি vector-এর প্রতিটি অবস্থানের একটি নির্দিষ্ট অর্থ থাকে। দুটি vector তুলনা করার আগে নিশ্চিত হতে হয় যে তাদের ঘরগুলো একই ক্রমে একই জিনিস বোঝায়: রিনার প্রথম ঘর যদি “অর্ডার” হয় আর সুমনের প্রথম ঘর “গড় খরচ”, তাহলে তাদের যোগ, গুণ বা দূরত্ব সবই অর্থহীন, যদিও কোড কোনো error দেবে না।

লেখার নিয়ম: এই পর্বে vector-কে ছোট হাতের অক্ষরে লিখব (a, b, v), আর তার i-তম ঘরকে ai. একক কোনো সংখ্যাকে, যেমন ২ বা ০.৫, বলে scalar; এর কোনো দিক নেই, শুধু মান।

২ · যোগ আর scalar গুণ — তীর জোড়া লাগানো আর লম্বা করা

রিকশায় বাসা থেকে প্রথমে পূর্বে ৩ ব্লক আর উত্তরে ১ ব্লক গেলেন (a = [3, 1]), তারপর সেখান থেকে পূর্বে ১ আর উত্তরে ২ ব্লক (b = [1, 2]). শেষে বাসা থেকে আপনি মোট কোথায়? পূর্বে 3 + 1 = 4, উত্তরে 1 + 2 = 3. এটিই vector যোগ: ঘরে ঘরে যোগ, a + b = [a1 + b1, a2 + b2] = [4, 3]. ছবিতে এর মানে: প্রথম তীরের মাথায় দ্বিতীয় তীরের লেজ বসানো। কোন তীর আগে, তাতে কিছু যায় আসে না; দুই পথেই একই বিন্দুতে পৌঁছান, তাই ছবিতে একটি সামান্তরিক (parallelogram) তৈরি হয়।

Scalar গুণ আরও সহজ: একটি সংখ্যা দিয়ে প্রতিটি ঘর গুণ। 2a = [6, 2]: একই দিক, দ্বিগুণ লম্বা। 0.5a অর্ধেক লম্বা, আর −1 দিয়ে গুণ করলে তীর ঘুরে ঠিক উল্টো দিকে যায়। মনে রাখুন, ধনাত্মক scalar দিয়ে গুণ করলে দিক বদলায় না; এই সাধারণ কথাটিই পরে cosine similarity-র প্রাণ।

তীর জোড়া লাগানো (যোগ) আর লম্বা করা (scalar গুণ)

Vector addition a + b and scalar multiplication 2a on a grid

Vector যোগ আর scalar গুণ 0123456 1234 a = (3, 1) b = (1, 2) a + b = (4, 3) 2a = (6, 2) (3+1, 1+2) = (4, 3) · 2×(3, 1) = (6, 2)
ছবি ৩৩.১ — নীল a-এর মাথায় সবুজ b বসালে (ড্যাশ), অথবা সবুজ b-এর মাথায় নীল a বসালে (ড্যাশ), দুই পথেই পৌঁছানো যায় (4, 3)-এ: এটিই কমলা a + b. বেগুনি 2a ঠিক a-এর দিকেই, শুধু দ্বিগুণ লম্বা।

AI-তে এই দুটি কাজ সর্বত্র। বাজারঘরের দুই মাসের বিক্রির vector যোগ করলে দুই মাসের মোট; একটি vector-কে 0.5 দিয়ে গুণ করলে অর্ধেক। দুটি vector-এর গড় মানে (a + b) / 2, তীরের মাথা দুটির ঠিক মাঝের বিন্দু; পর্ব ৬৫-এ (Clustering আর PCA) একটি দলের “কেন্দ্র” এভাবেই বের হবে। আর একটি neural network-এর প্রশিক্ষণে প্রতিটি ধাপে weight-এর vector থেকে একটি ছোট scalar গুণ করা vector বিয়োগ হয়; পর্ব ৩৪-এ (Derivative আর gradient descent) এই ধাপটি আসবে। NumPy-তে দুটি কাজই এক লাইনের: a + b আর 2 * a, দুটিই element-wise.

৩ · দৈর্ঘ্য মাপা — norm, unit vector আর normalisation

তীরটি কত লম্বা? ২ মাত্রায় উত্তর স্কুলের পিথাগোরাস: a = [3, 4] হলে দৈর্ঘ্য √(3² + 4²) = √25 = 5. ৩ বা বেশি মাত্রায় একই নিয়ম, শুধু বর্গ বেশি: সব ঘরের বর্গ যোগ করে বর্গমূল। দৈর্ঘ্যের এই মাপকে বলে L2 norm, আর চিহ্নে লেখা হয় ‖a‖ (দুই পাশে দুটি খাড়া দাগ)।

‖a‖2 = √(a12 + a22 + … + an2) ‖a‖1 = |a1| + … + |an| ‖a‖∞ = max |ai|

এখানে n হলো মাত্রার সংখ্যা, আর |ai| মানে চিহ্ন (± ) বাদ দেওয়া মান (absolute value). পর্ব ১৭-এ ঢাকার রাস্তার ছবি দিয়ে তিনটি norm দেখেছেন; এক লাইনে মনে করিয়ে দিই: L1 হলো রিকশার পথ (ঘরগুলোর absolute মানের যোগ, যার আরেক নাম Manhattan), L2 হলো কাকের সোজা পথ, আর L∞ (max norm) শুধু সবচেয়ে বড় একটি ঘর দেখে। [3, 4]-এর জন্য L1 = 7, L2 = 5, L∞ = 4. যেকোনো vector-এ L∞ ≤ L2 ≤ L1.

Unit vector — শুধু দিক, দৈর্ঘ্য ১

অনেক সময় আমরা শুধু দিকটা চাই, দৈর্ঘ্য নয়। যেমন দুজন ক্রেতা কোন ধরনের পণ্য বেশি কেনেন, সেটা তুলনা করতে চাই, কে কত বেশি কেনেন সেটা নয়। তখন vector-কে তার নিজের দৈর্ঘ্য দিয়ে ভাগ করা হয়: u = v / ‖v‖. ফলের দিক একই থাকে (ধনাত্মক scalar দিয়ে গুণ, অংশ ২ মনে করুন), কিন্তু দৈর্ঘ্য হয় ঠিক ১. দৈর্ঘ্য ১-এর vector-কে বলে unit vector, আর এই ভাগ করার কাজটিকে বলে normalisation. ২ মাত্রায় সব unit vector-এর মাথা একটি ব্যাসার্ধ ১-এর বৃত্তের ওপর থাকে। নিচে v = [2, 3, 6] ধাপে ধাপে normalise করে দেখুন। “পরে” চাপুন।

ধাপ ১প্রতিটি ঘরের বর্গ
v      = [2, 3, 6]
v ** 2 = [4, 9, 36]
ধাপ ২বর্গগুলোর যোগ
4 + 9 + 36 = 49
ধাপ ৩বর্গমূল = দৈর্ঘ্য
‖v‖ = √49 = 7
np.linalg.norm(v) → 7.0
ধাপ ৪প্রতিটি ঘরকে 7 দিয়ে ভাগ
u = v / 7
  = [2/7, 3/7, 6/7]
  ≈ [0.286, 0.429, 0.857]
ধাপ ৫যাচাই: দৈর্ঘ্য কি ১?
ভগ্নাংশে: 4/49 + 9/49 + 36/49 = 49/49 = 1
৩ ঘর দশমিকে: 0.0818 + 0.1840 + 0.7344
            ≈ 1.0003  # rounding-এর ছাপ
  1. বর্গ। প্রতিটি ঘরকে নিজের সঙ্গে গুণ: 2² = 4, 3² = 9, 6² = 36. বর্গ করার ফলে ঋণাত্মক মানও ধনাত্মক হয়ে যায়, তাই দিক যা-ই হোক দৈর্ঘ্য কখনো ঋণাত্মক নয়।
  2. যোগ। 4 + 9 + 36 = 49. এটি দৈর্ঘ্যের বর্গ, ‖v‖². অনেক হিসাবে (যেমন দূরত্ব তুলনায়) বর্গমূল না নিয়েই এই সংখ্যাটি ব্যবহার করা যায়, কারণ বড়-ছোটর ক্রম একই থাকে।
  3. বর্গমূল। √49 = 7, তাই ‖v‖ = 7. সংখ্যাগুলো ইচ্ছা করে এমন বেছে নেওয়া হয়েছে যাতে উত্তর পূর্ণসংখ্যা হয়; বাস্তবে প্রায় সবসময় দশমিক আসে।
  4. ভাগ। প্রতিটি ঘরকে 7 দিয়ে ভাগ: 2/7 ≈ 0.286, 3/7 ≈ 0.429, 6/7 ≈ 0.857. দিক একই আছে, কারণ তিনটি ঘর একই হারে ছোট হয়েছে; অনুপাত 2 : 3 : 6 বদলায়নি।
  5. যাচাই। ভগ্নাংশে বর্গের যোগ হুবহু 1. কিন্তু তিন ঘর দশমিকে কেটে নিলে যোগ ≈ 1.0003 আসে; এই সামান্য বাড়তি শুধু rounding-এর ফল, ভুল হিসাবের নয়। কম্পিউটারের float-এও একই ঘটনা আরও ছোট মাপে ঘটে।

শূন্য vector normalise করা যায় না। [0, 0, 0]-এর দৈর্ঘ্য 0, তাই v / ‖v‖ মানে 0 দিয়ে ভাগ। NumPy error দেয় না, ফল আসে nan; আর সেই nan চুপচাপ পরের সব হিসাবে ছড়িয়ে পড়ে। বাস্তবে এটি ঘটে, যেমন একজন নতুন ক্রেতা যিনি এখনো কিছুই কেনেননি। তাই normalise করার আগে দৈর্ঘ্য শূন্য কি না দেখা, বা ভাগের নিচে খুব ছোট একটি সংখ্যা যোগ করা, ভালো অভ্যাস।

৪ · Dot product — গুণ করে যোগ, আর কোণের মাপ

বাজারঘরের দুজন ক্রেতা তিন ধরনের পণ্যে (মুদি, প্রসাধনী, ঘরের জিনিস) মাসে কয়টি অর্ডার দেন: ক্রেতা ক = a = [1, 2, 2], ক্রেতা খ = b = [2, 1, 2]. দুজনের কেনার ধরন কতটা মেলে? প্রথম হাতিয়ার dot product, যার প্রথম রূপটি নিছক হিসাব: ঘরে ঘরে গুণ করে সব যোগ।

a · b = a1b1 + a2b2 + … + anbn = ∑i aibi

∑ (সিগমা) মানে “সব i-এর জন্য যোগ করুন”। ফল একটিমাত্র সংখ্যা, vector নয়। পর্ব ১৭-এ এটিই ছিল দামের তালিকা আর অর্ডারের পরিমাণ থেকে মোট বিল। দ্বিতীয় রূপটি জ্যামিতিক, আর এটিই এই পর্বের কেন্দ্র:

a · b = ‖a‖ ‖b‖ cos θ

এখানে θ (থিটা) হলো দুটি তীরের মাঝের কোণ, আর cos θ হলো সেই কোণের cosine: কোণ 0° হলে 1, 90° হলে 0, 180° হলে −1. মানে dot product দুটি জিনিসের গুণফল: দুই তীর কত লম্বা, আর তারা কতটা একই দিকে। দুটি রূপ একই সংখ্যা দেয় (এটি একটি উপপাদ্য, cosine-এর সূত্র থেকে কয়েক লাইনে প্রমাণ হয়), তাই প্রথম রূপে সহজে হিসাব করে দ্বিতীয় রূপ থেকে কোণ বের করা যায়: cos θ = (a · b) / (‖a‖ ‖b‖). নিচে ক আর খ-এর হিসাব ধাপে ধাপে দেখুন।

1a, b = [1, 2, 2], [2, 1, 2]2for i in range(3):3    s += a[i] * b[i]4na, nb = norm(a), norm(b)5cos = s / (na * nb)
মান (Watch)
i
a[i] × b[i]
s
‖a‖, ‖b‖
cos θ
  1. প্রথম ঘর (মুদি)। 1 × 2 = 2. যোগফল s এখন 2.
  2. দ্বিতীয় ঘর (প্রসাধনী)। 2 × 1 = 2, s = 2 + 2 = 4. যে ঘরে দুজনেরই মান বড়, সেই ঘর যোগফলে বেশি যোগ করে; মিলের মাপ এভাবেই তৈরি হয়।
  3. তৃতীয় ঘর (ঘরের জিনিস)। 2 × 2 = 4, s = 8. তাহলে a · b = 8.
  4. দুটি দৈর্ঘ্য। ‖a‖ = √(1 + 4 + 4) = √9 = 3, ‖b‖ = √(4 + 1 + 4) = √9 = 3. দুজনের মোট অর্ডারের “পরিমাণ” সমান।
  5. কোণ। cos θ = 8 / (3 × 3) = 8/9 ≈ 0.889, আর θ = arccos(0.889) ≈ 27.3°. দুটি তীর বেশ কাছাকাছি দিকে: দুজনের কেনার ধরন অনেকটা মেলে, পুরোপুরি নয়।

এবার একই হিসাব দুইভাবে কোডে: প্রথমে নিজে loop লিখে, তারপর NumPy দিয়ে। দুটির ফল মিলতে হবে, আর হাতের হিসাবের সঙ্গেও। সঙ্গে v = [2, 3, 6]-এর normalisation-ও যাচাই হবে।

import numpy as np

a = np.array([1, 2, 2])   # ক্রেতা ক: মুদি, প্রসাধনী, ঘরের জিনিস
b = np.array([2, 1, 2])   # ক্রেতা খ

# নিজে loop লিখে
s = 0
for x, y in zip(a, b):
    s += x * y
len_a = sum(x * x for x in a) ** 0.5
len_b = sum(y * y for y in b) ** 0.5
print("loop : dot =", s, " |a| =", len_a, " |b| =", len_b)

# NumPy দিয়ে
print("NumPy: dot =", a @ b, " |a| =", np.linalg.norm(a),
      " |b| =", np.linalg.norm(b))
print("L1 =", np.linalg.norm(a, 1), " Linf =", np.linalg.norm(a, np.inf))

cos = (a @ b) / (np.linalg.norm(a) * np.linalg.norm(b))
print("cos =", cos, " ~", round(cos, 4))
print("angle =", round(np.degrees(np.arccos(cos)), 2), "degrees")

v = np.array([2, 3, 6])
u = v / np.linalg.norm(v)            # unit vector
print("|v| =", np.linalg.norm(v), " u =", u.round(3))
print("|u| =", np.linalg.norm(u), " u @ u =", u @ u)

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

loop : dot = 8  |a| = 3.0  |b| = 3.0
NumPy: dot = 8  |a| = 3.0  |b| = 3.0
L1 = 5.0  Linf = 2.0
cos = 0.8888888888888888  ~ 0.8889
angle = 27.27 degrees
|v| = 7.0  u = [0.286 0.429 0.857]
|u| = 0.9999999999999999  u @ u = 0.9999999999999999

নিজের loop আর NumPy একই ফল দিয়েছে, আর দুটিই হাতের হিসাবের সঙ্গে মিলেছে: a · b = 8, ‖a‖ = ‖b‖ = 3. Dot product পূর্ণসংখ্যা (8), কারণ পূর্ণসংখ্যার গুণ আর যোগ; দৈর্ঘ্য float (3.0), কারণ বর্গমূল সবসময় float দেয়। a = [1, 2, 2]-এর L1 = 1 + 2 + 2 = 5 আর L∞ = 2. cos θ ছাপা হয়েছে 0.8888888888888888: 8/9-এর দশমিক অসীম, float মাত্র প্রায় ১৬টি অঙ্ক রাখতে পারে, তাই শেষ অঙ্কে 9 না হয়ে 8 এসেছে। কোণ 27.27°, হাতের 27.3°-এর সঙ্গে মিলেছে।

শেষ লাইনটি লক্ষ করুন। ভগ্নাংশে u-এর দৈর্ঘ্য হুবহু 1, কিন্তু কম্পিউটার দেখাচ্ছে 0.9999999999999999. 2/7, 3/7, 6/7 কোনোটিই binary float-এ নিখুঁতভাবে রাখা যায় না, তাই প্রায় 10−16 মাপের একটি পার্থক্য থেকে যায় (stepper-এর তিন-ঘর দশমিকের rounding-এর ক্ষুদ্র সংস্করণ)। তাই float-এর তুলনায় == 1 নয়, np.isclose(np.linalg.norm(u), 1) লেখাই নিরাপদ। আর u @ u মানে ‖u‖², একটি vector-এর নিজের সঙ্গে dot product সবসময় তার দৈর্ঘ্যের বর্গ।

কোণ বলে দেয় dot product-এর চিহ্ন

Same direction, perpendicular, opposite: cos θ = 1, 0, −1

একই দিক, সমকোণ, উল্টো দিক θ = 0°cos θ = 1a · b > 0 θ = 90°cos θ = 0a · b = 0 θ = 180°cos θ = −1a · b < 0 একই দিকসমকোণউল্টো দিক
ছবি ৩৩.২ — দৈর্ঘ্য যা-ই হোক, তীর দুটি একই দিকে হলে dot product ধনাত্মক, সমকোণে (90°) শূন্য, উল্টো দিকে ঋণাত্মক। সমকোণের দুটি vector-কে বলে orthogonal.

সমকোণের ঘটনাটি AI-তে খুব কাজের। [1, 0] আর [0, 1]-এর dot product 1×0 + 0×1 = 0: দুটির মধ্যে কোনো “মিল” নেই। একটি ক্রেতা যদি শুধু মুদি কেনেন আর অন্যজন শুধু প্রসাধনী, তাদের কেনার vector orthogonal. আর পর্ব ৬৮-এ (Neural network-এর ধারণা) দেখবেন, একটি কৃত্রিম নিউরনের প্রথম কাজই হলো ইনপুট আর weight-এর dot product: weight-এর দিকে যত বেশি মেলে, ফল তত বড়।

৫ · Cosine similarity — দৈর্ঘ্য নয়, দিক

অংশ ৪-এর শেষ সূত্রটিরই একটি নাম আছে: cosine similarity.

cos(a, b) = (a · b) / (‖a‖ ‖b‖)

তিনটি বৈশিষ্ট্য মনে রাখুন। প্রথমত, এর মান সবসময় −1 থেকে 1-এর মধ্যে (Cauchy–Schwarz অসমতা এটি নিশ্চিত করে)। 1 মানে হুবহু একই দিক, 0 মানে সমকোণ, −1 মানে উল্টো দিক। দ্বিতীয়ত, সব ঘর যদি শূন্য বা ধনাত্মক হয় (যেমন অর্ডারের সংখ্যা, খরচ, শব্দ কতবার এসেছে), তাহলে মান 0 থেকে 1-এর মধ্যেই থাকে, ঋণাত্মক হয় না। তৃতীয়ত, এবং সবচেয়ে গুরুত্বপূর্ণ: দৈর্ঘ্য এর হিসাবে ঢোকে না. যেকোনো vector-কে 10 দিয়ে গুণ করলে উপরে dot product 10 গুণ হয়, নিচে দৈর্ঘ্যও 10 গুণ হয়, কাটাকাটি হয়ে যায়। আসলে cosine similarity মানে দুটি unit vector-এর dot product: আগে normalise, তারপর dot.

উপমা — দুই পরিবারের বাজারের ব্যাগ। একটি ছোট পরিবার সপ্তাহে ২ কেজি চাল আর ১ কেজি ডাল কেনে; একটি বড় যৌথ পরিবার কেনে ২০ কেজি চাল আর ১০ কেজি ডাল। পরিমাণে দুই পরিবার অনেক দূরের, কিন্তু রুচি বা অনুপাতে (চাল : ডাল = ২ : ১) হুবহু এক। আপনার প্রশ্ন যদি হয় “কোন পরিবারের জন্য কোন অফার”, তাহলে অনুপাত (দিক) বেশি গুরুত্বপূর্ণ; প্রশ্ন যদি হয় “কোন ডেলিভারি ভ্যান পাঠাব”, তাহলে পরিমাণ (দৈর্ঘ্য)। Cosine প্রথম প্রশ্নের উত্তর দেয়, Euclidean দূরত্ব দ্বিতীয়টির কাছাকাছি।

এবার এমন একটি উদাহরণ যেখানে দুটি মাপ পরিষ্কারভাবে ভিন্ন উত্তর দেয়। তিনজন ক্রেতার মাসিক খরচ, শত টাকায়, [মুদি, প্রসাধনী]: A = [2, 1], B = [20, 10], C = [1, 3]. হাতে হিসাব করুন:

  • A আর B: Euclidean দূরত্ব √(18² + 9²) = √405 ≈ 20.12. Cosine = (40 + 10) / (√5 × √500) = 50 / 50 = 1. B ঠিক 10 × A, তাই একই দিক।
  • A আর C: Euclidean দূরত্ব √(1² + 2²) = √5 ≈ 2.24. Cosine = (2 + 3) / (√5 × √10) = 5 / √50 ≈ 0.707, মানে 45° কোণ।

Euclidean বলছে A-র সবচেয়ে কাছের প্রতিবেশী C (2.24 বনাম 20.12); cosine বলছে B (1 বনাম 0.707). দুটিই “সঠিক”, শুধু দুটি আলাদা প্রশ্নের উত্তর। নিচের কোড এই হিসাব যাচাই করে আর তীরগুলো আঁকে (ছবির লেখা ইংরেজিতে, কারণ এই পাতার matplotlib বাংলা অক্ষর ঠিকমতো জোড়া লাগাতে পারে না; পর্ব ২৮ দেখুন)।

import numpy as np
import matplotlib.pyplot as plt

# মাসিক খরচ (শত টাকা): [মুদি, প্রসাধনী]
A, B, C = np.array([2, 1]), np.array([20, 10]), np.array([1, 3])

def cosine(x, y):
    return (x @ y) / (np.linalg.norm(x) * np.linalg.norm(y))

for name, other in [("B", B), ("C", C)]:
    d = np.linalg.norm(A - other)
    c = cosine(A, other)
    print(f"A-{name}: Euclidean = {d:6.2f}  cosine = {c:.4f}  "
          f"cosine distance = {1 - c:.4f}")

fig, ax = plt.subplots(figsize=(5.5, 3.6))
for vec, lab, col in [(A, "A (2, 1)", "tab:blue"),
                      (B, "B (20, 10)", "tab:green"),
                      (C, "C (1, 3)", "tab:red")]:
    ax.annotate("", xy=vec, xytext=(0, 0),
                arrowprops=dict(arrowstyle="->", color=col, lw=2))
    ax.text(vec[0] + 0.4, vec[1] + 0.3, lab, color=col)
ax.set_xlim(0, 24); ax.set_ylim(0, 12); ax.set_aspect("equal")
ax.set_xlabel("groceries (100 taka)")
ax.set_ylabel("cosmetics (100 taka)")
ax.set_title("Euclidean: A is close to C.  Cosine: A matches B.")
plt.show()

আসল আউটপুট (ব্রাউজারের Pyodide-এ; ছবিটি আউটপুটের নিচে আসে):

A-B: Euclidean =  20.12  cosine = 1.0000  cosine distance = 0.0000
A-C: Euclidean =   2.24  cosine = 0.7071  cosine distance = 0.2929

হাতের হিসাব হুবহু মিলেছে। A আর B-এর Euclidean দূরত্ব 20.12, কিন্তু cosine 1.0000 আর cosine distance 0: কেনার ধরন অভিন্ন। A আর C-এর দূরত্ব মাত্র 2.24, কিন্তু cosine 0.7071 (= 1/√2, মানে 45° কোণ)। ছবিতে সবুজ B তীরটি নীল A-র ঠিক একই রেখা ধরে দশ গুণ লম্বা, আর লাল C ছোট কিন্তু অন্য দিকে হেলানো। তাই প্রশ্ন ঠিক করে দেয় মাপ: “কে সমান পরিমাণ কেনেন” জানতে Euclidean, “কে একই অনুপাতে কেনেন” জানতে cosine. একটি ছোট সতর্কতা: :.4f দিয়ে ছাপানো বলে 1.0000 দেখাচ্ছে; float-এ আসল মান 1-এর খুব কাছের কোনো সংখ্যা হতে পারে, আগের block-এর মতো।

আরও গভীরে — cosine, correlation আর Cauchy–Schwarz. Cauchy–Schwarz অসমতা বলে |a · b| ≤ ‖a‖ ‖b‖, তাই cosine কখনো ১-এর বেশি হয় না, আর সমান হয় কেবল তখনই যখন একটি vector অন্যটির ধনাত্মক গুণিতক। একটি সুন্দর যোগসূত্র: দুটি vector থেকে তাদের নিজ নিজ গড় বাদ দিয়ে (centering) তারপর cosine নিলে যা পাওয়া যায়, তা-ই Pearson correlation coefficient; পর্ব ৩৭-এ (ডেটার পরিসংখ্যান) correlation আসবে। আর unit vector-এর জন্য ‖u − v‖² = 2 − 2 cos(u, v), তাই normalise করা ডেটায় Euclidean দূরত্বে সাজানো আর cosine-এ সাজানো একই ক্রম দেয়।

৬ · দূরত্বের তিন মাপ, আর scaling কেন আগে

“মিল” মাপার উল্টো পিঠ হলো “দূরত্ব”: মিল যত বেশি, দূরত্ব তত কম। AI-তে তিনটি দূরত্ব সবচেয়ে বেশি দেখা যায়।

দূরত্বসূত্রকী মাপেকোথায় বেশি
Euclidean‖a − b‖2 = √∑(ai − bi)2সোজা লাইনের দূরত্ব; দৈর্ঘ্য আর দিক দুটিইসংখ্যার feature, k-NN, clustering
Manhattan‖a − b‖1 = ∑|ai − bi|ঘর ধরে পার্থক্যের যোগ; বড় একটি পার্থক্যকে বর্গ করে নাoutlier থাকলে, ছকের মতো সমস্যা
Cosine distance1 − cos(a, b)শুধু দিকের পার্থক্য; 0 (একই দিক) থেকে 2 (উল্টো)লেখা, embedding, কেনার ধরন

একটি সতর্কতা: cosine distance গণিতের কঠোর অর্থে “দূরত্ব” (metric) নয়, কারণ ত্রিভুজ অসমতা সবসময় মানে না; তবে কাছের-দূরের ক্রম সাজাতে এটি দিব্যি কাজ করে। SciPy-তে তিনটিই আছে: scipy.spatial.distance-এর euclidean, cityblock (Manhattan) আর cosine; মনে রাখুন SciPy-র cosine ফেরত দেয় দূরত্ব (1 − cos), similarity নয়। এই উল্টো-পাল্টা একটি পরিচিত বাগের উৎস।

টাকা বনাম দিন — কোন কলাম দূরত্ব ঠিক করে?

অংশ ১-এর ক্রেতা-vector-এ ফিরি: [বছরে অর্ডার, গড় খরচ (টাকা), শেষ অর্ডারের পর কত দিন]. বাজারঘরে নতুন ক্রেতা করিম = [11, 905, 8]: প্রায়ই কেনেন, সম্প্রতি কিনেছেন। পাঁচজন চেনা ক্রেতার মধ্যে কে তাঁর সবচেয়ে কাছের, যাতে তাঁর পছন্দ দেখে করিমকে প্রস্তাব দেওয়া যায়?

ক্রেতাঅর্ডার/বছরগড় খরচ (টাকা)শেষ অর্ডারের পর দিন
রিনা128505
সুমন390060
আলম1114007
নীলা282090
তানিয়া10100010
করিম (নতুন)119058

চোখে দেখলেই বোঝা যায় রিনা আর তানিয়া করিমের মতো: ঘন ঘন কেনেন, সম্প্রতি কিনেছেন। সুমন বছরে মাত্র ৩টি অর্ডার দেন আর দুই মাস কেনেননি। কিন্তু কাঁচা সংখ্যায় Euclidean দূরত্ব কী বলে? নিচের হিসাবটি ডেস্কটপে চালানো (সত্যিকারের run, লেখার সময়ে Python 3.14.4 আর NumPy 2.5.4):

~/bazarghar-ai$ python scaling.py
কাঁচা দূরত্ব  : [ 55.09  52.85 495.   118.45  95.03]
সবচেয়ে কাছে : সুমন
সুমনের পার্থক্যের বর্গ: [  64.   25. 2704.]
mean: [  7.6 994.   34.4]  std: [  4.22 212.    34.52]
standardise-এর পরে: [0.36 2.42 2.34 3.22 0.51]
সবচেয়ে কাছে : রিনা

স্ক্রিপ্টটি ছোট: পাঁচজনের X (shape (5, 3)) থেকে করিমের q বিয়োগ করে broadcasting-এ সারি ধরে np.linalg.norm(X - q, axis=1); তারপর প্রতিটি কলাম থেকে তার গড় বাদ দিয়ে standard deviation দিয়ে ভাগ, আর আবার একই দূরত্ব।

কাঁচা দূরত্বে করিমের সবচেয়ে কাছে সুমন (52.85), রিনা একটু পিছিয়ে (55.09). কেন? রিনার সঙ্গে করিমের পার্থক্য প্রায় পুরোটাই খরচে: 905 − 850 = 55 টাকা, যার বর্গ 3025. সুমনের সঙ্গে খরচের পার্থক্য মাত্র 5 টাকা; তাঁর বড় পার্থক্য দিনে (52 দিন, বর্গ 2704) আর অর্ডারে (8টি, বর্গ মাত্র 64). মানে কাঁচা হিসাবে “১টি অর্ডারের পার্থক্য” আর “১ টাকার পার্থক্য” সমান ওজন পাচ্ছে, অথচ বাস্তবে বছরে ৮টি অর্ডারের পার্থক্য অনেক বড় কথা আর ৫৫ টাকা খুব সামান্য। যে কলামের সংখ্যাগুলো বড় পরিসরে ছড়ানো (এখানে টাকা, তারপর দিন), সেটিই দূরত্ব ঠিক করে দেয়; অর্ডারের কলাম প্রায় কিছুই বলতে পারে না।

সমাধান feature scaling. সবচেয়ে প্রচলিত রূপ standardisation (z-score): প্রতিটি কলাম থেকে তার গড় (μ, মিউ) বাদ দিয়ে standard deviation (σ, সিগমা) দিয়ে ভাগ, z = (x − μ) / σ. ফলে প্রতিটি কলামের একক হয় “গড় থেকে কত standard deviation দূরে”, আর তিনটি কলাম তুলনীয় হয়। Standardise করার পরে করিমের সবচেয়ে কাছে রিনা (0.36), তারপর তানিয়া (0.51), আর সুমন অনেক দূরে (2.42): চোখের বিচারের সঙ্গে মিলেছে।

গড় আর σ কোথা থেকে নেবেন? এখানে μ আর σ নেওয়া হয়েছে শুধু চেনা পাঁচজন থেকে, নতুন করিমকে বাদ দিয়ে। মডেল বানানোর সময় নিয়মটি একই: scaling-এর মান শুধু train ডেটা থেকে, নইলে test ডেটার তথ্য আগেভাগে ঢুকে পড়ে (data leakage; পর্ব ৫৬, Train, validation আর test). আর পাঁচজনের মতো ছোট নমুনায় σ নিজেই অস্থির, তাই এই সংখ্যাগুলো শেখানোর জন্য, সাধারণ নিয়ম হিসেবে নয়। পর্ব ৫৭-এ (k-Nearest Neighbors — NumPy দিয়ে নিজে লেখা) এই দূরত্ব দিয়ে আসল classifier বানাবেন, আর পর্ব ৬৩-এ (Feature engineering) scaling-এর অন্য উপায় (min-max, robust scaling) আর কখন কোনটি, তা আসবে।

লক্ষ করুন, cosine similarity নিজেও scaling-এর প্রশ্ন থেকে মুক্ত নয়। Cosine প্রতিটি সারির দৈর্ঘ্য উপেক্ষা করে, কিন্তু কলামগুলোর মাপের পার্থক্য উপেক্ষা করে না: টাকার কলাম বড় হলে vector-এর দিকও প্রায় পুরোটাই টাকার দিকে হেলে থাকে। তাই কাঁচা ক্রেতা-ডেটায় cosine নিলেও আগে কলাম scale করা দরকার।

৭ · অনেক মাত্রার অদ্ভুত জগৎ — দূরত্ব কাছাকাছি চলে আসে

২ বা ৩ মাত্রায় আমাদের অন্তর্দৃষ্টি ভালো কাজ করে। কিন্তু AI-এর vector প্রায়ই শত শত বা হাজার মাত্রার। সেখানে একটি অবাক ঘটনা ঘটে: এলোমেলো বিন্দুর মধ্যে সবচেয়ে কাছের আর সবচেয়ে দূরের বিন্দুর দূরত্ব প্রায় সমান হয়ে আসে। একে বলে distance concentration. নিচের কোডে প্রতিটি মাত্রায় ৫০০টি এলোমেলো বিন্দু নেওয়া হয়েছে (প্রতিটি ঘর 0 থেকে 1-এর মধ্যে), আর একটি query বিন্দু থেকে সবচেয়ে কাছের ও সবচেয়ে দূরের দূরত্বের অনুপাত মাপা হয়েছে। অনুপাত 0-এর কাছে মানে “কাছের” আর “দূরের” স্পষ্ট আলাদা; 1-এর কাছে মানে সবাই প্রায় সমান দূরে।

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(42)
dims = [2, 5, 10, 50, 100, 500, 1000]
ratios = []
for d in dims:
    X = rng.random((500, d))     # ৫০০টি এলোমেলো বিন্দু
    q = rng.random(d)            # একটি query বিন্দু
    dist = np.linalg.norm(X - q, axis=1)
    r = dist.min() / dist.max()
    ratios.append(r)
    print(f"d={d:5d}  nearest={dist.min():7.3f}  "
          f"farthest={dist.max():7.3f}  ratio={r:.3f}")

fig, ax = plt.subplots(figsize=(5.5, 3.4))
ax.plot(dims, ratios, marker="o")
ax.set_xscale("log"); ax.set_ylim(0, 1)
ax.set_xlabel("number of dimensions d (log scale)")
ax.set_ylabel("nearest / farthest")
ax.set_title("Distances concentrate as dimension grows")
plt.show()

আসল আউটপুট (ব্রাউজারের Pyodide-এ; নিচে ছবি):

d=    2  nearest=  0.014  farthest=  1.049  ratio=0.013
d=    5  nearest=  0.185  farthest=  1.391  ratio=0.133
d=   10  nearest=  0.627  farthest=  1.874  ratio=0.335
d=   50  nearest=  1.812  farthest=  3.425  ratio=0.529
d=  100  nearest=  3.542  farthest=  4.806  ratio=0.737
d=  500  nearest=  8.507  farthest=  9.712  ratio=0.876
d= 1000  nearest= 12.269  farthest= 13.441  ratio=0.913

২ মাত্রায় সবচেয়ে কাছের বিন্দুটি প্রায় query-র গায়ে (0.014), সবচেয়ে দূরেরটি 1.049: অনুপাত 0.013, “কাছের” আর “দূরের” পরিষ্কার আলাদা। মাত্রা বাড়ার সঙ্গে অনুপাত বাড়তেই থাকে, আর ১০০০ মাত্রায় পৌঁছায় 0.913: সবচেয়ে কাছের বিন্দুও 12.27 দূরে, সবচেয়ে দূরেরটি 13.44. ৫০০টি বিন্দুর সবাই প্রায় একই দূরত্বের একটি পাতলা খোলসে। ছবির রেখাটি (log scale-এ মাত্রা) এই ধীরে ধীরে ওপরে ওঠাই দেখায়। এটি seed 42-এর একটি run; অন্য seed-এ সংখ্যা সামান্য আলাদা হবে, কিন্তু প্রবণতা একই। কেন এমন হয়, তার হিসাব ভাবনার প্রশ্ন ৪-এ আছে: দূরত্ব বাড়ে প্রায় √(d/6) হারে (১০০০ মাত্রায় প্রায় 12.9, ঠিক এই দুই সংখ্যার মাঝে), কিন্তু ছড়ানো প্রায় একই থাকে।

এই ঘটনা একটি বড় ধারণার অংশ, যাকে বলে curse of dimensionality (মাত্রার অভিশাপ)। শব্দটি সাধারণত Richard Bellman-এর নামে চলে, ১৯৫০-এর দশকের শেষে dynamic programming-এর কাজে; আর ১৯৯৯ সালে Beyer ও সহকর্মীরা “nearest neighbor কখন অর্থবহ” প্রশ্নে দূরত্বের এই কেন্দ্রীভবন বিশ্লেষণ করেন (পর্ব ১৭-এর ভাবনার প্রশ্ন ৪-এও এর কথা আছে)। তবে অভিশাপ কথাটি সতর্কভাবে পড়ুন। পরীক্ষাটি করা হয়েছে পুরোপুরি এলোমেলো, প্রতিটি মাত্রা স্বাধীন ডেটায়। বাস্তব ডেটায় গঠন থাকে: ১০০০ মাত্রার embedding-ও প্রায়ই আসলে অনেক কম “কার্যকর” মাত্রায় বিন্যস্ত থাকে, তাই সেখানে কাছের প্রতিবেশী খোঁজা অর্থবহ থাকতে পারে। প্রশ্নটি তাই “মাত্রা বেশি কি না” নয়, বরং “ডেটার মধ্যে আসল গঠন আছে কি না, আর আমার দূরত্ব সেই গঠন ধরছে কি না”। ব্যবহারিক শিক্ষা: অপ্রয়োজনীয় feature যোগ করলে দূরত্বভিত্তিক পদ্ধতি দুর্বল হয়, তাই feature বাছাই আর মাত্রা কমানো (পর্ব ৬৫, PCA) কাজে লাগে।

৮ · Embedding — অর্থকে vector বানানো, আর সবচেয়ে কাছের প্রতিবেশী

এতক্ষণ vector-এর ঘরগুলো ছিল আমাদের চেনা সংখ্যা: অর্ডার, টাকা, দিন। কিন্তু একটি শব্দ, একটি পণ্য বা একটি বাংলা রিভিউকে কীভাবে সংখ্যায় বদলাবেন, যাতে “অর্থে কাছাকাছি” জিনিস vector-এও কাছাকাছি হয়? এই ধারণার নাম embedding: প্রতিটি জিনিসকে একটি নির্দিষ্ট মাত্রার vector দেওয়া, যাতে মিল মানে ছোট কোণ (বড় cosine). লাইব্রেরির তাকের কথা ভাবুন: ভালো লাইব্রেরিয়ান একই বিষয়ের বই পাশাপাশি রাখেন, তাই একটি বই খুঁজে পেলে তার আশেপাশে একই ধরনের আরও বই পাওয়া যায়। Embedding হলো সেই তাক, শুধু ২ মাত্রার দেয়ালের বদলে শত মাত্রার জগতে।

নিচের টেবিলটি শেখানোর জন্য হাতে বানানো, কোনো মডেল থেকে শেখা নয়। বাজারঘরের আটটি পণ্য, প্রতিটি ৪-মাত্রার vector. বোঝার সুবিধার জন্য মাত্রাগুলোর একটি অর্থ দেওয়া হয়েছে।

পণ্যখাবার-রান্নাযন্ত্রশিশু-যত্নদামি
চাল0.900.000.100.10
ডাল0.850.000.150.10
তেল0.800.050.050.20
রাইস কুকার0.600.700.000.50
ব্লেন্ডার0.500.750.000.45
চার্জার0.000.900.050.20
ডায়াপার0.100.000.950.30
লোশন0.050.050.900.35

আসল embedding এমন দেখতে নয়। বাস্তবের embedding মানুষ হাতে লেখে না; একটি মডেল বিপুল ডেটা থেকে শেখে (যেমন কোন পণ্য একসঙ্গে কেনা হয়, কোন শব্দ কোন শব্দের পাশে বসে)। সেখানে মাত্রা সাধারণত শত বা হাজার, আর আলাদা কোনো মাত্রার “খাবার” বা “যন্ত্র”-এর মতো পরিষ্কার অর্থ সাধারণত থাকে না; অর্থ ছড়িয়ে থাকে অনেক মাত্রায় মিলিয়ে। কীভাবে শেখা হয় আর আসল একটি ছোট মডেল দিয়ে বাংলা লেখার embedding, তা পর্ব ৬৯-এর (Embedding আর vector search) বিষয়। এখানে শুধু ধারণাটি: embedding পেলে বাকি কাজ এই পর্বের cosine.

এখন প্রশ্ন: একজন ক্রেতা রাইস কুকার দেখছেন; “এর মতো আর কী দেখাবেন”? এটিই nearest neighbour search: query vector-এর সঙ্গে প্রতিটি পণ্যের cosine মেপে সবচেয়ে বড়গুলো বেছে নেওয়া। ছোট করে দেখাতে নিচের ধাপে মাত্র চারটি প্রার্থী নেওয়া হয়েছে।

1q = E["রাইস কুকার"]2for p in candidates:3    cos = (q @ p) / (norm(q) * norm(p))4top = sorted(scores, reverse=True)[:3]
মান (Watch)
প্রার্থী
q · p
‖p‖
cos
scores
  1. Query. q = [0.6, 0.7, 0, 0.5]. দৈর্ঘ্য ‖q‖ = √(0.36 + 0.49 + 0 + 0.25) = √1.10 ≈ 1.049. এটি একবারই হিসাব করতে হয়।
  2. ব্লেন্ডার [0.5, 0.75, 0, 0.45]. q · p = 0.30 + 0.525 + 0 + 0.225 = 1.05. ‖p‖ = √1.015 ≈ 1.008. cos = 1.05 / (1.049 × 1.008) ≈ 0.994: প্রায় একই দিক, রান্নাঘরের যন্ত্র দুটিই।
  3. চার্জার [0, 0.9, 0.05, 0.2]. q · p = 0 + 0.63 + 0 + 0.10 = 0.73. ‖p‖ = √0.8525 ≈ 0.923. cos ≈ 0.73 / 0.968 ≈ 0.754: “যন্ত্র” মাত্রায় মেলে, রান্নায় নয়।
  4. চাল [0.9, 0, 0.1, 0.1]. q · p = 0.54 + 0 + 0 + 0.05 = 0.59. ‖p‖ = √0.83 ≈ 0.911. cos ≈ 0.59 / 0.955 ≈ 0.617: রান্নার দিকে মেলে, কিন্তু চাল যন্ত্র নয়।
  5. ডায়াপার [0.1, 0, 0.95, 0.3]. q · p = 0.06 + 0 + 0 + 0.15 = 0.21. ‖p‖ = √1.0025 ≈ 1.001. cos ≈ 0.21 / 1.050 ≈ 0.200: প্রায় ভিন্ন দিক।
  6. বাছাই। বড় থেকে ছোট সাজিয়ে প্রথম তিনটি: ব্লেন্ডার 0.994, চার্জার 0.754, চাল 0.617. খেয়াল করুন, প্রতিটি ধাপে উপরের হিসাব একটি dot product আর দুটি norm; এই ছয় ধাপই পরের কোডে একটি matrix গুণে হবে।

সব পণ্যের সঙ্গে সব পণ্যের cosine একসঙ্গে চাইলে loop লাগে না। প্রথমে প্রতিটি সারি normalise করুন (প্রতিটি সারি unit vector), তারপর U @ U.T: shape (8, 4) @ (4, 8) → (8, 8). ফলের [i, j] ঘর হলো পণ্য i আর j-এর cosine. এই টেবিলকে বলে similarity matrix; সারি ধরে normalise করতে keepdims=True লাগে যাতে (8, 1) shape-এর দৈর্ঘ্য broadcasting-এ প্রতিটি সারিকে ভাগ করে (পর্ব ১৫)।

import numpy as np
np.set_printoptions(precision=2, suppress=True)

# শেখানোর জন্য হাতে বানানো 4-মাত্রার embedding (আসল মডেলের নয়)
# মাত্রা: [খাবার-রান্না, যন্ত্র, শিশু-যত্ন, দামি]
names = ["চাল", "ডাল", "তেল", "রাইস কুকার",
         "ব্লেন্ডার", "চার্জার", "ডায়াপার", "লোশন"]
E = np.array([[0.90, 0.00, 0.10, 0.10],
              [0.85, 0.00, 0.15, 0.10],
              [0.80, 0.05, 0.05, 0.20],
              [0.60, 0.70, 0.00, 0.50],
              [0.50, 0.75, 0.00, 0.45],
              [0.00, 0.90, 0.05, 0.20],
              [0.10, 0.00, 0.95, 0.30],
              [0.05, 0.05, 0.90, 0.35]])

U = E / np.linalg.norm(E, axis=1, keepdims=True)  # প্রতিটি সারি unit
S = U @ U.T                                       # (8, 8) cosine টেবিল
print("S.shape:", S.shape)
print(S)

def top_k(i, k=3):
    order = np.argsort(-S[i])          # বড় থেকে ছোট
    order = order[order != i][:k]      # নিজেকে বাদ দিয়ে
    return [(names[j], round(float(S[i, j]), 3)) for j in order]

for i in [0, 3, 5, 6]:
    print(names[i], "→", top_k(i))

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

S.shape: (8, 8)
[[1.   1.   0.99 0.62 0.54 0.03 0.24 0.19]
 [1.   1.   0.98 0.61 0.54 0.03 0.3  0.25]
 [0.99 0.98 1.   0.71 0.63 0.11 0.23 0.2 ]
 [0.62 0.61 0.71 1.   0.99 0.75 0.2  0.24]
 [0.54 0.54 0.63 0.99 1.   0.82 0.18 0.23]
 [0.03 0.03 0.11 0.75 0.82 1.   0.12 0.18]
 [0.24 0.3  0.23 0.2  0.18 0.12 1.   1.  ]
 [0.19 0.25 0.2  0.24 0.23 0.18 1.   1.  ]]
চাল → [('ডাল', 0.998), ('তেল', 0.988), ('রাইস কুকার', 0.617)]
রাইস কুকার → [('ব্লেন্ডার', 0.994), ('চার্জার', 0.754), ('তেল', 0.708)]
চার্জার → [('ব্লেন্ডার', 0.822), ('রাইস কুকার', 0.754), ('লোশন', 0.179)]
ডায়াপার → [('লোশন', 0.995), ('ডাল', 0.296), ('চাল', 0.236)]

Similarity matrix-টি (সারি আর কলাম দুটিই টেবিলের ক্রমে: চাল, ডাল, তেল, রাইস কুকার, ব্লেন্ডার, চার্জার, ডায়াপার, লোশন) প্রতিসম: [i, j] = [j, i], কারণ cos(a, b) = cos(b, a); আর কর্ণে সব 1, প্রতিটি পণ্য নিজের সঙ্গে হুবহু একই দিকে। টেবিলে তিনটি দল চোখে পড়ে: খাবার (চাল-ডাল-তেল, পরস্পর 0.98–1), রান্নাঘরের যন্ত্র (রাইস কুকার-ব্লেন্ডার, 0.99) আর শিশু-যত্ন (ডায়াপার-লোশন, 0.995, যা দুই ঘর দশমিকে 1. হয়ে ছাপা হয়েছে)। রাইস কুকারের প্রথম দুটি stepper-এর সঙ্গে মিলেছে (0.994, 0.754); তৃতীয়টি এখানে তেল (0.708), কারণ stepper-এ তেল প্রার্থী ছিল না, আর তেলের “দামি” ঘর চালের চেয়ে বড়।

সবচেয়ে শিক্ষণীয় লাইনটি চার্জারের: তৃতীয় “সবচেয়ে কাছের” পণ্য লোশন, cosine মাত্র 0.179. Top-k সবসময় k-টি ফল দেয়, মিল যত কমই হোক। বাস্তব ব্যবস্থায় তাই একটি ন্যূনতম similarity রাখা হয়, আর সেই সীমা ঠিক করা হয় আসল ডেটায় যাচাই করে, অনুমানে নয়।

এই ছোট উদাহরণে প্রতিটি পণ্যের সঙ্গে প্রতিটি পণ্য তুলনা করা হয়েছে: n পণ্যে প্রায় n² তুলনা। আটটি পণ্যে তা কিছুই নয়, কিন্তু লাখ লাখ পণ্য বা কোটি রিভিউতে প্রতিটি query-র জন্য সব vector মাপা ধীর। তাই বড় ব্যবস্থায় approximate nearest neighbour (ANN) পদ্ধতি ব্যবহার হয়, যা সামান্য নির্ভুলতা ছেড়ে অনেক দ্রুত “প্রায় সবচেয়ে কাছের” ফল দেয়। Vector database এই কাজটিই করে; ডেটাবেসের দিক থেকে ধারণাটি দেখতে পারেন DBMS কোর্সের vector database পর্বে, আর পর্ব ৬৯-এ এটি হাতে-কলমে আসবে। আর সাজানোর খরচ বুঝতে DSA-র heap পর্ব দেখুন: পুরো তালিকা সাজানোর বদলে heap দিয়ে শুধু top-k বের করা যায়।

“king − man + woman ≈ queen” — বিখ্যাত দাবি, আর তার আসল চেহারা। ২০১৩ সালে Tomas Mikolov ও সহকর্মীরা word2vec নামে শব্দের embedding শেখার একটি দ্রুত পদ্ধতি প্রকাশ করেন, আর একই বছর Mikolov, Yih ও Zweig-এর একটি প্রবন্ধ দেখায় যে এমন word embedding-এ vector যোগ-বিয়োগ দিয়ে কিছু সম্পর্ক ধরা যায়: “king”-এর vector থেকে “man” বিয়োগ করে “woman” যোগ করলে যে vector পাওয়া যায়, cosine-এ তার সবচেয়ে কাছের শব্দ প্রায়ই “queen”. এই word analogy উদাহরণটি embedding-এর সবচেয়ে বিখ্যাত ছবি হয়ে ওঠে। কিন্তু পরের গবেষণা দেখিয়েছে ছবিটি যতটা পরিষ্কার দেখানো হয়, বাস্তবে ততটা নয়। সাধারণ মূল্যায়নে প্রশ্নের তিনটি শব্দকে (king, man, woman) উত্তর হিসেবে বাদ দেওয়া হয়; বাদ না দিলে অনেক ক্ষেত্রে সবচেয়ে কাছের শব্দটি আসলে “king” নিজেই। অনেক সম্পর্কের ধরনে নির্ভুলতা অনেক কম, আর ফল নির্ভর করে কোন ডেটা আর কোন পদ্ধতিতে embedding শেখা হয়েছে তার ওপর। তাই এটিকে “embedding অর্থ বোঝে”-র প্রমাণ না ভেবে একটি আকর্ষণীয় কিন্তু সীমিত প্রবণতা হিসেবে দেখাই নিরাপদ।

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

“এই পণ্যগুলোর মধ্যে similar গুলো বের করো” বা “ক্রেতাদের nearest neighbour দাও” লিখলে AI সহকারী দ্রুত কোড দেয়। এই বিষয়ে ভুলগুলো প্রায় কখনো error দেয় না, শুধু ভুল প্রতিবেশী দেয়, তাই চোখে পড়ে না। AI সহকারীরা প্রায়ই যা করে:

  • দূরত্বের আগে scaling বাদ দেয়, অথবা scaling-এর μ আর σ পুরো ডেটা (test সহ) থেকে নেয়; আর ফল দেখায় সেই ডেটাতেই, যা দিয়ে সব ঠিক করা হয়েছে।
  • Similarity আর distance গুলিয়ে ফেলে: SciPy-র cosine (দূরত্ব) দিয়ে “সবচেয়ে বড়” বাছে, অথবা argsort-এর ক্রম উল্টো না করেই সবচেয়ে অমিলগুলো “top-3” বলে দেয়, আর query নিজেকেই প্রথম ফল হিসেবে রেখে দেয়।
  • শূন্য vector-এ 0 দিয়ে ভাগ উপেক্ষা করে; nan চাপা দিতে np.nan_to_num বসিয়ে দেয়, ফলে নতুন ক্রেতা চুপচাপ সবার “সমান কাছের” হয়ে যায়।
  • বানানো বা পুরোনো API লেখে, যেমন NumPy 2-এ সরানো np.float, বা np.linalg.norm(x, p=2)-এর মতো parameter যা নেই (আসল নাম ord); অথবা sklearn-এর function-এর নাম আন্দাজে লেখে।
  • বিশ্বাসযোগ্য শোনায় কিন্তু না-মাপা দাবি করে: “cosine সবসময় Euclidean-এর চেয়ে ভালো”, “এই embedding অর্থ বোঝে”, বা similarity-র একটি threshold (যেমন 0.8) কোনো যাচাই ছাড়াই “ভালো মান” বলে দেয়।

কোড চাওয়ার সময় ডেটার অর্থ, দূরত্বের পছন্দ আর যাচাইয়ের উপায় নিজেই বলে দিন। নিচের প্রম্পটে খেয়াল করুন: আসল ক্রেতার ডেটা নেই, বানানো সারি; কোন কলাম কোন এককে, তা স্পষ্ট; আর হাতে মেলানোর মতো ছোট একটি উদাহরণ চাওয়া হয়েছে।

আমি Python 3.14 আর NumPy 2.x ব্যবহার করছি। বানানো ডেটা: X = [[12,850,5],[3,900,60],[11,1400,7],[2,820,90],[10,1000,10]] (shape (5,3); কলাম: বছরে অর্ডার, গড় খরচ টাকায়, শেষ অর্ডারের পর দিন)। নতুন ক্রেতা q = [11,905,8]. ১. শুধু X থেকে mean আর std নিয়ে X আর q দুটিকেই standardise করো; q-কে mean/std-এর হিসাবে ঢোকাবে না। ২. Euclidean দূরত্ব আর cosine similarity দুটি দিয়েই q-এর top-2 প্রতিবেশী বের করো; similarity হলে বড় থেকে ছোট, দূরত্ব হলে ছোট থেকে বড় সাজাবে। ৩. কোনো সারির দৈর্ঘ্য শূন্য হলে স্পষ্ট error দাও, nan চাপা দেবে না। ৪. প্রতিটি লাইনের পাশে ফলের shape কমেন্টে লেখো, আর scaling-এর আগে ও পরে ফল পাশাপাশি ছাপাও। কোনো function বা parameter আমার NumPy version-এ আছে কি না নিশ্চিত না হলে বলো, বানিয়ে লিখবে না।

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

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

  • a = [1, 2, 3], b = [4, −5, 6]-এর dot product? 4 − 10 + 18 = 12.
  • Cosine similarity কেন দৈর্ঘ্য উপেক্ষা করে? উপরে dot product আর নিচে দুই দৈর্ঘ্যের গুণফল একই হারে বদলায়, তাই scalar গুণ কাটাকাটি হয়; এটি দুটি unit vector-এর dot product.
  • দুটি unit vector-এর Euclidean দূরত্ব আর cosine-এর সম্পর্ক? ‖u − v‖² = 2 − 2 cos θ; তাই normalise করা ডেটায় দুটি একই ক্রম দেয়।
  • k-NN-এর আগে scaling কেন? বড় এককের কলাম (টাকা) দূরত্বে আধিপত্য করে; standardise করলে প্রতিটি কলাম তুলনীয় হয়।
  • Curse of dimensionality-র একটি লক্ষণ? এলোমেলো ডেটায় মাত্রা বাড়লে সবচেয়ে কাছের আর দূরের দূরত্বের অনুপাত 1-এর দিকে যায়, তাই “কাছের প্রতিবেশী” কম অর্থবহ হয়।

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

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

প্র ০১Search engine কি ইন্টারনেটের আগেই cosine ব্যবহার করত? লেখাকে vector বানানোর ধারণা কোথা থেকে এলো?

হ্যাঁ, ধারণাটি ইন্টারনেটের চেয়ে পুরোনো। ১৯৬০-এর দশকে, প্রথমে Harvard-এ আর পরে Cornell বিশ্ববিদ্যালয়ে, Gerard Salton আর তাঁর দল SMART নামে একটি তথ্য-খোঁজার (information retrieval) ব্যবস্থা নিয়ে কাজ শুরু করেন। মূল বুদ্ধিটি ছিল সরল: প্রতিটি নথিকে একটি লম্বা vector হিসেবে দেখা, যেখানে প্রতিটি ঘর একটি শব্দ, আর ঘরের মান সেই শব্দ নথিতে কতটা গুরুত্বপূর্ণ। ব্যবহারকারীর প্রশ্নও একই রকম একটি vector. তারপর প্রশ্নের vector-এর সঙ্গে প্রতিটি নথির cosine similarity মেপে বড় থেকে ছোট সাজানো। ১৯৭৫ সালে Salton, Wong আর Yang-এর “A Vector Space Model for Automatic Indexing” প্রবন্ধটি এই দৃষ্টিভঙ্গির একটি বহুল উদ্ধৃত দলিল, আর “vector space model” নামটিও এই ধারার কাজ থেকেই জনপ্রিয় হয়।

Cosine কেন? কারণ নথির দৈর্ঘ্য। একটি দীর্ঘ নথিতে প্রায় সব শব্দই বেশিবার আসে, তাই কাঁচা dot product-এ লম্বা নথি অকারণে এগিয়ে যেত। Cosine দৈর্ঘ্য বাদ দিয়ে শুধু “শব্দের মিশ্রণ” তুলনা করে, ঠিক যেমন অংশ ৫-এ ছোট আর বড় পরিবারের বাজারের ব্যাগ। পরে গবেষকেরা দেখেন যে দৈর্ঘ্যকে পুরোপুরি উপেক্ষা করাও সবসময় ঠিক নয় (দীর্ঘ নথি সত্যিই বেশি প্রাসঙ্গিক হতে পারে), তাই দৈর্ঘ্যের প্রভাব আংশিক রাখার নানা কৌশল আসে, আর পরে BM25-এর মতো সম্ভাবনাভিত্তিক মডেল অনেক ক্ষেত্রে জনপ্রিয় হয়।

শব্দকে কীভাবে ওজন দেওয়া হবে, সেই প্রশ্ন থেকে আসে TF-IDF: কোনো শব্দ একটি নথিতে বেশি কিন্তু সব নথিতে কম থাকলে তার ওজন বেশি। বাংলা লেখায় এটি পর্ব ৬৭-এ (বাংলা লেখার ডেটা) আসবে। আর ভাষাতত্ত্বে প্রায় একই সময়ে আরেকটি ধারণা চলছিল: Zellig Harris (১৯৫৪) আর J. R. Firth (১৯৫৭)-এর “distributional” দৃষ্টিভঙ্গি, Firth-এর বহুল উদ্ধৃত কথায় “একটি শব্দকে চেনা যায় তার সঙ্গীদের দিয়ে”। আজকের word embedding এই দুই ধারার মিলন: শব্দের অর্থ ধরা হয় সে কোন শব্দের পাশে বসে তা থেকে, আর তুলনা করা হয় cosine দিয়ে।

মূল উপলব্ধি: আজকের “semantic search” নতুন শোনালেও তার কাঠামো অর্ধশতাব্দী পুরোনো: লেখাকে vector বানানো, তারপর cosine দিয়ে সাজানো। বদলেছে মূলত vector কীভাবে তৈরি হয়, গুনে না শিখে।

প্র ০২“king − man + woman = queen” উদাহরণটি কতটা সত্যি, আর গবেষকেরা এর কোন দুর্বলতা দেখিয়েছেন?

২০১৩ সালে Google-এ কর্মরত Tomas Mikolov ও সহকর্মীরা word2vec নামে দুটি সরল কিন্তু খুব দ্রুত মডেল প্রকাশ করেন, যা বিপুল লেখা থেকে প্রতিটি শব্দের জন্য কয়েকশো মাত্রার vector শেখে। একই বছর Mikolov, Wen-tau Yih আর Geoffrey Zweig-এর একটি প্রবন্ধ দেখায়, এই vector-এ কিছু সম্পর্ক প্রায় একটি নির্দিষ্ট “দিক” হিসেবে বসে আছে বলে মনে হয়: পুরুষ থেকে নারী, দেশ থেকে রাজধানী, একবচন থেকে বহুবচন। পরীক্ষার পদ্ধতি অংশ ৮-এর মতোই: king − man + woman হিসাব করে cosine-এ সবচেয়ে কাছের শব্দ খোঁজা। ফল প্রায়ই “queen”, আর উদাহরণটি বক্তৃতা, বই আর ব্লগে প্রায় প্রতীক হয়ে যায়।

কিন্তু খুঁটিয়ে দেখলে ছবিটি জটিল। প্রথমত, প্রচলিত মূল্যায়নে প্রশ্নের তিনটি শব্দকে উত্তরের তালিকা থেকে বাদ দেওয়া হয়। ২০২০ সালে Malvina Nissim, Rik van Noord আর Rob van der Goot একটি প্রবন্ধে দেখান, এই বাদ না দিলে অনেক সময় সবচেয়ে কাছের শব্দ “king” নিজেই, মানে “woman − man” দিকটি যতটা শক্তিশালী মনে হয় ততটা নয়। দ্বিতীয়ত, Omer Levy আর Yoav Goldberg (২০১৪) দেখান যে এই সাফল্যের বড় অংশ সাধারণ শব্দ-গণনার vector দিয়েও পাওয়া যায়, আর তুলনার সূত্র বদলালে ফলও বদলায়। তৃতীয়ত, সম্পর্কের ধরনভেদে নির্ভুলতা খুব আলাদা: কিছু ব্যাকরণগত সম্পর্কে ভালো, অনেক অর্থগত সম্পর্কে দুর্বল।

আরও একটি দিক আছে, যা প্রশ্ন ৫-এ বিশদে আসবে: একই পদ্ধতি সমাজের পক্ষপাতও তুলে আনে, যেমন পেশার সঙ্গে লিঙ্গের stereotype. তাই উদাহরণটি দুই দিকেই শিক্ষণীয়: embedding-এর জ্যামিতিতে সত্যিই কিছু অর্থবহ গঠন থাকে, কিন্তু সেই গঠন নিখুঁত নয়, আর তা শেখা ডেটার প্রতিফলন। বাংলার মতো ভাষায়, যেখানে শব্দের রূপ (বিভক্তি, প্রত্যয়) অনেক, শব্দভিত্তিক analogy আরও জটিল হতে পারে; এ নিয়ে নিশ্চিত সাধারণ দাবি করা কঠিন।

মূল উপলব্ধি: একটি বিখ্যাত উদাহরণ একটি প্রবণতা দেখায়, নিয়ম নয়; কোনো দাবি যাচাই করতে হলে মূল্যায়নের খুঁটিনাটি (কী বাদ দেওয়া হলো, কোন ডেটা, কত শতাংশ ক্ষেত্রে) দেখতে হয়।

প্র ০৩অনেক embedding ব্যবস্থা আগে সব vector normalise করে, তারপর শুধু dot product নেয়। কেন? আর cosine কি সবসময় ঠিক মাপ?

কারণটি গতি আর সরলতা। Cosine = dot product ÷ দুই দৈর্ঘ্য। যদি সব vector আগেই একবার normalise করে রাখা হয়, তাহলে প্রতিটি query-তে ভাগের কাজটি আর লাগে না: cosine আর dot product হুবহু এক। লাখ লাখ vector-এর সঙ্গে প্রতিটি তুলনায় এই সাশ্রয় বড়, আর matrix গুণের দ্রুত library (পর্ব ১৭-এর BLAS) সরাসরি কাজে লাগানো যায়, যেমন অংশ ৮-এর U @ U.T. তার ওপর অংশ ৫-এর “আরও গভীরে” বাক্সের সম্পর্কটি (‖u − v‖² = 2 − 2 cos) বলে, normalise করা ডেটায় Euclidean দূরত্বের ক্রম আর cosine-এর ক্রম এক; তাই Euclidean-এর জন্য বানানো দ্রুত খোঁজার কাঠামোও ব্যবহার করা যায়।

বড় মাপে খোঁজার জন্য গবেষণাও অনেক। ১৯৯৮ সালে Piotr Indyk আর Rajeev Motwani-র কাজ locality-sensitive hashing ধারার একটি ভিত্তি, যেখানে কাছাকাছি vector একই “বালতিতে” পড়ার সম্ভাবনা বেশি। পরে graph-ভিত্তিক পদ্ধতি HNSW (Malkov আর Yashunin, প্রথমে ২০১৬ সালে প্রকাশিত) আর ২০১৭ সালে Johnson, Douze আর Jégou-র GPU-তে বিপুল মাপের similarity search নিয়ে কাজ (FAISS library) ব্যাপক ব্যবহৃত হয়েছে। সবগুলোরই মূল আপস একই: সামান্য নির্ভুলতা ছেড়ে বিপুল গতি।

কিন্তু cosine সবসময় সঠিক মাপ নয়। দৈর্ঘ্যের মধ্যেও তথ্য থাকতে পারে: কিছু মডেলে vector-এর দৈর্ঘ্য কোনো জিনিস কতটা জনপ্রিয় বা কতটা নিশ্চিত, তার সঙ্গে জড়িত থাকে, আর normalise করলে সেই তথ্য হারায়। ২০২৪ সালে Harald Steck ও সহকর্মীদের একটি প্রবন্ধ (“Is Cosine-Similarity of Embeddings Really About Similarity?”) দেখায়, কিছু ধরনের শেখা embedding-এ cosine-এর ফল প্রশিক্ষণের খুঁটিনাটির ওপর এমনভাবে নির্ভর করতে পারে যে তা প্রায় খেয়ালখুশির মতো হয়ে যায়। তাদের যুক্তি সব মডেলে খাটে কি না, তা নিয়ে আলোচনা চলছে; তবে শিক্ষাটি পরিষ্কার।

মূল উপলব্ধি: Normalise করে dot product একটি চমৎকার প্রকৌশল-কৌশল, কিন্তু কোন similarity ব্যবহার করবেন তা নির্ভর করে embedding কীভাবে শেখা হয়েছে তার ওপর; মডেলের নথি (model card) দেখুন আর আসল প্রশ্নে কয়েকটি ফল চোখে যাচাই করুন।

প্র ০৪অংশ ৭-এ মাত্রা বাড়লে অনুপাত 1-এর দিকে গেল। তবু হাজার মাত্রার embedding-এ nearest neighbour কাজ করে কেন?

আগে বুঝি অংশ ৭-এ কী ঘটেছিল। দুটি এলোমেলো বিন্দুর প্রতিটি ঘর 0 থেকে 1-এর মধ্যে সমানভাবে ছড়ানো হলে এক ঘরে পার্থক্যের বর্গের গড় 1/6. d মাত্রায় বর্গ-দূরত্ব হলো d-টি এমন স্বাধীন সংখ্যার যোগ, তাই তার গড় প্রায় d/6, আর দূরত্ব প্রায় √(d/6): d = 1000-এ প্রায় 12.9, যা অংশ ৭-এর run-এর nearest আর farthest-এর মাঝামাঝি। কিন্তু যোগফলের ওঠানামা (standard deviation) বাড়ে অনেক ধীরে, প্রায় স্থির থাকে। ফলে গড় দূরত্ব বড় হতে থাকে আর ছড়ানো তুলনায় ছোট হয়ে যায়: সবাই প্রায় একই দূরত্বে। এটি law of large numbers-এর একটি রূপ, যা পর্ব ৩৬-এ আসবে।

যুক্তিটির মূল শর্ত হলো “প্রতিটি মাত্রা স্বাধীন আর এলোমেলো”। বাস্তব ডেটা এমন নয়। একটি শেখা embedding-এ ঘরগুলো একে অপরের সঙ্গে গভীরভাবে সম্পর্কিত, আর ডেটা প্রায়ই অনেক কম মাত্রার একটি বাঁকানো “পৃষ্ঠের” কাছে থাকে বলে ধারণা করা হয় (manifold hypothesis, যা একটি কার্যকর ধারণা, প্রমাণিত নিয়ম নয়)। তার ওপর ডেটা দলে দলে জমাট বাঁধে: রান্নাঘরের যন্ত্রগুলো একটি দলে, শিশুর জিনিস আরেক দলে। একটি দলের ভেতরের দূরত্ব আর দলের বাইরের দূরত্বে পার্থক্য থাকে, তাই কাছের প্রতিবেশী অর্থবহ থাকে।

তবু অভিশাপের ছায়া পুরোপুরি যায় না। অনেক মাত্রায় “hubness” নামে একটি ঘটনা দেখা যায়: কিছু বিন্দু অস্বাভাবিক বেশি বিন্দুর nearest neighbour হয়ে যায়, ফলে সুপারিশে একই কয়েকটি জিনিস বারবার আসে। অপ্রয়োজনীয় বা শব্দ-ভরা (noisy) feature যোগ করলে আসল সংকেত ক্ষীণ হয়, ঠিক অংশ ৬-এর টাকার কলামের মতো। Richard Bellman শব্দটি এনেছিলেন অন্য প্রসঙ্গে (অনেক মাত্রায় একটি ছক ধরে সব সম্ভাবনা খুঁজতে খরচ সূচকীয় হারে বাড়ে), আর পরে এটি মেশিন লার্নিংয়ের নানা সমস্যার একটি সাধারণ নাম হয়ে গেছে।

মূল উপলব্ধি: মাত্রার সংখ্যা নিজে শত্রু নয়; শত্রু হলো গঠনহীন বা অপ্রাসঙ্গিক মাত্রা। দূরত্বভিত্তিক পদ্ধতির আগে প্রশ্ন করুন, ডেটায় আসল গঠন আছে কি না, আর আপনার feature সেই গঠন ধরছে কি না।

প্র ০৫Word embedding কি সমাজের পক্ষপাত শিখে ফেলে? Similarity-র জ্যামিতি দিয়ে কীভাবে তা মাপা হয়েছে?

২০১৬ সালে Tolga Bolukbasi ও সহকর্মীরা একটি প্রবন্ধ প্রকাশ করেন, যার শিরোনামই ছিল একটি analogy: “Man is to Computer Programmer as Woman is to Homemaker?” তারা সংবাদের বিপুল লেখা থেকে শেখা একটি প্রচলিত word embedding-এ দেখান, প্রশ্ন ২-এর মতো vector-গণিত দিয়ে পেশার সঙ্গে লিঙ্গের stereotype বেরিয়ে আসে। পদ্ধতিটি এই পর্বের হাতিয়ারই: “he − she” দিকের সঙ্গে প্রতিটি পেশার শব্দের vector-এর cosine মেপে দেখা কোন পেশা কোন দিকে হেলে আছে।

২০১৭ সালে Aylin Caliskan, Joanna Bryson আর Arvind Narayanan Science পত্রিকায় একটি পদ্ধতি প্রকাশ করেন (Word Embedding Association Test, WEAT), যা মনোবিজ্ঞানের Implicit Association Test-এর আদলে cosine similarity দিয়ে শব্দের দলের মধ্যে সম্পর্ক মাপে। তারা জানান, মানুষের পরীক্ষায় পরিচিত অনেক পক্ষপাত (নাম, লিঙ্গ, জাতি সম্পর্কিত) embedding-এও দেখা যায়। ব্যাখ্যাটি সহজ: embedding শেখে কোন শব্দ কোন শব্দের পাশে বসে, আর মানুষের লেখায় সমাজের stereotype-ও থাকে। মডেল কোনো “মত” রাখে না; সে ডেটার পরিসংখ্যান প্রতিফলিত করে।

সমাধান নিয়ে বিতর্ক আছে। Bolukbasi-রা লিঙ্গের দিকটি চিহ্নিত করে কিছু শব্দ থেকে সেই দিকের অংশ বাদ দেওয়ার (projection) একটি পদ্ধতি প্রস্তাব করেন। ২০১৯ সালে Hila Gonen আর Yoav Goldberg “Lipstick on a Pig” প্রবন্ধে দেখান, এই পদ্ধতির পরেও পক্ষপাতের তথ্য অন্য দিকগুলোতে ছড়িয়ে থেকে যায়, শুধু একটি মাপে লুকায়। তাই এখন অনেকে মনে করেন, ডেটা, মডেল আর ব্যবহারের প্রসঙ্গ মিলিয়ে পক্ষপাত মাপা আর নথিবদ্ধ করা দরকার। বাজারঘরের মতো একটি সুপারিশ ব্যবস্থায়ও এটি প্রাসঙ্গিক: “আপনার মতো ক্রেতারা” কারা, আর কোন দল বারবার কোন পণ্য দেখছে, তা পরীক্ষা করা দরকার। পর্ব ৭২-এ (দায়িত্বশীল AI-এর অভ্যাস) bias যাচাই আসবে।

মূল উপলব্ধি: যে জ্যামিতি “মিল” ধরে, সেই একই জ্যামিতি ডেটার পক্ষপাতও ধরে; cosine দিয়ে তা মাপা যায়, কিন্তু একটি দিক মুছে দিলেই সমস্যা শেষ হয় না।

অনুশীলন

প্রথমটি হাতে হিসাবের, দ্বিতীয়টি আউটপুট অনুমানের, তৃতীয়টি নকশা ও সমালোচনার, শেষটি কোড লেখার।

  1. হাতে হিসাব: a = [3, 4, 0], b = [4, 0, 3]. বের করুন: (ক) a · b, ‖a‖, ‖b‖ আর cosine similarity; (খ) Euclidean আর Manhattan দূরত্ব আর cosine distance; (গ) ‖a − b‖² = ‖a‖² + ‖b‖² − 2(a · b) সূত্রটি দিয়ে (খ)-এর Euclidean যাচাই।

    (ক) a · b = 3×4 + 4×0 + 0×3 = 12. ‖a‖ = √(9 + 16 + 0) = 5, ‖b‖ = √(16 + 0 + 9) = 5. cosine = 12 / 25 = 0.48 (কোণ প্রায় 61.3°)।

    (খ) a − b = [−1, 4, −3]. Euclidean = √(1 + 16 + 9) = √26 ≈ 5.10. Manhattan = 1 + 4 + 3 = 8. cosine distance = 1 − 0.48 = 0.52.

    (গ) 25 + 25 − 2×12 = 26, আর √26 ≈ 5.10: মিলেছে। সূত্রটি দেখায় dot product আর দূরত্ব একই সুতোয় বাঁধা; dot product যত বড়, দূরত্ব তত ছোট (দৈর্ঘ্য স্থির থাকলে)। (ডেস্কটপে NumPy দিয়ে মেলানো: 12.0, 0.48, 5.0990…, 8.0.)

  2. আউটপুট অনুমান করুন: চালানোর আগে প্রতিটি লাইনের আউটপুট লিখুন, তারপর যেকোনো Python-এ চালিয়ে মিলিয়ে দেখুন।
    import numpy as np
    a = np.array([1.0, 0.0])
    b = np.array([5.0, 0.0])
    c = np.array([0.0, 2.0])
    cos = lambda x, y: x @ y / (np.linalg.norm(x) * np.linalg.norm(y))
    print(cos(a, b), cos(a, c), cos(a, -a))
    print(np.linalg.norm(a - b), np.linalg.norm(a - c).round(3))
    print(np.linalg.norm(3 * c), np.linalg.norm(c, 1), np.linalg.norm(c, np.inf))
    1.0 0.0 -1.0
    4.0 2.236
    6.0 2.0 2.0

    a আর b একই দিকে, তাই cosine 1 (দৈর্ঘ্য 1 আর 5 হলেও); a আর c সমকোণে, তাই 0; a আর −a উল্টো, তাই −1. দূরত্ব: a − b = [−4, 0], দৈর্ঘ্য 4; a − c = [1, −2], দৈর্ঘ্য √5 ≈ 2.236. শেষ লাইন: 3c = [0, 6], দৈর্ঘ্য 6; c-এর L1 = 2, L∞ = 2. মানে cosine-এ a-র কাছে b, কিন্তু Euclidean-এ c (2.236 < 4). (আউটপুটটি ডেস্কটপে NumPy 2.5.4-এ চালিয়ে মেলানো।)

  3. নকশা ও সমালোচনা: বাজারঘরের একজন সহকর্মী “similar products” ফিচার বানাতে নিচের পরিকল্পনা দিয়েছেন। অন্তত চারটি দুর্বলতা চিহ্নিত করুন আর ভালো একটি পথ বলুন। পরিকল্পনা: “প্রতিটি পণ্যকে [দাম (টাকা), গড় রেটিং (1–5), মাসে বিক্রি] vector বানাব। কাঁচা মানে Euclidean দূরত্ব নিয়ে প্রতিটি পণ্যের সবচেয়ে কাছের ৩টি দেখাব। মিল যত কম হোক, ৩টি সবসময় দেখাব। কাজ করছে কি না, তা দেখতে আমি নিজে ৫টি পণ্যের ফল দেখব।”
    • Scaling নেই: দামের কলাম (শত থেকে হাজার টাকা) দূরত্বে আধিপত্য করবে, রেটিং (1–5) প্রায় কিছুই বলবে না; অংশ ৬-এর মতো ভুল প্রতিবেশী আসবে। অন্তত standardise করা দরকার, μ আর σ নির্দিষ্ট ডেটা থেকে।
    • Feature-গুলো “মিল” ধরে না: একই দাম আর রেটিংয়ের একটি চার্জার আর একটি ডালের প্যাকেট কাছে চলে আসবে। পণ্যের ধরন, বিবরণ বা একসঙ্গে কেনার তথ্য থেকে embedding (পর্ব ৬৯) বেশি অর্থবহ।
    • Threshold নেই: top-3 সবসময় ৩টি দেয়, মিল খুব কম হলেও; অংশ ৮-এর run-এ চার্জারের তৃতীয় “কাছের” পণ্যের cosine ছিল মাত্র ০.১৭৯-এর মতো। একটি ন্যূনতম similarity রাখা, আর না পেলে কম দেখানো ভালো।
    • মূল্যায়ন দুর্বল: ৫টি পণ্য চোখে দেখা যথেষ্ট নয়। আগের কেনাকাটার ডেটায় যাচাই (যেমন সুপারিশ করা পণ্য পরে আসলে কেনা হয়েছে কি না), একটি সরল baseline (“সবচেয়ে জনপ্রিয় পণ্য”) এর সঙ্গে তুলনা, আর সম্ভব হলে A/B test.
    • শূন্য আর নতুন পণ্য: নতুন পণ্যের বিক্রি 0, রেটিং নেই; missing value আর cold start-এর পরিকল্পনা লাগবে।
  4. কোড লিখুন: একটি function top_k_cosine(Q, X, k) লিখুন, যেখানে X-এর shape (n, d) (n-টি item) আর Q-এর shape (m, d) (m-টি query). প্রতিটি query-র জন্য সবচেয়ে বড় cosine-এর k-টি item-এর index ফেরত দিন (shape (m, k)), loop ছাড়া। শূন্য-দৈর্ঘ্যের সারি থাকলে ValueError দিন।
    import numpy as np
    
    def top_k_cosine(Q, X, k):
        nq = np.linalg.norm(Q, axis=1, keepdims=True)   # (m, 1)
        nx = np.linalg.norm(X, axis=1, keepdims=True)   # (n, 1)
        if (nq == 0).any() or (nx == 0).any():
            raise ValueError("zero-length row")
        S = (Q / nq) @ (X / nx).T                       # (m, n)
        return np.argsort(-S, axis=1)[:, :k]            # (m, k)
    
    X = np.array([[1, 0], [0.9, 0.1], [0, 1], [-1, 0]])
    Q = np.array([[2, 0], [0, 3]])
    print(top_k_cosine(Q, X, 2))

    প্রত্যাশিত ফল [[0 1] [2 1]]: প্রথম query [2, 0]-এর দিকে সবচেয়ে কাছে item 0 (cos 1), তারপর item 1 (cos ≈ 0.994); দ্বিতীয় query [0, 3]-এর কাছে item 2 (cos 1), তারপর item 1 (cos ≈ 0.110). Query-এর দৈর্ঘ্য (2 বা 3) ফলে কোনো প্রভাব ফেলেনি, এটিই cosine. বড় n-এ পুরো সারি সাজানোর বদলে np.argpartition দিয়ে আগে k-টি বেছে নিয়ে শুধু সেগুলো সাজানো দ্রুত (পর্ব ১৪)। (কোডটি ডেস্কটপে NumPy 2.5.4-এ চালিয়ে মেলানো।)

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

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

ক্রমপণ্যের feature থেকে cosine দিয়ে “এর মতো পণ্য” খোঁজার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📋 প্রতিটি পণ্যকে একই ক্রমের feature vector বানানো
  2. ⚖️ কলামগুলো scale করা (μ আর σ শুধু train ডেটা থেকে)
  3. 📏 প্রতিটি সারিকে unit vector বানানো, শূন্য-দৈর্ঘ্য পরীক্ষা করে
  4. ✖️ U @ U.T দিয়ে similarity matrix বানানো
  5. 🔽 বড় থেকে ছোট সাজিয়ে নিজেকে বাদ দিয়ে top-k নেওয়া
  6. 👀 কয়েকটি ফল চোখে আর একটি baseline-এর সঙ্গে যাচাই

a = [1, 2, 3] আর b = [4, −5, 6] হলে dot product a · b কত?

  • 11
  • 32
  • 12
  • [4, −10, 18]

একটি vector a = [2, 1]-কে 10 দিয়ে গুণ করে b = [20, 10] পাওয়া গেল। a আর b-এর cosine similarity কত?

  • 1
  • 10
  • 0
  • 0.1

v = [2, 3, 6]-এর unit vector কোনটি?

  • [2/11, 3/11, 6/11]
  • [2/7, 3/7, 6/7]
  • [2/49, 3/49, 6/49]
  • [1/3, 1/2, 1]

ক্রেতার feature: [বছরে অর্ডার (2–12), গড় খরচ টাকায় (800–1400), শেষ অর্ডারের পর দিন (5–90)]. কাঁচা মানে Euclidean দূরত্ব নিলে কী সমস্যা সবচেয়ে বেশি?

  • ৩ মাত্রায় Euclidean দূরত্ব সংজ্ঞায়িত নয়
  • অর্ডারের কলাম দূরত্বে আধিপত্য করবে
  • দূরত্ব ঋণাত্মক আসতে পারে
  • বড় এককের কলাম (টাকা, দিন) দূরত্ব ঠিক করে দেবে, তাই আগে scaling দরকার

দুটি unit vector u আর v-এর cosine similarity 0.5. তাদের Euclidean দূরত্ব কত?

  • 0.5
  • 2
  • 1
  • √2 ≈ 1.414

স্বাধীন, সমানভাবে এলোমেলো ঘরের ডেটায় মাত্রা d বাড়তে থাকলে একটি query থেকে (সবচেয়ে কাছের দূরত্ব ÷ সবচেয়ে দূরের দূরত্ব) অনুপাতের কী হয়?

  • 0-এর দিকে নামে, কাছের প্রতিবেশী আরও স্পষ্ট হয়
  • 1-এর দিকে যায়, দূরত্বগুলো প্রায় সমান হয়ে আসে
  • মাত্রার ওপর নির্ভর করে না
  • 1 ছাড়িয়ে বাড়তে থাকে

SciPy-র scipy.spatial.distance.cosine(a, b) কী ফেরত দেয়?

  • cosine distance, মানে 1 − cosine similarity
  • cosine similarity
  • দুটি vector-এর মাঝের কোণ (ডিগ্রিতে)
  • dot product

শব্দকোষ

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

মনে রাখুন

  1. Vector = ক্রম মেনে সাজানো সংখ্যা, যা একই সঙ্গে একটি তীর (দিক আর দৈর্ঘ্য) আর ডেটার একটি সারি; যোগ মানে তীর জোড়া লাগানো, ধনাত্মক scalar গুণ দিক বদলায় না।
  2. Norm মানে দৈর্ঘ্য: L2 পিথাগোরাস, L1 ঘরের absolute মানের যোগ, L∞ সবচেয়ে বড় ঘর; v / ‖v‖ দিলে দৈর্ঘ্য ১-এর unit vector, তবে শূন্য vector-এ নয়।
  3. a · b = ∑ aibi = ‖a‖‖b‖ cos θ; ধনাত্মক মানে একই দিকে, 0 মানে সমকোণ, ঋণাত্মক মানে উল্টো।
  4. Cosine similarity দিক মাপে, দৈর্ঘ্য নয়; তাই Euclidean আর cosine ভিন্ন প্রতিবেশী দিতে পারে। দূরত্বের আগে কলামগুলো scale করুন, আর মনে রাখুন এলোমেলো অনেক মাত্রায় দূরত্ব প্রায় সমান হয়ে আসে।
  5. Embedding অর্থকে vector বানায়, যাতে মিল মানে ছোট কোণ; normalise করে U @ U.T দিলে similarity matrix, আর top-k দিলে “এর মতো” জিনিস। বিখ্যাত analogy উদাহরণগুলো প্রবণতা, নিয়ম নয়।

পরের পর্বে: ঢাল ধরে নিচে নামা। Derivative মানে কী, অনেক চলকের gradient কোন দিকে নির্দেশ করে, আর gradient descent কীভাবে ধাপে ধাপে একটি মডেলের ভুল কমায়; এই পর্বের vector সেখানে gradient হয়ে ফিরবে।

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

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