## Лабораторная работа №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

<Figure size 1000x1000 with 0 Axes>
```
![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%, при этом объекты с визуально схожими признаками, например кошки и собаки, распознаются хуже, чем однозначные объекты, такие как автомобили и корабли. Результаты демонстрируют, что сверточные сети эффективно извлекают признаки из изображений и обеспечивают высокое качество классификации, особенно на структурированных данных, хотя для сложных цветных изображений требуется более глубокая архитектура или дополнительные методы улучшения обучения.