From e45076df91be18606d72efdbb753dfa99efd8e60 Mon Sep 17 00:00:00 2001 From: RoundRonin Date: Sun, 31 Mar 2024 16:37:00 +0300 Subject: [PATCH] Objecs for import and neural model created Model still doesn't support custom structure --- .gitignore | 4 +- .vscode/settings.json | 6 +- image_recognition/base.py | 20 --- image_recognition/cli.py | 157 +++++------------- image_recognition/modules/__init__.py | 0 image_recognition/modules/importer.py | 74 +++++++++ image_recognition/modules/model.py | 71 ++++++++ .../{ => modules}/visualization.py | 127 +++++++------- 8 files changed, 260 insertions(+), 199 deletions(-) delete mode 100644 image_recognition/base.py create mode 100644 image_recognition/modules/__init__.py create mode 100644 image_recognition/modules/importer.py create mode 100644 image_recognition/modules/model.py rename image_recognition/{ => modules}/visualization.py (81%) diff --git a/.gitignore b/.gitignore index aebf5f5..e0fc5ab 100644 --- a/.gitignore +++ b/.gitignore @@ -134,4 +134,6 @@ dmypy.json # Dataset Data* -**/Data* \ No newline at end of file +**/Data* + +Save* \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index dcb1530..1363ec0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ -{ - "python.analysis.typeCheckingMode": "basic", - "python.analysis.autoImportCompletions": true +{ + "python.analysis.typeCheckingMode": "basic", + "python.analysis.autoImportCompletions": true } \ No newline at end of file diff --git a/image_recognition/base.py b/image_recognition/base.py deleted file mode 100644 index 0e19fcf..0000000 --- a/image_recognition/base.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -image_recognition base module. - -This is the principal module of the image_recognition project. -here you put your main classes and objects. - -Be creative! do whatever you want! - -If you want to replace this with a Flask application run: - - $ make init - -and then choose `flask` as template. -""" - -# example constant variable -NAME = "image_recognition" - -def hello(): - print("hello_there") \ No newline at end of file diff --git a/image_recognition/cli.py b/image_recognition/cli.py index 68cd7ef..7a5065c 100644 --- a/image_recognition/cli.py +++ b/image_recognition/cli.py @@ -1,36 +1,23 @@ -"""CLI interface for image_recognition project. -Be creative! do whatever you want! - -- Install click or typer and create a CLI app -- Use builtin argparse -- Start a web application -- Import things from your .base module -""" -import image_recognition.base as base -from image_recognition.visualization import plotter_evaluator - -import numpy as np - -import os -import keras -from keras import layers -from tensorflow import data as tf_data - -from keras.models import Sequential -from keras.layers import Conv2D, MaxPooling2D, Dense, Flatten, Dropout -from keras.optimizers import RMSprop - -from keras.callbacks import ReduceLROnPlateau +from image_recognition.modules.importer import importer +from image_recognition.modules.model import model +from image_recognition.modules.visualization import plotter_evaluator def main(): # pragma: no cover - base.hello() ## Формирование классов на основе файловой структуры # Внутри указанной директории должны нахдиться папки, имя которых соотвествует классу. - # В папках -- изображения, соответсвующие классу. + # В папках -- изображения, соответсвующие классу. Называние изображений не имеет значения. + + # data_directory/ + # ...class_a/ + # ......a_image_1.jpg + # ......a_image_2.jpg + # ...class_b/ + # ......b_image_1.jpg + # ......b_image_2.jpg # Размер по вертикали, размер по горизонтали. К этим значениям будут приведены все изображения (сжаты/растянуты, не обрезаны) height = 140 @@ -41,64 +28,32 @@ def main(): # pragma: no cover batch_size = 32 # Вышеописанная директория - path_to_data = "Data50" - - train_ds, val_ds = keras.utils.image_dataset_from_directory( - path_to_data, - validation_split=0.2, - subset="both", - seed=1337, - label_mode="categorical", - shuffle=True, - image_size=image_size, - batch_size=batch_size, - color_mode="grayscale", - crop_to_aspect_ratio=True, - ) + data_directory = "Data50" + + # Разделение на тренировочные и тестовые данные в долях. Указывается доля тестовых данны (.2 по-умолчанию) + validation_split = 0.2 + + i = importer(image_size, batch_size, data_directory, validation_split) # Получение имён классов, числа классов. - # TODO: Rework - labels = np.array([]) - for x, y in val_ds: - labels = np.concatenate([labels, np.argmax(y.numpy(), axis=-1)]) - - class_names = set(labels) - num_classes = len(class_names) - print(len(labels)) + class_names = i.class_names + num_classes = i.num_classes ## Обработка данных # Вносится рандомизация (ротация, зум, перемещение). Также приводится яркость к понятному нейросети формату (вместо 0-255, 0-1). - data_augmentation_layers = [ - layers.RandomRotation(0.08), - layers.RandomZoom( - height_factor=[-0.2,0.2], - width_factor=[-0.2,0.2], - fill_mode="constant", - fill_value=255.0, - ), - layers.RandomTranslation( - height_factor = [-0.1, 0.1], - width_factor = [-0.1, 0.1], - fill_mode="constant", - fill_value=255.0, - ), - layers.Rescaling(1.0 / 255) - ] - - def data_augmentation(images): - for layer in data_augmentation_layers: - images = layer(images) - return images + i.generate_augmentation_layers(0.2, 0.1, 0.08) ### Применение слоёв обработки данных - train_ds = train_ds.map( - lambda img, label: (data_augmentation(img), label), - num_parallel_calls=tf_data.AUTOTUNE, - ) + i.apply_augmentation() + + ## Получение данных для модели + + train_ds = i.train_ds + val_ds = i.validation_ds ## Формирование модели @@ -108,58 +63,38 @@ def data_augmentation(images): # Последний слой имеет число нейронов, равное количеству классов. - model = Sequential() - - model.add(keras.Input(shape=(height,width,1))) - - model.add(Conv2D(16, (3,3), 1, activation='relu')) - model.add(MaxPooling2D()) - # model.add(Dropout(0.25)) - - model.add(Conv2D(32, (3,3), 1, activation='relu')) - model.add(MaxPooling2D()) - # model.add(Dropout(0.25)) - - model.add(Conv2D(16, (3,3), 1, activation='relu')) - model.add(MaxPooling2D()) - # model.add(Dropout(0.25)) - - model.add(Flatten()) - model.add(Dense(256, activation='relu')) - # model.add(Dropout(0.25)) - - model.add(Dense(num_classes, activation = "softmax")) - - # Компиляция модели - optimizer = RMSprop(learning_rate=0.001, rho=0.9, epsilon=1e-08) - model.compile(optimizer = optimizer , loss = "categorical_crossentropy", metrics=["accuracy"]) + # Первое число -- количество фильтров, второе -- окно в пискислях (3 на 3, напрмиер), которым алгоритм проходит по изображению. + # Каждый новый tuple -- новый слой Conv2D. Можно эксперементировать с числами. + conv_descriptor = ( + (16, (3,3)), + (32, (3,3)), + (16, (3,3)) + ) + # Количсетво простых слоёв + dense_layer_number = 1 - model.summary() + m = model(image_size, num_classes, dense_layer_number, conv_descriptor) + m.compile() # Обучение нейросети # Число проходов по набору данных. Не всегда улучшает результат. Надо смотреть на графики. (50 по умолчанию, при малом наборе данных) epochs = 50 - learning_rate_reduction = ReduceLROnPlateau(monitor='accuracy', - patience=3, - verbose=1, - factor=0.5, - min_lr=0.00001) - callbacks = [ - # keras.callbacks.ModelCheckpoint("save_at_{epoch}.keras"), - learning_rate_reduction - ] + m.init_learning_rate_reduction() + # m.init_save_at_epoch() + + m.train(train_ds, epochs, val_ds) - history = model.fit(train_ds, epochs = epochs, validation_data = val_ds, callbacks=callbacks) + history = m.history + model_instance = m.model # Визуализация - pe = plotter_evaluator(history, model, class_names) + pe = plotter_evaluator(history, model_instance, class_names) pe.calc_predictions(val_ds) ## Графики потерь и точности - # Высокой должна быть и accuracy и val_accuracy. Первая -- точность на обучающей выборке, вторая -- на тестовой. # Когда/если точность на обучающей выборке начинает превосходить точность на тестовой, продолжать обучение не следует. @@ -168,13 +103,11 @@ def data_augmentation(images): pe.plot_loss_accuracy() ## Вычисление отчёта о качестве классификации - # Значения accuracy, recall, f1 должны быть высокими. pe.print_report() ## Матрица запутанности - # Хорший способ понять, как именно нейросеть ошибается pe.plot_confusion_matrix() diff --git a/image_recognition/modules/__init__.py b/image_recognition/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/image_recognition/modules/importer.py b/image_recognition/modules/importer.py new file mode 100644 index 0000000..b8fd568 --- /dev/null +++ b/image_recognition/modules/importer.py @@ -0,0 +1,74 @@ + + +import keras +from tensorflow import data as tf_data +import numpy as np + +class importer: + + __SEED = 42 + __data_augmentation_layers: list + + train_ds: tf_data.Dataset + validation_ds: tf_data.Dataset + + class_names: set + num_classes: int + + def __init__(self, image_size, batch_size, data_directory, validation_split): + + self.train_ds, self.validation_ds = keras.utils.image_dataset_from_directory( + data_directory, + validation_split=validation_split, + subset="both", + seed=self.__SEED, + label_mode="categorical", + shuffle=True, + image_size=image_size, + batch_size=batch_size, + color_mode="grayscale", + crop_to_aspect_ratio=True, + ) + + self.__get_stats() + + def generate_augmentation_layers(self, zoom_factor: float, move_factor: float, rotation_factor: float): + + self.__data_augmentation_layers = [ + keras.layers.RandomRotation(rotation_factor), + keras.layers.RandomZoom( + height_factor=[-zoom_factor,zoom_factor], + width_factor=[-zoom_factor,zoom_factor], + fill_mode="constant", + fill_value=255.0 + ), + keras.layers.RandomTranslation( + height_factor = [-move_factor, move_factor], + width_factor = [-move_factor, move_factor], + fill_mode="constant", + fill_value=255.0, + ), + keras.layers.Rescaling(1.0 / 255) + ] + + def apply_augmentation(self): + + self.train_ds = self.train_ds.map( + lambda img, label: (self.__data_augmentation(img), label), + num_parallel_calls=tf_data.AUTOTUNE, + ) + + def __data_augmentation(self, images): + for layer in self.__data_augmentation_layers: + images = layer(images) + return images + + def __get_stats(self): + + # TODO: Rework + labels = np.array([]) + for _, y in self.validation_ds: # type: ignore + labels = np.concatenate([labels, np.argmax(y.numpy(), axis=-1)]) + + self.class_names = set(labels) + self.num_classes = len(self.class_names) \ No newline at end of file diff --git a/image_recognition/modules/model.py b/image_recognition/modules/model.py new file mode 100644 index 0000000..f41c04d --- /dev/null +++ b/image_recognition/modules/model.py @@ -0,0 +1,71 @@ + + +import keras +from keras.models import Sequential +from keras.layers import Conv2D, MaxPooling2D, Dense, Flatten, Dropout +from keras.optimizers import RMSprop +from keras.callbacks import History + +from keras.callbacks import ReduceLROnPlateau +from tensorflow import data as tf_data + +class model: + + model: keras.Model + history: History + callbacks = [] + + def __init__(self, image_size: tuple[int,int], num_classes: int, dense_layers_number: int, conv_descriptor: tuple): + + model = Sequential() + + model.add(keras.Input(shape=image_size + (1, ))) + + model.add(Conv2D(16, (3,3), 1, activation='relu')) + model.add(MaxPooling2D()) + # model.add(Dropout(0.25)) + + model.add(Conv2D(32, (3,3), 1, activation='relu')) + model.add(MaxPooling2D()) + # model.add(Dropout(0.25)) + + model.add(Conv2D(16, (3,3), 1, activation='relu')) + model.add(MaxPooling2D()) + # model.add(Dropout(0.25)) + + model.add(Flatten()) + model.add(Dense(256, activation='relu')) + # model.add(Dropout(0.25)) + + model.add(Dense(num_classes, activation = "softmax")) + + self.model = model + + model.summary() + + def compile(self): + + # Компиляция модели + optimizer = RMSprop(learning_rate=0.001, rho=0.9, epsilon=1e-08) + self.model.compile(optimizer = optimizer , loss = "categorical_crossentropy", metrics=["accuracy"]) + # self.model.summary() + + def train(self, train_ds: tf_data.Dataset, epochs: int, validation_data: tf_data.Dataset): + + self.history = self.model.fit(train_ds, epochs = epochs, validation_data = validation_data, callbacks=self.callbacks) + + def init_learning_rate_reduction(self): + + learning_rate_reduction = ReduceLROnPlateau( + monitor='accuracy', + patience=3, + verbose=1, + factor=0.5, + min_lr=0.00001 + ) + + self.callbacks.append(learning_rate_reduction) + + def init_save_at_epoch(self): + + self.callbacks.append(keras.callbacks.ModelCheckpoint("save_at_{epoch}.keras")) \ No newline at end of file diff --git a/image_recognition/visualization.py b/image_recognition/modules/visualization.py similarity index 81% rename from image_recognition/visualization.py rename to image_recognition/modules/visualization.py index e2ddffb..5e254b6 100644 --- a/image_recognition/visualization.py +++ b/image_recognition/modules/visualization.py @@ -1,64 +1,65 @@ - - -import clang -import matplotlib.pyplot as plt -import numpy as np -from sklearn.metrics import classification_report -import keras.callbacks as cb -import keras as k -from scikitplot.metrics import plot_confusion_matrix - -class plotter_evaluator: - - model: k.Model - history: cb.History - class_names: list - labels: np.ndarray - pred_labels: list - - def __init__(self, history: cb.History, model: k.Model, class_names: set): - self.history = history - self.model = model - - if type(list(class_names)[0]) is not str: - self.class_names=list(map(str,class_names)) - else: - self.class_names = list(class_names) - - def calc_predictions(self, test_values: list): - - labels = np.array([]) - for _, y in test_values: - labels = np.concatenate([labels, np.argmax(y.numpy(), axis=-1)]) - - self.labels = labels - - Predictions = self.model.predict(test_values) - self.pred_labels = np.argmax(Predictions, axis = 1) - - def plot_loss_accuracy(self): - - history = self.history - fig = plt.figure(figsize=(9,5)) - - plt.subplot(211) - plt.plot(history.history['loss'], color='teal', label='loss') - plt.plot(history.history['val_loss'], color='orange', label='val_loss') - plt.legend(loc="upper left") - - plt.subplot(212) - plt.plot(history.history['accuracy'], color='teal', label='accuracy') - plt.plot(history.history['val_accuracy'], color='orange', label='val_accuracy') - plt.legend(loc="upper left") - - fig.suptitle('Loss and Accuracy', fontsize=19) - plt.show() - - def print_report(self): - - print(classification_report(self.labels, self.pred_labels, target_names=self.class_names)) - - def plot_confusion_matrix(self): - - fig, ax = plt.subplots(1, 2, figsize = (25, 8)) + + +import clang +import matplotlib.pyplot as plt +import numpy as np +from sklearn.metrics import classification_report +from keras.callbacks import History +import keras +from scikitplot.metrics import plot_confusion_matrix +from tensorflow import data as tf_data + +class plotter_evaluator: + + model: keras.Model + history: History + class_names: list + labels: np.ndarray + pred_labels: list + + def __init__(self, history: History, model: keras.Model, class_names: set): + self.history = history + self.model = model + + if type(list(class_names)[0]) is not str: + self.class_names=list(map(str,class_names)) + else: + self.class_names = list(class_names) + + def calc_predictions(self, test_values: tf_data.Dataset): + + labels = np.array([]) + for _, y in test_values: # type: ignore + labels = np.concatenate([labels, np.argmax(y.numpy(), axis=-1)]) + + self.labels = labels + + Predictions = self.model.predict(test_values) + self.pred_labels = np.argmax(Predictions, axis = 1) + + def plot_loss_accuracy(self): + + history = self.history + fig = plt.figure(figsize=(9,5)) + + plt.subplot(211) + plt.plot(history.history['loss'], color='teal', label='loss') + plt.plot(history.history['val_loss'], color='orange', label='val_loss') + plt.legend(loc="upper left") + + plt.subplot(212) + plt.plot(history.history['accuracy'], color='teal', label='accuracy') + plt.plot(history.history['val_accuracy'], color='orange', label='val_accuracy') + plt.legend(loc="upper left") + + fig.suptitle('Loss and Accuracy', fontsize=19) + plt.show() + + def print_report(self): + + print(classification_report(self.labels, self.pred_labels, target_names=self.class_names)) + + def plot_confusion_matrix(self): + + fig, ax = plt.subplots(1, 2, figsize = (25, 8)) ax = plot_confusion_matrix(self.labels, self.pred_labels, ax = ax[0], cmap= 'YlGnBu') \ No newline at end of file