diff --git a/.gitignore b/.gitignore index 2d0fadb..aebf5f5 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,8 @@ dmypy.json # templates .github/templates/* + +# Dataset + +Data* +**/Data* \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..dcb1530 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.analysis.typeCheckingMode": "basic", + "python.analysis.autoImportCompletions": true +} \ No newline at end of file diff --git a/README.md b/README.md index 373987c..5130a18 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,18 @@ Awesome image_recognition created by RoundRonin -## Install it from PyPI +## Установка приложения ```bash pip install image_recognition ``` +## Установка зависимостей + +```bash +pip install -e . +``` + ## Usage ```py diff --git a/image_recognition/base.py b/image_recognition/base.py index 36dd345..0e19fcf 100644 --- a/image_recognition/base.py +++ b/image_recognition/base.py @@ -15,3 +15,6 @@ # 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 b96db8f..68cd7ef 100644 --- a/image_recognition/cli.py +++ b/image_recognition/cli.py @@ -7,22 +7,174 @@ - 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 def main(): # pragma: no cover - """ - The main function executes on commands: - `python -m image_recognition` and `$ image_recognition `. - - This is your program's entry point. - - You can change this function to do whatever you want. - Examples: - * Run a test suite - * Run a server - * Do some other stuff - * Run a command line application (Click, Typer, ArgParse) - * List all available tasks - * Run an application (Flask, FastAPI, Django, etc.) - """ - print("This will do something") + + base.hello() + ## Формирование классов на основе файловой структуры + + # Внутри указанной директории должны нахдиться папки, имя которых соотвествует классу. + # В папках -- изображения, соответсвующие классу. + + # Размер по вертикали, размер по горизонтали. К этим значениям будут приведены все изображения (сжаты/растянуты, не обрезаны) + height = 140 + width = 90 + image_size = (height, width) + + # Больше -- быстрее, меньше -- точнее. В теории. + 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, + ) + + # Получение имён классов, числа классов. + # 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)) + + ## Обработка данных + + # Вносится рандомизация (ротация, зум, перемещение). Также приводится яркость к понятному нейросети формату (вместо 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 + + ### Применение слоёв обработки данных + + train_ds = train_ds.map( + lambda img, label: (data_augmentation(img), label), + num_parallel_calls=tf_data.AUTOTUNE, + ) + + ## Формирование модели + + # Модель последовательная. Состоит из слоёв, каждый из которых исполняется после предыдущего. + # В первом слое описывается форма подаваемых данных. Первые два параметра -- размеры изображения (описаны в начале). + # Третий пораметр: 1 -- ч/б изображение, 2 -- RGB, 3 -- RGBA + + # Последний слой имеет число нейронов, равное количеству классов. + + 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"]) + + model.summary() + + # Обучение нейросети + + # Число проходов по набору данных. Не всегда улучшает результат. Надо смотреть на графики. (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 + ] + + history = model.fit(train_ds, epochs = epochs, validation_data = val_ds, callbacks=callbacks) + + # Визуализация + + pe = plotter_evaluator(history, model, class_names) + pe.calc_predictions(val_ds) + + ## Графики потерь и точности + + # Высокой должна быть и accuracy и val_accuracy. Первая -- точность на обучающей выборке, вторая -- на тестовой. + # Когда/если точность на обучающей выборке начинает превосходить точность на тестовой, продолжать обучение не следует. + + # Потери (loss) должны быть низкими. + + pe.plot_loss_accuracy() + + ## Вычисление отчёта о качестве классификации + + # Значения accuracy, recall, f1 должны быть высокими. + + pe.print_report() + + ## Матрица запутанности + + # Хорший способ понять, как именно нейросеть ошибается + + pe.plot_confusion_matrix() diff --git a/image_recognition/visualization.py b/image_recognition/visualization.py new file mode 100644 index 0000000..e2ddffb --- /dev/null +++ b/image_recognition/visualization.py @@ -0,0 +1,64 @@ + + +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)) + ax = plot_confusion_matrix(self.labels, self.pred_labels, ax = ax[0], cmap= 'YlGnBu') \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b05f2a6..31f75f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,14 @@ # Add the requirements you need to this file. # or run `make init` to create this file automatically based on the template. # You can also run `make switch-to-poetry` to use the poetry package manager. + +numpy +pandas +matplotlib +scipy==1.11.4 + +scikit-learn +scikit-plot +tensorflow +keras +opencv-python \ No newline at end of file