Clustering on Intel MIC with Huge Datasets

Werbung
Clustering on Intel MIC with Huge Datasets
Andreas Bauer, Gregor Daiß, Max Franke
Institut für Parallele und Verteilte Systeme
Universität Stuttgart
Zusammenfassung Due to the spread of cheap and accurate sensors, huge amounts of data
are generated in relatively short times. To process these amounts of data, special hardware and
special algorithms which utilize the available hardware efficiently are required. One interesting
device is the Xeon Phi co-processor based on the Intel many integrated Cores Architecture.
These are very efficient for highly parallel code.
In this Project we have implemented a clustering algorithm developed by Benjamin Peherstorfer, as described in [PPB12], to run it on a Xeon Phi. The algorithm is divided into two
sub-algorithms. The first one is an algorithm to compute a similarity graph and the second one
calculates a density estimation based on a sparse grids. Both sub-algorithms have a structure
that can easily be split into a high amount of threads and therefore this algorithm makes good
use of the possibilities of the Xeon Phi.
The data we processed with this algorithm consists of solar data. It’s uncompressed size is
roughly 2.5 Terabyte. We pre-processed the data for better use and to get rid of redundant
data.
1
Das Clustering-Verfahren
Wir haben für diese Arbeit den Clusteringalgorithmus aus [PPB12] sowohl für eine
CPU mit mehreren Kernen, als auch für Intel Xeon Phi Beschleuniger-Karten implementiert. Der Algorithmus dient dazu, Cluster – also Anhäufungen von Datenpunkten
– in einer Menge von Daten zu finden. Hierzu wird eine dünngitterbasierte Dichteschätzung mit einem Algorithmus zu Nachbarschaftssuche kombiniert.
Der Algorithmus wird, wie in [PPB12] beschrieben, in 4 Schritte aufgeteilt:
1. Erstellen eines Nachbarschafts-Graphen zur Repräsentation der Datenpunkte
2. Berechnung der Dichteschätzung f mit Hilfe der Dünngitter basierten Dichteschätzung
3. Entfernen der Knoten und adjazenten Kanten, auf denen die Dichtefunktion unter
einen Schwellwert fällt
4. Zuordnung der übrigen Knoten zu den entsprechenden Clustern
Bei dem Algorithmus zur Nachbarschaftssuche haben wir den k-nearest-neighbors Algorithmus aus [Bis06] verwendet. Hierbei wird ein Graph erzeugt, bei dem jeder Knoten mit seinen k nächsten Nachbarn verbunden ist, wobei ein Knoten im Graph einen
Datenpunkt in der Eingabe darstellt.
Aus dem konstruierten Graph werden basierend auf der Dichtefunktion Knoten, die in
Bereichen geringer Dichte liegen, mit allen adjazenten Kanten entfernt. Anschließend
werden die Knoten einer Zusammenhangskomponente markiert, um die verschiedenen
Cluster zu kennzeichnen.
1.1
Intel Xeon Phi
Die Zielarchitektur, auf der der fertige Algorithmus laufen soll, ist die Xeon Phi von
Intel. Dabei handelt es sich um einen Coprozessor, der speziell für hoch-parallelisierten
Code entwickelt wurde. Bei voller Auslastung schafft ein einzelner Coprozessor bis zu
1,2 Teraflops bei doppelter Genauigkeit. Zur Programmierung des Xeon Phis haben
wir OpenCL benutzt.
2
2.1
Dichteschätzung
Ansatz
Wir bereits in der Einführung erwähnt, brauchen wir zum Clustering der Datenpunkte mithilfe des Graphalgorithmus zunächst eine Schätzung der Dichte. Um diese zu
berechnen, benötigen wir aufgrund der Größe des Datensatzes einen Algorithmus, der
auch bei großen Datensätzen effizient ist.
Eine Möglichkeit, das zu erreichen, wäre die Dichteschätzung mit einem Gitter an
Basisfunktionen, allerdings wachsen diese Gitter exponentiell mit der Anzahl der Dimensionen. Bei einem hochdimensionalen Datensatz ist dieser Ansatz so also nicht
ideal, auch wenn die Laufzeit nur von der Gridgröße abhängen würde. Dies nennt man
Fluch der Dimensionalität.
Um die Laufzeit zu begrenzen, nutzen wir den Algorithmus von [Pfl10] auf dünnen Gittern. Bei diesem versuchen wir nur die Gridpunkte eines vollen Gitters zu reduzieren,
indem wir nur die Gridpunkte beachten, die am meisten zur entgültigen Lösung beitragen. Wir opfern also etwas Genauigkeit, da wir weniger Gridpunkte nutzen, bekommen
aber eine bessere Laufzeit des Algorithmus.
2.2
Dünne Gitter
Ein Gitterpunkt eines vollen Gitters wird durch jeweils einen Level lj und einen Index
ij pro Dimension j identifiziert. Mit diesen Werten kann man die Basisfunktion eines
beliebigen Gitterpunkts berechnen:
ϕ(x) = max(1 − |x|, 0)
(2.1)
ϕl,i = ϕ(2l x − i)
(2.2)
Diese Funktion liegt auf dem Gitterpunkt der an der Stelle xl,i = 2−l i. Diese Basisfunktionen werden im d-dimensionalen Fall einfach mit einem Tensorprodukt erweitert.
ϕ~l,~i :=
d
Y
j=1
2
ϕlj ,ij (xj )
(2.3)
Die entgültige Dichteschätzung des Gitters ergibt sich dann aus der Summe der gewichteten Basisfunktionen der Gitterpunkte, wobei N die Anzahl der Gitterpunkte ist.
u(~x) =
N
X
αk ϕ~l,~i (~x)
(2.4)
k=0
Um eine Dichteschätzung zu erhalten, müssen wir also den Vektor α bestimmen.
Der mögliche Funktionsraum wird durch eine Reihe von Subräumen W~l festgelegt,
welche jeweils den möglichen Funktionsraum eines Levels (oder im d-dimensionalen
Fall eines Vektors an Leveln) darstellen. I~l sind die Indexmengen der Unterräume W~l,
welche wie folgt festgelegt sind:
I~l = {i : 1 ≤ ij ≤ 2lj − 1, ij odd, 1 ≤ j ≤ d}
(2.5)
o
n
W~l = span ϕ~l,~i (~x) : ~i ∈ I~l
(2.6)
Bei einem vollen Gitter der Tiefe n erhält man nun einen Funktionsraum:
Vn = ⊕l≤n W~l
(2.7)
Bei einem dünnen Gitter bleibt die Definition der Unterräume gleich, allerdings werden
nun weniger davon genutzt.
Vn = ⊕|~l|≤n+d−1 W~l
(2.8)
Die Abbildung 1 zeigt, wie solche Gitter aussehen. Im Folgenden benutzen wir N als
die Anzahl der Gitterpunkte, wobei n die Tiefe des Gitters bezeichnet. Die Tiefe des
Gitters bestimmt den maximalen Wert, den der Level eines Gitterpunkts annehmen
kann. Bei dünnen Gittern kann man die Anzahl der Gitterpunkte mit der Formel
d−1
O(h2n (log h−1
) abschätzen, wobei hn = 2−n gilt. Bei einem vollen Gitter wäre die
n )
Anzahl der Gitterpunkte hingegen h−d
n , wir haben die Anzahl der Gitterpunkte also
deutlich verringern können. Den angesprochenen Verlust der Genauigkeit ist in [Pfl10]
näher beschrieben.
2.3
Herleitung
Die Aufgabe ist also, den Vektor α so zu bestimmen, dass die Funktion 2.4 eine Schätzung der Dichte ergibt. Der Funktionsraum, aus dem diese Funktion stammt, bezeichnen wir ab sofort gemäß [Pfl10] Vn .
Die Idee ist, zunächst eine initiale Schätzung f für die Dichteschätzung aufzustellen.
Von dieser Funktion ausgehend will man nun eine geglättete Funktion bekommen, die
noch möglichst nah an der Schätzung ist aber dennoch glatter ist. Als Variationsproblem erhält man so folgende Gleichung:
Z
Z
2
(u(x) − f ) dx + λ (Lu)2 dx
(2.9)
Ω
Ω
3
Abbildung 1: Dreidimensionales Gitter der Tiefe 6
R
Der Term Ω (u(x) − f )2 dx bewirkt, dass
möglichst gering von der inR die Abweichung
2
itialen Dichteschätzung ist. Der Term λ Ω (Lu) dx wird Regularisierungsterm genannt.
Durch diesen wird bestimmt, wie glatt die Ergebnisfunktion der Variationsgleichung
wird. Dies wird über den Regularisierungs Parameter λ bestimmt.
Unser Variationsproblem sieht nun also wie folgt aus:
Z
Z
2
2
((u(x) − f ) + λ(Lu) )dx =
P dx
Ω
(2.10)
Ω
Nach [PPB12] erhält man damit folgendes Gleichungssystem:
(A + λC)α =
1 T
B
N
(2.11)
Ai,k = (ϕi , ϕk )L2
(2.12)
Ci,k = (Lϕi , Lϕk )L2
(2.13)
Bi,j = ϕi xj
(2.14)
Wenn man dieses Gleichungssystem nach α auflöst, erhält man die gesuchte Dichtefunktion u(x).
4
2.4
Analyse der Dichtegleichung (2.11)
RegularisierungstermR Um die Matrix C zu konkretisieren, müssen wir noch den
Regularisierungsterm λ Ω (Lu)2 dx wählen. Wir nutzen den in ?? vorgeschlagene AnP
2
satz des Regularisierungsterm N
i=1 αi . Es wurde gezeigt, dass dieser eine gute Wahl
für die hierarchische Basis eines dünnen Gitters, ist. Die Vorteile dieses Terms werden
wir am Ende der Herleitung sehen, da eine der Matrizen, mit denen wir rechnen zur
Identitätsmatrix wird.
Rechte Gleichungsseite Dieser Teil ist in O(N d) berechenbar. Allerdings ist der
Speicherbedarf O(N ), daher ist es ausreichend, diesen Teil nur einmal zu berechnen
und abzuspeichern.
Linke Gleichungsseite Die Größe der Matrix A ist offenbar N 2 , wobei N die Anzahl der Gridpunkte ist. Die Größe eines Grids lässt mit der Formel abbschätzen. Für
große und genau aufgelöstete Grids steigt die Größe also sehr schnell an, daher kann
man diese Matrix nicht unbedingt explizit abspeichern, da der Speicherbedarf O(N 2 )
zu hoch ist. Die Matrix muss also implizit dargestellt werden, das heißt, man muss
bei jeder Matrix-Vektor Multiplikation die entsprechenden Matrixeinträge erneut berechnen. Dies wird die meiste Rechenzeit beanspruchen, daher wird im nächsten Kapitel detailiert auf die Parallelsierungsmöglichkeiten und OpenCL-Implementierung der
Matrix-Vektor-Multiplikation eingegangen.
Lösungsalgorithmus Zum Lösen der Gleichung 2.11 verwenden wir direkt den Lösungsalgorithmus der konjugierten Gradienten aus der SGpp-Library.
2.5
Parallelisierung
Die naheliegendste Ansatz ist es, das Matrix-Vektor-Produkt der Matrix A + λC zu
parallelisieren, da man über den Ergebnisvektor der Multiplikation parallelisieren kann
und diese Operation zum Lösen der Gleichung 2.11 oft verwendet werden muss.
Brute Force Kernel Unsere erste Realisierung der Parallelisierung des MatrixVektor-Produkts besteht aus einem OpenCL-Kernel, der die Abbildung 3 umsetzt.
Ein Thread berechnet exakt einen Eintrag vom Ergebnisvektor des Matrix-VektorProdukts. Da die Matrix A, wie bereits erwähnt, nicht abgespeichert werden kann,
muss jeder Thread in der Lage sein, das komplette dünne Gitter durchzugehen um
die Einträge einer Zeile der Matrix zu berechnen. Das bedeutet der OpenCL-Kernel
benötigt die Level und Indices der einzelnen Gridpunkte, sowie natürlich den α-Vektor,
mit dem die Matrix multipliziert wird.
Diese Werte werden im OpenCL-Kernel als globale Arrays behandelt. Der Vorteil daran
ist, dass jeder Thread im Code einfach darauf zugreifen kann. Allerdings muss jeder
5
Thread selbst alle Werte auslesen, was dazu führt, dass unnötig viele Leseoperationen
durchgeführt werden. Zusätzlich übergeben wir in der Implementierung noch λ als
Skalar an den Kernel, sodass die Einträge der Matrix direkt als A + λC entsprechen.
Ein Performancetest, bei dem wir die Multiplikationsgeschwindigkeit auf der MIC
getestet haben, ist in 2 zu sehen.
Abbildung 2: Performance des Kernels
Streaming-Kernel Bei der Beschreibung des Brute-Force-Kernels sprachen wir an,
dass unnötig viele Leseoperationen durchgeführt werden, da jeder Thread unabhängig
von den anderen auf die Daten zugreift. Dieses Problem lässt sich durch die Nutzung
von Workgroups in OpenCL lösen. Eine Workgroup besteht aus einer gewissen Anzahl von Threads, die ihre Daten mithilfe von locale arrays austauschen und sich mit
Barriers synchron halten lassen.
Die Funktionsweise dieses Kernel entspricht ungefähr der des Brute-Force-Kernels.
Auch hier wird pro Thread ein Eintrag des Ergebnisvektors berechnet. Allerdings wartet ein Thread i mit der Berechnung von (A + λ)t,i αi , bis auch die anderen Threads
derselben Workgroup den Eintrag mit dem Spaltenindex i berechnen. Nun lädt jeder
6
Abbildung 3: Schema des Brute-Force-Kernels
Thread die Level und Indizes seines momentanen Gridpunkts in den lokalen Speicher.
So können die anderen Threads der Workgroup nun ebenfalls auf die Daten dieses
Gridpunkts zugreifen, ohne diese noch einmal aus dem globalen Speicher laden zu
müssen. Eine Workgroup arbeitet die Matrix also quasi in kleinen Quadraten ab (zu
sehen in der Abbildung 4 ). Sobald alle Threads einer Workgroup ein Quadrat abgearbeitet haben, beginnt ein Neues und die Threads fangen wieder an, den lokalen
Speicher mit ihrem momentanen Gridpunkt zu überschreiben.
Wenn man nun beispielsweise, wie in der Abbildung 4 eine Workgroup der Größe 2
hat, bedeutet es, dass im lokalen Speicher zu Beginn eines Quadrats die Daten zu 2
Gridpunkten sind. Daher können die Threads 1 und 2 direkt 2 Matrixeinträge berechnen und verarbeiten, ohne Daten aus dem globalen Speicher nachladen zu müssen. Je
größer also die Workgroups sind, desto mehr Daten können im lokalen Speicher von
anderen Threads wiederverwendet werden.
Dieses Prinzip funktioniert, da die Matrix symmetrisch ist. Zur Berechnung des Eintrags an der Stelle (i, j) benötigt man, genauso wie an der Stelle (j, i), die Daten des
Gridpunkts i.
Zuvor mussten O(2dN 2 ) Leseoperationen auf dem globalen Speicher des Kernels durchgeführt werden, da jeder Threads unabhängig von den Anderen alle Daten zu den
2dN 2
) LeseopeGridpunkten laden musste. Nun benötigt man nurnoch O( W ORKGROU
P SIZE
rationen, da sich einige Daten im lokalen Speicher befinden. Die Performancesteigerung
dieses Kernels hängt also stark davon ab, um wieviel schneller der Speicherzugriff auf
den lokalen Speicher im Vergleich zum Zugriff auf den globalen Speicher ist. Der Einfachheit halber haben wir jedoch im Folgenden den Brute-Force-Kernel verwendet.
7
Abbildung 4: Schema des Streaming-Kernels
Data Kernel Mit den vorherigen 2 Kernels haben wir die linke Seite der zu lösenden Dichtegleichung bereits erfolgreich parallelisiert. Allerdings ist es sinnvoll, auch
die rechte Seite der Gleichung parallel zu berechnen, da für große Gitter und eine
hohe Anzahl von Datenpunkten hier sonst ein Flaschenhals entsteht, obwohl man das
Ergebnis zum Lösen der Gleichung nur einmal berechnen muss.
Es müssen lediglich wieder die Level und Indices der Gridpunkte (zur Berechnung
von ϕ(x)), sowie die Datenpunkte selbst in den Kernel geladen werden. Jeder Thread
kann dann
PNwieder einen Eintrag des Ergebnis Vektors berechnen, gemäß der Formel
1
fj = M i=1 ϕj (xi ), wobei ϕj die Basisfunktion des Gridpunkts j ist.
3
3.1
k-nearest-neighbors
Aufbau des Graphen
Der erste Schritt des Algorithmus besteht aus der Konstruktion eines NachbarschaftsGraphen zur Repräsentation der Datenpunkte. Hier haben wir uns für die Brute-Force-
8
Abbildung 5: Schema des Data-Kernels
Variante des Algorithmus entschieden, da diese besonders gut parallelisierbar ist. Bei
dem verwendeten Nachbarschafts-Graphen handelt es sich um einen gerichteten ungewichteten Graphen. Die Konstruktion des Graphen erfolgt, wie in [Bis06] beschrieben.
Dieser Algorithmus hat eine Laufzeitkomplexität von O(d · n2 ), wobei d die Dimension
und n die Anzahl der Datenpunkte darstellt, da für jeden Punkt der Abstand von
diesem zu jedem anderen Punkt berechnet werden muss, was jeweils O(d) Operationen
benötigt.
3.2
Elimination von Knoten/Kanten
In Abhängigkeit von dem gewählten k ist der Graph mehr oder weniger zusammenhängend, zumindest jedoch sollte das k groß genug sein, dass alle Punkte, die dem selben
Cluster zugeordnet werden können, in der gleichen Zusammenhangskomponente repräsentiert sind. Das Ziel der Knotenelimination ist es, dass jedes Cluster im Graph
durch eine Zusammenhangskomponente dargestellt wird. Da nicht jeder Knoten einem Cluster zugeordnet werden kann, müssen zu diesem Zweck einzelne Knoten sowie
adjazente Kanten entfernt werden.
Dazu wird die errechnete Dichtefunktion auf jedem Knoten ausgewertet und diejenigen
Knoten, bei denen das Ergebnis unter einen Schwellwert fällt, sowie deren adjazente
Kanten, werden aus dem Graph entfernt. Bei vielen Problemen reicht dies schon aus,
um alle Cluster zu erkennen. Nahe beieinander liegenden Clustern können jedoch Verbindungen untereinander haben. Da jedoch beide Knoten zu einem Cluster gehören,
würde keiner der Knoten entfernt werden und die beiden Cluster würden als eines erkannt. Um dies zu vermeiden, kann man die Dichtefunktion zusätzlich auf den Kanten
9
zwischen den Knoten auswerten und damit einzelne Kanten aus dem Graph entfernen,
ohne dass Knoten entfernt werden müssen. Um höhere Genauigkeit zu erreichen, kann
man die Auswertung auch an mehreren Stellen auf einer Kante vornehmen.
4
4.1
Implementierung k-nearest-neighbors
k-nearest-neighbors
Der Graph wird durch eine Adjazenzliste dargestellt, die wir durch einen Vektor von
integer-Arrays implementiert haben. Jeder Eintrag in dem Vektor repräsentiert einen
Knoten und jeder Wert in den integer-Arrays repräsentiert eine Kante. Dabei ist jeder
integer-Wert der Index des Knotens, auf den diese Kante zeigt.
Die Eingabe besteht aus einem Vektor von Datenpunkten v, sowie der Anzahl an
Dimensionen d und einem k. v enthält dabei m double-Werte, von denen immer d
zusammen einen Datenpunkt bilden, d.h. v enthält n = md Datenpunkte. v ist wie folgt
aufgebaut: x1,1 , x1,2 , x1,3 , . . . , x1,d , x2,1 , x2,2 , x2,3 , . . . , x2,d , . . . In einer Schleife über alle
Datenpunkte wird für jeden Punkt zunächst ein Eintrag in der Adjazenzliste erzeugt,
danach werden die adjazenten Kanten berechnet.
Da wir einen ungewichteten, ungerichteten Graph erstellen, ist das Hinzufügen von
Kanten zu einem Knoten unabhängig von allen anderen, da in der Adjazenzliste nur
ausgehende Kanten gespeichert werden. Dadurch, und durch die Verwendung der
Brute-Force-Variante, ist ein besonders hoher Grad an Parallelität erreichbar. Genauer gesagt kann jede Position der Adjazenzliste in einem eigenen Thread berechnet
werden. Dies ist aus zwei Gründen möglich: Zum einen durch die Verwendung eines
gerichteten Graphen, zum anderen sind bei der Berechnung der nächsten Nachbarn
nur Lesezugriffe auf die gemeinsamen Daten nötig. Bei den gemeinsamen Daten handelt es sich um v. Da zur Suche der Nachbarn nur n − 1 mal eine euklidsche Distanz
berechnet werden muss, wozu nur Leseoperationen auf v durchgeführt werden, ist die
Nachbarschaftssuche pro Knoten unabhängig.
Des weiteren schreibt jeder Thread an eine andere Stelle der Adjazenzliste, wodurch
auch hier kein Konflikt entstehen kann.
4.2
OpenCL-Kernel
Die OpenCL-Implementierung ist nach folgendem Prinzip aufgebaut: Das heißt, für
jeden Datenpunkt wird ein eigenes work item verwendet.
4.3
Kantenentfernung
Das Entfernen von Knoten und Kanten kann ebenfalls recht gut parallelisiert werden.
Auf jedem Knoten kann, unabhängig von anderen Knoten, überprüft werden, ob dieser entfernt werden muss oder nicht. Das Entfernen besteht in der Implementierung
10
Abbildung 6: Schema des Graph-Kernels
darin, dass an der entsprechenden Stelle in der Adjazenzliste alle Einträge auf -1 gesetzt werden. Da es sich um einen gerichteten Graphen handelt und keine Kanten, die
auf gelöschte Knoten zeigen, existieren dürfen, muss im Anschluss die Adjazenzliste
durchlaufen und alle Kanten zu dem gelöschten Knoten entfernt, d.h die entsprechenden Array-Einträge auf -1 gesetzt werden. Bei diesem Vorgang ist es möglich, dass zwei
oder mehr verschiedene Threads die gleiche Kante löschen wollen und dadurch mehr
als ein Schreibzugriff auf die selbe Stelle stattfindet. Dies ist jedoch kein Problem, da
jeder der Threads die gleiche Zahl an diese Stelle schreibt.
Einige Probleme machen das Löschen von einzelnen Kanten notwendig, wie das Beispiel in Abbildung 7 zeigt. Dieser Datensatz enthält zwei Cluster, wobei jeder der
vorhandenen Datenpunkte ein Teil eines Clusters ist. Dadurch ist ein Löschen von
Knoten zur Clusterisolierung nicht möglich, wie man mit geplotteter Dichte erkennen
kann (siehe Abbildung 8).
Wenn bei dieser Eingabe das k klein genug gewählt ist, werden die beiden Cluster
trotzdem erkannt. Bei einem etwas zu großen k werden jedoch Kanten hinzugefügt,
welche die beiden Halbmonde verbinden. Eine Auswertung der Dichtefunktion auf
einem Punkt auf dieser Kante würde ein Ergebnis unterhalb des Schwellwerts ergeben,
die Auswertung auf dem entsprechenden Knoten jedoch nicht.
5
5.1
Vorverarbeitung des Datensatzes
Der Datensatz
Ursprung Der Datensatz wurde vom Institut für Photovoltaik an der Universität
Stuttgart in Zusammenarbeit mit der Technischen Universität in Hamburg-Harburg
11
Abbildung 7: Mond ohne Anzeige der Dichte
erstellt. Er enthält eine Vielzahl verschiedener Messdaten, die über den Lauf von acht
Jahren an drei Standorten aufgenommen wurden.
Der erste Standort[Zin10] ist Stuttgart. Wir haben uns bei unserem Projekt auf die
Messdaten des Stuttgarter Standortes beschränkt, da die drei Teildatensätze vollkommen unabhängig voneinander betrachtet werden können. Die beiden anderen Standorte
sind Nikosia in Zypern und Kairo in Ägypten, deren Daten wir aber nicht verarbeiet
haben.
Für mehr Hintergrundinformationen zur Datenerfassung, wie der verwendeten Speichertechnik oder der Motivation hinter manchen der gemessenen Datentypen, verweisen wir auf die Diplomarbeit[Adl13] von Hendrik Adler.
Format Der Datensatz wurde uns als komprimierter Dump übergeben, also im Grunde als gigantisches CSV. Komprimiert hatte er eine Größe von etwa 200 Gigabyte.
Dieser Dump wurde entpackt und in eine PostgreSQL-Datenbank eingespielt. In der
Datenbank nahm er etwa 2.5 Terabyte (2500 Gigabyte) Speicherplatz ein. Der Datensatz bestand aus einer Tabelle, die für jedes Paar von Zeitstempel und SensorID einen
12
Abbildung 8: Mond mit Anzeige der Dichte
Wert enthielt. Daneben enthielt er noch weitere Tabellen mit Statistikwerten zu den
Daten, die für uns nicht relevant waren.
Größe und Zeitspanne Der Datensatz erstreckt sich von Anfang Dezember 2006
bis Mitte/Ende November 2014. Es wurde für jede Sekunde ein Datenwert angelegt.
Allerdings sollte beachtet werden, dass diese Werte oft minutenlang gleich bleiben. Das
liegt an dem Eventsystem, was für die Datenerfassung verwendet wurde[Adl13][WA].
In der Datenbank wurden die Daten in einer dreispaltigen, unindizierten Tabelle abgespeichert. Diese Tabelle hatte die Spalten timestamp, sensorid und value. Somit
existierten für jeden Zeitpunkt 111 Tupel in der Tabelle. Da die Tabelle nicht indiziert
war, wurde das Abfragen aller Werte sehr zeitaufwändig. Auf einer Tabelle ohne Index
muss für jede Abfrage die gesamte Tabelle durchsucht werden. Somit dauerten auch
schon scheinbar sehr einfache Abfragen im Bereich von 4 bis 6 Stunden.
Das war für uns, da wir die Werte sowieso immer nach Zeitpunkt gruppiert benutzen
würden, eine sehr gute Motivation, den Datensatz umzuformen in ein Format, das für
Abfragen deutlich effizienter ist.
Eine weitere Motivation war Speicherplatz:
Für die Universität Stuttgart wurden die Daten von 111 Sensoren verwendet. Die
Daten wurden in der Datenbank mit dem PostgreSQL-Datentyp REAL abgespeichert.
Dieser Datentyp ist eine 32-Bit-Fließkommazahl, oder 4 Byte. Der Zeitstempel war
13
vom Typ TIMESTAMP WITHOUT TIME ZONE, der 8 Byte groß ist. Die SensorID hat den
Type SMALLINT, was eine 2-Byte-Ganzzahl ist.
Somit hatte jedes Tupel eine Größe im Speicher von
8B + 2B + 4B = 14B
Der Datensatz erstreckt sich über groß acht Jahren, somit existieren in der Datenbank
8 · |{z}
365 · |{z}
24 ·
|{z}
Jahre
Tage
Stunden
·
60
· 60}
| {z
≈ 28 · 109
111
|{z}
Minuten, Sekunden Anzahl Sensoren
Tupel. Damit hatte die Datenbank überschlagsweise eine Größe von
14B · 28 · 109 ≈ 400GB
Tatsächlich war die Datenbank sogar 2.5TB (2.5 · 1012 Byte) groß, da noch die Daten
von zwei anderen Standorten und einige für uns nicht relevanten anderen Tabellen
enthalten waren.
Ein Zeitpunkt nahm nach dem alten Speichersystem
111 · 14B = 1544B
Speicherplatz ein. Wir haben uns ein neues Speichersystem überlegt, in dem ein Zeitpunkt durch genau ein Tupel in der Datenbank repräsentiert wird. Dabei werden die
Sensoren in eigene Zeilen gespeichert. Dadurch nimmt ein Zeitpunkt nur noch
8B
|{z}
Zeitstempel
+
·
111
|{z}
4
|{z}
= 452B
Anzahl Sensoren Größe von REAL
Platz ein. Damit hatte die Datenbank noch eine Größe von
452
B
· 8a ≈ 114GB
s
Diese Größe haben wir mit der ummodellierten Datenbank auch tatsächlich erreichen
können.
5.2
Umformatierung des Datensatzes
Wie in 5.1 bereits erklärt wurde, war der ursprüngliche Datensatz in einem für unsere Zwecke sehr ungünstigem Format abgespeichert, in dem Datenbankabfragen wegen
ihrer Dauer nicht vertretbar waren. Daher haben wir uns dafür entschieden, die Datenbank für unsere Zwecke von Grund auf umzustrukturieren.
In der alten Tabelle waren einem Zeitpunkt 111 verschiedene Tupel zugeordnet, in
denen jeweils der Sensor angegeben war, von dem sie stammten. Das ermöglichte es,
14
SensorID Gemessene Größe
Einheit
11 Windgeschwindigkeit
12 Windrichtung
13 Zimmertemperatur1
m · s−1
14
15
16
17
18
19
20
Modul 4
Gleichspannung
Gleichstrom
Modultemperatur
Gleichstromleistung
Gleichstromenergie
Wechselstrom-Wirkleistung
Wechselstromenergie
21
22
23
24
25
26
27
28
29
30
Lufttemperatur (außen)
Solareinstrahlung (Pyrano2 )
Solareinstrahlung 1
Solareinstrahlung 2
Solareinstrahlung 3
Solareinstrahlung 4
Solareinstrahlung 5
Solareinstrahlung 6
Solareinstrahlung 7
Solareinstrahlung 8
31 bis 37
38 bis 44
45 bis 51
52 bis 58
59 bis 65
66 bis 72
73 bis 86
87 bis 93
94 bis 100
101 bis 107
108 bis 114
115 bis 121
Messwerte
Messwerte
Messwerte
Messwerte
Messwerte
Messwerte
Messwerte
Messwerte
Messwerte
Messwerte
Messwerte
Messwerte
Modul
Modul
Modul
Modul
Modul
Modul
Modul
Modul
Modul
Modul
Modul
Modul
5
6
7
8
9
10
11
12
13
14
15
16
◦
◦
C
V
A
◦
C
W
kWh
W
kWh
◦
C
W · m−2
W · m−2
W · m−2
W · m−2
W · m−2
W · m−2
W · m−2
W · m−2
W · m−2
s.
s.
s.
s.
s.
s.
s.
s.
s.
s.
s.
s.
14
14
14
14
14
14
14
14
14
14
14
14
bis
bis
bis
bis
bis
bis
bis
bis
bis
bis
bis
bis
20
20
20
20
20
20
20
20
20
20
20
20
Tabelle 1: Beschreibung der Sensoren des Standorts Stuttgart. Jeweils sieben Sensoren
beschreiben ein Solarmodul, außerdem wurden noch Sonneneinstrahlung und Winddaten sowie Temperatur gemessen.
15
dass Werte unterschiedlicher Sensoren unabhängig voneinander gesammelt und der
Datenbank zugeführt werden konnten. Da das Messsystem so entworfen wurde, dass
die Daten nur einmal täglich, oder sogar noch seltener, von den Messstationen abgeholt
werden mussten, hat diese Datenbankstruktur durchaus Sinn gemacht. Um später mit
den Daten zu arbeiten, war die Struktur allerdings vollkommen ungeeignet.
Die neue Struktur sah es vor, für jeden Sensor eine Spalte in der Datenbank anzulegen.
Dadurch sähe eine Zeile der Datenbank etwa so aus:
28723545, 2013-05-27 12:56:01, 3.417, 35.4, 20.114, ..., 205.114, 0;
Dadurch wurden pro Sekunde in den 8 Messjahren 110 Werte für die SensorID (220
Byte) und 110 Werte für den Zeitstempel (880 Byte) eingespart. Bei grob 252288000
Sekunden fiel das extrem ins Gewicht, wodurch die Datenbank auf eine angenehmere
Größe von etwa 100 Gigabyte reduziert werden konnte.
Durch die Indizierung der Tupel und die drastisch (Faktor 100) reduzierte Anzahl von
Tupeln wurde auch die Zugriffszeit auf Werte aus der Datenbank deutlich niedriger.
Während eine kleine Anfrage auf die alte Datenbank bereits über vier Stunden gedauert hat, ist eine Abfrage auf die neue Datenbank in zwischen 20 Sekunden und fünf
Minuten fertig, je nach Komplexität der Anfrage.
Die Umstrukturierung selber wurde monatsweise durchgeführt, da schon ein Monat
ein Datenvolumen im zweistelligen Gigabyte-Bereich bedeutet. Eine Query zum Auswählen aller Werte für einen Monat kann in Listing 1 gesehen werden.
Listing 1: Auswahl eines Kalendermonats
−− Werte A p r i l 2013
SELECT " timestamp " , " s e n s o r i d " , " v a l u e "
FROM p u b l i c . "Rawdata"
WHERE " timestamp " >= ’ 2013−04−01␣ 0 0 : 0 0 : 0 0 ’
AND
" timestamp " < ’ 2013−05−01␣ 0 0 : 0 0 : 0 0 ’ ;
Unser Vorverarbeitungsprogramm hat dann eine Map3 aufgestellt, die jedem Zeitstempel eine Liste von Werten zuordnet, wobei die Werte gemäß ihrer Reihenfolge den
Sensoren zugeordnet waren. War diese Map fertig aufgebaut, wurde für jedes SchlüsselWert-Paar der Map ein Datentupel aufgestellt und in die neue Tabelle geschrieben.
Diese Prozedur wurde für jeden Monat wiederholt. Das Ganze hat etwa 200 Stunden
gedauert, oder 8 Tage 8 Stunden.
3
Eine Map ist eine Datenstruktur, die Schlüsselwerten Datenwerte zuordnet. Dabei sind die Schlüsselwerte
eindeutig.
16
5.3
Statistiken über die Datenbank
Das große Problem mit der alten Datenbank war, dass es zeittechnisch nicht vertretbar
war, darauf Datenbankanfragen durchzuführen. Das lag daran, dass für die meisten
sinnvollen Abfragen die gesamte Datenbank einmal durchsucht werden musste. Bei
einer Größe von über 1 Terabyte war das eine Sache von mehreren Stunden, je nach
Anfrage auch im zweistelligen Bereich. Ein Grund dafür war, dass die Tabelle keinen
Index hatte, also letztendlich ungeordnet war.
Es war durchaus kein Problem, beliebige Daten schnell zu bekommen, diese waren
allerdings mehr oder weniger zufällig gewählt. Das Problematische daran war, dass
ein für uns verwertbarer Datenpunkt aus 111 Tupeln in der Datenbank bestand. Die
einfachste Methode, zusammengehörige Daten aus dem Datensatz zu bekommen, war,
den Zeitraum einzuschränken, wie im Listing 1 gezeigt. Um diese Daten zu erhalten,
wurde jedoch die gesamte Datenbank einmal durchsucht.
Aus der alten Datenbank direkt Datensätze zu generieren, stand also außer Frage.
Wohlgemerkt wäre es kein Problem gewesen, eine unindizierte Datenbank vorliegen
zu haben, wären die Daten bereits nach Zeitstempel gruppiert gewesen. In dem Falle
hätte man einfach problemlos eine Reihe zufälliger Tupel aussuchen können.
So wäre allerdings die einzige Methode gewesen, zufällige timestamp, sensorid,
value-Tupel zu wählen und dann für jedes die fehlenden 110 Tupel nochmal aus der
Datenbank zu suchen. Das wäre aus Zeitgründen einfach nicht tragbar gewesen.
Interessanterweise war es relativ unerheblich, wie viele Daten aus der alten Tabelle
gelesen werden sollten, die Anfragen haben alle etwa gleich lange gebraucht. Das lag,
wie schon erwähnt, daran, dass die gesamte Tabelle für jede Anfrage durchsucht wurde.
Dahingegen konnten aus der neuen Tabelle Daten in linearer Zeit abgerufen werden,
siehe Abbildung 9. Dass bei einer dermaßen großen Datenbank so gute Zugriffszeiten
von weniger als 20 Sekunden für alles außer extrem große Anfragen gegeben waren, war
in sich schon ein gutes Resultat. Jetzt war es uns möglich, sinnvoll mit der Datenbank
zu arbeiten.
6
6.1
Generierung von Datensätzen
Wahl der Sensoren
Um sinnvoll Daten clustern zu können, musste bedacht werden, dass der ClusteringAlgorithmus bei zu vielen Dimensionen trotz dünner Gitter sehr schnell langsamer
wird. Daher war es essenziell, sich auf eine moderate Anzahl an Sensoren zu beschränken. Das beinhaltete auch, Sensoren zu wählen, deren Cluster physikalisch sinnvolle
Ergebnisse liefern würde.
Unsere erste Entscheidung war es daher, die Sensoren 12 und 13 – Windrichtung
und Zimmertemperatur – vollständig außer Acht zu lassen. Zudem musste vermieden
werden, dass Messwerte unterschiedlicher Module kombiniert werden. Unser erster
generierter Datensatz sah deshalb vor, folgende Sensoren auszuwählen:
17
Abbildung 9: Zugriffszeiten auf die neue Tabelle in Abhängigkeit der Abfragegröße. Der
Graph ist in doppellogarithmischer Darstellung und hat eine Steigung von 1, was ein
Wachstum von O(n) beschreibt. Die neue Tabelle hat 217849954 Tupel, daher wächst
die Zugriffszeit hier nicht weiter an.
SensorID Gemessene Größe
11 Windgeschwindigkeit
14
15
16
17
18
19
20
Gleichspannung UDC Modul 4
Gleichstrom IDC Modul 4
Modultemperatur Modul 4
Gleichstromleistung PDC Modul 4
Gleichstromenergie EDC Modul 4
Wechselstrom-Wirkleistung PAC Modul 4
Wechselstromenergie EAC Modul 4
21 Lufttemperatur (außen)
22 Solareinstrahlung (Pyrano)
Einheit
m
s
V
A
◦
C
W
kWh
W
kWh
◦
C
W · m−2
Tabelle 2: Gewählte Sensoren für den ersten Datensatz. Hierbei haben wir uns auf das
Solarmodul 4 beschränkt.
18
Damit konnten wir unser Clustering für das Solarmodul 4 durchführen. Bei Bedarf
können hier die Sensoren für Modul 4 gegen die eines beliebigen anderen Moduls
ausgewechselt werden.
6.2
Wahl der Umgebungsbedingungen
Auch wichtig für die Generierung eines sinnvollen Datensatzes ist die Wahl des betrachteten Zeitintervalls. Es ist sinnvoll, nicht alle Daten auf einmal anzuschauen, sondern
nach Tages- oder Jahreszeit zu filtern, je nachdem, was man aus den Daten ablesen
will. Da es sich um Daten eines Solarsensors handelt, ist es relativ unsinnig, Werte in
Betracht zu ziehen, bei denen es Nacht ist.
Unser Testdatensatz hatte folgende Einschränkungen:
Listing 2: Zeitliche Einschränkungen unseres Datensatzes
date_part ( ’ hour ’ , " timestamp " ) >= 5
AND date_part ( ’ hour ’ , " timestamp " ) < 19
Das heißt, es wurden nur Daten ausgewählt, deren Zeitstempel einen Stundenwert h
mit 5 ≤ h < 19 enthalten. Damit wurden die meisten Nachtdaten ausgeschlossen. Ein
zweiter Datensatz hat zudem alle Werte ignoriert, die nicht in den drei (meteorologischen) Sommermonaten lagen:
Listing 3: Zeitliche Einschränkungen des Sommerdatensatzes
date_part ( ’ hour ’ , " timestamp " ) >= 5
AND date_part ( ’ hour ’ , " timestamp " ) < 19
AND date_part ( ’ month ’ , " timestamp " ) >= 7
AND date_part ( ’ month ’ , " timestamp " ) <= 9
Aus dem so generierten Datensatz konnten wir schon einige Trends erkennen, als wir
jeweils zwei Datenreihen gegeneinander geplottet haben. Mehr dazu in Unterkapitel
6.3.
6.3
Einige Beispiele aus dem generierten Testdatensatz
Aus dem Testdatensatz haben wir mit matplotlib.pyplot, einer Python-Bibliothek,
Graphen aus allen Sensorpaaren erstellt. Die Graphen 10a, 10b, 11a, 11b, 12a, 12b sind
19
2d-Histogramme, das heißt, der Wertebereich wird in Quadrate unterteilt, deren Wert
bestimmt wird durch die Anzahl der Datenpunkte innerhalb des Quadrats. Dieser
Wert wird dann durch die Farbe des Quadrats repräsentiert. Diese Art von Graph
wird dann nützlich, wenn man zu viele Datenpunkte für einen Scatter-Plot hat. Bei
diesem werden einfach alle Datenpunkte eingezeichnet. Dies wird irgendwann sehr
nichtssagend, wenn zu viele Punkte eng zusammen liegen. Aus einem Histogramm
lässt sich jedoch die Punktdichte ablesen.
Wie in Abschnitt 6.5 erklärt, wurden alle Dimensionen im Datensatz auf das Einheitsintervall [0, 1] herunterskaliert. Das Ursprungsintervall ist bei allen Graphen angegeben. Die Skalierung war linear.
Abbildung 10: (l) Histogramm von Gleichspannung des Moduls 4 und Außentemperatur. Hier lassen sich einige potentielle Cluster erkennen.
Die Spannung wurde vom Intervall [−1V, 245V] runterskaliert, die Außentemperatur
von [−17.7◦ C, 35.6◦ C].
(r) Histogramm von Gleichstrom zu Gleichstromleistung. Die Linien entstehen durch
unterschiedliche Spannungen des Moduls gemäß der physikalischen Gesetzmäßigkeit
P
= U.
I
Der Strom wurde vom Intervall [−1A, 10.4A] runterskaliert, die Leistung von
[−3.6W, 1870W].
6.4
Behandlung von null-Werten
Beim Messen von Daten kann es von Zeit zu Zeit vorkommen, dass ein Sensor ausfällt.
Dadurch entstehen Lücken im Datensatz. Diese müssen beim Auswerten der Daten
berücksichtigt werden.
Das Standardvorgehen für fehlende Daten ist es, die Datenwerte aus den umliegenden
Werten zu interpolieren, mit der Argumentation, dass sich die Werte innerhalb von
nur wenigen Zeitschritten nicht groß ändern werden. Das ist, vor allem bei sehr großen
Datensätzen, wo kleine Fehler in der puren Menge von Daten verschwindende Relevanz
haben, ein akzeptables Vorgehen, wenn mal ein oder zwei Werte fehlen.
20
Abbildung 11: (l) Histogramm der Modultemperatur gegen die Lufttemperatur. Man
sieht, dass beide meist linear abhängig sind. Bei höherer Lufttemperatur ist die Modultemperatur zeitweise deutlich höher. Dies lässt sich auf Aufwärmung durch Sonneneinstrahlung zurückführen.
Die Modultemperatur wurde von [−19.6◦ C, 63.1◦ C] runterskaliert, die Außentemperatur von [−17.5◦ C, 35.7◦ C]
(r) Histogramm der Modultemperatur gegen die Sonneneinstrahlung.
Die Modultemperatur
wurde
von [−19.6◦ C, 63.1◦ C] runterskaliert, die Sonneneinstrah
W
W
lung von 0 m2 , 1405 m2
Abbildung 12: (l) Histogramm der Gleichstromenergie gegen die Lufttemperatur.
Die Gleichstromenergie wurde von [0kWh, 11.3kWh] runterskaliert, die Außentemperatur von [−17.5◦ C, 35.7◦ C]
(r) Histogramm der Lufttemperatur gegen die Sonneneinstrahlung.
Die Modultemperatur
wurde
von [−17.5◦ C, 35.7◦ C] runterskaliert, die Sonneneinstrah W
lung von 0 m2 , 1405 mW2
21
Im Photovoltaikdatensatz wurden durch die Verwendung von SenseTrace[WA] einzelne
kurze Ausfälle bereits behoben, siehe dazu Abschnitt 6.2. Um solche mussten wir uns
also nicht mehr kümmern. Übrig blieben Ausfälle in der Größenordnung von einem Tag
bis zwei Monaten. Bei solchen Sensorausfällen gibt es nichts mehr, was man machen
kann, um die Daten wieder herzustellen.
Wir haben uns daher dafür entschieden, bei der Auswahl der Datensätze Tupel zu
ignorieren, in denen mindestens eine von uns ausgewählte Datenreihe null ist. Tatsächlich lagen die null-Intervalle bei den meisten Sensoren auch auf den gleichen
Zeitabschnitten, wodurch ein Großteil der Tupel vollständig vorhanden war.
Die Datenbank hat im Bereich von 0.5% null-Werte. Damit ist sie noch zu sehr großen
Teilen verwendbar.
6.5
Abspeichern der Datensätze
Für das Clustering wurden kleinere Datensätze aus dem Gesamtdatensatz generiert,
da es unrealistisch war, den vollständigen Datensatz auf einmal zu clustern. Diese
Testdatensätze enthielten etwa 10 Sensoren (siehe Abschnitt 6.1) und zwischen 1000
und 20000 Tupel.
Die Bibliothek Weka[Wai], die zum Arbeiten mit großen Datensätzen entwickelt wurde,
hat auch ein Speicherformat für Datensätze, das ARFF4 -Format, entwickelt. Dieses
hat einen Header, der eine Beschreibung des Datensatzes sowie den Typ und Namen
jeder Dimension angibt. Darunter folgen die Daten im CSV5 -Format.
Diese Datensätze sehen dann etwa folgendermaßen aus:
Listing 4: Auszug aus einem ARFF-Datensatz
@RELATION " t e s t d a t a "
@ATTRIBUTE
@ATTRIBUTE
...
@ATTRIBUTE
@ATTRIBUTE
s e n s o r 1 1 NUMERIC
s e n s o r 1 4 NUMERIC
s e n s o r 2 1 NUMERIC
s e n s o r 2 2 NUMERIC
@DATA
0.203778 ,0.618368 ,... ,0.0763272
0.0359501 ,0.00126472 ,... ,0.0609097
0.0528662 ,0.647453 ,... ,0.103448
0.28476 ,0.607578 ,... ,0.130055
0.0697747 ,0.576479 ,... ,0.0538646
4
5
attribute-related file format
Comma Separated Values, also Daten im Textformat, die Spalten durch Kommas getrennt.
22
0.0666449 ,0.656422 ,... ,0.0732332
...
Ein solcher Datensatz kann, da er sich im Textformat befindet, noch stark komprimiert werden. Der Vorteil daran, die Daten nicht on-the-fly beim Clustering selbst zu
generieren war, dass die Datensätze nur einmal generiert werden müssen.
7
Resultate
Der in den Kapiteln zuvor beschriebene Algorithmus zur Clusterbestimmung mittels
Dichteschätzung und Graphverfahren wurde erfolgreich implementiert, parallelisiert
und getestet. Zum Testen haben wir eine eigene Testumgebung in Python geschrieben,
welche es sehr leicht macht Musterdatensätze zu generieren bzw zu laden und darauf
den Algorithmus zur Clusterbestimmung anzuwenden. Diese Testumgebung erlaubt es
auch einfach mithilfe einer grafischen Oberfläche die Plattform und das Gerät auf dem
die OpenCL Kernel der Subalgorithmen ausgeführt werden zu ändern. Die Graphen
7 8 1 wurden zum Beispiel mit dieser Testumgebung erzeugt! Als Musterdatensätze
können in dieser Testumgebung Datensätze wie die Halbmonde ersteller werden, aber
zweidimensionale Datensätze bei denen sich die Datenpunkte um eine beliebige Anzahl
Cluster herum konzentrieren.
Auch die Auswertung von höherdimensionalen Datensätzen funktioniert! Wir haben
Die Auswertung erfolgte auf der Xeon Phi mit einem Datensatz von 100 000 Datenpunkten bei 10 Dimensionen. Die Laufzeiten der einzelnen Schritte sind in den
folgenden Graphen eingetragen.
1.138s
Rechte Seite
der Gleichung
berechnen
0.046s
LGS berechnen
(CG Vefahren)
181.6s
Kanten Entfernen + Cluster suchen
21.53s
Graph erstellen
(k nearest neighbors
Mit einer Parameterbelegung von k = 6 erhalten wir 731 Cluster. Allerdings befinden
sich beinahe alle Datenpunkte in einem einzigen Cluster (37447 Datenpunkte) oder
in überhaupt keinem Cluster da ihre Dichte zu gering ist (61425 Datenpunkte). Die
restlichen Cluster haben sehr wenige Datenpunkte (meist nur zwischen 1 und 30).
Die Datenpunkte dieses Datensatzes scheinen sich also hauptsächlich um ein Cluster
herum zu verteilen, allerdings mit einer recht geringen Dichte wie man an der Anzahl
der Datenpunkte sieht die es in kein Cluster geschaft haben. Die kleineren Cluster sind
23
dann durch kleinere Anhäufungen an Datenpunkten zu erklären die um das eigentliche
Cluster herum auftreten und es über die Dichteschwelle schaffen.
8
Fazit
Die Implementierung und die Vorverarbeitung der Daten waren erfolgreich. Wir haben
zwar nicht mehr den kompletten Datensatz geclustert, jedoch die Vorraussetzungen
dafür geschaffen. Der Algorithmus wurde auf der Zielarchitektur ausgeführt und hat
zufriedenstellende Ergebnisse geliefert. Die Datenbank ist nun in einem Zustand, mit
dem man sehr gut arbeiten kann.
24
Literatur
Adl13.
Bis06.
Adler, Hendrik: Langzeitüberwachung von Photovoltaikanlagen. 2013. – Diplomarbeit
Bishop, Christopher M.: Pattern Recognition and Machine Learning (Information Science and
Statistics). Secaucus, NJ, USA : Springer-Verlag New York, Inc., 2006. – ISBN 0387310738
Gro.
Group, The PostgreSQL Global D.: PostgreSQL 8.4.22 Documentation. http://www.postgresql.
org/docs/8.4/static/
McK12. McKinney, W.: Python for Data Analysis: Data Wrangling with Pandas, NumPy, and IPython.
O’Reilly Media http://books.google.de/books?id=v3n4_AK8vu0C. – ISBN 9781449323615
Pfl10.
Pflüger, Dirk: Spatially Adaptive Sparse Grids for High-Dimensional Problems. Verlag Dr. Hut
http://www5.in.tum.de/pub/pflueger10spatially.pdf. – ISBN 9783868535556
PPB12. Peherstorfer, Benjamin ; Pflüger, Dirk ; Bungartz, Hans-Joachim: Clustering Based on
Density Estimation with Sparse Grids. In: KI 2012: Advances in Artificial Intelligence Bd. 7526,
Springer, Oktober 2012
Ver.
Vermeulen, Jeroen T.: libpqxx C++ PostgreSQL Online Documentation. pqxx.org/devprojects/
libpqxx/doc/2.6.4/html/Reference/
WA.
Wurster, Thomas ; Adler, Hendrik: SenseTrace. Internes Dokument, . – Handbuch
Wai.
Waikato, Machine Learning G. o.: Weka Online Documentation. http://www.cs.waikato.ac.nz/
ml/weka/documentation.html
Zin10. Zinßer, Bastian: Jahresenergieerträge unterschiedlicher Photovoltaik-Technologien bei verschiedenen klimatischen Bedingungen. 2010
25
Zugehörige Unterlagen
Herunterladen