Кто не задавался вопросом, что на самом деле означают рецензии на фильмы на IMDb, скрывающиеся за поверхностным текстом? Я имею в виду, что фраза «этот фильм был нормальным» может означать всё что угодно: от «я бы посмотрел его снова завтра» до «я бы предпочёл пришить себе веки». Сегодня мы построим систему анализа настроений, которая развеет двусмысленность, словно горячий нож масло, используя BERT и TensorFlow. Оставайтесь со мной, и к концу этой статьи у вас будет модель, которая распознаёт сарказм лучше, чем ваш бывший.

Почему BERT? Потому что язык сложен (и интересен)

Прежде чем мы углубимся в код, давайте ответим на очевидный вопрос: зачем использовать BERT для анализа настроений, если существуют более простые модели? Потому что язык полон восхитительных противоречий. Вот пример: «Еда в этом ресторане была настолько плохой, что это почти хорошо». Простая модель сочла бы это негативным отзывом. BERT же понимает, что ключевое значение здесь имеет слово «почти».

BERT (Bidirectional Encoder Representations from Transformers) обрабатывает текст двунаправлено, улавливая контекст с обеих сторон каждого слова. Это похоже на разговор, в котором вы на самом деле слушаете, прежде чем ответить — редко встречается в политике, но необходимо в NLP.

Настройка нашей цифровой мастерской

Давайте займёмся делом. Сначала скучная, но необходимая настройка:

# Устанавливаем необходимое — думаем об этом как о подготовке кухни перед готовкой
!pip install tensorflow-text transformers datasets tensorflow-hub
!pip install -q tf-models-official
import os
import numpy as np
import tensorflow as tf
import tensorflow_hub as hub
import tensorflow_text as text
from official.nlp import optimization
import matplotlib.pyplot as plt

Чувствуете волнение? Это как сбор инструментов супергероя перед битвой. Только наша битва — с неоднозначными настроениями, а наше оружие — ещё больше кода.

Дилемма данных: поиск нашей площадки для анализа настроений

Для этого приключения мы будем использовать классический набор данных с рецензиями на фильмы IMDb — 75 000 безжалостно честных мнений о фильмах. Некоторые из них красноречивы, некоторые — эмоциональные тирады, все идеально подходят для обучения нашего детектора настроений.

# Загружаем набор данных IMDb — 50k для обучения, 25k для тестирования
dataset = tf.keras.utils.get_file(
    fname="aclImdb.tar.gz", 
    origin="http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz", 
    extract=True,
    cache_dir='.',
    cache_subdir=''
)
# Обрабатываем набор данных в управляемые блоки
def get_sentiment_data(subset):
    texts, labels = [], []
    for label in [0, 1]:  # 0 = отрицательный, 1 = положительный
        label_dir = os.path.join(os.path.dirname(dataset), f'aclImdb/{subset}/{label}')
        for fname in os.listdir(label_dir):
            if fname.endswith('.txt'):
                with open(os.path.join(label_dir, fname), encoding='utf-8') as f:
                    texts.append(f.read())
                labels.append(label)
    return np.array(texts), np.array(labels)
train_texts, train_labels = get_sentiment_data('train')
test_texts, test_labels = get_sentiment_data('test')
# Перемешиваем данные обучения, потому что случайность — это приправа к жизни модели
indices = np.arange(len(train_texts))
np.random.shuffle(indices)
train_texts = train_texts[indices]
train_labels = train_labels[indices]

Теперь у нас есть сырьё. Помните: что в, то и наружу. Но поскольку рецензии IMDb известны своей эмоциональностью (и иногда безумием), мы в надёжных руках.

Выбор нашего BERT: не все герои носят плащи

TensorFlow Hub предлагает несколько вариантов BERT. Для анализа настроений нам нужен что-то сбалансированное между производительностью и эффективностью. Мне нравится small_bert/bert_en_uncased_L-4_H-512_A-8 — это как компактный спортивный автомобиль среди моделей BERT: не самый большой, но достаточно быстрый для наших нужд.

Эта схема архитектуры показывает, как BERT вписывается в наш конвейер анализа настроений:

graph LR A[Необработанный текст] --> B[Предварительная обработка] B --> C[Кодировщик BERT] C --> D[Уровень объединения] D --> E[Классификационная головка] E --> F[Оценка настроения] style A fill:#f9f,stroke:#333,stroke-width:2px style F fill:#bbf,stroke:#333,stroke-width:2px

Магия предварительной обработки: делаем текст пригодным для BERT

BERT не ест сырой текст на завтрак — ему нужны тщательно приготовленные токенизированные сэндвичи. Здесь многие руководства опускают детали, оставляя вас в недоумении, когда ваша модель выдаёт бессмысленный результат. Давайте сделаем это правильно.

# Загружаем предварительно обработанную модель BERT из TensorFlow Hub
preprocessor = hub.load("https://tfhub.dev/tensorflow/bert_en_uncased_preprocess/3")
# Создаём функцию для предварительной обработки нашего текста
def preprocess_texts(texts):
    return preprocessor(tf.constant(texts))
# Тестируем на примере
sample_text = ['Этот фильм был абсолютно фантастическим!']
preprocessed = preprocess_texts(sample_text)
print("Форма идентификаторов слов:", preprocessed['input_word_ids'].shape)
print("Форма маски:", preprocessed['input_mask'].shape)
print("Форма идентификаторов типа:", preprocessed['input_type_ids'].shape)

Вы увидите вывод типа:

Форма идентификаторов слов: (1, 128)
Форма маски: (1, 128)
Форма идентификаторов типа: (1, 128)

Ах, какое прекрасное зрелище — правильно сформированные тензоры! Давайте поговорим о том, что они означают:

  • input_word_ids: Токенизированный текст, преобразованный в числа (из словаря BERT)
  • input_mask: 1 там, где у нас есть реальные токены, 0 для заполнения (BERT игнорирует их)
  • input_type_ids: Для различения между двумя предложениями (0 для первого, 1 для второго)

BERT ожидает именно этот набор входных данных — как тщательно приготовленное трёхкурсное блюдо.

Создание нашей модели: сердце операции

Теперь финальная часть: сборка нашей машины для анализа настроений. Здесь BERT встречается с нашей скромной классификационной головкой.

# Загружаем фактическую модель BERT
bert_model = hub.load("https://tfhub.dev/tensorflow/small_bert/bert_en_uncased_L-4_H-512_A-8/1")
# Строим архитектуру нашей модели — простая, но мощная
def build_sentiment_model():
    text_input = tf.keras.layers.Input(shape=(), dtype=tf.string, name='text')
    # Сначала предварительно обрабатываем текст с помощью препроцессора BERT
    preprocessed_text = preprocessor(text_input)
    # Затем отправляем его через BERT
    outputs = bert_model(preprocessed_text)
    # "pooled_output" — это представление всей последовательности
    # Мы будем использовать это для классификации
    pooled_output = outputs["pooled_output"]
    # Добавляем слой дропаута для предотвращения переобучения (ахиллесова пята BERT)
    dropout = tf.keras.layers.Dropout(0.1)(pooled_output)
    # Финальный классификационный слой — простой, но эффективный
    outputs = tf.keras.layers.Dense(1, activation='sigmoid')(dropout)
    # Создаём и возвращаем модель
    model = tf.keras.Model(inputs=text_input, outputs=outputs)
    return model
sentiment_model = build_sentiment_model()
# Компилируем с такой скоростью обучения, которая не вызовет у BERT экзистенциальный кризис
loss = tf.keras.losses.BinaryCrossentropy()
metrics = tf.metrics.BinaryAccuracy()
# Настраиваем оптимизатор с расписанием скорости обучения
steps_per_epoch = len(train_labels) // 32
num_train_steps = steps_per_epoch * 3
num_warmup_steps = int(0.1 * num_train_steps)
init_lr = 3e-5
optimizer = optimization.create_optimizer(
    init_lr=init_lr,
    num_train_steps=num_train_steps,