Das letzte Mal, als ich versucht habe, C4.5 zu verstehen, bin ich gescheitert, aber ich habe eine Variante von ID3 implementiert - ursprünglich aus Neugier, aber sie wurde schließlich als Teil eines Overkill-Codegenerators mit mehreren Versendungen verwendet. Dies betrifft jedoch nie große Datenmengen und ist ein guter Job. Sie würden nicht gut daran tun, das meiste von dem nachzuahmen, was ich getan habe, aber vielleicht mit ein paar Ausnahmen, und natürlich habe ich ein bisschen aus den Fehlern gelernt.
Ich neige dazu, einen Entscheidungsbaum für ein Expertensystem zu erstellen, daher verwende ich die folgenden Begriffe - sorry, wenn das verwirrend ist ...
Column = Question ..... A question the expert system might ask
Row = Conclusion ... A possible conclusion the expert system might reach
Cell = Answer ....... For the question and conclusion, what answer should
the user be expected to give
In meinem Fall habe ich die Schlussfolgerung in einer anderen Spalte gezogen - ähnlich einer Wahrheitstabelle für ein Logikgatter. Zeilennummern waren daher nur Zeilennummern. Dadurch konnte ich Probleme im XOR-Stil behandeln, die nicht einmal dargestellt werden können, wenn dieselbe Schlussfolgerung nicht in mehreren Zeilen angezeigt werden kann. Ich bin mir nicht sicher, ob dies für Sie relevant ist oder nicht. Auf jeden Fall ignoriere ich dies unten - es macht keinen großen Unterschied, es sei denn, Sie sehen sich die Details der Auswahl der nächsten Frage an. Für Data Mining haben Sie wahrscheinlich ohnehin keine bestimmte Information, die Sie als Zielschlussfolgerung behandeln könnten - die "Schlussfolgerung" ist genau das, was übrig bleibt, wenn Sie sich entscheiden, keine Fragen mehr zu stellen.
Also - für jeden bisher abgeleiteten Entscheidungsbaumknoten haben Sie eine Reihe offener Fragen (Spalten) und eine Reihe noch nicht eliminierter Schlussfolgerungen (Zeilen). Das ist, was ich tat. Der einzige Punkt, der hinzugefügt werden sollte, ist, dass ich Bitvektoren verwendet habe.
IIRC, C ++ std::vector<bool>
und std::array<bool>
können als Bitvektoren implementiert werden, aber Sie sind immer noch auf die STL-Algorithmen für gesetzte Operationen angewiesen, die jeweils ein Element ausführen. Ich habe meine eigene Bitvektorklasse verwendet, die über einen bestimmten Zeitraum schrittweise aufgebaut wurde und bitweise Operatoren für den Basiswert verwendet std::vector<CHUNK>
(wobei CHUNK
es sich um einen vorzeichenlosen int-Typ handelt, der normalerweise 32 Bit breit ist).
Möglicherweise gibt es in C ++ 11 oder in Boost eine bessere Bitvektoroption, und es muss einige gute Bibliotheken geben, von denen einige - es gibt viele Arten von Programmen, bei denen Sie am Ende mit vorzeichenlosen Ganzzahlen arbeiten. Ich weiß einfach nicht viel über sie, weil ich zu faul war, von meiner eigenen zu wechseln.
Bitvektoren sind dort jedoch am besten, wenn die Mengen meist dicht sind. In diesem Fall ist der Satz von Zeilen das offensichtliche Problem. Nur der Wurzelknoten des Entscheidungsbaums hat einen perfekt dichten Zeilensatz. Wenn Sie sich weiter von der Wurzel entfernen, werden die Zeilensätze immer sparsamer, wobei jede Frage beantwortet wird, was dazu führt, dass der Satz von Zeilen auf zwei oder mehr disjunkte Zeilensätze des nächsten Knotens verteilt wird.
Ein einfaches sortiertes Array von Zeilennummern könnte daher die beste Darstellung für diese Mengen sein. Es ist jedoch auch möglich, dass sich ein "spärlicher Bitvektor" lohnt. Eine mögliche Implementierung ist ein sortiertes Array von Paaren, wobei das erste jedes Paares die erste Zeilen-ID eines Blocks und das zweite ein Bitvektor fester Größe für diesen Block ist. Beispielsweise könnte die Zeilennummer 35 in Block 32 ( 35 & ~(32 - 1)
) an Bitposition 3 ( 35 & (32 - 1)
) gespeichert sein . Wenn Sie nur die Paare speichern, bei denen der Bitvektor nicht Null ist, ergibt sich etwas zwischen einem sortierten Array von IDs und einem einfachen Bitvektor - spärliche Arrays werden relativ gut verarbeitet, insbesondere wenn IDs dazu neigen, sich in Sätzen eng zusammenzuschließen.
Es kann sich auch lohnen, eine Klasse zu verwenden, die von einer Bitvektor- zu einer sortierten Array-Darstellung wechseln kann, wenn die Größe klein genug wird. Die zusätzliche Komplikation, die nur einigen Knoten in der Nähe der Wurzel zugute kommt, ist jedoch wahrscheinlich sinnlos.
Wie auch immer diese Sätze dargestellt werden, da sie auf eine einzelne konstante "Datenbank" verweisen, spart dies viel Datenkopie und Platzverschwendung, wenn der Algorithmus ausgeführt wird. Aber es lohnt sich immer noch, sich diese "Datenbank" anzusehen.
Ich habe eine assoziative Datenstruktur verwendet, die es mir ermöglicht, mit einem Tupel aus Frage-ID und Abschluss-ID nachzuschlagen, um eine Antwort-ID zu erhalten. Das heißt, ich hatte einen Overhead pro Element für den Schlüssel (Frage-ID und Abschluss-ID) und in diesem Fall auch einen Overhead für den Baum im B + -Stil. Der Grund - im Grunde Gewohnheit. Ich habe Container, die sehr flexibel sind, und ich benutze sie häufig, weil ich nicht vorhersehen kann, welche Funktionen ich später tatsächlich benötige. Dafür gibt es einen Preis, aber das ist nur die alte Sache der vorzeitigen Optimierung.
In Ihrem Fall verwenden Sie eine Matrix - ich gehe von einem zweidimensionalen Array aus, das durch Frage-ID und Antwort-ID indiziert ist.
Ich kann mir nur vorstellen, dass meine Version effizienter ist als Ihre, wenn die meisten Antworten nicht bekannt sind. In einer Matrix benötigen Sie dafür eine spezielle, nicht bekannte Antwort-ID, die denselben Platz wie eine bekannte Antwort-ID einnimmt. In einem assoziativen Container schließen Sie diese Zeilen aus.
Trotzdem wäre ein sortiertes Array effizienter als meine B + -Baum-basierte Lösung. Sie müssen keine effizienten Einfügungen zulassen, daher ist der einzige notwendige Overhead für die Schlüssel.
Wenn Sie zwei Schlüsselfelder verwenden (Frage und Schlussfolgerung, Zeile und Spalte), die möglicherweise ein Problem darstellen (ich erinnere mich nicht wirklich), können Sie möglicherweise nicht einfach eine Kopie der Tabelle in einer sortierten Reihenfolge aufbewahren. Wenn Sie jedoch einen einzelnen berechneten Schlüssel nach dem Vorbild von verwenden (row * num_columns) + column
, implementieren Sie im Grunde genommen ohnehin ein zweidimensionales Array mit geringer Dichte.
Für mich bedeutet das Vorhandensein unbekannter / undefinierter Antworten auf eine bestimmte Frage, dass ich diese Frage noch nicht stellen darf - und selbst das war nur die Theorie, die ich bei der ersten Implementierung des Algorithmus verwendet habe. Ich habe das nie wirklich benutzt. Es gibt eine Verwendung, für die ich es verwenden könnte, aber ich bin nie dazu gekommen. Für den Datensatz bestand in diesem Mehrfachversand-Codegenerator eine Idee darin, basierend auf Feldern im Typ zu versenden. Da der Typ selbst polymorph ist, sind diese Felder möglicherweise nicht einmal vorhanden. Sie können sie daher nur anzeigen, wenn Sie bestätigt haben, dass sie vorhanden sein müssen.
Wenn Sie keine Anwendung für unbekannte / undefinierte Antworten haben, ist Ihre vorhandene Matrix wahrscheinlich bereits die beste Lösung.
Im Grunde ist es das - ich kann keine wirklich besseren Optionen anbieten, und was Sie tun, ist wahrscheinlich schon besser als das, was ich getan habe. Es gibt jedoch einige Kompromissmöglichkeiten, die Sie in Betracht ziehen könnten - vorausgesetzt, dies ist natürlich keine vorzeitige (und möglicherweise falsche) Optimierung.
Das Hauptkompromissproblem betrifft die Effizienz der Darstellung von spärlichen und dichten Wertesätzen, sodass es nicht wirklich spezifisch für C4.5 oder die Erstellung von Entscheidungsbäumen ist. Und ein "ausgefeilterer" Ansatz ist oft weniger effizient als ein einfacher, der mit Sorgfalt gewählt wurde.