• Keine Ergebnisse gefunden

3 Multi-terminal decision diagrams with addition zur komprimierten Darstellung von Matrizen

3.2 Matrixoperationen auf MTTD + s

3.2.5 Matrixelement berechnen

), grundlegend in ihrer Struktur modifiziert werden müssen.

Die Grundidee ist, diejenigen DownStep-Produktionen, die auf dem Weg vom Startsymbol zum gesuchten Matrixelement durchlaufen werden, derart zu modifizieren, dass sie jeweils nur das Nichtterminalsymbol erzeugen, welches die Teilmatrix repräsentiert, in der das gesuchte Matrixelement liegt (dabei benutzen wir eine Art Hilfsproduktion, die lediglich von einem Nichtterminal auf ein weiteres Nichtterminal führt).

Wir müssen also für jede dieser DownStep-Produktionen bestimmen, in welcher der vier erzeugten Teilmatrizen ( ), ( ), ( ), und ( ) sich das gesuchte Element befindet - dies ist jeweils abhängig von der Höhenstufe, in der sich die Produktion befindet. Das bedeutet, dass wir für jede Höhenstufe individuell ausrechnen müssen, um welche Teilmatrix es sich jeweils handelt.

Es ist noch anzumerken, dass wir im Voraus nicht wissen, welche DownStep-Produktionen bei der Erzeugung des Elements durchlaufen werden und welche nicht, sodass wir der Einfachheit halber direkt alle (entsprechend ihrer Höhenstufe) modifizieren. Dies ist ohnehin notwendig, da ein +-Circuit keinerlei DownStep-Produktionen besitzen darf.

Konkret berechnen wir für jede Höhenstufe die Indizes und desjenigen Nichtterminals, welches die gesuchte Teilmatrix erzeugt (z.B. deuten und auf das Nichtterminal ) und modifizieren dementsprechend die DownStep-Produktionen.

Dafür benutzen wir Zahlen und , die jeweils für den Zeilen- und Spaltenindex des gesuchten Elements in der aktuell bearbeiteten Teilmatrix stehen – diese Werte sind wie wir sehen werden veränderlich. Wir fangen bei Höhenstufe an und arbeiten uns Schritt für Schritt bis zu Höhenstufe durch. Klar: Anfangs gilt und .

Wir wissen: Jede Produktion der Höhenstufe erzeugt eine Matrix der Seitenlänge , deren Zeilen und Spalten von bis durchnummeriert sind. Man stelle sich diese Matrix in vier gleichgroße Teile aufgeteilt vor – es gilt nun anhand der Indizes und herauszufinden, in welcher Teilmatrix sich das gesuchte Element befindet: Falls bzw.

, dann befindet sich das gesuchte Element in der oberen bzw. linken Hälfte der Matrix – andernfalls ist es in der unteren bzw. rechten Hälfte zu verorten. Formal bedeutet dies:

{

{

Nun müssen wir noch die Indizes des gesuchten Elements für die nächsttiefere Höhenstufe anpassen – diese sind relativ zur Teilmatrix, auf die sie sich beziehen:

Auf diese Weise lassen sich sukzessive für jede Höhenstufe (bis Höhenstufe 1) die Indizes der gesuchten Teilmatrix bestimmen. Als letzten Schritt wandeln wir nun schlussendlich die DownStep-Produktionen um:

Eine Regel der Form (

), wobei Höhe besitzt, wird zu .

Eine Modifikation der Additions- sowie der Terminalproduktionsproduktionen ist nicht erforderlich, da sie bereits so wie sie sind unserem Zweck genügen.

Die Argumentation zur Laufzeit ist abermals ähnlich zu der in 1.2.3. – auch hier haben wir

Wir wollen unter Benutzung des Algorithmus den Wert in Zeile 4 und Spalte 6 bestimmen (weiß eingefärbt).

Es gilt und . Es ergibt sich sowie , da sowohl 4 als auch 6 größer gleich sind. Demnach müssen wir in der unteren rechten Teilmatrix

weitersuchen (hellgrau unterlegt). Durch Anwenden der oben genannten Formel bekommen wir und . Daraus ergibt sich und , was wiederum der oberen rechten Teilmatrix entspricht (dunkelgrau unterlegt). Wir erhalten und – diesmal ist das gesuchte Element in der oberen linken Teilmatrix zu verorten. Nun sind wir fertig und modifizieren die DownStep-Produktionen entsprechend.

Es ergibt sich ein +-Circuit ( ) mit

 { } (vereinfacht)

 { } Auswerten liefert ( )

3.2.6 Matrixmultiplikation

Ziel ist es, aus zwei MTTD+s ( ) und ( ) einen MTTD+ zu konstruieren, der das Matrixprodukt der beiden Matrizen ( ) und ( ) repräsentiert.

Dazu erzeugen wir für jedes Nichtterminal und jedes Nichtterminal (wobei und die gleiche Höhe haben) ein Nichtterminal ( ) für , sodass (( )) ( ) ( ) gilt – das bedeutet insbesondere ( ) (( )) ( ) ( ) ( ) ( ), womit klar ist, dass der Algorithmus zum gewünschten Ergebnis führt.

Konkret sind vier Fälle zu berücksichtigen:

1. und haben beide die Höhe und ihre zugehörigen Produktionen sind Terminalproduktionen, d.h. und , wobei . Dann gilt trivialerweise ( ) , wobei .

2. Die Regel für ist eine Additionsregel, hat also die Form . Dann gilt ( ) ( ) ( ). Dies ist korrekt, weil auch bei Matrixmultiplikationen das Distributivgesetz gilt: (( )) ( ) ( ) ( ( ) ( )) ( ) ( ) ( ) ( ) ( ) (( )) (( )).

3. Die Regel für ist eine Additionsregel, hat also die Form . Dann gilt ( ) ( ) ( ). Die Argumentation ist vollständig analog zu Fall 2.

4. Die Regeln für A und B sind beides DownStep-Produktionen: (

) und B → (

). Hier ist es lediglich erforderlich, die Regeln der Matrixmultiplikation („Zeile mal Spalte“) zu beachten. Demnach muss gelten:

( ) (

)

Da wir die Produktion so natürlich nicht speichern können, müssen wir für jeden eine Teilmatrix repräsentierenden Term eine eigene Produktion einführen. Letztendlich ergibt sich ( ) (

), wobei ( ) ( ) für { }

Die Produktionsmenge von besteht somit aus allen auf diese Weise gebildeten Produktionen. Aus ebendieser lässt sich natürlich auch die Menge der Nichtterminale herleiten, indem man alle linken Seiten der Produktionen zusammenfasst. Das Startsymbol von ist (( )).

Die Anzahl der zu erstellenden Regeln ist durch ( ) beschränkt, woraus sich die Laufzeit dieses Algorithmus sowie die Größe des resultierenden MTTD+ ergibt.

Wir verzichten auf dieser Stelle an ein Beispiel, da, falls man im Beispiel alle möglichen Fälle behandeln will, bereits die Multiplikation zweier -Matrizen zu einer derart großen Zahl an Produktionsregeln führt, dass es unmöglich ist, das Beispiel übersichtlich zu gestalten.

3.2.7 Gleichheitstest

MTTD+s bieten durchaus die Möglichkeit, zwei identische Matrizen auf unterschiedliche Art und Weise zu komprimieren. Dieser Algorithmus hat den Zweck, zwei mittels MTTD+

komprimierte Matrizen einem Gleichheitstest zu unterziehen und je nach Ergebnis ‚wahr‘

oder ‚falsch‘ auszugeben.

Um zwei Matrizen ( ) und ( ) auf Gleichheit zu testen (wobei und Nichtterminale gleicher Höhe sind und zur Menge einem MTTD+ gehören), baut der Algorithmus im Wesentlichen ein lineares Gleichheitssystem auf, welches im Berechnungsverlauf (bei dem die Grammatik top-down durchlaufen wird) immer wieder erweitert, modifiziert, reduziert und schlussendlich durch Einsetzen von bestimmten Werten für die unbekannten Variablen auf Korrektheit getestet wird.

Genauer speichert der Algorithmus auf jeder Höhenstufe ein LGS von bis zu Gleichungen (wobei durch die Anzahl der Unbekannten im LGS beschränkt ist) der Form

, wobei die Variablen bis Nichtterminalen der Höhe entsprechen.

Das anfängliche Gleichungssystem der Höhe besteht aus einer einzigen Gleichung (was freilich äquivalent zu ist). und sind hier als Variablen zu verstehen – haben diese Variablen den „richtigen Wert“, d.h. führen und ultimativ auf identische Matrizen, geht die Gleichung auf. Mit dieser Initialen Gleichung als Grundlage schlussfolgern wir anhand der Produktionen von G weitere Gleichungen und bauen dadurch herabsteigend von Höhenstufe für jede Höhenstufe ein Gleichungssystem auf. Auf Höhe Null eingekommen setzen wir für alle Nichtterminale, welche nun nur noch mit Terminalproduktionen korrespondieren können, die entsprechenden Terminale ein und überprüfen, ob alle Gleichungen aufgehen.

Um das Gleichungssystem für die nächstniedrigere Stufe zu bestimmen, führen wir nacheinander folgende Schritte durch (aktuelle Höhenstufe ist – Ziel ist es, das Gleichungssystem der Höhenstufe zu berechnen):

1. Standardisieren sämtlicher Gleichungen: Alle Gleichungen sind in die Standardform

zu überführen. Dies ist notwendig, da die Gleichungen nach Durchführung von Schritt 3 bzw. 4 nicht mehr der Standardform genügen – diese ist allerdings für Schritt 2 erforderlich, weswegen alle Gleichungen wieder in ein einheitliches Format überführt werden müssen.

2. Entfernen redundanter Gleichungen: Die maximale Anzahl nicht-redundanter Gleichungen ist wie in Kapitel 1.2.3 beschrieben durch die Anzahl der in den Gleichungen vorkommenden Variablen beschränkt. Da allerdings durch Schritt 3 und 4 die Zahl der Gleichungen die Zahl der vorkommenden Variablen übersteigen kann, ist es gerade bei sehr großen Matrizen unerlässlich, regelmäßig die redundanten Gleichungen zu entfernen. Dies tun wir mithilfe des Gauß-Jordan-Algorithmus.

3. Mit Additionsproduktionen korrespondierende Nichtterminale substituieren: Für jedes im Gleichungssystem vorkommende Nichtterminal der Höhe , welches mit einer

Regel der Form korrespondiert: Ersetze jedes Vorkommen der Variable im LGS durch den Term . Dies ist solange zu wiederholen, bis kein im Gleichungssystem vorkommendes Nichtterminal der Höhe mehr mit einer Additionsproduktion korrespondiert. Da ( ) ( ) ( ) gilt, ändern diese Substitutionen nicht die Aussage der Gleichungen, führen uns aber offensichtlich unserem Ziel näher, die Höhe der in den Gleichungen vorkommenden Nichtterminale zu verringern.

4. Mit DownStep-Produktionen korrespondierende Nichtterminale substituieren: An dieser Stelle korrespondieren ausschließlich Produktionen der Form (

) mit den in den Gleichungen vorkommenden Nichtterminalen der Höhe . Ersetze jedes dieser Nichtterminale durch die rechte Seite seiner korrespondierenden Produktion – wir erhalten dadurch Gleichungen, deren Variablen ausschließlich aus -Matrizen bestehen, welche ihrerseits Nichtterminale der Höhe enthalten.

Schlussendlich zerlegen wir jede Gleichung in vier Gleichungen – eine Gleichung nur mit denjenigen Elementen, welche in den -Matrizen oben links vorkommen, eine mit den Elementen oben rechts, eine mit den Elementen unten links und eine mit den Elementen unten rechts. Auch hier bleiben die Aussagen der ursprünglichen Gleichungen erhalten, sie werden lediglich jeweils in vier Teilgleichungen zerlegt.

Auf Höhenstufe Null angekommen führen wir erneut die Schritte 1-3 durch und setzen anschließend für alle Nichtterminale diejenigen Terminale ein, die ihre korrespondierenden Terminalproduktionen erzeugen, und überprüfen, ob alle Gleichungen aufgehen. Ist dies der Fall, dann ist auch die initiale Gleichung erfüllt, d.h. es gilt ( ) ( ), andernfalls gilt ( ) ( ).

Der dominierende Faktor der Laufzeit dieses Algorithmus ist der zum Entfernen redundanter Gleichungen verwendete Gauß-Jordan-Algorithmus, welcher kubische Laufzeit in der Anzahl der Anzahl der Variablen im LGS besitzt. Diese ist durch die Anzahl der Nichtterminale von beschränkt, welche identisch mit der Anzahl der Produktionen von ist. Wir führen den Gauß-Jordan-Algorithmus maximal einmal je Höhenstufe aus - die Anzahl der Höhenstufen ist ebenfalls durch die Anzahl der Produktionen beschränkt, denn es kann nicht mehr Höhenstufen als Produktionen geben. Daraus ergibt sich die (allerdings sehr grobe) Laufzeitschranke ( ). Additionsproduktion korrespondiert, ersetzen wir in Schritt 3 durch seine Summanden. Wir bekommen die Gleichung ( ) Es existieren nun nur noch Nichtterminale, die mit DownStep-Produktionen korrespondieren.

Wir substituieren diese zunächst - ( ) (( ) ( )) – und teilen die Gleichung nun nach dem beschriebenen Muster in vier Teilgleichungen auf (Schritt 4):

( ) ( ) ( ) ( )

Wir haben nun erreicht, dass alle Nichtterminale im Vergleich zur Eingabegleichung um eine Höhenstufe reduziert sind - insbesondere sind wir auf Höhenstufe 0 angekommen. Zunächst werden alle Gleichungen standardisiert:

In Schritt 2 wird festgestellt, dass nur die ersten beiden Gleichungen essentiell sind, die anderen beiden sind redundant (dies ist offensichtlich, da sie identisch zu den ersten beiden sind).

4 Implementationsdokumentation

Der Code befindet sich als Cabalprojekt unter:

http://www.ki.informatik.uni-frankfurt.de/bachelor/programme/matrizen

In diesem Kapitel werden wir unsere Implementierung anhand des Codes vorstellen, Besonderheiten erklären und eine Übersicht über die theoretischen Laufzeiten geben. In einigen Berechnungen verwenden wir den Buchstaben , um die größte in der Grammatik zu vorhandene Zahl anzugeben. Wie bereits im vorigen Kapitel beschrieben verwenden wir bei in weiten Teilen kein logarithmisches sondern das einfache Maß zur Laufzeitberechnung.

4.1 Datentypen

In diesem Abschnitt stellen wir unsere Datentypen für die Matrizen vor.

data MatrixGrammar a = MatrixGrammar { startSymbol :: NonTerminal,

productions :: [ProductionMap a]

} deriving Show

type ProductionMap a = Map NonTerminal (RightHandSide a) type NonTerminal = String

data RightHandSide a =

DownStep NonTerminal NonTerminal NonTerminal NonTerminal | Addition NonTerminal NonTerminal

| Terminal a

| CircuitStep NonTerminal deriving Show

data Circuit a = Circuit NonTerminal (ProductionMap a) deriving Show

Die Grammatikproduktionen werden aus den Typen NonTerminal und RightHandSide a zu einer ProductionMapa zusammengesetzt, wobei die Typvariable a mindestens eine Instanz von Ord sein muss, um eine Grammatik zu erzeugen, und eine Instanz von Num, um die implementierten Funktionen zu nutzen. Damit können sowohl Float als auch Integer Matrizen dargestellt werden, wobei Matrizen mit Fließkommazahlen nur in wenigen Fällen sinnvoll erscheinen, z.B. bei schwach besetzten Matrizen, da sonst kein großer Kompressionserfolg zu erwarten ist.

Der Typ NonTerminal stellt die linke Seite einer Produktion da. In der Implementierung werden oft nicht Terminale verglichen oder nach der zugehörigen rechten Seite in den ProductionMaps gesucht. Daher haben wir einige Tests zu Zugriffszeiten durchgeführt, welche sich im Anhang zu diesem Kapitel finden. Als mögliche Datentypen haben wir uns hierbei auf Strings und Integer konzentriert, wobei es für Integer bei der Multiplikation nötig gewesen wäre auch Integer Tupel als Nichtterminale zuzulassen, sodass der Typ polymorph sein müsste. In unserer ersten Testphase (siehe nächstes Kapitel) haben wir herausgefunden, dass Strings eine bessere Speichersignatur haben als Tupel und sich die Zugriffszeiten nicht wesentlich voneinander unterscheiden. Daher halten wir die Einschränkung für sinnvoll. Strings bieten außerdem die Möglichkeit Produktionen mit sinnvollen Namen auszustatten, falls dies nötig sein sollte.

Der Typ RightHandSide a stellt die rechte Seite einer Produktion da. Er implementiert vier

 CircuitStep: Eine spezielle Produktion für eindimensionale Grammatiken, welche wir im Folgenden als Circuits bezeichnen. Diese zeigen nur auf ein weiteres Nichtterminal.

Der primäre Datentyp MatrixGrammar enthält eine Liste von ProductionMaps, wobei jede ProductionMap nur Produktionen für eine bestimme Höhe enthält. Die Liste ist absteigend geordnet, sodass in den implementierten Algorithmen die Zugriffszeiten ein wenig minimiert werden können, da nicht alle Produktionen der Grammatik in jedem Schritt von Belang sind.

Des Weiteren enthält der Datentyp noch ein Startsymbol, welches theoretisch aus der ersten Map der Liste erschlossen werden könnte, was aber den Code an manchen Stellen unnötig verkompliziert. Eigentlich definiert die Datenstruktur auch noch eine Menge, in der alle Nichtterminale gesammelt werden. Auf diese haben wir aber verzichtet, da sie implizit durch die Maps gegeben ist und somit nicht noch einmal separat gespeichert werden muss.

Der Typ Circuit a ist ein Hilfstyp, welcher eine 0-dimensionale Grammatik implementiert, dessen Evaluierung einen einzelnen Wert zum Ergebnis hat. Er enthält wie MatrixGrammar ein Startsymbol und eine einzelne ProductionMap.