Clustering Algorithmen

Hallo zusammen,

ich hatte im vergangenen Semester das Seminarfach „Big Data Mining“ und hatte vor kurzem die Lust verspürt ein paar Algorithmen nachzuprogrammieren :wink:
Die Algorithmen sind dem Buch „Mining of massive datasets“ (Mining of Massive Datasets, frei downloadbar) entnommen, konkret ist es zum einen das hierarchische Clustering und der K-Means Algorithmus.
Da ich mich dann entschlossen hatte das Ganze auch zu visualisieren hier mal ein Screenshot:

Dass sich die Kreise überlappen liegt schlicht und einfach daran, dass der Radius eines Clusters nicht dem geometrischen Radius eines Kreises entspricht, insbesondere ist 2*Radius != Durchmesser. Daher sieht es augenscheinlich so aus, als ob Punkte in mehreren Clustern liegen :wink:

Hier mal ein paar Features:
[ul]
[li]Zufallsgenerierung von Punkten
[/li][li]Wahl der Min- und Max-Werte der X- und Y-Koordinaten
[/li][li]3 Abbruchbedingungen beim hierarchischen Clustering wählbar
[/li][li]K-Means wahlweise mit hierarchischem Preclustering oder „zufällige“ Wahl der k voneinander entferntesten Punkte
[/li][li]Koordinatensystem ist sroll- und zoombar
[/li][li]Punkte können per Drag&Drop auch als Json-Input eingegeben werden
[/li][li]Farbliche Hervorhebung von selektierten Clustern und Punkten
[/li][/ul]

Den Sourcecode kann hier gefunden werden:

Eine beispielhafte Json Datei kann unter res/ gefunden werden.

Download für das JAR:
Clustering.jar

Aber 2 Dinge bitte bei etwaigen Urteilen berücksichtigen: a) Ich bin kein GUI-Profi und b) kam die Idee einer visuellen Repräsentation erst später auf.

Gerade b) ist es geschuldet, dass man noch auf ein paar sinnlose Klassen (wie z.B ‚Clusterable‘) trifft. Diese Klassen sind entstanden, da ich zuerst auch Punkte in nicht-euklidischen Räumen clustern wollte. Die lassen sich aber schlecht visualisieren :stuck_out_tongue:

Als weiteren Ausblick würde mich die Implementierung von Algorithmen reizen, die für Datenmengen konzipiert sind, die nicht im Hauptspeicher gehalten werden können (BFR, GRGPF).

Es würde mich interessieren, was ihr noch für Verbesserungsvorschläge & Anregungen habt.

Grüße vom Shadoka.

Vorneweg schonmal: ::manklatsch für die (Eigen)Initiative! :slight_smile:

Hab’s noch nicht getestet, nur mal kurz über den Code drübergeschaut. Da könnte man natürlich 1000 Sachen hinterfragen oder kritisieren. Mehr oder weniger „wichtige“ Dinge.

Was mir aufgefallen ist, ist die Verwendung von „Vector“. Das ist üblicherweise zu speziell. Ein „Vector“ ist eigentlich nur eine (synchronisierte) Implementierung des „List“-Interfaces. Also statt sowas wie
Vector<Point> points = new Vector<Point>();
sollte man i.a. eher schreiben
List<Point> points = new Vector<Point>();
Aber auch DAS (wenn überhaupt) nur, wenn man eine synchronisierte Liste braucht. Meistens sollte es eine
List<Point> points = new ArrayList<Point>();
schon tun.
(NB: Das „synchronisiert“ bedeutet, grob gesagt, dass man „Vector“ mit mehreren Threads verwenden kann. Darüber solltest du dir auch mal Gedanken machen, aber das hier auszubreiten würde wohl zu weit führen…)

Insbesondere sollte in Methodensignaturen praktisch NIE das Wort „Vector“ oder „ArrayList“ auftauchen. Man sollte immer das Interface verwenden, das die minimale Menge von Operationen anbietet, die man braucht, um das machen zu können, was man will. Das nennt man manchmal „Gegen das Interface programmieren“.

Als Beispiele: So eine Methode sieht erstmal OK aus:

void computeSum(ArrayList<Point> points) {
    Point result = new Point();
    for (Point point : points) result.add(point);
    return result;
}

Man kann sie aufrufen mit

ArrayList<Point> points = ...;
Point sum = computeSum(points);

Aber man kann sie NICHT aufrufen mit

LinkedList<Point> points = ...;
Point sum = computeSum(points);

(Weil ja eine ArrayList erwartet wird, und keine LinkedList!)

Wenn man das verallgemeinert zu

void computeSum(List<Point> points) { // List statt ArrayList
   ...
}

Dann kann man auch eine LinkedList übergeben.

Aber NOCH allgemeiner wäre sowas wie

void computeSum(Collection<Point> points) { // Collection statt List
   ...
}

Dann kann man nicht nur eine ArrayList oder LinkedList übergeben, sondern auch eine HashSet oder so.

Tatsächlich würde in diesem Fall sogar das hier reichen:

void computeSum(Iterable<Point> points) { // Iterable statt Collection
   ...
}

Weil man in der Methode ja nichts anderes macht, als über alle Punkte drüberzuiterieren. Und dazu braucht man NUR das „Iterable“-Interface.

Und NOCH einen Tick allgemeiner:

void computeSum(Iterable<? extends Point> points) { // "? extends Point" statt "Point"
   ...
}

Das hat den Vorteil, dass man dort auch Dinge übergeben kann, die eine Unterklasse von „Point“ sind. Um’s zu verdeutlichen:

List<MySpecialPoint> points = ...; // Wobei "MySpecialPoint extends Point" gilt
Point sum = computeSum(points); // Das geht nur, wenn man "? extends Point" verwendet hat!

Die letzte Methodensignatur ist also das allgemeinst-mögliche für diesen Fall: Man braucht etwas, wo man sich nacheinander Dinge abholen kann, die mindestens „Point“ sind.


Du scheinst dort an einigen Stellen das „Visitor“-Pattern zu verwenden. Wie und wo und warum hat sich mir beim schnellen Drüberhschauen nicht ganz erschlossen. Es sieht erstmal seltsam aus, KANN aber OK sein (da kann ich gerade nichts dazu sagen…)


Zum Clusterable Interface: Jaa… :smiley: In der Apache Math Library gibt es auch Clustering-Algorithmen. Dort gab es in Version 3.0 auch dieses Interface Clusterable:
https://commons.apache.org/proper/commons-math/javadocs/api-3.0/org/apache/commons/math3/stat/clustering/Clusterable.html

Als ich das gesehen habe, habe ich mir gedacht: " :eek: Welcher IDOT. hat sich denn diesen MST ausgedacht!! :suspect: "

Warum? Ganz einfach: Wenn man 1000000 Datenobjekte hat, und die Clustern will, dann muss man für jedes einzelne dieser Datenobjekte ein passendes Objekt einer Klasse erstellen, die dieses Clusterable-Interface implementiert. Ein Krampf. Oder auch: FAIL.

In Version 3.3 der Apache Math Library wurde das Interface dann „Deprecated“, und durch ein neues Clusterable-Interface ersetzt:
ALT: http://commons.apache.org/proper/commons-math/apidocs/org/apache/commons/math3/stat/clustering/Clusterable.html
NEU: http://commons.apache.org/proper/commons-math/apidocs/org/apache/commons/math3/ml/clustering/Clusterable.html

Auch das neue ist in mancher Hinsicht fragwürdig, aber zumindest besser als vorher…


Insgesamt könnte man in die theoretische Planung und Strukturierung so eines Programms/so einer Bibliothek SEHR viel (geradezu beliebig viel!) Arbeit stecken. Aber dazu muss man sich mit den Clusteringverfahren und ihren Eigenheiten und Anforderungen schon sehr gut auskennen. Wenn dann noch Streaming, Fuzzy-Clustering oder Out-Of-Memory-Ansätze dazukommen, wird das schnell kompliziert.

BTW: Das Buch sieht ganz interessant aus (da werd’ ich ggf. mal über das eine oder andere Kapitel drüberschauen, wenn ich Zeit habe).

Danke für den Hinweis, das weiß ich eigentlich auch, nur bin ich in der Hinsicht immer etwas nachlässig, wenn ich für mich selber programmiere D:
Ich gelobe Besserung und werde das auch noch im Code richtig stellen.

Wir hatten im 3. Semester nebenläufige Programmierung, war ein intensives Semester und Vector hat sich irgendwie als Standard bei mir eingebürgert…generell achte ich viel zu wenig auf die Spezialitäten der verschiedenen Collection-Typen…

Tatsächlich nutze ich momentan das Pattern nur, um die verschiedenen Exceptions richtig zur Oberfläche zu bringen. Im SettingsPanelController gibt es eine Methodepublic void handleException(AbstractGuiException e), die das Handling vornimmt. Ich bevorzuge Visitoren eigentlich immer gegenüber der Nutzung von instanceof, schon allein deswegen, da mir in größeren Projekten bei Hinzufügen eines neuen Untertypen sofort angezeigt wird, wo dieser Typ überall noch „gehandelt“ werden muss - sowas hat man bei instanceof natürlich nicht. Außerdem schreibe ich die schon so ziemlich automatisch, da ich (gelinde gesagt) sehr viel mit denen (auch auf einer abstrakteren Ebene) zu tun hatte :stuck_out_tongue:
Visitoren können ja beliebig generisch gehalten werden, wie das Beispiel zeigt:


	public X handle(Y y) throws Z;
}```

[quote=Marco13;97768]Ganz einfach: Wenn man 1000000 Datenobjekte hat, und die Clustern will, dann muss man für jedes einzelne dieser Datenobjekte ein passendes Objekt einer Klasse erstellen, die dieses Clusterable-Interface implementiert. Ein Krampf. Oder auch: FAIL.[/quote]
Mit `Clusterable` hätte man zumindest den Vorteil, dass man eine passende Metrik sofort ohne weiteres Zutun vom System aus festlegen könnte. Das war zumindest mein Hintergedanke dabei, aber ich bin ja jetzt sowieso nur mit der euklidischen Distanz unterwegs :)

[quote=Marco13;97768]BTW: Das Buch sieht ganz interessant aus (da werd' ich ggf. mal über das eine oder andere Kapitel drüberschauen, wenn ich Zeit habe).[/quote]
Das Buch ist super und einsame Klasse! Es erklärt alles Wichtige und mathematische Prinzipien werden (zumindest meinem Empfinden nach) auf exakt der richtigen Stufe erklärt - es fängt nicht komplett bei 0 an und es gibt immer super Beispiele. An diesem Buch hatte sich das Seminar ausgerichtet, jede 2er-Gruppe hatte 2 Kapitel, die sie jeweils in einem ~2-stündigen Vortrag 'dozieren' sollten. Wir hatten Kapitel 3, insbesondere 'Locality-sensitive Hashing'(LSH) und LSH-Funktionsfamilien, und Kapitel 7, Clustering eben. Bei so einem Vortrag merkt man erstmal, wie relativ doch Zeit ist :P

Wie gesagt: Wie “wichtig” der eine oder andere Kritikpunkt ist, darüber kann man streiten.

BTW: LSH hatte ich mir vor einiger Zeit auch mal angeschaut. Für sowas wie “k-Nearest-Neighbors in einem ca. 800-dimensionalen Raum”. Aber das probabilistische daran, und die vielen (vieeelen) Tuning-Parameter haben mich dann doch dazu veranlasst, das ganze mit einer simplen linearen Suche zu lösen :o

Ja, bei LSH kommt es ja eigentlich NUR darauf an, die Wahrscheinlichkeit einer richtigen Zuordnung mithilfe von Stellschrauben zu beeinflussen…im Buch waren die Kapitel 3.8.4 + 3.8.5 dafür sehr interessant, wo es um das One-To-Many Problem bei der Fingerabdruckerkennung geht. Ich glaube „normal“ wurde Fingerabdruck mit 0.5% Wahrscheinlichkeit in den richtigen Bucket gehashed, erst nach einem 1024-OR wurden daraus Werte, mit denen man was anfangen konnte :smiley:

Ich hab jetzt nurmal gaaaanz schnell drübergeschaut. Dabei ist mir noch folgendes Aufgefallen:

  1. Vermeide es Klassen nach bereits Existierenden Klassen zu bennen (Beispiel: Point).
  2. Erben vom JFrame: eine Sache die ganz oft und gerne gemacht wird. Allerdings verwendest du lediglich den JFrame in deinem Fall. Also ist eine Vererbung in dem Sinne nicht Notwendig (composition over inheritance).