С ОД/ДО

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

Бизнес-проблема

описание проблемы

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

Цель и ограничение

Цель

  • Для заданного вопроса и ответов на него (правильный/неправильный) выберите правильный ответ.
  • Получите высокое значение MRR для модели.

Ограничение

  • Не строгое ограничение задержки, но не должно занимать более нескольких секунд.

Данные

Источник данных

Данные загружаются по этой ссылке.

Обзор данных

В наборе данных пять столбцов. В первом столбце указан номер вопроса, во втором — текст вопроса, в третьем — ответ на соответствующий вопрос, в четвертом — метка класса (0,1), а в пятом — имеет номер ответа на вопрос.

На каждый вопрос есть 10 ответов, и только один из них правильный. правильные ответы имеют метку класса 1и неправильные ответы иметь метку класса 0.

Проблема машинного обучения

Сопоставление реальной проблемы с проблемой машинного обучения

Тип проблемы машинного обучения

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

Показатель эффективности

Ранг правильного ответа должен быть выше ранга неправильного ответа. Таким образом, основной метрикой будет ранговая матрица. MRR (Mean Reciprocal Rank) — основной показатель. Матрица путаницы, матрица точности и матрица отзыва являются вторичными показателями.

MRR: это среднее обратных рангов результатов для выборки подзапросов Q.

где rank_iотносится к позиции в рейтинге первого соответствующего документа для i -й запрос.

MRR для приведенной выше таблицы будет (1/3+1/2+1)/3 = 0,61(приблизительно).

label_ranking_average_precision_score используется для расчета MRR. Средняя точность ранжирования меток (LRAP) усредняет по выборкам ответы на следующий вопрос: для каждой метки истинности, какая часть меток с более высоким рангом была истинными метками? Этот показатель производительности будет выше, если вы сможете присвоить более высокий рейтинг меткам, связанным с каждым образцом. Полученная оценка всегда строго больше 0, а наилучшее значение равно 1.

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

Формально, учитывая бинарную индикаторную матрицу основных меток истинности y∈{0,1}^(n_samples*n_labels) и оценку, связанную с каждой меткой f_cap∈ℝ^(n_samples*n_labels),средняя точность определяется как

где

|⋅| вычисляет мощность набора (т. е. количество элементов в наборе), а ||⋅||_0 — это норма ℓ0 (которая вычисляет количество ненулевых элементов в векторе). Этот раздел взят из этого здесь.

В нашем случае есть только один правильный ответ на вопрос, поэтому он возвращает MRR. В нашем случае ||𝑦𝑖||_0 и |Lij| всегда будет 1, а в уравнении j будет иметь только одно возможное значение. Так что теперь эта формула станет:

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

В нашем наборе данных пять столбцов, но для нашей задачи требуются только три из них. Поэтому я сохраняю только необходимые столбцы, и имена этих столбцов — question , answer и label. В наборе данных 5 241 880 строк.

Выполним предварительную обработку текстовых данных.

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

Для предварительной обработки текстовых данных я удалю стоп-слова, лишние пробелы, специальные символы, теги HTML. В стоп-слова я не буду включать слова «не», «что», «где», «как», «когда» и «почему». Я расширю такие слова, как «я буду, я буду», «я буду», «мы должны иметь» и т. д. Давайте рассмотрим один вопрос и ответ.

Question: . what is a corporation? 
Answer: A company is incorporated in a specific nation, often within the bounds of a smaller subset of that nation, such as a state or province. The corporation is then governed by the laws of incorporation in that state. A corporation may issue stock, either private or public, or may be classified as a non-stock corporation. If stock is issued, the corporation will usually be governed by its shareholders, either directly or indirectly.

Приведенный выше фрагмент кода будет обрабатывать текстовые данные.

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

Количество NaNs в наборе данных

print(df.isna().sum())
------------------------------
question    0 
answer      0 
label       0 
dtype: int64
-----------------------------

Судя по приведенному выше результату, в наборе данных нет NaN/Null/None, но есть несколько строк, в которых вопрос null, и он не может быть обнаружен df.isna().sum(), потому что null присутствует как string. Таких строк 10.

aa = []
for i, text in enumerate(df["question"]):
    if text == 'null':
        aa.append(i)
# questions as `null` string
df.iloc[aa,:]

Есть ряд строк, в которых нет ни вопросов, ни ответов. Есть 10строк, где нет вопросов (т.е. len(question)==0)и есть 28строк, где нет ответов (т.е. len(answer)==0) .

len_q = [len(i) for i in df["question"]]
len_a = [len(i) for i in df["answer"]]
q,a = [], []
for i in range(len(len_q)):
    if len_q[i] == 0:
        q.append(i)
    if len_a[i] == 0:
        a.append(i)

Мы можем удалить эти строки, потому что наш набор данных довольно большой. Чтобы удалить эти строки, я использую хак. Сначала я заменяю такие записи на np.nan, а затем удаляю их с помощью df.dropna(axis=0, how='any', inplace=True). Количество строк, которые будут удалены, равно 10+28+10 = 48.

Код для их удаления:

# relacing such question and answers with np.Nan
# and then droping them
df.replace(["", "null"], np.nan, inplace = True)
df.dropna(axis = 0, how = "any", inplace= True)

Давайте перепроверим, были ли они удалены или нет.

# storing the lenght of questions and answer
len_q = [len(i) for i in df["question"]]
len_a = [len(i) for i in df["answer"]]# lists to store where answer/question lenght is 0
q,a = [], []
for i in range(len(len_q)):
    if len_q[i] == 0:
        q.append(i)
    if len_a[i] == 0:
        a.append(i)
# list to store where question == 'null' as string
aa = []
for i, text in enumerate(df["question"]):
    if text == 'null':
        aa.append(i)
print(df.iloc[q, :])
print(df.iloc[a, :])
print(df.iloc[aa, :])

Количество уникальных вопросов/ответов

Есть 9,66% вопросов и 89,77% уникальных ответов.

print("Number of unique questions in the dataset is {:,}\
out of {:,} questions i.e {:.3f}%".format(len(df['question'].unique()), df.shape[0],                100*len(df['question'].unique())/df.shape[0]))
print("Number of unique answer inthe dataset is {:,}\
out of {:,} answers i.e. {:3f}%".format(len(df['answer'].unique()), df.shape[0],100*len(df['answer'].unique())/df.shape[0]))
---------------------------------------------------------------
Number of unique questions on the dataset is 510,136 out of 5,241,832 questions i.e. 9.732% 
Number of unique questions on the dataset is 4,706,060 out of 5,241,832 answers i.e. 89.778917%

Количество duplicate строк

В наборе данных есть 67 478повторяющихся строк. Опустим и их.

print("Number of duplicate rows in dataset: \
{:,}".format(len(df)-len(df.drop_duplicates(subset = ["question", "answer"],keep = False))))
--------------------------------------------------------
Number of duplicate rows in dataset: 67,478
--------------------------------------------------------print("Before droping dulicated number of datapoints = {:,}".format(len(df)))
df.drop_duplicates(subset = ["question", "answer"], keep = False, inplace = True)
print("After droping dulicated number of  datapoints = {:,}".format(len(df)))
-------------------------------------------------------------
Before droping dulicated number of datapoints = 5,241,832
After droping dulicated number of  datapoints = 5,174,354
-------------------------------------------------------------

В начале в наборе данных было 5 241 880 точек данных. Во-первых, 48 строк были удалены из-за отсутствия текстовых данных в вопросе/ответе и вопросе как null . Таким образом, до удаления повторяющихся строк было 5 241 832 строк. После удаления 67 478 повторяющихся строк осталось 5 174 354 строк.

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

  • Если вопрос имеет более 1 правильного ответа, отбросьте его.
  • Если вопрос не имеет правильного ответа, отбросьте его.
  • Если в вопросе есть только один правильный ответ и нет неправильного ответа, отбросьте его.
  • Если на вопрос есть один правильный ответ и менее 9 неправильных ответов, отбрасывайте его.
  • Если в вопросе более 10 ответов, отбросьте его.

Вопросы с более чем одним правильным ответом

Есть некоторые вопросы, которые имеют более одного правильного ответа, давайте найдем их и отбросим.

a = df.groupby('question')\
    .agg({'label': 'sum'})\
    .reset_index()\
    .rename(columns={'label': 'correct answers'})
# questions that have more than 1 correct answer
multiple_correct_answers_df = a[a['correct answers']>1]
multiple_correct_answers_df.head()

print('Number questions that have more than 1 correct answers: {:,}'.format(len(multiple_correct_answers_df)))
# getting the questions that have more than 1 correct answer
multiple_correct_answers =  multiple_correct_answers_df['question'].values
# dropping such questions
print('number of rows before dropping questions more than 1 correct answer is: {:,}'.format(len(df)))
df.drop(df[df['question'].isin(multiple_correct_answers)].index, axis = 0, inplace=True)
df.reset_index(inplace=True, drop= True)
print('number of rows after dropping questions more than 1 correct answer is: {:,}'.format(len(df)))
--------------------------------------------------------------------
Number questions that have more than 1 correct answers: 9,819
number of rows before dropping questions more than 1 correct answer is: 5,174,354
number of rows after dropping questions more than 1 correct answer is: 4,971,431

Вопросы, на которые нет правильного ответа

Есть некоторые вопросы, на которые нет правильного ответа, давайте найдем их и отбросим.

a = df.groupby('question')\
    .agg({'label': 'sum'})\
    .reset_index()\
    .rename(columns={'label': 'correct answers'})
# questions that have more than 1 correct answer
no_correct_answers_df = a[a['correct answers'] == 0]
no_correct_answers_df.head()

print('Number questions that do not have correct answers: {:,}'.format(len(no_correct_answers_df)))
# getting the questions that don't have correct answers
no_correct_answers = no_correct_answers_df['question'].values
# dropping such questions
print('number of rows before dropping questions than do not have correct answer is: {:,}'.format(len(df)))
df.drop(df[df['question'].isin(no_correct_answers)].index, axis = 0, inplace=True)
df.reset_index(inplace=True, drop= True)
print('number of rows after dropping questions that do not correct answer is: {:,}'.format(len(df)))
--------------------------------------------------------------------
Number questions that do not have correct answers: 4,488
number of rows before dropping questions than do not have correct answer is: 4,971,431
number of rows after dropping questions that do not correct answer is: 4,934,335

Вопросы, в которых есть только один правильный ответ и нет неправильных

Давайте найдем те вопросы, которые имеют только один правильный ответ и не имеют неправильных ответов. На такие вопросы будет только один ответ, и это будет правильный ответ.

a = df.groupby('question')\
    .agg({'answer': 'count', 'label': 'sum'})\
    .reset_index()\
    .rename(columns={'answer':'total answers',
                      'label': 'correct answers'})
# questions that have only no incorrect answer and one correct answer
one_correct_no_incorrect_answers_df = a[(a['correct answers']==1)
                                         &(a['total answers']==1)]
one_correct_no_incorrect_answers_df.head()

Вопросы, на которые меньше 10 ответов

a = df.groupby('question')\
    .agg({'answer': 'count'})\
    .reset_index()\
    .rename(columns={'answer':'total answers'})
# questions that have less than 10 answer
less_thanlen(answer)==0answers_df = a[a['total answers'] < 10]
less_thanlen(answer)==0answers_df.head()

print('Number questions that have less than 10 answers is: {:,}'.format(len(less_thanlen(answer)==0answers_df)))
# getting the questions that have less than 10 answers
less_thanlen(answer)==0answers = less_thanlen(answer)==0answers_df['question'].values
# dropping such questions
print('number of rows before dropping questions that have less than 10 answers is: {:,}'.format(len(df)))
df.drop(df[df['question'].isin(less_thanlen(answer)==0answers)].index, axis =   0, inplace=True)
df.reset_index(inplace=True, drop= True)
print('number of rows after dropping questions that have less than 10 answers is: {:,}'.format(len(df)))
--------------------------------------------------------------------
Number questions that have less than 10 answers is: 13,595
number of rows before dropping questions that have less than 10 answers is: 4,934,335
number of rows after dropping questions that have less than 10 answers is: 4,828,383

Вопросы, на которые есть более 10 ответов

a = df.groupby('question')\
    .agg({'answer': 'count'})\
    .reset_index()\
    .rename(columns={'answer':'total answers'})
# questions that have only more than 10 answer
greater_thanlen(answer)==0answers_df = a[a['total answers'] > 10]
greater_thanlen(answer)==0answers_df.head()

print('Number questions that have greater than 10 answers is: {:,}'.format(len(greater_thanlen(answer)==0answers_df)))
# getting the questions that have greater than 10 answers
greater_thanlen(answer)==0answers = greater_thanlen(answer)==0answers_df['question'].values
# dropping such questions
print('number of rows before dropping questions that have greater than 10 answers is: {:,}'.format(len(df)))
df.drop(df[df['question'].isin(greater_thanlen(answer)==0answers)].index, axis = 0, inplace=True)
df.reset_index(inplace=True, drop= True)
print('number of rows after dropping questions that have greater than 10 answers is: {:,}'.format(len(df)))
--------------------------------------------------------------------
Number questions that have greater than 10 answers is: 924
number of rows before dropping questions that have greater than 10 answers is: 4,828,383
number of rows after dropping questions that have greater than 10 answers is: 4,812,950

Вышеупомянутый процесс можно выполнить несколькими строками кода, написав все условия в одной строке с помощью оператора OR.

a = df.groupby('question')\
    .agg({'answer': 'count', 'label': 'sum'})\
    .reset_index()\
    .rename(columns={'answer':'total answers', 
                     'label': 'correct answers'}) 
#let's create a mask
mask = (a['correct answers']>1) or (a['correct answers'] == 0) or   (a['correct answers']==1) & (a['total answers']==1)) or 
(a['total answers'] < 10) or (a['total answers'] > 10)
a = a[mask]
# getting the required questions
req_ques = a['question'].values
#drop such questions df.drop(df[df['question'].isin(req_ques)].index,axis=0,inplace=True)
df.reset_index(inplace=True, drop= True)

Теперь в наборе данных 4 812 950вопросов. После выполнения всей вышеперечисленной предварительной обработки будет 10 ответов на вопрос, и только один из них будет правильным.

Исследовательский анализ данных

Распространение labels

Код для просмотра распределения класса:

fig, ax1 = plt.subplots(1,1, figsize =(6,6))
ax1.set_title('Distribution of label over whole dataset', fontsize=15)
sns.countplot(df["label"], ax = ax1)
ax1.set_xlabel("target", fontsize = 12)
ax1.set_ylabel("count", fontsize = 12)
for p in ax1.patches:
    ax1.annotate('{:.0f}%'.format(p.get_height()*100/len(df)), 
                (p.get_x()+0.25, p.get_height()+1), fontsize=12)
plt.show()

На вопрос дается 10 ответов, и только один из них правильный, поэтому будут 90%данные метки класса 0 и 10% данные метка класса 1.

Особенности ручной работы

В этом разделе будут разработаны некоторые функции, основанные на длине.

Код для создания этих функций:

df = hand_craft_features(df)

После создания этих функций отсортируйте данные по столбцу question, а затем сохраните данные.

df.sort_values(by = ['question'], inplace=True)
df.reset_index(drop=True, inplace=True)
df.to_csv("./text_processed.csv", index = False)

EDA о функциях ручной работы

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

Код для создания этих графиков:

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

Разделение данных

Случайное разделение не будет хорошим выбором для такого набора данных. Поэтому я буду использовать первые 80% данных для обучения, следующие 10% для проверки и следующие 10% для тестирования. Я собираюсь разделить на 80%-10%-10%. В этом разделе весь набор данных используется для проверки стабильности текстовых данных.

df_train = df.iloc[0:int(len(df)*0.8), :] 
df_cv    = df.iloc[int(0.8*len(df)):int(0.9*len(df)), :]
df_test  = df.iloc[int(0.9*len(df)):, :]

Стабильность question и answer в наборе данных train/CV/test после разделения

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

Приведенный ниже код создаст наборы слов вопросов/ответов в наборе данных train, CV и test.

def return_set_words(data_frame, col_name):
    """This will return set of words"""
    corpus = data_frame[col_name].values
    big_text = ""
    for sen in corpus:
        big_text += str(sen)
    return set(big_text.split())
# for questions
train_question_words_set = return_set_words(df_train, "question")
cv_question_words_set    = return_set_words(df_cv, "question")
test_question_words_set  = return_set_words(df_test, "question")
# for answers
train_answer_words_set = return_set_words(df_train, "answer")
cv_answer_words_set    = return_set_words(df_cv, "answer")
test_answer_words_set  = return_set_words(df_test, "answer")

52,55% слов вопросов из набора данных CV и 50,49% слов вопросов из тестового набора данных присутствуют в обучающем наборе данных. 45,35% слов ответов набора данных CV и 46,31% слов ответов тестового набора данных присутствуют в обучающем наборе данных. Давайте визуализируем это с помощью диаграмм Венна.

Для question

from matplotlib_venn import venn3, venn2
fig, (ax1, ax2, ax3) = plt.subplots(1,3,figsize = (20,7))
out1=venn2([train_answer_words_set, cv_answer_words_set], set_labels=(['Train','CV']),ax=ax1)
out2=venn2([train_answer_words_set, test_answer_words_set], set_labels=(['Train','Test']),ax=ax2)
out3=venn3([train_answer_words_set, cv_answer_words_set, test_answer_words_set],set_labels=(['Train','CV','Test']),ax=ax3)
# to chnage the font size: 
for out in [out1, out2, out3]:
    for text in out.set_labels:
         text.set_fontsize(14)
    for text in out.subset_labels:
        text.set_fontsize(12)
plt.show()

Из приведенной выше диаграммы Венна видно, что вопросы не очень стабильны в наборе данных для обучения, тестирования и резюме. Почти 50% слов вопросов в наборе данных теста/резюме отсутствуют в обучающих данных.

Для answer

Используя тот же приведенный выше код для построения диаграммы Венна

Из приведенной выше диаграммы Венна видно, что ответы не очень стабильны в наборе данных для обучения, тестирования и резюме. Почти 55% слов вопросов в наборе данных теста/резюме отсутствуют в обучающих данных.

  • Вопросы выглядят более стабильно, чем ответы.
  • Вопросы и ответы оба не очень стабильны.

Для расчета MRR данные должны быть такими, чтобы первые 10 строк относились к первому вопросу, следующие 10 строк — ко второму вопросу и так далее.

ML-модели

Случайная модель

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

Функция для расчета MRR

from sklearn.metrics import label_ranking_average_precision_score
def get_mrr(y_true, y_pred):
    """
    to calcuate the mrr.
    it take the y_true and y_pred and make list of lists such that
    for each question ten answer result in list.
    """
    y_true =  np.array([y_true[i*10:(i+1)*10]
                        for i in range(len(y_true)//10)])
    y_pred = np.array([y_pred[i*10:(i+1)*10] 
                        for i in range(len(y_pred)//10)])
    return label_ranking_average_precision_score(y_true, y_pred)

Таким образом, любая разумная модель будет иметь MRRбольше, чем 0,29.

Выбор векторизатора

Модель логистической регрессии обучается с помощью BoW (uni и bi-grams)и TF-IDF (уни- и биграммы). Для этой задачи для обучения модели используется только 250 тыс. точек данных.

Из приведенной выше таблицы видно, что TF-IDF обеспечивает лучшую производительность, чем BoW. Поэтому векторизатор TFIDF (bi-gram) используется для обучения моделей машинного обучения.

Векторизация

В этом разделе из набора данных будут выбраны первые 2 миллиона точек данных. Первые 80% на обучение, следующие 10% на резюме и следующие 10% на тестирование.

Разделение данных

df = pd.read_csv("./text_processed.csv", low_memory= True)
df = df.head(2000000) #top 2 million datapoints
df_train = df.iloc[0:int(len(df)*0.8), :] 
df_cv    = df.iloc[int(0.8*len(df)):int(0.9*len(df)), :]
df_test  = df.iloc[int(0.9*len(df)):, :]
X_train, y_train = df_train[[col for col in df.columns if col != "label"]], df_train[["label"]]
X_cv, y_cv       = df_cv[[col for col in df.columns if col != "label"]], df_cv[["label"]]
X_test, y_test   = df_test[[col for col in df.columns if col != "label"]], df_test[["label"]]
print("Number of datapoints in train data: {:,}\n\
Number of datapoints in CV data: {:,}\n\
Number of datapoints in test data: {:,}".format(X_train.shape[0],
                                              X_cv.shape[0],
                                              X_test.shape[0]))
--------------------------------------------------------------------Number of datapoints in train data: 1,600,000
Number of datapoints in CV data: 200,000
Number of datapoints in test data: 200,000

TF-IDF (bi-gram) используется для векторизации текстовых данных. Количество уникальных слов в вопросе/ответе очень велико, поэтому размерность векторизованных данных будет очень большой. Но max_features=5000 используется для создания 5000-мерного вектора для вопроса и ответа на оба вопроса. max_featuresиспользует частотность терминов во всем корпусе, чтобы выбрать 5000 лучших слов в качестве словарного запаса.

# for questions
tfidf_vectorizer1=TfidfVectorizer(ngram_range = (1,2),
                                     min_df= 1, max_features = 5000)
tfidf_train_que=tfidf_vectorizer1\
                  .fit_transform( X_train["question"].values)
tfidf_cv_que = tfidf_vectorizer1.transform(X_cv["question"].values) tfidf_test_que=tfidf_vectorizer1.transform(X_test["question"].values)
# for answers
tfidf_vectorizer2=TfidfVectorizer(ngram_range = (1,2), min_df = 1,
                                   max_features = 5000)
tfidf_train_ans=tfidf_vectorizer2.fit_transform(X_train["answer"]\
                                                 .values)
tfidf_cv_ans = tfidf_vectorizer2.transform(X_cv["answer"].values) tfidf_test_ans=tfidf_vectorizer2.transform(X_test["answer"].values)

Есть 13 созданных вручную функций, и они будут объединены с векторами TF-IDF.

train_hand_crafted = X_train[X_train.columns[2:]] 
cv_hand_crafted    = X_cv[X_cv.columns[2:]]
test_hand_crafted  = X_test[X_test.columns[2:]]
std = StandardScaler()
train_hand_crafted = std.fit_transform(train_hand_crafted)
cv_hand_crafted = std.transform(cv_hand_crafted)
test_hand_crafted = std.transform(test_hand_crafted)

hstack программы scipy используется для укладки векторов.

train_tfidf = hstack((tfidf_train_que, tfidf_train_ans,
                       train_hand_crafted))
cv_tfidf = hstack((tfidf_cv_que, tfidf_cv_ans, cv_hand_crafted))
test_tfidf = hstack((tfidf_test_que, tfidf_test_ans, 
                     test_hand_crafted))

Теперь данные представляют собой 5000+5000+13 = 10013мерный вектор.

Модели

Логистическая регрессия

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

На приведенном выше рисунке ось x равна np.log10(C), а ось y равна MRR. В np.log10(C)=-2 МРР CV максимален, после этого МРР остается постоянным. Итак, C=0.01 — лучшее значение гиперпараметра.

Дерево решений

Дерево решений использует примеси Джинни или получение информации для разделения данных в каждом узле. В узле используется функция с максимальным приростом информации. Гиперпараметр дерева решений — max_dept . DecisionTreeClassifier sklearn используется для обучения модели. Деревья решений могут легко соответствовать данным поезда, если используется высокое значение max_depth.

Из приведенного выше графика видно, что по мере увеличения max_depth тренировочный MRR быстро увеличивается, но CV MRR увеличивается, но не с той же скоростью, что и тренировочный MRR. max_depth=30 используется как лучший гиперпараметр. Даже при max_depth=30 модель все еще переоснащается, но ниже этой глубины производительность модели дерева решений ниже, чем у случайной модели.

Дерево принятия решений по повышению градиента (GBDT)

Это сборная модель. В GBDT каждый базовый ученик обучается на потере предыдущего базового ученика. Это очень мощная модель. Это уменьшило смещение, сохранив дисперсию почти постоянной. В GBDT используются очень поверхностные базовые обучающиеся. Базовые обучающиеся в GBDT представляют собой деревья решений с очень небольшой глубиной. GBDT легко переобучается, если используются базовые обучающиеся с большой глубиной. LGBMClassifier из lightgbm используется для обучения модели. Он имеет множество гиперпараметров, которые можно настроить. Но настраиваются только n_estimators и max_depth. Проверка показала, что max_depth=5 и n_estimators=1000 являются лучшим выбором из этих двух гиперпараметров.

Модели DL

В моделях DL используется 4 миллиона точек данных.

  • Первые 95% на обучение,
  • следующие 5% для проверки и
  • следующие 5% для тестирования.

Загрузка и разделение данных

Код к этому:

df = pd.read_csv("./text_processed.csv", low_memory= True)
df = df.head(4000000) #top 4 million datapoints
df_train = df.iloc[0:3800000, :] 
df_cv    = df.iloc[3800000:3900000, :]
df_test  = df.iloc[3900000:, :]
X_train, y_train = df_train[[col for col in df.columns if col != "label"]], df_train[["label"]]
X_cv, y_cv       = df_cv[[col for col in df.columns if col != "label"]], df_cv[["label"]]
X_test, y_test   = df_test[[col for col in df.columns if col != "label"]], df_test[["label"]]
print("Number of datapoints in train data: {:,}\n\
Number of datapoints in CV data: {:,}\n\
Number of datapoints in test data: {:,}".format(X_train.shape[0],
                                              X_cv.shape[0],
                                              X_test.shape[0]))
--------------------------------------------------------------------Number of datapoints in train data: 3,800,000
Number of datapoints in CV data: 100,000
Number of datapoints in test data: 100,000

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

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

Для встраивания слов используется предварительно обученная модель W2V, которая обучается на данных Википедии. Используется 200-мерная модель векторного представления. На этом сайте есть другие модели W2V.Модель, которая используется здесь, может быть загружена с здесь. После загрузки давайте загрузим эту модель. Словарь, в котором будут храниться эти данные. Ключами словаря будут слова, а значения словаря будут 200-мерными векторами. В этом словаре будут только те слова, которые есть в данных.

Перед обучением встроенной модели нам нужно преобразовать текстовые данные в последовательности. Каждая последовательность будет соответствовать слову, и, используя эту последовательность, создадим матрицу встраивания. keras Tokenizer используется для преобразования текста в последовательность.

tokens = Tokenizer()
tokens.fit_on_texts(list(df["question"]+ " "+ df["answer"]))

Максимальная длина вопроса и ответа в обучающих данных составляет 27 и 222 соответственно. Но выбрана максимальная длина вопроса и ответа 50 и 250.

def text_to_seq(texts, keras_tokenizer, max_len):
    """
    this function  return sequence of text after padding truncating
    """
    x = pad_sequences(keras_tokenizer.texts_to_sequences(texts), 
                      maxlen = max_len, padding = 'post',
                      truncating = 'post')
    return x

Ниже код преобразует текстовые данные в последовательности:

Вызовите вышеуказанную функцию следующим образом:

encoded_que_train,encoded_que_cv, \
encoded_que_test,encoded_ans_train,\
 encoded_ans_tcv, encoded_ans_test = return_sequnece_embed_matrix(
tokens, X_train,X_cv, X_test, max_length_que, max_length_ans)

Давайте создадим матрицу встраивания, используя модель W2V. Этот код вдохновлен machinelearningmastery.com.

file_name = './glove.6B.200d.txt'
vocab_size = len(tokens.word_index) + 1
# below array will be used in Embedding layer
embedding_matrix1 = np.zeros((vocab_size, 200), dtype = 'float32')
with open(file_name, 'r') as f:
    for line in f:
        values = line.split()
        word = values[0]
        vector = [float(i) for i in values[1:]]
        #index of word in our tokenizer
        word_index = tokens.word_index.get(word) 
        if word_index:
            embedding_matrix1[word_index] = vector

Теперь данные готовы для создания и обучения моделей DL. Обучены три модели DL.

Conv1D

Эта архитектура вдохновлена ​​этой бумагой. После встраивания вопроса и ответа пропустите его через отдельный слой Conv1D. Возьмите максимальный пул выходных данных Conv1D. Теперь возьмите скалярное произведение и передайте его выходному слою, имеющему одну сигмовидную единицу.

Количество обучаемых параметров этой архитектуры составляет 105 782.

Двунаправленный на базе ГРУ

Эта архитектура также вдохновлена ​​той же бумагой. В этой архитектуре вместо Conv1D используются двунаправленные блоки GRU. Максимальный пул также заменяется средним пулом.

Количество обучаемых параметров этой архитектуры составляет 92 982.

LSTM/CNN-внимание

Эта архитектура также вдохновлена ​​этой бумагой. Он использует как Conv1D, так и двунаправленный LSTM. Очень похожее внимание реализовано в TensorFlow tf.keras.layers.AdditiveAttention().

Количество обучаемых параметров этой архитектуры составляет 136 710.

БЕРТ

Для обучения модели BERT большинство идей кодирования берутся здесь, здесь и этот. Для обучения модели используется библиотека ktrain. Эта модель показала наилучшие результаты. Но обучение модели BERT стоит очень дорого, поэтому было взято 500 000 точек данных. 300к за обучение, 100к за резюме и 100к за тестирование.

Вывод

Производительность каждой модели описана в таблице ниже.

Из всех этих моделей модель BERT показала наилучшую производительность.

Будущая работа

Модели 1, 2 и 3 DL можно обучать с использованием 300-мерной или более многомерной модели W2V. Модель BERT можно обучить на большем количестве данных, и можно избежать переобучения в BERT.

использованная литература