فهرست مقاله
وقتی برچسبِ وضعیتِ حسگر نداریم، میتوانیم از خوشهبندی برای بررسیِ گروههای هندسیِ داده شروع کنیم؛ اما شمارهٔ خوشه، نامِ عیب یا حقیقتِ اندازهگیری نیست. این درس ماژولی کوچک برای دو گامِ لوید میسازد و حالتِ پایانِ محاسبه را دقیق گزارش میکند. دادهها تألیفیاند؛ مسئله، بررسیِ یک الگوریتم و مرزِ استفاده از حالتِ برازش است، نه کشفِ عیب در تجهیزات واقعی.
۱. دادهٔ بیبرچسب و هدفِ مربعِ فاصله

شش مشاهدهٔ دوکاناله داریم: زوجهای صفر و صفر، صفر و دو، دو و صفر، هشت و هشت، هشت و ده، سپس ده و هشت. هر دو مختصات را ولت میگیریم؛ هر سطر یک مشاهده است. دو مرکزِ آغازین، صفر و صفر، و ده و دهاند. این مرکزها ورودیِ صریحِ برنامهاند؛ بذرِ تصادفی، دادهٔ پنهان یا برچسبِ آموزشی نداریم.
هدف، مجموع مربعِ فاصلهٔ هر مشاهده تا نزدیکترین مرکز است؛ واحدِ این مجموع ولتِ مربع میشود. بخش کاملِ k-means در راهنمای scikit-learn این هدف را از خودِ مرکزها و تعدادِ انتخابشدهٔ خوشهها جدا میکند. مرکزِ نهایی معمولاً یکی از مشاهدههای اولیه نیست؛ در مثال، مؤلفههای مرکزِ نخست دوسوم و مرکزِ دوم بیستوششسوم ولت میشوند.
در آغاز، هدف بیستوچهار ولتِ مربع است. سه نقطهٔ نخست به مرکزِ نخست و سه نقطهٔ بعدی به مرکزِ دوم تخصیص مییابند. بعد از جایگزینیِ مرکزها با میانگینِ گروه، سهم هر گروه شانزدهسوم و مجموع سیودوسوم ولتِ مربع است. این پاسخِ دستی، مستقل از اجرای کتابخانه یا مشاهدهٔ نمودارِ خوشههاست؛ از آن کیفیتِ جداسازیِ یک وضعیتِ فیزیکی نتیجه نمیگیریم.
همواحد بودنِ دو کانال، انتخابِ مقیاس را روشنتر میکند، ولی اهمیتِ برابرِ ویژگیها را ثابت نمیکند. تبدیلِ فقط یک کانال، هندسه را تغییر میدهد. آزمون پیشپردازشِ برازششده روی آموزش، منشأ حالتِ مقیاسگذاری را بررسی میکند؛ این برنامه مقیاسگذاری، انتخابِ تعدادِ خوشه یا پاکسازیِ داده را خودکار انجام نمیدهد.
۲. دو گامِ لوید و سیاستِ تساوی و خوشهٔ خالی

در گامِ تخصیص، برای هر نقطه همهٔ فاصلههای مربعیِ محاسبهشده را مقایسه میکنیم. اگر برابر باشند، اندیسِ کوچکترِ مرکز برنده است؛ این سیاستِ قطعیِ همین تابع است و معنای آماری ندارد. سپس میانگینِ هر مختصهٔ اعضای گروه، مرکزِ بعدی میشود. ترتیبِ مرکزها حفظ میشود؛ برنامه به دلخواه شمارهها را برای شبیه شدن به نتیجهای دیگر جابهجا نمیکند.
برای خوشهٔ بیعضو، میانگینی نداریم. سیاستِ ما نگهداشتنِ مرکزِ قبلی و گزارشِ اندیسِ خالی است؛ صفرگذاری، انتخابِ تصادفی یا انتقالِ خودکارِ دورترین نمونه نداریم. در دادهٔ تککانالهٔ صفر، دو و ده با مرکزهای آغازین صفر، دو و صد، نتیجهٔ پایدار مرکزهای یک، ده و صد است. مرکزِ سوم همچنان خالی است؛ درخواستِ سه مرکز، تضمینِ سه گروهِ اشغالشده نیست.
این سیاست، گزینهٔ عمومیِ همهٔ پیادهسازیهای k-means معرفی نمیشود. برای کاربردی که حتماً سه گروهِ ناتهی میخواهد، چنین نتیجهای باید در لایهٔ پذیرش بررسی شود. نگهداشتنِ حالتِ خالی، امکانِ بازبینیِ مشکلِ آغاز را حفظ میکند؛ نامِ خالی را با نبودِ اندازهگیری، مقدارِ صفرِ کانال یا کشفِ یک وضعیتِ نادر یکی نمیگیریم.
در حسابِ دقیق، انتخابِ نزدیکترین مرکز و سپس میانگینگیری، هدفِ متناظر را زیاد نمیکنند. این استدلال برای هدفِ مشخصِ مربعِ فاصله است، نه هر معیارِ دلخواه. محاسباتِ ما float هستند و گردکردن میتواند رفتارِ مرزِ فاصلهها را عوض کند؛ برابریِ مقایسه، برابریِ عددهای محاسبهشده است. تضمینِ سراسریِ پایداری و رسیدن به بهترین تقسیم از آن استخراج نمیکنیم.
۳. کد کامل و حالتِ توقف

ورودی، فهرست یا تاپلِ غیرخالی از سطرهای همبُعدِ غیرخالی است. مختصات باید int یا float واقعیِ پایتون و پس از تبدیل متناهی باشند؛ بولی، رشته، مختلط و NaN رد میشوند. تعدادِ مرکزها از تعدادِ مشاهدهها بیشتر نیست. سقفِ کوچکِ ساختگی برای بُعد یا تعداد نگذاشتهایم، ولی سرریزِ فاصله یا هدف به خطای صریح میانجامد؛ این کد وعدهٔ پردازشِ هر اندازهای نیست.
پس از هر بهروزرسانیِ مرکز، تخصیص را دوباره میسازیم. فقط اگر این تخصیص با گروهبندیِ استفادهشده برای میانگینها یکسان باشد، حالتِ پایدار اعلام میشود. در غیر این صورت، دورِ بعدی لازم است؛ سقفِ دورها نیز عدد صحیحِ مثبت است. این معیار، سیاستِ ماست و برابر با تلورانسِ جابهجاییِ مرکزها در قرارداد KMeans معرفی نمیشود.
"""Deterministic finite-round Lloyd fit, explicit empty-center and stop policies."""
from dataclasses import dataclass, asdict
from fractions import Fraction
from itertools import product, combinations
import json
import math
import platform
def rows(values):
if type(values) not in (list,tuple) or not values:
raise ValueError('nonempty list/tuple of rows required')
result=[]; dimension=None
for row in values:
if type(row) not in (list,tuple) or not row:
raise ValueError('nonempty coordinate row required')
if dimension is None: dimension=len(row)
if len(row)!=dimension: raise ValueError('all row dimensions must agree')
converted=[]
for v in row:
if type(v) not in (int,float): raise ValueError('actual int/float, not bool')
try: f=float(v)
except OverflowError as exc: raise ValueError('finite float conversion required') from exc
if not math.isfinite(f): raise ValueError('finite coordinates required')
converted.append(f)
result.append(tuple(converted))
return tuple(result)
def squared_distance(a,b):
try: result=math.fsum((u-v)**2 for u,v in zip(a,b,strict=True))
except OverflowError as exc: raise ArithmeticError('squared distance overflow') from exc
if not math.isfinite(result): raise ArithmeticError('nonfinite squared distance')
return result
def assign(points,centers):
return tuple(min(range(len(centers)),key=lambda j:(squared_distance(x,centers[j]),j)) for x in points)
def objective(points,centers,labels):
try: value=math.fsum(squared_distance(x,centers[j]) for x,j in zip(points,labels,strict=True))
except OverflowError as exc: raise ArithmeticError('objective overflow') from exc
if not math.isfinite(value): raise ArithmeticError('nonfinite objective')
return value
def predict(points,centers):
x,c=rows(points),rows(centers)
if len(x[0])!=len(c[0]): raise ValueError('point/center dimensions differ')
return assign(x,c)
@dataclass(frozen=True)
class Fit:
centers: tuple
labels: tuple
last_update_labels: tuple
empty_clusters_last_update: tuple
objective_history: tuple
rounds: int
converged: bool
stop_reason: str
def fit_lloyd(points,initial_centers,max_rounds=50):
x,c=rows(points),rows(initial_centers)
if len(x[0])!=len(c[0]) or len(c)>len(x):
raise ValueError('matching dimensions and center count <= sample count required')
if type(max_rounds)is not int or max_rounds<1:
raise ValueError('positive actual integer max_rounds required')
history=[objective(x,c,assign(x,c))]
for round_index in range(1,max_rounds+1):
used=assign(x,c); updated=[]; empty=[]
for j,old in enumerate(c):
members=[v for v,label in zip(x,used,strict=True) if label==j]
if not members:
updated.append(old);empty.append(j)
else:
center=tuple(math.fsum(v[d]/len(members) for v in members) for d in range(len(old)))
if not all(math.isfinite(v) for v in center): raise ArithmeticError('nonfinite center')
updated.append(center)
c=tuple(updated); labels=assign(x,c)
history.append(objective(x,c,labels))
converged=labels==used
if converged: break
return Fit(c,labels,used,tuple(empty),tuple(history),round_index,converged,
'assignment_fixed_point' if converged else 'round_limit')
def exact_assign(points,centers):
"""Enumerate all label vectors, minimize exact objective, lexicographic ties."""
candidates=product(range(len(centers)),repeat=len(points))
def score(labels):
return sum(sum((v-w)**2 for v,w in zip(x,centers[j],strict=True)) for x,j in zip(points,labels,strict=True))
return min(candidates,key=lambda labels:(score(labels),labels))
def exact_fit(values,seeds,limit=30):
x=tuple(tuple(Fraction(v) for v in row) for row in values)
c=tuple(tuple(Fraction(v) for v in row) for row in seeds)
for step in range(1,limit+1):
used=exact_assign(x,c);new=[]
for j,old in enumerate(c):
group=[v for v,label in zip(x,used,strict=True) if label==j]
new.append(old if not group else tuple(sum(v[d] for v in group)/len(group) for d in range(len(old))))
c=tuple(new);labels=exact_assign(x,c)
if labels==used:break
return c,labels,used,step,labels==used
def global_partition_cost_1d(values,k):
"""Enumerate nonempty partitions and their exact optimal means; tiny oracle."""
x=tuple(Fraction(v) for v in values);best=None
for labels in product(range(k),repeat=len(x)):
if set(labels)!=set(range(k)):continue
centers=tuple(sum(v for v,j in zip(x,labels) if j==i)/labels.count(i) for i in range(k))
cost=sum((v-centers[j])**2 for v,j in zip(x,labels))
if best is None or cost<best:best=cost
return best
def fails(exc,function,*args,**kwargs):
try:function(*args,**kwargs)
except exc:return True
return False
def run():
checks={}
def check(name,passed):checks[name]=bool(passed);assert checks[name],name
x=((0,0),(0,2),(2,0),(8,8),(8,10),(10,8));seeds=((0,0),(10,10))
fit=fit_lloyd(x,seeds)
check('literal_partition_centers_objective_and_fixed_point',fit.labels==(0,0,0,1,1,1) and all(math.isclose(a,b,abs_tol=1e-14) for a,b in zip(fit.centers[0],(2/3,2/3))) and all(math.isclose(a,b,abs_tol=1e-14) for a,b in zip(fit.centers[1],(26/3,26/3))) and math.isclose(fit.objective_history[-1],32/3,abs_tol=1e-13) and fit.converged)
queries=((5,5),(100,100));saved_centers=fit.centers
fixed_prediction=predict(queries,fit.centers)
deliberate_refit=fit_lloyd(queries,fit.centers)
check('predict_does_not_refit_centers',fixed_prediction==(1,1) and deliberate_refit.labels==(0,1) and fit.centers==saved_centers)
check('equal_distance_prefers_lower_center_index',predict(((1,),(0,)),((0,),(2,)))==(0,0))
empty=fit_lloyd(((0,),(2,),(10,)),((0,),(2,),(100,)))
check('empty_cluster_keeps_previous_center',empty.converged and empty.centers[2]==(100.,) and empty.empty_clusters_last_update==(2,) and 2 not in empty.labels)
limited=fit_lloyd(((0,),(2,),(3,),(9,)),((0,),(3,)),max_rounds=1)
check('round_limit_reports_distinct_update_and_prediction_labels',not limited.converged and limited.stop_reason=='round_limit' and limited.last_update_labels==(0,1,1,1) and limited.labels==(0,0,1,1) and limited.centers==((0.,),(14/3,)))
bad=fit_lloyd(((0,),(3,),(4,),(7,)),((0,),(7,)))
better=fit_lloyd(((0,),(3,),(4,),(7,)),((0,),(3,)))
optimum=global_partition_cost_1d((0,3,4,7),2)
check('stable_assignment_is_not_global_optimum',bad.converged and bad.objective_history[-1]==9 and optimum==Fraction(26,3) and math.isclose(better.objective_history[-1],float(optimum),abs_tol=1e-13))
cases=0;max_difference=0.
for values in [(0,1,3,4),(0,2,4,6),(0,3,4,7),(1,2,5,6)]:
for shift in (0,10):
points=tuple((v+shift,) for v in values)
for i,j in combinations(range(4),2):
initial=(points[i],points[j]);actual=fit_lloyd(points,initial,max_rounds=30);ref=exact_fit(points,initial)
assert actual.labels==ref[1] and actual.last_update_labels==ref[2] and actual.converged==ref[4] and actual.rounds==ref[3]
delta=max(abs(float(b)-a) for ar,br in zip(actual.centers,ref[0]) for a,b in zip(ar,br));max_difference=max(max_difference,delta);assert delta<=2e-12
cases+=1
check('Fraction_exhaustive_assignment_reference_48_fixtures_ONE_group',cases==48)
check('objective_history_descends_in_limited_fixture',all(b<=a+2e-12 for a,b in zip(limited.objective_history,limited.objective_history[1:])) and len(limited.objective_history)==limited.rounds+1)
translated=fit_lloyd(tuple(tuple(v+5 for v in row) for row in x),tuple(tuple(v+5 for v in row) for row in seeds))
check('common_translation_preserves_partition_and_objective',translated.labels==fit.labels and math.isclose(translated.objective_history[-1],fit.objective_history[-1],abs_tol=2e-12))
gain=fit_lloyd(tuple(tuple(-2*v for v in row) for row in x),tuple(tuple(-2*v for v in row) for row in seeds))
check('common_gain_scales_squared_objective',gain.labels==fit.labels and math.isclose(gain.objective_history[-1],4*fit.objective_history[-1],abs_tol=2e-12))
original=[[0],[2],[10]];initial=[[0],[2],[100]];fit_lloyd(original,initial)
check('input_lists_not_mutated_and_state_is_tuple_based',original==[[0],[2],[10]] and initial==[[0],[2],[100]] and type(empty.centers)is tuple)
one=fit_lloyd(((0,),(2,),(4,)),((100,),))
check('single_cluster_is_overall_mean',one.centers==((2.,),) and one.labels==(0,0,0) and one.objective_history[-1]==8)
check('shape_center_count_and_round_rejections',all(fails(ValueError,fit_lloyd,*args,**kwargs) for args,kwargs in [(([],((0,),)),{}),((((0,1),),((0,),)),{}),((((0,),),((0,),(1,))),{}),((((0,),(1,)),((0,),)),{'max_rounds':0}),((((0,),(1,)),((0,),)),{'max_rounds':True})]))
check('nonfinite_and_nonreal_coordinates_rejected',all(fails(ValueError,fit_lloyd,((v,),),((0,),)) for v in (True,'1',1+0j,float('nan'),float('inf'))))
check('finite_coordinate_distance_overflow_is_explicit',fails(ArithmeticError,fit_lloyd,((1e308,),),((-1e308,),)))
return {'python':platform.python_version(),'dependencies':'stdlib only','units':{'coordinates_and_centers':'V','objective':'V^2'},'hand_fit':asdict(fit),'prediction_boundary_witness':{'queries':queries,'fixed_center_labels':fixed_prediction,'deliberate_refit_labels':deliberate_refit.labels},'empty_cluster_fit':asdict(empty),'one_round_fit':asdict(limited),'local_minimum_witness':{'stable_objective_v2':bad.objective_history[-1],'better_seed_objective_v2':better.objective_history[-1],'exact_global_optimum_v2':str(optimum)},'Fraction_oracle_fixtures_in_ONE_group':cases,'max_center_difference_V':max_difference,'oracle_absolute_tolerance_V':2e-12,'random_draws':0,'checks':checks}
if __name__=='__main__':print(json.dumps(run(),ensure_ascii=False,indent=2,allow_nan=False))
دو فهرستِ متفاوت در خروجی نگه میداریم: برچسبهای نزدیکترین مرکزِ نهایی و برچسبهایی که آخرین میانگین با آنها ساخته شده است. در توقفِ ناشی از سقفِ دور، این دو میتوانند فرق کنند. مستنداتِ KMeans نیز دربارهٔ ناسازگاریِ میانگینِ گروهها با برچسبِ بازتخصیصیافته در توقفِ زودهنگام هشدار میدهند؛ رابطِ ما هر دو مرحله را آشکار میکند و موفقیتِ کاذبِ همگرایی نمینویسد.
پیشبینی باید با مرکزهای ذخیرهشده انجام شود. برای دو پرسشِ پنج و پنج، و صد و صد ولت، مرکزهای برازشِ اصلی هر دو را به گروهِ یک میفرستند. اگر عمداً همان دو پرسش را دوباره برازش کنیم، پاسخ صفر و یک میشود. این شاهد در آزمون آمده است تا یک بازبرازشِ اشتباه، پشتِ پرسشهای آسان پنهان نشود. مرز با kNN دارای برچسبِ ذخیرهشده نیز روشن است: اینجا خودِ مرکزها از دادهٔ بیبرچسب یاد گرفته میشوند.
۴. مرجع کسری و شاهدِ جوابِ محلی

مرجع مستقل برای دادههای کوچکِ صحیح، همهٔ بردارهای ممکنِ برچسب را میشمارد و هزینهٔ تخصیص به مرکزهای معلوم را با Fraction مقایسه میکند؛ از تابعِ نزدیکترین مرکزِ مسیرِ اصلی استفاده نمیکند. سپس میانگینهای کسری و شرطِ توقف را میسازد. چهلوهشت ترکیبِ داده، انتقال و مرکزِ آغازین در یک گروه بررسی شدهاند؛ تعدادِ تقسیمهای درونی را گروهِ تازه حساب نمیکنیم.
مقایسهٔ مرکزها با مرجع، حدِ مطلقِ دو ضربدر ده به توان منفی دوازده ولت دارد؛ بیشترین اختلافِ مشاهدهشده حدود ۱٫۷۸ ضربدر ده به توان منفی پانزده ولت بود. این مقدار، نتیجهٔ همین fixtureهای محدود است و عدم قطعیتِ حسگر یا کرانِ خطای همهٔ دادهها نیست. جوابِ دستی و شاهدِ محلی نیز جدا از مرجعِ تکرارها کنترل میشوند.
برای دادهٔ صفر، سه، چهار و هفت ولت، آغازِ صفر و هفت به دو گروهِ صفر و سه، سپس چهار و هفت میرسد؛ هدف نه است و تخصیص پایدار میماند. آغازِ صفر و سه جوابِ دیگری با هدف بیستوششسوم میدهد. شمارشِ مستقلِ همهٔ تقسیمهای ناتهیِ همین چهار نقطه نیز همین هزینهٔ کوچکتر را تأیید میکند؛ در نتیجه حالتِ پایدارِ نخست، جوابِ سراسری نیست.
یک شاهدِ جدا برای سقفِ دور داریم: دادهٔ صفر، دو، سه و نه با آغازِ صفر و سه، پس از یک دور مرکزهای صفر و چهاردهسوم دارد. برچسبِ مورد استفاده برای میانگین، صفر، یک، یک، یک است، ولی پیشبینیِ نهایی صفر، صفر، یک، یک میشود. خروجی بهدرستی توقفِ محدود را گزارش میکند؛ مرکزها را میانگینِ همین برچسبهای نهایی جا نمیزنیم.
{
"python": "3.14.5",
"dependencies": "stdlib only",
"units": {
"coordinates_and_centers": "V",
"objective": "V^2"
},
"hand_fit": {
"centers": [
[
0.6666666666666666,
0.6666666666666666
],
[
8.666666666666666,
8.666666666666666
]
],
"labels": [
0,
0,
0,
1,
1,
1
],
"last_update_labels": [
0,
0,
0,
1,
1,
1
],
"empty_clusters_last_update": [],
"objective_history": [
24.0,
10.666666666666668
],
"rounds": 1,
"converged": true,
"stop_reason": "assignment_fixed_point"
},
"prediction_boundary_witness": {
"queries": [
[
5,
5
],
[
100,
100
]
],
"fixed_center_labels": [
1,
1
],
"deliberate_refit_labels": [
0,
1
]
},
"empty_cluster_fit": {
"centers": [
[
1.0
],
[
10.0
],
[
100.0
]
],
"labels": [
0,
0,
1
],
"last_update_labels": [
0,
0,
1
],
"empty_clusters_last_update": [
2
],
"objective_history": [
64.0,
20.0,
2.0
],
"rounds": 2,
"converged": true,
"stop_reason": "assignment_fixed_point"
},
"one_round_fit": {
"centers": [
[
0.0
],
[
4.666666666666667
]
],
"labels": [
0,
0,
1,
1
],
"last_update_labels": [
0,
1,
1,
1
],
"empty_clusters_last_update": [],
"objective_history": [
37.0,
25.555555555555554
],
"rounds": 1,
"converged": false,
"stop_reason": "round_limit"
},
"local_minimum_witness": {
"stable_objective_v2": 9.0,
"better_seed_objective_v2": 8.666666666666666,
"exact_global_optimum_v2": "26/3"
},
"Fraction_oracle_fixtures_in_ONE_group": 48,
"max_center_difference_V": 1.7763568394002505e-15,
"oracle_absolute_tolerance_V": 2e-12,
"random_draws": 0,
"checks": {
"literal_partition_centers_objective_and_fixed_point": true,
"predict_does_not_refit_centers": true,
"equal_distance_prefers_lower_center_index": true,
"empty_cluster_keeps_previous_center": true,
"round_limit_reports_distinct_update_and_prediction_labels": true,
"stable_assignment_is_not_global_optimum": true,
"Fraction_exhaustive_assignment_reference_48_fixtures_ONE_group": true,
"objective_history_descends_in_limited_fixture": true,
"common_translation_preserves_partition_and_objective": true,
"common_gain_scales_squared_objective": true,
"input_lists_not_mutated_and_state_is_tuple_based": true,
"single_cluster_is_overall_mean": true,
"shape_center_count_and_round_rejections": true,
"nonfinite_and_nonreal_coordinates_rejected": true,
"finite_coordinate_distance_overflow_is_explicit": true
}
}
پانزده گروهِ معنادار با Python 3.14.5 و کتابخانهٔ استاندارد اجرا شدهاند؛ scikit-learn اجرا نشده است. نوع و شکل، خوشهٔ خالی، تکخوشه، تساوی، تغییرِ مقیاس، حفظ ورودی و سرریز بررسی شدهاند. آزمونِ پیشبینی پیش از انجمادِ مقاله تقویت شد؛ اجرای نخست و رسیدش حفظاند، اجرای نسخهٔ نهایی یک بار انجام شده و تعدادِ گروهها افزایش نیافته است.
۵. چهار پرسش و مرزِ خوشهبندی

پرسش اول: نقطهٔ یک میان مرکزهای صفر و دو به کجا میرود؟ پاسخ: هر دو فاصلهٔ محاسبهشده برابرند، پس به اندیس صفر میرود. تغییرِ ترتیبِ مرکزهای آغازین میتواند نامِ عددیِ گروهها را تغییر دهد؛ این شماره، برچسبِ فیزیکیِ سالم یا خراب نیست.
پرسش دوم: ضربِ همهٔ مختصات و مرکزهای آغازین در منفی دو چه میکند؟ پاسخ: در شاهدِ ثبتشده، تقسیم ثابت و هدف چهار برابر میشود؛ هر اختلاف دو برابر و مربعش چهار برابر است. تبدیلِ تنها یک ویژگی چنین خاصیتی ندارد و میتواند اعضای گروهها را عوض کند.
پرسش سوم: با مرکزِ خالیِ صد چه کنیم؟ پاسخ: تابع آن را حفظ و خالی بودنش را گزارش میکند. اگر مصرفکننده سه گروهِ فعال میخواهد، باید نتیجه را نپذیرد یا سیاستِ آغاز و ترمیمِ دیگری را صریح تعریف کند. پنهان کردنِ آن با مرکزِ صفر، شاهدِ گروهِ تازه نمیسازد.
پرسش چهارم: همگرایی یعنی بهترین خوشهبندی؟ پاسخ: خیر؛ شاهدِ نه در برابر بیستوششسوم خلافش را نشان میدهد. انتخابِ چند آغاز میتواند مقایسهٔ بیشتری بدهد، اما بهتنهایی گواهیِ بهینگیِ همیشگی نیست. منابع در ۲۶ مهر ۱۴۰۵ خوانده شدهاند؛ آموزش پایه است و هیچ دقتِ تشخیصِ عیب، سرعتِ دستگاه یا آزمایشِ واقعی ادعا نمیشود.
منابع و مطالعهٔ بیشتر
بررسی منابع: ۲۶ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

