Ausarbeitung korrigiert (insbes. Laufzeitanalyse), Rechtschreibfehler in DCB/Structures.hs korrigiert

This commit is contained in:
tpajenka 2014-04-04 15:04:36 +02:00
parent 64dd652d4a
commit 3555ed5073
3 changed files with 20 additions and 18 deletions

Binary file not shown.

View File

@ -177,36 +177,39 @@ Die Wahl der Programmiersprache zur Verwirklichung des Projekts beeinflusst star
Unser Projekt geht in eine etwas andere Richtung. Bei imperativer Programmierung muss ein großes Augenmerk auf die Vermeidung unerwünschter wechselseitiger Beeinflussungen verschiedener Threads und Prozesse gelegt werden, die fehlerhafte Rechenergebnisse zur Folge haben. Außerdem muss bei der Thread-/Prozesskommunikation immer die Gefahr von Verklemmungen beachtet werden, die schlimmstenfalls zu einem kompletten Stillstand der Programmausführung führen. Beide Probleme sind schwierig zu detektieren und zu lokalisieren.\par
Die genannten klassischen Probleme des Parallelrechnens können mit pur funktionaler Programmierung gut vermieden werden. Nebenbedingungen treten in pur funktionalem Programmcode (einen korrekten Compiler/Interpreter vorausgesetzt) garantiert nicht auf. Da das DCB-Problem bis auf das Einlesen der Eingabedaten und die Ausgabe pur funktional realisierbar ist, ist es optimal für eine derartige Implementierung geeignet Die konkrete Wahl der funktionalen Programmiersprache fiel auf \emph{Haskell}. \par
\medskip
Für Haskell wurden Bibliotheken entwickelt, die eine einfache und effiziente Programmierung paralleler Programme erlauben. Wir verwenden das Paket \emph{parallel} in Verbindung mit \emph{repa}-Arrays. Durch \emph{parallel} können geeignete Algorithmen mit wenig Aufwand, aufgeteilt werden. Diese Funktionsaufrufe werden unevaluiert in ein Array gepackt und dort von freien Threads abgearbeitet. Diese Technik nennt man Work-Stealing und die noch nicht ausgewerteten Funktionen werden in Haskell \emph{Sparks} genannt. Man kann sich dies als einen auf den Funktionsaufruf beschränkten light-weight Thread vorstellen - mit weniger Overhead. Die \emph{repa}-Arrays bieten Funktionen, um die einzelnen Arrayelemente parallel zu berechnen. Mit diesen Techniken lässt sich sequentieller Programmcode einfach parallelisieren, da hierfür nur wenige Änderungen erforderlich sind. Es müssen lediglich die Berechnungsfunktionen an die parallelisierende Funktion übergeben und die Funktion zur Auswertung der Arrayelemente ausgetauscht werden. \par
Für Haskell wurden Bibliotheken entwickelt, die eine einfache und effiziente Programmierung paralleler Programme erlauben. Wir verwenden das Paket \emph{parallel} in Verbindung mit \emph{repa}-Arrays. Durch \emph{parallel} können geeignete Algorithmen mit wenig Aufwand, aufgeteilt werden. Dabei werden Funktionsaufrufe unevaluiert in einem Array gespeichert und dort von freien Threads abgearbeitet. Diese Technik nennt man Work-Stealing und die noch nicht ausgewerteten Funktionen werden in Haskell \emph{Sparks} genannt. Man kann sich dies als einen auf den Funktionsaufruf beschränkten light-weight Thread vorstellen -- mit weniger Overhead. Die \emph{repa}-Arrays bieten Funktionen, um die einzelnen Elemente eines Arrays parallel zu berechnen. Mit diesen Techniken lässt sich sequentieller Programmcode einfach parallelisieren, da hierfür nur wenige Änderungen im Programmcode erforderlich sind. Es müssen lediglich die Berechnungsfunktionen an die parallelisierende Funktion übergeben und die Funktion zur Auswertung der Arrayelemente ausgetauscht werden. \par
\medskip
Zwei wichtige Punkte müssen dennoch beachtet werden. Zum einen verwendet Haskell das Konzept \emph{\en{Lazy Evaluation}}. Befehle werden immer nur soweit berechnet, wie sie an anderer Stelle benötigt werden. Dadurch entstehen manchmal zur Laufzeit große Bäume nur teilweise ausgewerteter Befehle, welche die Ausführungszeit durch eine hohe Garbage-Collector-Auslastung stark negativ beeinflussen. Es muss demnach darauf geachtet werden, die Berechnung später ohnehin erforderlicher Funktionen frühzeitig zu erzwingen. Zum anderen ist die Anzahl der Sparks standardmäßig nicht begrenzt, sodass auch hier zu große Arrays entstehen können, deren Abarbeitung allerdings im Verlaufe des Programms durch o.g. Lazy Evaluation evtl. gar nicht erforderlich ist. Daher beschränken wir die Anzahl der möglichen Sparks (und somit der maximal Möglichen Worker-Threads) auf 1000. Erwähnenswert ist noch, dass diese Technik \emph{nicht} von Hyper-Threading profitiert (da nichtmal mehr ein Kontextwechsel der Threads nötig ist) und wir somit 1000 \glqq echte\grqq \ Kerne für eine maximale Auslastung benötigen. Die obere Grenze wird hier dann eher durch Amdahls Gesetz, denn durch die verfügbaren Kerne beschränkt.
Zwei wichtige Punkte müssen dennoch beachtet werden. Zum einen verwendet Haskell das Konzept \emph{\en{Lazy Evaluation}}. Befehle werden immer nur soweit berechnet, wie sie an anderer Stelle benötigt werden. Dadurch entstehen manchmal zur Laufzeit große Bäume nur teilweise ausgewerteter Befehle, welche die Ausführungszeit durch eine hohe Garbage-Collector-Auslastung stark negativ beeinflussen. Es muss demnach darauf geachtet werden, die Berechnung später ohnehin erforderlicher Funktionen frühzeitig zu erzwingen. Zum anderen ist die Anzahl der Sparks standardmäßig nicht begrenzt, sodass auch hier zu große Arrays entstehen können, deren Abarbeitung allerdings im Verlaufe des Programms durch o.\,g. Lazy Evaluation evtl.\ gar nicht erforderlich ist. Daher beschränken wir die Anzahl der möglichen Sparks (und somit der maximal möglichen Worker-Threads) auf 1000. Erwähnenswert ist noch, dass diese Technik \emph{nicht} von Hyper-Threading profitiert,da kein Kontextwechsel der Threads nötig ist, und wir somit 1000 \glqq echte\grqq \ Kerne für eine maximale Auslastung benötigen. Die obere Grenze wird eher durch Amdahls Gesetz, denn durch die verfügbaren Kerne beschränkt.\par
\section{Der Algorithmus}
Der DCB-Algorithmus besteht aus einer Vorverarbeitungsphase, in der Cluster-Seeds aus 2 jeweils verbundenen Knoten generiert werden und einer anschließenden Expansion dieser Seeds unter Berücksichtigung der in \ref{dcb} vorgestellten Nebenbedingungen (Constraints). Diese Cluster (im Folgenden Graphen genannt) bestehen zu Anfang aus genau 2 Knoten, die sämtliche Bedingungen erfüllen. Eine erste Optimierung findet nun statt, da es hiernach auch Knoten geben kann, die nicht zur initialen Bildung der Graphen begetragen haben. Diese können im Folgenden komplett ausgeschlossen werden, da die einzige Bedingung, die diese nicht erfüllen konnten eine Attributs-Bedingung sein muss\footnote{Ein Graph aus 2 verbundenen Knoten ist immer maximal dicht und zusammenhängend.}. Folglich würde eine Hinzufügung dieses Knotens zu einem beliebigen Graphen diesen unweigerlich auch insgeamt gegen selbige Attributsbedingung verstossen lassen. Alle hiervon betroffenen Knoten können somit aus der Adjazienzmatrix gelöscht werden.
Der DCB-Algorithmus besteht aus einer Vorverarbeitungsphase, in der Cluster-Seeds aus 2 jeweils verbundenen Knoten generiert werden, und einer anschließenden Expansion dieser Seeds unter Berücksichtigung der in Abschnitt~\ref{dcb} vorgestellten Nebenbedingungen (Constraints). Diese Cluster (im Folgenden Graphen genannt) bestehen zu Anfang aus genau 2 Knoten, die sämtliche Bedingungen erfüllen. Eine erste Optimierung findet nun statt, da es nach diesem Schritt auch verbundene Knoten geben kann, die nicht zur initialen Bildung der Graphen beigetragen haben. Diese Paare können im Folgenden komplett ausgeschlossen werden, da sie die Attributsbedingung nicht erfüllen können\footnote{Ein Graph aus 2 verbundenen Knoten ist immer maximal dicht und zusammenhängend.}. Folglich würde jeder Graph mit diesem Knotenpaar insgesamt auch gegen selbige Attributsbedingung verstoßen. Alle hiervon betroffenen Kanten können somit aus der Adjazenzmatrix gelöscht werden.\par
\begin{figure}[h!]
\centering
\includegraphics[scale=0.5,keepaspectratio=true]{./img/DCB-Module.png}
% DCB-Module.png: 1024x512 pixel, 96dpi, 27.09x13.54 cm, bb=0 0 768 384
\caption{Übersicht über die Hierachie des DCB-Moduls. Gelbe Funktionen sind nach außen hin sichtbar.}
\caption{Übersicht über die Hierarchie des DCB-Moduls. Gelb hinterlegte Funktionen sind nach außen hin sichtbar.}
\label{fig:DCB-Overview}
\end{figure}
Wir exportieren 2 Funktionen nach außen, die in der Lage sind den Graphen zu expandieren: \texttt{step} und \texttt{maxDCB}. \texttt{step} expandiert alle Graphen und liefert diese - allerdings verliert man somit alle Graphen, die nicht expandiert werden konnten. Dies hat den Zweck, dass man eine gewisse mindestzahl an Knoten im Graphen hat. Da die Seeds mit 2 Knoten beginnen, man aber z.\,B. alle DCB mit 4 Knoten oder mehr haben möchte, kann man \texttt{step} so häufig aufrufen, dass alle Graphen mit weniger Knoten gar nicht zurückgegeben werden. Wir verwenden dies in unserem Algorithmus einmalig, da wir nur Cluster mit 3 oder mehr Knoten zurückliefern.\\
Wir exportieren 2 Funktionen nach außen, die in der Lage sind, den Graphen zu expandieren: \texttt{step} und \texttt{maxDCB}. \texttt{step} liefert alle möglichen expandierten Graphen aus einer Liste von bestehenden DCB -- allerdings verliert man somit alle Graphen, die nicht expandiert werden konnten. Als Ergebnis hat man eine gewisse Mindestzahl an Knoten im Graphen. Da die Seeds mit 2 Knoten beginnen, man aber z.\,B. alle DCB mit 4 Knoten oder mehr haben möchte, kann man \texttt{step} so häufig aufrufen, dass alle Graphen mit weniger Knoten gar nicht zurückgegeben werden. Wir verwenden dies in unserem Algorithmus einmalig, da wir nur Cluster mit 3 oder mehr Knoten zurückliefern.\par
\medskip
Die Funktion maxDCB übernimmt die eigentliche Arbeit, sodass wir diese im Detail besprechen werden. Zunächst jedoch geben wir einen kleinen Überblick über die hilfs-Funktionen im Hintergrund:
Die Funktion \texttt{maxDCB} übernimmt die eigentliche Berechnung, sodass wir diese im Detail besprechen. Zunächst jedoch geben wir einen kleinen Überblick über die Hilfsfunktionen im Hintergrund:
\begin{description}[style=multiline,leftmargin=2.75cm,font=\bfseries]
\item[filterLayer] filtert eine Menge an Graphen, indem es Duplikate herausfiltert. \\ Laufzeit: $\mathcal{O}(n \log n)$
\item[constraint] überprüft, ob der Graph noch die Constraints erfüllt und wenn ja, wie diese aussehen. \\ Laufzeit: $\mathcal{O}(n^2)$\todo{korrekt?}
\item[updateDensity] errechnet die Änderung der Dichte des Graphen anhand des hinzuzufügenden Punktes. \\ Laufzeit: $\mathcal{O}(n \cdot m)$ bei m Knoten im Ursprungsgraph
\item[reduceDim] ist eine interne Hilfsfunktion, die eine Dimension einer Array-Shape verwirft. \\ Laufzeit: $\mathcal{O}(1)$
\item[addablePoints] traversiert die Adjazienzmatrix und liefert bei gegebenem Graphen alle potentiell hinzufügbaren Knoten zurück. \\ Laufzeit: $\mathcal{O}(n \cdot m)$ bei m Knoten im Ursprungsgraphen.
\item[addPoint] fügt einen neuen Knoten zu einem bestehenden Graphen hinzu, indem es zunächst ein \texttt{updateDensity} macht und anschließend mittels \texttt{constraint} überprüft, ob alle Nebenbedingungen noch erfüllt sind. \\ Laufzeit: $\mathcal{O}(n^2 + n \cdot m)$, falls die Density-Constraint erfüllt blebit, $\mathcal{O}(n \cdot m)$, falls nicht.
\item[expand] wendet \texttt{addPoint} auf alle Ergebnisse von \texttt{addablePoints} an.
\item[filterLayer] filtert eine Menge von $n$ Graphen, indem es Duplikate herausfiltert. \\ Laufzeit: $\mathcal{O}(n \log n)$. \todo{ist da mit drin, dass der Vergleich zweier Graphen nicht in O(1) geht?}
\item[constraint] überprüft, ob der Graph noch die Constraints erfüllt und wenn ja, wie diese aussehen. \\ Laufzeit: $\mathcal{O}(k)$ bei $k$ Attributen, da die Maximal- und Minimalwerte des Graphen tabelliert werden.\todo{korrekt? jetzt ja}
\item[updateDensity] errechnet die Änderung der Dichte des Graphen anhand des hinzuzufügenden Punktes. \\ Laufzeit: $\mathcal{O}(m)$ bei $m$ Knoten im Ursprungsgraph, da die Graphendichten gespeichert werden.
\item[reduceDim] ist eine interne Hilfsfunktion, die eine Dimension einer Array-Shape verwirft. \\ Laufzeit: $\mathcal{O}(1)$.
\item[addablePoints] traversiert die Adjazenzmatrix und liefert alle mit einem Graphen verbundenen Knoten, die nicht selbst im Graphen enthalten sind. \\ Laufzeit: $\mathcal{O}(n \cdot m)$ bei $m$ Knoten im Ursprungsgraphen und einer Adjazenzmatrix $n\times n$.
\item[addPoint] erweitert wenn möglich einen bestehenden Graphen um einen Knoten, indem es zunächst ein \texttt{updateDensity} durchführt und anschließend mittels \texttt{constraint} alle Nebenbedingungen überprüft. \\ Laufzeit: $\mathcal{O}(k + m)$, falls die Density-Constraint erfüllt bleibt, sonst $\mathcal{O}(k)$.
\item[expand] wendet \texttt{addPoint} auf alle Ergebnisse von \texttt{addablePoints} an.\\
Laufzeit: $\mathcal{O}(n \cdot m \cdot (k + m))$ im worst-case einer voll besetzten Adjazenzmatrix. In der Praxis sind die Eingabegraphen kaum vernetzt und die Attributzahl $k$ ist klein, sodass sich die average-case-Laufzeit unter Berücksichtigung der alternativen Laufzeit von \texttt{addPoint} an $\mathcal{O}(n \cdot m \cdot k) \approx \mathcal{O}(n \cdot m)$ annähert.
\end{description}
\todo[inline]{Laufzeit-Analyse ist doof ... ich überseh sicher was oder mache Fehler. Auch kann ich mit O-Notation nicht zum Ausdruck bringen, was ich eigentlich sagen wollte ... nämlich, dass der Algo zwar in n*n*n läuft, aber wir z.b. bei addPoint in n*m liegen KÖNNEN, wenn die sterne gut stehen..}
\todo[inline]{Laufzeit-Analyse ist doof ... ich überseh sicher was oder mache Fehler. Auch kann ich mit O-Notation nicht zum Ausdruck bringen, was ich eigentlich sagen wollte ... nämlich, dass der Algo zwar in n*n*n läuft, aber wir z.b. bei addPoint in n*m liegen KÖNNEN, wenn die sterne gut stehen..\\
Ich denke, das wird jetzt klar :-)}
\begin{lstlisting}[caption={Die maxDCB-Funktion},label=lst:maxDCB]
-- | Calculates all maximum DCB. A maximum DCB is a densely connected bicluster that
@ -227,10 +230,9 @@ maxDCB gs adj attr dens maxDiv minHit =
-- of this iteration
\end{lstlisting}
Der rekursive Funktionsaufruf findet in Zeile 14 statt. Hier werden rekursiv alle expandierbaren Möglichkeiten evaluiert und alle nicht weiter expandierbaren Graphen angehängt. In Zeile 8/9 wird die Expansion auf den Eingabegraphen \texttt{gs} parallel in einem Buffer von höchstens 1000 parallelen Anweisungen ausgeführt. Die Strategie, welche wir für die parallele Evaluation verwenden ist \texttt{rdeepseq}, was bedeutet, dass diese Graphen nachher vollständig evaluiert vorliegen und nicht (z.B. durch lazy-evaluation) nach-evaluiert werden müssen.\par
Der rekursive Funktionsaufruf findet in Zeile~14 statt. Hier werden iterativ alle expandierbaren Möglichkeiten evaluiert bis sie maximal erweitert sind und an die nicht erweiterbaren Graphen angehängt. In den Zeilen~8/9 wird die Expansion auf den Eingabegraphen \texttt{gs} parallel in einem Puffer von höchstens 1000 parallelen Anweisungen ausgeführt. Die Strategie, welche wir für die parallele Evaluation verwenden, lautet \texttt{rdeepseq}. Dadurch werden diese Graphen direkt vollständig ausgewertet und müssen nicht (z.\,B. durch Lazy Evaluation) nachberechnet werden.\par
\medskip
Anschließend partitionieren wir die expandierten Graphen in bereits maximal Expandierte und in weiter expandierbare (Z.~10). Letztere filtern wir noch (Z.~11) nach duplikaten, um eine redundante Expansion (und damit einen erhöhten Rechenaufwand) zu vermeiden.\\
Zurückgeliefert werden somit alle Graphen, die maximal expandiert sind.
Anschließend partitionieren wir die expandierten Graphen in maximal erweiterte und in weiter expandierbare (Z.~10). Letztere filtern wir noch (Z.~11) nach Duplikaten, um redundante Weiterberechnung (und damit einen erhöhten Rechenaufwand) zu vermeiden. Zurückgeliefert werden somit alle Graphen, die maximal expandiert sind. \par
\section{Ausführung und Auswertung}

View File

@ -23,7 +23,7 @@ type Adj = Matrix A.U Int8
-- It stores the minimum (zeroth column) and maximum (first column) value of all
-- the 'Graph's nodes per attribute.
-- The 'Vector' stores values of @1@ if the bounds are within the allowed range
-- ragarding the corresponding attribute, or @0@ if not.
-- regarding the corresponding attribute, or @0@ if not.
type Constraints = (Vector A.U Int, Matrix A.U Double)
-- | A 'Vector' of weights indicating how much divergence is allowed in which dimension.
-- Each dimension represents an attribute.