## Лабораторная работа №4 ИС. Распознавание последовательностей.

#### Выполнили: Ишутина Е. И., Голубев Т. Л.

Проведена работа с отзывами с IMDb в виде последовательностей индексов и в человекочитаемом виде. Выполнена предобработка: все тексты приведены к единой длине путём дополнения / усечения, в результате чего получены матрицы одинаковой размерности, пригодные для подачи в нейронную сеть. 

На основании предобработанных данных была построена рекуррентная модель, включавшая слои Embedding, LSTM, Dropout и полносвязный классификатор, и проведено её обучение с использованием доли обучающей выборки в качестве валидационной и проверка точности работы модели.

Перед началом работы импортированы все нужные модули:

```python
# Блок импортов и общих настроек
import os
import random
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.datasets import imdb
from tensorflow.keras.preprocessing.sequence import pad_sequences
from sklearn.metrics import classification_report, roc_curve, auc, roc_auc_score

print("TensorFlow version:", tf.__version__)
```
* `layers` предоставляет готовые типы слоёв (Embedding, LSTM, Dense и др.).
* `imdb` загружает предобработанный набор IMDb в виде последовательностей индексов.
* `pad_sequences` приводит последовательности к фиксированной длине путём дополнения или усечения.
* `sklearn.metrics` функции для оценки качества классификации (отчёт, ROC, AUC).

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

```python
# 1) Проверка наличия GPU (в Colab после выбора Runtime -> Change runtime type -> GPU)
device_name = tf.test.gpu_device_name()
if device_name != '/device:GPU:0':
    raise SystemError('GPU device not found')
print('Found GPU at: {}'.format(device_name))

```
Вывод дает понять, что подключен аппаратный ускоритель на основе графического процессора (GPU):

```python
Found GPU at: /device:GPU:0
```
#### *2. Загрузить набор данных IMDb, содержащий оцифрованные  отзывы на фильмы, размеченные на два класса: позитивные и негативные. При загрузке набора данных параметр seed выбрать равным (4k–1), где k – номер бригады. Вывести размеры полученных обучающих и тестовых массивов данных.*

```python
k = 5
seed = 4 * k - 1

vocabulary_size = 5000
index_from = 3

(X_train, y_train), (X_test, y_test) = imdb.load_data(
    path="imdb.npz",
    num_words=vocabulary_size,
    skip_top=0,
    maxlen=None,
    seed=seed,
    start_char=1,
    oov_char=2,
    index_from=index_from
)

print("Размеры: X_train={}, y_train={}, X_test={}, y_test={}".format(
    X_train.shape, y_train.shape, X_test.shape, y_test.shape))

```
Функция загрузила по 25 000 примеров в обучающую и тестовую выборки, что и было выведено как формы массивов, потому что набор IMDb в keras был заранее организован в таких размерах. В выводе отображались одномерные массивы, поэтому после запятой не было указано никакой второй размерности.

```python
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/imdb.npz
17464789/17464789 ━━━━━━━━━━━━━━━━━━━━ 0s 0us/step
Размеры: X_train=(25000,), y_train=(25000,), X_test=(25000,), y_test=(25000,)
```

#### *3. Вывести один отзыв из обучающего множества в виде списка индексов слов. Преобразовать список индексов в текст и вывести отзыв в виде текста. Вывести длину отзыва. Вывести метку класса данного отзыва и название класса (1 – Positive, 0 – Negative).*

```python
# словарь id->word
word_to_id = imdb.get_word_index()
word_to_id = {key: (value + index_from) for key, value in word_to_id.items()}
word_to_id["<PAD>"] = 0
word_to_id["<START>"] = 1
word_to_id["<UNK>"] = 2
word_to_id["<UNUSED>"] = 3
id_to_word = {value: key for key, value in word_to_id.items()}

some_index = 0
review_indices = X_train[some_index]
print("Отзыв (список индексов):", review_indices)
review_text = ' '.join(id_to_word.get(i, "<UNK>") for i in review_indices)
print("\nОтзыв (текст):\n", review_text)
print("\nДлина отзыва (число индексов):", len(review_indices))
print("Меткa класса (y):", y_train[some_index],
      "- название класса:", ("Positive" if y_train[some_index] == 1 else "Negative"))
```
Преобразование индексов в текст выполнялось через обратный словарь `id_to_word`, сформированный из `word_to_id` с учётом сдвига `index_from` и добавления служебных токенов. Для каждого индекса находилось соответствующее слово, а при отсутствии — подставлялся <UNK>. Полученные токены объединялись в строку, что давало текстовое отображение отзыва.

Токен — это минимальная единица текста, с которой работает модель: слово, символ, подслово или специальный маркер. В датасете IMDb токеном является слово, которому присвоен числовой индекс.

Числа в тензоре — это индексы токенов, то есть номера слов в словаре IMDb.
Каждое число соответствует одному слову или служебному токену:

* 1 — START
* 2 — UNK
* 0 — PAD

остальные числа — реальные слова корпуса, закодированные целыми числами.

```python
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/imdb_word_index.json
1641221/1641221 ━━━━━━━━━━━━━━━━━━━━ 0s 0us/step
Отзыв (список индексов): [1, 48, 25, 181, 8, 106, 6, 147, 2, 20, 79, 1070, 7, 4, 2, 2063, 14, 2, ... 989, 143, 8, 4, 2, 15, 1838, 2, 10, 10, 13, 447, 175, 786, 7, 12]
```
```
Отзыв (текст):
 <START> if you want to watch a real <UNK> movie get hold of the <UNK> formula this <UNK> film must have cost all of 50 to make it features a <UNK> thin script <UNK> bad sets lighting and camera work and a stop motion paper <UNK> monster that is utterly laughable it looks like they sometimes used a guy in a rubber suit and or a <UNK> <UNK> for the monster but all were equally dreadful br br the actors all speak their lines as though theyve never seen them before and are reading off a <UNK> the special effects are way beyond lousy and the only sad thing is that they dropped the really <UNK> original title <UNK> <UNK> which <UNK> up exactly what you get for the full 90 minutes br br this is what happens when you <UNK> the bottom of the <UNK> so hard you break through to the <UNK> that lies <UNK> br br i loved every minute of it


Длина отзыва (число индексов): 165
Меткa класса (y): 0 - название класса: Negative
```

#### *4. Вывести максимальную и минимальную длину отзыва в обучающем множестве*

```python
max_len = len(max(X_train, key=len))
min_len = len(min(X_train, key=len))
print("Максимальная длина отзыва (в индексах):", max_len)
print("Минимальная длина отзыва (в индексах):", min_len)
```
Видно,что отзывы сильно различаются по объёму, и работать с ними в исходном виде невозможно. Нейронная сеть принимает тензоры фиксированной формы. Без выравнивания обучение невозможно, т.к. батч не может содержать последовательности разной длины.

```python
Максимальная длина отзыва (в индексах): 2494
Минимальная длина отзыва (в индексах): 11
```
#### *5. Провести предобработку данных. Выбрать единую длину, к которой будут приведены все отзывы. Короткие отзывы дополнить спецсимволами, а длинные обрезать до выбранной длины.*

```python
max_words = 500
X_train_prep = pad_sequences(X_train, maxlen=max_words, value=0, padding='pre', truncating='post')
X_test_prep  = pad_sequences(X_test,  maxlen=max_words, value=0, padding='pre', truncating='post')
print("Форма X_train_prep:", X_train_prep.shape)
print("Форма X_test_prep:", X_test_prep.shape)
```
Все отзывы приведены к фиксированной длине 500. Это значение было выбрано как компромисс: оно достаточно большое, чтобы сохранить основное содержание средних и длинных отзывов, и достаточно маленькое, чтобы не перегружать память и ускорить обучение. При приведении длины короткие отзывы были дополнены токеном <PAD>, а слишком длинные — обрезаны справа.

```python
Форма X_train_prep: (25000, 500)
Форма X_test_prep: (25000, 500)
```

#### *6. Повторить п. 4.*

```python
print("После предобработки: длина", X_train_prep.shape[1])
```

```python
После предобработки: длина 500
```


#### *7. Повторить п. 3. Сделать вывод о том, как отзыв преобразовался после предобработки.*

```python
prep_review_indices = X_train_prep[some_index]
print("Предобработанный отзыв (индексы):", prep_review_indices)
prep_review_text = ' '.join(id_to_word.get(i, "<PAD>") for i in prep_review_indices if i != 0)
print("\nПредобработанный отзыв (текст, без <PAD>):\n", prep_review_text)
print("\nДлина предобработанного отзыва:", len(prep_review_indices))
```

В исходном отзыве оказалось 165 токенов, что меньше выбранной фиксированной длины 500, поэтому при предобработке произошло только дополнение слева токеном PAD, а обрезания не было. 

```
Предобработанный отзыв (индексы): [   0   0 ...  175  786    7   12]

Предобработанный отзыв (текст, без <PAD>):
 <START> if you want to watch a real <UNK> movie get hold of the <UNK> formula this <UNK> film must have cost all of 50 to make it features a <UNK> thin script <UNK> bad sets lighting and camera work and a stop motion paper <UNK> monster that is utterly laughable it looks like they sometimes used a guy in a rubber suit and or a <UNK> <UNK> for the monster but all were equally dreadful br br the actors all speak their lines as though they've never seen them before and are reading off a <UNK> the special effects are way beyond lousy and the only sad thing is that they dropped the really <UNK> original title <UNK> <UNK> which <UNK> up exactly what you get for the full 90 minutes br br this is what happens when you <UNK> the bottom of the <UNK> so hard you break through to the <UNK> that lies <UNK> br br i loved every minute of it

Длина предобработанного отзыва: 500
```
Чтобы убедиться в корректности преобразования, найден и выведен отзыв с длиной больше 500, к которому применено обрезание (код для поиска опущен, длинные тензоры укорочены).

```
Исходный отзыв (первые 50 индексов):
[1, 2, 34, 1308, ... 532, 2, 18]
Исходный отзыв (последние 50 индексов):
[4, 323, 198, 89, ... 495]

После предобработки (первые 50 индексов):
[   1    2   34 1308 ...  532    2   18]
После предобработки (последние 50 индексов):
[   2  501  ... 10   32]
```
Как видно, последние индексы теперь выглядят по-другому, то есть произошло обрезание. При выводе появляются пробелы, связанные с тем, что Python пытается выровнять столбцы у `tf.Tensor`.

#### *8. Вывести предобработанные массивы обучающих и тестовых данных и их размерности.*


```python
print("X_train_prep shape:", X_train_prep.shape)
print("X_test_prep shape: ", X_test_prep.shape)
print("y_train shape:", y_train.shape)
print("y_test shape: ", y_test.shape)

for i in range(3):
    print(f"\nПример {i} (индексы):", X_train_prep[i][:300], "...")
    print(f"Метка:", y_train[i])
```
Слишком длинные последовательности индексов были укорочены.

```python
X_train_prep shape: (25000, 500)
X_test_prep shape:  (25000, 500)
y_train shape: (25000,)
y_test shape:  (25000,)

Пример 0 (индексы): [0 0 0 ... 0 0 0 0]
Метка: 0

Пример 1 (индексы): [   0    0    0    ...    11  585    2   11  940   11   4    2  720   11    4 4691]
Метка: 1

Пример 2 (индексы): [   0    0  ... 406  1522  29  186    8  412   27  113]
Метка: 1
```


#### *9. Реализовать модель рекуррентной нейронной сети, состоящей из слоев Embedding, LSTM, Dropout, Dense, и обучить ее на обучающих данных с выделением части обучающих данных в качестве валидационных. Вывести информацию об архитектуре нейронной сети. Добиться качества обучения по метрике accuracy не менее 0.8.*

В этом пункте была реализована рекуррентная нейронная сеть для задачи бинарной классификации.

* Embedding преобразовал числовые индексы слов в плотные векторные представления размерности embedding_dim=32. Благодаря этому слова, имеющие близкий контекст, получали похожие векторы, что облегчало обучение модели.

* LSTM (Long Short-Term Memory) — рекуррентный слой с 64 единицами, он учитывал последовательный порядок слов, позволял модели «помнить» информацию из начала текста при обработке его конца.

* Dropout — слой регуляризации с вероятностью 0.3, случайным образом отключавший часть нейронов во время обучения.

* Dense — полносвязный слой с одним нейроном и сигмоидной активацией, который выдавал вероятность положительного класса (1 — Positive, 0 — Negative).

Обучение проводилось с использованием 80% данных для тренировки и 20% для валидации (validation_split=0.2).

```python
vocab_size = vocabulary_size
embedding_dim = 32
input_length = max_words

model = keras.Sequential([
    layers.Embedding(input_dim=vocab_size, output_dim=embedding_dim, input_length=input_length),
    layers.LSTM(64),
    layers.Dropout(0.3),
    layers.Dense(1, activation='sigmoid')
])

model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
model.summary()

# Обучение с выделением валидации
history = model.fit(
    X_train_prep, y_train,
    epochs=4,
    batch_size=64,
    validation_split=0.2,
    verbose=1
)
```

#### *10. Оценить качество обучения на тестовых данных: вывести значение метрики качества классификации на тестовых данных, вывести отчет о качестве классификации тестовой выборки, построить ROC-кривую по результату обработки тестовой выборки и вычислить площадь под ROC-кривой (AUC ROC).*


```python
eval_results = model.evaluate(X_test_prep, y_test, verbose=1)
print("Результаты оценки на тесте (loss, accuracy):", eval_results)

# Получение "сырых" предсказаний и бинарных меток
y_score = model.predict(X_test_prep)
y_pred = [1 if y_score[i,0] >= 0.5 else 0 for i in range(len(y_score))]

print("\nClassification report:\n")
print(classification_report(y_test, y_pred, labels=[0,1], target_names=['Negative','Positive']))

# ROC-кривая и AUC
fpr, tpr, thresholds = roc_curve(y_test, y_score)
roc_auc = auc(fpr, tpr)
print("\nAUC ROC (ручной вычисление):", roc_auc)
print("AUC ROC (sklearn):", roc_auc_score(y_test, y_score))

import matplotlib.pyplot as plt
plt.figure()
plt.plot(fpr, tpr)
plt.grid()
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC')
plt.show()
```

```python
782/782 ━━━━━━━━━━━━━━━━━━━━ 8s 10ms/step - accuracy: 0.8603 - loss: 0.3458
Результаты оценки на тесте (loss, accuracy): [0.34332218766212463, 0.8626000285148621]
782/782 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step

Classification report:

              precision    recall  f1-score   support

    Negative       0.83      0.92      0.87     12500
    Positive       0.91      0.81      0.85     12500

    accuracy                           0.86     25000
   macro avg       0.87      0.86      0.86     25000
weighted avg       0.87      0.86      0.86     25000


AUC ROC (ручной вычисление): 0.9381251584
AUC ROC (sklearn): 0.9381251584
```
![image](pics/i1.png)

Идеальное значение площади под графиком — единица (100% угадывание положительных классов), а худший случай — 0,5 («вслепую»). Значение 0,93 показывает хорошую точность предсказаний.

#### *11. Сделать выводы по результатам применения рекуррентной нейронной сети для решения задачи определения тональности текста.* 

В ходе лабораторной работы была построена рекуррентная нейронная сеть с LSTM для задачи бинарной классификации отзывов IMDb. Модель показала высокие показатели качества: точность на тестовой выборке достигла ≈86%, а площадь под ROC-кривой (AUC ROC ≈0.93) подтверждает способность модели надёжно различать положительные и отрицательные отзывы.