Реализация на прост, но мощен алгоритъм за клъстеризация на 2D точки, представени като наредени двойки (Double,Double)
. Използвани са прости и оптимални за повечето цели структури от данни, които се обхождат итеративно, избягва се търсене в големи списъци, някои изчисления са стриктни и т.н. Допълнително се измерва общото време за работата на програмата (вкл. IO операциите, парсването на входа и форматирането на изхода), както и след колко итерации е приключил алгоритъма. Накрая се изчислява и within-cluster sum of squares - сумата от квадратите на разстоянията между всяка точка от даден клъстер и центъра на клъстера (общо за всички клъстери). Това количество на практика се опитва алгоритъмът да минимизира, но в зависимост от избраните начални центрове може да не достигне глобален минимум (задачата за намирането на глобален минимум е NP-hard). За удобство достигнатият минимум се изписва на стандартния изход, заедно с броя итерации и времето за работа на програмата.
Необходим е пакетът clock
за точно отмерване на времето (без него кодът, естествено, не се компилира).
Една от най-съществените и ефективни хитринки се проявява във функцията reClusterize
, която с едно линейно итеративно и стриктно обхождане на пълния списък от точки изчислява за всяка от тях кой е новият ѝ клъстер, и по същото време следи дали някоя си го променя, и връща тази информация като Bool
в наредена двойка със получения списък от точки. Същата техника за извличане на цялата необходима информация с единствено слято (fused) и строго обхождане на списък е използвана и във функциите mean
и mapAndLength
.
От друга страна, функцията groupToClusters
може да се подобри значително, тъй като също се извиква по веднъж на всяка итерация, а блести по-скоро с простота, отколкото с ефективност. Има и други функции, които могат да се оптимизират по начини, за които компилаторът просто няма как да се досети, а може и да се изберат различни структури от данни (като unboxed списъци и/или вектори), но това са long-distance цели за подобрение. Във всички случаи в този си вид това е proof of concept.
Кодът може да се изпълни с функцията kmeans :: String -> Int -> String -> IO ()
, която получава три аргумента:
- име на файл с входните данни - текстови файл, в който всеки ред съдържа координатите на една точка, разделени с празно място (вж.
input1250.txt
за пример) - брой търсени клъстери
- име на файл за изходните данни - резултата от клъстеризирането.
Естествено, за по-бързо изпълнение кодът може да се компилира до executable, който при стартиране получава три аналогични command-line аргумента:
> ghc k-means.hs
> .\k-means.exe input.txt 4 output.txt
Аргументите не се валидират, следи се единствено техният брой (поне три, всички след третия се игнорират). Единствената валидация на входните данни е дали броят желани клъстери не надвишава количеството данни.
Файлът input1250.txt
служи за примерен вход и съдържа 1249 точки, без никаква особена вътрешна структура (любезно предоставен с цел тестване от @zaki1993). На моя неособен лаптоп клъстеризирането им в 4 клъстера отнема средно ~0,3 секунди, а в 10 клъстера - около 0,7 секунди.