Step-by-step to Transformer:深入解析工作原理(以Pytorch机器翻译为例)
大家好,我是青青山螺應(yīng)如是,大家可以叫我青青,工作之余是一名獨(dú)立攝影師。喜歡美食、旅行、看展,偶爾整理下NLP學(xué)習(xí)筆記,不管技術(shù)文還是生活隨感,都會(huì)分享本人攝影作品,希望文藝的技術(shù)青年能夠喜歡~~如果有想拍寫真的妹子也可以在個(gè)人公號(hào)【青影三弄】留言~
Photograhy?Sharing
拍攝參數(shù)? ?|? ?f2.8/ 190mm/ ISO1500
設(shè)備? ?|? ?Canon6D2 / 70-200mm f2.8L III USM? ?
先分享一張?jiān)诜鹆_倫薩的人文攝影~
今年去意呆的時(shí)候特別熱,每天都是白晃晃的大太陽,所以我總喜歡躲到附近的教堂,那里是“免費(fèi)的避暑勝地”。
“諸圣教堂”離我住處很近,當(dāng)時(shí)遇到神父禱告,他還緩緩唱了首歌,第一次覺得美聲如此動(dòng)人,怪不得意呆人那么熱愛歌劇,連我這種音樂小白都被感染到了~
喜歡“諸圣教堂”的另一個(gè)原因是這里埋葬著基爾蘭達(dá)約、波提切利和他愛慕的女神西蒙內(nèi)塔。生前“小桶”因她創(chuàng)作了《維納斯的誕生》,離開人世他又得償所愿和心愛之人共眠。情深如此,我能想到的中國式浪漫大概也只有“庭有枇杷樹,吾妻死之年所手植也,今已亭亭如蓋矣”相比了..
AI sharing
之前介紹過Seq2Seq+SoftAttention這種序列模型實(shí)現(xiàn)機(jī)器翻譯,那么拋棄RNN,全面擁抱attention的transformer又是如何實(shí)現(xiàn)的呢。
本篇介紹Transformer的原理及Pytorch實(shí)現(xiàn),包括一些細(xì)枝末節(jié)的trick和個(gè)人感悟,這些都是在調(diào)試代碼過程中深切領(lǐng)會(huì)的。網(wǎng)上查了很多文章,大部分基于哈佛那片論文注釋,數(shù)據(jù)集來源于tochtext自帶的英-德翻譯,但是本篇為了和上面攝影分享對(duì)應(yīng)以及靈活的自定義數(shù)據(jù)集,采用意大利-英語翻譯。
CONTENT
1、Transformer簡(jiǎn)介
2、模型概覽
3、數(shù)據(jù)加載及預(yù)處理
?? ? ?3.1原始數(shù)據(jù)構(gòu)造DataFrame
? ? ? 3.2自定義Dataset
? ? ? 3.3構(gòu)建字典
? ? ??3.4Iterator實(shí)現(xiàn)動(dòng)態(tài)批量化
? ? ??3.5生成mask
4、Embedding層
? ? ??4.1普通Embedding
? ? ??4.2位置PositionalEncoding
? ? ??4.3層歸一化
5、SubLayer子層組成
? ? ??5.1MulHeadAttention(self+context attention)
? ? ? ? ? ?self attention
? ? ? ? ? ?attention score:scaled dot product
? ? ? ? ? ?multi head
? ? ??5.2Position-wise Feed-forward前饋傳播
? ? ??5.3Residual Connection殘差連接
6、Encoder組合
7、Decoder組合
8、損失函數(shù)和優(yōu)化器
? ? ?8.1損失函數(shù)實(shí)現(xiàn)標(biāo)簽平滑
? ? ?8.2優(yōu)化器實(shí)現(xiàn)動(dòng)態(tài)學(xué)習(xí)率
9、模型訓(xùn)練Train
10、測(cè)試生成
11、注意力分布可視化
12、數(shù)學(xué)原理解釋transformer和rnn本質(zhì)區(qū)別
【資料索取】
公眾號(hào)回復(fù):Transformer
可獲取完整代碼
1.?Transfomer簡(jiǎn)介
《Attention Is All Your Need》是一篇Google提出全面使用self-Attention的論文。這篇論文中提出一個(gè)全新的模型,叫 Transformer,拋棄了以往深度學(xué)習(xí)任務(wù)里面使用到的 CNN 和 RNN。目前大熱的Bert就是基于Transformer構(gòu)建的,這個(gè)模型廣泛應(yīng)用于NLP領(lǐng)域,例如機(jī)器翻譯,問答系統(tǒng),文本摘要和語音識(shí)別等等方向。
眾所周知RNN雖然模型設(shè)計(jì)小巧精妙,但是其線性序列模型決定了無法實(shí)現(xiàn)并行,從兩個(gè)任意輸入和輸出位置獲取依賴關(guān)系都需要大量的運(yùn)算,運(yùn)算量嚴(yán)重受到距離的制約;而且距離不但影響性能也影響效果,隨著記憶時(shí)序的拉長,記憶削弱,導(dǎo)致學(xué)習(xí)能力削弱。
為了拋棄RNN step by step線性時(shí)序,Transformer使用了可以biself-attention,不依靠順序和距離就能獲得兩個(gè)位置(實(shí)質(zhì)是key和value)的依賴關(guān)系(hidden)。這種計(jì)算成本減少到一個(gè)固定的運(yùn)算量,雖然注意力加權(quán)會(huì)減少有效的resolution表征力,但是使用多頭multi-head attention可以彌補(bǔ)平均注意力加權(quán)帶來的損失。
自注意力是一種關(guān)注自身序列不同位置的注意力機(jī)制,能計(jì)算序列的表征representation。
和之前分享的Seq2Seq+SoftAttention相比,Transformer不僅關(guān)注encoder和decoder之間的attention,也關(guān)注encoder和decoder各自內(nèi)部自己的attention。也就是說前者的hidden是靠lstm來實(shí)現(xiàn),而transformer的encoder或者decoder的hidden是靠self-attention來實(shí)現(xiàn)。
2.?模型概覽
Transformer結(jié)構(gòu)和Seq2Seq模型是一樣的,也采用了Encoder-Decoder結(jié)構(gòu),但Transformer更復(fù)雜。
2.1宏觀組成
Encoder由6個(gè)EncoderLayer構(gòu)成,Decoder由6個(gè)DecoderLayer構(gòu)成:
對(duì)應(yīng)的代碼邏輯如下,make_model包含EncoderDecoder模塊,可以看到N=6表示Encoder和Decoder的子層數(shù)量,d_ff是前饋神經(jīng)網(wǎng)絡(luò)的中間隱層維度,h=代表的是注意力層的頭數(shù)。
后面還規(guī)定了初始化的策略,如果每層參數(shù)維度大于1,那么初始化服從均勻分布init.xavier_uniform
EncoderDecoder里面除了Encoder和Decoder兩個(gè)模塊,還包含embed和generator。Embed層是對(duì)對(duì)輸入進(jìn)行初始化,詞嵌入包含普通的Embeddings和位置標(biāo)記PositionEncoding;Generator作用是對(duì)輸出進(jìn)行full linear+softmax
其中可以看到Decoder輸入的memory就是來自前面Encoder的輸出,memory會(huì)分別喂入Decoder的6個(gè)子層。
class EncoderDecoder(nn.Module): def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):super(EncoderDecoder, self).__init__()self.encoder = encoderself.decoder = decoderself.src_embed = src_embedself.tgt_embed = tgt_embedself.generator = generatordef forward(self, src, tgt, src_mask, tgt_mask):return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)def encode(self, src, src_mask):return self.encoder(self.src_embed(src), src_mask)def decode(self, memory, src_mask, tgt, tgt_mask):return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask) class Generator(nn.Module):def __init__(self, d_model, vocab):super(Generator, self).__init__()self.proj = nn.Linear(d_model, vocab)def forward(self, x):return F.log_softmax(self.proj(x), dim=-1)2.2內(nèi)部結(jié)構(gòu)
上面是Transformer宏觀上的結(jié)構(gòu),那Encoder和Decoder內(nèi)部都有哪些不同于Seq2Seq的技術(shù)細(xì)節(jié)呢:
(1)Encoder的輸入序列經(jīng)過word embedding和positional encoding后,輸入到encoder。
(2)在EncoderLayer里面先經(jīng)過8個(gè)頭的self-attention模塊處理source序列自身。這個(gè)模塊目的是求得序列的hidden,利用的就是自注意力機(jī)制,而非之前RNN需要step by step算出每個(gè)hidden。然后經(jīng)過一些norm和drop基本處理,再使用殘差連接模塊,目的是為了避免梯度消失問題。(后面代碼實(shí)現(xiàn)和上圖在實(shí)現(xiàn)順序上有一點(diǎn)出入)
(3)在EncoderLayer里面再進(jìn)入Feed-Forward前饋神經(jīng)網(wǎng)絡(luò),實(shí)際上就是做了兩次dense,linear2(activation(linear1))。然后同上經(jīng)過一些norm和drop基本處理,再使用殘差連接模塊。
(4)Decoder的輸入序列處理方式同上
(5)在DecoderLayer里面也要經(jīng)過8個(gè)頭的self-attentention模塊處理target序列自身。不同于Encoder層,這里只需要關(guān)注輸入時(shí)刻t之前的部分,目的是為了符合decoder看不到未來信息的邏輯,所以這里的mask是融合了pad-mask和sequence-mask兩種。同Encoder,這個(gè)模塊目的也是為了求得target序列自身的hidden,然后經(jīng)過一些norm和drop基本處理,再使用殘差連接模塊。
(6)在DecoderLayer里面再進(jìn)入src-attention模塊,這個(gè)模塊也是相比Encoder增加的注意力層。其實(shí)注意力結(jié)構(gòu)都是相似的,只是(query,key,value)不同,對(duì)于self-attention這三個(gè)值都是一致的,對(duì)于src-attention,query來自decoder的hidden,key和value來自encoder的hidden
(7)在DecoderLayer里面最后進(jìn)入Feed-Forward前饋神經(jīng)網(wǎng)絡(luò),同上。
介紹完Transformer整體結(jié)構(gòu),下面從數(shù)據(jù)集處理到各層代碼實(shí)現(xiàn)細(xì)節(jié)進(jìn)行詳細(xì)說明~
3.?數(shù)據(jù)加載及預(yù)處理
GPU環(huán)境使用Google Colab 單核16g,數(shù)據(jù)集eng-ita.txt,普通的英意翻譯對(duì)的文本數(shù)據(jù)。數(shù)據(jù)集預(yù)處理使用的是torchtext+spacy工具,他使用的整體思路是構(gòu)造Dataset,字典、Iterator實(shí)現(xiàn)批量化、對(duì)矩陣進(jìn)行mask pad。
數(shù)據(jù)預(yù)處理非常重要,這里涉及很多提高訓(xùn)練性能的trick。下面具體看一下如何使用自定義數(shù)據(jù)集來完成這些預(yù)處理步驟。
3.1原始數(shù)據(jù)構(gòu)造DataFrame
先加載文本,并將source和target兩列轉(zhuǎn)換為兩個(gè)獨(dú)立的list:
因?yàn)閠orchtext的dataset的輸入需要DataFrame格式,所以這里先利用上面的source和target list構(gòu)造DataFrame。訓(xùn)練集、驗(yàn)證集、測(cè)試集按照實(shí)際要求進(jìn)行劃分:
3.2構(gòu)造Dataset
這里主要包含分詞、指定起止符和補(bǔ)全字符以及限制序列最大長度。
因?yàn)閠orchtext的Dataset是由example組成,example的含義就是一條翻譯對(duì)記錄。
# 分詞 spacy_it = spacy.load('it') spacy_en = spacy.load('en') def tokenize_it(text):return [tok.text for tok in spacy_it.tokenizer(text)] def tokenize_en(text):return [tok.text for tok in spacy_en.tokenizer(text)] # 定義FIELD配置信息 # 主要包含以下數(shù)據(jù)預(yù)處理的配置信息,比如指定分詞方法,是否轉(zhuǎn)成小寫,起始字符,結(jié)束字符,補(bǔ)全字符以及詞典等等 BOS_WORD = '<s>' EOS_WORD = '</s>' BLANK_WORD = "<blank>" SRC = data.Field(tokenize=tokenize_it, pad_token=BLANK_WORD) TGT = data.Field(tokenize=tokenize_en, init_token=BOS_WORD, eos_token=EOS_WORD, pad_token=BLANK_WORD) # get_dataset構(gòu)造并返回Dataset所需的examples和fields def get_dataset(csv_data, text_field, label_field, test=False):fields = [('id', None), ('src', text_field), ('trg', label_field)]examples = []if test:for text in tqdm(csv_data['src']): # tqdm的作用是添加進(jìn)度條examples.append(data.Example.fromlist([None, text, None], fields))else:for text, label in tqdm(zip(csv_data['src'], csv_data['trg'])):examples.append(data.Example.fromlist([None, text, label], fields))return examples, fields # 得到構(gòu)建Dataset所需的examples和fields train_examples, train_fields = get_dataset(train_df, SRC, TGT) valid_examples, valid_fields = get_dataset(valid_df, SRC, TGT) test_examples, test_fields = get_dataset(test_df, SRC, None, True) 構(gòu)建Dataset數(shù)據(jù)集 # 構(gòu)建Dataset數(shù)據(jù)集 # 這里的ita最大長度也就56 MAX_LEN = 100 train = data.Dataset(train_examples,train_fields,filter_pred=lambda x: len(vars(x)['src'])<= MAX_LEN and len(vars(x)['trg']) <= MAX_LEN) valid = data.Dataset(valid_examples, valid_fields,filter_pred=lambda x: len(vars(x)['src']) <= MAX_LEN and len(vars(x)['trg']) <= MAX_LEN) test = data.Dataset(test_examples, test_fields,filter_pred=lambda x: len(vars(x)['src']) <= MAX_LEN)3.3構(gòu)造字典
MIN_FREQ = 2 #統(tǒng)計(jì)字典時(shí)要考慮詞頻 SRC.build_vocab(train.src, min_freq=MIN_FREQ) TGT.build_vocab(train.trg, min_freq=MIN_FREQ)3.4構(gòu)造Iterator
torchtext的Iterator主要負(fù)責(zé)把Dataset進(jìn)行批量劃分、字符轉(zhuǎn)數(shù)字、矩陣pad。
這里有個(gè)很重要的點(diǎn)就是批量化,本例使用的是動(dòng)態(tài)批量化,即每個(gè)batch的批大小是不同的,是以每個(gè)batch的token數(shù)量作為統(tǒng)一劃分標(biāo)準(zhǔn),也就是說每個(gè)batch的token數(shù)基本一致,這個(gè)機(jī)制是通過batch_size_fn來實(shí)現(xiàn)的,比如batch1[16,20],batch2是[8,40],兩者的批大小是不同的分別是16和8,但是token總數(shù)是一樣的都是320。
采取這種方式的特點(diǎn)就是他會(huì)把長度相同序列的聚集到一起,然后進(jìn)行pad,從而減少了pad的比例,為什么要減少pad呢:
(1)padding是對(duì)計(jì)算資源的浪費(fèi),pad越多訓(xùn)練耗費(fèi)的時(shí)間越長。
(2)padding的計(jì)算會(huì)引入噪聲,nsformer 中,LayerNorm 會(huì)使 padding 位置的值變?yōu)榉?,這會(huì)使每個(gè) padding 都會(huì)有梯度,引起不必要的權(quán)重更新。
下圖是隨意組織pad的batch(paddings)和長度相近原則組織pad的batch(baseline)
實(shí)現(xiàn)代碼部分,可以看到這里MyIterator實(shí)現(xiàn)了data.Iterator的create_batch()函數(shù),其實(shí)真正起作用的是里面的torchtext.data.batch()函數(shù)部分,他的功能是把原始的字符數(shù)據(jù)按照batch_size_fn算法來進(jìn)行batch并且shuffle,沒有做數(shù)字化也沒有做pad。
class MyIterator(data.Iterator):def create_batches(self):if self.train:def pool(d, random_shuffler):for p in data.batch(d, self.batch_size * 100):p_batch = data.batch(sorted(p, key=self.sort_key),self.batch_size, self.batch_size_fn)for b in random_shuffler(list(p_batch)):yield bself.batches = pool(self.data(), self.random_shuffler)else:self.batches = []for b in data.batch(self.data(), self.batch_size,self.batch_size_fn):self.batches.append(sorted(b, key=self.sort_key)) global max_src_in_batch, max_tgt_in_batch def batch_size_fn(new, count, sofar):global max_src_in_batch, max_tgt_in_batchif count == 1:max_src_in_batch = 0max_tgt_in_batch = 0max_src_in_batch = max(max_src_in_batch, len(new.src))max_tgt_in_batch = max(max_tgt_in_batch, len(new.trg) + 2)src_elements = count * max_src_in_batchtgt_elements = count * max_tgt_in_batchreturn max(src_elements, tgt_elements)那么什么時(shí)候?qū)atch進(jìn)行數(shù)字化和pad呢,看了下源碼,實(shí)際這些操作封裝在data.Iterator.__iter__的torchtext.data.Batch這個(gè)類中。
這里還有一個(gè)問題,BATCH_SIZE這個(gè)參數(shù)設(shè)置多少合適呢,這個(gè)參數(shù)在這里的含義代表每個(gè)batch的token總量,我測(cè)試了下單核16G的colabGPU訓(xùn)練環(huán)境需要6000,如果超過這個(gè)數(shù)值,內(nèi)存容易爆。
BATCH_SIZE = 6000 train_iter = MyIterator(train, batch_size=BATCH_SIZE, device=0, repeat=False,sort_key=lambda x: (len(x.src), len(x.trg)),batch_size_fn=batch_size_fn, train=True) valid_iter = MyIterator(valid, batch_size=BATCH_SIZE, device=0, repeat=False,sort_key=lambda x: (len(x.src), len(x.trg)),batch_size_fn=batch_size_fn, train=False)3.5生成mask
我們知道整個(gè)模型的輸入就是src,src_mask,tgt,tgt_mask,現(xiàn)在src和tgt已經(jīng)比較明確了,那mask部分呢,總的來說mask就是對(duì)上面的pad部分做一個(gè)統(tǒng)計(jì)行程對(duì)應(yīng)的mask矩陣。
但是前面在講整體結(jié)構(gòu)的時(shí)候提到,Decoder的mask比Encoder的mask多一層含義,就是sequence_mask,下面說下這兩類mask。
(1)padding mask
Seq2Seq+SoftAttention里面計(jì)算的mask就是padding mask。每個(gè)批次輸入序列長度是不一樣的,要對(duì)輸入序列進(jìn)行對(duì)齊。具體來說,就是給在較短的序列后面填充0。因?yàn)檫@些填充的位置,其實(shí)是沒什么意義的,所以我們的attention機(jī)制不應(yīng)該把注意力放在這些位置上,所以我們需要進(jìn)行一些處理。具體的做法是,把這些位置的值機(jī)上一個(gè)非常大的復(fù)數(shù),經(jīng)過softmax這些位置就會(huì)接近0。而我們的padding mask實(shí)際上是一個(gè)張量,每個(gè)值都是一個(gè)Bool,值為False的地方就是我們要進(jìn)行處理的地方。
(2)sequence mask
自然語言生成(例如機(jī)器翻譯,文本摘要)是auto-regressive的,在推理的時(shí)候只能依據(jù)之前的token生成當(dāng)前時(shí)刻的token,正因?yàn)樯僧?dāng)前時(shí)刻的token的時(shí)候并不知道后續(xù)的token長什么樣,所以為了保持訓(xùn)練和推理的一致性,訓(xùn)練的時(shí)候也不能利用后續(xù)的token來生成當(dāng)前時(shí)刻的token。這種方式也符合人類在自然語言生成中的思維方式。
那么具體怎么做呢,也很簡(jiǎn)單,產(chǎn)生一個(gè)上三角矩陣,上三角的值全為1,下三角的值全為0,對(duì)角線也是0。把這個(gè)矩陣作用在每一個(gè)序列上,就達(dá)到我們的目的。如圖:
總結(jié)一下transformer里面的mask使用情況:
* encoder里的self-attention使用的是padding mask
* decoder里面的self-attention使用的是padding mask+sequence mask;context-attention使用的是padding mask
下面看代碼來實(shí)現(xiàn)上面兩種mask,其中make_std_mask里面實(shí)現(xiàn)了pad mask+sequence mask;
除了mask,代碼還對(duì)target部分做了處理,self.trg表示輸入,self.trg表示最終loss里面標(biāo)簽角色,比self.trg往后挪一列。
class BatchMask:def __init__(self, src, trg=None, pad=0):self.src = srcself.src_mask = (src != pad).unsqueeze(-2)if trg is not None:self.trg = trg[:, :-1]self.trg_y = trg[:, 1:]self.trg_mask = \self.make_std_mask(self.trg, pad)self.ntokens = (self.trg_y != pad).data.sum()@staticmethoddef make_std_mask(tgt, pad):tgt_mask = (tgt != pad).unsqueeze(-2)tgt_mask = tgt_mask & Variable(subsequent_mask(tgt.size(-1)).type_as(tgt_mask.data))return tgt_mask def subsequent_mask(size):attn_shape = (1, size, size)subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')return torch.from_numpy(subsequent_mask) == 0 def batch_mask(pad_idx, batch):src, trg = batch.src.transpose(0, 1), batch.trg.transpose(0, 1)return BatchMask(src, trg, pad_idx) pad_idx = TGT.vocab.stoi["<blank>"]4. Embedding層
這一部分針對(duì)輸入模型的數(shù)據(jù)的詞嵌入處理,主要包含三個(gè)過程:普通詞嵌入word Embeddings、位置編碼PositionalEncoding、層歸一化LayerNorm。
(word Embedding是對(duì)詞匯本身編碼;Positional encoding是對(duì)詞匯的位置編碼)
4.1普通Embedding
初始化embedding matrix,通過embedding lookup將Inputs映射成token embedding,大小是[batch size, max seq length, embedding size],然后乘以embedding size的開方。那么這里為什么要乘以√dmodel ? ?論文并沒有講為什么這么做,我看了代碼,猜測(cè)是因?yàn)閑mbedding matrix的初始化方式是xavier init,這種方式的方差是1/embedding size,因此乘以embedding size的開方使得embedding matrix的方差是1,在這個(gè)scale下可能更有利于embedding matrix的收斂。
class Embeddings(nn.Module):def __init__(self, d_model, vocab):super(Embeddings, self).__init__()self.lut = nn.Embedding(vocab, d_model)self.d_model = d_modeldef forward(self, x):return self.lut(x) * math.sqrt(self.d_model)4.2位置編碼PositionalEncoding
我們知道RNN使用了step by step這種時(shí)序算法保持了序列本有的順序特征,但缺點(diǎn)是無法串行降低了性能,transformer的主要思想self-attention在本質(zhì)上拋棄了rnn這種時(shí)序特征,也就拋棄了所謂的序列順序特征,如果缺失了序列順序這個(gè)重要信息,那么結(jié)果就是所有詞語都對(duì)了,但是就是無法組成有意義的語句。那么他是怎么彌補(bǔ)的呢?
為了處理這個(gè)問題,transformer給encoder層和decoder層的輸入添加了一個(gè)額外的向量Positional Encoding,就是位置編碼,維度和embedding的維度一樣,這個(gè)向量采用了一種很獨(dú)特的方法來讓模型學(xué)習(xí)到這個(gè)值,這個(gè)向量能決定當(dāng)前詞的位置,或者說在一個(gè)句子中不同的詞之間的距離。
這個(gè)位置向量的具體計(jì)算方法有很多種,論文中的計(jì)算方法如下sinusoidal version,這里的2i就是指的d_model這個(gè)維度上的,和pos不是一回事,pos就是可以自己定義1-5000的序列,每個(gè)數(shù)字代表一個(gè)序列位置:
上式右端表達(dá)式中pos下面的除數(shù)如果使用exp來表示的話,手寫推導(dǎo)如下:
代碼表示:
position?=?torch.arange(0,?max_len).unsqueeze(1)
div_term?=?torch.exp(torch.arange(0,?d_model,?2)?*?-(math.log(10000.0)/d_model))
pe[:,?0::2]?=?torch.sin(position?*?div_term)
pe[:,?1::2]?=?torch.cos(position?*?div_term)
其中pos是指當(dāng)前詞在句子中的位置,i是指向量d_model中每個(gè)值的index,可以看出,在d_model偶數(shù)位置index,使用正弦編碼,在奇數(shù)位置,使用余弦編碼。上面公式的dmodel就是模型的維度,論文默認(rèn)是512。
這個(gè)編碼的公式的意思就是:給定詞語的位置pos,我們可以把它編碼成dmodel維的向量,也就是說位置編碼的每個(gè)維度對(duì)應(yīng)正弦曲線,波長構(gòu)成了從2π到10000*2π的等比序列。上面的位置編碼是絕對(duì)位置編碼,但是詞語的相對(duì)位置也非常重要,這就是論文為什么使用三角函數(shù)的原因。
正弦函數(shù)能夠表達(dá)相對(duì)位置信息。主要數(shù)學(xué)依據(jù)是以下兩個(gè)公式,對(duì)于詞匯之間的位置偏移k,PE(pos+k)可以表示成PE(pos)和PE(k)的組合形式,這就是表達(dá)相對(duì)位置的能力:
可視化位置編碼效果如下,橫軸是位置序列seq_len這個(gè)維度,豎軸是d_model這個(gè)維度:
代碼中加入了dropout,具體實(shí)現(xiàn)如下。self.register_buffer可以將tensor注冊(cè)成buffer,網(wǎng)絡(luò)存儲(chǔ)時(shí)也會(huì)將buffer存下,當(dāng)網(wǎng)絡(luò)load模型是,會(huì)將存儲(chǔ)模型的buffer也進(jìn)行賦值,buffer在forward中更新而不再梯度下降中更新,optim.step只能更新nn.Parameter類型參數(shù)。
class PositionalEncoding(nn.Module):def __init__(self, d_model, dropout, max_len=5000):super(PositionalEncoding, self).__init__()self.dropout = nn.Dropout(p=dropout)pe = torch.zeros(max_len, d_model)position = torch.arange(0, max_len).unsqueeze(1)div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model))pe[:, 0::2] = torch.sin(position * div_term)pe[:, 1::2] = torch.cos(position * div_term)pe = pe.unsqueeze(0)self.register_buffer('pe', pe)def forward(self, x):x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)return self.dropout(x)4.3層歸一化
層歸一化layernorm是一種基礎(chǔ)數(shù)據(jù)里手段,作用于輸入和輸出,那么本例都什么時(shí)候用到呢,一個(gè)是source和target數(shù)據(jù)經(jīng)過詞嵌入后進(jìn)入子層前要進(jìn)行層歸一化;另一個(gè)就是encoder和decoder模塊輸出的時(shí)候要進(jìn)行層歸一化。
layernorm不同于batchnorm,他是在d_model這個(gè)維度上計(jì)算平均值和方差,公式如下:
可以看到這里引入了參數(shù)α和β,所以可以使用torch里面的nn. Parameter,他的作用就是初始化一個(gè)可進(jìn)行訓(xùn)練優(yōu)化的參數(shù),并將這個(gè)參數(shù)綁定到module里面。
代碼如下:
class LayerNorm(nn.Module):def __init__(self, features, eps=1e-6):super(LayerNorm, self).__init__()self.a_2 = nn.Parameter(torch.ones(features))self.b_2 = nn.Parameter(torch.zeros(features))self.eps = epsdef forward(self, x):mean = x.mean(-1, keepdim=True)std = x.std(-1, keepdim=True)return self.a_2 * (x - mean) / (std + self.eps) + self.b_25. SubLayer子層組成
這里的sublayer存在于每個(gè)encoderlayer和decoderlayer中,是公用的部分,encoderlayer里面有兩個(gè)子層(mulhead-self attention/feed-forward)靠殘差層連接;decoderlayer里面有三個(gè)子層(mulhead-self attention/mulhad-context attention/feed-forward)。
這里的mulhead-self attention和mulhad-context attention是整個(gè)transformer最核心的部分。前者是自注意力機(jī)制,出現(xiàn)在encoder或decoder內(nèi)部序列自身學(xué)習(xí)hidden;后者上下文注意力機(jī)制,相當(dāng)于之前分享文章里的Seq2Seq+softattention,是encoder和decoder之間的注意力為了學(xué)習(xí)context。這兩種注意力機(jī)制結(jié)構(gòu)實(shí)際上是相同的,不同的在于輸入部分(query,key,value):self-attention的三個(gè)數(shù)值都是一致的;而contex-attention的query來自decoder,key和value來自encoder。
5.1多頭MuHead Attention(self+context attention)
由于之前分享的文章詳細(xì)講解過context-attention(相當(dāng)于soft-attention),所以這里詳細(xì)說明self-attention和multi-head兩種機(jī)制。
(1)self-attention
我們看個(gè)例子:
The animal didn't cross the street because it was too tired
這里的 it 到底代表的是 animal 還是 street 呢,對(duì)于人來說能很簡(jiǎn)單的判斷出來,但是對(duì)于機(jī)器來說,是很難判斷的,self-attention就能夠讓機(jī)器把 it 和 animal 聯(lián)系起來,接下來我們看下詳細(xì)的處理過程。
首先,self-attention會(huì)計(jì)算出三個(gè)新的向量,在論文中,向量的維度是512維,我們把這三個(gè)向量分別稱為Query、Key、Value,這三個(gè)向量是用embedding向量與一個(gè)參數(shù)矩陣W相乘得到的結(jié)果,這個(gè)矩陣是隨機(jī)初始化的。
計(jì)算self-attention的分?jǐn)?shù)值,該分?jǐn)?shù)值決定了當(dāng)我們?cè)谀硞€(gè)位置encode一個(gè)詞時(shí),對(duì)輸入句子的其他部分的關(guān)注程度。這個(gè)分?jǐn)?shù)值的計(jì)算方法是Query與Key做點(diǎn)成,以下圖為例,首先我們需要針對(duì)Thinking這個(gè)詞,計(jì)算出其他詞對(duì)于該詞的一個(gè)分?jǐn)?shù)值,首先是針對(duì)于自己本身即q1·k1,然后是針對(duì)于第二個(gè)詞即q1·k2。
接下來,把點(diǎn)成的結(jié)果除以一個(gè)常數(shù),這里我們除以8,這個(gè)值一般是采用上文提到的矩陣的第一個(gè)維度的開方即64的開方8,當(dāng)然也可以選擇其他的值,然后把得到的結(jié)果做一個(gè)softmax的計(jì)算。得到的結(jié)果即是每個(gè)詞對(duì)于當(dāng)前位置的詞的相關(guān)性大小,當(dāng)然,當(dāng)前位置的詞相關(guān)性肯定會(huì)會(huì)很大。
下一步就是把Value和softmax得到的值進(jìn)行相乘,并相加,得到的結(jié)果即是self-attetion在當(dāng)前節(jié)點(diǎn)的值。
在實(shí)際的應(yīng)用場(chǎng)景,為了提高計(jì)算速度,我們采用的是矩陣的方式,直接計(jì)算出Query, Key, Value的矩陣,然后把embedding的值與三個(gè)矩陣直接相乘,把得到的新矩陣 Q 與 K 相乘,乘以一個(gè)常數(shù),做softmax操作,最后乘上 V 矩陣。
這種通過 query 和 key 的相似性程度來確定 value 的權(quán)重分布的方法被稱為scaled dot-product attention。
結(jié)構(gòu)圖如下:
(2)attention score:scaled dot-product
那么這里Transformer為什么要使用scaled dot-product來計(jì)算attention score呢?論文的描述含義就是通過確定Q和K之間的相似度來選擇V,公式:
scaled dot-product attention和dot-product attention唯一的區(qū)別就是,scaled dot-product attention有一個(gè)縮放因子:
上面公式中的dk表示的k的維度,在論文里面默認(rèn)是64。為什么需要加上這個(gè)縮放因子呢,論文解釋:對(duì)于dk很大的時(shí)候,點(diǎn)積得到結(jié)果量級(jí)很大,方差很大,使得處于softmax函數(shù)梯度很小的區(qū)域。我們知道,梯度很小的情況,對(duì)反向傳播不利。下面簡(jiǎn)單的測(cè)試反映出不同量級(jí)對(duì),最大值的概率變化。
f?=?lambda?x:?exp(6*x)?/?(exp(2*x)+exp(2*x+1)+exp(3*x)+exp(4*x)+exp(5*x+4)+exp(6*x))
x?=?np.linspace(0,?30,?100)
y_3?=?[f(x_i)?for?x_i?in?x]
plt.plot(x,?y_3)
plt.show()
可以看到f是softmax的最大值6x的曲線,當(dāng)x處于1~50之間不同的量級(jí)的時(shí)候,所表示的概率,當(dāng)x量級(jí)在>7的時(shí)候,差不多其分配的概率就接近1了。也就是說輸入量級(jí)很大的時(shí)候,就會(huì)造成梯度消失。
為了克服這個(gè)負(fù)面影響,除以一個(gè)縮放因子可以一定程度上減緩這種情況。點(diǎn)積除以√dmodel ? ,將控制方差為1,也就有效的控制了梯度消失的問題。
注意部分的代碼表示:
def attention(query, key, value, mask=None, dropout=None):d_k = query.size(-1)scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)if mask is not None:scores = scores.masked_fill(mask == 0, -1e9)p_attn = F.softmax(scores, dim=-1)if dropout is not None:p_attn = dropout(p_attn)context = torch.matmul(p_attn, value)return context, p_attn(3)multi-head
這篇論文另一個(gè)牛的地方是給attention加入另外一個(gè)機(jī)制——multi head,該機(jī)制理解起來很簡(jiǎn)單,就是說不僅僅只初始化一組Q、K、V的矩陣,而是初始化多組,tranformer是使用了8組,所以最后得到的結(jié)果是8個(gè)矩陣。
論文提到,他們發(fā)現(xiàn)將Q、K、V通過一個(gè)線性映射之后,分成h份,對(duì)每一份進(jìn)行scaled dot-product attention效果更好。然后,把各個(gè)部分的結(jié)果合并起來,再次經(jīng)過線性映射,得到最終的輸出。這就是所謂的multi-head attention。上面的超參數(shù)h就是heads數(shù)量。論文默認(rèn)是8。
下面是multi-head attention的結(jié)構(gòu)圖。可以看到QKV在輸入前后都有線性變換,總共有四次,上面dk=64=512/8
代碼表示:
原論文中說到進(jìn)行Multi-head Attention的原因是將模型分為多個(gè)頭,形成多個(gè)子空間,可以讓模型去關(guān)注不同方面的信息,最后再將各個(gè)方面的信息綜合起來。其實(shí)直觀上也可以想到,如果自己設(shè)計(jì)這樣的一個(gè)模型,必然也不會(huì)只做一次attention,多次attention綜合的結(jié)果至少能夠起到增強(qiáng)模型的作用,也可以類比CNN中同時(shí)使用多個(gè)卷積核的作用,直觀上講,多頭的注意力有助于網(wǎng)絡(luò)捕捉到更豐富的特征/信息。
5.2Position-wise Feed-forward前饋傳播
這一層很簡(jiǎn)單,就是一個(gè)全連接網(wǎng)絡(luò),包含兩個(gè)線性變換和一個(gè)非線性函數(shù)Relu;
代碼:
class PositionwiseFeedForward(nn.Module):# 這里input和output都是d_model,中間層維度是d_ff,本例設(shè)置為2048def __init__(self, d_model, d_ff, dropout=0.1):super(PositionwiseFeedForward, self).__init__()self.w_1 = nn.Linear(d_model, d_ff)self.w_2 = nn.Linear(d_ff, d_model)self.dropout = nn.Dropout(dropout)def forward(self, x):# 兩次線性變換,第一次activation是relu,第二次沒有return self.w_2(self.dropout(F.relu(self.w_1(x))))這個(gè)線性變換在不同的位置(encoder or decoder)都表現(xiàn)地一樣,并且在不同的層之間使用不同的參數(shù)。論文提到,這個(gè)公式還可以用兩個(gè)核大小為1的一維卷積來解釋,卷積的輸入輸出都是dmodel=512,中間層的維度是dff=2048
那么為什么要在multi-attention后面加一個(gè)fnn呢,類比cnn網(wǎng)絡(luò)中,cnn block和fc交替連接,效果更好。相比于單獨(dú)的multi-head attention,在后面加一個(gè)ffn,可以提高整個(gè)block的非線性變換的能力。
5.3殘差連接ResidualConnec
殘差連接其實(shí)很簡(jiǎn)單,在encoderlayer和decoderlayer里面都一樣,本文結(jié)構(gòu)如下:
那么殘差結(jié)構(gòu)有什么好處呢?顯而易見:因?yàn)樵黾恿艘豁?xiàng)x,那么該層網(wǎng)絡(luò)對(duì)x求偏導(dǎo)的時(shí)候,多了一個(gè)常數(shù)項(xiàng)1!所以在反向傳播過程中,梯度連乘,也不會(huì)造成梯度消失!
文章開始的transformer架構(gòu)圖中的Add & Norm中的Add也就是指的這個(gè)shortcut。
代碼如下:
class SublayerConnection(nn.Module):def __init__(self, size, dropout):super(SublayerConnection, self).__init__()self.norm = LayerNorm(size)self.dropout = nn.Dropout(dropout)def forward(self, x, sublayer): # 這里的x是指的輸入層src 需要對(duì)其進(jìn)行歸一化norm_x = self.norm(x)sub_x = sublayer(norm_x)sub_x = self.dropout(sub_x)return x + sub_x6.?Encoder組合
EncoderLayer由上面兩個(gè)sublayer(multihead-selfattention和residualconnection)組成;Encoder由6個(gè)EncoderLayer組成。
代碼如下:
class EncoderLayer(nn.Module):def __init__(self, size, self_attn, feed_forward, dropout):super(EncoderLayer, self).__init__()self.self_attn = self_attnself.feed_forward = feed_forwardself.residual_conn = clones(SublayerConnection(size, dropout), 2)self.size = sizedef forward(self, x, mask):x = self.residual_conn[0](x, lambda x: self.self_attn(x, x, x, mask))return self.residual_conn[1](x, self.feed_forward) class Encoder(nn.Module):def __init__(self, layer, N):super(Encoder, self).__init__()self.layers = clones(layer, N)self.norm = LayerNorm(layer.size)def forward(self, x, mask):for layer in self.layers:x = layer(x, mask)return self.norm(x)7. Decoder組合
DecoderLayer由上面三個(gè)sublayer(multihead-selfattention、multihead-contextattention和residualconnection)組成;Encoder由6個(gè)EncoderLayer組成。
代碼如下:
class DecoderLayer(nn.Module):def __init__(self, size, self_attn, src_attn, feed_forward, dropout):super(DecoderLayer, self).__init__()self.size = sizeself.self_attn = self_attnself.src_attn = src_attnself.feed_forward = feed_forwardself.residual_conn = clones(SublayerConnection(size, dropout), 3)def forward(self, x, memory, src_mask, tgt_mask):m = memoryx = self.residual_conn[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))x = self.residual_conn[1](x, lambda x: self.src_attn(x, m, m, src_mask))return self.residual_conn[2](x, self.feed_forward) class Decoder(nn.Module):def __init__(self, layer, N):super(Decoder, self).__init__()self.layers = clones(layer, N)self.norm = LayerNorm(layer.size)def forward(self, x, memory, src_mask, tgt_mask):for layer in self.layers:x = layer(x, memory, src_mask, tgt_mask)return self.norm(x)8. 損失函數(shù)和優(yōu)化器
Transfomer里的損失函數(shù)引入標(biāo)簽平滑的概念;梯度下降的優(yōu)化器引入了動(dòng)態(tài)學(xué)習(xí)率。下面詳細(xì)說明。
8.1損失函數(shù)實(shí)現(xiàn)標(biāo)簽平滑
Transformer使用的標(biāo)簽平滑技術(shù)屬于discount類型的平滑技術(shù)。這種算法簡(jiǎn)單來說就是把最高點(diǎn)砍掉一點(diǎn),多出來的概率平均分給所有人。
為什么要實(shí)現(xiàn)標(biāo)簽平滑呢,其實(shí)就是增加困惑度perplexity,每個(gè)時(shí)間步都會(huì)在一個(gè)分布集合里面隨機(jī)挑詞,那么平均情況下挑多少個(gè)詞才能挑到正確的那個(gè)呢。多挑幾次那么就意味著困惑度越高使得模型不確定性增加,但是這樣子的好處是提高了模型精度和BLEU score。
在實(shí)際實(shí)現(xiàn)時(shí),這里使用KL div loss實(shí)現(xiàn)標(biāo)簽平滑。沒有使用one-hot目標(biāo)分布,而是創(chuàng)建了一個(gè)分布,對(duì)于整個(gè)詞匯分布表,這個(gè)分布含有正確單詞度和剩余部分平滑塊的置信度。
代碼如下,簡(jiǎn)單解釋下,一般來說損失函數(shù)的輸入crit(x,target),標(biāo)簽平滑主要是在處理實(shí)際標(biāo)簽target
(1)先使用clone來把target構(gòu)造成和x一樣維度的矩陣
(2)然后使用fill_在上面的新矩陣?yán)锩嫣畛淦交蜃觭moothing
(3)然后使用scatter_把confidence(1-smoothing)填充到上面的矩陣中,按照target index數(shù)值,填充到維度對(duì)應(yīng)位置上。比如scatter_(1,(1,2,3),0.6) target新矩陣是(3,10) 那么就在10這個(gè)維度上找到index 1、2、3
(4)按照一定規(guī)則對(duì)target矩陣進(jìn)行pad mask
(5)最后使用損失函數(shù)KLDivLoss相對(duì)熵,他是求兩個(gè)概率分布之間的差異,size_averge=False損失值是sum類型,也就是說求得所有token的loss總量。
class LabelSmoothing(nn.Module):def __init__(self, size, padding_idx, smoothing=0.0):super(LabelSmoothing, self).__init__()self.criterion = nn.KLDivLoss(size_average=False)self.padding_idx = padding_idxself.confidence = 1.0 - smoothingself.smoothing = smoothingself.size = sizeself.true_dist = Nonedef forward(self, x, target):assert x.size(1) == self.sizetrue_dist = x.data.clone()true_dist.fill_(self.smoothing / (self.size - 2))true_dist.scatter_(1, target.data.unsqueeze(1), self.confidence)true_dist[:, self.padding_idx] = 0mask = torch.nonzero(target.data == self.padding_idx)if mask.dim() > 0:true_dist.index_fill_(0, mask.squeeze(), 0.0)self.true_dist = true_distreturn self.criterion(x, Variable(true_dist, requires_grad=False))舉個(gè)簡(jiǎn)單的例子,可視化感受下標(biāo)簽平滑。深藍(lán)色的T形表示target被pad的部分,黃色部分是可信confidence部分,普藍(lán)色(顏色介于黃和深藍(lán))代表模糊區(qū)間。
crit?=?LabelSmoothing(5,?1,?0.5)
predict?=torch.FloatTensor([[0.25,?0,?0.25,?0.25,?0.25],
???????????????[0.4,?0,?0.2,?0.2,?0.2],?
???????????????[0.625,?0,?0.125,?0.125,?0.125],
???????????????[0.25,?0,?0.25,?0.25,?0.25],
???????????????[0.4,?0,?0.2,?0.2,?0.2],?
???????????????[0.625,?0,?0.125,?0.125,?0.125]])
v?=?crit(Variable(predict.log()),Variable(torch.LongTensor([1,2,0,3,2,0])))
那么平滑率到底對(duì)loss下降曲線有什么影響呢,舉個(gè)簡(jiǎn)單的例子看一下。可以看到當(dāng)smooth越大,也就是說confidence越小,也就是標(biāo)簽越模糊,loss下降效果反而更好。
crits?=?[LabelSmoothing(5,?0,?0.1),
?????LabelSmoothing(5,?0,?0.05),
?????LabelSmoothing(5,?0,?0),
?????nn.NLLLoss()
?????]
def?loss(x,crit):
????d?=?x?+?3?*?1
? ? predict?=?torch.FloatTensor([[0,?x/d,?1/d,?1/d,?1/d],])
? ? return?crit(Variable(predict.log()),Variable(torch.LongTensor([1]))).item()
plt.plot(np.arange(1,?100),?[[loss(x,crit)?for?crit?in?crits]?for?x?in?range(1,?100)])
plt.legend(["0.1","0.05","0","NLLoss"])
8.2優(yōu)化器實(shí)現(xiàn)動(dòng)態(tài)學(xué)習(xí)率
我們知道學(xué)習(xí)率是梯度下降的重要因素,隨著梯度的下降,使用動(dòng)態(tài)變化的學(xué)習(xí)率,往往取的較好的效果。
這里的算法實(shí)現(xiàn)的是先warmup增大學(xué)習(xí)率,達(dá)到摸個(gè)合適的step再減小學(xué)習(xí)率。公式如下。:
可以看出來在開始的warmup steps(本例是8000)的時(shí)候?qū)W習(xí)率隨著step線性增加,然后學(xué)習(xí)率隨著步數(shù)的導(dǎo)數(shù)平方根step_num^(-0.5)成比例的減小,8000就是那個(gè)轉(zhuǎn)折點(diǎn)。
代碼如下。除去step,learningrate還和d_model,factor,warmup有關(guān)。這個(gè)優(yōu)化器封裝了對(duì)lr的修改算法
class NoamOpt:def __init__(self, model_size, factor, warmup, optimizer):self.optimizer = optimizerself._step = 0self.warmup = warmupself.factor = factorself.model_size = model_sizeself._rate = 0def step(self):self._step += 1rate = self.rate()for p in self.optimizer.param_groups:p['lr'] = rateself._rate = rateself.optimizer.step()def rate(self, step=None):if step is None:step = self._stepreturn self.factor * \(self.model_size ** (-0.5) *min(step ** (-0.5), step * self.warmup ** (-1.5)))下面把影響學(xué)習(xí)率變化的三個(gè)超參數(shù)model_size/factor/warmup進(jìn)行可視化:
opts?=?[NoamOpt(512,?1,?4000,?None),?
? ? ?NoamOpt(512,?2,?4000,?None),?
?????NoamOpt(512,?2,?8000,?None),
?????NoamOpt(512,?1,?8000,?None),
?????NoamOpt(256,?1,?4000,?None)]
plt.plot(np.arange(1,?20000),?[[opt.rate(i)?for?opt?in?opts]?for?i?in?range(1,?20000)])
plt.legend(["512:1:4000","512:2:4000","512:2:8000","512:1:8000",?"256:1:4000"])
可以看到,隨著step的增加,可以看到學(xué)習(xí)率隨著三個(gè)超參數(shù)的變化曲線:
(1)d_model越小,學(xué)習(xí)率峰值越大;
(2)factor越大,學(xué)習(xí)率峰值越大
(3)warmupsteps越大,學(xué)習(xí)率的峰值越往后推遲,且學(xué)習(xí)率峰值相對(duì)降低一些
【本文采取的超參數(shù)是512,2,8000】
8.3整合
SimpleLossCompute這個(gè)類包含了softmax+loss+optimizer三個(gè)功能。
(1)這里包含了三個(gè)功能先用generator計(jì)算linear+softmax
(2)利用criterion來計(jì)算loss?這里的loss?function是KLDivLoss?求得loss是個(gè)sum的形式?所以要根據(jù)ntokens數(shù)量來求平均loss
(3)優(yōu)化器是opt?也就是梯度下降算法?這個(gè)優(yōu)化器里面有對(duì)lr的算法
class SimpleLossCompute:def __init__(self, generator, criterion, opt=None):self.generator = generatorself.criterion = criterionself.opt = optdef __call__(self, x, y, norm):x = self.generator(x)loss = self.criterion(x.contiguous().view(-1, x.size(-1)), y.contiguous().view(-1)) / normloss.backward()if self.opt is not None:self.opt.step()self.opt.optimizer.zero_grad()return loss.item() * norm9. 模型訓(xùn)練Train
因?yàn)槲抑皇窃赾olab上訓(xùn)練,所以就是單核16GPU,20個(gè)epoch,約10000次迭代,花費(fèi)了3個(gè)多小時(shí)。
訓(xùn)練模型中包含了時(shí)間計(jì)數(shù)、loss記錄、數(shù)據(jù)和model的cuda()、step計(jì)數(shù)、學(xué)習(xí)率記錄。
代碼如下:
USE_CUDA = torch.cuda.is_available() print_every = 50 plot_every = 100 plot_losses = [] def time_since(t):now = time.time()s = now - tm = math.floor(s / 60)s -= m * 60return '%dm %ds' % (m, s) def run_epoch(data_iter, model, loss_compute):"Standard Training and Logging Function"start_epoch = time.time()total_tokens = 0total_loss = 0tokens = 0plot_loss_total = 0plot_tokens_total = 0for i, batch in enumerate(data_iter):src = batch.src.cuda() if USE_CUDA else batch.srctrg = batch.trg.cuda() if USE_CUDA else batch.trgsrc_mask = batch.src_mask.cuda() if USE_CUDA else batch.src_masktrg_mask = batch.trg_mask.cuda() if USE_CUDA else batch.trg_maskmodel = model.cuda() if USE_CUDA else modelout = model.forward(src, trg, src_mask, trg_mask)trg_y = batch.trg_y.cuda() if USE_CUDA else batch.trg_yntokens = batch.ntokens.cuda() if USE_CUDA else batch.ntokensloss = loss_compute(out, trg_y, ntokens)total_loss += lossplot_loss_total += losstotal_tokens += ntokensplot_tokens_total += ntokenstokens += ntokensif i % print_every == 1:elapsed = time.time() - start_epochprint("Epoch Step: %3d Loss: %10f time:%8s Tokens per Sec: %6.0f Step: %6d Lr: %0.8f" %(i, loss / ntokens, time_since(start), tokens / elapsed,loss_compute.opt._step if loss_compute.opt is not None else 0,loss_compute.opt._rate if loss_compute.opt is not None else 0))tokens = 0start_epoch = time.time()if i % plot_every == 1:plot_loss_avg = plot_loss_total / plot_tokens_totalplot_losses.append(plot_loss_avg)plot_loss_total = 0plot_tokens_total = 0return total_loss / total_tokens model = make_model(len(SRC.vocab), len(TGT.vocab), N=6) criterion = LabelSmoothing(size=len(TGT.vocab), padding_idx=pad_idx, smoothing=0.1) model_opt = NoamOpt(model.src_embed[0].d_model, 2, 8000,torch.optim.Adam(model.parameters(), lr=0, betas=(0.9, 0.98), eps=1e-9))訓(xùn)練結(jié)果:
start = time.time() for epoch in range(20):print('EPOCH',epoch,'--------------------------------------------------------------')model.train()run_epoch((batch_mask(pad_idx, b) for b in train_iter),model,SimpleLossCompute(model.generator, criterion, opt=model_opt))model.eval()loss=run_epoch((batch_mask(pad_idx, b) for b in valid_iter),model,SimpleLossCompute(model.generator, criterion, opt=None))print(loss)? .....step達(dá)到8000后的訓(xùn)練情況
??.....最后epoch
保存模型:
loss曲線:
10. 模型測(cè)試生成
測(cè)試生成部分利用的model.decode()函數(shù),采樣方式使用的貪婪算法,部分代碼:
def greedy_decode(model, src, src_mask, max_len, start_symbol):memory = model.encode(src, src_mask)ys = torch.ones(1, 1).fill_(start_symbol).type_as(src.data)for i in range(max_len - 1):out = model.decode(memory, src_mask, Variable(ys), Variable(subsequent_mask(ys.size(1)).type_as(src.data)))prob = model.generator(out[:, -1])_, next_word = torch.max(prob, dim=1)next_word = next_word.data[0]ys = torch.cat([ys, torch.ones(1, 1).type_as(src.data).fill_(next_word)], dim=1)return ys輸出結(jié)果:
12.注意力分布可視化?
先隨機(jī)對(duì)valid驗(yàn)證集中某個(gè)句子進(jìn)行翻譯
Source ? ? ?: non ho mai detto a nessuno che mio padre è in prigione . Target ? ? ?: I 've never told anyone that my father is in prison . Translation : I never told anyone that my father is in prison . ? ? ?
可視化:
tgt_sent = trans.split() # 翻譯數(shù)據(jù) sent = source.split() # 源數(shù)據(jù)src def draw(data, x, y, ax):seaborn.heatmap(data, xticklabels=x, square=True, yticklabels=y, vmin=0.0, vmax=1.0, cbar=False, ax=ax) for layer in range(1, 6, 2):fig, axs = plt.subplots(1, 8, figsize=(25, 15))print("Encoder Layer", layer + 1)for h in range(8):draw(model.encoder.layers[layer].self_attn.attn[0, h].data[:12, :12],sent, sent if h == 0 else [], ax=axs[h])plt.show() for layer in range(1, 6, 2):fig, axs = plt.subplots(1, 8, figsize=(25, 15))print("Decoder Self Layer", layer + 1)for h in range(8):draw(model.decoder.layers[layer].self_attn.attn[0, h].data[:len(tgt_sent), :len(tgt_sent)],tgt_sent, tgt_sent if h == 0 else [], ax=axs[h])plt.show()print("Decoder Src Layer", layer + 1)fig, axs = plt.subplots(1, 8, figsize=(25, 15))for h in range(8):draw(model.decoder.layers[layer].src_attn.attn[0, h].data[:len(tgt_sent), :len(sent)],sent, tgt_sent if h == 0 else [], ax=axs[h])plt.show()可以看到8個(gè)頭在不同注意力層里分布情況,實(shí)際上在不同的子空間學(xué)習(xí)到了不同的信息。
13.數(shù)學(xué)原理解釋Transformer和RNN本質(zhì)區(qū)別
至此,大家應(yīng)該可以感受到Transformer之所以橫掃碾壓RNN,其實(shí)是多個(gè)機(jī)制大力出奇跡的成果,并不單單是attention的應(yīng)用。
但我們深一步思考下,Transformer可以把這么多機(jī)制組合在一起而性能沒有下降是為什么呢,我個(gè)人覺得還是attention的應(yīng)用大大提升了模型并行運(yùn)算,但是只是用attention精度可能并不如人意,所以attention省下的空間和時(shí)間可以把其他能提高精度的模塊(比如position encoding、residual、mask、multi-head等等)一起添加進(jìn)來。所以從這個(gè)角度來講,attention還是transformer最核心的部分,這個(gè)大家應(yīng)該沒有異議的。
再深入一下,不管是attention還是傳統(tǒng)的rnn,其實(shí)都是為了在計(jì)算序列的hidden,RNN使用gate(sigmoid)的概念,計(jì)算hidden的權(quán)重;而attention使用softmax來計(jì)算hidden的權(quán)重,無論RNN還是attention他們計(jì)算完權(quán)重都是為了共同的目標(biāo)——求得上下文context。
先看下RNN的數(shù)學(xué)公式。
再看下attention 相關(guān)數(shù)學(xué)公式:
大家有沒有發(fā)現(xiàn)呢?RNN求得各種門是不是很像softmax求得的權(quán)重分布?這里RNN里c<t-1>和c^(t)可以類比attention公式中的V,他們都是hidden的含義。
那么我們?cè)僮屑?xì)看下RNN的門和attention的權(quán)重,是不是也能很像,都是對(duì)(query,key)使用了非線性激活函數(shù),前者使用了sigmoid,后者使用了softmax,不管使用哪個(gè)激活函數(shù)activation,其實(shí)目的都是再尋找(query,key)之間的相似度,RNN使用了加性運(yùn)算(W(a,x)+b),而Transformer使用的是乘性運(yùn)算(QK^T)。
至此是不是恍然大悟呢,這兩個(gè)經(jīng)典模型追蹤溯源竟然只是sigmoid和softmax的區(qū)別。那么我們?cè)倩仡櫹逻@兩個(gè)函數(shù):
sigmod計(jì)算是標(biāo)量,而transformer計(jì)算的是向量,my god,這不正好符合rnn和transformer的特性么?
rnn使用的是step by step順序算法,每次都是計(jì)算當(dāng)前input(query)和上一個(gè)cell傳來的hidden(key)的關(guān)系,由于一次只能喂入一個(gè),所以自然使用sigmoid的標(biāo)量屬性;但是softmax不同,他針對(duì)的是一個(gè)向量,transformer里的key可不就是一個(gè)序列所有的值么,他們的和為1,計(jì)算這個(gè)序列向量的權(quán)重分布,也就是所謂的并行計(jì)算。
網(wǎng)上很多人探討兩者的區(qū)別,但總讓我有種隔靴搔癢的感覺,花了點(diǎn)時(shí)間從數(shù)學(xué)原理的角度感知了兩者底層的本質(zhì),讓我對(duì)(QUERY,KEY,VALUE)模式有了更深的理解,希望對(duì)大家也有所幫助~
END
總結(jié)
以上是生活随笔為你收集整理的Step-by-step to Transformer:深入解析工作原理(以Pytorch机器翻译为例)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 打脸!一个线性变换就能媲美“最强句子em
- 下一篇: 一训练就显存爆炸?Facebook 推出