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

Что такое БЕРТ?

BERT — это платформа машинного обучения с открытым исходным кодом для обработки естественного языка (NLP). BERT разработан, чтобы помочь компьютерам понять значение неоднозначного языка в тексте, используя окружающий текст для установления контекста.

BERT, что означает двунаправленные представления кодировщика от преобразователей, основан на преобразователях, модели глубокого обучения, в которой каждый выходной элемент связан с каждым входным элементом, а веса между ними вычисляются динамически на основе их соединения (в НЛП этот процесс называется внимание).

Мотивация для объяснимости BERT: Почему?

Позвольте мне сначала объяснить проблему непрофессионально, прежде чем углубляться в детали и бросать вам много технического жаргона и жаргона машинного обучения. Итак, проще говоря, если у нас есть текстовые столбцы в нашем наборе данных наряду с числовыми столбцами, и если мы хотим понять, как содержимое этого текстового столбца способствует нашим прогнозам (какие слова, биграммы, триграммы играют важную роль), то как мы можем сделать это? Наиболее распространенный подход, который мы используем для текстового столбца на этапе предварительной обработки, — либо удалить их, либо преобразовать в фиктивные переменные, либо выполнить какие-то математические преобразования (потому что наши модели любят числа!). Это может быть полезным подходом, но в большинстве случаев при этом мы теряем ценную информацию, которую можем получить из контекста языка и слов. Например, когда мы говорим о фильме «Спецэффекты», «Брэд Питт», «Анджелина Джоли» и так далее, могут быть очень важны слова и биграммы. Таким образом, вместо того, чтобы делать неуклюжие преобразования и полностью удалять текстовые столбцы, мы могли бы сделать что-то более полезное и продуктивное. Предположим, что если бы наша модель могла также понимать контекст текстовых столбцов в отношении того, что мы пытаемся предсказать, она могла бы творить чудеса и давать более точные результаты. Таким образом, мы можем использовать такие модели, как BERT, чтобы понять контекст наших текстовых столбцов, а изучение того, какой аспект языка изучает BERT, помогает проверить надежность нашей модели. Но все же есть небольшая проблема — как узнать, какие слова наша модель считает важными в наших текстовых столбцах, даже после использования сложных моделей, таких как BERT?

Многие модели AutoML имеют встроенный выбор функций и важность функций. Но проблема все еще остается — эти библиотеки/программное обеспечение/пакеты AutoML большую часть времени не могут объяснить важность функций (какие слова могли способствовать тому, чтобы сделать эту функцию важной), если они взяты из текстовых столбцов (если есть) входной набор данных. Если эти функции считаются важными в соответствии с конвейером выбора функций, они будут просто показаны конечному пользователю в списке важных функций с помощью программного обеспечения AutoML, например «‹column_name›_‹some_number›_‹feature number›».

В последние дни, когда AutoML (например, TPOT, H2O.ai, HyperOpt и т. д.) переживает бум, интерпретируемость моделей стала необходимостью часа.

Начиная с этого момента, я объясню остальную часть статьи, взяв набор данных IMDB в качестве входного набора данных, который имеет два текстовых столбца Отзывы и Настроения. Это можно скачать с https://www.kaggle.com/lakshmi25npathi/imdb-dataset-of-50k-movie-reviews.

Мы будем рассматривать «Отзывы» как функцию ввода, которая представляет собой не что иное, как текстовый столбец.

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

Немного справочной информации о том, что это за изображение, объясняющее и откуда оно было взято — это SHAPLY объяснение важности функции в наборе данных IMDB. Набор данных IMDB был взят в качестве входных данных, и после очистки и предварительной обработки столбцов он был передан в BERT (предварительно обученная модель bert-base-uncased) для понимания языкового контекста. Примечание для новых читателей (для которых BERT — неизвестная территория) состоит в том, что BERT будет предоставлять 768 выходных чисел (называемых векторами выходных признаков) для каждого введенного вами текста. Это просто означает, что для каждой из строк в вашем наборе данных каждый текстовый столбец будет иметь свои собственные 768 выходных данных (считайте, что это своего рода волшебное преобразование, в котором сохраняется весь языковой контекст). В нашем случае, поскольку в нашем наборе данных всего 2 столбца (отзывы и мнения), каждый из них будет иметь свои собственные 768 выходных чисел из BERT, и да, каждая строка будет иметь выходные числа 768x2. Проще говоря, BERT преобразует 2 текстовых столбца из нашего набора входных данных в числовые столбцы размером 768x2. После создания этих функций они передаются в H2O.ai autoML для расчета важности функций. Помните, что для H2O это всего лишь входные числовые функции 768x2. Однако для того, чтобы любая модель выполняла прогноз, ей нужны ввод и вывод, поэтому в нашем случае мы сделали «Настроения» в качестве вывода и «Отзывы» в качестве ввода (которые теперь составляют 768 числовых значений после прохождения через BERT). Чувства не были переданы в BERT, потому что их следует рассматривать как цель, которую мы собираемся предсказать или сделать что-то позже. Итак, изображение выше, которое вы видите, теперь является результатом важности функций топ-модели H2O (только один текстовый столбец преобразован в 768 числовых столбцов), объясненный SHAP. Я надеюсь, что это ясно до этого момента.

Предполагая, что конечный пользователь не является техническим специалистом, для него естественно не понимать значения этих производных функций (помните, в чем заключалась наша проблема?). Следовательно, эта объяснимость BERT поможет конечным пользователям интуитивно понять значение этих производных функций.

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

С чего начать?

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

Какие подходы мы могли бы использовать?

а. Подход термина документа

Мы можем использовать подход мешок слов для поиска слов, которые сильно коррелируют с заданным вектором признаков из BERT. Сначала мы можем объединить все векторы признаков (которые должны быть объяснены) с текстовыми столбцами (если набор данных слишком велик, мы берем образец набора данных или же используем весь набор данных). Затем мы создаем термин документа. матрица» из выборочного набора данных. Матрица терминов документа представляет собой двумерную матрицу, в которой все слова в нашем текстовом корпусе представлены в виде столбцов. Каждая строка представляет собой строку в выбранном наборе данных, в ней есть количество слов, появляющихся в этой строке. Ниже приведен пример матрицы терминов документа.

Мы можем вычислить матрицу терминов документа, удалить стоп-слова, применить минимальную частоту и вычислить униграммы и биграммы.

import pandas as pd
import re
import numpy as np
from tqdm import notebook, tqdm
from sklearn.feature_extraction.text import CountVectorizer
from xgboost import XGBRegressor
from tqdm import tqdm
import torch
def create_document_term_matrix(text_sub):  
 vec = CountVectorizer(min_df=3, stop_words=stop_words,     ngram_range=(1,2), max_features=10000)  
 X = vec.fit_transform(text_sub)  
 df = pd.DataFrame(X.toarray(), columns=vec.get_feature_names())  
 return df

Предоставление первых 4 строк набора данных IMDB в качестве входных данных ниже представляет собой матрицу терминов выходного документа из кода

ПРИМЕЧАНИЕ. В выводе будут игнорироваться все стоп-слова, если вы их указали.

Используя эту матрицу в качестве входных признаков X и признак, который нужно объяснить, как целевой вектор Y, мы можем передать их в XGBRegressor (или любую другую соответствующую модель, которая может придать важность каждому признаку). Таким образом, мы получаем важность каждого слова в словаре для этого конкретного целевого вектора Y (который является одним из выходных векторов признаков из BERT).

def compute_important_words(fv):     
 gpu_available = torch.cuda.is_available()
 if gpu_available:   
  model = XGBRegressor(tree_method="gpu_hist")  
 else:   
  model = XGBRegressor()  
model.fit(document_term_matrix, fv)  
xgb_importance = model.feature_importances_  
imp_word_indices = xgb_importance.argsort()[-20:][::-1]  
res = []  
for ind in imp_word_indices:             res.append((document_term_matrix.columns[ind],
 str(round(model.feature_importances_[ind], 4))))  
return res

Окончательный вывод кода

Вывод из XGBRegerssor в фактическом коде

Отсортировав словарный запас по важности, мы можем взять k лучших слов, чтобы объяснить этот вектор признаков из BERT. Затем мы обрабатываем обучение XGBRegressor с различными целевыми векторами Y.

word_groups = compute_important_words(df_feature_vector)

ПРИМЕЧАНИЕ. Матрица терминов документа создается только один раз, и XBGRegressor обучается снова и снова для каждого вектора признаков из BERT. Также обратите внимание, что XGBRegressor придает большее значение словам, которые повторяются чаще, т. е. имеют большое количество в матрице терминов документа. Хотя не всегда может быть правдой, что чем большее количество раз слово появляется, тем выше его важность, этот подход все же может быть справедливым для объяснения важных слов.

б. Подход важности слова

Метод атрибуции оценивает входные данные на основе прогнозов, которые делает модель, т. е. приписывает прогнозы своим входным сигналам или функциям, используя баллы для каждой функции. Интегрированные градиенты — один из таких методов. Грубо говоря, это равно (функция * градиент). Градиент — это сигнал, сообщающий нейронной сети, насколько увеличить или уменьшить определенный вес/коэффициент в сети при обратном распространении. Для этого он в значительной степени зависит от входных функций. Таким образом, градиент, связанный с каждой входной функцией по отношению к выходной, может помочь нам понять, насколько важна функция. Интегрированные градиенты позволяют нам связать выбранную выходную функцию из модели BERT с ее входными данными. Важность слов может быть сгенерирована для каждой из выходных функций следующим образом:

Это позволяет нам понять, какие слова привели к функции (которая является одним из векторов функций из BERT), на которую они смотрят.

ПРИМЕЧАНИЕ. Зеленый цвет означает, что токен положительно коррелирует с выходной функцией, красный — отрицательно. Каждый токен получает реальное числовое значение, которое может быть положительным или отрицательным.

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

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

import torch
import torch.nn as nn

from transformers import BertTokenizer, BertForQuestionAnswering, BertConfig

from captum.attr import visualization as viz
from captum.attr import LayerConductance, LayerIntegratedGradients
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# replace <PATH-TO-SAVED-MODEL> with the real path of the saved model
model_path = '<PATH-TO-SAVED-MODEL>'

# load model
model = BertModel.from_pretrained(model_path)
model.to(device)
model.eval()
model.zero_grad()

# load tokenizer
tokenizer = BertTokenizer.from_pretrained(model_path)
ref_token_id = tokenizer.pad_token_id # A token used for generating token reference
sep_token_id = tokenizer.sep_token_id # A token used as a separator between question and text and it is also added to the end of the text.
cls_token_id = tokenizer.cls_token_id # A token used for prepending to the concatenated question-text word sequence

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

def predict(inputs, token_type_ids=None, position_ids=None, attention_mask=None):
    output = model(inputs, token_type_ids=token_type_ids,
                 position_ids=position_ids, attention_mask=attention_mask, )
    return output.start_logits, output.end_logits

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

def squad_pos_forward_func(inputs, token_type_ids=None, position_ids=None, attention_mask=None, position=0):
    pred = predict(inputs,
                   token_type_ids=token_type_ids,
                   position_ids=position_ids,
                   attention_mask=attention_mask)
    pred = pred[position]
    return pred.max(1).values

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

def predict(inputs, token_type_ids=None, position_ids=None, attention_mask=None):
    output = model(inputs, token_type_ids=token_type_ids,
                 position_ids=position_ids, attention_mask=attention_mask, )
    return output.start_logits, output.end_logits
def compute_attributions():
lig = LayerIntegratedGradients(squad_pos_forward_func, model.bert.embeddings)

attributions_start, delta_start = lig.attribute(inputs=input_ids,
                                  baselines=ref_input_ids,
                                  additional_forward_args=(token_type_ids, position_ids, attention_mask, 0),
                                  return_convergence_delta=True)
attributions_end, delta_end = lig.attribute(inputs=input_ids, baselines=ref_input_ids,
                                additional_forward_args=(token_type_ids, position_ids, attention_mask, 1),
                                return_convergence_delta=True)
attributions_start_sum = summarize_attributions(attributions_start)
attributions_end_sum = summarize_attributions(attributions_end)

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

def construct_input_ref_pair(text, ref_token_id, sep_token_id, cls_token_id):
    
    text_ids = tokenizer.encode(text, add_special_tokens=False)

    # construct input token ids
    input_ids = [cls_token_id] + [sep_token_id] + text_ids + [sep_token_id]

    # construct reference token ids 
    ref_input_ids = [cls_token_id] + [sep_token_id] + \
        [ref_token_id] * len(text_ids) + [sep_token_id]

    return torch.tensor([input_ids], device=device), torch.tensor([ref_input_ids], device=device)

Теперь, когда мы рассчитали атрибуции, мы можем суммировать атрибуции для каждого токена слова в последовательности и получить важные тексты для отображения нашим конечным пользователям. Валла!!

def summarize_attributions(attributions):
    attributions = attributions.sum(dim=-1).squeeze(0)
    attributions = attributions / torch.norm(attributions)
    return attributions

Окончательный вывод кода

Ниже представлен окончательный результат подхода IG для вычисления важных слов, которые BERT мог посчитать важными при вычислении одного из целевых выходных векторов (#551 в нашем примере), которые H2O сочла важными в целом. для нашего набора данных.

Как мы видим, этот подход намного лучше, чем подход матрицы терминов документа, поскольку вывод важных слов имеет больше смысла, чем предыдущий.

ПРИМЕЧАНИЕ. Поскольку этот подход использует обратное распространение для вычисления важных слов, этот подход требует больших вычислительных ресурсов. Потребовалось почти 30 минут, чтобы объяснить важные слова всего для 500 строк, и это при мощном графическом процессоре. Представьте, сколько времени потребуется, чтобы объяснить более десятков тысяч строк в вашем наборе данных. Если мы сможем найти способ ускорить его работу, это может быть НАСТОЯЩИМ подходом.

Заключение

В этой статье мы увидели, как мы можем использовать подход Document Term и Integrated Gradients для достижения объяснимости BERT и решения проблемы, когда программное обеспечение AutoML не может полностью объяснить важность функций, если функции происходят из текстов во входном наборе данных. Хотя эти два подхода работают хорошо, их можно улучшить, чтобы улучшить качество важных слов, чтобы они имели больше смысла для конечного пользователя, когда они видят, что это слова, которые действительно оказали положительное или отрицательное воздействие. на их предсказания.

Рекомендации

Блокнот Ссылки для прохождения кода