ML: Attention - Модель BERT


Введение

После того как, основанная на механизме внимания, архитектура Трансформера показала свою эффективность, её отдельные части получили самостоятельное существование. Сначала Open AI разработал сеть под названием Generative Pre-trained Transformer (GPT), которая использовала модифицированый декодер трансформера. Затем Google создал Bidirectional Encoder Representations from Transformers (BERT), используя его энкодер.

По-мимо трансформера, новые модели объединяет стратегия обучения на большом корпусе неразмеченных текстов.
GPT предсказывает очередное слово текста, а BERT - "закрытые" слова внутри предложения. В результате такого обучения формируется языковая модель, включающая в себя грамматику, семантику и даже определённые знания. После предварительного обучения производится тонкая настройка параметров модели под конкретную задачу уже на размеченных данных.


Архитектура

Сеть BERT является энкодером трансформера. Механизм внимания для каждого слова использует контекст всего текста (влево и вправо от слова). Для задач типа "Question Answering", текст, поступающий на вход, состоит из двух "предложений". Поэтому при обучении BERT, текст также разбивается на две последовательно идущие части, которые разделяются служебным токеном <SEP>. Весь текст начинается с ещё одного служебного токена <CLS>, выход C которого, служит для классификационных задач типа "Sentiment Analysis".

К обычному эмбедингу слова добавляется эмбединг его номера в данном предложении и эмбединг номера предложения (ниже второй рисунок). Все три эмбединга имеют одинаковые размерности, но различные словари. Например, словарь положений слов - это набор чисел 0,1,...,511 каждому из которых ставится в соответствие свой E-мерный вектор эмбединга.

Предварительная тренировка на неразмеченных данных состоит их двух этапов. На первом этапе строиться "маскированная языковая модель" (Masked LM). Для этого 15% входных слов заменяются на служебный токен <MASK> и сеть учится эти слова восстанавливать (ошибка вычисляется только по маскированным словам). При тонкой настройке токена <MASK> уже не будет. Чтобы смягчить этот факт, процедура обучения выглядит следующим образом: отбираются 15% слов текста, 80% их маскируется, 10% остаётся неизменными и 10% заменяются на случайное слово.

Второй этап предварительной тренировки языковой модели учит предсказывать следующее предложение: Next Sentence Prediction (NSP). Для этого в обучающем корпусе берётся предложение A и в 50% следующее после него предложение B. Этот случай помечается классом IsNext. В остальных 50% примерах предложение B является случайным, что помечается как NotNext. Выход C первого служебного токена <CLS> служит для распознавания одного из этих классов.

Авторы выложили в открытый доступ две модели (размерность полносвязного слоя в обоих равна 4*E, число голов - 64 ):

Модель Emb: E Heads: H Layers: L Params
BERT-BASE 768 12 12 110M
BERT-LARGE 1024 16 24 340M
Для сравнения в Трансформере было E,H,L = 512, 8, 6. В качестве обучающих корпусов BERT использовал BooksCorpus (800M слов) и English Wikipedia (2,500M слов).


Эмбединг позиции слова

Рассмотрим свойства векторов эмбединга положения слова. Нулевое значение отведено для токена <CLS>, а слова нумеруются последовательно, начиная с единицы. Ниже приведены косинусные расстояния $1-\cos(\mathbf{v},\mathbf{u})$ ближайших соседей к векторам положений с номерами 1,10,20,...100. Им (самим с собой) соответствует нулевое значение (минимумы графика):

Как и должно быть, ближайшими соседями данного слова всегда являются соседние слова (предшествующее и следующее). Впрочем, расстояния к соседям достаточно быстро растут.

Вектор положения токена <CLS> "равноудалён" от токенов положений слов (единичное косинусное расстояние соответствует перпендикулярным векторам):


Тонкая настройка

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

Ниже на первом рисунке (a) приведен пример тонкой настройки классификационной задачи для двух предложений S1 и S2. Например в задаче "Вывод на естественном языке" (Natural Language Inference): S1 => S2 есть три класса: (следует, противоречит, нейтрально). Аналогично настраиваются классификационные задачи с одним предложением (на втором рисунке b). Например, в задаче "Анализа настроений" (Sentiment Analysis) необходимо определить позитивность или негативность отзыва (одно "предложение" и два класса). Естественно, "предложение" реально может состоять из нескольких предложений языка.

При классификации выходной токен $\mathbf{C}$ размерности E умножается на матрицу $\mathbf{W}$ формы (E,K), где K - число классов и далее вычисляется стандартная классификационная ошибка, т.е. $\log (\text{softmax} (\mathbf{C}\cdot\mathbf{W})$. При этом тонкой настройке подвергаются только (?) параметры матрицы $\mathbf{W}$.

Третий пример (c) связан с задачей "Ответа на вопрос" (Question Answering). Например в SQuAD v.1.1 приводится абзац текст, затем следует вопрос, ответ на который является куском входного текста. В BERT вопрос оформляется как предложение A, а текст в котором надо найти ответ, как предложение B. Единственными обучаемыми параметрами являются два вектора $\mathbf{S}$ и $\mathbf{E}$ размерности $E$. Вероятность $P_i$ того, что слово $\mathbf{T}_i$ текста является началом ответа вычисляется по формуле: $P_i = \text{softmax}(\mathbf{S}\mathbf{T}_i)$ и аналогично $P_j = \text{softmax}(\mathbf{E}\mathbf{T}_j)$ равна вероятности того, что токен $\mathbf{T}_j$ является концом ответа. При обучении, как обычно, максимизируется логарифм этих вероятностей, а при тестировании берётся максимальное значение их суммы для $j > i$.

Четвёртый пример (d) относится к задаче CoNLL-2003 распознавания имен людей (PER), организаций (ORG), и географических названий (LOC) в тексте (тег O - слово вне именованных сущностей).


Токенизация

В BERT используется WordPiece токенизация (Wu et.al. 2016) со словарём в 30'000 токенов. Если текущее слово присутствует в словаре, оно остаётся без изменений. Слова, которых нет в словаре, разбиваются на части при помощи предварительно обученной модели. К частям добавляются специальные символы так, чтобы обратное декодирование было однозначным.

Токенизатор BERT доступен в библиотеке transformers, содержащей множество обученных моделей обработки естественного языка:

from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
print(tokenizer.tokenize("looked got parents healthy unhealthy tokenizing"))  

#['looked', 'got', 'parents', 'healthy', 'un', '##hea', '##lth', '##y', 'token', '##izing']
Выше в примере слов unhealthy, tokenizing в словаре нет и они разбиты на части.

Словарь модели находится в атрибуте vocab (его начало забито зарезервированными токенами и иероглифами):

", ".join(list(tokenizer.vocab)[1986:2087])

!, (, ), ,, -, ., /, :, ?, ~, the, of, and, in, to, was, he, is, as, for, on, with, that, it, his, by, at, from, her, ##s, she, you, had, an, were, but, be, this, are, not, my, they, one, which, or, have, him, me, first, all, also, their, has, up, who, out, been, when, after, there, into, new, two, its, ##a, time, would, no, what, about, said, we, over, then, other, so, more, ##e, can, if, like, back, them, only, some, could, ##i, where, just, ##ing, during, before, ##n, do, ##o, made, school, through, than, now, years

vocab = tokenizer.get_vocab()                                                # token to id
for w in "[PAD] [CLS] [SEP] [MASK] the help scandals".split():
    print(vocab[w], end=", ")                                                # id токенов
    
# 0, 101, 102, 103, 1996, 2393, 29609, 


Распознавание маски

Посмотрим как BERT справляется с угадыванием слова, "закрытого" тегом [MASK]. Для этого в библиотеке transformers воспользуемся высокоуровневым pipeline, которому в первом параметре укажем решаемую задачу, а в параметре model - имя модели (базовый BERT, нечувствительный к регистру):

from transformers import pipeline
nlp = pipeline('fill-mask', model='bert-base-uncased')

res = nlp("Tom shot Ann and put the gun away. She [MASK].")

for r in res:
    st = r['token_str']
    if st[0] == 'Ġ': st = st[1:]
    print(f"{st}({r['score']:.3f})", end=", ")

Приведём несколько примеров, указывая предсказываемое слово и его "вероятность". В силу используемых корпусов (интернет), модель обладает определённым сексизмом:

The man worked as a [MASK].    => carpenter(0.097), waiter(0.052), barber(0.050), mechanic(0.038), salesman(0.038), 
The woman worked as a [MASK].  => nurse(0.220), waitress(0.160), maid(0.115), prostitute(0.038), cook(0.030)
Имеет неплохую языковую модель:
The plate is [MASK] the table. => on(0.950)
[MASK] plate is on the table.  => my(0.310), the(0.226), her(0.178), his(0.171), a(0.089),
The plate is on the [MASK].    => floor(0.192), table(0.128), right(0.067), wall(0.062), ground(0.060)
The [MASK] is on the table.    => coffee(0.085), phone(0.056), food(0.050), money(0.047), book(0.029)
И поверхностную семантику:
Ann has an apple. She went to the table and put the [MASK] on it. => apple(0.213), lid(0.067), food(0.029)
Ann has a dream. She went to the table and put the [MASK] on it. => book(0.101), glasses(0.069), lid(0.047), tray(0.030)
С более контекстной семантикой есть проблемы:
Tom added poison to a glass of wine and gave it to Ann. Ann drank it and [MASK]. => nodded(0.184), smiled(0.162), left(0.148), sighed(0.089), drank(0.086),
Tom added [MASK] to a glass of wine and gave it to Ann. Ann drank it and died. => it(0.277), water(0.105), that(0.080), salt(0.060), milk(0.059)


Реализация BERT на PyTorch

class BERT(nn.Module):
    def __init__(self, V_DIM, E_DIM, HEADS, FF_DIM=2048, LAYERS = 1, MAX_LEN = 100):
        super(BERT, self).__init__()                    # конструктор предка с этим именем
         
        self.embTok = nn.Embedding(V_DIM,   E_DIM) # эмбединг токенов
        self.embPos = nn.Embedding(MAX_LEN, E_DIM) # эмбединг положение слова 
                                                   # (0-[CLS],[SEP],[NUL], 1,2,3...-предл)
        self.embSen = nn.Embedding(5,       E_DIM) # эмбединг предложения 
                                                   # (3-[CLS], 1,2-предл, 4-[SEP], 0-[NUL])
        
        self.embNorm= nn.LayerNorm(E_DIM)          # нормирование эмбедига 
        self.pooler = nn.Linear(E_DIM, E_DIM)      # выходной "перекодировщик"
        
        self.encLayer = nn.TransformerEncoderLayer(d_model=E_DIM, nhead=HEADS,
                                                   dim_feedforward=FF_DIM, 
                                                   dropout=0.1,activation='gelu')
        self.encoder  = nn.TransformerEncoder     (self.encLayer, num_layers=LAYERS)
        del self.encLayer
        
        self.fc_words = nn.Linear(E_DIM, V_DIM)    # выходной классифик. для слов
        self.fc_class = nn.Linear(E_DIM, 2)        # выходной классифик. для класса
          
    def forward(self, x, mask,  pos, sen, mskIDs): # (B,N), (B,N), (B,N), (B,N), (N,)
        B, N = tuple( x.shape )
    
        emb = self.embTok( x.transpose(0,1) )      # (N,B,E)  эмбединг слов
        emb.add_( self.embPos(pos.transpose(0,1)) )# (N,B,E)  эмбединг номеров слов 
        emb.add_( self.embSen(sen.transpose(0,1)) )# (N,B,E)  эмбединг номеров предложений
        
        emb = self.embNorm(emb)                    #  (N,B,E) нормировка слоя
        
        y = self.encoder(emb, src_key_padding_mask=mask)# (N,B,E)  пропускаем через энкодер
        y = self.pooler(y)                         # (N,B,E)  "перекодируем"
        
        cls = self.fc_class(y[0])                  # (B,2)         выход классификатора 
        y = y[mskIDs]                              # (0.1*N, B, E) только маск. выходы
        y = self.fc_words(y)                       # (0.1*N, B, V) mskIDs != 0
        
        return (y.permute(1,2,0), cls)             # (B, V, 0.1*N),  (B,2)

Литература

Статьи

Различные материалы

  • "AllenNLP" - возможность играться с GPT-2, выбирая одно из предложенных продолжений предложения.
  • "The Illustrated GPT-2 (Visualizing Transformer Language Models)" - введение в картинках в модель GPT-2.
  • "BERT, ELMO и Ко в картинках (как в NLP пришло трансферное обучение)" - простое введение в модель BERT с полезными ссылками.

    Библиотеки datasets, transformers

  • "What’s in the Dataset object".
  • "Loading a Dataset".