bert中的special token到底是怎麼發揮作用的(待續)
- 2021 年 4 月 6 日
- AI
bert中的special token有 [cls],[sep],[unk],[pad],[mask];
首先是[pad],
這個很簡單了,就是佔位符,和程式設計有關,和lstm中做padding一樣,tf或者torch的bert之類的預訓練model的介面api只能接受長度相同的input,所以用[pad]讓所有短句都能夠對齊,長句就直接做截斷,[pad]這個符號只是一種約定的用法,看文檔:
>>> batch_sentences = ["Hello I'm a single sentence",
... "And another sentence",
... "And the very very last one"]
>>> batch = tokenizer(batch_sentences, padding=True, truncation=True, return_tensors="pt")
>>> print(batch)
{'input_ids': tensor([[ 101, 8667, 146, 112, 182, 170, 1423, 5650, 102],
[ 101, 1262, 1330, 5650, 102, 0, 0, 0, 0],
[ 101, 1262, 1103, 1304, 1304, 1314, 1141, 102, 0]]),
'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0]]),
'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 0]])}這裡padding的部分的index都是0,即在embedding矩陣中對應第0行,因為pad在所難免,所以input輸入的時候,你總得在embedding矩陣里加一行embedding vector專門用來代表[pad],否則程式報錯,但是這個embedding因為被mask後續不會參與到權重更新,所以其實把bert原始的token embedding矩陣中這一行對應的權重改成任意的向量都沒什麼區別;
然後是[UNK],
這個更多是為了預測服務的,如果為了input的句子里有embedding矩陣沒有的token,分token的時候,這類token都會變成unk,同樣 bert的embedding矩陣里也有一行embedding向量專門用來表示UNK,不過具體不知道bert pretrained model里這個UNK是怎麼設計的,簡單的方法就是直接取所有embedding vector的均值作為代替;
然後是[mask],
[mask]主要是為了MLM任務,同樣Bert的embedding向量中也有專門存放[mask]這個token的embedding向量,設計的目的也是為了保證程式正常運行,如果embedding layer里沒有[mask]則程式報錯,
但是如果bertmodel的下游沒有設置MLM任務之類的和[mask]相關的預訓練任務對應的layer,把token替換成[mask]啥屁用都沒有,你就真的把這個詞變成[mask]了。
而所謂的預訓練任務對應的layer,可以看看keras官方文檔,重頭擼到尾:
Keras documentation: End-to-end Masked Language Modeling with BERT
mlm_output = layers.Dense(config.VOCAB_SIZE, name="mlm_cls", activation="softmax")(
encoder_output
)這就是mlm任務對應的任務頭(layer),就是一個vocab_size大小的超級多分類,這裡沒法用word2vec中用的負取樣因為mask可能有多個,也就是輸出可能是[0,0,0,1,0,1,0,....]這樣的向量輸出。
可以看到[mask]除了對input進行替換之外,全程打醬油,最終是根據輸出來確定了[mask]的作用,注意,bert中針對input的的mask機制僅僅針對padding部分進行mask,對[mask]是不做mask操作的。
所以,當我們把某個token mask之後,如果這個句子中沒有被mask的token都和被mask的token不相同的話,那麼這個被mask 的token的word embedding不會得到任何訓練,因為壓根也沒有往前傳。
最後是最讓人糾結的[cls]和[sep]
[cls],huggingface的berttokenize默認是給句子配一個[cls]和一個[seq],分別在句首和句尾,我看了很多百度知乎和Google上的說法:
CLS:special classification embedding,用於分類的向量,會聚集所有的分類資訊SEP:輸入是QA或2個句子時,需添加SEP標記以示區別
基本都是這麼解釋的。
這種解釋壓根難以make sence。
首先,如果我們的預訓練任務或者是下游的應用的輸入都是單個句子,[sep]根本就沒用,不用加到分token後的句子里。
先來理解一下[cls],
先看下原文的解釋:
The first token of every sequence is always a special classification token ([CLS]). The final hidden state corresponding to this token is used as the aggregate sequence representation for classification tasks.
講了和沒講差不多。。。
然後我又看了一下google bert的源程式碼,其中對這部分做了解釋:
//github.com/google-research/bert/blob/eedf5716ce1268e56f0a50264a88cafad334ac61/run_classifier.pyFor classification tasks, the first vector (corresponding to [CLS]) is used as the "sentence vector". Note that this only makes sense because the entire model is fine-tuned.
翻譯過來就是,你要用[cls]去做下游句子分類任務的fine-tune才有意義,反正最後根據下游任務finetune之後,bert的參數就會發生一定的變化使得[cls]處得到的hidden state包含句子的表徵資訊,換句話說,你用別的token的last hidden state來做finetune也沒問題。。。。。wtm
//github.com/hanxiao/bert-as-service#q-what-are-the-available-pooling-strategies在bert as service中也提到了這個問題以及作者的回答:
Q: Why not use the hidden state of the first token as default strategy, i.e. the[CLS]?
A: Because a pre-trained model is not fine-tuned on any downstream tasks yet. In this case, the hidden state of[CLS]is not a good sentence representation. If later you fine-tune the model, you may use[CLS]as well.
後續看完sentence-bert之後,發現確實cls這麼做效果沒有直觀的方法——對所有的input的token的last hidden state做pooling效果來的好。
Elesdspline:BERT中CLS效果真的好嘛?這篇文章告訴你答案
這篇文章也提到了相應的介紹。
所以,說老實話,我覺得[cls]加不加沒啥太大區別(從keras的mlm的官方的實現來看也可以看到,作者從頭訓練了一個MLM的bert model,壓根就沒有把[cls]這個special token加到詞表裡。)。無論是預訓練mlm任務還是句子分類這樣的下游任務,完全可以捨棄cls,只不過huggingface以及後續不少開源的model的語料訓練的時候大家都模仿了最早的bert往裡面加[cls],而預訓練的模型的輸入本身已經將[cls]+句子,這樣的輸入形式學習到模型的權重中去了(導致[cls]這個token在訓練的過程中也被編碼了某些知識
Jiang Lynn:解構BERT:從一億參數中獲取六種pattern
可以參考這篇,[cls]還是學習到了一些東西的),
如果你後續finetune的時候不這麼做,[cls]+句子A和直接使用句子A分別作為輸入來finetune下游的文本分類任務之類的,可能捨棄了[cls]就浪費了一部分資訊,效果和保持原始輸入形式的效果就會有一些差別。
最最後說一下[sep]:
其實前面提到的[sep]和[cls]的意義幾乎差不多,就是預訓練的時候一部分的知識被編碼到這兩個token對應的權重里了,sep用來區分句子,因為bert中有個nsp的任務:

這個其實去看下huggingface的bertmodel的源程式碼:
//github.com/huggingface/transformers/blob/1c06240e1b3477728129bb58e7b6c7734bb5074e/src/transformers/models/bert/modeling_bert.py就可以知道,embedding部分壓根就沒有關於[sep]的額外處理,就單獨對pad的index進行了定義,也就是[sep]也是當作一個字元放進去了。google上有不少train from scratch的程式碼,比如這段:
def __getitem__(self, idx):
t1,t2 = self.get_sentence(idx)
t1_random, t1_label,_ = self.random_word(t1)
t2_random, t2_label,_ = self.random_word(t1)
t1 = [self.vocab['[CLS]']] + t1_random + [self.vocab['[SEP]']]
t2 = t2_random + [self.vocab['[SEP]']]
t1_label = [self.vocab['[PAD]']] + t1_label + [self.vocab['[PAD]']]
t2_label = t2_label + [self.vocab['[PAD]']]
segment_label = ([0 for _ in range(len(t1))] + [1 for _ in range(len(t2))])[:self.seq_len]
bert_input = (t1 + t2)[:self.seq_len]
padding = [self.vocab['[PAD]'] for _ in range(self.seq_len - len(bert_input))]
attention_mask = len(bert_input) * [1] + len(padding) * [0]
bert_input.extend(padding)
bert_label = t1_label
padding = [self.vocab['[PAD]'] for _ in range(self.seq_len - len(bert_label))]
bert_label.extend(padding)# , segment_label.extend(padding)
attention_mask = np.array(attention_mask)
bert_input = np.array(bert_input)
segment_label = np.array(segment_label)
bert_label = np.array(bert_label)
#is_next_label = np.array(is_next_label)
output = {"input_ids": bert_input,
"token_type_ids": segment_label,
'attention_mask': attention_mask,
"bert_label": bert_label}#, is_next_label
return output是關於nsp任務準備的,可以發現,句子的先後順序,也就是segment embedding對應的
segment_label:
segment_label = ([0 for _ in range(len(t1))] + [1 for _ in range(len(t2))])[:self.seq_len]和[sep]沒有半毛錢關係。
總結:不是因為special token有特殊意義我們才要刻意地使用special token,而是因為開源地預訓練模型是在這樣地輸入形式上訓練地,所以我們使用地時候也要尊崇其語料地形式對新語料進行改造,換句話說,如果你有能力自己預訓練一個新的model,special token,例如[pad],[mask],[unk]想換成什麼都行,而[cls],[seq]不加都可以,或者某些開源的預訓練模型里也沒有[cls],[seq]之類的奇怪的token,那麼就完全不需要加。
