В первую неделю года работа шла медленно, поэтому я решил впервые попробовать свои силы в конкурсе Kaggle (да, я знаю, что опаздываю на вечеринку). После регистрации и осмотра я оказался на Задаче классификации токсичных комментариев Jigsaw. Если вы просматриваете только Medium и не знаете, что означает токсичный комментарий, вот и все:

В этом сообщении описывается моя (вроде) успешная попытка обучить ConvNet классифицировать комментарий по одному или нескольким типам токсичности: угроза, непристойность, оскорбление и т. Д. (Всего 6 классов). По сравнению с лидером log-loss 0,022, моя простая модель набрала ~ 0,055 - Не удивительно, но довольно хорошо для ‹100 строк кода с Keras! В конце поста я также упомяну некоторые мета-уроки о моем первом опыте соревновательного машинного обучения :-).

Предварительная обработка текста

Данные для обучения были предоставлены в виде файла CSV с ~ 100 тыс. Строк. Каждая строка содержала уникальный идентификатор, текст и 1/0 для каждого класса, обозначающего классификацию.

from backports import csv
import numpy as np
# Helps in reading long texts
csv.field_size_limit(sys.maxsize)


def get_texts_and_targets(filename):
    texts = []
    targets = []

    with io.open(filename, encoding='utf-8') as csvfile:
        readCSV = csv.reader(csvfile)
        for i, row in enumerate(readCSV):
            if i == 0:
                # Header row
                continue
            texts.append(row[1].strip().encode('ascii', 'replace'))
            targets.append(np.array([float(x) for x in row[2:]]))
    print("Total number of texts: %s" % len(texts))
    return texts, targets

Будучи новичком в области глубокого обучения, я начал писать классный код предварительной обработки в NLTK. Оказывается, Keras предоставляет удобный класс Tokenizer для решения всех основных задач, таких как удаление специальных символов и преобразование в нижний регистр. Так что я поленился и просто использовал это:

from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
# Max number of input words in any sample
MAX_SEQUENCE_LENGTH = 200
VALIDATION_SPLIT = 0.1
def get_datasets(texts, targets, tokenizer=None):
    if tokenizer is None:
        tokenizer = Tokenizer()
        tokenizer.fit_on_texts(texts)

    sequences = tokenizer.texts_to_sequences(texts)
    word_index = tokenizer.word_index
    data = pad_sequences(sequences, maxlen=MAX_SEQUENCE_LENGTH)

    targets = np.asarray(targets)

    indices = np.arange(data.shape[0])
    np.random.shuffle(indices)
    data = data[indices]
    targets = targets[indices]
    nb_validation_samples = int(VALIDATION_SPLIT * data.shape[0])

    x_train = data[:-nb_validation_samples]
    y_train = targets[:-nb_validation_samples]
    x_val = data[-nb_validation_samples:]
    y_val = targets[-nb_validation_samples:]

    return tokenizer, word_index, x_train, y_train, x_val, y_val

Вложения слов

Для встраивания слов я использовал 100-размерные векторы Glove Twitter. Другими вариантами были предварительно обученные векторы из Word2Vec или Fasttext. Я попробовал Word2Vec, и, как и другие, Glove у меня работала лучше. Мне не удалось применить Fasttext, что является многообещающей перспективой - в основном потому, что Fasttext имеет векторы для дробей слов, и это может быть полезно для слов с орфографическими ошибками (или других терминов OOV), которые часто встречаются в комментариях.

Если вы, как и я, привыкли к пакету Python Gensim, вы можете использовать их скрипт для преобразования вложений Glove в формат word2vec. Как только это будет сделано, векторы можно будет довольно легко загрузить:

from gensim.models import KeyedVectors
def load_glove_model():
    word2vec = KeyedVectors.load_word2vec_format(
            os.path.join(WORD2VEC_FOLDER,
                'word2vec_twitter_glove.txt'),
            binary=False)
    return word2vec

Слой внедрения можно определить в Keras как:

def get_embedding_layer(word_index, gensim_model):
    embedding_dim = len(gensim_model.wv['apple'])
    embedding_matrix = np.zeros((len(word_index) + 1, embedding_dim))
    for word, i in word_index.items():
        if word in gensim_model.wv.vocab:
            embedding_matrix[i] = gensim_model.wv[word]
    embedding_layer = Embedding(len(word_index) + 1,
            embedding_dim,
            weights=[embedding_matrix],
            input_length=MAX_SEQUENCE_LENGTH,
            trainable=True)
    return embedding_layer

Обратите внимание на часть trainable = True в приведенном выше фрагменте - мы могли бы использовать вложения как они есть, но их тонкая настройка во время обучения изменяет их семантическое местоположение для нашего конкретного приложения. По сути, это форма трансферного обучения.

CNN

В этот момент вам может быть интересно, почему я использовал CNN для задачи понимания текста. Во-первых, потому что я никогда не тренировал CNN в Керасе (а я хотел). Но что ж… эта статья дает хорошее представление о том, как одномерные свертки могут быть полезны для обработки текста. В одномерных свертках вы, по сути, перебираете участки слов, а не пиксели (подумайте о скользящем окне слов, например, чтении). Для визуального восприятия (и чтобы сделать этот пост более привлекательным) я добавил это полностью оригинальное изображение:

CNN не особенно хороши для большинства задач НЛП, поскольку они теряют в последовательном потоке информации. Но поскольку цель здесь сводится к распознаванию «блоков» настроений, разбросанных по тексту, они работают прилично хорошо!

Мы используем 2 блока Convolutional + Max-Pooling, за которыми следуют 3 плотных слоя:

from keras.layers import *
from keras.models import Model
N_TARGET_CLASSES = 6
def get_convnet_model(embedding_layer):
    sequence_input = Input(shape=(MAX_SEQUENCE_LENGTH,), dtype='int32')
    embedded_sequences = embedding_layer(sequence_input)
    x = Conv1D(128, 5, activation='relu')(embedded_sequences)
    x = MaxPooling1D(5)(x)
    x = Conv1D(128, 5, activation='relu')(x)
    x = MaxPooling1D(5)(x)
    x = Flatten()(x)
    x = Dense(128, activation='relu')(x)
    x = Dense(64, activation='relu')(x)
    preds = Dense(N_TARGET_CLASSES, activation='sigmoid')(x)

    model = Model(sequence_input, preds)
    return model

Сигмоид (а не Softmax) является здесь более подходящей целевой функцией, поскольку каждый образец может принадлежать нескольким классам (комментарий может быть оскорблением и непристойным одновременно) .

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

Обучение

Я обнаружил, что Adagrad (с его настройками по умолчанию) лучше всего подходит для этого варианта использования. В Keras есть поддержка различных оптимизаторов, и я не пробовал настраивать такие параметры, как затухание (что могло бы еще больше уменьшить ошибку). Краткий обзор различных техник оптимизации можно найти в потрясающем сообщении в блоге Рудера.

texts, targets = get_texts_and_targets('train.csv')
tokenizer, word_index, x_train, y_train, x_val, y_val = get_datasets(texts, targets)
word2vec = load_word2vec_model()
embedding_layer = get_embedding_layer(word_index, word2vec)
model = get_convnet_model(embedding_layer)
model.compile(loss='binary_crossentropy',
        optimizer='adagrad',
        metrics=['accuracy'])
model.fit(x_train, y_train, validation_data=(x_val, y_val), epochs=2, batch_size=32, verbose=1)

Цель binary_crossentropy - это версия Keras потери журнала (так что вы получите то же значение). Поскольку я использовал предварительно обученные векторы и набор данных из ~ 85 тыс. Экземпляров, 2 эпохи достаточно (на основе журналов Keras, потеря, похоже, выходит на плато в последней половине самой второй эпохи).

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

А теперь несколько случайных размышлений о потере девственности Kaggle:

  1. Сложный не всегда лучше: я начал с Адама, но Адаград оказался лучше. Существует множество дискуссий на StackOverflow о том, как Adagrad временами работает лучше, чем его расширение Adadelta. В любом случае вначале не стоит беспокоиться о точном оптимизаторе, поскольку большинство из них обычно сходятся к достаточно хорошему значению.
  2. Сначала начните с больших изменений: это не пришло мне в голову естественным образом, и я признаю, что должно было прийти. До сих пор я в основном копировал фрагменты TensorFlow из других сообщений в блогах, поэтому мне никогда не приходилось настраивать свои собственные нейронные сети. Для настройки гиперпараметров (когда вы не используете что-то вроде hyperopt) всегда лучше сначала поиграться с теми изменениями, которые окажут максимальное влияние: например, Количество слоев ›Значение момента в оптимизаторе. Возможно, это не всегда так, но это хорошее практическое правило.
  3. Ансамбли - король (для лучших результатов): большинство лидеров Kaggle используют структуры ансамблей (например, XGBoost) или усредняют результаты нескольких сложных моделей (кто-то на досках обсуждений использовал LSTM + CNN).
  4. Kaggle - хорошее упражнение в обучении: хотя существует обоснованный скептицизм по поводу того, насколько опыт Kaggle актуален для отраслевой науки о данных, это, несомненно, хороший опыт обучения. Испытание нескольких алгоритмов, чтение форумов и просто тонкая настройка параметров (и наблюдение за журналами обучения) многое расскажут о том, как глубокое обучение ведет себя на практике. Фактически, всего за один раз в Kaggle я выучил довольно много практических правил, о которых я бы не знал иначе (например, размер партии = 32 - обычно хорошее место для начала. Для меня 16 был слишком медленным, и 128 никогда не сходился).
  5. Это вызывает привыкание: может быть, дело только в мне, но я не мог удержаться от того, чтобы снова и снова заглядывать в журналы обучения, чтобы увидеть, как меняются значения потерь для каждого эксперимента. Это в первую очередь причина того, что я не буду использовать Kaggle во время серьезной работы в офисе.
  6. Для глубокого обучения не нужна теория: Мои знания о глубоком обучении улучшились за последние несколько месяцев, но я все еще обнаружил, что использую классы / методы, не имея представления о том, как именно они работают (например, вся концепция одномерных сверток). В каком-то смысле это хорошо, поскольку делает обучение доступным для всех, у кого есть хороший компьютер и знание Python. При этом знание теории полезно для того, чтобы начать движение в правильном направлении и иметь возможность применять алгоритмы машинного обучения к неочевидным (читай: доступным в Интернете) сценариям использования.

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