پیاده سازی الگوریتم K-means++ در پایتون — راهنمای گام به گام
در آموزشهای پیشین مجله فرادس، با پیاده سازی الگوریتم خوشه بندی K-means در پایتون آشنا شدیم. در این آموزش، روش گام به گام پیادهسازی نوع خاصی از این الگوریتم خوشهبندی، یعنی پیاده سازی الگوریتم K-means++ در پایتون را بیان میکنیم.
الگوریتم K-means++
همانطور که میدانیم، در مسئله K-means تابع هزینه به شکل زیر تعریف میشود:
به عبارتی این تابع حاصل جمع فاصله اقلیدسی هر داده از مرکز خوشه مربوطه را نشان میدهد که به آن Within Cluster Sum of Squares گفته میشود.
بهینهسازی موقعیت خوشهها برای دست یافتن به کمترین مقدار تابع هزینه، یک مسئله NP-Hard است و بهلحاظ محاسباتی هزینهبر است. به این دلیل از الگوریتمهایی برای یافتن بهینههای محلی (Local Optimum) استفاده میشود. الگورتم لوید (Lloyd Algorithm) که در آموزشهای قبلی پیادهسازی شد نیز مثالی از این روشها است.
همانطور که گفته شد، این الگوریتمها اغلب بهجای یافتن بهینه سراسری (Global Minimum)، بهینه محلی را مییابند. به همین دلیل، نتایج حاصل نیز به شرایط شروع (Initialization) بسیار وابسته هستند.
برای حل این مشکل، راهحلهایی ارائه شدهاند که یکی از آنها، استفاده از روش K-means++ است.
در این روش، اولین مرکز خوشه به صورت تصادفی از بین دادهها انتخاب میشود. سایر مراکز نیز بهصورت تصادفی از بین دادهها انتخاب میشوند، با این تفاوت که برای دادههایی با فاصله بیشتر از مراکز قبلی، احتمال انتخاب بیشتری داده میشود. به این ترتیب، احتمال انتخاب مراکز خوشه نزدیک به هم کاهش مییابد.
این الگوریتم را میتوان در مراحل زیر توصیف کرد:
۱. مرکز خوشه اول () بهصورت تصادفی از بین دادهها انتخاب میشود.
۲. مراحل زیر به تعداد بار تکرار میشود:
a. فاصله همه دادهها از تمامی مراکز خوشه محاسبه و کمترین آن بهصورت زیر ذخیره میشود:
b. احتمال انتخاب متناظر برای هر داده به شکل زیر تعریف میشود:
c. یک داده با توجه به احتمالهای محاسبه شده، انتخاب میشود.
۴. مراکز خوشه به نقاط شروع انتخاب میشوند.
۵. لگوریتم Lloyd اجرا میشود.
به این ترتیب، مراحل کار الگوریتم کامل بیان میشود.
برای یادگیری برنامهنویسی با زبان پایتون، پیشنهاد میکنیم به مجموعه آموزشهای مقدماتی تا پیشرفته پایتون فرادرس مراجعه کنید که لینک آن در ادامه آورده شده است.
بررسی وابستگی الگوریتم Lloyd به شرایط اولیه
حال وارد محیط برنامهنویسی میشویم و الگوریتم نوشتهشده در مطلب قبلی با عنوان «پیاده سازی الگوریتم خوشه بندی K-means در پایتون — راهنمای گام به گام» را که لینک آن را در ابتدای متن آوردیم، به شکل زیر بازنویسی میکنیم:
1import numpy as np
2import matplotlib.pyplot as plt
3
4def CreateDataset(Cs:np.ndarray, nD:int, S:float):
5 nC = Cs.shape[0]
6 N = nD * nC
7 X = np.zeros((N, 2))
8 for i in range(nC):
9 for j in range(nD):
10 X[nC*j + i, 0] = Cs[i, 0] + np.random.randn() / S
11 X[nC*j + i, 1] = Cs[i, 1] + np.random.randn() / S
12 return X
13
14def GetClusters(Centers:np.ndarray, X:np.ndarray):
15 nClusters = Centers.shape[0]
16 Clusters = []
17 for i in range(nClusters):
18 Clusters.append([])
19 for xj in X:
20 d = np.zeros(nClusters)
21 for i in range(nClusters):
22 d[i] = np.linalg.norm(xj - Centers[i])
23 a = np.argmin(d)
24 Clusters[a].append(xj)
25 for i in range(nClusters):
26 Clusters[i] = np.array(Clusters[i])
27 return Clusters
28
29def UpdateCenters(Centers:np.ndarray, Clusters:list):
30 nClusters = Centers.shape[0]
31 for i in range(nClusters):
32 if np.size(Clusters[i]) != 0:
33 Centers[i] = np.mean(Clusters[i], axis = 0)
34 return Centers
ابتدا دادهها را ایجاد و نمایش میدهیم:
1np.random.seed(0)
2plt.style.use('ggplot')
3
4Cs = np.array([[-0.5, +1.0], [+1.6, -0.1],
5 [-0.9, -1.7], [-2.9, +2.3],
6 [+0.7, +2.2], [-2.2, +0.3]])
7nD = 200
8S = 2.5
9
10X = CreateDataset(Cs, nD, S)
11
12plt.scatter(X[:, 0], X[:, 1], c='k', s=9, label='Data')
13plt.scatter(Cs[:,0], Cs[:, 1], c='r', s=100, marker = '*', label='Center')
14plt.title('Scatter Plot of Data')
15plt.xlabel('X1')
16plt.ylabel('X2')
17plt.legend()
18plt.show()
که شکل زیر خواهیم داشت.
حال تابع هزینه را در قالب یک تابع تعریف میکنیم. در ورودی این تابع دادهها و مراکز خوشهها را دریافت و در خروجی Within Cluster Sum of Squares را برمیگردانیم:
1def Cost(Centers:np.ndarray, X:np.ndarray):
2 WCSS = 0
3 for i in Centers:
4 d = []
5 for j in X:
6 d.append(np.linalg.norm(i - j, ord=2)**2)
7 WCSS += min(d)
8 return WCSS
حال برنامه را ۱۰۰ بار با شرایط شروع متفاوت اجرا میکنیم و مقدار تابع هزینه را ذخیره میکنیم:
1nRun = 100
2nClusters = 5
3nIter = 10
4
5Costs = np.zeros(nRun)
6
7for i in range(nRun):
8 Centers = np.random.uniform(-2, +2, (nClusters, 2))
9 Clusters = GetClusters(Centers, X)
10 for _ in range (nIter):
11 Centers = UpdateCenters(Centers, Clusters)
12 Clusters = GetClusters(Centers, X)
13 Costs[i] = Cost(Centers, X)
اکنون یک نمودار هیستوگرام (Histogram) برای مقدار تابع هزینه رسم میکنیم:
1plt.hist(Costs, bins=11, color='crimson', alpha=0.5)
2plt.title('K-Means Algorithm Final Cost Function Value With Different Initilizations')
3plt.xlabel('Cost')
4plt.ylabel('Frequency')
5plt.show()
توجه داشته باشید که کم بودن تعداد bins توزیع را با جزئیات مناسب نشان نمیدهد و زیاد بودن آن باعث ایجاد ناپیوستگیهایی در نمودار میشود و باز هم مطلوب نیست. به همین دلیل، باید تعداد آن با توجه به اندازه داده و در بازه مطلوب قرار گرفته باشد.
پس از اجرای کد فوق نمودار زیر حاصل خواهد شد.
به این ترتیب، مشاهده میکنیم که حدود ۷۰ بار اجرا، به هزینهای کمتر از ۰٫۰۲ ختم شده است. در مقابل، برای برخی اجراها، تابع هزینه به بیشتر ۰٫۱۴ رسیده است که ۳۵ برابر کمترین مقدار است. به این ترتیب، بهسادگی وابستگی الگوریتم Lloyd به شرایط اولیه مشاهده میشود.
پیاده سازی الگوریتم K-means++ در پایتون
برای پیادهسازی الگوریتم K-means++ در پایتون یک تابع ایجاد میکنیم که در ورودی تعداد مرکز و مجموعه داده را دریافت میکند:
1def KMeansPP(nClusters:int, X:np.ndarray):
حال تعداد داده را استخراج و سپس ماتریسی خالی برای ذخیره مراکز دسته انتخاب شده ایجاد میکنیم:
1def KMeansPP(nClusters:int, X:np.ndarray):
2 nD = X.shape[0]
3 Centers = np.zeros((nClusters, 2))
حال اولین مرکز دسته را بهصورت تصادفی از مجموعه داده انتخاب میکنیم:
1def KMeansPP(nClusters:int, X:np.ndarray):
2 nD = X.shape[0]
3 Centers = np.zeros((nClusters, 2))
4 Centers[0] = X[np.random.randint(nD)]
حال یک حلقه برای بار تکرار کد مینویسیم:
1def KMeansPP(nClusters:int, X:np.ndarray):
2 nD = X.shape[0]
3 Centers = np.zeros((nClusters, 2))
4 Centers[0] = X[np.random.randint(nD)]
5 for i in range(nClusters - 1):
حال باید ماتریس فاصله هر داده از نزدیکترین مرکز را محاسبه و ذخیره کنیم، برای این کار، ابتدا ماتریسی خالی برای آن ایجاد میکنیم:
1def KMeansPP(nClusters:int, X:np.ndarray):
2 nD = X.shape[0]
3 Centers = np.zeros((nClusters, 2))
4 Centers[0] = X[np.random.randint(nD)]
5 for i in range(nClusters - 1):
6 D = np.zeros(nD)
حال یه حلقه ایجاد میکنیم تا برای هر داده فاصله را محاسبه کنیم:
1def KMeansPP(nClusters:int, X:np.ndarray):
2 nD = X.shape[0]
3 Centers = np.zeros((nClusters, 2))
4 Centers[0] = X[np.random.randint(nD)]
5 for i in range(nClusters - 1):
6 D = np.zeros(nD)
7 for j in range(nD):
در این مرحله، یک لیست خالی ایجاد میکنیم تا فاصله داده از هر مرکز خوشه را به آن اضافه کنیم:
1def KMeansPP(nClusters:int, X:np.ndarray):
2 nD = X.shape[0]
3 Centers = np.zeros((nClusters, 2))
4 Centers[0] = X[np.random.randint(nD)]
5 for i in range(nClusters - 1):
6 D = np.zeros(nD)
7 for j in range(nD):
8 d = []
حال میتوانیم یک حلقه برای تمامی مراکز خوشه ایجاد و فاصله را به لیست ایجادشده اضافه کنیم. توجه داشته باشید که با هر بار اجرای حلقه بیرونی، تعداد مراکز خوشه افزایش مییابد؛ به این دلیل، باید تعداد مراکز از متغیر تبعیت کند:
1def KMeansPP(nClusters:int, X:np.ndarray):
2 nD = X.shape[0]
3 Centers = np.zeros((nClusters, 2))
4 Centers[0] = X[np.random.randint(nD)]
5 for i in range(nClusters - 1):
6 D = np.zeros(nD)
7 for j in range(nD):
8 d = []
9 for k in range(i+1):
10 d.append(np.linalg.norm(X[j] - Centers[k], ord=2))
حال باید کمترین فاصله مشاهده شده را به آرایه D اضافه کنیم:
1def KMeansPP(nClusters:int, X:np.ndarray):
2 nD = X.shape[0]
3 Centers = np.zeros((nClusters, 2))
4 Centers[0] = X[np.random.randint(nD)]
5 for i in range(nClusters - 1):
6 D = np.zeros(nD)
7 for j in range(nD):
8 d = []
9 for k in range(i+1):
10 d.append(np.linalg.norm(X[j] - Centers[k], ord=2))
11 D[j] = min(d)
به این ترتیب، کمترین فاصله هر داده از مراکز محاسبه میشود. با توجه به فرمول، باید از توان دوم فاصلهها استفاده کنیم؛ بنابراین، بهشکل زیر توان دوم ماتریس D را محاسبه میکنیم:
1def KMeansPP(nClusters:int, X:np.ndarray):
2 nD = X.shape[0]
3 Centers = np.zeros((nClusters, 2))
4 Centers[0] = X[np.random.randint(nD)]
5 for i in range(nClusters - 1):
6 D = np.zeros(nD)
7 for j in range(nD):
8 d = []
9 for k in range(i+1):
10 d.append(np.linalg.norm(X[j] - Centers[k], ord=2))
11 D[j] = min(d)
12 D2 = np.power(D, 2)
حال مجموع مقادیر آرایه D2 را محاسبه کرده و در نهایت احتمال متناظر با هر داده را مییابیم:
1def KMeansPP(nClusters:int, X:np.ndarray):
2 nD = X.shape[0]
3 Centers = np.zeros((nClusters, 2))
4 Centers[0] = X[np.random.randint(nD)]
5 for i in range(nClusters - 1):
6 D = np.zeros(nD)
7 for j in range(nD):
8 d = []
9 for k in range(i+1):
10 d.append(np.linalg.norm(X[j] - Centers[k], ord=2))
11 D[j] = min(d)
12 D2 = np.power(D, 2)
13 SD2 = np.sum(D2)
14 P = D2 / SD2
به این ترتیب، احتمال انتخاب هر داده بهعنوان مرکز بعدی محاسبه شد. حال باید با استفاده از Random Choice یک داده را انتخاب کنیم:
1def KMeansPP(nClusters:int, X:np.ndarray):
2 nD = X.shape[0]
3 Centers = np.zeros((nClusters, 2))
4 Centers[0] = X[np.random.randint(nD)]
5 for i in range(nClusters - 1):
6 D = np.zeros(nD)
7 for j in range(nD):
8 d = []
9 for k in range(i+1):
10 d.append(np.linalg.norm(X[j] - Centers[k], ord=2))
11 D[j] = min(d)
12 D2 = np.power(D, 2)
13 SD2 = np.sum(D2)
14 P = D2 / SD2
15 Index = np.random.choice(nD, size=1, p=P)[0]
16 Centers[i+1] = X[Index]
به این ترتیب، داده مورد نظر با توجه به احتمالهای محاسبهشده، انتخاب میشود. توجه داشته باشید که تابع np.random.choice در خروجی یک آرایه برمیگرداند که با توجه به اینکه تنها یک عدد در آن وجود دارد، تنها عضو اول آن را انتخاب میکنیم.
در نهایت، نیز مراکز خوشهها را در خروجی برمیگردانیم و تابع نهایی شکل زیر را به خود میگیرد:
1def KMeansPP(nClusters:int, X:np.ndarray):
2 nD = X.shape[0]
3 Centers = np.zeros((nClusters, 2))
4 Centers[0] = X[np.random.randint(nD)]
5 for i in range(nClusters - 1):
6 D = np.zeros(nD)
7 for j in range(nD):
8 d = []
9 for k in range(i+1):
10 d.append(np.linalg.norm(X[j] - Centers[k], ord=2))
11 D[j] = min(d)
12 D2 = np.power(D, 2)
13 SD2 = np.sum(D2)
14 P = D2 / SD2
15 Index = np.random.choice(nD, size=1, p=P)[0]
16 Centers[i+1] = X[Index]
17 return Centers
حال برای استفاده از این تابع، کد قبلی را به شکل زیر تغییر میدهیم:
1nRun = 100
2nClusters = 5
3nIter = 10
4
5Costs = np.zeros(nRun)
6
7for i in range(nRun):
8 Centers = KMeansPP(nClusters, X)
9 Clusters = GetClusters(Centers, X)
10 for _ in range (nIter):
11 Centers = UpdateCenters(Centers, Clusters)
12 Clusters = GetClusters(Centers, X)
13 Costs[i] = Cost(Centers, X)
پس از اجرای این کد و رسم نمودار هیستوگرام، نمودار زیر حاصل میشود.
در اولین نگاه، کاملاً مشهود از که ۹۰٪ اجراها به هزینهای کمتر از ۰٫۰۲ رسیدهاند که در مقایسه با حالت معمولی (۷۰٪) بسیار بهتر است.
توجه داشته باشید که برای اثبات اثربخشی هر الگوریتم، باید تعداد دفعات اجرا بسیار بیشتر از ۱۰۰ بار باشد. از طرفی، بررسی عملکرد روی چندین مجموعه داده نیز از الزامات این کار است. برای مثال، در نمودار اخیر، برای یک اجرا مقدار تابع هزینه به ۰٫۲ رسیده که نسبت به الگوریتم معمولی افزایش یافته است. این حالت برخلاف تصور ما و غالب مشاهدات ما مبنی بر عملکرد بهتر الگوریتم K-means++ بوده است. به همین دلیل، میتوان با افزایش تعداد دفعات اجرای الگوریتم، با اطمینان بهتری عمل مقایسه را انجام داد.
جمعبندی پیاده سازی الگوریتم K-means++ در پایتون
در این مطلب، تابع هزینه و وابستگی الگوریتم Lloyd به شرایط اولیه را بررسی کردیم. همچنین، به پیادهسازی الگوریتم K-means++ در پایتون پرداختیم.
برای مطالعه بیشتر، میتوان پاسخ پرسشهای زیر را تحقیق کرد:
- چرا از توان دوم فاصله بهعنوان معیار مرتبط با احتمال انتخاب داده استفاده کردیم؟
- آیا ممکن است در صورت استفاده از الگوریتم K-means++ نیز مراکز خوشه اولیه نزدیک به هم باشند؟ چه راهکاری برای حل این مشکل میتوان ارائه داد؟
- اگر نمودار هیستوگرام دو الگوریتم را روی هم رسم و آنها را مقایسه کنیم، چه معیارهای آماری میتوانند عملکرد بهتر الگوریتم K-means++ را نشان دهند؟
- آیا الگوریم K-means++ میتواند بهلحاظ هزینه محاسباتی، مزیتی داشته باشد؟