Классификация текстов - один из вариантов использования машинного обучения (ML) в обработке естественного языка (NLP). Пример классификации текста - определить, является ли электронное письмо спамом. Другой - разделить набор документов на список категорий. В нашем случае мы хотим создать кхмерский новостной портал и сканировали документы с разных кхмерских новостных сайтов. Многие статьи содержат много статей о дорожно-транспортных происшествиях. Это важные новости, но нам нужна возможность группировать их, чтобы пользователь мог видеть конкретные, не загромождая главную страницу. Поэтому мы хотим иметь возможность классифицировать их как связанные с дорожно-транспортными происшествиями или нет. Исходный сайт не помечает их соответствующим образом, поэтому для этого нам нужно использовать алгоритм машинного обучения. Итоговый результат на нашем сайте: https://domnung.com. Тот же процесс может применяться к нескольким категориям, которые вы можете увидеть здесь.

В этом посте мы проанализируем производительность различных функций на разных алгоритмах из scikit-learn. Затем выберите лучший классификатор производительности для использования в производстве. Мы опишем подход от обучения к сохранению модели и развертыванию ее в производстве.

Шаги

Мы выделяем следующие подходы:

  1. Загрузка и тегирование данных - анализ данных с использованием сегментации по кхмерскому тексту
  2. Извлечение функций из документа с помощью TFIDF
  3. Запустите несколько разных алгоритмов машинного обучения и сравните результаты
  4. Сохраните выбранную модель и загрузите ее для запуска в производство

1. Загрузка данных и теги

У нас уже есть процесс сканирования различных сайтов на кхмерском языке для получения названия и содержания сайта. Вручную идентифицировал 104 документа (56 документов по дорожно-транспортному происшествию, 48 по не дорожно-транспортному происшествию). Это считается небольшим, но мы можем добавить больше, пройдя несколько циклов.

Из базы данных у меня есть таблица article со столбцами id, title, body и category. Столбец категория - это строковый тип данных, который я вручную ввел «авария» или «не_акцидент» в зависимости от содержимого. Поэтому я просто запрашиваю данные в БД с помощью:

select id, title, body, category from dbo.article 
WHERE category IS NOT NULL

Я создал метод getArticles, который выводит эти поля в виде списков и извлекает их следующим образом:

(docIds, doc_titles, doc_contents, categories) = getArticles();

Теперь у нас есть текст и его ожидаемая категория. Мы можем начать анализировать данные и превратить текст в функции.

Сегментация слов

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

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

របស់យើងប្រសើរជាងមុន។ => របស់ យើង ប្រសើរ ជាង មុន។

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

2. Извлечение функций

Этот шаг состоит в том, чтобы взять сегментированное слово и подсчитать его вхождение во всех документах. У нас уже есть существующий процесс TFIDF, но у нас не было функций для извлечения биграммы (два последовательных члена). Позже будет показано, что этот подход дает лучший результат. Итак, мы будем использовать TfidfVectorizer в библиотеках scikit-learn.

from sklearn.feature_extraction.text import TfidfVectorizer

Текущая опция TfidfVectorizer по умолчанию не обрабатывает кхмерский Unicode должным образом. Он токенизирует и игнорирует кхмерский нижний индекс. Поскольку мы уже токенизировали наш текст, мы будем использовать настраиваемый tokenizer (tokenizersplit), который просто разделен пробелом.

def tokenizersplit(str):
    return str.split();
tfidf = TfidfVectorizer(tokenizer=tokenizersplit, encoding='utf-8', min_df=2, ngram_range=(1, 2), max_features=25000)
tfidf_vect.fit(df['text'])
tfidf_vect.transform(df['text'])

Вот некоторые подробности о параметрах для TfidfVectorizer:

  • кодировка: установлено значение «utf-8» для обработки кхмерских символов Unicode.
  • min_df: игнорировать термин с числом документов меньше заданного значения. Значение 2 означает, что для подсчета термин должен существовать как минимум в 2 документах.
  • ngram_range: ngram, который вы хотите извлечь (подробнее ниже)
  • max_features: максимальное количество функций.

Униграмма против биграммы

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

Итак, мы хотим сравнить производительность параметров ngram_range, которые создают униграммы стихов униграммы и биграммы. Биграмма предоставит дополнительные возможности, и нам нужно будет посмотреть, значительно ли это повысит производительность.

Вот результат для униграммы на 100 артикулов и 3879 термина униграммы продукта. Результатом является два прогона с набором рандомизации поезда / проверки с набором проверки 30%.

Naive Bayes accuracy:         0.63, 0.63
Logistic Regression accuracy: 0.90, 0.96
SVM accuracy:                 0.50, 0.50
Random Forest accuracy:       0.83, 0.96

Для униграммы и биграммы в тех же 100 статьях получается 14692 термина (униграмма + биграмма). Результат:

Naive Bayes accuracy:         0.60, 0.60
Logistic Regression accuracy: 0.77, 0.80
SVM accuracy:                 0.50, 0.50
Random Forest accuracy:       0.83, 0.93

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

3. Оцените эффективность с помощью другого классификатора.

Чтобы оценить количество обучающих данных, я собираюсь протестировать несколько разных документов. Я пробовал от 50 до 75, 100 документов. Вот результаты различных алгоритмов:

С 50 или менее документами производительность хуже, чем у случайного гостя. Это не годится. Но всего со 100 документами мы видим приличную производительность.

Анализ производительности

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

from sklearn import model_selection, linear_model, metrics
# split the dataset into training and validation datasets
train_x, valid_x, train_y, valid_y = model_selection.train_test_split(df['text'], df['cat'], test_size=0.35, random_state=0)
from sklearn import metrics
m=linear_model.LogisticRegression()
m.fit(xtrain_tfidf, train_y)
y_pred = m.predict(xvalid_tfidf)
print(metrics.classification_report(valid_y, y_pred, 
'''
## 85 docs for training, 15 docs for validation set
             precision    recall  f1-score   support

no_accident       0.93      1.00      0.96        13
   accident       1.00      0.92      0.96        13

avg / total       0.96      0.96      0.96        26

## 65 docs on training, 35 docs for validation set
             precision    recall  f1-score   support

no_accident       0.80      1.00      0.89        20
   accident       1.00      0.71      0.83        17

avg / total       0.89      0.86      0.86        37
'''

Увеличение тренировки за счет уменьшения набора проверки действительно немного увеличивает производительность с 0,89 до 0,96 по точности и аналогично по отзыву. Мы не хотим, чтобы проверка была слишком маленькой, иначе вы не можете быть уверены в ее эффективности. Но я думаю, что около 30% проверочного набора - это хорошо.

4. Сохраните модель и загрузите ее в производство.

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

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

import pickle;
tfidf = TfidfVectorizer(tokenizer=tokenizersplit, encoding=’utf-8');
tfidf.fit(df.text);
pickle.dump(tfidf, open(‘feature_100.pkl’, ‘wb’));

Точно так же, когда мы хотим сохранить используемую модель:

...
model = linear_model.LogisticRegression()
model.fit(features, labels)
import pickle
pickle.dump(model, open("model.pkl", 'wb'))

Для загрузки мы просто используем функцию загрузки из pickle. Чтобы загрузить модель, вам понадобится определенная функция настраиваемого токенизатора, которую мы назвали tokenizersplit:

import pickle
# needed to load pickle tfidf
def tokenizersplit(str):
    return str.split();
tfidf = pickle.load(open('feature_100.pkl', 'rb'))
...
loaded_model = pickle.load(open('model.pkl', 'rb'))

Пошаговая информация о тренировочном процессе

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

  1. Загрузить учебные документы
(docIds, docTitles, docBodies, categories) = getArticles();

2. Запустите сегментацию и создайте фрейм данных pandas.

(token_bodies, token_titles) = tokenizeDocs(docTitles, docBodies);
# concatenate title, body with space into tokenText
tokenText = [token_titles[i] + " " + token_bodies[i] for i in xrange(len(token_titles))]
import pandas as pd
df = pd.DataFrame({id: docIds});
df[‘text’] = tokenText;
df[‘cat’] = categories;

3. Запустите tfidf fit.

tfidf = TfidfVectorizer(tokenizer=tokenizersplit, encoding=’utf-8');
tfidf.fit(df.text);

4. Сохраните соответствие TFIDF (чтобы tfidf можно было повторно использовать в новых документах, чтобы размер элемента соответствовал модели)

import pickle;
pickle.dump(tfidf, open(‘feature_100.pkl’, ‘wb’));

5. Запустите преобразование tfidf.

features = tfidf.transform(df.text)

6. Разделите данные на набор для обучения и проверки.

from sklearn import model_selection, preprocessing
# split the dataset into training and validation datasets
train_x, valid_x, train_y, valid_y = model_selection.train_test_split(df['text'], df['cat'], test_size=0.30, random_state=1)

# label encode the target variable
encoder = preprocessing.LabelEncoder()
train_y = encoder.fit_transform(train_y)
valid_y = encoder.fit_transform(valid_y)

7. Запустите преобразование tfidf в наборе для обучения и проверки.

xtrain_tfidf =  tfidf_vect.transform(train_x)
xvalid_tfidf =  tfidf_vect.transform(valid_x)

8. Выполните подгонку модели и сделайте прогноз на наборе для обучения и проверки.

from sklearn import metrics, linear_model, naive_bayes, metrics, svm, xgboost
def train_model(classifier, trains, t_labels, valids, v_labels):
    # fit the training dataset on the classifier
    classifier.fit(trains, t_labels)

    # predict the labels on validation dataset
    predictions = classifier.predict(valids)

    return metrics.accuracy_score(predictions, v_labels)
# Naive Bayes
accuracy = train_model(naive_bayes.MultinomialNB(), xtrain_tfidf, train_y, xvalid_tfidf, valid_y);
print "NB accuracy: ", accuracy; # 94%, 65%, 60%, 60%

# Logistic Regression
accuracy = train_model(linear_model.LogisticRegression(), xtrain_tfidf, train_y, xvalid_tfidf, valid_y);
print "LR accuracy: ", accuracy; # 96%, 84%, 94%, 100%, 97%
# SVM
accuracy = train_model(svm.SVC(), xtrain_tfidf, train_y, xvalid_tfidf, valid_y);
print "SVM accuracy: ", accuracy; # 54%, 48%, 48%
# Random Forest
accuracy = train_model(ensemble.RandomForestClassifier(), xtrain_tfidf, train_y, xvalid_tfidf, valid_y)
print "RF accuracy: ", accuracy # 94% ,97%, 94%, 85%
# Extereme Gradient Boosting (not from scikit-learn)
accuracy = train_model(xgboost.XGBClassifier(), xtrain_tfidf.tocsc(), train_y, xvalid_tfidf.tocsc());
print "Xgb accuracy: ", accuracy;  # 82%, 91%,92%

9. Выберите топ-модель и повторно обучите все данные (не разделяя на набор для обучения и проверки) и сохраните ее.

# convert cat ("accident/non-accident) into category_id 0,1
df['category_id'] = df['cat'].factorize(sort=True)[0]
labels = df.category_id
features = tfidf.transform(df.text)
model = linear_model.LogisticRegression()
model.fit(features, labels)
import pickle
pickle.dump(model, open("model.pkl", 'wb'))

Шаги за шагом к запуску в производство

1. Получите новые документы

(all_documents, doctitles, docIds) = getNewArticles()

2. Выполните сегментацию и форматируйте во фрейм данных panda в качестве шага обучения.

(tokenized_documents, tokenized_document_title) = tokenizeDocs(all_documents, doctitles);
# concatenate title, body with space into tokenText
tokenText = [doctitles[i] + " " + all_documents[i] for i in xrange(len(doctitles))]
import pandas as pd
df = pd.DataFrame({id: docIds});
df[‘text’] = tokenText;

3. Загрузите файл рассола TFIDF (в соответствии со словарями).

# needed to load pickle feature_100.pkl
def tokenizersplit(str):
    return str.split();

# load tfidf.fit
tfidf = pickle.load(open('feature_100.pkl', 'rb'))
features = tfidf.transform(df.text)

4. Запустите TFIDF для новых документов.

features = tfidf.transform(df.text)

5. Загрузите, сохраните модель и запустите прогноз для новых документов.

import pickle
loaded_model = pickle.load(open('model.pkl', 'rb'))
y_pred = loaded_model.predict(features)

6. Отобразите или сохраните результат.

df['tag'] = y_pred
print(df[[id,'tag']])

Вывод

В этом посте показано, как использовать scikit-learn для разделения документов на две категории. Мы используем подход tfidf, обычно применяемый в НЛП, в качестве функций для классификаторов. Мы рассмотрим, как обучать различные алгоритмы, и посмотрим на производительность. Затем разверните его в производственной среде.

С этими ограниченными данными этикеток мы получили довольно приличную точность около 93%. По мере того, как мы обучаем все больше документов в производстве, мы можем вручную проверять добавление дополнительных ярлыков к данным. Затем мы можем обучить больший набор данных и заново оценить производительность. Мы можем также начать изучение подхода глубокого обучения, чтобы увидеть, может ли он улучшить производительность. А пока, если вы умеете читать кхмерский текст, вы можете увидеть результат здесь: https://domnung.com/cambodia/accident.

Обновления

После того, как мы увеличили количество учебных документов примерно до 500, точность была увеличена. С классификатором XGBoost мы можем достичь точности 98%. Вместо этого мы используем этот алгоритм. Вот результат для нескольких прогонов с различным соотношением обучение / проверка на униграмме TFIDF.

# 20% validation set
NB accuracy:  0.960784313725
LR accuracy:  0.941176470588
SVM accuracy:  0.666666666667
RF accuracy:  0.911764705882
Xgb accuracy:  0.980392156863
# 25% validation set
LR accuracy:  0.934210526316
SVM accuracy:  0.684210526316
RF accuracy:  0.960526315789
Xgb accuracy:  0.986842105263
# 30% validation set
NB accuracy:  0.947368421053
LR accuracy:  0.940789473684
SVM accuracy:  0.644736842105
RF accuracy:  0.953947368421
Xgb accuracy:  0.960526315789

См. Следующую статью о мультиклассовой классификации.