# Отчет по Лабораторной работе №1 по курсу "Интеллектуальные системы"
## Бригада №5 (Голубев Т. Л., Ишутина Е. И.)

### *1. В среде GoogleColab создать новый блокнот (notebook). Импортировать необходимые для работы библиотеки и модули.*

tensorflow.keras — высокоуровневый API для построения и обучения нейронных сетей. Мы используем его для загрузки датасета и построения моделей.

matplotlib.pyplot — библиотека для визуализации данных, построения графиков и отображения изображений.

numpy — библиотека для работы с многомерными массивами и выполнения численных операций.

sklearn (scikit-learn) — библиотека для машинного обучения, содержит инструменты для предварительной обработки данных, метрик, кластеризации и классификации.

Задание рабочей директории, импорт необходимых библиотек и модулей:

```python
import os
os.chdir('/content/drive/MyDrive/Colab Notebooks')
from tensorflow import keras
import matplotlib.pyplot as plt
import numpy as np
import sklearn
```


### *2. Загрузить набор данных MNIST, содержащий размеченные изображения рукописных цифр*

Набор данных MNIST (Modified National Institute of Standards and Technology) — это стандартный набор изображений для обучения и тестирования алгоритмов распознавания рукописных цифр.
Он содержит 70 000 изображений цифр от 0 до 9, из них 60 000 для обучения (training set) и 10 000 — для тестирования (test set).

Каждое изображение имеет размер 28×28 пикселей и представлено в градациях серого.
В TensorFlow набор данных MNIST встроен в модуль keras.datasets, что позволяет легко загрузить его одной командой.

```python
(X_train, y_train), (X_test, y_test) = mnist.load_data()
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11490434/11490434 ━━━━━━━━━━━━━━━━━━━━ 2s 0us/step
```


* X_train и X_test — массивы изображений рукописных цифр,
* y_train и y_test — соответствующие метки (от 0 до 9), обозначающие, какая цифра изображена.

### *3. Разбить набор данных на обучающие и тестовые данные в соотношении 60000:10000 элементов. При разбиении параметр random_state выбрать равным (4k–1), где k–номер бригады. Вывести размерности полученных обучающих и тестовых массивов данных.*

Параметр random_state обеспечивает воспроизводимость разбиения данных.
Для номера бригады = 5 получим `random_state = 19`.
Функция train_test_split() случайным образом делит данные на обучающую и тестовую выборки указанного размера.

```python
# создание своего разбиения датасета
from sklearn.model_selection import train_test_split

# объединяем в один набор
X = np.concatenate((X_train, X_test))
y = np.concatenate((y_train, y_test))

# разбиваем по вариантам
X_train, X_test, y_train, y_test = train_test_split(X, y,
                                                    test_size = 10000,
                                                    train_size = 60000,
                                                    random_state = 19)
```

```python
# вывод размерностей
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) 
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. Вывести  первые 4 элемента обучающих данных (изображения и метки цифр).*

Данный код отображает первые четыре изображения из обучающего набора и их метки.
Каждое изображение разворачивается обратно в форму 28×28 для корректного отображения.
```python
# вывод первых 4 изображений и их меток
plt.figure(figsize=(8, 2))
for i in range(4):
    plt.subplot(1, 4, i + 1)
    plt.imshow(X_train[i].reshape(28, 28), cmap='gray')
    plt.title(f'Label: {y_train[i]}', fontsize = 6)
    plt.axis('off')
plt.show()
![alt text](image-2.png)
```

### *5. Провести предобработку данных: привести обучающие и тестовые данные к формату, пригодному для обучения нейронной сети. Входныеданные должны  принимать  значения  от  0  до  1, метки  цифрдолжны  быть закодированы  по  принципу  «one-hotencoding».Вывести  размерности предобработанных обучающих и тестовых массивов данных.*

Перед обучением нейронной сети необходимо подготовить данные.
Каждое изображение в наборе MNIST имеет размер 28×28 пикселей, но полносвязная нейронная сеть принимает на вход вектор признаков.
Поэтому каждое изображение «вытягивается» в одномерный вектор длиной 784 (28×28), где каждый элемент соответствует яркости одного пикселя.

Кроме того, метки классов (цифры от 0 до 9) переводятся в формат one-hot encoding.
При этом каждая цифра представляется вектором длиной 10, где только один элемент равен 1 (номер класса), а остальные — 0.
Например, цифра 3 кодируется как [0, 0, 0, 1, 0, 0, 0, 0, 0, 0].
Такой формат удобен для обучения сети, потому что выходной слой содержит 10 нейронов — по одному для каждой цифры.

```python
# развертывание изображений 28x28 в вектор длиной 784 и нормализация
num_pixels = X_train.shape[1] * X_train.shape[2]
X_train = X_train.reshape(X_train.shape[0], num_pixels).astype('float32') / 255
X_test = X_test.reshape(X_test.shape[0], num_pixels).astype('float32') / 255

# кодирование меток по принципу one-hot encoding
from tensorflow.keras.utils import to_categorical
y_train = to_categorical(y_train, 10)
y_test = to_categorical(y_test, 10)
num_classes = y_train.shape[1]

# вывод размерностей
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)
Shape of X train: (60000, 784)
Shape of y train: (60000, 10)
Shape of X test: (10000, 784)
Shape of y test: (10000, 10)
```

### *6. Реализовать модель однослойной нейронной сети и обучить её на обучающих данных с выделением части обучающих данных в качестве валидационных. Вывести информацию об архитектуре нейронной сети. Вывести график функции ошибки на обучающих и валидационных данных по эпохам.*

Модель представляет собой однослойную нейронную сеть без скрытых слоёв.
На вход подаются векторы длиной 784, выходной слой содержит 10 нейронов (по количеству классов).

Используются параметры:
функция активации — `softmax`, функция ошибки — `categorical_crossentropy`, оптимизатор — `sgd`, метрика — `accuracy`.

```python
# создание модели однослойной нейронной сети
from keras.models import Sequential
from keras.layers import Dense

model0 = Sequential()
# добавляем выходной слой
model0.add(Dense(units=num_classes, input_dim=784, activation='softmax'))

# компиляция модели
model0.compile(loss='categorical_crossentropy',
               optimizer='sgd',
               metrics=['accuracy'])

# вывод информации об архитектуре модели
print(model0.summary())
/usr/local/lib/python3.12/dist-packages/keras/src/layers/core/dense.py:93: 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 # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ dense (Dense)                   │ (None, 10)             │         7,850 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 7,850 (30.66 KB)
 Trainable params: 7,850 (30.66 KB)
 Non-trainable params: 0 (0.00 B)
None
```
```python
# обучение модели
H0 = model0.fit(X_train, y_train,
                validation_split=0.1,
                epochs=50,
                verbose=1)

# вывод графика функции ошибки
plt.plot(H0.history['loss'])
plt.plot(H0.history['val_loss'])
plt.grid()
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend(['train_loss', 'val_loss'])
plt.title('Loss by epochs (Model 0)')
plt.show()
Epoch 1/50
1688/1688 ━━━━━━━━━━━━━━━━━━━━ 6s 3ms/step - accuracy: 0.6993 - loss: 1.1736 - val_accuracy: 0.8783 - val_loss: 0.5063
...
![alt text](image-3.png)
```

### *7. Применить обученную модель к тестовым данным. Вывести значение функции ошибки и значение метрики качества классификации на тестовых данных.*

После обучения однослойной нейронной сети проведём оценку её работы на тестовых данных, которые не участвовали в обучении.
Для этого используется метод `evaluate()`, который возвращает значение функции ошибки и метрики качества (`accuracy`).

```python
# оценка качества модели на тестовых данных
scores = model0.evaluate(X_test, y_test)

print('Loss on test data:', scores[0])
print('Accuracy on test data:', scores[1])
313/313 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.9194 - loss: 0.2818
Loss on test data: 0.28366461396217346
Accuracy on test data: 0.9205999970436096
```
Таким образом, точность однослойной нейронной сети на тестовых данных составила примерно 92,4%,
что показывает, что даже простая модель способна достаточно эффективно распознавать изображения рукописных цифр из набора MNIST.

### *8. Добавить в модель один скрытый слой и провести обучение и тестирование (повторить п. 6–7) при 100, 300 и 500 нейронах в скрытом слое. По метрике качества классификации на тестовых данных выбрать наилучшее количество нейронов. В качестве функции активации нейронов в скрытом слое использовать функцию sigmoid.*

В данном пункте добавляется один скрытый слой с функцией активации sigmoid.
Архитектура сети теперь состоит из входного слоя размерности 784, одного скрытого слоя (Dense) с количеством нейронов 100, 300 или 500, выходного слоя из 10 нейронов с активацией `softmax`. Для каждой модели выполняется компиляция, обучение и тестирование аналогично пунктам 6–7.

```python
from keras.models import Sequential
from keras.layers import Dense

neurons = [100, 300, 500]
results = {}

for n in neurons:
    print(f'\n=== Модель со скрытым слоем {n} нейронов ===')
    
    # создание модели
    model = Sequential()
    model.add(Dense(units=n, input_dim=784, activation='sigmoid'))
    model.add(Dense(units=num_classes, activation='softmax'))
    
    # компиляция модели
    model.compile(loss='categorical_crossentropy',
                  optimizer='sgd',
                  metrics=['accuracy'])
    
    # обучение модели
    H = model.fit(X_train, y_train,
                  validation_split=0.1,
                  epochs=50,
                  verbose=1)  # чтобы не печатать все эпохи
    
    # оценка на тестовых данных
    scores = model.evaluate(X_test, y_test, verbose=1)
    results[n] = scores[1]
    
    print(f'Accuracy on test data: {scores[1]:.4f}')

=== Модель со скрытым слоем 100 нейронов ===
Accuracy on test data: 0.9445
=== Модель со скрытым слоем 300 нейронов ===
Accuracy on test data: 0.9347
=== Модель со скрытым слоем 500 нейронов ===
Accuracy on test data: 0.9310
```

```python
plt.figure()
plt.plot(list(results.keys()), list(results.values()), marker='o')
plt.grid()
plt.title('Accuracy on test data depending on hidden layer size')
plt.xlabel('Number of neurons in hidden layer')
plt.ylabel('Accuracy')
plt.show()
![alt text](image-4.png)
```

Результаты тестирования показывают, что с увеличением числа нейронов в скрытом слое точность модели немного снижается. По графику видно, что увеличение числа нейронов не приводит к улучшению качества классификации,
а наоборот, вызывает лёгкое переобучение и снижение точности на тестовой выборке.

Таким образом, наилучший результат достигается при 100 нейронах в скрытом слое
(accuracy ≈ 94,45%), что делает эту архитектуру оптимальной для данной задачи.

### *9. Добавить в наилучшую архитектуру, определённую в п. 8, второй скрытый слой и провести обучение и тестирование (повторить п. 6–7) при 50 и 100 нейронах во втором скрытом слое. В качестве функции активации нейронов в скрытом слое использовать функцию sigmoid.*

В качестве основы берётся лучшая модель из п. 8 — сеть с одним скрытым слоем из 100 нейронов.
Теперь добавляется второй скрытый слой с 50 или 100 нейронами и функцией активации `sigmoid`.
Выходной слой остаётся таким же — 10 нейронов с активацией `softmax`.

Функция `softmax` преобразует выходные значения нейронов в вероятности:
сумма всех выходов равна 1, а каждое значение показывает уверенность модели в том,
что изображение принадлежит соответствующему классу.
Наибольшее значение определяет предсказанный класс.

```python
from keras.models import Sequential
from keras.layers import Dense

hidden2 = [50, 100]
results_2 = {}

for n2 in hidden2:
    print(f'\n=== Модель со вторым скрытым слоем {n2} нейронов ===')
    
    # создание модели
    model2 = Sequential()
    model2.add(Dense(units=100, input_dim=784, activation='sigmoid'))  # первый скрытый слой
    model2.add(Dense(units=n2, activation='sigmoid'))                   # второй скрытый слой
    model2.add(Dense(units=num_classes, activation='softmax'))          # выходной слой
    
    # компиляция модели
    model2.compile(loss='categorical_crossentropy',
                   optimizer='sgd',
                   metrics=['accuracy'])
    
    # обучение модели
    H2 = model2.fit(X_train, y_train,
                    validation_split=0.1,
                    epochs=50,
                    verbose=1)
    
    # оценка на тестовых данных
    scores = model2.evaluate(X_test, y_test, verbose=1)
    results_2[n2] = scores[1]
    
    print(f'Accuracy on test data: {scores[1]:.4f}')

=== Модель со вторым скрытым слоем 50 нейронов ===
Accuracy on test data: 0.9443
=== Модель со вторым скрытым слоем 100 нейронов ===
Accuracy on test data: 0.9445
```

Добавление второго скрытого слоя не привело к заметному улучшению качества классификации — точность осталась примерно на уровне 94,4 %, как и у модели с одним скрытым слоем.

Это объясняется тем, что задача распознавания цифр из набора MNIST относительно простая, и уже один скрытый слой с 100 нейронами способен выделять необходимые закономерности.
Добавление второго слоя увеличивает количество параметров, но не добавляет новой информации, которую сеть могла бы использовать.
Кроме того, использование функции активации sigmoid ограничивает способность сети обучать глубокие слои из-за эффекта затухающего градиента, поэтому более сложная архитектура не даёт заметного прироста точности.

### *10. Результаты исследования архитектуры нейронной сети занести в таблицу:*

|  №  | Количество скрытых слоёв | Нейронов в 1-м скрытом слое | Нейронов во 2-м скрытом слое | Accuracy на тестовых данных |
| :-: | :----------------------: | :-------------------------: | :--------------------------: | :-------------------------: |
|  1  |             0            |              –              |               –              |            0.923            |
|  2  |             1            |             100             |               –              |          **0.9445**         |
|  3  |             1            |             300             |               –              |            0.9347           |
|  4  |             1            |             500             |               –              |            0.9310           |
|  5  |             2            |             100             |              50              |            0.9443           |
|  6  |             2            |             100             |              100             |            0.9445           |

Вывод:
С ростом числа нейронов и слоёв качество классификации не всегда улучшается.
Наилучший результат (accuracy ≈ 0.9445) показали модели: с одним скрытым слоем из 100 нейронов, и с двумя скрытыми слоями (100 + 100 нейронов).

Датасет MNIST относительно простой — цифры хорошо различимы, и даже простая однослойная модель способна выделить нужные закономерности. Более сложная сеть не получает новой информации, которую можно было бы использовать для улучшения качества. К тому же, увеличение числа нейронов и слоёв повышает риск переобучения. Модель начинает «запоминать» обучающие данные, теряя способность обобщать новые примеры. Это приводит к снижению точности на тестовой выборке. Использование функции активации `sigmoid` в скрытых слоях также ограничивает обучение глубоких моделей. При большом числе слоёв градиенты становятся очень малыми (эффект `vanishing gradient`), и обучение перестаёт быть эффективным.

### *11. Сохранить  наилучшую  нейронную  сеть  на  диск.  Данную  нейронную сеть потребуется загрузить с диска в одной из следующих лабораторных работ.*

```python
model2.save('/content/drive/MyDrive/Colab Notebooks/best_model_2x100.h5')

WARNING:absl:You are saving your model as an HDF5 file via `model.save()` or `keras.saving.save_model(model)`. This file format is considered legacy. We recommend using instead the native Keras format, e.g. `model.save('my_model.keras')` or `keras.saving.save_model(model, 'my_model.keras')`.
``` 

### *12. Для нейронной сети наилучшей архитектуры вывести два тестовых изображения, истинные метки и результат распознавания изображений.*

Для визуальной проверки работы обученной модели выбраны два случайных изображения из тестовой выборки. Модель предсказывает класс (цифру), который с наибольшей вероятностью соответствует изображению. Сравним предсказанные значения с истинными метками.

```python
import numpy as np
import matplotlib.pyplot as plt

# выбираем индексы двух случайных изображений
indices = np.random.choice(range(X_test.shape[0]), 2, replace=False)

# получаем предсказания
predictions = model2.predict(X_test[indices])
predicted_labels = np.argmax(predictions, axis=1)
true_labels = np.argmax(y_test[indices], axis=1)

# вывод изображений и меток
plt.figure(figsize=(6, 3))
for i, idx in enumerate(indices):
    plt.subplot(1, 2, i + 1)
    plt.imshow(X_test[idx].reshape(28, 28), cmap='gray')
    plt.title(f'True: {true_labels[i]}, Pred: {predicted_labels[i]}')
    plt.axis('off')
plt.show()
![alt text](image-5.png)
``` 
`np.argmax()` извлекает индекс максимального значения в векторе вероятностей (то есть номер класса). `model2.predict()` возвращает вероятность принадлежности изображения к каждому из 10 классов. Для наглядности в заголовках указаны истинная метка (True) и предсказанная моделью (Pred).

### *13. Каждому члену бригады создать собственное изображение рукописной цифры, подобное представленным в наборе MNIST. Цифру выбрать как остаток от деления на 10 числа своего дня рождения (например, 29 февраля → 29 mod10 = 9). Сохранить изображения. Загрузить, предобработать и подать  на  вход  обученной  нейронной  сети  собственные  изображения. Вывести изображения и результаты распознавания.*

Членам бригады соответствуют цифры 5 и 6. Они были написаны от руки, отсканированы и преобразованы в черно-белый формат размером по 28 х 28 пикселей.

Загрузка файлов:

```python
from google.colab import files
uploaded = files.upload()
```

Предварительная подготовка аналогично началу работы:

```python
from tensorflow.keras.preprocessing import image
import numpy as np
import matplotlib.pyplot as plt

file_names = list(uploaded.keys())

X_custom = []

for fname in file_names:
    # загружаем изображение в оттенках серого и приводим к 28×28
    img = image.load_img(fname, color_mode='grayscale', target_size=(28, 28))
    img_array = image.img_to_array(img)

    # инвертируем цвета, если фон белый (MNIST — белая цифра на чёрном фоне)
    img_array = 255 - img_array  

    # нормализация
    img_array = img_array / 255.0

    # разворачиваем в вектор длиной 784
    img_flat = img_array.reshape(1, 784)

    X_custom.append(img_flat)

X_custom = 1 - X_custom
predictions = model2.predict(X_custom)
print(np.argmax(predictions, axis=1))

[5 6]
```
`image.load_img`(..., `color_mode='grayscale'`) — загружает изображение в градациях серого.

`img_array / 255.0` — нормирует значения пикселей в диапазон [0, 1].

`X_custom = 1 - X_custom` — выполняет инвертирование яркости, чтобы фон стал тёмным, а цифра светлой — как в наборе MNIST. Без этой операции модель ошибалась, воспринимая цифру как фон.

`np.argmax(predictions, axis=1)` — определяет итоговые распознанные цифры.

### *14. Каждому члену бригады создать копию собственного изображения, отличающуюся от оригинала поворотом на 90 градусов в любую сторону. Сохранить изображения. Загрузить, предобработать и подать на вход обученной нейронной сети изменённые изображения. Вывести изображения и результаты распознавания.*

Для проверки устойчивости модели к поворотам изображений были созданы версии исходных цифр, повёрнутые на 90°. Далее выполняется их загрузка, нормализация, инверсия и распознавание обученной моделью.

```python
from google.colab import files
uploaded = files.upload()
Число файлов: 2
6 (1).png(image/png) - 6089 bytes, last modified: 15.10.2025 - 100% done
5 (1).png(image/png) - 6167 bytes, last modified: 15.10.2025 - 100% done
Saving 6 (1).png to 6 (1) (1).png
Saving 5 (1).png to 5 (1).png
```

```python
from tensorflow.keras.preprocessing import image
import numpy as np
import matplotlib.pyplot as plt

file_names = list(uploaded.keys())
X_rotated = []

for fname in file_names:
    # загружаем повернутое изображение и приводим к формату 28×28
    img = image.load_img(fname, color_mode='grayscale', target_size=(28, 28))
    img_array = image.img_to_array(img) / 255.0   # нормализация
    
    # разворачиваем в вектор длиной 784
    img_flat = img_array.reshape(1, 784)
    X_rotated.append(img_flat)

X_rotated = np.vstack(X_rotated)

# инвертируем цвета (цифры — светлые, фон — тёмный)
X_rotated = 1 - X_rotated

# предсказания модели
predictions = model2.predict(X_rotated)
predicted_labels = np.argmax(predictions, axis=1)

# вывод изображений и предсказаний
plt.figure(figsize=(6, 3))
for i, fname in enumerate(file_names):
    img = image.load_img(fname, color_mode='grayscale', target_size=(28, 28))
    plt.subplot(1, len(file_names), i + 1)
    plt.imshow(img, cmap='gray')
    plt.title(f'Pred: {predicted_labels[i]}')
    plt.axis('off')
plt.show()

print('Распознанные цифры:', predicted_labels)

Распознанные цифры: [3 3]
```

```python
np.set_printoptions(precision=4, suppress=True)
[[0.0003 0.     0.1308 0.8647 0.     0.0037 0.     0.     0.0006 0.    ]
 [0.0012 0.0001 0.0845 0.8197 0.     0.0825 0.     0.     0.012  0.    ]]
 ```

После поворота изображений на 90° нейронная сеть неверно классифицировала обе цифры, определив их как «3». Это показывает, что модель чувствительна к изменению ориентации изображений и не обладает устойчивостью к поворотам.

---

В ходе работы была реализована и исследована однослойная и многослойная нейронные сети для классификации изображений из набора MNIST.
Оптимальной оказалась модель с одним скрытым слоем на 100 нейронов, обеспечившая точность около 94 %.
Проведённый анализ показал, что увеличение числа нейронов или слоёв не всегда улучшает результат, а устойчивость к поворотам и искажениям не появляется по умолчанию и требует дополнительных методов обучения.