"Обработка естественного языка"

Предсказание жанров на основе диалога фильмов

Классификация НЛП с несколькими метками

«Когда-нибудь, а этот день может никогда не наступить, я призову вас оказать мне услугу. Но до того дня считай это правосудие подарком моей дочери в день свадьбы ». - Дон Вито Корлеоне, Крестный отец (Фрэнсис Форд Коппола, 1972)

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

Но резонирует ли величие кинематографического диалога и в ушах машины? Эта статья направлена ​​на использование возможностей обработки естественного языка (NLP) для построения модели классификации для прогнозирования жанров фильмов на основе диалогов в их диалогах.

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

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

Процесс строительства разделен на три основных этапа:

  1. Компиляция, очистка и предварительная обработка набора обучающих данных
  2. Исследовательский анализ данных обучения
  3. Построение и оценка модели классификации

Часть I. Составление набора данных для обучения

Данные для этого проекта были получены из публикации Корнельского университета (указана в разделе «Благодарности»).

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

  • Беседы о фильмах: обмен диалогами в виде комбинаций идентификаторов строк и соответствующих идентификаторов фильмов.
  • Строки из фильма: текст каждой строки диалога вместе с соответствующим идентификатором строки.
  • Метаданные названий фильмов: атрибуты фильмов, включенные в данные, например названия и жанры.

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

Извлечение, преобразование и загрузка данных обучения

Конвейер ETL, используемый для создания окончательного обучающего набора, будет включать следующие этапы:

  1. Чтение данных из каждого из трех текстовых файлов в фреймы данных pandas
  2. Назначение идентификатора разговора каждому обмену, содержащемуся в наборе данных разговоров
  3. Объединение фрейма данных разговоров таким образом, чтобы каждая строка диалога отображалась в отдельной строке с соответствующим идентификатором разговора.
  4. Объединение объединенного фрейма данных с набором данных строк для получения фактического текста для каждого идентификатора строки
  5. Объединение отдельных строк с помощью идентификатора разговора таким образом, чтобы весь обмен отображался в текстовом формате в отдельной строке.
  6. Наконец, объединение фрейма данных текстовых бесед с метаданными фильма для извлечения жанров для каждого текстового документа и загрузка окончательного фрейма данных в базу данных SQLite.

После запуска конвейера ETL для необработанных файлов набор обучающих данных будет выглядеть следующим образом:

Переформатирование целевой переменной

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

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

genres = df['genres'].tolist()
genres = ','.join(genres)
genres = genres.split(',')
genres = sorted(list(set(genres)))
for genre in genres:
    df[genre] = df['genres'].apply(lambda x: 1 if genre in x else 0)

Набор двоичных столбцов жанров будет представлять собой матрицу целевой переменной, в которой каждому фильму может быть присвоено любое количество из 24 уникальных меток.

Часть II: Исследовательский анализ данных обучения

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

Большинству фильмов в нашем наборе данных присвоено 2–4 жанровых метки. Если учесть, что всего существует 24 возможных метки, это подчеркивает, что мы можем ожидать, что наша матрица целевой переменной будет содержать гораздо больше отрицательных классификаций, чем положительных.

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

df[genres].mean().mean()
>>> 0.12317299038986919

Вышеупомянутое указывает на то, что только 12% меток набора данных относятся к положительному классу. Этому фактору следует уделить особое внимание при выборе метода оценки модели.

Давайте также оценим количество положительных примеров для каждого жанрового ярлыка:

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

Это, вероятно, повлияет на успех модели в разных жанрах.

Обсуждение результатов анализа

Приведенный выше анализ раскрывает два важных вывода о наших данных обучения:

  1. Классовое распределение сильно несбалансировано в пользу отрицательного.

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

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

Мы должны помнить об этом при выборе метрики (показателей) производительности для оценки модели. Если, например, мы будем судить о модели на основе точности (правильные классификации как доля от общего числа классификаций), мы можем ожидать достижения показателя c,88%, просто прогнозируя каждый случай как отрицательный. (учитывая, что только 12% обучающих этикеток положительны).

Такие показатели, как точность (доля фактических положительных результатов, которые были классифицированы правильно) и отзыв (доля сделанных положительных оценок, которые были правильными), более подходят в этом контексте.

2. Несбалансированное распределение положительных классов среди ярлыков.

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

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

Часть III: Построение классификационной модели

Обработка естественного языка (NLP)

В настоящее время данные для функций нашей модели все еще представлены в необработанном текстовом формате, в котором они были предоставлены. Чтобы преобразовать данные в формат, подходящий для машинного обучения, нам потребуется использовать некоторые методы НЛП.

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

  1. Очистите текст, чтобы удалить все знаки препинания и специальные символы.
  2. Разделите отдельные слова в каждом документе на токены
  3. Сделайте лемматизацию текста (объединение слов с изменяемой формулой, например, замена слов «обучение» и «выучил» на «учиться»)
  4. Удалите пробелы из токенов и установите их в нижнем регистре
  5. Удалите все стоп-слова (например, «the», «and», «of» и т. Д.)
  6. Векторизуйте каждый документ по количеству слов
  7. Выполните преобразование частоты терминов с обратной частотой документов (TF-IDF) для каждого документа, чтобы сгладить подсчет на основе частоты терминов в корпусе.

Мы можем записать операции очистки текста (шаги 1–5) в одну функцию:

def tokenize(text):
    text = re.sub('[^a-zA-Z0-9]', ' ', text)
    tokens = word_tokenize(text)
    lemmatizer = WordNetLemmatizer()
    clean_tokens = (lemmatizer.lemmatize(token).lower().strip() for token in tokens if token \
                    not in stopwords.words('english'))
    return clean_tokens

который затем можно передать в качестве токенизатора в функцию CountVectorizer scikit-learn (шаг 6) и завершить процесс функцией TfidfTransformer (шаг 7).

Реализация конвейера машинного обучения

Переменные характеристик должны пройти преобразование NLP, прежде чем они могут быть переданы в алгоритм классификации. Если бы мы запускали преобразование для всего набора данных, это технически вызвало бы утечку данных, поскольку векторизация подсчета и преобразование TF-IDF будут основаны на данных как из обучающего, так и из тестового наборов. .

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

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

pipeline = Pipeline([
    ('vect', CountVectorizer(tokenizer=tokenize)),
    ('tfidf', TfidfTransformer()),
    ('clf', MultiOutputClassifier(DecisionTreeClassifier()))
    ])

Обратите внимание, что нам нужно указать оценщик как MultiOutputClassifier. Это означает, что модель должна возвращать прогноз для каждой из указанных меток жанра для каждого экземпляра.

Оценка базовой модели

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

Помимо учета дисбаланса классов, нам также необходимо настроить некоторые метрики оценки, чтобы обеспечить вывод с несколькими метками, поскольку, в отличие от классификации с одной меткой, каждый прогнозируемый экземпляр больше не является жестким правильным или неправильным. Например, экземпляр, для которого модель правильно классифицирует 20 из 24 возможных меток, следует считать более успешным, чем экземпляр, для которого ни одна из меток не классифицирована правильно.

Для читателей, заинтересованных в более глубоком изучении методов оценки моделей классификации с несколькими метками, я могу порекомендовать Единый взгляд на показатели эффективности с несколькими метками (Wu & Zhou, 2017).

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

1 - hamming_loss(y_test, y_pred)
>>> 0.8667440038568157

Оценка точности 86,7% поначалу кажется отличным результатом. Однако, прежде чем собираться и считать проект успешным, мы должны принять во внимание, что рассмотренный ранее дисбаланс классов, вероятно, означает, что эта оценка слишком велика.

Давайте сравним потерю Хэмминга с точностью и запоминанием модели. Чтобы получить средние оценки по меткам, взвешенные по количеству положительных классов каждой метки, мы можем передать average=’weighted’ в качестве аргумента в функции:

precision_score(y_test, y_pred, average='weighted')
>>> 0.44485346325188513
recall_score(y_test, y_pred, average='weighted')
>>> 0.39102002566871064

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

Имея это в виду, мы будем использовать показатель F1 (среднее гармоническое значение между точностью и отзывчивостью) в качестве основного показателя при оценке модели:

f1_score(y_test, y_pred, average='weighted')
>>> 0.41478130331069335

Сравнение производительности разных лейблов

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

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

Как упоминалось ранее, лучший способ обойти это - собрать более сбалансированный набор данных при построении второй версии модели.

Улучшение модели: выбор алгоритма классификации

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

Перед запуском цикла давайте добавим в конвейер дополнительный этап: разложение по сингулярным числам (TruncatedSVD). Это форма уменьшения размерности, которая определяет наиболее значимые свойства матрицы признаков и удаляет то, что осталось. Он похож на анализ главных компонентов (PCA), но может использоваться для разреженных матриц.

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

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

tree = DecisionTreeClassifier()
forest = RandomForestClassifier()
knn = KNeighborsClassifier()
models = [tree, forest, knn]
model_names = ['tree', 'forest', 'knn']
for model in models:
    pipeline = Pipeline([
    ('vect', CountVectorizer(tokenizer=tokenize)),
    ('tfidf', TfidfTransformer()),
    ('svd', TruncatedSVD()),
    ('clf', MultiOutputClassifier(model))
    ])
    cv_scores = cross_val_score(pipeline, X, y, scoring='f1_weighted', cv=4, n_jobs=-1)
    score = round(np.mean(cv_scores), 4)
    scores.append(score)
model_compare = pd.DataFrame({'model': model_names, 'score': scores})
print(model_compare)
>>> model   score
>>> 0    tree  0.2930
>>> 1  forest  0.2274
>>> 2     knn  0.2284

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

Улучшение модели: настройка гиперпараметров

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

Поскольку мы используем конвейер для соответствия модели, мы можем определить значения параметров для тестирования не только для оценщика, но и для этапов НЛП, таких как векторизатор.

pipeline = Pipeline([
    ('vect', CountVectorizer(tokenizer=tokenize)),
    ('tfidf', TfidfTransformer()),
    ('svd', TruncatedSVD()),
    ('clf', MultiOutputClassifier(DecisionTreeClassifier()))
    ])
parameters = {
    'vect__ngram_range': [(1, 1), (1, 2)],
    'clf__estimator__max_depth': [250, 500, 1000],
    'clf__estimator__min_samples_split': [1, 2, 6]
}
cv = GridSearchCV(pipeline, param_grid=parameters, scoring='f1_weighted', cv=4, n_jobs=-1, verbose=10)
cv.fit(X, y)

После завершения поиска по сетке мы можем просмотреть параметры и оценку для нашей окончательной настроенной модели:

print(cv.best_params_)
>>> {'clf__estimator__max_depth': 500, 'clf__estimator__min_samples_split': 2, 'vect__ngram_range': (1, 1)}
print(cv.best_score_)
>>> 0.29404722954784424

Настройка гиперпараметров позволила нам очень незначительно улучшить производительность модели на 0,1 процентного пункта, в результате чего итоговая оценка F1 составила 29,4%. Это означает, что мы можем ожидать, что модель правильно классифицирует чуть менее трети истинных положительных результатов.

Заключительное слово

Таким образом, мы смогли построить модель, которая пытается предсказать жанры фильма на основе его диалогов:

  1. Манипулирование корпусом текста, полученным из публикации Корнельского университета, для создания набора обучающих данных
  2. Использование методов НЛП для преобразования текстовых данных в матрицу переменных функций
  3. Создание базового классификатора с использованием конвейера машинного обучения и улучшение модели путем оценки показателей производительности, подходящих в контексте классификации с несколькими метками со значительным дисбалансом классов

Окончательная модель может быть использована для создания прогнозов для новых диалоговых обменов. В следующем примере используется цитата из Карнавал душ (Херк Харви, 1962):

def predict_genres(text):
    pred = pd.DataFrame(cv.predict([text]), columns=genres)
    pred = pred.transpose().reset_index()
    pred.columns = ['genre', 'prediction']
    predictions = pred[pred['prediction']==1]['genre'].tolist()
    return predictions
line = "It's funny... the world is so different in the daylight. In the dark, your fantasies get so out of hand. \
But in the daylight everything falls back into place again."
print(predict_genres(line))
>>> ['family', 'scifi', 'thriller']

Итак, каков окончательный вердикт? Можем ли мы предложить IMDb принять нашу модель как средство автоматизации их жанровой категоризации? На данном этапе, наверное, нет. Однако модель, созданная в этом посте, должна стать достаточно хорошей отправной точкой, с возможностями для внесения улучшений в будущие версии, например, путем компиляции более крупного набора данных, более сбалансированного для разных жанров.

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

Благодарности

Кристиан Данеску-Никулеску-Мизил. Корнелл Фильм - Корпус диалогов. Корнельский университет 2011

Си-Чжу Ву и Чжи-Хуа Чжоу. Единое представление о показателях эффективности нескольких лейблов. ICML 2017