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

В этой статье мы будем вместе работать над одной из таких часто используемых задач - ответами на вопросы. Мы будем использовать уже доступную отлаженную модель BERT из библиотеки Hugging Face Transformers, чтобы отвечать на вопросы, основанные на историях из набора данных CoQA. Я уверен, что, взглянув на код, вы поймете, насколько легко использовать отлаженную модель для нашей цели. 😁

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

Версии, использованные в статье: фонарик - 1.7.1, трансформаторы - 4.4.2.

Давайте сначала ответим на несколько важных вопросов, связанных с этой статьей.

Что такое "Обнимающее лицо" и "Трансформеры"? 🤔

Hugging Face - поставщик технологий обработки естественного языка (NLP) с открытым исходным кодом. Вы можете использовать современные модели с обнимающимися лицами для создания, обучения и развертывания собственных моделей. Трансформеры - их библиотека НЛП. Я настоятельно рекомендую вам ознакомиться с потрясающей работой, проделанной командой Hugging Face, и их огромной коллекцией предварительно обученных моделей НЛП.

Что такое CoQA? 🤔

CoQA - это набор данных с ответами на разговорные вопросы, выпущенный Stanford NLP в 2019 году. Это крупномасштабный набор данных для создания систем ответов на разговорные вопросы. Этот набор данных предназначен для измерения способности машин понимать отрывок текста и отвечать на ряд взаимосвязанных вопросов, которые появляются в разговоре. Уникальной особенностью этого набора данных является то, что каждый разговор собирается путем объединения двух рабочих толпы, чтобы обсудить отрывок в форме вопросов и ответов, и, следовательно, вопросы являются диалоговыми. Чтобы понять формат данных JSON, перейдите по этой ссылке. Мы будем использовать историю, вопрос и ответ из набора данных JSON для формирования нашего фрейма данных.

Что такое BERT? 🤔

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

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

Итак, давайте начнем, но давайте сначала посмотрим на наш набор данных. 😊

В данных JSON много полей. Для наших целей мы будем использовать «рассказ», «input_text» из «вопросов» и «ответов» и сформируем наш фрейм данных.

Установка трансформаторов

!pip install transformers

Импорт библиотек

import pandas as pd
import numpy as np
import torch
from transformers import BertForQuestionAnswering
from transformers import BertTokenizer

Загрузка данных с веб-сайта Стэнфорда

coqa = pd.read_json('http://downloads.cs.stanford.edu/nlp/data/coqa/coqa-train-v1.0.json')
coqa.head()

Очистка данных

Мы будем иметь дело со столбцом «данные», поэтому давайте просто удалим столбец «версия».

del coqa["version"]

К каждой паре вопрос-ответ мы будем прикреплять связанную историю.

#required columns in our dataframe
cols = ["text","question","answer"]
#list of lists to create our dataframe
comp_list = []
for index, row in coqa.iterrows():
    for i in range(len(row["data"]["questions"])):
        temp_list = []
        temp_list.append(row["data"]["story"])
        temp_list.append(row["data"]["questions"][i]["input_text"])
        temp_list.append(row["data"]["answers"][i]["input_text"])
        comp_list.append(temp_list)
new_df = pd.DataFrame(comp_list, columns=cols) 
#saving the dataframe to csv file for further loading
new_df.to_csv("CoQA_data.csv", index=False)

Загрузка данных из локального CSV-файла

data = pd.read_csv("CoQA_data.csv")
data.head()

Вот наша очищенная версия данных.

print("Number of question and answers: ", len(data))

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

Number of question and answers:  108647

Создание чат-бота

Лучшее в использовании этих предварительно обученных моделей заключается в том, что вы можете загрузить модель и ее токенизатор всего двумя простыми строчками кода. 😲 Разве это не просто вау? Для таких задач, как классификация текста, нам нужно точно настроить BERT в нашем наборе данных. Но для задач с ответами на вопросы мы даже можем использовать уже обученную модель и получать достойные результаты, даже если наш текст взят из совершенно другой области. Чтобы получить достойные результаты, мы используем модель BERT, которая настроена на тесте SQuAD.

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

model = BertForQuestionAnswering.from_pretrained('bert-large-uncased-whole-word-masking-finetuned-squad')
tokenizer = BertTokenizer.from_pretrained('bert-large-uncased-whole-word-masking-finetuned-squad')

Ожидайте, что загрузка займет пару минут, так как BERT-large - это действительно большая модель с 24 слоями и параметрами 340M, что делает ее моделью 1,34 ГБ.

Задать вопрос

Давайте случайным образом выберем номер вопроса.

random_num = np.random.randint(0,len(data))
question = data["question"][random_num]
text = data["text"][random_num]

Давайте токенизируем вопрос и текст как пару.

input_ids = tokenizer.encode(question, text)
print("The input has a total of {} tokens.".format(len(input_ids)))

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

The input has a total of 427 tokens.

Чтобы посмотреть, что делает наш токенизатор, давайте просто распечатаем токены и их идентификаторы.

tokens = tokenizer.convert_ids_to_tokens(input_ids)
for token, id in zip(tokens, input_ids):
    print('{:8}{:8,}'.format(token,id))

BERT имеет уникальный способ обработки токенизированных входных данных. На скриншоте выше мы видим два специальных токена [CLS] и [SEP]. Токен [CLS] обозначает классификацию и предназначен для представления классификации на уровне предложения и используется при классификации. Другой токен, используемый BERT, - это [SEP]. Он используется для разделения двух частей текста. На скриншотах выше вы можете увидеть два жетона [SEP], один после вопроса, а другой - после текста.

Помимо «встраивания токенов», BERT внутренне также использует «встраивания сегментов» и «встраивание позиций». Встраивание сегментов помогает BERT отличить вопрос от текста. На практике мы используем вектор 0, если вложения из предложения 1, иначе вектор из 1, если вложения из предложения 2. Вложения позиций помогают в указании позиции слов в последовательности. Все эти вложения поступают на входной уровень.

Библиотека Transformers может создавать вложения сегментов самостоятельно с помощью PretrainedTokenizer.encode_plus (). Но мы можем даже создать свои собственные. Для этого нам просто нужно указать 0 или 1 для каждого токена.

#first occurence of [SEP] token
sep_idx = input_ids.index(tokenizer.sep_token_id)
print("SEP token index: ", sep_idx)
#number of tokens in segment A (question) - this will be one more than the sep_idx as the index in Python starts from 0
num_seg_a = sep_idx+1
print("Number of tokens in segment A: ", num_seg_a)
#number of tokens in segment B (text)
num_seg_b = len(input_ids) - num_seg_a
print("Number of tokens in segment B: ", num_seg_b)
#creating the segment ids
segment_ids = [0]*num_seg_a + [1]*num_seg_b
#making sure that every input token has a segment id
assert len(segment_ids) == len(input_ids)

Вот результат.

SEP token index: 8
Number of tokens in segment A: 9
Number of tokens in segment B: 418

Давайте теперь скормим это нашей модели.

#token input_ids to represent the input and token segment_ids to differentiate our segments - question and text
output = model(torch.tensor([input_ids]),  token_type_ids=torch.tensor([segment_ids]))

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

#tokens with highest start and end scores
answer_start = torch.argmax(output.start_logits)
answer_end = torch.argmax(output.end_logits)
if answer_end >= answer_start:
    answer = " ".join(tokens[answer_start:answer_end+1])
else:
    print("I am unable to find the answer to this question. Can you please ask another question?")
    
print("\nQuestion:\n{}".format(question.capitalize()))
print("\nAnswer:\n{}.".format(answer.capitalize()))

Вот наш вопрос и ответ на него.

Question:
Who is the acas director?

Answer:
Agnes karin ##gu.

Ух ты! БЕРТ предсказал правильный ответ - «Агнес Карингу». Но что это за «##» в ответе? Продолжайте читать! 📙

BERT использует токенизацию словесных слов. В BERT редкие слова разбиваются на подслова / части. При токенизации Wordpiece используется ## для разграничения разделенных токенов. Пример этого: «Карин» - обычное слово, поэтому словесное слово не разбивает его. Однако «карингу» - редкое слово, поэтому словоупотребление разбило его на слова «Карин» и «## гу». Обратите внимание, что перед gu добавлен символ ##, чтобы указать, что это вторая часть разбитого слова.

Идея использования токенизации словосочетания состоит в том, чтобы уменьшить размер словарного запаса, что улучшает эффективность обучения. Обдумайте слова «беги», «беги», «бегун». Без токенизации словаря модель должна независимо хранить и изучать значение всех трех слов. Однако при токенизации словесных фрагментов каждое из трех слов будет разделено на «run» и соответствующее «## SUFFIX» (если вообще есть суффикс - например, «run», «## ning», «## ner »). Теперь модель узнает контекст слова «бегать», а остальное значение будет закодировано в суффиксе, который можно узнать из других слов с аналогичными суффиксами.

Интересно, правда? Мы можем восстановить эти слова, используя следующий простой код.

answer = tokens[answer_start]
for i in range(answer_start+1, answer_end+1):
    if tokens[i][0:2] == "##":
        answer += tokens[i][2:]
    else:
        answer += " " + tokens[i]

Приведенный выше ответ теперь будет выглядеть следующим образом: Агнес карингу.

Давайте теперь для простоты превратим этот процесс ответов на вопросы в функцию.

def question_answer(question, text):
    
    #tokenize question and text as a pair
    input_ids = tokenizer.encode(question, text)
    
    #string version of tokenized ids
    tokens = tokenizer.convert_ids_to_tokens(input_ids)
    
    #segment IDs
    #first occurence of [SEP] token
    sep_idx = input_ids.index(tokenizer.sep_token_id)
    #number of tokens in segment A (question)
    num_seg_a = sep_idx+1
    #number of tokens in segment B (text)
    num_seg_b = len(input_ids) - num_seg_a
    
    #list of 0s and 1s for segment embeddings
    segment_ids = [0]*num_seg_a + [1]*num_seg_b
    assert len(segment_ids) == len(input_ids)
    
    #model output using input_ids and segment_ids
    output = model(torch.tensor([input_ids]), token_type_ids=torch.tensor([segment_ids]))
    
    #reconstructing the answer
    answer_start = torch.argmax(output.start_logits)
    answer_end = torch.argmax(output.end_logits)
    if answer_end >= answer_start:
        answer = tokens[answer_start]
        for i in range(answer_start+1, answer_end+1):
            if tokens[i][0:2] == "##":
                answer += tokens[i][2:]
            else:
                answer += " " + tokens[i]
                
    if answer.startswith("[CLS]"):
        answer = "Unable to find the answer to your question."
    
    print("\nPredicted answer:\n{}".format(answer.capitalize()))

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

text = """New York (CNN) -- More than 80 Michael Jackson collectibles -- including the late pop star's famous rhinestone-studded glove from a 1983 performance -- were auctioned off Saturday, reaping a total $2 million. Profits from the auction at the Hard Rock Cafe in New York's Times Square crushed pre-sale expectations of only $120,000 in sales. The highly prized memorabilia, which included items spanning the many stages of Jackson's career, came from more than 30 fans, associates and family members, who contacted Julien's Auctions to sell their gifts and mementos of the singer. Jackson's flashy glove was the big-ticket item of the night, fetching $420,000 from a buyer in Hong Kong, China. Jackson wore the glove at a 1983 performance during \"Motown 25,\" an NBC special where he debuted his revolutionary moonwalk. Fellow Motown star Walter \"Clyde\" Orange of the Commodores, who also performed in the special 26 years ago, said he asked for Jackson's autograph at the time, but Jackson gave him the glove instead. "The legacy that [Jackson] left behind is bigger than life for me,\" Orange said. \"I hope that through that glove people can see what he was trying to say in his music and what he said in his music.\" Orange said he plans to give a portion of the proceeds to charity. Hoffman Ma, who bought the glove on behalf of Ponte 16 Resort in Macau, paid a 25 percent buyer's premium, which was tacked onto all final sales over $50,000. Winners of items less than $50,000 paid a 20 percent premium."""
question = "Where was the Auction held?"
question_answer(question, text)
#original answer from the dataset
print("Original answer:\n", data.loc[data["question"] == question]["answer"].values[0]))

Выход:

Predicted answer:
Hard rock cafe in new york ' s times square
Original answer:
Hard Rock Cafe

Совсем неплохо. Фактически, наша модель BERT дала более развернутый ответ.

Вот небольшая функция, чтобы проверить, насколько хорошо BERT понимает контексты. Я просто сделал процесс ответа на вопрос в виде цикла, чтобы поиграть с моделью. 💃

text = input("Please enter your text: \n")
question = input("\nPlease enter your question: \n")
while True:
    question_answer(question, text)
    
    flag = True
    flag_N = False
    
    while flag:
        response = input("\nDo you want to ask another question based on this text (Y/N)? ")
        if response[0] == "Y":
            question = input("\nPlease enter your question: \n")
            flag = False
        elif response[0] == "N":
            print("\nBye!")
            flag = False
            flag_N = True
            
    if flag_N == True:
        break

И, результат! 😎

Please enter your text: 
The Vatican Apostolic Library (), more commonly called the Vatican Library or simply the Vat, is the library of the Holy See, located in Vatican City. Formally established in 1475, although it is much older, it is one of the oldest libraries in the world and contains one of the most significant collections of historical texts. It has 75,000 codices from throughout history, as well as 1.1 million printed books, which include some 8,500 incunabula.   The Vatican Library is a research library for history, law, philosophy, science and theology. The Vatican Library is open to anyone who can document their qualifications and research needs. Photocopies for private study of pages from books published between 1801 and 1990 can be requested in person or by mail.   In March 2014, the Vatican Library began an initial four-year project of digitising its collection of manuscripts, to be made available online.   The Vatican Secret Archives were separated from the library at the beginning of the 17th century; they contain another 150,000 items.   Scholars have traditionally divided the history of the library into five periods, Pre-Lateran, Lateran, Avignon, Pre-Vatican and Vatican.   The Pre-Lateran period, comprising the initial days of the library, dated from the earliest days of the Church. Only a handful of volumes survive from this period, though some are very significant.

Please enter your question: 
When was the Vat formally opened?

Answer:
1475

Do you want to ask another question based on this text (Y/N)? Y

Please enter your question: 
what is the library for?

Answer:
Research library for history , law , philosophy , science and theology

Do you want to ask another question based on this text (Y/N)? Y

Please enter your question: 
for what subjects?

Answer:
History , law , philosophy , science and theology
Do you want to ask another question based on this text (Y/N)? N

Bye!

Вуаля! Отлично работает! 🤗

Я надеюсь, что эта статья дала вам представление о том, как мы можем легко использовать предварительно обученную модель из библиотеки Hugging Face Transformer и выполнить нашу задачу. Если вы хотите посмотреть код как файл записной книжки, вот ссылка Github.

Ссылки:

  1. Https://huggingface.co/
  2. Https://arxiv.org/pdf/1810.04805.pdf
  3. Https://arxiv.org/pdf/1808.07042.pdf
  4. Https://github.com/google-research/bert/issues/44

Всем спасибо, что прочитали эту статью. Поделитесь своими ценными отзывами или предложениями. Приятного чтения! 📗 🖌