Part 02 · Machine Learning

Clustering

K-meansDBSCANDistance metricsChoosing k

কোনো label নেই: k-means আর DBSCAN দিয়ে একই রকম জিনিস এক group এ রাখা, কয়টা group হবে সেটা ঠিক করা, আর clustering কীভাবে চুপচাপ ভুল বোঝায়।

⏱ 1-2 ঘণ্টা · Station 10 / 92

জিনিসটা কী, কেন দরকার

একটা supermarket chain এর দশ লাখ customer, কিন্তু কার সাথে কীভাবে কথা বলবে কোনো ধারণা নেই। কেউ কাউকে "দাম খোঁজা মানুষ" বা "সপ্তাহের বাজার করা পরিবার" label দেয়নি। অথচ data র ভেতরে দলগুলো আছেই: কেউ কম আসে কিন্তু অনেক কেনে, কেউ রোজ এসে দুই একটা জিনিস নেয়। Clustering label ছাড়াই এই দলগুলো খুঁজে বের করে। Unsupervised learning এর সবচেয়ে common রূপ এটাই।

Team গুলো এটা দিয়ে customer ভাগ করে, একরকম support ticket এক জায়গায় আনে, ছবির রং কমায়, document কে topic ধরে সাজায়, আর অস্বাভাবিকটাকে ধরে (fraud, নষ্ট sensor)। সমস্যা হলো: মিলিয়ে দেখার মতো কোনো সঠিক answer নেই, তাই clustering চুপচাপ এমন দল দিতে পারে যেগুলো দেখতে সুন্দর কিন্তু কোনো মানে নেই। এই lesson এ দেখবে এটা কীভাবে কাজ করে, আর কীভাবে ধোঁকা দেয়।

ভেতরে কীভাবে চলে

আগে দূরত্ব। Clustering এর দরকার দুইটা জিনিস কতটা দূরে সেটা বলার একটা উপায়। সংখ্যার জন্য default হলো Euclidean distance (সোজা লাইনে দূরত্ব)। Text embedding এর জন্য নেবে cosine distance (Part 5)। আলাদা scale এর feature আগে scale করে নিতে হয়, নইলে সবচেয়ে বড় সংখ্যাগুলোই সব ঠিক করে দেয় (পরের lesson)।

k-means দুইটা ধাপ বারবার করে k টা দল খোঁজে:

  1. Assign: প্রতিটা point তার সবচেয়ে কাছের center এ যোগ দেয়।
  2. Update: প্রতিটা center তার point গুলোর গড়ে (mean) সরে যায়।

যতক্ষণ কিছু না বদলায়, চলতে থাকে। দ্রুত আর সহজ, কিন্তু এর কিছু জেদ আছে: ধরে নেয় দলগুলো গোল আর কাছাকাছি সাইজের, k তোমাকে বলে দিতে হয়, আর প্রতিটা point কে কোনো না কোনো cluster এ থাকতেই হবে, যে কোথাও খাপ খায় না সেটাকেও।

k বাছাই। Inertia মাপো, মানে প্রতিটা point থেকে তার center এর দূরত্বের বর্গের যোগফল। k বাড়ালে এটা সবসময় কমে (k = point এর সংখ্যা হলে 0), তাই খোঁজো elbow, যেখানে আরেকটা cluster যোগ করলে আর তেমন লাভ হয় না। আরো ভালো হলো জিজ্ঞেস করা দলগুলো কাজের কিনা: marketing team কাজে লাগাতে পারে এমন তিনটা segment, পারে না এমন সাতটার চেয়ে ভালো।

DBSCAN অন্যভাবে দেখে: cluster মানে একটা ঘন জায়গা। যে point এর eps দূরত্বের মধ্যে অন্তত min টা প্রতিবেশী আছে সেটা core point; একে অপরের নাগাল পায় এমন core point গুলো মিলে একটা cluster; ফাঁকা জায়গার point গুলো noise। k বলে দিতে হয় না, অদ্ভুত আকারও সামলায়, আর জোর করে দলে না ঢুকিয়ে বলে "এটা কোথাও খাপ খায় না"। তবে eps বাছতে হয়, যেটা তোমার data র scale এর উপর নির্ভর করে।

আগে অনুমান করো

Customer দের উপর k = 3 দিয়ে k-means চালিয়ে তিনটা cluster পেলে। কোন কথাটা নিশ্চিন্তে বলা যায়?

ছবিতে দেখো

visits per monthbasket (×100 taka)C1C2C3start: 3 centers on spread-out customersrepeat: join nearest center, move center to the meanthe outlier keeps C3; two real groups share C1
k = 3 দিয়ে k-means। Center গুলো শুরু হয় তিনজন দূরে দূরে থাকা customer এর উপর, তারপর বারবার: প্রতিটা customer সবচেয়ে কাছের center এ যোগ দেয়, প্রতিটা center তার দলের গড়ে সরে যায়। একজন অস্বাভাবিক বড় ক্রেতা C3 কে একাই দখল করে রাখে, ফলে দুইটা আসল দল, কম আসা ছোট ক্রেতা আর ঘন ঘন আসা ছোট ক্রেতা, একসাথে C1 এর ভাগে পড়ে।

Code এ customer এর দল

ষোলজন customer, তিনটা পরিষ্কার দল আর একজন অস্বাভাবিক বড় ক্রেতা। k = 1 থেকে 5 পর্যন্ত k-means, k = 3 আর k = 4 এর cluster, তারপর একই data তে DBSCAN। দুইটা program একই output দেয়।

import math

# Shop customers: (visits per month, average basket in hundred taka). No labels.
PTS = [(2, 3), (3, 2), (2, 2), (3, 3), (4, 2),          # rare, small baskets
       (10, 3), (11, 4), (12, 3), (10, 4), (11, 2),     # frequent, small baskets
       (3, 12), (2, 13), (4, 14), (3, 13), (4, 12),     # rare, big baskets
       (20, 20)]                                         # one unusual customer

def dist(a, b):
    return math.hypot(a[0] - b[0], a[1] - b[1])

def kmeans(k, iters=20):
    # start from spread-out points: the first point, then repeatedly the farthest from those chosen
    centers = [PTS[0]]
    while len(centers) < k:
        centers.append(max(PTS, key=lambda p: min(dist(p, c) for c in centers)))
    for _ in range(iters):
        groups = [[] for _ in range(k)]
        for p in PTS:                                   # step 1: assign each point to its nearest center
            groups[min(range(k), key=lambda i: dist(p, centers[i]))].append(p)
        centers = [(sum(x for x, _ in g) / len(g), sum(y for _, y in g) / len(g)) for g in groups]  # step 2: move centers
    inertia = sum(min(dist(p, c) ** 2 for c in centers) for p in PTS)
    return centers, groups, inertia

print("k  inertia   (sum of squared distances to the nearest center)")
for k in range(1, 6):
    print(f"{k}  {kmeans(k)[2]:8.1f}")

for k in (3, 4):
    centers, groups, _ = kmeans(k)
    print(f"\nk = {k}:")
    for c, g in sorted(zip(centers, groups)):
        print(f"  center ({c[0]:5.2f}, {c[1]:5.2f})  {len(g)} customers")

# DBSCAN: a point with at least MIN neighbours within EPS starts or grows a cluster; others are noise.
EPS, MIN = 2.0, 3
labels = [None] * len(PTS)
cluster = 0
for i, p in enumerate(PTS):
    if labels[i] is not None:
        continue
    near = [j for j, q in enumerate(PTS) if dist(p, q) <= EPS]
    if len(near) < MIN:
        labels[i] = -1                                  # noise for now
        continue
    cluster += 1
    queue = near
    while queue:
        j = queue.pop()
        if labels[j] in (None, -1):
            labels[j] = cluster
            more = [m for m, q in enumerate(PTS) if dist(PTS[j], q) <= EPS]
            if len(more) >= MIN:
                queue.extend(m for m in more if labels[m] in (None, -1))
print(f"\nDBSCAN eps={EPS} min={MIN}: {cluster} clusters")
for c in sorted(set(labels)):
    members = [PTS[i] for i in range(len(PTS)) if labels[i] == c]
    print(f"  {'noise' if c == -1 else 'cluster ' + str(c)}: {len(members)} customers, first {members[0]}")

Output:

k  inertia   (sum of squared distances to the nearest center)
1     928.0
2     553.3
3     176.8
4      15.2
5      13.0

k = 3:
  center ( 3.20, 12.80)  5 customers
  center ( 6.80,  2.80)  10 customers
  center (20.00, 20.00)  1 customers

k = 4:
  center ( 2.80,  2.40)  5 customers
  center ( 3.20, 12.80)  5 customers
  center (10.80,  3.20)  5 customers
  center (20.00, 20.00)  1 customers

DBSCAN eps=2.0 min=3: 3 clusters
  noise: 1 customers, first (20, 20)
  cluster 1: 5 customers, first (2, 3)
  cluster 2: 5 customers, first (10, 3)
  cluster 3: 5 customers, first (3, 12)

কী দেখলাম:

  • Inertia k = 4 পর্যন্ত হুড়মুড় করে কমে (15.2), তারপর k = 5 এ প্রায় নড়ে না (13.0)। Elbow হলো 4 এ।
  • k = 3 এ outlier জিতে যায়। (20, 20) এর একজন customer পুরো একটা center একাই পায়, আর ঘন ঘন আসা ছোট ক্রেতা আর কম আসা ছোট ক্রেতা মিলে 10 জনের একটা cluster হয়, center (6.8, 2.8) এ, যেখানে আসলে কোনো customer ই নেই। Marketing team এমন একটা customer এর জন্য campaign বানাত যার অস্তিত্বই নেই।
  • k = 4 এ তিনটা আসল দল পরিষ্কারভাবে আসে, আর outlier একা একটা cluster এ।
  • DBSCAN ঘনত্ব দিয়ে তিনটা আসল দল খুঁজে পায়, আর বড় ক্রেতাকে noise বলে, কয়টা দল খুঁজতে হবে না বলেই। সাধারণত এটাই চাও: outlier কে আলাদা করে জানাও, হয়তো সে তোমার সেরা customer, হয়তো fraud।

Production এ যা মাথায় রাখবে

  • আগে feature scale করো। Basket যদি টাকায় হয় (হাজার) আর visit এক অঙ্কের, তাহলে দূরত্ব মানে প্রায় শুধু basket। Clustering এর আগে প্রতিটা feature standardise করো (পরের lesson)।
  • k-means কয়েকবার চালাও। আলাদা শুরু আলাদা answer দিতে পারে। Library গুলো k-means++ দিয়ে শুরু করে আর কয়েকটা run এর সেরাটা রাখে (n_init)। Random seed fix করো, যাতে প্রতিবার চালালে segment গুলো এলোমেলো না হয়।
  • Cluster সরে যায়। Customer এর আচরণ বদলায়; এই মাসের segment 2 আর পরের মাসের segment 2 এক না, আর run ভেদে cluster এর ID অদলবদল হতে পারে। কখনো "cluster 3 = VIP" hard-code করবে না; center দেখে cluster মেলাও।
  • Scale আর speed। সাধারণ k-means হলো O(point × k × iteration), লাখ লাখ row এও চলে। Spatial index ছাড়া DBSCAN এর প্রতিবেশী খোঁজা ধীর। অনেক dimension এ (embedding) দূরত্বের মানে কমে যায়; আগে dimension কমাও বা cosine নাও।
  • মানুষ দিয়ে যাচাই করো। প্রতিটা cluster থেকে কিছু sample domain জানা কাউকে দেখাও। সে যদি একটা cluster এর নাম দিতে না পারে, সম্ভবত ওটা কোনো cluster ই না।

এক কথায়

Clustering label ছাড়া data কে দূরত্ব দিয়ে দলে ভাগ করে: k-means পালা করে "সবচেয়ে কাছের center এ যাও" আর "center কে গড়ে সরাও" করে, k লাগে আর outlier এ টান খায়; DBSCAN ঘনত্ব দিয়ে দল বানায় আর ফাঁকা জায়গার point কে noise বলে। দলগুলোর আসলে মানে আছে কিনা সবসময় check করো।

নিজে করে দেখো

Data থেকে বড় ক্রেতা (20, 20) কে বাদ দিয়ে আবার চালাও। Elbow এখন কোথায়, আর k = 3 এর cluster গুলো কেমন? তারপর তাকে ফিরিয়ে এনে eps = 1.0 দিয়ে DBSCAN চালাও। কী বদলায়, আর কেন?

Answer

Outlier ছাড়া elbow সরে k = 3 এ আসে (inertia 553 → 177 → 15.2, তারপর k = 4 এ 13.0), আর k = 3 এর cluster গুলো হুবহু তিনটা আসল দল: k = 4 এ নিয়ে যাচ্ছিল outlier টাই। eps = 1.0 এ DBSCAN এখনো 3 টা cluster পায়, কিন্তু 1 জনের বদলে 4 জন noise হয়ে যায়: বড় ক্রেতা, সাথে (12, 3) এর মতো কিনারার customer রা, যাদের সবচেয়ে কাছের প্রতিবেশী 1 এর একটু বেশি দূরে। দলগুলো ছোট হয়ে 5, 3 আর 4 জন। eps কে তোমার data র ফাঁকের সাথে মিলতে হবে; বেশি ছোট হলে আসল সদস্যরা noise হয়ে যায়, বেশি বড় হলে আলাদা দল মিশে যায়।

Station stamp

Chapter শেষ করো

পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।

Pro নিয়ে XP পাও →