TU-Chemnitz-Zwickau

Chemnitz, 4.2.97

Fakultät für Informatik

Lehrstuhl Datenverwaltungssysteme

 Praktikum: Datenbanken und Neuronale Netze

Thema: Schreibfehler-tolerante SQL-Anfragen

 

 


 Inhalt:

1. Spezifikation der funktionellen Anforderungen

1.1. Aufgabenstellung

1.2. Modelle funktioneller Anforderungen

1.2.1. Kontextdiagramm

1.2.2. Primäres Verhaltensmodell

2. Modul „Neuronales Netz" (Falk Seifert)

2.1. Schnittstelle des Moduls „Neuronales Netz" (snnsapp.h)

2.2. Nutzung des Moduls „Neuronales Netz" im Normalbetrieb

2.2.1. Netz aufbauen: int NNCreate (char* name)

2.2.2. Netz trainieren: int NNTrain (char* name, int override)

2.2.3. Worterkennung mit dem trainierten Netz: int NNPropagate (char* name, char* word, float* activation)

2.3. Nutzung des Moduls „Neuronales Netz" im „Online"-Betrieb

2.4. Die C-Schnittstelle des SNNS-Kernels

2.5. Erläuterungen zum Fehlergenerator (psdoerr.c)

2.5.1. Die Arbeitsweise des Pseudo-Fehlergenerators (PseudoError())

2.5.2. Die Kodierung der Buchstaben (Tobias Neubert)

2.6. Verbesserungsmöglichkeiten

3. Interaktiver Editor (Tobias Neubert)

4. Datenbankanbindung (Tobias Neubert)

4.1. Statische Datenbankanfrage

4.2. Dynamische Datenbankanfrage

5. Der Parser (Frank Seifert)

 


  

1 Spezifikation der funktionellen Anforderungen

1.1 Aufgabenstellung

SQL-Anfragen an eine Datenbank unterliegen einer exakten Syntax. Diese Tatsache erfordert vom Anwender die genaue Kenntnis der Schreibweise von datenbankspezifischen Namen, die Relationen und Attribute bezeichnen. Weitere Voraussetzung ist das Beherrschen von SQL. Das Ziel ist es, den Anwender beim Finden korrekter Namen zu unterstützen.

Beispiel: Die Datenbank enthält die Relation „Personal". Der Anwender stellt eine Anfrage, die statt des Relationennamens „Personal" nun „Personen" enthält. Gibt es keine Relation mit dem Namen „Personen", so soll das zu entwerfende System in der Lage sein, einen entsprechend ähnlich geschriebenen Relationennamen vorzuschlagen. In diesem Falle wäre das „Personal".

Der zweite Aufgabenbereich des Systems liegt in der Korrektur von Schreibfehlern. Das betrifft Schlüsselwörter sowie datenbankspezifische Namen. Jedoch sollte die Struktur der SQL-Anfrage, wie z.B. Klammerung und Anordnung der Schlüsselwörter, syntaktisch korrekt sein. Eine weitere Forderung ist die korrekte Schreibweise des Schlüsselwortes „from".

Eine vom Nutzer entworfene SQL-Anfrage kann, bevor diese an die Datenbank gestellt wird, zur Untersuchung bzw. Korrektur an das zu entwerfende System gegeben werden.

Im Rahmen dieses Praktikums ist ein System zu realisieren, das mit einer Testdatenbank auf einem an der TU verfügbaren DBMS arbeitet. Die Erkennung und Korrektur von Schreibfehlern und ähnlichen Namen ist mit Hilfe eines neuronalen Netzes zu realisieren.

1.2 Modelle funktioneller Anforderungen

1.2.1 Kontextdiagramm

kontext.jpg (19813 Byte)

1.2.2 Primäres Verhaltensmodell

verhalten.jpg (63724 Byte)

2 Modul „Neuronales Netz" (Falk Seifert)

2.1 Schnittstelle des Moduls „Neuronales Netz" (snnsapp.h)

/* Fehlercodes des Moduls */
#define ERROR_PSEUDOERROR -1
#define ERROR_NET_EXISTS -2
#define ERROR_PATTERN_EXISTS -3
#define ERROR_NO_WORDS -4
#define ERROR_NO_NET -5
#define ERROR_NO_PATTERNS -6
#define ERROR_TOO_FEW_WORDS -7
#define ERROR_NO_VALIDSET -8

/* Parameter für die Generierung der Lern- und Testmuster */
#define NN_NOISE 8
#define NN_PATTERN_CYCLES 10

/* Parameter für das Lernen und Testen */
#define NN_LEARN_CYCLES 0
#define NN_LEARN_RATE 0.2
#define NN_DELTA_MAX 0.1
#define NN_LEARN_ABORT 0.01
#define VALIDATION_DISTANCE 10
#define PROPAGATE_WEIGHTED_CHARS

/* Funktionen zum Normal-Betrieb: */
int NNCreate (char* name);
int NNTrain (char* name, int override);
int NNPropagate (char* name, char* word, float* activation);

/* Funktion zum Online-Betrieb: Erzeugen von Netz und Pattern, trainiern, propagieren und löschen nacheinander */
int GetBestMatchOnline (char* name, char* word, float *activation);

/* Ausgabe eines Fehlertextes entsprechend des Fehlercodes "error_code", siehe ERROR_* - Konstanten */
void OnSNNSAppError (int error_code);

2.2 Nutzung des Moduls „Neuronales Netz" im Normalbetrieb

Das Ziel der drei Funktionen NNCreate(), NNTrain() und NNPropagate() ist es, ein fehlerhaft geschriebenes Wort zu erkennen. Das geschieht, indem das fehlerhafte Wort einem Wort aus einer vorgegebenen Menge von Wörtern mit Hilfe eines Backpropagation-Netzes zugeordnet wird. Zu diesem Zweck sind die erwähnten Funktionen in der angegebenen Reihenfolge zu benutzten.

NNCreate()generiert ein neuronales Netz im Format des SNNS. An passender Stelle kann die Funktion NNTrain() aufgerufen werden, um das generierte Netz zu trainieren. Das Training kann länger dauern, da dieser Prozeß exponentielles Zeitverhalten besitzt. Die Traningsdauer ist abhängig von der Anzahl der zu lernenden Wörter und von der Länge des längsten zu lernenden Wortes.

Soll ein fragliches/ fehlerhaftes Wort einem gelernten Wort der Liste zugeordnet werden, ist die Funktion NNPropagate() anzuwenden.

2.2.1 Netz aufbauen: int NNCreate (char* name)

Der Parameter „name" dient als Basisname. Vor dem Aufruf dieser Funktion muß eine Datei mit dem Name „name.wrd" existieren, welche mindestens 2 durch „newline" getrennte Wörter enthält. (Diese Wörter werden dann später vom Netz gelernt.) Gibt es keine Datei „name.wrd", liefert die Funktion NNCreate einen Fehlercode < 0. Eine verbale Beschreibung des Fehlers kann mit Hilfe der Funktion OnSNNSAppError()ausgegeben werden.

Anschließend wird geprüft, ob schon ein neuronales Netz unter dem Name „name.net" generiert wurde. Ist das der Fall, kehrt die Funktion mit Fehlercode zurück. Andernfalls wird

  1. die Größe des Backpropagation-Netzes berechnet (Anzahl der Neuronen in den 3 Schichten),
  2. das Netzwerk im SNNS-Kernel aufgebaut,
  3. das Netzwerk im SNNS-Format unter „name.net" gespeichert,
  4. das Netzwerk aus dem SNNS-Kernel entfernt.

zu 1.: Die Größe des Netzes umfaßt die Anzahl der Neuronen in den 3 Schichten des Netzwerkes:

Anzahl der Neuronen der Input-Schicht = Anzahl der Buchstaben des längsten Wortes in „name.wrd" * Breite der Kodierung eines Buchstabens (6 Bits)
Anzahl der Neuronen der Output-Schicht = Anzahl der Wörter (Zeilen) in „name.wrd"
Anzahl der Neuronen der Hidden-Schicht = Mittelwert aus Anzahlen in der Input- und Output-Schicht

Beispiel: Die Datei „name.wrd" enthält folgende 3 Zeilen:

SELECT
FROM
WHERE

Dann hat das Netzwerk folgende Dimensionen:

Anzahl der Neuronen der Input-Schicht : 6 Buchstaben * 6 Bit => 36 Neuronen
Anzahl der Neuronen der Output-Schicht : 3 Wörter => 3 Neuronen
Anzahl der Neuronen der Hidden-Schicht : (36+3)/2=19.5 => 19 Neuronen

2.2.2 Netz trainieren: int NNTrain (char* name, int override)

Der Parameter „name" dient wiederum als Basisname. Voraussetzung für die korrekte Abarbeitung der Funktion ist das Vorhandensein der Datei mit den zu lernenden Wörtern „name.wrd" und das Vorhandensein der Datei, die die Beschreibung des zu trainierenden Netzes enthält „name.net" (vorher von NNCreate() erzeugt ).

Existiert bereits eine entsprechende, von NNTrain() erzeugte Lernmusterdatei „name.pat" kann durch den Parameter override > 0 das Überschreiben der alten Datei veranlaßt werden. Ist override gleich 0 (also False) und existiert „name.pat" wird diese Funktion abgebrochen und liefert einen entsprechenden Fehlercode.

Kann die Abarbeitung der Funktion fortgesetzt werden, dann wird unter Zuhilfenahme des Pseudo-Fehlergenerators (PseudoError() in Datei psdoerr.c) eine Datei „name.pat" mit Lernmustern im .pat-Format des SNNS generiert. Diese Datei enthält mehrfach die Wörter aus „name.wrd" in kodierter Form, welche zusätzlich noch zufällig mit Fehlern versehen wurden. Anhand der zufällig generierten Fehler soll das Netz für mehrere fehlerhafte Ausprägungen des originalen Wortes angelernt werden. Ähnlich wie die Lernmuster werden noch sogenannte Validation- bzw. Testmuster generiert und in „name.val" abgelegt. Diese Generation der Testmuster erfolgt genau wie die Generation der Lernmuster nur mit dem Unterschied, daß die Testmuster im ASCII-Format und nicht im .pat-Format gespeichert werden.

Nachdem nun das Neuronale Netz „name.net" in den SNNS-Kernel geladen und initialisiert wurde, kann nun das eigentliche Lernen beginnen. Die Lernmuster werden als Mustersatz 0 und die Testmuster als Mustersatz 1 in den SNNS-Kernel geladen. Während der Trainingsphase werden mehrere Trainingszyklen ,unterbrochen von einigen Testzyklen, durchgeführt. Bei Bedarf kann zwischen beiden Mustersätzen - 0 und 1 - umgeschaltet werden. Während eines Trainingszyklus werden dem Netz alle Lernmuster einmal präsentiert. Nach einer gewissen Anzahl von Trainingszyklen (z.B. 10) empfiehlt es sich, die Testmuster für einen Testzyklus an das Netz anzulegen und für diese, dem Netz unbekannten Muster, den Gesamtfehler zu bestimmen. Ist ein solcher Fehler größer als der Fehler im vorhergehenden Testzyklus, heißt das, daß das Netz auf unbekannte Eingabemuster zunehmend schlechter reagiert. Obwohl der Fehler für die Trainingsmuster immer noch weiter abnehmen könnte, ist nun das Trainieren des Netzes zu beenden. Darüber hinaus gehende Trainingszyklen führen zum sogenannten Übertraining. Die Lernmuster werden zwar zunehmend besser erkannt, jedoch verliert das jetzt Netz zunehmend die Fähigkeit der Generalisierung und damit wird die Erkennung von unbekannten Mustern (den Testmustern) immer schlechter.

training.jpg (14501 Byte)

Nach erfolgreichem Training wird das Netz im gegenwärtigen Zustand in „name.net" gespeichert. Vor dem Beenden der Funktion werden die Muster und das Netz aus dem Kernel entfernt. Der Rückgabewert 0 signalisiert den ordnungsgemäßen Ablauf des Trainings.

Mit folgenden Konstanten kann die Lernphase gesteuert werden:

2.2.3 Worterkennung mit dem trainierten Netz: int NNPropagate (char* name, char* word, float* activation)

Die Anwendung des trainierten Netzes wird in dieser Funktion realisiert. der Parameter „name" ist hier wieder der Basisname für die Dateien „name.wrd", „name.net", „name.pat" und „name.val". Fehlen die Dateien „name.net" oder „name.pat" meldet sich der SNNS-Kernel mit einer Fehlermeldung und das Programm wird beendet. Das Vorhandensein dieser Dateien sollte also sichergestellt sein. Das neuronale Netz wird nun in den Simulator-Kernel geladen. Anschließend wird die Eingabedatei „name.wrd" analysiert. Dabei wird eine Tabelle aufgestellt, die die Vorkommen der einzelnen Buchstaben enthält.

Der Parameter „word" enthält das zu erkennende Wort. Dieses Wort wird nun in ein für das Netzwerk entsprechendes Format umgewandelt und an das Netzwerk angelegt.

Wurde das Symbol PROPAGATE_WEIGHTED_CHARS definiert, so werden die Buchstaben des (fehlerhaften) Wortes „word" dabei nach der Häufigkeit der Buchstaben in „name.wrd" gewichtet.

Beispiel: Die Datei „name.wrd" enthält folgende 3 Zeilen:

SELECT
FROM
WHERE

Das Wort „word" soll in diesem Beispiel „WELECZ" sein. Die Buchstaben werden wie folgt gewichtet:

„W" mit 1, weil „W" in „name.wrd" genau einmal vorkommt.
„E" mit 1/4, weil „E" in „name.wrd" viermal vorkommt.
...
„Z" mit 0, weil „Z" nicht in „name.wrd" vorkommt.

Eine Gewichtung der Buchstaben kann vorteilhaft sein. Ein „W" ist beispielsweise ein sehr charakteristischer Buchstabe für das Wort „WHERE". Wird ein „W" angelegt, so sorgt die hohe Gewichtung von 1 dafür, daß diesem Buchstabe mehr Aufmerksamkeit geschenkt wird als z.B. dem „E". Nachteilig dagegen können sich Tippfehler auswirken, die genau solche hoch gewichtete Buchstaben enthalten.

Das Symbol PROPAGATE_WEIGHTED_CHARS erlaubt das Experimentieren mit und ohne Gewichtung.

Nachdem nun das angelegte, und evtl. gewichtete, Wort durch das Netzwerk propagiert wurde, wird das Outputneuron mit der größten Aktivation durch Vergleich ermittelt. Dieses Neuron ist das Gewinnerneuron und symbolisiert das zugeordnete Wort aus „name.wrd".

Das erste Outputneuron verkörpert das erste Wort aus „name.wrd", das zweite Outputneuron verkörpert das zweite Wort aus „name.wrd" usw.

Die Nummer der Zeile in „name.wrd", welche durch das Gewinnerneuron indiziert wird, ist der Rückgabewert dieser Funktion. Als weiterer Rückgabeparameter liefert „*activation" die Aktivation des Gewinnerneurons. Diese liegt zwischen 0 und 1.

Vor dem Ende der Funktion wird der Speicher des SNNS-Kernel wieder aufgeräumt.

2.3 Nutzung des Moduls „Neuronales Netz" im „Online"-Betrieb

Hier werden die Funktionen NNCreate(), NNTrain() und NNPropagate() in einer Funktion zusammengefaßt:

int GetBestMatchOnline (char* name, char* word, float *activation);

Die Parameter dieser Funktion stimmen mit denen der Funktion NNPropagate()überein. Da die Funktionen NNCreate(), NNTrain() und NNPropagate()von GetBestMatchOnline() nur der Reihe nach aufgerufen werden, wird hier auf die Beschreibung in den betreffenden Abschnitten verwiesen.

2.4 Die C-Schnittstelle des SNNS-Kernels

Der Stuttgarter Neuronale Netze Simulator (SNNS) besteht aus 2 wesentlichen Teilen, dem Kernel und der graphischen Benutzeroberfläche. Der Kernel besitzt eine C-Schnittstelle, mit deren Hilfe eigene Applikationen Neuronale Netze auf dem Kernel simulieren können. Diese C-Schnittstelle ist im SNNS-Manual und im WWW knapp beschrieben. Um die vom Kernel bereitgestellten Funktionen (krui_*) in der eigenen C-Applikation benutzen zu können, müssen die entsprechenden Headerfiles eingebunden und die 2 Kernelbibliotheken gelinkt werden (Reihenfolge beachten !). Für das Erzeugen von Backpropagation-Netzen sind im Kernel schon viele Parameter voreingestellt.

Headerfiles:

Bibliotheken:

2.5 Erläuterungen zum Fehlergenerator (psdoerr.c)

Der Fehlergenerator dient zum Generieren von Lern- und Testmustern für das neuronale Netzwerk. Dabei wird eine Eingabedatei zeilenweise eingelesen. Diese Zeilen werden vervielfacht, mit Fehlern behaftet und in eine Ausgabedatei geschrieben. Die Schnittstelle zum Fehlergenerator bildet folgende Funktion:

int PseudoError (char* infile, char* outfile, int noise, int cycles, int asPattern)

infile: Name der Eingabedatei, in der Regel eine „.wrd"- Datei
outfile: Name der Ausgabedatei, in der Regel eine „.pat" oder „.val"-Datei
noise: Häufigkeit der zu generierenden Fehler, siehe Beschreibung von NN_NOISE
cycles: gibt an, wie viele fehlerbehaftete Exemplare eines Wortes zu erzeugen sind, siehe Beschreibung von NN_PATTERN_CYCLES
asPattern: ist dieser Parameter gleich 0, dann werden die erzeugten Muster im ASCII-Format gespeichert (verwendet für „.val"-Dateien) andernfalls wird das Ausgabefile im „.pat"-Format des SNNS geschrieben
return value: -1 wenn Fehler aufgetreten ist, 0 sonst

       

2.5.1 Die Arbeitsweise des Pseudo-Fehlergenerators (PseudoError())

Zunächst wird versucht, die beiden Dateien zu öffnen - „infile" zum Lesen und „outfile" zum Schreiben. Anschließend wird ein Fehlerschema generiert (GenerateErrorScheme()). Für dieses Fehlerschema wird. Speicher in der Größe von Länge der Datei „infile" * Anzahl der Zyklen bei der Fehlergenerierung reserviert. Initialisiert wird dieser Speicherbereich mit „T"'s (für TRUE). Anschließend wird in jedem Abschnitt, der „noise" Zeichen lang ist, zufällig ein Fehler generiert und mit „F" (für FALSE) gekennzeichnet. An diesen Stellen wird dann später das Lern- bzw. Testmuster zufällig verändert und somit werden Tippfehler simuliert. Abhängig davon, in welchem Format (ASCII oder SNNS-Pattern) die Ausgabedatei gespeichert werden soll, wird nun weiter verfahren.

Für den Fall, daß die Ausgabe im SNNS-Pattern-Format zu erfolgen hat, wird zunächst die Eingabedatei hinsichtlich ihrer Zeilen- und Spaltenanzahl analysiert. Die gewonnenen Werte werden benutzt, um den Kopf der Ausgabedatei entsprechend SNNS-Pattern-Format zu schreiben. Eine Beschreibung der Syntax des SNNS-Pattern-Formates (und somit des Kopfes) ist im SNNS-Manual zu finden. Nun wird die Eingabedatei zeilenweise eingelesen und verarbeitet. Für eine Zeile der Eingabedatei, welche ein Wort repräsentiert, werden folgende Schritte durchgeführt:

1. Modifizieren des Wortes entsprechend des vorher generierten Fehlerschemas (siehe unten).
2. Schreiben des modifizierten Wortes entsprechend SNNS-Pattern-Format in die Ausgabedatei.
3. Wiederholen der Schritte 1. und 2. so oft, wie mittels „cycles" vorgegeben wurde.

Im anderen Fall, der Ausgabe im ASCII-Format, wird prinzipiell wie eben beschrieben verfahren. Hier ist aber kein Kopf erforderlich, und die modifizierten Wörter werden im ASCII-Format (und nicht binär wie im SNNS-Pattern-Format) geschrieben. Das Modifizieren eines Wortes erfolgt genau an den Stellen, an denen das korrespondierende Fehlerschema ein „F" enthält.

Beispiel: Zeile der Eingabedatei: SELECT
korrespondierendes Fehlerschema: TTFTTT


An der Stelle, an der das „L" auftritt, wird ein Fehler generiert. Die Art des zu generierenden Fehlers wird zufällig bestimmt. Alle Fehlerarten können mit gleicher Wahrscheinlichkeit auftreten. An dieser Stelle könnte man durch statistische Untersuchungen, die den Rahmen dieses Praktikums sprengen würden, eine bessere Abstimmung finden. Folgende Fehlerarten werden unterschieden:

  1. CharFalse: der Buchstabe wird durch einen anderen ersetzt, der sich in der Kodierung aber nur um eine Bitstelle unterscheidet - damit wird ein Abrutschen eines Fingers auf eine benachbarte Taste oder das Verwechseln eines Fingers mit dem entsprechenden Finger der anderen Hand simuliert (siehe Abschnitt „Kodierung der Buchstaben").
  2. CharLess: der Buchstabe wird entfernt.
  3. CharMoreBefore: ein Buchstabe wird vor dem betreffendem Buchstaben eingefügt, der eingefügte Buchstabe unterscheidet sich wiederum nur um eine Bitstelle von seinem Nachfolger.
  4. CharMoreAfter: wie CharMoreBefore, nur wird ein neuer Buchstabe jetzt dahinter eingefügt.
  5. CharSwap: vertauscht den betreffenden Buchstaben mit seinem Nachfolger.

Für das obige Beispiel könnten diese Fehlerarten folgende „zufällige" Fehler generieren:

  1. CharFalse:SEOECT
  2. CharLess:SEECT
  3. CharMoreBefore:SEWLECT
  4. CharMoreAfter:SELSECT
  5. CharSwap:SEELCT

Nachdem die Eingabedatei auf diese Art und Weise vollständig abgearbeitet wurde, werden die Dateien geschlossen und die Funktion beendet.

2.5.2 Die Kodierung der Buchstaben (Tobias Neubert)

Die übliche Codierung der Buchstaben im ASCII-Format erwies sich für unsere Zwecke sehr ungünstig, da die Zeichen auf der Tastatur in einer anderen Reihenfolge angeordnet sind und Schreibfehler eher im Zusammenhang mit der Verwechslung benachbarter Buchstaben auftreten. Weiterhin können beim Schreiben mit zehn Fingern korrespondierende Buchstaben der einen mit denen der anderen Hand verwechselt werden. Das neuronale Netz kann falsche Buchstaben aber eher zuordnen, wenn deren Bitmuster dem des richtigen Buchstabens sehr nahe kommt. So entwickelten wir eine Codierung, die dies berücksichtigt und deren Code sich bei benachbarten sowie bei korrespondierenden Buchstaben jeweils nur an einer Bitstelle ändert.

Weitere Merkmale der Codierung:

 Hier nun der Entwurf der Codierung:

5. + 4. Bitstelle                        

00

1

2

3

4

5

6

7

*

8

(

9

)

0

_

-

+

=

01

Q

W

E

R

T

Y

U

I

O

P

   

11

 

A

S

D

F

G

H

J

K

L

 

"

10

 

Z

X

C

V

B

N

M

<

,

>

.

/

 
         

 

             

 

 

3. 2. 1. Bitstelle

 

0. Bitstelle

 

außen

innen

 

X kleiner Finger

001

011

Linke Hand: 0

X Ringfinger

111

 

Rechte Hand: 1

X Mittelfinger

110

   

X Zeigefinger links

100

000

 

X Zeigefinger rechts

100

000

 

Damit ergeben sich folgende Codes (spezieller Code für Leertaste):

Zeichen

Bitstellen

543210

Hexadezimal

Zeichen

Bitstellen

543210

Hexadezimal

Zeichen

Bitstellen

543210

Hexadezimal

A

110110

38

R

011000

28

8

001001

09

B

100000

20

S

111110

3E

9

001101

0D

C

101100

2C

T

010000

10

_

101010

2A

D

111100

3C

U

011001

19

-

000111

07

E

011100

1C

V

101000

28

"

wie ‘

 

F

111000

38

W

011110

1E

110011

33

G

110000

30

X

101110

2E

,

101101

2D

H

110001

31

Y

010001

11

+

wie =

 

I

011101

1D

Z

100110

26

*

wie 8

 

J

111001

39

0

001111

0F

/

100111

27

K

111101

3D

1

000010

02

<

wie ,

 

L

111111

3F

2

000110

06

>

wie .

 

M

101001

29

3

001110

0E

=

000011

03

N

100001

21

4

001100

0C

.

101111

2F

O

0111111

1F

5

001000

08

(

wie 9

 

P

010111

17

6

000000

00

)

wie 0

 

Q

010110

16

7

000001

01

Leertaste

101010

2A

 

2.6 Verbesserungsmöglichkeiten

Der Datenaustausch zwischen den Modulen und mit dem SNNS-Kernel erfolgt zum größten Teil über Dateien. Die etwas aufwendiger zu implementierende Möglichkeit, die Daten über Speicherbereiche weiter zu vermitteln würde einige Geschwindigkeitsvorteile bieten. Geschwindigkeit steht nicht im Vordergrund unserer Arbeit, sondern vielmehr die Einfachheit, um Zwischenergebnisse kontrollieren und nachvollziehen zu können.

Von uns wurden keinerlei statistische Untersuchungen unternommen, wie z.B. Analysen von Tippfehlern. Der Algorithmus zu Fehlergenerierung (für Lern- und Testmuster) beruht auf Erfahrungswerten bzw. Abschätzungen. Hier besteht die Möglichkeit, realistischere Lern- und Testmuster zu verwenden oder zu generieren. Eine bessere Erkennungsrate des neuronalen Netzes wäre dann zu erwarten.

Das Trainieren der neuronalen Netze ist sehr rechen- und speicherintensiv. Noch vertretbarer Aufwand ist das Erzeugen und Trainieren eines Netzes, welches z.B. 100 Wörter mit je maximal 20 Buchstaben unterscheiden kann. Hierfür wären dann 330 Neuronen und 24.200 Verbindungen zwischen den Neuronen erforderlich. Der Rechenaufwand und der Speicherplatzbedarf wächst exponentiell. Somit sind Netze, die 1000 Wörter und mehr unterscheiden sollen, auf einem Rechner kaum noch erzeugbar und noch schwerer trainierbar. Neben den Netzen wachsen ja auch noch die Lern- und Testmuster gewaltig an. Eine Möglichkeit, dieses exponentielle Zeit- und Speicherplatzverhalten aufzuknacken, wäre die Kaskadierung mehrerer kleiner neuronaler Netze anstelle eines einzigen, riesigen Netzes. Ein weiterer Vorteil ist, daß eine Veränderung in der Menge der zu lernenden bzw. erkennenden Wörter nur Auswirkungen auf das entsprechende Teilnetz hat. Dadurch beschränkt sich das Training auf das veränderte Teilnetz. Das Kriterium für die Auswahl der Wörter zu kleineren Gruppen könnte die Länge der Wörter sein. Grund: ausschlaggebend für die Anzahl der Neuronen in der Eingabeschicht ist die Länge des längsten Wortes der Gruppe. Also ist es günstig, etwa gleichlange Wörter zusammenzufassen. Nachteilig ist, daß alle nachfolgenden Kaskadierungsstufen (rot) online benutzt werden müssen, da die Gewinner der vorherigen Stufe erst ermittelt werden müssen.

kaskade.jpg (50951 Byte)

Eine weitere Möglichkeit, die Ausmaße des Netzes zu reduzieren, ist eine entsprechende Kodierung der Ausgabeschicht. Bisher wurde jedem Wort ein Ausgabeneuron zugeordnet. Das Neuron mit der größten Aktivation entspricht dem erkannten Wort. Bei einem Netz für 100 Wörter sind also 100 Neuronen in der Ausgabeschicht. Eine binäre Kodierung mit z.B. 7 Neuronen würde 128 Wörter in der Ausgabeschicht unterscheiden können. Dazu muß ein Grenzwert festgelegt werden, der zwischen den Zuständen 0 und 1 für ein Neuron unterscheidet (z.B. Aktivation >= 0.5 ist 1, sonst 0). Es ist aber zu prüfen, ob die Wörter durch die drastische Einsparung an Neuronen noch hinreichend gut unterscheiden werden können.

3 Interaktiver Editor (Tobias Neubert)

Quelldatei: QueryEdit.c

Der Editor ist ein einfaches Xlib- Programm. Er stellt folgende Funktionen zur Verfügung:

1 Die genannten Funktionen sind in dbconnect.sc.c implementiert und werden im Kapitel „Statische Datenbankanfrage" behandelt.

Erläuterung der Funktionalität:

Der Inhalt des Editorfensters wird in die Datei ‘User.Query’ gespeichert. Anschließen wird der Parser aufgerufen. Bei Rückkehr des Parsers wird zunächst die Datei ‘Query.Error’ auf eventuell anzuzeigende Fehlermeldungen untersucht. Anschließend wird das Ergebnis aus der Datei ‘Revised.Query’ in den Editor geladen.

Der Inhalt des Editorfensters wird in die Datei ‘Sent.Query’ gespeichert. Anschließend wird die Funktion SendQuery()2 aufgerufen, die eine dynamische Datenbankanfrage durchführt und das Ergebnis in der Datei ‘Query.Result’ speichert. Diese wird anschließend in den Editor geladen

User Query Laden der Datei ‘User.Query’ in den Editor.
Revised Query Laden der Datei ‘Revised.Query’ in den Editor.
Sent Query Laden der Datei ‘Sent.Query’ in den Editor.
Query Result Laden der Datei ‘Query.Result’ in den Editor.
New Löschen des Editorinhaltes
Load Laden einer Datei
Save As Speichern des Editorinhaltes in eine Datei
Quit Beenden des Programmes

2 Die Funktion SendQuery() ist in dynqyery.sc.c implementiert und wird im Kapitel „Dynamische Datenbankanfrage" behandelt.

4 Datenbankanbindung (Tobias Neubert)

Die Datenbankanbindung erfolgt mittels ESQL an eine INGRES - Datenbank (hier: AIR). Diese Datenbank ist zur Zeit „fest verdrahtet", d. h. direkt im Quelltext angegeben (ä dbconnect.sc.c, Funktion dbconnect() ). Der Zugriff auf die Datenbank erfolgt auf prinzipiell zwei verschiedene Arten, je nachdem ob es sich um feste, zum Programmierzeitpunkt bekannte Anfragen (s. Statische Datenbankanfrage) oder um Nutzeranfragen, die erst zur Laufzeit feststehen (ä Dynamische Datenbankanfrage) handelt.
In beiden Fällen wird der Quelltext von einem Precompiler überarbeitet, weshalb die ESQL- Anweisungen nicht der üblichen C- Syntax entsprechen.

4.1 Statische Datenbankanfrage

Headerdatei: dbconnect.h
Quelldatei: dbconnect.sc.c
Die statische Datenbankanfrage kommt zum Einsatz, um die Tabellen- und Spaltennamen der Relationen zu ermitteln. Diese sind in den Systemtabellen von INGRES gespeichert. Folgende Abfragen werden durchgeführt:

      1.
      select table_name
      from iitables
      where ((table_type like 'T') or (table_type like 'V')) and
      ((table_owner = 'ingres') or (table_owner = '$ingres'));

Diese Abfrage ermittelt die Tabellennamen aller Basistabellen und Views, aber nicht der Index- Tabellen. Wir haben uns hier auf Systemtabellen und Tabellen mit dem Eigentümer ‘ingres’ beschränkt, da zum einen auf andere Tabellen kein allgemeiner Zugriff möglich ist und zum anderen eine Erweiterung des Namensraumes für die Tabellennamen erforderlich wäre (verschiedene Nutzer können Tabellen mit gleichem Namen haben), die hier nicht implementiert ist.

      2.
      select column_name
      from iicolumns, iitables
      where ((table_type like 'T') or (table_type like 'V')) and
      ((table_owner = 'ingres') or (table_owner = '$ingres')) and
      (iitables.table_name=iicolumns.table_name) and
      (iicolumns.table_name like :tab_name);

Diese Abfrage dient der Ermittlung aller Spaltennamen einer angegebenen Tabelle (Variable :tab_name). Wiederum gilt hier die Beschränkung auf Basistabellen und Views, sowie bzgl. des Owners auf ‘ingres’ und Systemtabellen.

Beide Abfragen wurden in C- Funktionen gekapselt und stehen in verschiedenen Varianten zur Verfügung:

int dbGetRelationNames(char**)

Liest die Tabellennamen und gibt sie in einem Array zurück. Der Speicher für das Array muß initialisiert sein, der für die jeweiligen char- Strings nicht.

int dbCreateRelationFile()

Erzeugt unter Benutzung der Funktion dbGetRelationNames() eine Datei mit allen Tabellennamen. Der Name der Datei wird mit der Konstante RELATIONS_FILE festgelegt.

int dbGetColumnNames(char* TableName, char** ColumnNames)

Liest die Spaltennamen der angegebenen Tabelle und gibt sie in einem Array zurück. Es gelten die selben Einschränkungen, wie bei dbGetRelationNames().

Int dbCreateColumnFile(char* TableName)

Erzeugt unter Benutzung der Funktion dbGetColumnNames() eine Datei mit allen Spaltennamen der angegebenen Tabelle. Der Name der Datei ergibt sich aus dem Tabellennamen und einer Extension entsprechend der Konstante COLUMNS_FILE_EXT. Wurde ein NULL- Pointer anstatt eines Namens angegeben, wird eine Datei mit den Spaltennamen aller verfügbaren Relationen (Einschränkung s. o. - Erläuterung zur Abfrage 1) erzeugt. Der Name ergibt sich dann aus der Konstante COLUMNS_FILE.

Void dbCreateColumnFiles()

Erzeugt unter Benutzung der Funktionen dbGetRelationNames() und dbGetColumnNames() eine Datei für jede verfügbare Relation.

Außerdem werden in dieser Datei noch die Funktionen dbconnect() und dbdisconnect() zum Öffnen und Schließen der Datenbank zur Verfügung gestellt.

4.2 Dynamische Datenbankanfrage

Headerdatei: dynquery.h
Quelldatei: dynquery.sc.c

Um die vom neuronalen Netz ausgewertete Anfrage schließlich auch an die Datenbank senden zu können, bedarf es der Möglichkeiten einer dynamischen Datenbankanfrage. Diese wird mit der Funktion SendQuery(char* inp, char* outp) bereitgestellt. Die Funktion erwartet als Parameter die Namen einer Datei mit der Anfrage (*inp) und einer Datei für das Resultat (*outp). Falls die mit *outp bezeichnete Datei schon existiert, wird sie überschrieben. Sie enthält nach Rückkehr der Funktion nicht nur das Anfrageergebnis sondern auch eventuelle Fehlermeldungen.

Ein zentrales Element für die Implementierung einer dynamischen SQL Anfrage ist das SQL Descriptor Area (SQLDA), eine Struktur, die folgende Elemente besitzt:

sqlaid

Ein 8- Byte String mit Inhalt „SQLDA " zur Identifizierung in Speicherblöcken, hat sonst keine Bedeutung
sqlabc 4- Byte integer, gibt Größe des SQLDA an
sqln 2- Byte integer, gibt die Anzahl der sqlvar - Elemente an, für die Speicher reserviert wurde (Anzahl der Spalten)
sqld 2- Byte integer, gibt die Anzahl der zurück gelieferten Spalten an; falls größer als sqln, muß für sqlvar mehr Speicher reserviert werden
sqlvar Ein Feld mit sqln Elementen, jeweils bestehend aus:
sqltype Codierung der Domaine der Spalte
sqllen Größe der Spalte
sqldata Zeiger auf die eigentlichen Daten
sqlind Zeiger auf eine Indikatorvariable
sqlname Spaltenname

(Ausführliche Informationen zum SQLDA sind zu finden in „INGRES/SQL Reference Manual for the UNIX and VMS Operating System" ab Seite 5-1.)

Vorgehensweise bei der Auswertung des Statements:

5 Der Parser (Frank Seifert)

Die Funktion des Parsers ist die Überprüfung der Syntax von SQL-Anfragen. Dazu testet der Parser mit Hilfe gegebener Grammatikregeln die Zulässigkeit von Schlüsselwörtern, Relations- und Attributnamen sowie diverser Sonderzeichen an ihrer Position.

Konnte eine Wort der Anfrage nicht identifiziert werden, wird es ans Netz geschickt (Wörter sind hier ASCII - Zeichenketten gefolgt von einer Integerzahl). Im Netz wird das ähnlichste an dieser Position zulässige Wort und dessen Wahrscheinlichkeit ermittelt. Falls diese Wahrscheinlichkeit einen bestimmten Grenzwert übersteigt, wird das nicht erkannte durch das korrigierte Wort ersetzt.

Es ist möglich, daß in einer Anfrage per Alias auf eine Relation zugegriffen wird. Der Alias wird aber erst später in der Anfrage mit einer Relation assoziiert. Um nun die Überprüfbarkeit und Korrigierbarkeit der Anfrage zu gewährleisten, muß der Parser vorausschauend jeden Alias einer Relation zuordnen. Deshalb ist es notwendig, daß der Parser die Zuordnungsschemata erkennt. Zu diesem Zweck benötigt er einen Fixpunkt, das Schlüsselwort FROM (denn hinter FROM erfolgt die Assoziation von Alias und Relation). Aus diesem Grunde wird gefordert, daß in jeder SELECT-Teilanfrage das Schlüsselwort FROM vorkommt und richtig geschrieben ist.

Weiterhin sollte die Klammerung richtig erfolgen, da deren Korrektur nur in eindeutigen Fällen (fehlerfrei) geschehen kann.

 SQL - Grammatik in Backus - Naur - Form

sql_query ::=

data_definition_statement

| basic_data_manipulation_statement

| transaction_processing_statement

data_definition_statement ::=

CREATE TABLE table ( table_def_item_list )

| CREATE VIEW view [ ( column_list ) ]

AS query-spec [ WITH CHECK OPTION ]

| GRANT { ALL [ PRIVILEGES ] | privilege-list }

ON table

TO { PUBLIC | user_list }

[ WITH GRANT OPTION ]

basic_data_manipulation_statement ::=

select_statement

| INSERT INTO table [ ( column-list ) ]

VALUES { ( insert_item_list ) | select_statement }

| DELETE FROM table [ WHERE search_condition ]

| UPDATE table SET assignment_list [ WHERE search_condition ]

transaction_processing_statement ::=

COMMIT WORK

| ROLLBACK WORK

query_expr ::= query_item [UNION [ ALL ] query_item

query_item ::= select_statement | ( query_expr )

subquery ::= ( select_statement )

select_statement ::=

SELECT [ ALL | DISTINCT ] { select_item_list | * }

FROM table_ref_list

[ WHERE search_condition ]

[ GROUP BY column_ref_list ]

[ HAVING search_condition ]

search_condition ::= search_item [ { AND | OR } search_item ]

search_item ::= [ NOT ] { search_test | ( search_condition ) }

search_test ::= comparison | between | like | null | set | quantified | existence

comparison ::= expr { = | <> | < | <= | > | >= } { expr | subquery }

between ::= expr [ NOT ] BETWEEN expr AND expr

like ::= column_ref [ NOT ] LIKE value [ ESCAPE value ]

null ::= column_ref IS [ NOT ] NULL

set ::= expr [ NOT ] IN { value_list | subquery }

quantified ::= expr { = | <> | < | <= | > | >= } [ ALL | ANY | SOME ] subquery

existence ::= EXISTS subquery

expr ::= expr_item [ { + | - | * | / } expr_item ]

expr_item ::= [ + | - ] { USER | literal | column_ref | function | ( expr ) }

function ::= COUNT ( * ) | {AVG | MAX | MIN | SUM | COUNT } {distinct_function | all_function}

distinct_function ::= ( DISTINCT column_ref )

all_function ::= ( [ ALL ] expr )

assignment_list ::= assignment [ , assignment_list ]

insert_item_list ::= insert_item [ , insert_item_list ]

select_item_list ::= expr [ , select_item_list ]

table_ref_list ::= table_ref [ , table_ref_list ]

column_ref_list ::= column_ref [ , column_ref_list ]

table_def_item_list ::= table_def_item [ , table_def_item_list ]

column_list ::= column [ , column_list ]

privilege_list ::= privilege [ , privilege_list ]

user_list ::= user [ , user_list ]

assignment ::= column = { expr | NULL }

insert_item ::= { USER | NULL | literal }

table_ref ::= table [ alias ]

column_ref ::= [ { table | alias } . ] column

table_def_item ::=

column_def

| { UNIQUE | PRIMARY KEY | FOREIGN KEY } ( column_list )

| REFERENCES table [ ( column_list )

column_def ::= column data_type [ DEFAULT { literal | USER | NULL } ]

privilege ::= SELECT | INSERT | DELETE | UPDATE [ ( column_list ) ]

table ::= Relation der Datenbank

column ::= Attribut der Datenbank

user ::= Name eines Datenbankanwenders (wird nicht korrigiert)

literal ::= eine Zahl oder eine Zeichenkette in Anführungszeichen

data_type ::= ein SQL Datentyp

alias ::= Alias für eine Relation