В этом блоге обсуждается, как быстро настроить Huggingface DistilBert для ответов на вопросы на основе данных Уголовного кодекса Индии (IPC).

Начиная с 2017 года, произошел сдвиг парадигмы в понимании языка и создании задач для машинного обучения благодаря успеху моделей на основе Transformers и Bert. В сочетании с доказанной эффективностью трансферного обучения для задач, связанных с языком, эти архитектуры стали стандартом как в промышленности, так и в исследованиях, с приложениями в разговорном ИИ, ответах на вопросы, классификации текстов и многом другом. В этом блоге рассказывается и шаг за шагом показывается, как выполнять точную настройку (перенос обучения) в пользовательском наборе данных, который я подготовил самостоятельно и который состоит из вопросов и ответов из всех глав Уголовного кодекса Индии (IPC). Мы собираемся использовать HuggingFace Transformers для этой задачи и, в частности, модель DistilBertForQuestionAnswering. В конце мы увидим, как модель способна улавливать и понимать терминологию из МПК и, следовательно, правильно и точно отвечать на вопросы.

Весь код и набор данных можно найти в моем репозитории github

Набор данных и формат ввода

Набор данных, который я подготовил, взят из Уголовного кодекса Индии. Вы можете найти его где угодно в Google, просто введите «IPC все главы» или что-то подобное. Единственное, что вам нужно убедиться, это то, что для задач тонкой настройки эти модели (Дистильберт или Берт) ожидают данные в определенном формате (по крайней мере, при использовании трансформеров Hugging Face). Задача состоит в том, чтобы преобразовать любые данные, которые мы получаем из Интернета (в формате pdf/html/json), в формат, подобный json, с полями для текста, вопроса, ответа, start_position, end_position и т. д. Для этого я использовал CDQA( Ответы на закрытые вопросы) аннотатор, классный инструмент аннотации на основе пользовательского интерфейса. Вам просто нужно открыть файл IPC json в пользовательском интерфейсе CQDA и вручную ввести вопросы, а затем выбрать ответ из текста. CDQA позаботится обо всем остальном и предоставит набор данных json с начальной и конечной позициями ответа, вопросами, текстом и т. д. Для этого проекта мы можем пропустить эту часть, так как я уже сделал аннотацию и файл доступен. в репозитории GitHub. Аннотированный текст будет выглядеть примерно так:

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

Задача ответа на вопрос для моделей на основе BERT заключается в следующем:

Учитывая контекст и вопрос, найдите ответ в контексте, где максимальная длина вопроса + контекст составляет 512 токенов.

Две вещи здесь:

  1. Максимальная длина ограничена 512 из-за внимания. Внимание в языковых моделях квадратично масштабируется с длиной входных данных, потому что каждый токен имеет вес, вычисляемый вместе с каждым другим токеном. Следовательно, большинство моделей на основе BERT/Transformer усекают текст за пределами 512 токенов. Хотя в последнее время такие модели, как Longformer, представили скользящее окно внимания, которое помогает выдавать до 4096 токенов в качестве входных данных. Подробнее об этом в другом блоге.
  2. Мы должны дать контекст здесь. То есть модель должна знать, что есть некий текстовый фрагмент (в нашем случае раздел МПК), из которого будет найден ответ. Это не очень практично в задачах поиска информации или ответов на вопросы. Здесь мы можем использовать такие методы, как TF-IDF, Графики знаний, чтобы сначала отфильтровать наиболее релевантные документы/разделы, а затем использовать преобразователи для этих документов, чтобы получить точные результаты.

Теперь, когда мы поняли, как выглядят размеченные данные, давайте кратко определим тонкую настройку с помощью DistilBertForQuestionAnswering.

Наша первая задача — загрузить модель и токенизатор из трансформеров Huggingface. Мы вызываем метод from_pretrained(), который принимает в качестве параметра тип модели («distilbert-base-uncased»). Tokenizer используется для разбиения текста на основе лексики distilbert. Модель будет нашей предварительно обученной моделью DistilBert, специально предназначенной для последующей задачи контроля качества.

from transformers import DistilBertTokenizerFast,DistilBertForQuestionAnswering
model_db = DistilBertForQuestionAnswering.from_pretrained("distilbert-base-uncased")
tokenizer = DistilBertTokenizerFast.from_pretrained('distilbert-base-uncased')

Следует отметить, что эта модель может быть непосредственно использована для ответов на вопросы. Однако, поскольку эта модель не была специально обучена на юридических данных, она будет работать намного лучше, как только мы ее настроим. Мы делаем разделение 80–20 для обучения и тестирования, с 294 и 74 парами обучения и тестирования QA соответственно (поскольку не в каждом разделе есть хорошие кандидаты на ответы на вопросы).

Далее необходимо создать кодировки из данных и создать объект класса набора данных, чтобы передать данные обучения/тестирования для точной настройки. Поскольку модели не могут понимать текст, нам нужно заменить каждое слово/токен числами, и здесь в дело вступает кодировка. Каждый токен заменяется соответствующим номером в словарном файле модели. Наряду с этим кодированием мы также добавляем отступы к текстам, поэтому все входные данные имеют постоянную длину 512. Наконец, также добавляется маска внимания, которая представляет собой просто список/тензор из 1 и 0, показывающий, как долго является ввод и сколько слов включено в расчет собственного внимания для текущего ввода.

train_encodings = tokenizer(train_contexts, train_questions, truncation=True, padding=True)
val_encodings = tokenizer(val_contexts, val_questions, truncation=True, padding=True)
class IpcDataset(torch.utils.data.Dataset):
    def __init__(self, encodings):
        self.encodings = encodings

    def __getitem__(self, idx):
        return {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}

    def __len__(self):
        return len(self.encodings.input_ids)

train_dataset = IpcDataset(train_encodings)
val_dataset = IpcDataset(val_encodings)

После того, как у нас есть наборы данных, следующим делом будет тонкая настройка модели. Мы используем оптимизатор Adam, размер партии 16 и простой цикл обучения pytorch, как показано ниже:

for epoch in range(10):
    for x,batch in enumerate(train_loader):
        optim.zero_grad()
        input_ids = batch['input_ids'].to(device)
        attention_mask = batch['attention_mask'].to(device)
        start_positions = batch['start_positions'].to(device)
        end_positions = batch['end_positions'].to(device)
        outputs = model_db(input_ids, attention_mask=attention_mask, start_positions=start_positions, end_positions=end_positions)
        loss = outputs[0]
        loss.backward()
        optim.step()
        if x == 10:
            print("batch: "+str(x)+" loss: "+str(loss))

Вывод нашей модели

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

Вопрос: наказание за попытку кражи?

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

Ответ (человек): лишение свободы на срок до десяти лет, а также подлежит штрафу.

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

encoding = tokenizer.encode_plus(text=question,text_pair=paragraph)
input_ids, attention_mask = encoding["input_ids"], encoding["attention_mask"]
###### shift all tensors to device
ip=torch.tensor([input_ids]).to(device)
attention=torch.tensor([attention_mask]).to(device)
###### run inferencing
output = model_db(ip, attention)
##### model outputs start and end scores 
start_scores = output.start_logits
end_scores = output.end_logits
##### argmax gives the positions with max start and end scores, ##### these will be out starting and ending indices for predicted ##### answer
max_startscore = torch.argmax(start_scores)
max_endscore = torch.argmax(end_scores)
ans_tokens = input_ids[max_startscore: max_endscore + 1]
# answer = ' '.join(tokens[start_index:end_index+1])
answer = ' '.join(tokens[max_startscore:max_endscore+1])
print(answer)

Ответ (образец): тюремное заключение строгого режима на срок до десяти лет , а также наложение штрафа .

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

Вот и все! Точно настроить DistilBert на наших пользовательских наборах данных очень просто. Закомментированный код для этого блога можно найти на мой github. Спасибо за чтение и дайте мне знать, если у вас есть какие-либо вопросы/отзывы :))