## Лабораторная работа №3 ИС. Распознавание изображений #### Выполнили: Ишутина Е. И., Голубев Т. Л. В работе проводится исследование моделей глубокого обучения при классификации изображений. Рассматривались два набора данных: MNIST с черно-белыми изображениями цифр, и CIFAR-10 с цветными изображениями десяти классов (cat, deer, truck и т.д.) размерности 32×32 пикселя. Для обоих наборов была выполнена нормализация и приведение меток классов к формату one-hot. One-hot - кодирование данных в виде вектора, содержащего столько элементов, сколько существует классов. Все элементы равны нулю (или близки) кроме значения на позиции, соответствующей истинному классу (там значение ближе к единице). Такой формат нужен в нейронных сетях, где выходной слой формирует распределение вероятностей по классам. Для набора MNIST обучена сверточная нейронная сеть, а затем произведено её сравнение с лучшей полносвязной моделью из ЛР1. Для набора CIFAR-10 была реализована модель сверточной нейронной сети и оценена результативность ее работы. ## Задание 1 #### *1. В среде Google Colab создать новый блокнот (notebook). Импортировать необходимые для работы библиотеки и модули* Подключены библиотеки. Создана рабочая директория на Google Диске и зафиксированы генераторы случайных чисел для обеспечения воспроизводимости результатов. ```python from google.colab import drive drive.mount('/content/drive') import os os.chdir('/content/drive/MyDrive/Colab Notebooks/is_lab3') import numpy as np import matplotlib.pyplot as plt from tensorflow import keras from tensorflow.keras import layers from tensorflow.keras.models import Sequential from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay from sklearn.model_selection import train_test_split import tensorflow as tf tf.random.set_seed(123) np.random.seed(123) ``` ```python Mounted at /content/drive ``` #### *2. Загрузить набор данных MNIST, содержащий размеченные изображения рукописных цифр.* Загружен набор данных MNIST, включающий 70 000 размеченных изображений рукописных цифр размерностью 28×28 пикселей. Набор состоял из 60 000 изображений обучающей выборки и 10 000 изображений тестовой выборки, при этом каждой матрице пикселей соответствовала метка класса от 0 до 9. После загрузки обе части набора были объединены в единые массивы данных, чтобы потом выполнить разбиение согласно варианту задания. ```python from keras.datasets import mnist (X_train_full, y_train_full), (X_test_full, y_test_full) = mnist.load_data() X = np.concatenate((X_train_full, X_test_full), axis=0) y = np.concatenate((y_train_full, y_test_full), axis=0) ``` ```python Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz 11490434/11490434 ━━━━━━━━━━━━━━━━━━━━ 0s 0us/step ``` #### *3. Разбить набор данных на обучающие и тестовые данные в соотношении 60000:10000 элементов. При разбиении параметр random_state выбрать равным (4k–1), где k – номер бригады. Вывести размерности полученных обучающих и тестовых массивов данных.* ```python k = 5 random_state = 4 * k - 1 X_train, X_test, y_train, y_test = train_test_split( X, y, train_size=60000, test_size=10000, random_state=random_state, shuffle=True ) print('Shape of X_train:', X_train.shape) print('Shape of y_train:', y_train.shape) print('Shape of X_test:', X_test.shape) print('Shape of y_test:', y_test.shape) ``` Выведенные размерности подтвердили корректность проведённого разбиения и соответствие полученных массивов заданным параметрам. ```python Shape of X_train: (60000, 28, 28) Shape of y_train: (60000,) Shape of X_test: (10000, 28, 28) Shape of y_test: (10000,) ``` #### *4. Провести предобработку данных: привести обучающие и тестовые данные к формату, пригодному для обучения сверточной нейронной сети. Входные данные должны принимать значения от 0 до 1, метки цифр должны быть закодированы по принципу «one-hot encoding». Вывести размерности предобработанных обучающих и тестовых массивов данных.* Значения пикселей приведены к диапазону [0, 1], метки классов были преобразованы в формат one-hot, где каждый класс представлен вектором длины десять. ```python num_classes = 10 input_shape = (28, 28, 1) # приведение значений к диапазону [0,1] X_train = X_train.astype('float32') / 255.0 X_test = X_test.astype('float32') / 255.0 # добавление размерности каналов X_train = np.expand_dims(X_train, -1) X_test = np.expand_dims(X_test, -1) # one-hot кодирование меток y_train_cat = keras.utils.to_categorical(y_train, num_classes) y_test_cat = keras.utils.to_categorical(y_test, num_classes) print('Shape of transformed X_train:', X_train.shape) print('Shape of transformed y_train:', y_train_cat.shape) print('Shape of transformed X_test:', X_test.shape) print('Shape of transformed y_test:', y_test_cat.shape) ``` Выведенные размерности подтвердили корректное преобразование изображений в тензоры формы 28×28×1 и меток в матрицы 60000×10 и 10000×10 для обучающей и тестовой выборок соответственно. ```python Shape of transformed X_train: (60000, 28, 28, 1) Shape of transformed y_train: (60000, 10) Shape of transformed X_test: (10000, 28, 28, 1) Shape of transformed y_test: (10000, 10) ``` #### *5. Реализовать модель сверточной нейронной сети и обучить ее на обучающих данных с выделением части обучающих данных в качестве валидационных. Вывести информацию об архитектуре нейронной сети.* Пояснения по коду: * batch_size – размер батча (количество изображений, обрабатываемых одновременно за один шаг градиентного спуска). Используется Sequential API, где слои добавляются один за другим. Это удобно для простых последовательных моделей CNN. * Conv2D(32, (3,3)): 32 фильтра размером 3×3, которые будут сканировать изображение. * input_shape=input_shape: форма входных данных (например, (28,28,1) для серых изображений MNIST). * MaxPooling2D(2,2) уменьшает размерность признаков в 2 раза, выбирая максимум в каждом окне 2×2. * Dropout(0.5) случайным образом отключает 50% нейронов во время обучения, чтобы уменьшить переобучение. ```python batch_size = 512 epochs = 15 model = Sequential() model.add(layers.Conv2D(32, kernel_size=(3,3), activation='relu', input_shape=input_shape)) model.add(layers.MaxPooling2D(pool_size=(2,2))) model.add(layers.Conv2D(64, kernel_size=(3,3), activation='relu')) model.add(layers.MaxPooling2D(pool_size=(2,2))) model.add(layers.Dropout(0.5)) model.add(layers.Flatten()) model.add(layers.Dense(num_classes, activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) model.summary() history = model.fit(X_train, y_train_cat, batch_size=batch_size, epochs=epochs, validation_split=0.1) ``` ```python /usr/local/lib/python3.12/dist-packages/keras/src/layers/convolutional/base_conv.py:113: UserWarning: Do not pass an `input_shape`/`input_dim` argument to a layer. When using Sequential models, prefer using an `Input(shape)` object as the first layer in the model instead. super().__init__(activity_regularizer=activity_regularizer, **kwargs) Model: "sequential" | Layer (type) | Output shape | Param # | |----------------------------|----------------------|---------| | conv2d (Conv2D) | (None, 26, 26, 32) | 320 | | max_pooling2d (MaxPooling) | (None, 13, 13, 32) | 0 | | conv2d_1 (Conv2D) | (None, 11, 11, 64) | 18,496 | | max_pooling2d_1 (MaxPooling) | (None, 5, 5, 64) | 0 | | dropout (Dropout) | (None, 5, 5, 64) | 0 | | flatten (Flatten) | (None, 1600) | 0 | | dense (Dense) | (None, 10) | 16,010 | Total params: 34,826 (136.04 KB) Trainable params: 34,826 (136.04 KB) Non-trainable params: 0 (0.00 B) ``` #### *6. Оценить качество обучения на тестовых данных. Вывести значение функции ошибки и значение метрики качества классификациина тестовых данных.* ```python scores = model.evaluate(X_test, y_test_cat, verbose=2) print('Loss on test data:', scores[0]) print('Accuracy on test data:', scores[1]) ``` ```python 313/313 - 3s - 8ms/step - accuracy: 0.9879 - loss: 0.0402 Loss on test data: 0.04024936258792877 Accuracy on test data: 0.9879000186920166 ``` Vодель обучена хорошо и показывает высокое качество на тестовой выборке. #### *7. Подать на вход обученной модели два тестовых изображения. Вывести изображения, истинные метки и результаты распознавания.* Пояснения по коду: * Берём два изображения из тестового набора (с индексами 0 и 1). * X_test[n:n+1] — формируем батч из одного изображения. * model.predict() возвращает вектор вероятностей для каждого класса (10 элементов для цифр 0–9). * NN output vector показывает вероятности для всех 10 классов. ```python indices = [0, 1] for n in indices: result = model.predict(X_test[n:n+1]) plt.figure() plt.imshow(X_test[n].reshape(28,28), cmap='gray') plt.title(f"Real: {y_test[n]} Pred: {np.argmax(result)}") plt.axis('off') plt.show() print('NN output vector:', result) print('Real mark:', y_test[n]) print('NN answer:', np.argmax(result)) ``` ![image](pics/i1.png) ```python NN output vector: [[3.5711860e-08 3.5435047e-11 6.5117740e-07 7.4699518e-09 5.9110135e-08 1.4115658e-03 9.9851364e-01 2.6488631e-12 7.4022493e-05 2.6488609e-10]] Real mark: 6 NN answer: 6 ``` ![image](pics/i2.png) ```python NN output vector: [[9.2878885e-08 3.3229617e-06 4.1963812e-04 3.1485452e-04 1.7722991e-09 2.6501787e-09 5.7302459e-13 9.9888808e-01 1.0063148e-05 3.6401587e-04]] Real mark: 7 NN answer: 7 ``` Как видно, модель верно распознала случайно выбранные две цифры. В выходном векторе у всех значений, кроме позиции верного класса, были значения порядка 10^-4 - 10^-13. Значение для верного класса близко к единице. #### *8. Вывести отчет о качестве классификации тестовой выборки и матрицу ошибок для тестовой выборки.* * Precision = 0.99 для класса 0 означает, что почти все объекты, которые сеть предсказала как «0», действительно 0. Recall 1.00 для класса 0 означает, что сеть нашла все объекты «0» в тестовой выборке. Accuracy (общая точность) = 0.99 дает понять, что модель правильно классифицирует 99% изображений. * В матрице ошибок основная часть значений находится на диагонали, а значит, большинство предсказаний верные. С помощью небольших ошибок вне диагоналей можно понять, какие числа нейросеть «путает». Например, сеть может перепутать «4» и «9», или «3» и «5», если они визуально похожи. ```python true_labels = y_test predicted_labels = np.argmax(model.predict(X_test), axis=1) print(classification_report(true_labels, predicted_labels)) conf_matrix = confusion_matrix(true_labels, predicted_labels) display = ConfusionMatrixDisplay(confusion_matrix=conf_matrix) display.plot() plt.show() ``` ```python precision recall f1-score support 0 0.99 1.00 0.99 969 1 0.99 0.99 0.99 1155 2 0.99 0.98 0.98 969 3 0.99 0.99 0.99 1032 4 1.00 0.98 0.99 1016 5 0.98 0.99 0.98 898 6 0.99 0.99 0.99 990 7 0.98 0.99 0.99 1038 8 0.99 0.98 0.99 913 9 0.99 0.98 0.98 1020 accuracy 0.99 10000 macro avg 0.99 0.99 0.99 10000 weighted avg 0.99 0.99 0.99 10000 ``` ![image](pics/i3.png) #### *9. Загрузить, предобработать и подать на вход обученной нейронной сети собственное изображение, созданное при выполнении лабораторной работы №1. Вывести изображение и результат распознавания.* ```python from PIL import Image img_path = '../5.png' file_data = Image.open(img_path) file_data = file_data.convert('L') # перевод в градации серого test_img = np.array(file_data) plt.imshow(test_img, cmap='gray') plt.axis('off') plt.show() # нормализация и изменение формы test_proc = test_img.astype('float32') / 255.0 test_proc = np.reshape(test_proc, (1, 28, 28, 1)) result = model.predict(test_proc) print("NN output vector:", result) print("I think it's", np.argmax(result)) ``` ![image](pics/i4.png) ```python NN output vector: [[1.5756325e-12 5.2755486e-15 1.4891595e-10 7.3797599e-07 1.8559115e-12 9.9998915e-01 3.5407410e-08 5.2025315e-12 1.5018414e-06 8.6681475e-06]] I think it's 5 ``` ### 10. Загрузить с диска модель, сохраненную при выполнении лабораторной работы №1. Вывести информацию об архитектуре модели.Повторить для этой модели п.6. При работе с моделью из ЛР1 необходимо взять данные в исходном формате, иначе получится двойная нормализация. В CNN данные нормализовались на этапе подготовки к сети (X/255.0 и reshape к (28,28,1)), но модель из ЛР1 ожидала плоский вектор 784 элементов на изображение. ```python # возьмём оригинальные X, y — до всех преобразований для CNN (X_train_full, y_train_full), (X_test_full, y_test_full) = mnist.load_data() # объединим, чтобы сделать то же разбиение, что и в ЛР1 X_all = np.concatenate((X_train_full, X_test_full), axis=0) y_all = np.concatenate((y_train_full, y_test_full), axis=0) from sklearn.model_selection import train_test_split X_train_l1, X_test_l1, y_train_l1, y_test_l1 = train_test_split( X_all, y_all, train_size=60000, test_size=10000, random_state=19 ) # теперь — подготовка данных ЛР1 X_test_lr1 = X_test_l1.reshape((X_test_l1.shape[0], 28*28)).astype('float32') / 255.0 y_test_lr1 = keras.utils.to_categorical(y_test_l1, 10) # оценка модели scores_lr1 = model_lr1.evaluate(X_test_lr1, y_test_lr1, verbose=2) print(scores_lr1) ``` Точность уменьшилась, так как в ЛР1 использовалась полносвязная сеть, а не сверточная. Сверточные сети лучше извлекают признаки у изображений, а потому дают большую точность. ```python 313/313 - 2s - 6ms/step - accuracy: 0.9445 - loss: 0.1969 [0.1968761384487152, 0.9445000290870667] ``` ### 11. Сравнить обученную модель сверточной сети и наилучшую модель полносвязной сети из лабораторной работы №1 по следующим показателям: количество настраиваемых параметров в сети, количество эпох обучения, качество классификации тестовой выборки. Сделать выводы по результатам применения сверточной нейронной сети для распознавания изображений. ```python # загрузка сохранённой модели ЛР1 model_lr1_path = '../best_model_2x100.h5' model_lr1 = load_model(model_lr1_path) model_lr1.summary() # подготовка тестового набора для модели ЛР1 X_test_l1 = X_test_l1.reshape((X_test_l1.shape[0], 28 * 28)).astype('float32') / 255.0 y_test_l1_cat = keras.utils.to_categorical(y_test_l1, 10) # оценка модели ЛР1 scores_lr1 = model_lr1.evaluate(X_test_l1, y_test_l1_cat, verbose=2) print('LR1 model - Loss:', scores_lr1[0]) print('LR1 model - Accuracy:', scores_lr1[1]) # оценка сверточной модели ЛР3 scores_conv = model.evaluate(X_test, y_test_cat, verbose=2) print('Conv model - Loss:', scores_conv[0]) print('Conv model - Accuracy:', scores_conv[1]) # вывод числа параметров обеих моделей print('LR1 model parameters:', model_lr1.count_params()) print('Conv model parameters:', model.count_params()) ``` В MLP (ЛР1) количество параметров = 89610, а в CNN (ЛР3) оно равно 34826. CNN имеет значительно меньше параметров, примерно в 2,5 раза меньше, чем MLP. Это произошло потому, что в сверточных слоях параметры делятся по ядрам свертки и применяются к локальным областям изображения, что снижает избыточность. MLP полностью соединяет все нейроны между слоями, а значит, имеет больше весов. Меньшее число параметров влечет к меньшей вероятности переобучения и более экономное использование памяти. Значение функции потерь у CNN почти в 5 раз меньше, что указывает на лучшее соответствие предсказаний истинным меткам. ```python WARNING:absl:Compiled the loaded model, but the compiled metrics have yet to be built. `model.compile_metrics` will be empty until you train or evaluate the model. Model: "sequential_9" | Layer (type) | Output shape | Param # | |--------------|--------------|---------| | dense_18 | (None, 100) | 78,500 | | dense_19 | (None, 100) | 10,100 | | dense_20 | (None, 10) | 1,010 | Total params: 89,612 (350.05 KB) Trainable params: 89,610 (350.04 KB) Non-trainable params: 0 (0.00 B) Optimizer params: 2 (12.00 B) 313/313 - 3s - 9ms/step - accuracy: 0.9445 - loss: 0.1969 LR1 model - Loss: 0.1968761384487152 LR1 model - Accuracy: 0.9445000290870667 313/313 - 6s - 20ms/step - accuracy: 0.9879 - loss: 0.0402 Conv model - Loss: 0.04024936258792877 Conv model - Accuracy: 0.9879000186920166 LR1 model parameters: 89610 Conv model parameters: 34826 ``` ## Задание 2. #### *1–3. Загрузка CIFAR-10 и разбиение 50 000 : 10 000, вывод 25 изображений* CIFAR-10 — это стандартный набор цветных изображений маленького размера (32×32 пикселя) с 10 классами объектов, включая транспорт, животных и птиц, предназначенный для задач классификации изображений. Аналогично заданию №1, данные нормализуются и преобразовываются в формат one-hot. ```python from keras.datasets import cifar10 (X_train_c, y_train_c), (X_test_c, y_test_c) = cifar10.load_data() print('Shapes (original):', X_train_c.shape, y_train_c.shape, X_test_c.shape, y_test_c.shape) class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck'] # вывод 25 изображений plt.figure(figsize=(10,10)) for i in range(25): plt.subplot(5,5,i+1) plt.xticks([]) plt.yticks([]) plt.grid(False) plt.imshow(X_train_c[i]) plt.xlabel(class_names[y_train_c[i][0]]) plt.show() ``` ```python Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz 170498071/170498071 ━━━━━━━━━━━━━━━━━━━━ 4s 0us/step Shapes (original): (50000, 32, 32, 3) (50000, 1) (10000, 32, 32, 3) (10000, 1) ``` ![image](pics/i5.png) #### *4. Предобработка CIFAR-10 (нормализация и one-hot)* ```python num_classes = 10 input_shape_cifar = (32, 32, 3) X_train_c = X_train_c.astype('float32') / 255.0 X_test_c = X_test_c.astype('float32') / 255.0 y_train_c_cat = keras.utils.to_categorical(y_train_c, num_classes) y_test_c_cat = keras.utils.to_categorical(y_test_c, num_classes) print('Transformed shapes:', X_train_c.shape, y_train_c_cat.shape, X_test_c.shape, y_test_c_cat.shape) ``` ```python Transformed shapes: (50000, 32, 32, 3) (50000, 10) (10000, 32, 32, 3) (10000, 10) ``` #### *5. Реализация и обучение сверточной сети для CIFAR-10* Используются три слоя Conv2D с увеличивающимся числом фильтров (32 → 64 → 128) для извлечения признаков с изображений CIFAR-10. Между сверточными слоями используются MaxPooling2D для уменьшения размерности и концентрации на важных признаках. ```python model_cifar = Sequential() model_cifar.add(layers.Conv2D(32, (3,3), activation='relu', input_shape=input_shape_cifar)) model_cifar.add(layers.MaxPooling2D((2,2))) model_cifar.add(layers.Conv2D(64, (3,3), activation='relu')) model_cifar.add(layers.MaxPooling2D((2,2))) model_cifar.add(layers.Conv2D(128, (3,3), activation='relu')) model_cifar.add(layers.MaxPooling2D((2,2))) model_cifar.add(layers.Flatten()) model_cifar.add(layers.Dense(128, activation='relu')) model_cifar.add(layers.Dropout(0.5)) model_cifar.add(layers.Dense(num_classes, activation='softmax')) model_cifar.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) model_cifar.summary() batch_size = 512 epochs = 20 history_cifar = model_cifar.fit(X_train_c, y_train_c_cat, batch_size=batch_size, epochs=epochs, validation_split=0.1) ``` ```python /usr/local/lib/python3.12/dist-packages/keras/src/layers/convolutional/base_conv.py:113: UserWarning: Do not pass an `input_shape`/`input_dim` argument to a layer. When using Sequential models, prefer using an `Input(shape)` object as the first layer in the model instead. super().__init__(activity_regularizer=activity_regularizer, **kwargs) Model: "sequential_1" | Layer (type) | Output Shape | Param # | | ------------------------------ | ------------------ | ------- | | conv2d_2 (Conv2D) | (None, 30, 30, 32) | 896 | | max_pooling2d_2 (MaxPooling2D) | (None, 15, 15, 32) | 0 | | conv2d_3 (Conv2D) | (None, 13, 13, 64) | 18,496 | | max_pooling2d_3 (MaxPooling2D) | (None, 6, 6, 64) | 0 | | conv2d_4 (Conv2D) | (None, 4, 4, 128) | 73,856 | | max_pooling2d_4 (MaxPooling2D) | (None, 2, 2, 128) | 0 | | flatten_1 (Flatten) | (None, 512) | 0 | | dense_1 (Dense) | (None, 128) | 65,664 | | dropout_1 (Dropout) | (None, 128) | 0 | | dense_2 (Dense) | (None, 10) | 1,290 | Total params: 160,202 (625.79 KB) Trainable params: 160,202 (625.79 KB) Non-trainable params: 0 (0.00 B) ``` #### *6. Оценка качества на тестовой выборке CIFAR-10* ```python scores_cifar = model_cifar.evaluate(X_test_c, y_test_c_cat, verbose=2) print('CIFAR - Loss on test data:', scores_cifar[0]) print('CIFAR - Accuracy on test data:', scores_cifar[1]) ``` ```python 313/313 - 8s - 26ms/step - accuracy: 0.6855 - loss: 0.8885 CIFAR - Loss on test data: 0.8884508609771729 CIFAR - Accuracy on test data: 0.6855000257492065 ``` #### *7-8. Подать два тестовых изображения: одно верно, другое ошибочно. Вывести отчет о качестве классификации тестовой выборки и матрицу ошибок для тестовой выборки* ```python print(classification_report(true_cifar, preds_cifar, target_names=class_names)) conf_matrix_cifar = confusion_matrix(true_cifar, preds_cifar) display = ConfusionMatrixDisplay(confusion_matrix=conf_matrix_cifar, display_labels=class_names) plt.figure(figsize=(10,10)) # figsize задаётся здесь display.plot(cmap='Blues', colorbar=False) # без figsize plt.xticks(rotation=45) plt.show() ``` ```python precision recall f1-score support airplane 0.78 0.66 0.71 1000 automobile 0.82 0.81 0.81 1000 bird 0.61 0.55 0.58 1000 cat 0.49 0.43 0.46 1000 deer 0.62 0.67 0.64 1000 dog 0.51 0.71 0.59 1000 frog 0.81 0.73 0.77 1000 horse 0.72 0.71 0.71 1000 ship 0.77 0.82 0.80 1000 truck 0.80 0.76 0.78 1000 accuracy 0.69 10000 macro avg 0.69 0.69 0.69 10000 weighted avg 0.69 0.69 0.69 10000
``` ![image](pics/i6.png) Для CIFAR-10 точность составила ~68.55%, что ниже, чем для MNIST, из-за большей сложности изображений (цветные, более сложные объекты). Видно, что классы cat и dog хуже распознаются (точность 0.49 и 0.51), а automobile, ship, truck распознаются лучше (~0.8). Матрица ошибок (ConfusionMatrixDisplay) позволяет визуально увидеть, какие классы чаще путаются между собой (например, cat и dog). ## Вывод Проведенное исследование показало, что сверточные нейронные сети превосходят полносвязные модели в задачах распознавания изображений. Для MNIST сверточная сеть достигла точности 98,79% при меньшем числе параметров (34 826 против 89 610 у MLP), что обеспечивает более экономное использование памяти и снижает риск переобучения. На более сложном наборе CIFAR-10 сеть показала точность 68,55%, при этом объекты с визуально схожими признаками, например кошки и собаки, распознаются хуже, чем однозначные объекты, такие как автомобили и корабли. Результаты демонстрируют, что сверточные сети эффективно извлекают признаки из изображений и обеспечивают высокое качество классификации, особенно на структурированных данных, хотя для сложных цветных изображений требуется более глубокая архитектура или дополнительные методы улучшения обучения.