Классификация текста с помощью TensorFlow Estimator

машинное обучение TensorFlow

выбран изruder.io, Автор: Себастьян Рудер, составлено Heart of the Machine.

В этой статье рассказывается, как использовать пользовательский оценщик TensorFlow, методы встраивания и модуль tf.layers для обработки задач классификации текста с использованием набора данных обзора IMDB. В этой статье вы узнаете, как использовать методы встраивания слов и передачи слов в word2vec, чтобы повысить производительность модели, когда размеченных данных недостаточно.

Основное содержание этой статьи следующее:

  • Загрузка данных с помощью наборов данных
  • Создайте базовый уровень, используя готовые оценщики
  • Используйте методы встраивания слов
  • Создавайте пользовательские оценки с помощью сверточных слоев и слоев LSTM.
  • Загрузите предварительно обученные векторы слов
  • Оценивайте и сравнивайте модели с помощью TensorBoard

Эта статья является частью 4 серии сообщений в блогах, посвященных модулям TensorFlow Datasets и Estimators. Читателю не нужно читать весь предыдущий контент, если вы хотите вернуться к некоторым концепциям, вы можете проверить следующие ссылки:

Содержание четвертой части будет основываться на всех предыдущих главах, и мы будем иметь дело с другим набором проблем обработки естественного языка (NLP). В этой статье показано, как использовать пользовательские оценщики TensorFlow, методы встраивания и модуль tf.layers (woohoo.tensorflow.org/API_docs/friends…) для решения задач классификации текста. В этой статье мы узнаем word2vec встраивание слов и методы обучения передачи для повышения производительности модели, когда помеченных данных недостаточно.

Мы покажем соответствующие фрагменты кода. Вот полный код Jupyter Notebook, вы можете запустить его локально или в Google Colaboratory. Чистые исходные файлы ".py" доступны по следующим ссылкам: (GitHub.com/Eysen Конвергенция…).

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

Задача этой статьи

Набор данных, который мы будем использовать, представляет собой крупномасштабный набор данных обзора фильмов IMDB (Love.Stanford.Amount/~Amaas/data…), который содержит 25 000 высокодифференцированных обзоров фильмов в качестве обучающих данных и еще 25 000 в качестве тестовых данных. Мы будем использовать этот набор данных для обучения модели бинарной классификации, которая может предсказать, будет ли отзыв положительным или отрицательным.

Например, вот негативный отзыв в датасете (222 лайка):

Теперь я люблю итальянские фильмы ужасов. Чем вульгарнее, тем лучше! Тем не менее, это не дрянной итальянский фильм. Это соус для пасты недельной давности с тухлыми фрикадельками. Этот фильм выглядит любительским во всех отношениях! Никакого ожидания, никакого страха, лишь несколько капель крови, падающих вокруг, напоминающих вам, что вы на самом деле смотрите фильм ужасов.

Keras предоставляет удобный обработчик для импорта наборов данных, которые также доступны в виде файла сериализованного массива numpy ".npz" отсюда (Да 3. Amazon AWS.com/text — привкус...) Скачать. Стандартной практикой в ​​классификации текстов является ограничение размера словаря, чтобы набор данных не стал слишком разреженным и слишком многомерным, тем самым предотвращая переоснащение. Таким образом, каждый обзор состоит из последовательности словесных индексов от «4» (наиболее часто встречающееся слово «the» в наборе данных) до «4999» (представляющее слово «оранжевый»). Индекс «1» представляет собой начало предложения, а индекс «2» присваивается всем неизвестным (также известным как «вне словарного запаса»). ООВ) слова. Эти индексы получаются после предварительной обработки в конвейере данных. Этот этап предварительной обработки включает в себя очистку данных, регуляризацию и сначала токенизацию каждого предложения, а затем создание словаря для индексации каждого слова по частоте.

После загрузки данных в память мы дополняли каждое предложение «0» до фиксированной длины для выравнивания (здесь длина равна 200). Таким образом, у нас есть два двумерных массива 25 000 * 200 в качестве обучающего и тестового массивов.

vocab_size = 5000  
sentence_size = 200  
(x_train_variable, y_train), (x_test_variable, y_test) = imdb.load_data(num_words=vocab_size)  
x_train = sequence.pad_sequences(  
    x_train_variable, 
    maxlen=sentence_size, 
    padding='post', 
    value=0)
x_test = sequence.pad_sequences(  
    x_test_variable,
    maxlen=sentence_size, 
    padding='post', 
    value=0)

входная функция

Платформа оценки использует функцию ввода, чтобы отделить конвейер данных от самой модели. Вы можете использовать некоторые вспомогательные методы для их создания, независимо от того, хранятся ли ваши данные в файле «.csv» или «pandas.DataFrame» и хранятся ли они в памяти или нет. В нашем примере и обучающий набор, и тестовый набор используют «Dataset.from_tensor_slices» для чтения данных.

x_len_train = np.array([min(len(x), sentence_size) for x in x_train_variable])  
x\_len\_test = np.array([min(len(x), sentence_size) for x in x_test_variable])

def parser(x, length, y):  
    features = {"x": x, "len": length}
    return features, y

def train_input_fn():  
    dataset = tf.data.Dataset.from_tensor_slices((x_train, x_len_train, y_train))
    dataset = dataset.shuffle(buffer_size=len(x_train_variable))
    dataset = dataset.batch(100)
    dataset = dataset.map(parser)
    dataset = dataset.repeat()
    iterator = dataset.make_one_shot_iterator()
    return iterator.get_next()

def eval_input_fn():  
    dataset = tf.data.Dataset.from_tensor_slices((x_test, x_len_test, y_test))
    dataset = dataset.batch(100)
    dataset = dataset.map(parser)
    iterator = dataset.make_one_shot_iterator()
    return iterator.get_next()

Мы перемешиваем данные обучения с помощью рандомизации, и нет заранее определенного количества итераций, которые мы хотим обучить модель, здесь нам нужно только один раз повторить тестовые данные, чтобы оценить модель. Нам также нужно дополнительное ключевое слово «len», чтобы получить длину исходной последовательности без дополнений, мы будем использовать их позже.

Создайте базовый уровень

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

Имея это в виду, давайте сначала попробуем одну из самых простых моделей классификации текста. Это будет разреженная линейная модель, которая присваивает вес каждому слову и суммирует все результаты, независимо от порядка слов. Поскольку эта модель не заботится о порядке слов в предложении, мы обычно называем ее методом мешка слов (BOW). Давайте посмотрим, как реализовать эту модель с помощью Estimator.

Мы начинаем с определения столбцов функций, которые будут использоваться в качестве входных данных для нашего классификатора. Как мы видели во второй части, «categorical_column_with_identity» — правильный выбор для предварительной обработки этого текстового ввода. Если мы получим необработанные текстовые слова, другие столбцы функций «feature_columns» могут выполнить для нас большую предварительную обработку. Теперь мы можем использовать готовый оценщик LinearClassifier.

column = tf.feature_column.categorical_column_with_identity('x', vocab_size)  
classifier = tf.estimator.LinearClassifier(  
    feature_columns=[column], 
    model_dir=os.path.join(model_dir, 'bow_sparse'))

Наконец, мы создали простую функцию для обучения классификатора и дополнительно создали кривую точности-отзыва. Поскольку мы не собираемся добиваться оптимальной производительности модели в этом сообщении блога, мы обучаем нашу модель только на 25 000 шагов.

def train_and_evaluate(classifier):  
    classifier.train(input_fn=train_input_fn, steps=25000)
    eval_results = classifier.evaluate(input_fn=eval_input_fn)
    predictions = np.array([p['logistic'][0] for p in classifier.predict(input_fn=eval_input_fn)])
    tf.reset_default_graph() 
    # Add a PR summary in addition to the summaries that the classifier writes
    pr = summary_lib.pr_curve('precision_recall', predictions=predictions, labels=y_test.astype(bool), num_thresholds=21)
    with tf.Session() as sess:
        writer = tf.summary.FileWriter(os.path.join(classifier.model_dir, 'eval'), sess.graph)
        writer.add_summary(sess.run(pr), global_step=0)
        writer.close()

train\_and\_evaluate(classifier) 

Одним из преимуществ выбора простой модели является то, что ее гораздо легче интерпретировать. Чем сложнее модель, тем сложнее ее тестировать и тем проще работать как черный ящик. В этом примере мы можем загрузить веса из последней контрольной точки нашей модели и посмотреть, какие слова имеют наибольшее абсолютное значение соответствующего веса. Результат выглядит так, как мы и ожидали.

# Load the tensor with the model weights  
weights = classifier.get_variable_value('linear/linear_model/x/weights').flatten()  
# Find biggest weights in absolute value  
extremes = np.concatenate((sorted_indexes[-8:], sorted_indexes[:8]))  
# word_inverted_index is a dictionary that maps from indexes back to tokens  
extreme_weights = sorted(  
    [(weights[i], word_inverted_index[i - index_offset]) for i in extremes])
# Create plot  
y_pos = np.arange(len(extreme_weights))  
plt.bar(y_pos, [pair[0] for pair in extreme_weights], align='center', alpha=0.5)  
plt.xticks(y_pos, [pair[1] for pair in extreme_weights], rotation=45, ha='right')  
plt.ylabel('Weight')  
plt.title('Most significant tokens')  
plt.show()  

Как видим, слова типа «освежающий» с наибольшими положительными весами явно связаны с положительной семантикой, тогда как слова с большими отрицательными весами несомненно вызывают отрицательные эмоции. Простая, но мощная модификация, которую мы можем сделать для улучшения модели, заключается в взвешивании слов в соответствии с их значениями tf-idf.

встроенный

Следующим шагом для увеличения сложности модели является встраивание слов. Вложения — это плотные низкоразмерные представления разреженных многомерных данных. Это позволяет нашей модели изучать более значимое представление каждого слова, а не просто индекс. Хотя одно измерение может не иметь большого смысла, было показано, что низкоразмерные пространства (при изучении из достаточно большого корпуса) охватывают такие отношения, как время, множественное число, род, связанные темы и т. д. Мы можем увеличить встраивание слов, преобразовав наши существующие столбцы функций в «embedding_column». Представление функции, видимое для модели, представляет собой среднее значение вложений слов для каждого слова (см. этот документ для обсуждения «объединителей»:woohoo.tensorflow.org/API_docs/friends…) может вставлять встроенные функции в готовые в DNNClassifier.

Напоминание для наблюдателей: «embedding_column» — это просто эффективный способ применить полностью связанный слой к вектору двоичных признаков разреженного слова, который умножается выбранным объединителем на соответствующую константу. Прямым следствием этого является то, что нет смысла использовать «embedding_column» непосредственно в «LinearClassifier», поскольку два последовательных линейных слоя без нелинейного сопоставления между ними не добавляют модели предсказательную силу, если только, конечно, вложения Word предварительно обучены.

embedding_size = 50  
word\_embedding\_column = tf.feature_column.embedding_column(  
    column, dimension=embedding_size)
classifier = tf.estimator.DNNClassifier(  
    hidden_units=[100],
    feature_columns=[word_embedding_column], 
    model_dir=os.path.join(model_dir, 'bow_embeddings'))
train\_and\_evaluate(classifier)  

Мы можем использовать t-SNE в TensorBoard (En. Wikipedia.org/wiki/T - День 3…) визуализируйте нашу 50-мерную векторную схему слов как R ^ 3. По нашим оценкам, похожие слова будут относительно близки друг к другу. Это можно назвать эффективным способом проверки весов нашей модели и обнаружения неожиданной производительности.

свертка

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

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

На рисунке ниже показано скольжение матрицы фильтра размера d × m F по каждому 3-граммовому словному окну для построения новой карты признаков. После этого обычно используются объединяющие слои для объединения смежных результатов.

Источник: Обучение ранжированию коротких текстовых пар с помощью сверточных глубоких нейронных сетей, Северин и др., [2015].

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

Создание пользовательского оценщика

Как мы видели в предыдущих сообщениях в блоге, инфраструктура «tf.estimator» предоставляет высокоуровневый API для обучения моделей машинного обучения, определяя операции «train()», «evaluate()» и «predict()», которые позволяют легко обрабатывать контрольные точки, загрузку данных, инициализацию, обслуживание, построение графиков и сеансов. У нас есть небольшой выбор готовых оценщиков, подобных тем, которые мы использовали раньше, но, скорее всего, вам придется создать свой собственный.

Чтобы написать собственное средство оценки, вам нужно написать функцию «model_fn (функции, метки, режим, параметры)», возвращаемое значение которой является EstimatorSpec. Первый шаг, который вам нужно сделать, это сопоставить функции с нашим слоем внедрения:

input_layer = tf.contrib.layers.embed_sequence(  
    features['x'], 
    vocab_size, 
    embedding_size,
    initializer=params['embedding_initializer'])

Затем мы используем «tf.layers» для обработки каждого вывода по порядку.

training = (mode == tf.estimator.ModeKeys.TRAIN)  
dropout_emb = tf.layers.dropout(inputs=input_layer,  
                                rate=0.2, 
                                training=training)
conv = tf.layers.conv1d(  
    inputs=dropout_emb,
    filters=32,
    kernel_size=3,
    padding="same",
    activation=tf.nn.relu)
pool = tf.reduce_max(input_tensor=conv, axis=1)  
hidden = tf.layers.dense(inputs=pool, units=250, activation=tf.nn.relu)  
dropout = tf.layers.dropout(inputs=hidden, rate=0.2, training=training)  
logits = tf.layers.dense(inputs=dropout_hidden, units=1)  

Наконец, мы будем использовать объект заголовка модели «Head», чтобы упростить написание последней части «model_fn». Голова модели уже знает, как вычислять прогнозы, потери, операции обучения (train_op), метрики и экспортировать эти выходные данные, и их можно повторно использовать в моделях. Этот подход также используется в готовых оценщиках и предоставляет нам унифицированную функцию оценки, которую можно использовать во всех моделях. Мы будем использовать «binary_classification_head», заголовок для модели бинарной классификации с одной меткой, которая использует «sigmoid_cross_entropy_with_logits» в качестве базовой функции потерь.

head = tf.contrib.estimator.binary_classification_head()  
optimizer = tf.train.AdamOptimizer()  
def _train_op_fn(loss):  
    tf.summary.scalar('loss', loss)
    return optimizer.minimize(
        loss=loss,
        global_step=tf.train.get_global_step())

return head.create_estimator_spec(  
    features=features,
    labels=labels,
    mode=mode,
    logits=logits,
    train_op_fn=_train_op_fn) 

Запустить эту модель так же просто, как и раньше:

initializer = tf.random_uniform([vocab_size, embedding_size], -1.0, 1.0))  
params = {'embedding_initializer': initializer}  
cnn_classifier = tf.estimator.Estimator(model_fn=model_fn,  
                                        model_dir=os.path.join(model_dir, 'cnn'),
                                        params=params)
train\_and\_evaluate(cnn_classifier)  

LSTM-сеть

Используя API «Estimator» и ту же головную часть модели, мы можем создать классификатор, который использует нейроны с долговременной кратковременной памятью (LSTM) вместо сверточных нейронов. Рекуррентные модели, подобные этим, являются наиболее успешными строительными блоками для приложений обработки естественного языка. LSTM последовательно обрабатывает весь документ, сохраняя текущее состояние в своей памяти и рекурсивно обрабатывая последовательность через свои нейроны.

Одним из недостатков рекурсивных моделей по сравнению с CNN является то, что из-за рекурсивной природы модели становятся глубже и сложнее, что часто приводит к увеличению времени обучения и ухудшению сходимости. LSTM (и RNN в целом) могут страдать от проблем сходимости, таких как дисперсия градиента или взрыв градиентов, то есть при достаточной настройке они могут достигать самых современных результатов по многим проблемам. В целом, CNN хороши для извлечения признаков, в то время как RNN хороши для задач, основанных на семантике целых предложений, таких как ответы на вопросы или машинный перевод.

Каждый нейрон обрабатывает встраивание одного слова за раз и обновляет свое внутреннее состояние в соответствии с дифференцируемым вычислением, которое зависит от вектора встраивания x_t и предыдущего состояния h_t-1. Чтобы лучше понять, как работают LSTM, вы можете обратиться к сообщению в блоге Криса Олаха (столбец ah.GitHub.IO/posts/2015-…).

Источник: Понимание сетей LSTM Криса Олаха.

Полная модель LSTM может быть представлена ​​​​в виде следующей простой блок-схемы:

В начале этой статьи мы дополнили все документы до 200 слов, что необходимо для построения правильного тензора. Однако, когда документ содержит менее 200 слов, мы не хотим, чтобы LSTM продолжал заполнять слова, так как это не добавит информации и снизит производительность. Поэтому мы также хотим предоставить нашей сети информацию об исходной длине последовательности до заполнения. Далее внутри модели копируется последнее состояние в конец последовательности. Мы можем сделать это, добавив функцию «len» в нашу функцию ввода. Теперь мы можем следовать приведенной выше логике и заменить слои свертки, объединения и выравнивания нашими нейронами LSTM.

lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(100)  
_, final_states = tf.nn.dynamic_rnn(  
        lstm_cell, inputs, sequence_length=features['len'], dtype=tf.float32)
logits = tf.layers.dense(inputs=final_states.h, units=1)  

предварительно обученный вектор

Подавляющее большинство моделей, которые мы показывали ранее, полагаются на встраивание слов в качестве первого слоя. До сих пор мы случайным образом инициализировали этот слой внедрения. Однако многие предыдущие исследования показали, что полезно использовать предварительно обученные встраивания в качестве инициализации на большом неразмеченном корпусе, особенно при обучении только на небольшом количестве размеченных примеров. Наиболее популярным методом встраивания слов с предварительным обучением является word2vec. Использование знаний из немаркированных данных с помощью предварительно обученных вложений является примером трансферного обучения. Для этого мы покажем, как их использовать в оценщике «Эстиматор». Мы будем использовать предварительно обученные векторы из другой популярной модели «GloVe».

embeddings = {}  
with open('glove.6B.50d.txt', 'r', encoding='utf-8') as f:  
    for line in f:
        values = line.strip().split()
        w = values[0]
        vectors = np.asarray(values[1:], dtype='float32')
        embeddings[w] = vectors

После загрузки векторов из файла в память мы сохраняем их в виде массива numpy, используя тот же индекс, что и наш словарь. Размер созданного массива (5000, 50). В каждом индексе строки он содержит 50-мерный вектор, представляющий слово с таким же индексом в нашем словаре,

embedding_matrix = np.random.uniform(-1, 1, size=(vocab_size, embedding_size))  
for w, i in word_index.items():  
    v = embeddings.get(w)
    if v is not None and i < vocab_size:
        embedding_matrix[i] = v

Наконец, мы можем использовать пользовательскую функцию инициализации и передать результат объекту «params», который затем можно использовать непосредственно в нашем «cnn_model_fn» без каких-либо изменений.

def my_initializer(shape=None, dtype=tf.float32, partition_info=None):  
    assert dtype is tf.float32
    return embedding_matrix
params = {'embedding_initializer': my_initializer}  
cnn\_pretrained\_classifier = tf.estimator.Estimator(  
    model_fn=cnn_model_fn,
    model_dir=os.path.join(model_dir, 'cnn_pretrained'),
    params=params)
train\_and\_evaluate(cnn_pretrained_classifier) 

Запустить ТензорБорад

Теперь мы можем запустить TensorBoard и сравнить наши обученные модели, чтобы увидеть, как они различаются по времени обучения и производительности. Запустить на терминале:

tensorboard --logdir={model_dir} 

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

получать прогнозы

Чтобы получить прогнозы для новых предложений, мы можем использовать метод «predict» для экземпляра «Estimator», который загружает последнюю контрольную точку для каждой модели и оценивает ее на невидимых примерах. Но перед тем, как передать данные в модель, мы должны очистить, токенизировать и сопоставить каждое слово с соответствующим индексом. Конкретный код выглядит следующим образом:

def text_to_index(sentence):  
    # Remove punctuation characters except for the apostrophe
    translator = str.maketrans('', '', string.punctuation.replace("'", ''))
    tokens = sentence.translate(translator).lower().split()
    return np.array([1] + [word_index[t] + index_offset if t in word_index else 2 for t in tokens])

def print_predictions(sentences, classifier):  
    indexes = [text_to_index(sentence) for sentence in sentences]
    x = sequence.pad_sequences(indexes,
                               maxlen=sentence_size, 
                               padding='post', 
                               value=-1)
    length = np.array([min(len(x), sentence_size) for x in indexes])
    predict_input_fn = tf.estimator.inputs.numpy_input_fn(x={"x": x, "len": length}, shuffle=False)
    predictions = [p['logistic'][0] for p in classifier.predict(input_fn=predict_input_fn)]
    print(predictions) 

Стоит отметить, что контрольных точек самих по себе недостаточно для прогнозирования, фактический код, используемый для построения оценщика, также необходим для сопоставления сохраненных весов с соответствующими тензорами. Рекомендуется связать сохраненные контрольные точки с ветвями кода, которые их создали. Если вы заинтересованы в экспорте своей модели полностью восстанавливаемым способом, вы можете проверить класс «SaveModel», который полезен для построения моделей с использованием API, предоставляемого TensorFlow Serving.

Суммировать

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

Для получения более подробной информации см.: