Рубрики
Без рубрики

Классификация текста с помощью BERT Tokenizer и TF 2.0 в Python

BERT-это техника представления текста, похожая на встраивание слов. В этой статье мы будем использовать BERT и TensorFlow 2.0 для классификации текста.

Автор оригинала: Usman Malik.

Это 23-я статья в моей серии статей по Python для НЛП. В предыдущей статье этой серии я объяснил, как выполнять нейронный машинный перевод с использованием архитектуры seq2seq с помощью библиотеки Keras Python для глубокого обучения.

В этой статье мы изучим BART , который расшифровывается как Двунаправленные кодирующие представления трансформаторов и его применение к классификации текста. BERT-это техника представления текста, такая как встраивание слов. Если вы не имеете ни малейшего представления о том, как работают встраивания слов, взгляните на мою статью о встраиваниях слов .

Как и встраивание слов, BERT также является методом представления текста, который представляет собой сплав различных современных алгоритмов глубокого обучения, таких как двунаправленный кодер LSTM и трансформаторы. BERT был разработан исследователями Google в 2018 году и зарекомендовал себя как современный инструмент для решения различных задач обработки естественного языка, таких как классификация текста, суммирование текста, генерация текста и т. Д. Совсем недавно Google объявила, что BERT используется в качестве основной части их поискового алгоритма для лучшего понимания запросов.

В этой статье мы не будем вдаваться в математические подробности того, как реализуется BERT, так как в Интернете уже есть множество ресурсов. Скорее мы увидим, как выполнить классификацию текста с помощью токенизатора BERT. В этой статье вы увидите, как токенизатор BERT может быть использован для создания модели классификации текста. В следующей статье я объясню, как токенизатор BERT, наряду со слоем встраивания BERT, может быть использован для создания еще более эффективных моделей НЛП.

Примечание : Все скрипты в этой статье были протестированы с использованием среды Google Colab , а среда выполнения Python установлена на GPU.

Набор данных

Набор данных, используемый в этой статье, можно загрузить по ссылке this Kaggle link .

Если вы загрузите набор данных и распакуете сжатый файл, то увидите CSV-файл. Файл содержит 50 000 записей и две колонки: обзор и тональность. Столбец review содержит текст для обзора, а столбец sentiment содержит sentiment для обзора. Столбец sentiment может иметь два значения, то есть “положительный” и “отрицательный”, что делает нашу задачу проблемой бинарной классификации.

Ранее мы проводили сентиментальный анализ этого набора данных в предыдущей статье, где мы достигли максимальной точности 92% на обучающем наборе с помощью метода встраивания word a и сверточной нейронной сети. На тестовом наборе максимальная достигнутая точность составила 85,40% при использовании встраивания слов и одиночного LSTM со 128 узлами. Давайте посмотрим, сможем ли мы получить лучшую точность, используя представление БЕРТА.

Установка и импорт необходимых библиотек

Прежде чем вы сможете использовать ЛУЧШЕЕ текстовое представление, вам нужно установить BERT для TensorFlow 2.0. Выполните следующие команды pip на вашем терминале, чтобы установить BERT для TensorFlow 2.0.

!pip install bert-for-tf2
!pip install sentencepiece

Далее вам нужно убедиться, что вы используете TensorFlow 2.0. Google Colab по умолчанию не запускает ваш скрипт на TensorFlow 2.0. Поэтому, чтобы убедиться, что вы запускаете свой скрипт через TensorFlow 2.0, выполните следующий скрипт:

try:
    %tensorflow_version 2.x
except Exception:
    pass
import tensorflow as tf

import tensorflow_hub as hub

from tensorflow.keras import layers
import bert

В приведенном выше скрипте, в дополнение к TensorFlow 2.0, мы также импортируем tensorflow_hub, который в основном является местом, где вы можете найти все готовые и предварительно обученные модели, разработанные в TensorFlow. Мы будем импортировать и использовать встроенную модель BERT из TF hub. Наконец, если в выводе вы видите следующий вывод, то вам хорошо идти:

TensorFlow 2.x selected.

Импорт и предварительная обработка набора данных

Следующий скрипт импортирует набор данных с помощью метода read_csv() фрейма данных Pandas. Сценарий также печатает форму набора данных.

movie_reviews = pd.read_csv("/content/drive/My Drive/Colab Datasets/IMDB Dataset.csv")

movie_reviews.isnull().values.any()

movie_reviews.shape

Выход

(50000, 2)

Выходные данные показывают, что наш набор данных имеет 50 000 строк и 2 столбца.

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

def preprocess_text(sen):
    # Removing html tags
    sentence = remove_tags(sen)

    # Remove punctuations and numbers
    sentence = re.sub('[^a-zA-Z]', ' ', sentence)

    # Single character removal
    sentence = re.sub(r"\s+[a-zA-Z]\s+", ' ', sentence)

    # Removing multiple spaces
    sentence = re.sub(r'\s+', ' ', sentence)

    return sentence
TAG_RE = re.compile(r'<[^>]+>')

def remove_tags(text):
    return TAG_RE.sub('', text)

Следующий скрипт очищает все текстовые обзоры:

reviews = []
sentences = list(movie_reviews['review'])
for sen in sentences:
    reviews.append(preprocess_text(sen))

Наш набор данных содержит два столбца, что можно проверить из следующего сценария:

print(movie_reviews.columns.values)

Выход:

['review' 'sentiment']

Столбец review содержит текст, а столбец sentiment – настроения. Столбец sentiments содержит значения в виде текста. Следующий сценарий отображает уникальные значения в столбце sentiment :

movie_reviews.sentiment.unique()

Выход:

array(['positive', 'negative'], dtype=object)

Вы можете видеть, что столбец sentiment содержит два уникальных значения: positive и negative . Алгоритмы глубокого обучения работают с числами. Поскольку у нас есть только два уникальных значения на выходе, мы можем преобразовать их в 1 и 0. Следующий скрипт заменяет positive sentiment на 1 и негативные настроения со стороны 0 .

y = movie_reviews['sentiment']

y = np.array(list(map(lambda x: 1 if x=="positive" else 0, y)))

Теперь переменная reviews содержит текстовые отзывы, а переменная y содержит соответствующие метки. Давайте произвольно напечатаем отзыв.

print(reviews[10])

Выход:

Phil the Alien is one of those quirky films where the humour is based around the oddness of everything rather than actual punchlines At first it was very odd and pretty funny but as the movie progressed didn find the jokes or oddness funny anymore Its low budget film thats never problem in itself there were some pretty interesting characters but eventually just lost interest imagine this film would appeal to stoner who is currently partaking For something similar but better try Brother from another planet 

Это явно похоже на негативный отзыв. Давайте просто подтвердим это, напечатав соответствующее значение метки:

print(y[10])

Выход:

0

Вывод 0 подтверждает, что это отрицательный отзыв. Теперь мы предварительно обработали наши данные и теперь готовы создавать ЛУЧШИЕ представления из наших текстовых данных.

Создание токенизатора BERT

Чтобы использовать встраивания текста BERT в качестве входных данных для обучения модели классификации текста, нам нужно маркировать ваши текстовые обзоры. Токенизация означает разделение предложения на отдельные слова. Для токенизации нашего текста мы будем использовать токенизатор BERT. Посмотрите на следующий сценарий:

BertTokenizer = bert.bert_tokenization.FullTokenizer
bert_layer = hub.KerasLayer("https://tfhub.dev/tensorflow/bert_en_uncased_L-12_H-768_A-12/1",
                            trainable=False)
vocabulary_file = bert_layer.resolved_object.vocab_file.asset_path.numpy()
to_lower_case = bert_layer.resolved_object.do_lower_case.numpy()
tokenizer = BertTokenizer(vocabulary_file, to_lower_case)

В приведенном выше скрипте мы сначала создаем объект класса Full Tokenizer из модуля bert.bert_tokenization . Затем мы создаем ЛУЧШИЙ слой встраивания, импортируя ЛУЧШУЮ модель из hub.Слой Кераса . Параметр trainable имеет значение False , что означает, что мы не будем обучать встраивание BERT. В следующей строке мы создаем файл словаря BERT в виде массива numpy. Затем мы устанавливаем текст в нижний регистр и, наконец, передаем наши переменные vocabulary_file и to_lower_case объекту BertTokenizer .

Уместно отметить, что в этой статье мы будем использовать только токенизатор BERT. В следующей статье мы будем использовать встраивания BERT вместе с токенизатором.

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

tokenizer.tokenize("don't be so judgmental")

Выход:

['don', "'", 't', 'be', 'so', 'judgment', '##al']

Вы можете видеть, что текст был успешно маркирован. Вы также можете получить идентификаторы токенов с помощью функции convert_tokens_to_ids() объекта tokenizer. Посмотрите на следующий сценарий:

tokenizer.convert_tokens_to_ids(tokenizer.tokenize("dont be so judgmental"))

Выход:

[2123, 2102, 2022, 2061, 8689, 2389]

Теперь определим функцию, которая принимает один текстовый обзор и возвращает идентификаторы маркированных слов в обзоре. Выполните следующий сценарий:

def tokenize_reviews(text_reviews):
    return tokenizer.convert_tokens_to_ids(tokenizer.tokenize(text_reviews))

И выполните следующий сценарий, чтобы фактически маркировать все обзоры во входном наборе данных:

tokenized_reviews = [tokenize_reviews(review) for review in reviews]

Подготовка Данных Для Обучения

Отзывы в нашем наборе данных имеют различную длину. Некоторые отзывы очень маленькие, а другие очень длинные. Для обучения модели входные предложения должны быть одинаковой длины. Чтобы создать предложения одинаковой длины, один из способов состоит в том, чтобы дополнить более короткие предложения 0s. Однако это может привести к тому, что разреженная матрица будет содержать большое количество нулей. Другой способ-дополнить предложения внутри каждой партии. Поскольку мы будем обучать модель пакетами, мы можем дополнять предложения внутри обучающей партии локально в зависимости от длины самого длинного предложения. Для этого нам сначала нужно найти длину каждого предложения.

Следующий сценарий создает список списков, где каждый подсписок содержит маркированный обзор, метку обзора и длину обзора:

reviews_with_len = [[review, y[i], len(review)]
                 for i, review in enumerate(tokenized_reviews)]

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

random.shuffle(reviews_with_len)

Как только данные будут перетасованы, мы отсортируем их по длине отзывов. Для этого мы используем функцию sort() списка и сообщим ей, что хотим отсортировать список по третьему элементу в подсписке, то есть по длине обзора.

reviews_with_len.sort(key=lambda x: x[2])

После того, как отзывы отсортированы по длине, мы можем удалить атрибут длины из всех отзывов. Для этого выполните следующий сценарий:

sorted_reviews_labels = [(review_lab[0], review_lab[1]) for review_lab in reviews_with_len]

Как только обзоры будут отсортированы, мы преобразуем набор данных так, чтобы его можно было использовать для обучения моделей TensorFlow 2.0. Выполните следующий код для преобразования отсортированного набора данных в форму входного набора данных, совместимую с TensorFlow 2.0.

processed_dataset = tf.data.Dataset.from_generator(lambda: sorted_reviews_labels, output_types=(tf.int32, tf.int32))

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

BATCH_SIZE = 32
batched_dataset = processed_dataset.padded_batch(BATCH_SIZE, padded_shapes=((None, ), ()))

Давайте распечатаем первую партию и посмотрим, как к ней была применена прокладка:

next(iter(batched_dataset))

Выход:

(,
 )

Вышеприведенные выходные данные показывают первые пять и последние пять дополненных обзоров. Из последних пяти обзоров вы можете видеть, что общее количество слов в самом большом предложении составило 21. Поэтому в первых пяти обзорах 0 добавляются в конце предложений, так что их общая длина также равна 21. Заполнение для следующей партии будет отличаться в зависимости от размера самого большого предложения в партии.

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

TOTAL_BATCHES = math.ceil(len(sorted_reviews_labels) / BATCH_SIZE)
TEST_BATCHES = TOTAL_BATCHES // 10
batched_dataset.shuffle(TOTAL_BATCHES)
test_data = batched_dataset.take(TEST_BATCHES)
train_data = batched_dataset.skip(TEST_BATCHES)

В приведенном выше коде мы сначала находим общее количество пакетов, разделив общее количество записей на 32. Для этого мы используем метод take() объекта batched_dataset() для хранения 10% данных в переменной test_data . Остальные данные хранятся в объекте train_data для обучения с помощью метода skip () .

Набор данных был подготовлен, и теперь мы готовы создать нашу текстовую классификационную модель.

Создание модели

Теперь мы все готовы к созданию нашей модели. Для этого мы создадим класс с именем TEXT_MODEL , который наследуется от tf.keras.Модель класс. Внутри класса мы определим наши слои модели. Наша модель будет состоять из трех сверточных слоев нейронной сети. Вместо этого вы можете использовать слои LSTM, а также увеличивать или уменьшать количество слоев. Я скопировал количество и типы слоев из SuperDataScience Google colab notebook , и эта архитектура, похоже, довольно хорошо работает и для набора данных IMDB Movie reviews.

Давайте теперь создадим класс модели:

class TEXT_MODEL(tf.keras.Model):
    
    def __init__(self,
                 vocabulary_size,
                 embedding_dimensions=128,
                 cnn_filters=50,
                 dnn_units=512,
                 model_output_classes=2,
                 dropout_rate=0.1,
                 training=False,
                 name="text_model"):
        super(TEXT_MODEL, self).__init__(name=name)
        
        self.embedding = layers.Embedding(vocabulary_size,
                                          embedding_dimensions)
        self.cnn_layer1 = layers.Conv1D(filters=cnn_filters,
                                        kernel_size=2,
                                        padding="valid",
                                        activation="relu")
        self.cnn_layer2 = layers.Conv1D(filters=cnn_filters,
                                        kernel_size=3,
                                        padding="valid",
                                        activation="relu")
        self.cnn_layer3 = layers.Conv1D(filters=cnn_filters,
                                        kernel_size=4,
                                        padding="valid",
                                        activation="relu")
        self.pool = layers.GlobalMaxPool1D()
        
        self.dense_1 = layers.Dense(units=dnn_units, activation="relu")
        self.dropout = layers.Dropout(rate=dropout_rate)
        if model_output_classes == 2:
            self.last_dense = layers.Dense(units=1,
                                           activation="sigmoid")
        else:
            self.last_dense = layers.Dense(units=model_output_classes,
                                           activation="softmax")
    
    def call(self, inputs, training):
        l = self.embedding(inputs)
        l_1 = self.cnn_layer1(l) 
        l_1 = self.pool(l_1) 
        l_2 = self.cnn_layer2(l) 
        l_2 = self.pool(l_2)
        l_3 = self.cnn_layer3(l)
        l_3 = self.pool(l_3) 
        
        concatenated = tf.concat([l_1, l_2, l_3], axis=-1) # (batch_size, 3 * cnn_filters)
        concatenated = self.dense_1(concatenated)
        concatenated = self.dropout(concatenated, training)
        model_output = self.last_dense(concatenated)
        
        return model_output

Приведенный выше сценарий довольно прост. В конструкторе класса мы инициализируем некоторые атрибуты значениями по умолчанию. Эти значения будут заменены позже значениями, переданными при создании объекта класса TEXT_MODEL .

Далее были инициализированы три слоя сверточной нейронной сети со значениями ядра или фильтра 2, 3 и 4 соответственно. Опять же, вы можете изменить размеры фильтров, если хотите.

Затем внутри функции call() к выходу каждого слоя сверточной нейронной сети применяется глобальный максимальный пул. Наконец, три сверточных слоя нейронной сети объединяются вместе, и их выходные данные поступают в первую плотно связанную нейронную сеть. Вторая плотно связанная нейронная сеть используется для прогнозирования выходного настроения, так как она содержит только 2 класса. Если у вас есть больше классов в выходных данных, вы можете обновить переменную output_classes соответствующим образом.

Теперь определим значения гиперпараметров нашей модели.

VOCAB_LENGTH = len(tokenizer.vocab)
EMB_DIM = 200
CNN_FILTERS = 100
DNN_UNITS = 256
OUTPUT_CLASSES = 2

DROPOUT_RATE = 0.2

NB_EPOCHS = 5

Далее нам нужно создать объект класса TEXT_MODEL и передать значения гиперпараметров, которые мы определили на последнем шаге, конструктору класса TEXT_MODEL .

text_model = TEXT_MODEL(vocabulary_size=VOCAB_LENGTH,
                        embedding_dimensions=EMB_DIM,
                        cnn_filters=CNN_FILTERS,
                        dnn_units=DNN_UNITS,
                        model_output_classes=OUTPUT_CLASSES,
                        dropout_rate=DROPOUT_RATE)

Прежде чем мы действительно сможем обучить модель, нам нужно ее скомпилировать. Следующий сценарий компилирует модель:

if OUTPUT_CLASSES == 2:
    text_model.compile(loss="binary_crossentropy",
                       optimizer="adam",
                       metrics=["accuracy"])
else:
    text_model.compile(loss="sparse_categorical_crossentropy",
                       optimizer="adam",
                       metrics=["sparse_categorical_accuracy"])

Наконец, для обучения нашей модели мы можем использовать метод fit класса model.

text_model.fit(train_data, epochs=NB_EPOCHS)

Вот результат после 5 эпох:

Epoch 1/5
1407/1407 [==============================] - 381s 271ms/step - loss: 0.3037 - accuracy: 0.8661
Epoch 2/5
1407/1407 [==============================] - 381s 271ms/step - loss: 0.1341 - accuracy: 0.9521
Epoch 3/5
1407/1407 [==============================] - 383s 272ms/step - loss: 0.0732 - accuracy: 0.9742
Epoch 4/5
1407/1407 [==============================] - 381s 271ms/step - loss: 0.0376 - accuracy: 0.9865
Epoch 5/5
1407/1407 [==============================] - 383s 272ms/step - loss: 0.0193 - accuracy: 0.9931

Вы можете видеть, что мы получили точность 99,31% на тренировочном наборе.

Теперь давайте оценим производительность нашей модели на тестовом наборе:

results = text_model.evaluate(test_dataset)
print(results)

Выход:

156/Unknown - 4s 28ms/step - loss: 0.4428 - accuracy: 0.8926[0.442786190037926, 0.8926282]

Из выходных данных видно, что мы получили точность 89,26% на тестовом наборе.

Вывод

В этой статье вы видели, как мы можем использовать BERT Tokenizer для создания вложений слов, которые можно использовать для выполнения классификации текста. Мы провели сентиментальный анализ отзывов о фильмах IMDB и достигли точности 89,26% на тестовом наборе. В этой статье мы не использовали вложения BERT, мы использовали только BERT Tokenizer для токенизации слов. В следующей статье вы увидите, как BERT Tokenizer вместе с BERT Embeddings можно использовать для выполнения классификации текста.