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".