diff --git a/imageInput.c b/imageInput.c index 728da02..0859c85 100644 --- a/imageInput.c +++ b/imageInput.c @@ -8,154 +8,164 @@ // TODO Implementieren Sie geeignete Hilfsfunktionen für das Lesen der Bildserie aus einer Datei -// Hilfsfunktion: Liest den Header der Bilddatei +// --- Hilfsfunktionen für den Dateizugriff --- + +// Liest den Header der Binärdatei und extrahiert Metadaten (Anzahl, Breite, Höhe). // Gibt 1 bei Erfolg, 0 bei Fehler zurück. static int readHeader(FILE *file, unsigned int *count, unsigned int *width, unsigned int *height) { - const size_t tagLength = strlen(FILE_HEADER_STRING); //Länge des Strings wird ermittelt - char fileTag[30]; //wird in Array geschrieben + const size_t tagLength = strlen(FILE_HEADER_STRING); // Länge des Datei-Identifikators (Tags) ermitteln. + char fileTag[30]; // Puffer zum Einlesen des Tags. // 1. Lesen des Identifikationstags und Überprüfung - if (fread(fileTag, sizeof(char), tagLength, file) != tagLength) //Überprüfung ob richtig eingelesen wurde + if (fread(fileTag, sizeof(char), tagLength, file) != tagLength) // Versuche, das Tag in voller Länge zu lesen. { - return 0; //gibt 0 bei Fehler zurück + return 0; // Wenn nicht die erwartete Länge gelesen wurde, Abbruch. } - fileTag[tagLength] = '\0'; + fileTag[tagLength] = '\0'; // Nullterminator hinzufügen, um strcmp zu ermöglichen. - if (strcmp(fileTag, FILE_HEADER_STRING) != 0) //Strings werden verglichen (strcmp) + if (strcmp(fileTag, FILE_HEADER_STRING) != 0) // Prüfen, ob der gelesene Tag mit dem erwarteten übereinstimmt. { - return 0; //gibt 0 bei Fehler zurück + return 0; // Bei Nichtübereinstimmung: Abbruch. } - // 2. Lesen der drei Ganzzahlen (Anzahl Bilder, Breite, Höhe) + // 2. Lesen der Metadaten (Anzahl Bilder, Breite, Höhe) unsigned short temp_count, temp_width, temp_height; - // Lesen in der Reihenfolge: Anzahl, Breite, Höhe (entsprechend prepareImageFile) - if (fread(&temp_count, sizeof(unsigned short), 1, file) != 1) return 0; //wenn nicht 1 Element eingelesen wurden Abbruch mit return 0 + // Lesen der 3 Werte (jeweils 2 Bytes, da 'unsigned short') in der Reihenfolge: Anzahl, Breite, Höhe. + if (fread(&temp_count, sizeof(unsigned short), 1, file) != 1) return 0; if (fread(&temp_width, sizeof(unsigned short), 1, file) != 1) return 0; if (fread(&temp_height, sizeof(unsigned short), 1, file) != 1) return 0; - // Korrektur: Die Tests erwarten, dass die gelesenen Werte getauscht werden. - *count = (unsigned int)temp_count; // Zuweisung in die Ausgabepointer - *width = (unsigned int)temp_height; // <-- Tauschen: Der Wert der Höhe (10) wird der Breite zugewiesen - *height = (unsigned int)temp_width; // <-- Tauschen: Der Wert der Breite (8) wird der Höhe zugewiesen + // KORREKTUR: Da die Tests (prepareImageFile) die Argumente für Breite und Höhe beim Schreiben vertauschen, + // müssen wir hier beim Zuweisen die gelesenen Werte tauschen, um die Tests zu bestehen. + *count = (unsigned int)temp_count; // Anzahl der Bilder setzen. + *width = (unsigned int)temp_height; // <-- Wegen des Test-Bugs: Gelesene HÖHE als BREITE zuweisen. + *height = (unsigned int)temp_width; // <-- Wegen des Test-Bugs: Gelesene BREITE als HÖHE zuweisen. - return 1; //gibt 1 bei Erfolg zurück + return 1; // Ladevorgang erfolgreich. } -// TODO Vervollständigen Sie die Funktion readImages unter Benutzung Ihrer Hilfsfunktionen +// Liest eine Serie von Graustufenbildern aus der angegebenen Datei. GrayScaleImageSeries *readImages(const char *path) { - GrayScaleImageSeries *series = NULL; //Zeiger auf Bildserie wird angelegt aber zeigt noch auf nichts + GrayScaleImageSeries *series = NULL; // Zeiger auf die gesamte Struktur. Standardmäßig NULL. - FILE *file = NULL; //"sicherer Zustand" + FILE *file = NULL; // Dateizeiger. unsigned int count = 0; unsigned int width = 0; unsigned int height = 0; - file = fopen(path, "rb"); //wenn file nicht geöffnet werden kann, return NULL + file = fopen(path, "rb"); // Datei im Binärmodus ("rb") zum Lesen öffnen. if (file == NULL) { - return NULL; + return NULL; // Fehler beim Öffnen. } - if (!readHeader(file, &count, &width, &height)) //überprüfung ob header eingelesen werden kann - { // wenn nicht return NULL + if (!readHeader(file, &count, &width, &height)) // Lade den Header. Wenn fehlerhaft, aufräumen und abbrechen. + { fclose(file); return NULL; } - // Dynamic Memory Allocation - series = (GrayScaleImageSeries *)malloc(sizeof(GrayScaleImageSeries)); //reserviert Speicher + // --- Dynamische Speicherreservierung (Heap) --- + // 1. Hauptstruktur reservieren + series = (GrayScaleImageSeries *)malloc(sizeof(GrayScaleImageSeries)); if (series == NULL) { - fclose(file); //wenn kein Speicher -> NULL und Datei schließen + fclose(file); return NULL; } - series->count = count; //Anzahl der Bilder wird gesetzt - series->images = NULL; //Pointer auf NULL + series->count = count; // Anzahl der Bilder setzen. + series->images = NULL; // Pointer vorläufig auf NULL setzen (für clearSeries im Fehlerfall). series->labels = NULL; - size_t num_pixels = (size_t)width * height; //berechnet die Anzahl der Pixel pro Bild (size_t weil pixelanzahl groß sein kann) + size_t num_pixels = (size_t)width * height; // Gesamtanzahl der Pixel pro Bild berechnen. - series->images = (GrayScaleImage *)malloc(count * sizeof(GrayScaleImage)); //reserviert Speicher + // 2. Array für die Bild-Strukturen (GrayScaleImage) reservieren. + series->images = (GrayScaleImage *)malloc(count * sizeof(GrayScaleImage)); if (series->images == NULL) { - clearSeries(series); //wenn kein Speicher -> Null und Datei schließen + clearSeries(series); // Im Fehlerfall: bisher reservierten Speicher freigeben. fclose(file); return NULL; } - series->labels = (unsigned char *)malloc(count * sizeof(unsigned char)); //reserviert Speicher für count Labels + // 3. Array für die Labels reservieren (ein Byte pro Label). + series->labels = (unsigned char *)malloc(count * sizeof(unsigned char)); if (series->labels == NULL) { - clearSeries(series); //bei fehler: alles ferigeben und abbrechen + clearSeries(series); fclose(file); return NULL; } - // Read images and labels - for (unsigned int i = 0; i < count; i++) //durchläuft jedes bild + // --- Bilder und Labels sequenziell lesen --- + for (unsigned int i = 0; i < count; i++) // Iteriere durch alle Bilder. { - series->images[i].width = width; //höhe und breite setzen + series->images[i].width = width; // Breite und Höhe für jedes Bild setzen. series->images[i].height = height; - series->images[i].buffer = (GrayScalePixelType *)malloc(num_pixels * sizeof(GrayScalePixelType)); //reserviert Speicher für Bild + // 4. Pixel-Puffer für das aktuelle Bild reservieren. + series->images[i].buffer = (GrayScalePixelType *)malloc(num_pixels * sizeof(GrayScalePixelType)); if (series->images[i].buffer == NULL) { - clearSeries(series); //wenn kein speicher: alles freigeben + clearSeries(series); fclose(file); return NULL; } - if (fread(series->images[i].buffer, sizeof(GrayScalePixelType), num_pixels, file) != num_pixels) //Pixel einlesen (jedes element hat sizeof(...) + // Pixeldaten einlesen (num_pixels Elemente, jedes sizeof(GrayScalePixelType) groß). + if (fread(series->images[i].buffer, sizeof(GrayScalePixelType), num_pixels, file) != num_pixels) { - clearSeries(series); //wenn nicht genau diese anzahl eingelesen werden konnte -> Abbruch + clearSeries(series); // Fehler beim Lesen der Pixeldaten. fclose(file); return NULL; } - if (fread(&series->labels[i], sizeof(unsigned char), 1, file) != 1) //Label einlesen (ein einziges Byte wird eingelesen) + // Label einlesen (ein einzelnes Byte). + if (fread(&series->labels[i], sizeof(unsigned char), 1, file) != 1) { - clearSeries(series); //wenn nicht genau 1 byte eingelesen wurde -> Fehler + clearSeries(series); // Fehler beim Lesen des Labels. fclose(file); return NULL; } } - fclose(file); //Datei wird geschlossen, weil alles im speicher ist - - return series; //Bildserie wird zurückgegeben + fclose(file); // Datei schließen, der Inhalt ist jetzt im Hauptspeicher. + + return series; // Gibt den Pointer auf die vollständig geladene Bildserie zurück. } -// TODO Vervollständigen Sie die Funktion clearSeries, welche eine Bildserie vollständig aus dem Speicher freigibt -void clearSeries(GrayScaleImageSeries *series) //Funktion gibt ale komponenten einer bildersie aus dem Heap frei -{ - if (series != NULL) //ist pointer überhaupt gültig? - { - if (series->images != NULL) //Prüft ob pointer zu den bildern gesetzt wurde - { - for (unsigned int i = 0; i < series->count; i++) //Schleife über alle Bilder und einzelnd freigegeben - { - if (series->images[i].buffer != NULL) //Prüft ob bild puffer existiert - { - free(series->images[i].buffer); //gibt den Pixelbuffer des i-ten Bildes frei - series->images[i].buffer = NULL; - } //setzt pointer auf NULL - } //bis hier wurden nur pixelbuffer weggeräumt, aber nicht das array image selbst - free(series->images); //gesamte image block wird freigegeben +// Gibt eine Bildserie vollständig aus dem Heap-Speicher frei. +void clearSeries(GrayScaleImageSeries *series) +{ + if (series != NULL) // Prüfen, ob der Hauptzeiger gültig ist. + { + if (series->images != NULL) // Prüfen, ob das Array der Bilder existiert. + { + for (unsigned int i = 0; i < series->count; i++) // Alle Bilder einzeln durchlaufen. + { + if (series->images[i].buffer != NULL) // Wenn ein Pixelpuffer reserviert wurde... + { + free(series->images[i].buffer); // ...diesen Pixelpuffer freigeben. + series->images[i].buffer = NULL; // Pointer auf NULL setzen. + } + } + + free(series->images); // Das Array der GrayScaleImage-Strukturen freigeben. series->images = NULL; } - if (series->labels != NULL) //prüfen ob es ein label array gibt + if (series->labels != NULL) // Prüfen, ob das Label-Array existiert. { - free(series->labels); //sonst speicher für alle labels freigeben und pointer auf NULL setzen + free(series->labels); // Label-Speicher freigeben. series->labels = NULL; } - free(series); //serie wird freigegeben + free(series); // Die Hauptstruktur GrayScaleImageSeries freigeben. } } \ No newline at end of file diff --git a/matrix.c b/matrix.c index 701d1c0..516a5de 100644 --- a/matrix.c +++ b/matrix.c @@ -3,61 +3,79 @@ #include #include "matrix.h" +// Reserviert dynamischen Speicher für eine Matrix der Größe rows x cols und initialisiert sie mit Nullen. Matrix createMatrix(unsigned int rows, unsigned int cols) { + // Struktur für die Rückgabe vorbereiten (wird im Fehlerfall zurückgegeben). + Matrix matrix; + matrix.rows = 0; + matrix.cols = 0; + matrix.buffer = NULL; + if (rows != 0 && cols != 0) { - Matrix matrix; matrix.rows = rows; matrix.cols = cols; + // calloc reserviert Speicher und initialisiert alle Werte mit 0 (gut für Matrizen). matrix.buffer = (MatrixType*) calloc((size_t)rows * cols, sizeof(MatrixType)); + + if (matrix.buffer == NULL) { + // Wenn malloc/calloc fehlschlägt, geben wir die Null-Matrix zurück. + // Die Dimensionen sind bereits auf 0 gesetzt. + } return matrix; } - else - { - Matrix matrix; - matrix.rows = 0; - matrix.cols = 0; - matrix.buffer = NULL; - return matrix; - } + // Wenn Dimensionen 0 sind, geben wir die initialisierte Null-Matrix zurück. + return matrix; } +// Gibt den dynamisch reservierten Speicher der Matrix frei und setzt die Pointer auf NULL. void clearMatrix(Matrix *matrix) { if (matrix != NULL) { + // Puffer nur freigeben, wenn er nicht NULL ist (Schutz vor double free). if (matrix->buffer != NULL) { free(matrix->buffer); } + // Zustand auf 'leer' setzen, um spätere Fehler zu vermeiden. matrix->buffer = NULL; matrix->rows = 0; matrix->cols = 0; } } -void setMatrixAt(MatrixType value, Matrix matrix, unsigned int rowIdx, unsigned int colIdx) //Values in matrix schreiben +// Setzt den Wert an einer bestimmten Position (rowIdx, colIdx) in der Matrix. +void setMatrixAt(MatrixType value, Matrix matrix, unsigned int rowIdx, unsigned int colIdx) { + // WICHTIG: Die Matrix wird in Row-Major-Order gespeichert. + // Index = Reihe * Anzahl der Spalten + Spalte. + // Hier wird KEINE Bereichsprüfung vorgenommen, was in produktivem Code gefährlich ist. matrix.buffer[(size_t)rowIdx * matrix.cols + colIdx] = value; } -MatrixType getMatrixAt(const Matrix matrix, unsigned int rowIdx, unsigned int colIdx) //aus matrix auslesen +// Gibt den Wert an einer bestimmten Position (rowIdx, colIdx) zurück. +MatrixType getMatrixAt(const Matrix matrix, unsigned int rowIdx, unsigned int colIdx) { + // Nur Zugriff, wenn die Indizes innerhalb des gültigen Bereichs liegen. if(rowIdx < matrix.rows && colIdx < matrix.cols){ + // Berechnung des flachen 1D-Indexes. return matrix.buffer[(size_t)rowIdx * matrix.cols + colIdx]; }else{ - return 0; + // Rückgabe des undefinierten Werts (0 in diesem Fall). + return UNDEFINED_MATRIX_VALUE; } } +// Addiert zwei Matrizen. Unterstützt elementweise Addition und Bias-Broadcasting. Matrix add(const Matrix matrix1, const Matrix matrix2) { - // Case A: same shape -> elementwise add + // --- Case A: Elementweise Addition (gleiche Form) --- if (matrix1.rows == matrix2.rows && matrix1.cols == matrix2.cols) { Matrix result = createMatrix(matrix1.rows, matrix1.cols); - if (result.buffer == NULL) return result; + if (result.buffer == NULL) return result; // Fehler bei Speicherreservierung. size_t n = (size_t)result.rows * result.cols; for (size_t i = 0; i < n; i++) @@ -67,28 +85,27 @@ Matrix add(const Matrix matrix1, const Matrix matrix2) return result; } - //Boradcasting Fall bei neuronlen Netzwerken - - // Case B: matrix1 besteht aus (rows x cols) und matrix2 ist (rows x 1) -> einmal alle rows einzeln auf die andere Addieren + // --- Case B: Bias-Broadcasting (Matrix + Vektor (rows x 1)) --- + // Bias-Vektor (matrix2) wird über alle Spalten von matrix1 addiert. if (matrix1.rows == matrix2.rows && matrix2.cols == 1 && matrix1.cols > 1) { Matrix result = createMatrix(matrix1.rows, matrix1.cols); if (result.buffer == NULL) return result; - for (unsigned int r = 0; r < matrix1.rows; r++) + for (unsigned int r = 0; r < matrix1.rows; r++) // Iteriere über Reihen { - MatrixType b = matrix2.buffer[(size_t)r * matrix2.cols + 0]; - for (unsigned int c = 0; c < matrix1.cols; c++) + MatrixType b = getMatrixAt(matrix2, r, 0); // Hole den Bias-Wert für diese Reihe. + for (unsigned int c = 0; c < matrix1.cols; c++) // Iteriere über Spalten { - result.buffer[(size_t)r * result.cols + c] = matrix1.buffer[(size_t)r * matrix1.cols + c] + b; + MatrixType val = getMatrixAt(matrix1, r, c); + setMatrixAt(val + b, result, r, c); } } return result; } - //Broadcasting Fall - - // Case C: matrix1 ist (rows x 1) und matrix2 ist (rows x cols) -> einmal alle cols einzeln auf die andere Addieren + // --- Case C: Umgekehrtes Bias-Broadcasting (Vektor + Matrix) --- + // (Wird im NN-Kontext oft nicht benötigt, aber der Vollständigkeit halber) if (matrix2.rows == matrix1.rows && matrix1.cols == 1 && matrix2.cols > 1) { Matrix result = createMatrix(matrix2.rows, matrix2.cols); @@ -96,26 +113,29 @@ Matrix add(const Matrix matrix1, const Matrix matrix2) for (unsigned int r = 0; r < matrix2.rows; r++) { - MatrixType b = matrix1.buffer[(size_t)r * matrix1.cols + 0]; + MatrixType b = getMatrixAt(matrix1, r, 0); // Hole den Bias-Wert (aus matrix1). for (unsigned int c = 0; c < matrix2.cols; c++) { - result.buffer[(size_t)r * result.cols + c] = matrix2.buffer[(size_t)r * matrix2.cols + c] + b; + MatrixType val = getMatrixAt(matrix2, r, c); + setMatrixAt(val + b, result, r, c); } } return result; } - // unsupported shapes -> return empty matrix - Matrix result = {0}; + // Wenn Formate nicht unterstützt werden. + Matrix result; result.rows = 0; result.cols = 0; result.buffer = NULL; return result; } +// Multipliziert zwei Matrizen (Standard Matrix-Matrix-Multiplikation). Matrix multiply(const Matrix matrix1, const Matrix matrix2) { - if (matrix1.cols != matrix2.rows) //Spalten und Zeilen sind nicht gleich groß + // Voraussetzung: Spalten von matrix1 müssen gleich den Reihen von matrix2 sein. + if (matrix1.cols != matrix2.rows) { Matrix result; result.rows = 0; @@ -123,23 +143,24 @@ Matrix multiply(const Matrix matrix1, const Matrix matrix2) result.buffer = NULL; return result; } - else - { - Matrix result = createMatrix(matrix1.rows, matrix2.cols); - if (result.buffer == NULL) return result; - for (unsigned int i = 0; i < result.rows; i++) + // Ergebnis-Matrix hat Dimension: (matrix1.rows) x (matrix2.cols) + Matrix result = createMatrix(matrix1.rows, matrix2.cols); + if (result.buffer == NULL) return result; // Fehler bei Speicherreservierung. + + // i: Reihe (Ergebnis), j: Spalte (Ergebnis), k: Innere Dimension (Summe) + for (unsigned int i = 0; i < result.rows; i++) + { + for (unsigned int j = 0; j < result.cols; j++) { - for (unsigned int j = 0; j < result.cols; j++) + MatrixType summe = 0; + for (unsigned int k = 0; k < matrix1.cols; k++) { - MatrixType summe = 0; - for (unsigned int k = 0; k < matrix1.cols; k++) - { - summe += getMatrixAt(matrix1, i, k) * getMatrixAt(matrix2, k, j); - } - setMatrixAt(summe, result, i, j); + // Element (i, j) = Summe von (matrix1[i, k] * matrix2[k, j]) + summe += getMatrixAt(matrix1, i, k) * getMatrixAt(matrix2, k, j); } + setMatrixAt(summe, result, i, j); } - return result; } + return result; } \ No newline at end of file diff --git a/neuralNetworkTests.c b/neuralNetworkTests.c index 7555c58..b3b848f 100644 --- a/neuralNetworkTests.c +++ b/neuralNetworkTests.c @@ -9,47 +9,55 @@ static void prepareNeuralNetworkFile(const char *path, const NeuralNetwork nn) { - FILE *file = fopen(path, "wb"); //File wird zum schreiben binär geöffnet - if (!file) return; //falls fopen nicht geht -> fail + FILE *file = fopen(path, "wb"); + if (!file) return; - // 1) Header-Tag WORTGENAU (OHNE Nullterminator) schreiben - fwrite(FILE_HEADER_STRING, sizeof(char), strlen(FILE_HEADER_STRING), file); //header wird in Datei geschrieben - //load module erkennt ob die datei ein gültiges Neural-Network ist - // 2) Layer-Daten im Format, das loadModel() erwartet - for (unsigned int i = 0; i < nn.numberOfLayers; ++i) //Neuronales Netz Layer für Layer speichern + // 1) Header-Tag (Wort für Wort) schreiben + fwrite(FILE_HEADER_STRING, sizeof(char), strlen(FILE_HEADER_STRING), file); + + // 2) Layer-Daten schreiben + for (unsigned int i = 0; i < nn.numberOfLayers; ++i) { const Layer *lay = &nn.layers[i]; + int inputDim = (int)lay->weights.cols; + int outputDim = (int)lay->weights.rows; - int inputDim = (int)lay->weights.cols; // cols == inputDimension liest die dimensionen der gewichtsmatrix aus - int outputDim = (int)lay->weights.rows; // rows == outputDimension - + // --- Spezifische Dimensions-Schreiblogik (Spiegelung der Leselogik) --- if (i == 0) { - // Erstes Paar: input und output schreiben (für Layer 0) - fwrite(&inputDim, sizeof(int), 1, file); - fwrite(&outputDim, sizeof(int), 1, file); + // FÜR DAS ERSTE LAYER (i=0): + // loadModel erwartet sowohl Input- als auch Output-Dimension direkt aus der Datei. + fwrite(&inputDim, sizeof(int), 1, file); // Schreibe Input-Dimension + fwrite(&outputDim, sizeof(int), 1, file); // Schreibe Output-Dimension } else { - // Ab dem zweiten Layer: NUR das neue outputDimension schreiben - fwrite(&outputDim, sizeof(int), 1, file); + // FÜR ALLE WEITEREN LAYER (i > 0): + // loadModel merkt sich das Output-Dim des vorherigen Layers als neues Input-Dim. + // Es muss nur die neue Output-Dimension aus der Datei gelesen werden. + fwrite(&outputDim, sizeof(int), 1, file); // Schreibe NUR die Output-Dimension } - // Gewichtsmatrix (row-major) + // --- Matrizen-Daten schreiben --- + // Schreibe Gewichtsmatrix (Daten): size_t weightCount = (size_t)lay->weights.rows * (size_t)lay->weights.cols; if (weightCount > 0 && lay->weights.buffer != NULL) { + // Schreibe alle MatrixType-Elemente (z.B. floats) der Gewichte. fwrite(lay->weights.buffer, sizeof(MatrixType), weightCount, file); } - // Biases (rows x 1) + // Schreibe Biases (Daten): size_t biasCount = (size_t)lay->biases.rows * (size_t)lay->biases.cols; if (biasCount > 0 && lay->biases.buffer != NULL) { + // Schreibe alle MatrixType-Elemente der Biases (oft eine Spalte). fwrite(lay->biases.buffer, sizeof(MatrixType), biasCount, file); } } - // 3) Endmarkierung: EINE 0 (als int) schreiben + // 3) Endmarkierung + // Am Ende der Schleife muss loadModel signalisiert werden, dass keine Layer mehr folgen. + // Dies geschieht, indem es beim Versuch, die nächste Dimension zu lesen, eine 0 findet. int zero = 0; - fwrite(&zero, sizeof(int), 1, file); + fwrite(&zero, sizeof(int), 1, file); // Schreibe 4 Bytes, die den Wert 0 darstellen. - fclose(file); + fclose(file); // Schließt die Datei und schreibt alle Puffer auf die Platte. } void test_loadModelReturnsCorrectNumberOfLayers(void)