Классификация с использованием машинного обучения

GitHub link: https://github.com/elegantwist/catalog_classifier

Необходимо построить классификатор элементов справочника (компании) на основе текстового описание сфер интересов компании-элемента.

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

Таким образом, на выходе получается каталог типа: Description — Carrier — Category — Service, и выглядит это в виде таблицы вот так:

где Description задается приходит извне, а Carrier, Category и Service (если они не заполнены) заполняются на основании Description

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

Поставлена цель: автоматизировать работу — максимально снизить, или даже убрать, участие человека в процессе разметки данных.

Ниже описано каким образом это было реализовано.

Для решения задачи было рассмотрено три подхода:

1. Использование фиксированных, заданным человеком правил, когда один фиксированный Description однозначно соответствует фиксированному набору Carrier-Category-Service

2. Использование статистического подхода, когда частота встречаемости сочетаний слов в Description трансформировалась бы в частоту соответствий категорий

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

В работу выбрали третий вариант, с использованием модели Decision Tree Classifier из пакета sklearn. Решение было принято исходя из анализа истории данных, и вывода о том, что логика конвертации Description в категории наилучшим образом описывается сочетанием подходов 1 и 2. Т.е. большим количеством правил жестко заданных правил, статистически оптимально описывающих требуемую конвертацию — что отлично ложится на логику работы Decision Tree Classifier.

Реализацию задачи разделили на четыре этапа:

1. Загрузка данных для обработки

2. Подготовка данных для классификации

3. Обучение модели и получение результата классификации

4. Валидация результата

В имплементации используется «стандартная» ML-связка: pandas + numpy + sklearn.

1. Загрузка данных для обработки

Реализуем следующие шаги:

– загружаем данные из CSV в pandas таблицы

– выбираем данные, которые будут являться базой для обучения модели (база)

– выбираем данные, которые будут использованы для применения обученной модели (целевые данные)

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

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

После загрузки, разделяем тренировочные и целевые данные. Для целей валидации достаточно 5% от всего корпуса, потому нам нужно только смешать исходные данные, и выделить любые 5% и 95% для остается для обучения — это типичный «целевой» объем, потому ориентируемся на него.

Вот код, который это делает:

mask_val = np.random.rand(len(catalog_dt)) < 0.95

train_dt = catalog_dt.loc[mask_val].reset_index(drop=True)

validation_dt = catalog_dt.loc[~mask_val].reset_index(drop=True)

2. Подготовка данных для классификации

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

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

Для человека эти нюансы не проблема. Но для того, чтобы корректно построить ML-модель, способную функционировать «в потоке», необходима «единая база» корпуса данных. Потому, как перед обучением модели, так и перед ее применением для разметки новых данных, Description необходимо привести к единому виду.

Для этого необходимо:

a) Сформировать правила очистки данных и применить их к нашим данным

b) Сформировать правила трансформации данных, полученных после очистки, к обучению, и применить их к нашим данным

c) Конвертировать текстовые данные в числовой формат, для использования в DTC

d) Конвертировать полученный результат работы DTC обратно, в текстовое описание классов

Реализация:

a) По результату анализа данных, сформировались следующие правила очистки для входящей строки описания, которая может встретиться в поле Description (translit, очевидно, переводит русские символы в латиницу):

def clean_rule(str_value=""):

vl = str_value
vl = re.sub('(?<=\().+?(?=\))', '', vl)
vl = vl.replace("(", "")
vl = vl.replace(")", "")
vl = vl.strip()
vl = vl.replace(".COM", "")
vl = vl.replace(".RU", "")
vl = vl.replace(".INFO", "")
vl = vl.replace(".ORG", "")
vl = vl.replace(".NET", "")
vl = translit.translify(vl)
vl = vl.upper()
return vl

теперь, можем применять это правило везде, где требуется привести сырые данные в «единый» вид

b) После этого, необходимо обработанную таблицу подготовить к обучению с использованием DTC

Дело в том, что поле Description содержат кванты информации, разделенные знаком «;», и заранее не известно в единой ли они последовательности заданы, и есть ли в квантах ошибки и «мусор».

Таким образом, для каждой строки таблицы необходимо

– Разбить строку на элементы-кванты

– Для каждого элемента применить правило очистки

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

– Из каждой строки Description возьмем первые 4 элемента (вне зависимости заполнены они или нет) и используем их как колонки таблицы, на которой будем проводить обучение (колонки Article-N)

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

a_l_list_raw = article_list_str.split(";")
a_l_list_cl = [clean_rule(item) for item in a_l_list_raw]
a_l_list = sorted(a_l_list_cl, key=len)

В результате обработки, получится таблица такого вида:

c) после приведения таблицы к «единому» виду, необходимо закодировать возможные варианты значения колонок Article (квант информации для поля Description) во внутренний цифровой классификатор, пригодный для обучения и использования в модели дерева. Т.е. произведем конвертацию значений article в двоичный код — то, что называется one-hot encoding.

Для реализации этого необходимо:

– склеить вместе training и test таблицы. Это необходимо, так как какие-то значения колонки article могут содержаться только в одной таблице, а нам необходимо использовать полный каталог (все возможные значения article). И при one-hot encoding варианты значения трансформируются в колонки, и мы должны работать с заранее известным количеством колонок-значений

– произвести двоичное кодирование значений каталога, используя встроенную функцию pandas — get_dummies

– результат трансформации необходимо заново разделить на тренировочную и целевую таблицу данных

Реализация:

var_columns = ['Article1', 'Article2', 'Article3', 'Article4']
print("> processing service")
# we need to append one to another to get the correct dummies
res_dt = train_dt[var_columns].append(predict_dt[var_columns]).reset_index(drop=True)
res_dt_d = pd.get_dummies(res_dt)
# and split them again
len_of_predict_dt = len(predict_dt[var_columns])
X_train, X_test = res_dt_d[:-len_of_predict_dt], res_dt_d[-len_of_predict_dt:]
X_train = X_train.reset_index(drop=True)
X_test = X_test.reset_index(drop=True)
y_train_ts = pd.get_dummies(train_dt['TypeService'])

Полученную one-hot кодированную таблицу мы передаем для обучения модели, и классификации целевых данных.

На этапе применения модели, возникает проблема в том, что результат классификации будет так же получен в виде one-hot кодированного двоичного набора данных, который мы не можем использовать напрямую как конечный результат работы. Потому требуется ретранслировать результат работы модели (как следствие — результат функции get_dummies) обратно в human-friendly каталог.

Для этого буем использовать следующую функцию:

def retranslate_dummies(pd_dummies_Y, y_pred):
c_n = {}
for p_i in range(len(pd_dummies_Y.columns)):
c_n.update({
p_i: pd_dummies_Y.columns[p_i],
})
d_df = pd.DataFrame(y_pred)
y_pred_df = d_df.rename(columns=c_n)
res = []
for i, row in y_pred_df.iterrows():
n_val = [clmn for clmn in y_pred_df.columns if row[clmn]==1]
if n_val == "" or len(n_val) == 0:
res = res + [np.NaN]
else:
res = res + n_val
return res

В которой на входе задается полный кодированный каталог и полученные в результате работы модели-классификатора значения, а на выходе выдается ретрансляция в исходный human-friendly формат, который и является целью работы системы

3. Обучение модели и классификация

Для самого процесса обучения и категоризации используется стандартная реализация Decision Tree Classifier из пакета sklearn.

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

После обучения, в обученную модель подаются закодированные целевые данные, а на выходе получаем закодированные one-hot результат классификации. Полученный результат необходимо сконвертировать обратно в human-friendly вид.

Мы применяем этот подход индивидуально для каждого из разделов: сначала для нахождения нужных Service, потом для Category и наконец для Carrier

4. Валидация результата

Необходимо удостоверится что результат работы соответствует ожиданиям. Так как используем заранее известный результат классификации (те самые 5%, которые выделили из исходного набора данных), то нам достаточно сравнить реальные и «правильные» категории, которые выбрал пользователь, с теми, которые были выбраны автоматически в результате процесса классификации.

По условиям задачи, если хотя бы одна из категорий была заполнена некорректно (заполненное автоматически отличалось от существующего в каталоге), то мы считаем заполнение «условно-некорректным». В «условно-некорректные» могут попадать значения следующие значения:

– однозначно некорректно определенные классификатором

– выбранные значения каталога по Description которые не встречались ранее

– выбранные значения каталога по Description которые не встречались ранее, но меньше адекватного для однозначной классификации количества раз

– если выбранный классификатором вариант конвертации встречается чуть реже чем постоянно, но при этом есть в истории записи о конвертации в нужный нам класс

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

Формализованная цель работы стоит в том, чтобы снизить % таких условно-некорректно заполненных категорий

Полученные результаты:

В результате реализации решения, была достигнута точность в 60% однозначно-корректно заполненных категорий, с 5% однозначно некорректным заполнением, и 35% «условно-некорректным» выбором.

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