日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Step-by-step to Transformer:深入解析工作原理(以Pytorch机器翻译为例)

發布時間:2024/7/5 编程问答 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Step-by-step to Transformer:深入解析工作原理(以Pytorch机器翻译为例) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

大家好,我是青青山螺應如是,大家可以叫我青青,工作之余是一名獨立攝影師。喜歡美食、旅行、看展,偶爾整理下NLP學習筆記,不管技術文還是生活隨感,都會分享本人攝影作品,希望文藝的技術青年能夠喜歡~~如果有想拍寫真的妹子也可以在個人公號【青影三弄】留言~

Photograhy?Sharing

拍攝參數? ?|? ?f2.8/ 190mm/ ISO1500

設備? ?|? ?Canon6D2 / 70-200mm f2.8L III USM? ?

先分享一張在佛羅倫薩的人文攝影~

今年去意呆的時候特別熱,每天都是白晃晃的大太陽,所以我總喜歡躲到附近的教堂,那里是“免費的避暑勝地”。

“諸圣教堂”離我住處很近,當時遇到神父禱告,他還緩緩唱了首歌,第一次覺得美聲如此動人,怪不得意呆人那么熱愛歌劇,連我這種音樂小白都被感染到了~

喜歡“諸圣教堂”的另一個原因是這里埋葬著基爾蘭達約、波提切利和他愛慕的女神西蒙內塔。生前“小桶”因她創作了《維納斯的誕生》,離開人世他又得償所愿和心愛之人共眠。情深如此,我能想到的中國式浪漫大概也只有“庭有枇杷樹,吾妻死之年所手植也,今已亭亭如蓋矣”相比了..


AI sharing

之前介紹過Seq2Seq+SoftAttention這種序列模型實現機器翻譯,那么拋棄RNN,全面擁抱attention的transformer又是如何實現的呢。

本篇介紹Transformer的原理及Pytorch實現,包括一些細枝末節的trick和個人感悟,這些都是在調試代碼過程中深切領會的。網上查了很多文章,大部分基于哈佛那片論文注釋,數據集來源于tochtext自帶的英-德翻譯,但是本篇為了和上面攝影分享對應以及靈活的自定義數據集,采用意大利-英語翻譯。

CONTENT

1、Transformer簡介

2、模型概覽

3、數據加載及預處理

?? ? ?3.1原始數據構造DataFrame

? ? ? 3.2自定義Dataset

? ? ? 3.3構建字典

? ? ??3.4Iterator實現動態批量化

? ? ??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、損失函數和優化器

? ? ?8.1損失函數實現標簽平滑

? ? ?8.2優化器實現動態學習率

9、模型訓練Train

10、測試生成

11、注意力分布可視化

12、數學原理解釋transformer和rnn本質區別

【資料索取】

公眾號回復:Transformer

可獲取完整代碼

1.?Transfomer簡介

《Attention Is All Your Need》是一篇Google提出全面使用self-Attention的論文。這篇論文中提出一個全新的模型,叫 Transformer,拋棄了以往深度學習任務里面使用到的 CNN 和 RNN。目前大熱的Bert就是基于Transformer構建的,這個模型廣泛應用于NLP領域,例如機器翻譯,問答系統,文本摘要和語音識別等等方向。

眾所周知RNN雖然模型設計小巧精妙,但是其線性序列模型決定了無法實現并行,從兩個任意輸入和輸出位置獲取依賴關系都需要大量的運算,運算量嚴重受到距離的制約;而且距離不但影響性能也影響效果,隨著記憶時序的拉長,記憶削弱,導致學習能力削弱。

為了拋棄RNN step by step線性時序,Transformer使用了可以biself-attention,不依靠順序和距離就能獲得兩個位置(實質是key和value)的依賴關系(hidden)。這種計算成本減少到一個固定的運算量,雖然注意力加權會減少有效的resolution表征力,但是使用多頭multi-head attention可以彌補平均注意力加權帶來的損失。

自注意力是一種關注自身序列不同位置的注意力機制,能計算序列的表征representation。

和之前分享的Seq2Seq+SoftAttention相比,Transformer不僅關注encoder和decoder之間的attention,也關注encoder和decoder各自內部自己的attention。也就是說前者的hidden是靠lstm來實現,而transformer的encoder或者decoder的hidden是靠self-attention來實現。

2.?模型概覽

Transformer結構和Seq2Seq模型是一樣的,也采用了Encoder-Decoder結構,但Transformer更復雜。

2.1宏觀組成

Encoder由6個EncoderLayer構成,Decoder由6個DecoderLayer構成:

對應的代碼邏輯如下,make_model包含EncoderDecoder模塊,可以看到N=6表示Encoder和Decoder的子層數量,d_ff是前饋神經網絡的中間隱層維度,h=代表的是注意力層的頭數。

后面還規定了初始化的策略,如果每層參數維度大于1,那么初始化服從均勻分布init.xavier_uniform

def make_model(src_vocab, tgt_vocab, N=6, d_model=512, d_ff=2048, h=8, dropout=0.1):c = copy.deepcopyattn = MultiHeadedAttention(h, d_model)ff = PositionwiseFeedForward(d_model, d_ff, dropout)position = PositionalEncoding(d_model, dropout)model = EncoderDecoder(Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N), nn.Sequential(Embeddings(d_model, src_vocab), c(position)),nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)),Generator(d_model, tgt_vocab))for p in model.parameters():if p.dim() > 1:nn.init.xavier_uniform(p)return model

EncoderDecoder里面除了Encoder和Decoder兩個模塊,還包含embed和generator。Embed層是對對輸入進行初始化,詞嵌入包含普通的Embeddings和位置標記PositionEncoding;Generator作用是對輸出進行full linear+softmax

其中可以看到Decoder輸入的memory就是來自前面Encoder的輸出,memory會分別喂入Decoder的6個子層。

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內部結構

上面是Transformer宏觀上的結構,那Encoder和Decoder內部都有哪些不同于Seq2Seq的技術細節呢:

(1)Encoder的輸入序列經過word embedding和positional encoding后,輸入到encoder。

(2)在EncoderLayer里面先經過8個頭的self-attention模塊處理source序列自身。這個模塊目的是求得序列的hidden,利用的就是自注意力機制,而非之前RNN需要step by step算出每個hidden。然后經過一些norm和drop基本處理,再使用殘差連接模塊,目的是為了避免梯度消失問題。(后面代碼實現和上圖在實現順序上有一點出入)
(3)在EncoderLayer里面再進入Feed-Forward前饋神經網絡,實際上就是做了兩次dense,linear2(activation(linear1))。然后同上經過一些norm和drop基本處理,再使用殘差連接模塊。

(4)Decoder的輸入序列處理方式同上

(5)在DecoderLayer里面也要經過8個頭的self-attentention模塊處理target序列自身。不同于Encoder層,這里只需要關注輸入時刻t之前的部分,目的是為了符合decoder看不到未來信息的邏輯,所以這里的mask是融合了pad-mask和sequence-mask兩種。同Encoder,這個模塊目的也是為了求得target序列自身的hidden,然后經過一些norm和drop基本處理,再使用殘差連接模塊。

(6)在DecoderLayer里面再進入src-attention模塊,這個模塊也是相比Encoder增加的注意力層。其實注意力結構都是相似的,只是(query,key,value)不同,對于self-attention這三個值都是一致的,對于src-attention,query來自decoder的hidden,key和value來自encoder的hidden

(7)在DecoderLayer里面最后進入Feed-Forward前饋神經網絡,同上。

介紹完Transformer整體結構,下面從數據集處理到各層代碼實現細節進行詳細說明~

3.?數據加載及預處理

GPU環境使用Google Colab 單核16g,數據集eng-ita.txt,普通的英意翻譯對的文本數據。數據集預處理使用的是torchtext+spacy工具,他使用的整體思路是構造Dataset,字典、Iterator實現批量化、對矩陣進行mask pad。

數據預處理非常重要,這里涉及很多提高訓練性能的trick。下面具體看一下如何使用自定義數據集來完成這些預處理步驟。

3.1原始數據構造DataFrame

先加載文本,并將source和target兩列轉換為兩個獨立的list:

corpus = open('./dataset/%s-%s.txt' % ('eng', 'ita') , 'r', encoding='utf-8').readlines() random.shuffle(corpus) def prepare_data(lang1_name, lang2_name, reverse=False):print("Reading lines...")input_lang, output_lang = [], [] # rawfor parallel in corpus:so,ta = parallel[:-1].split('\t') #一行實際是英文和法文對兒 用tab來分割if so.strip() == "" or ta.strip() == "":continueinput_lang.append(so)output_lang.append(ta)if reverse:return output_lang,input_langelse:return input_lang,output_lang input_lang, output_lang = prepare_data('eng', 'ita', True)

因為torchtext的dataset的輸入需要DataFrame格式,所以這里先利用上面的source和target list構造DataFrame。訓練集、驗證集、測試集按照實際要求進行劃分:

train_list=corpus[:-3756] valid_list=corpus[-1622:] test_list=corpus[-3756:-1622] c_train={'src':input_lang[:-3756],'trg':output_lang[:-3756]} train_df=pd.DataFrame(c_train) c_valid={'src':input_lang[-1622:],'trg':output_lang[-1622:]} valid_df=pd.DataFrame(c_valid) c_test={'src':input_lang[-3756:-1622],'trg':output_lang[-3756:-1622]} test_df=pd.DataFrame(c_test)

3.2構造Dataset

這里主要包含分詞、指定起止符和補全字符以及限制序列最大長度。

因為torchtext的Dataset是由example組成,example的含義就是一條翻譯對記錄。

# 分詞 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配置信息 # 主要包含以下數據預處理的配置信息,比如指定分詞方法,是否轉成小寫,起始字符,結束字符,補全字符以及詞典等等 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構造并返回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的作用是添加進度條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 # 得到構建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) 構建Dataset數據集 # 構建Dataset數據集 # 這里的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構造字典

MIN_FREQ = 2 #統計字典時要考慮詞頻 SRC.build_vocab(train.src, min_freq=MIN_FREQ) TGT.build_vocab(train.trg, min_freq=MIN_FREQ)

3.4構造Iterator

torchtext的Iterator主要負責把Dataset進行批量劃分、字符轉數字、矩陣pad。

這里有個很重要的點就是批量化,本例使用的是動態批量化,即每個batch的批大小是不同的,是以每個batch的token數量作為統一劃分標準,也就是說每個batch的token數基本一致,這個機制是通過batch_size_fn來實現的,比如batch1[16,20],batch2是[8,40],兩者的批大小是不同的分別是16和8,但是token總數是一樣的都是320。

采取這種方式的特點就是他會把長度相同序列的聚集到一起,然后進行pad,從而減少了pad的比例,為什么要減少pad呢:

(1)padding是對計算資源的浪費,pad越多訓練耗費的時間越長。

(2)padding的計算會引入噪聲,nsformer 中,LayerNorm 會使 padding 位置的值變為非0,這會使每個 padding 都會有梯度,引起不必要的權重更新。

下圖是隨意組織pad的batch(paddings)和長度相近原則組織pad的batch(baseline)

實現代碼部分,可以看到這里MyIterator實現了data.Iterator的create_batch()函數,其實真正起作用的是里面的torchtext.data.batch()函數部分,他的功能是把原始的字符數據按照batch_size_fn算法來進行batch并且shuffle,沒有做數字化也沒有做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)

那么什么時候對batch進行數字化和pad呢,看了下源碼,實際這些操作封裝在data.Iterator.__iter__的torchtext.data.Batch這個類中。

這里還有一個問題,BATCH_SIZE這個參數設置多少合適呢,這個參數在這里的含義代表每個batch的token總量,我測試了下單核16G的colabGPU訓練環境需要6000,如果超過這個數值,內存容易爆。

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

我們知道整個模型的輸入就是src,src_mask,tgt,tgt_mask,現在src和tgt已經比較明確了,那mask部分呢,總的來說mask就是對上面的pad部分做一個統計行程對應的mask矩陣。

但是前面在講整體結構的時候提到,Decoder的mask比Encoder的mask多一層含義,就是sequence_mask,下面說下這兩類mask。

(1)padding mask

Seq2Seq+SoftAttention里面計算的mask就是padding mask。每個批次輸入序列長度是不一樣的,要對輸入序列進行對齊。具體來說,就是給在較短的序列后面填充0。因為這些填充的位置,其實是沒什么意義的,所以我們的attention機制不應該把注意力放在這些位置上,所以我們需要進行一些處理。具體的做法是,把這些位置的值機上一個非常大的復數,經過softmax這些位置就會接近0。而我們的padding mask實際上是一個張量,每個值都是一個Bool,值為False的地方就是我們要進行處理的地方。

(2)sequence mask

自然語言生成(例如機器翻譯,文本摘要)是auto-regressive的,在推理的時候只能依據之前的token生成當前時刻的token,正因為生成當前時刻的token的時候并不知道后續的token長什么樣,所以為了保持訓練和推理的一致性,訓練的時候也不能利用后續的token來生成當前時刻的token。這種方式也符合人類在自然語言生成中的思維方式。

那么具體怎么做呢,也很簡單,產生一個上三角矩陣,上三角的值全為1,下三角的值全為0,對角線也是0。把這個矩陣作用在每一個序列上,就達到我們的目的。如圖:

總結一下transformer里面的mask使用情況:

* encoder里的self-attention使用的是padding mask

* decoder里面的self-attention使用的是padding mask+sequence mask;context-attention使用的是padding mask

下面看代碼來實現上面兩種mask,其中make_std_mask里面實現了pad mask+sequence mask;

除了mask,代碼還對target部分做了處理,self.trg表示輸入,self.trg表示最終loss里面標簽角色,比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層

這一部分針對輸入模型的數據的詞嵌入處理,主要包含三個過程:普通詞嵌入word Embeddings、位置編碼PositionalEncoding、層歸一化LayerNorm。

(word Embedding是對詞匯本身編碼;Positional encoding是對詞匯的位置編碼)

4.1普通Embedding

初始化embedding matrix,通過embedding lookup將Inputs映射成token embedding,大小是[batch size, max seq length, embedding size],然后乘以embedding size的開方。那么這里為什么要乘以√dmodel ? ?論文并沒有講為什么這么做,我看了代碼,猜測是因為embedding matrix的初始化方式是xavier init,這種方式的方差是1/embedding size,因此乘以embedding size的開方使得embedding matrix的方差是1,在這個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這種時序算法保持了序列本有的順序特征,但缺點是無法串行降低了性能,transformer的主要思想self-attention在本質上拋棄了rnn這種時序特征,也就拋棄了所謂的序列順序特征,如果缺失了序列順序這個重要信息,那么結果就是所有詞語都對了,但是就是無法組成有意義的語句。那么他是怎么彌補的呢?

為了處理這個問題,transformer給encoder層和decoder層的輸入添加了一個額外的向量Positional Encoding,就是位置編碼,維度和embedding的維度一樣,這個向量采用了一種很獨特的方法來讓模型學習到這個值,這個向量能決定當前詞的位置,或者說在一個句子中不同的詞之間的距離。

這個位置向量的具體計算方法有很多種,論文中的計算方法如下sinusoidal version,這里的2i就是指的d_model這個維度上的,和pos不是一回事,pos就是可以自己定義1-5000的序列,每個數字代表一個序列位置:

上式右端表達式中pos下面的除數如果使用exp來表示的話,手寫推導如下:

代碼表示:

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是指當前詞在句子中的位置,i是指向量d_model中每個值的index,可以看出,在d_model偶數位置index,使用正弦編碼,在奇數位置,使用余弦編碼。上面公式的dmodel就是模型的維度,論文默認是512。

這個編碼的公式的意思就是:給定詞語的位置pos,我們可以把它編碼成dmodel維的向量,也就是說位置編碼的每個維度對應正弦曲線,波長構成了從2π到10000*2π的等比序列。上面的位置編碼是絕對位置編碼,但是詞語的相對位置也非常重要,這就是論文為什么使用三角函數的原因。

正弦函數能夠表達相對位置信息。主要數學依據是以下兩個公式,對于詞匯之間的位置偏移k,PE(pos+k)可以表示成PE(pos)和PE(k)的組合形式,這就是表達相對位置的能力:

可視化位置編碼效果如下,橫軸是位置序列seq_len這個維度,豎軸是d_model這個維度:

代碼中加入了dropout,具體實現如下。self.register_buffer可以將tensor注冊成buffer,網絡存儲時也會將buffer存下,當網絡load模型是,會將存儲模型的buffer也進行賦值,buffer在forward中更新而不再梯度下降中更新,optim.step只能更新nn.Parameter類型參數。

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是一種基礎數據里手段,作用于輸入和輸出,那么本例都什么時候用到呢,一個是source和target數據經過詞嵌入后進入子層前要進行層歸一化;另一個就是encoder和decoder模塊輸出的時候要進行層歸一化。

layernorm不同于batchnorm,他是在d_model這個維度上計算平均值和方差,公式如下:

可以看到這里引入了參數α和β,所以可以使用torch里面的nn. Parameter,他的作用就是初始化一個可進行訓練優化的參數,并將這個參數綁定到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_2

5. SubLayer子層組成

這里的sublayer存在于每個encoderlayer和decoderlayer中,是公用的部分,encoderlayer里面有兩個子層(mulhead-self attention/feed-forward)靠殘差層連接;decoderlayer里面有三個子層(mulhead-self attention/mulhad-context attention/feed-forward)。

這里的mulhead-self attention和mulhad-context attention是整個transformer最核心的部分。前者是自注意力機制,出現在encoder或decoder內部序列自身學習hidden;后者上下文注意力機制,相當于之前分享文章里的Seq2Seq+softattention,是encoder和decoder之間的注意力為了學習context。這兩種注意力機制結構實際上是相同的,不同的在于輸入部分(query,key,value):self-attention的三個數值都是一致的;而contex-attention的query來自decoder,key和value來自encoder。

5.1多頭MuHead Attention(self+context attention)

由于之前分享的文章詳細講解過context-attention(相當于soft-attention),所以這里詳細說明self-attention和multi-head兩種機制。

(1)self-attention

我們看個例子:

The animal didn't cross the street because it was too tired

這里的 it 到底代表的是 animal 還是 street 呢,對于人來說能很簡單的判斷出來,但是對于機器來說,是很難判斷的,self-attention就能夠讓機器把 it 和 animal 聯系起來,接下來我們看下詳細的處理過程。

首先,self-attention會計算出三個新的向量,在論文中,向量的維度是512維,我們把這三個向量分別稱為Query、Key、Value,這三個向量是用embedding向量與一個參數矩陣W相乘得到的結果,這個矩陣是隨機初始化的。

計算self-attention的分數值,該分數值決定了當我們在某個位置encode一個詞時,對輸入句子的其他部分的關注程度。這個分數值的計算方法是Query與Key做點成,以下圖為例,首先我們需要針對Thinking這個詞,計算出其他詞對于該詞的一個分數值,首先是針對于自己本身即q1·k1,然后是針對于第二個詞即q1·k2。

接下來,把點成的結果除以一個常數,這里我們除以8,這個值一般是采用上文提到的矩陣的第一個維度的開方即64的開方8,當然也可以選擇其他的值,然后把得到的結果做一個softmax的計算。得到的結果即是每個詞對于當前位置的詞的相關性大小,當然,當前位置的詞相關性肯定會會很大。

下一步就是把Value和softmax得到的值進行相乘,并相加,得到的結果即是self-attetion在當前節點的值。

在實際的應用場景,為了提高計算速度,我們采用的是矩陣的方式,直接計算出Query, Key, Value的矩陣,然后把embedding的值與三個矩陣直接相乘,把得到的新矩陣 Q 與 K 相乘,乘以一個常數,做softmax操作,最后乘上 V 矩陣。

這種通過 query 和 key 的相似性程度來確定 value 的權重分布的方法被稱為scaled dot-product attention。

結構圖如下:

(2)attention score:scaled dot-product

那么這里Transformer為什么要使用scaled dot-product來計算attention score呢?論文的描述含義就是通過確定Q和K之間的相似度來選擇V,公式:

scaled dot-product attention和dot-product attention唯一的區別就是,scaled dot-product attention有一個縮放因子:

上面公式中的dk表示的k的維度,在論文里面默認是64。為什么需要加上這個縮放因子呢,論文解釋:對于dk很大的時候,點積得到結果量級很大,方差很大,使得處于softmax函數梯度很小的區域。我們知道,梯度很小的情況,對反向傳播不利。下面簡單的測試反映出不同量級對,最大值的概率變化。

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的曲線,當x處于1~50之間不同的量級的時候,所表示的概率,當x量級在>7的時候,差不多其分配的概率就接近1了。也就是說輸入量級很大的時候,就會造成梯度消失。

為了克服這個負面影響,除以一個縮放因子可以一定程度上減緩這種情況。點積除以√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

這篇論文另一個牛的地方是給attention加入另外一個機制——multi head,該機制理解起來很簡單,就是說不僅僅只初始化一組Q、K、V的矩陣,而是初始化多組,tranformer是使用了8組,所以最后得到的結果是8個矩陣。

論文提到,他們發現將Q、K、V通過一個線性映射之后,分成h份,對每一份進行scaled dot-product attention效果更好。然后,把各個部分的結果合并起來,再次經過線性映射,得到最終的輸出。這就是所謂的multi-head attention。上面的超參數h就是heads數量。論文默認是8。

下面是multi-head attention的結構圖。可以看到QKV在輸入前后都有線性變換,總共有四次,上面dk=64=512/8

代碼表示:

class MultiHeadedAttention(nn.Module):def __init__(self, h, d_model, dropout=0.1):super(MultiHeadedAttention, self).__init__()assert d_model % h == 0self.d_k = d_model // hself.h = hself.linears = clones(nn.Linear(d_model, d_model), 4)self.attn = Noneself.dropout = nn.Dropout(p=dropout)def forward(self, query, key, value, mask=None):if mask is not None:mask = mask.unsqueeze(1)nbatches = query.size(0)query, key, value = [l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)for l, x in zip(self.linears, (query, key, value))]x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)return self.linears[-1](x)

原論文中說到進行Multi-head Attention的原因是將模型分為多個頭,形成多個子空間,可以讓模型去關注不同方面的信息,最后再將各個方面的信息綜合起來。其實直觀上也可以想到,如果自己設計這樣的一個模型,必然也不會只做一次attention,多次attention綜合的結果至少能夠起到增強模型的作用,也可以類比CNN中同時使用多個卷積核的作用,直觀上講,多頭的注意力有助于網絡捕捉到更豐富的特征/信息。

5.2Position-wise Feed-forward前饋傳播

這一層很簡單,就是一個全連接網絡,包含兩個線性變換和一個非線性函數Relu;

代碼:

class PositionwiseFeedForward(nn.Module):# 這里input和output都是d_model,中間層維度是d_ff,本例設置為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))))

這個線性變換在不同的位置(encoder or decoder)都表現地一樣,并且在不同的層之間使用不同的參數。論文提到,這個公式還可以用兩個核大小為1的一維卷積來解釋,卷積的輸入輸出都是dmodel=512,中間層的維度是dff=2048

那么為什么要在multi-attention后面加一個fnn呢,類比cnn網絡中,cnn block和fc交替連接,效果更好。相比于單獨的multi-head attention,在后面加一個ffn,可以提高整個block的非線性變換的能力。

5.3殘差連接ResidualConnec

殘差連接其實很簡單,在encoderlayer和decoderlayer里面都一樣,本文結構如下:

那么殘差結構有什么好處呢?顯而易見:因為增加了一項x,那么該層網絡對x求偏導的時候,多了一個常數項1!所以在反向傳播過程中,梯度連乘,也不會造成梯度消失!

文章開始的transformer架構圖中的Add & Norm中的Add也就是指的這個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 需要對其進行歸一化norm_x = self.norm(x)sub_x = sublayer(norm_x)sub_x = self.dropout(sub_x)return x + sub_x

6.?Encoder組合

EncoderLayer由上面兩個sublayer(multihead-selfattention和residualconnection)組成;Encoder由6個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由上面三個sublayer(multihead-selfattention、multihead-contextattention和residualconnection)組成;Encoder由6個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. 損失函數和優化器

Transfomer里的損失函數引入標簽平滑的概念;梯度下降的優化器引入了動態學習率。下面詳細說明。

8.1損失函數實現標簽平滑

Transformer使用的標簽平滑技術屬于discount類型的平滑技術。這種算法簡單來說就是把最高點砍掉一點,多出來的概率平均分給所有人。

為什么要實現標簽平滑呢,其實就是增加困惑度perplexity,每個時間步都會在一個分布集合里面隨機挑詞,那么平均情況下挑多少個詞才能挑到正確的那個呢。多挑幾次那么就意味著困惑度越高使得模型不確定性增加,但是這樣子的好處是提高了模型精度和BLEU score。

在實際實現時,這里使用KL div loss實現標簽平滑。沒有使用one-hot目標分布,而是創建了一個分布,對于整個詞匯分布表,這個分布含有正確單詞度和剩余部分平滑塊的置信度。

代碼如下,簡單解釋下,一般來說損失函數的輸入crit(x,target),標簽平滑主要是在處理實際標簽target

(1)先使用clone來把target構造成和x一樣維度的矩陣

(2)然后使用fill_在上面的新矩陣里面填充平滑因子smoothing

(3)然后使用scatter_把confidence(1-smoothing)填充到上面的矩陣中,按照target index數值,填充到維度對應位置上。比如scatter_(1,(1,2,3),0.6) target新矩陣是(3,10) 那么就在10這個維度上找到index 1、2、3

(4)按照一定規則對target矩陣進行pad mask

(5)最后使用損失函數KLDivLoss相對熵,他是求兩個概率分布之間的差異,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))

舉個簡單的例子,可視化感受下標簽平滑。深藍色的T形表示target被pad的部分,黃色部分是可信confidence部分,普藍色(顏色介于黃和深藍)代表模糊區間。

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])))

那么平滑率到底對loss下降曲線有什么影響呢,舉個簡單的例子看一下。可以看到當smooth越大,也就是說confidence越小,也就是標簽越模糊,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優化器實現動態學習率

我們知道學習率是梯度下降的重要因素,隨著梯度的下降,使用動態變化的學習率,往往取的較好的效果。

這里的算法實現的是先warmup增大學習率,達到摸個合適的step再減小學習率。公式如下。:

可以看出來在開始的warmup steps(本例是8000)的時候學習率隨著step線性增加,然后學習率隨著步數的導數平方根step_num^(-0.5)成比例的減小,8000就是那個轉折點。

代碼如下。除去step,learningrate還和d_model,factor,warmup有關。這個優化器封裝了對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)))

下面把影響學習率變化的三個超參數model_size/factor/warmup進行可視化:

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的增加,可以看到學習率隨著三個超參數的變化曲線:

(1)d_model越小,學習率峰值越大;

(2)factor越大,學習率峰值越大

(3)warmupsteps越大,學習率的峰值越往后推遲,且學習率峰值相對降低一些

【本文采取的超參數是512,2,8000】

8.3整合

SimpleLossCompute這個類包含了softmax+loss+optimizer三個功能。

(1)這里包含了三個功能先用generator計算linear+softmax

(2)利用criterion來計算loss?這里的loss?function是KLDivLoss?求得loss是個sum的形式?所以要根據ntokens數量來求平均loss

(3)優化器是opt?也就是梯度下降算法?這個優化器里面有對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() * norm

9. 模型訓練Train

因為我只是在colab上訓練,所以就是單核16GPU,20個epoch,約10000次迭代,花費了3個多小時。

訓練模型中包含了時間計數、loss記錄、數據和model的cuda()、step計數、學習率記錄。

代碼如下:

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))

訓練結果:

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達到8000后的訓練情況

??.....最后epoch

保存模型:

state = {'model': model.state_dict(), 'optimizer': model_opt, 'epoch': epoch, 'loss': loss,'plot_losses': plot_losses} torch.save(state, 'mt_transformer_it&en%02d.pth.tar' % (epoch))

loss曲線:

10. 模型測試生成

測試生成部分利用的model.decode()函數,采樣方式使用的貪婪算法,部分代碼:

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

輸出結果:

12.注意力分布可視化?

先隨機對valid驗證集中某個句子進行翻譯

for i, batch in enumerate(valid_iter):if i == 2:........out = greedy_decode(model, src, src_mask, max_len=70, start_symbol=TGT.vocab.stoi["<s>"])source = ""print("Source :", end="\t")for i in range(1, batch.src.size(0)):sym = SRC.vocab.itos[src[0, i]]if sym == "</s>" or sym == "<blank>": breakprint(sym, end=" ")source += sym + " "print("Target :", end="\t")for i in range(1, batch.trg.size(0)):sym = TGT.vocab.itos[trg[0, i]]if sym == "</s>": breakprint(sym, end=" ")trans = ""print("Translation :", end="\t")for i in range(1, out.size(1)):sym = TGT.vocab.itos[out[0, i]]if sym == "</s>": breakprint(sym, end=" ")trans += sym + " "print()

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() # 翻譯數據 sent = source.split() # 源數據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個頭在不同注意力層里分布情況,實際上在不同的子空間學習到了不同的信息。

13.數學原理解釋Transformer和RNN本質區別

至此,大家應該可以感受到Transformer之所以橫掃碾壓RNN,其實是多個機制大力出奇跡的成果,并不單單是attention的應用。

但我們深一步思考下,Transformer可以把這么多機制組合在一起而性能沒有下降是為什么呢,我個人覺得還是attention的應用大大提升了模型并行運算,但是只是用attention精度可能并不如人意,所以attention省下的空間和時間可以把其他能提高精度的模塊(比如position encoding、residual、mask、multi-head等等)一起添加進來。所以從這個角度來講,attention還是transformer最核心的部分,這個大家應該沒有異議的。

再深入一下,不管是attention還是傳統的rnn,其實都是為了在計算序列的hidden,RNN使用gate(sigmoid)的概念,計算hidden的權重;而attention使用softmax來計算hidden的權重,無論RNN還是attention他們計算完權重都是為了共同的目標——求得上下文context。

先看下RNN的數學公式。

再看下attention 相關數學公式:

大家有沒有發現呢?RNN求得各種門是不是很像softmax求得的權重分布?這里RNN里c<t-1>和c^(t)可以類比attention公式中的V,他們都是hidden的含義。

那么我們再仔細看下RNN的門和attention的權重,是不是也能很像,都是對(query,key)使用了非線性激活函數,前者使用了sigmoid,后者使用了softmax,不管使用哪個激活函數activation,其實目的都是再尋找(query,key)之間的相似度,RNN使用了加性運算(W(a,x)+b),而Transformer使用的是乘性運算(QK^T)。

至此是不是恍然大悟呢,這兩個經典模型追蹤溯源竟然只是sigmoid和softmax的區別。那么我們再回顧下這兩個函數:

sigmod計算是標量,而transformer計算的是向量,my god,這不正好符合rnn和transformer的特性么?

rnn使用的是step by step順序算法,每次都是計算當前input(query)和上一個cell傳來的hidden(key)的關系,由于一次只能喂入一個,所以自然使用sigmoid的標量屬性;但是softmax不同,他針對的是一個向量,transformer里的key可不就是一個序列所有的值么,他們的和為1,計算這個序列向量的權重分布,也就是所謂的并行計算。

網上很多人探討兩者的區別,但總讓我有種隔靴搔癢的感覺,花了點時間從數學原理的角度感知了兩者底層的本質,讓我對(QUERY,KEY,VALUE)模式有了更深的理解,希望對大家也有所幫助~

END

總結

以上是生活随笔為你收集整理的Step-by-step to Transformer:深入解析工作原理(以Pytorch机器翻译为例)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

久久理论视频 | 日韩精品播放 | 国产精品地址 | 综合天天色 | 91av电影网 | 黄色av电影网 | 日韩在线免费观看视频 | 国内精品久久久精品电影院 | 美女福利视频网 | 欧美一级在线观看视频 | 天天操人人要 | 亚洲综合欧美日韩狠狠色 | 亚州五月 | 国产精品久久久久久久久久99 | 综合久久精品 | 欧洲一区二区三区精品 | 久草五月 | 国产成人在线观看免费 | 色吊丝在线永久观看最新版本 | 中文字幕在线观看免费 | 亚洲精品国产精品久久99 | 午夜美女wwww| 9在线观看免费高清完整版在线观看明 | 免费久久99精品国产婷婷六月 | 韩国av免费在线 | 九九热精品视频在线观看 | 在线播放 日韩专区 | 成人精品99 | 国产精品福利在线播放 | 日韩成人中文字幕 | 久久久久久久免费 | 国产九九精品视频 | 中文字幕频道 | 99热最新地址 | 日p视频| 国产在线看一区 | aa级黄色大片 | 色91av| 日本aaaa级毛片在线看 | 久久一区91| 视频成人| 碰超在线 | 午夜少妇一区二区三区 | 亚洲成人av电影在线 | 狠狠操狠狠干2017 | 久草网站| 综合久久网| 丝袜美腿在线 | 国产视频一区二区在线播放 | av电影免费观看 | 国产成人精品一区一区一区 | 亚洲国产成人高清精品 | 99在线视频免费观看 | 亚洲国产欧美在线人成大黄瓜 | 久久激五月天综合精品 | 久插视频| 国产精品观看在线亚洲人成网 | 欧美性免费 | 久久久久国产一区二区 | 成人国产精品 | 国产精品一区二区三区免费看 | 国产在线视频导航 | 午夜影视剧场 | av中文资源在线 | 日韩电影一区二区三区 | 日韩欧美国产激情在线播放 | 国产成人在线综合 | 亚洲视频六区 | 成人毛片100免费观看 | 88av视频| 国产精品一区二区久久精品爱微奶 | 久久精品2 | 开心激情五月婷婷 | 国产精品手机在线播放 | 欧美精品一区二区三区一线天视频 | 99在线精品视频在线观看 | 国产一区在线免费 | 亚洲精品国产视频 | 久久狠狠亚洲综合 | 久久成人欧美 | 久久草草热国产精品直播 | www夜夜 | 国产精品乱码高清在线看 | 人人看人人做人人澡 | 国产91九色视频 | 欧美性春潮 | 欧美精品小视频 | 96精品在线 | 成人欧美一区二区三区在线观看 | 日韩一区二区免费播放 | 色永久免费视频 | 综合影视 | 国产又粗又硬又长又爽的视频 | 欧美粗又大| 四虎在线免费观看 | 99久久超碰中文字幕伊人 | av九九九| 成人在线免费av | 国产精品久久久精品 | 成人黄色毛片 | 亚洲综合射 | 九九九在线 | 中文字幕观看视频 | 免费在线观看成人 | 日韩精品一区二区三区免费观看视频 | 18pao国产成视频永久免费 | 在线蜜桃视频 | 99re久久资源最新地址 | 日韩精品一区二 | 国产精品理论片在线观看 | 在线看片日韩 | 伊人看片 | 久久小视频 | 久久a级片 | 国产黑丝一区二区三区 | 欧美国产不卡 | 久久久久免费精品 | 精品在线播放 | 成人宗合网 | 国内综合精品午夜久久资源 | 婷婷视频在线播放 | 国产美女精品视频 | 一级黄色大片在线观看 | 久久精品一二三区白丝高潮 | 玖玖在线观看视频 | 四虎5151久久欧美毛片 | 美女网站在线播放 | 91tv国产成人福利 | 国产视频九色蝌蚪 | 2020天天干夜夜爽 | 精品超碰 | 一区二区三区四区五区在线视频 | 日韩激情视频在线观看 | 不卡的av电影 | 久草资源在线观看 | 国产精品v欧美精品 | 色网站在线 | 亚洲专区在线播放 | 波多野结衣一区 | 天天天操操操 | 精一区二区| 欧美网站黄色 | 天天干天天操天天 | 午夜久久福利 | 精品亚洲欧美一区 | 91成人精品一区在线播放 | 国产精品理论片在线观看 | 久久成人福利 | 亚洲国产精品久久久久婷婷884 | 在线播放国产精品 | 久久免费成人网 | 久久综合狠狠综合久久激情 | 五月天丁香 | 中文字幕在线播放日韩 | 91av资源网 | 国产 日韩 中文字幕 | 国产蜜臀av | 在线免费观看一区二区三区 | 国产精品久久久精品 | 中文字幕不卡在线88 | 国产精品网址在线观看 | 亚洲国产片 | 久久精品国产亚洲a | 人人超碰免费 | 波多野结衣电影一区 | 日本久久久精品视频 | 外国av网 | 国产精品永久久久久久久久久 | 成人一区在线观看 | 欧美美女视频在线观看 | 伊人色播 | 国产精品久久久久久久久久久免费看 | 激情欧美xxxx | 久久久久亚洲精品 | 在线精品视频免费播放 | 粉嫩av一区二区三区入口 | www.av免费 | 天天干天天射天天操 | 欧美成人性网 | 超碰人人草| 国产一级片免费观看 | 久草成人在线 | 99视频在线观看免费 | 99久久精品国产欧美主题曲 | 91精品在线播放 | 一级黄色大片在线观看 | 亚洲精品www久久久久久 | 久99久中文字幕在线 | 91九色自拍| 夜夜夜影院 | 国产999精品久久久久久绿帽 | 超碰资源在线 | 久久午夜色播影院免费高清 | 久久国产成人午夜av影院潦草 | 中文字幕在线播出 | 久久久久免费网 | 精品高清美女精品国产区 | 涩涩网站免费 | 久久大片| 亚洲涩综合 | 91九色国产在线 | 欧美日韩视频精品 | 丁香av在线 | 欧美激情视频一区二区三区免费 | 349k.cc看片app| 国产香蕉97碰碰久久人人 | 久久99久久精品国产 | 香蕉网在线播放 | 成人午夜在线电影 | 黄免费网站 | 国产自在线 | 五月婷婷在线观看视频 | 最新色站 | 日黄网站| 亚洲禁18久人片 | 久久久久成 | 91超级碰碰| 成人欧美日韩国产 | 天天爽夜夜爽人人爽曰av | 国产999视频| 成人在线一区二区三区 | 麻豆免费在线播放 | 日韩欧美精品在线观看视频 | 91热爆在线观看 | 五月婷婷色 | 亚洲无吗av| 欧美日韩a视频 | 日韩乱码中文字幕 | 国产精品久久久久久久午夜片 | 一区二区三区四区不卡 | 免费成人av电影 | 亚洲美女视频在线 | 色综合久久五月天 | 日本激情动作片免费看 | 最近中文字幕久久 | 日韩精品免费在线观看 | 久久美女电影 | 欧美性护士 | 婷婷六月激情 | 在线观看日韩免费视频 | 久久久久久久国产精品视频 | 日韩免费在线看 | 91精品亚洲影视在线观看 | www.av小说| 人人插人人舔 | 久久精品国产亚洲aⅴ | 天天色成人网 | 亚洲一级国产 | 亚洲精品国产精品国 | 日韩成人在线一区二区 | jizz欧美性9 国产一区高清在线观看 | 亚洲精品久久久久999中文字幕 | 激情五月色播五月 | 激情开心色 | 96精品视频| 免费看黄色小说的网站 | 日韩在线视频看看 | 国产另类av | 久久人人97超碰国产公开结果 | 亚洲一区二区高潮无套美女 | 中文乱码视频在线观看 | 欧美在线一二区 | 毛片的网址 | 亚洲成av人片在线观看www | 国产特黄色片 | 波多野结衣一区二区 | 手机成人免费视频 | 中国黄色一级大片 | 91大神在线观看视频 | 久久综合狠狠 | 色婷婷视频网 | 精品一区二区三区在线播放 | 免费三级网 | 国产 中文 日韩 欧美 | 精品国产观看 | 九九综合九九 | 久久视频热 | 亚洲开心激情 | 日韩精品一区二区不卡 | 久久免费美女视频 | 伊人亚洲综合网 | 97精品超碰一区二区三区 | 国产精品99久久99久久久二8 | 亚洲日本在线一区 | 特级西西www44高清大胆图片 | 91黄色小视频 | 日日夜夜天天操 | 欧美日本一区 | 久草免费在线 | 中文字幕在线字幕中文 | 久久久久免费网 | 免费久久99精品国产 | 成人久久18免费网站 | 日韩av黄 | 天堂在线免费视频 | 国产一级不卡视频 | 国产精品久久久久久妇 | 黄色成年片 | 久久精品一区二区三区国产主播 | 日韩网站免费观看 | 欧美激情视频在线免费观看 | 97爱| 美女网色 | 国产一区二区三区免费在线 | 亚洲精品tv久久久久久久久久 | 国产视频97 | 久久99精品久久久久久 | 精品9999 | 国产精品美女久久久久久2018 | 激情五月网站 | 四虎成人精品永久免费av九九 | 国产视频一区二区三区在线 | 国产福利精品在线观看 | 久99久久 | 成人久久久电影 | 在线播放精品一区二区三区 | 欧美综合在线视频 | 久久免费大片 | 91成人小视频 | 夜夜爽www | 午夜骚影| 在线观看日本韩国电影 | 天天操天天射天天爱 | 天天做综合网 | 日韩伦理一区二区三区av在线 | 亚洲精品动漫成人3d无尽在线 | 日韩二区三区在线观看 | 香蕉免费 | 国产亚洲精品v | 久久久久久久久久久影视 | 国产亚洲婷婷免费 | 欧美日韩高清一区二区三区 | 日本性生活一级片 | 国产精品久久久久久久av大片 | free,性欧美 九九交易行官网 | 国产精品 国内视频 | 中文字幕免费国产精品 | 日韩久久精品一区二区三区 | 久久成人亚洲欧美电影 | 欧美一级特黄高清视频 | 亚洲影院一区 | 91伊人久久大香线蕉蜜芽人口 | 免费视频97| 久久国产影院 | 中文字幕在线免费 | 91中文字幕永久在线 | 婷婷丁香激情五月 | 国产精品露脸在线 | 色婷婷国产精品一区在线观看 | 国产免费不卡 | 亚洲激情一区二区三区 | 天天色天天操综合 | 在线观看精品黄av片免费 | 99精品在线视频播放 | 亚洲国产一区av | 成年人免费av | 国产视频亚洲 | 日韩大片免费在线观看 | 久久久黄视频 | 激情五月播播久久久精品 | 亚洲免费观看视频 | 国产中文字幕在线 | 国产精品无av码在线观看 | 日日夜夜精品视频天天综合网 | 亚洲欧洲精品久久 | 国产一区精品在线观看 | 四虎成人av | 91福利区一区二区三区 | 国产69精品久久app免费版 | 亚洲韩国一区二区三区 | 天天激情站| 在线观看免费视频 | 国产毛片久久久 | 亚洲3级| 久久天天综合网 | 成人影片在线播放 | 日韩成人看片 | 免费裸体视频网 | 在线成人性视频 | 亚洲综合小说 | 国产97免费 | 亚洲高清av| 国产视频在线免费观看 | 亚洲欧美日本国产 | 91香蕉国产在线观看软件 | 在线精品亚洲 | 69国产盗摄一区二区三区五区 | 亚洲精品www久久久 www国产精品com | 成人h视频在线 | 99久久精品日本一区二区免费 | 中文字幕日韩伦理 | 婷婷成人亚洲综合国产xv88 | 国产午夜精品一区二区三区嫩草 | 久久久久久久久久久网站 | 国产免费人成xvideos视频 | 不卡的av电影 | 精品久久久久久久久久岛国gif | 天天色天天爱天天射综合 | 九九在线精品视频 | 99电影 | 日韩精品久久久久久久电影竹菊 | 亚洲精品中文字幕视频 | 日韩一区二区三区免费视频 | 久久av免费 | 天天操网址 | 色综合久久99| 97理论电影 | 久久1电影院| 九色91视频 | 九色视频网| 国产亚洲精品久久久久5区 成人h电影在线观看 | 免费观看的黄色片 | 国产一级免费片 | 欧美激情综合色综合啪啪五月 | 91成人蝌蚪| 四虎永久免费 | 四虎在线免费视频 | 欧美日韩视频在线一区 | 久久艹免费 | 精品在线观看一区二区 | 久久国产精品电影 | 亚洲天堂va| 一区二区三区在线观看免费视频 | 色免费在线 | 欧美日韩另类在线 | 福利视频网站 | 国产精品久久免费看 | 香蕉手机在线 | 久久精品三级 | 欧美美女激情18p | 丁香婷婷激情国产高清秒播 | 97成人在线视频 | 国产69久久久欧美一级 | 国产免费激情久久 | 亚洲综合少妇 | 伊人资源站 | 欧美99精品 | 天天操,夜夜操 | 91亚洲欧美激情 | 男女全黄一级一级高潮免费看 | 国产精品日韩久久久久 | 国产精品高清在线观看 | 欧美资源在线观看 | 日韩视频一区二区三区 | 国产流白浆高潮在线观看 | 亚洲一区二区视频在线 | 最近中文字幕视频完整版 | 日韩精品视频在线观看网址 | www色com| 亚洲激精日韩激精欧美精品 | 在线免费观看黄色av | 免费亚洲精品视频 | 激情五月在线 | av黄色免费网站 | 久久午夜色播影院免费高清 | 久久久久亚洲a | 深夜免费福利网站 | 久久av中文字幕片 | 91欧美视频网站 | 国产午夜视频在线观看 | 欧美在线观看禁18 | 欧美国产日韩中文 | 激情图片久久 | 精品视频久久久久久 | 黄色免费视频在线观看 | 国产成人精品久久久久 | 天天亚洲| 欧美大片www | 91影视成人 | 亚洲精品国产精品国自产在线 | 中文字幕第一页在线播放 | 日韩在线观看中文 | 亚洲欧美乱综合图片区小说区 | 在线观看一 | a极黄色片| 日韩极品在线 | 在线观看亚洲成人 | 亚洲免费av在线播放 | 天天搞夜夜骑 | 激情久久久久久久久久久久久久久久 | 免费在线色电影 | 91精品啪| 99精品99 | 不卡的av在线播放 | 日韩在线免费高清视频 | www.天天射.com| 国产在线精品国自产拍影院 | 亚洲一区二区视频 | 久久精品一区二区三 | 四虎成人精品永久免费av | 午夜久久福利影院 | 黄色片免费电影 | 国产精品第二页 | 中文字幕在线观看的网站 | 亚洲视频一级 | 国产精品白丝av | 激情在线免费视频 | 色播激情五月 | bbbb操bbbb | 国产美女在线免费观看 | 国产精品美女久久久久久久 | 超碰97在线人人 | 欧美日韩久| 成人wwwxxx视频 | 六月丁香激情综合色啪小说 | 亚洲欧美一区二区三区孕妇写真 | 亚洲九九 | 伊人久久五月天 | 免费午夜在线视频 | 久久久av电影 | 久久精品免费 | 久久久久麻豆v国产 | 亚洲成人免费 | 国产精品1区| 日韩电影一区二区三区在线观看 | 人人射人人爽 | 日韩精品一区二区三区在线播放 | www.狠狠操.com | 在线观看网站黄 | av一本久道久久波多野结衣 | 精品久久久久久久久久久久久久久久 | 国产高清av免费在线观看 | 精品一区二区免费在线观看 | 日一日操一操 | 在线视频你懂得 | 毛片a级片 | 最近日本韩国中文字幕 | 麻豆国产网站 | 六月激情丁香 | 99视频+国产日韩欧美 | 久久a热6| 色综合天天 | 久久久久久福利 | 久久第四色 | 一区二区网| 亚洲国产一区二区精品专区 | 成人黄色中文字幕 | 久久精品99 | 最新中文字幕在线资源 | 国产成人在线一区 | 欧美日韩视频在线 | 永久免费的av电影 | 国产成人av一区二区三区在线观看 | 免费高清在线视频一区· | 天天射天天搞 | 成人永久在线 | 99精品视频在线观看播放 | 国产美女搞久久 | 91免费网站在线观看 | av大全在线免费观看 | 日韩激情av在线 | 日韩sese| 91丨九色丨高潮 | 亚洲天堂网视频 | 四月婷婷在线观看 | 成 人 黄 色视频免费播放 | 97超视频 | 日韩国产精品一区 | 男女啪啪免费网站 | 97夜夜澡人人爽人人免费 | 999热视频 | 91精品免费看 | 国产又粗又猛又色又黄视频 | 天天天干夜夜夜操 | 黄网在线免费观看 | 日韩av三区 | 日韩激情第一页 | 欧美亚洲另类在线视频 | 国产小视频精品 | 91成人亚洲 | 狠狠ri| 日本精品视频在线 | 免费看的黄色录像 | 园产精品久久久久久久7电影 | bbb搡bbb爽爽爽 | 婷婷综合激情 | 97超级碰碰碰视频在线观看 | 国产精品电影一区 | 国产精品久久久久久久久久久久午夜 | 精品嫩模福利一区二区蜜臀 | 草久中文字幕 | 婷婷久久网站 | 亚洲最大激情中文字幕 | 人人爽久久久噜噜噜电影 | 国产精品伦一区二区三区视频 | 麻豆视频www| 在线观看一级 | 91九色视频| 亚洲美女久久 | 日韩av一区二区三区 | 欧美成人免费在线 | 久久视屏网 | 天天射天 | 91麻豆精品 | 国内精品久久久久久久影视简单 | 在线观看成人福利 | 久久视频这里有久久精品视频11 | 成人av电影免费 | 国产一级免费电影 | 天天爽天天搞 | 伊人影院在线观看 | 色中文字幕在线观看 | 久久av高清 | 波多野结衣电影一区二区 | www.伊人网| 国产伦精品一区二区三区… | 亚洲在线视频免费 | 操操综合网| 99爱精品在线 | 色视频在线 | 九九九在线观看 | 77国产精品 | 国产精品 日韩 | 欧美精品在线免费 | 香蕉视频在线看 | 成人综合婷婷国产精品久久免费 | 99在线精品视频在线观看 | 日本天天操 | 国产精品美乳一区二区免费 | 亚洲人xxx | 成人在线观看资源 | 在线观看黄网站 | 91视频啊啊啊 | 碰超在线 | 激情视频免费观看 | 中文字幕制服丝袜av久久 | 日韩在线视频二区 | 国产美女无遮挡永久免费 | 狠狠色丁香久久婷婷综合_中 | 69绿帽绿奴3pvideos | 欧洲在线免费视频 | 免费高清在线一区 | 欧美久久久久久久久 | 国产精品资源在线 | 成人av电影免费在线播放 | 美女网站免费福利视频 | 玖玖玖影院 | 久久av黄色| 国产视频在线观看免费 | 色综合久久88色综合天天 | 91视频观看免费 | 日本中文字幕在线 | 国产精品黄色影片导航在线观看 | 中文字幕乱码电影 | 色欧美综合 | 娇妻呻吟一区二区三区 | 成人h视频在线 | 在线 视频 一区二区 | 日韩xxxx视频 | 中文在线字幕免 | 国产在线播放一区 | 中文字幕av全部资源www中文字幕在线观看 | 狠狠色丁香婷婷综合久小说久 | av在线官网 | 国产在线一卡 | 黄色大片网 | 久久婷婷色综合 | 国产一级二级在线观看 | 91豆花在线 | 玖玖爱在线观看 | 国产九九热视频 | 亚洲在线a| 午夜影院先| 91片在线观看 | 国语自产偷拍精品视频偷 | 久草精品在线观看 | 国产一级免费电影 | 国产成人精品久久二区二区 | 91久久久久久久一区二区 | 久久久久女人精品毛片九一 | 2023亚洲精品国偷拍自产在线 | 一级性视频| 精品91久久久久 | 久久亚洲精品电影 | 97超在线 | 欧美一区二区日韩一区二区 | 欧美成人精品三级在线观看播放 | 精品国产免费久久 | 最新国产精品视频 | 欧美在线观看视频一区二区三区 | 亚洲成人网在线 | 天天色天天操综合 | 欧美性爽爽 | 精品久久一区 | 玖玖视频精品 | 午夜精品视频福利 | 日韩一区二区三区高清免费看看 | 狠狠88综合久久久久综合网 | 久久久天天操 | 在线视频 一区二区 | www.狠狠色| 国产亚洲精品久久久久久大师 | 美女在线免费观看视频 | 高清视频一区 | 色国产精品一区在线观看 | 夜夜夜| 日韩欧美网址 | www日韩高清| 亚洲欧洲国产精品 | 亚洲成人网在线 | 国产精品乱码久久久久久1区2区 | 日韩激情网 | 成年人在线免费看视频 | 999电影免费在线观看 | 欧美日韩视频在线一区 | 日韩免费在线视频观看 | 成人va天堂 | 亚洲精品视频大全 | 亚洲精品国偷拍自产在线观看 | 久久久视频在线 | 日本爽妇网 | 国产精品一区二区无线 | 久久私人影院 | 国产午夜在线 | 国产视频日韩视频欧美视频 | 又黄又爽又色无遮挡免费 | 久久天天躁夜夜躁狠狠躁2022 | 久久精品久久久久久久 | 91成人在线观看喷潮 | 欧美一二区在线 | 五月开心色 | 99综合久久 | 91中文视频 | 五月视频 | 狠狠色丁香 | 亚洲国产激情 | 日韩在线国产 | 99热精品在线观看 | 国产一区二区三区高清播放 | 国产精品1000 | 日韩免费一区二区在线观看 | 欧美日韩一区二区在线观看 | 中文字幕一区二区三区在线观看 | 久草久草久草久草 | 亚洲精品欧洲精品 | 91久久黄色 | 国产91粉嫩白浆在线观看 | 91亚洲网站| 国产精品国内免费一区二区三区 | 天天操天天操天天 | 精品免费久久久久久 | 在线亚洲人成电影网站色www | 免费又黄又爽的视频 | 国产精品成人免费 | 亚洲国产精品日韩 | 亚洲v精品 | 亚洲 欧洲av| 国产成人精品999在线观看 | 久久无码精品一区二区三区 | 激情图片久久 | 成人性生交大片免费观看网站 | 久久精品久久久久久久 | 香蕉视频18 | 亚洲视频电影在线 | 国产视频一区在线播放 | 久久亚洲欧美日韩精品专区 | 亚洲欧美日韩精品久久奇米一区 | 欧美精品黑人性xxxx | 国产成人一区二区三区久久精品 | 国产一级免费播放 | 免费网站v | 欧美日韩在线免费观看 | 国产精品激情在线观看 | 中文字幕在线视频一区二区 | 亚洲乱码中文字幕综合 | 欧美一区二区伦理片 | 国产黑丝袜在线 | 午夜久操 | 国产精品免费一区二区三区在线观看 | 99色视频| 日韩一区二区三免费高清在线观看 | 女人18片毛片90分钟 | 天天射天天射天天 | 在线日韩精品视频 | 91视频com | 欧美激情一区不卡 | 国产黄色在线网站 | 成人国产一区 | 天天射网站 | 久久精品999| 日本公妇色中文字幕 | 久久久麻豆 | 五月天久久激情 | 玖玖精品在线 | 国产黄色电影 | 五月天六月婷 | 91完整版在线观看 | 一区二区三区四区五区六区 | 国产精品www | 婷婷香蕉 | 亚洲日本欧美在线 | 在线观看视频在线观看 | 国产精品福利在线观看 | 天堂av在线免费观看 | 国产免费三级在线观看 | 亚洲欧美国产精品va在线观看 | 成人性生交大片免费观看网站 | 激情综合色综合久久综合 | 香蕉网址 | 在线观看91 | 丁香六月天婷婷 | 亚洲精品乱码白浆高清久久久久久 | 日韩电影一区二区三区在线观看 | 国内视频一区二区 | 亚洲欧美偷拍另类 | 国产视频久久 | 久久久久久久久久影院 | 精品国产黄色片 | 人人舔人人插 | 国产一区私人高清影院 | 粉嫩一二三区 | 在线成人中文字幕 | 视频三区 | 久久高清免费 | 亚洲国产中文字幕 | 一区二区三区在线观看中文字幕 | 亚洲国产三级 | 麻豆国产精品永久免费视频 | 99 久久久久 | 亚洲欧美综合精品久久成人 | 黄色片网站av | 亚洲天天在线 | 狠狠操操操 | 美女网站视频免费黄 | 奇米777777| 97网| 一区二区精品国产 | 97夜夜澡人人双人人人喊 | 五月天综合在线 | 岛国精品一区二区 | 日韩精品一区二区在线观看视频 | 免费a视频在线 | 亚洲精品美女久久久久网站 | 国产在线国偷精品产拍免费yy | 国产精品99久久久久久久久久久久 | 97国产在线| 欧美激情综合五月色丁香小说 | 丝袜网站在线观看 | 国产精品美女999 | 91视视频在线直接观看在线看网页在线看 | 免费日韩在线 | 99色资源| www.av在线.com| 国产精品久久久久av福利动漫 | 国产高清视频免费 | 国产精品女人网站 | 热热热热热色 | 五月天六月婷 | 丁香六月婷 | 一区二区三高清 | 精品久久一区 | 精品在线观看国产 | 午夜精品久久久久久久99热影院 | 国产成人精品一区二区三区 | 婷婷网在线 | av噜噜噜在线播放 | 久久网站av | 国产成人在线精品 | 国产一区二区精品久久 | 精品理论片| 久久夜av| 97精品一区二区三区 | 日韩在线观看中文 | 国产糖心vlog在线观看 | 色婷婷播放 | 伊人黄| 亚洲91视频| 欧美一级免费高清 | 免费男女羞羞的视频网站中文字幕 | 国产高清福利在线 | 久久96| 国产视频99 | 国产原创在线 | 亚洲国产字幕 | 97视频免费看| 国产一区播放 | av亚洲产国偷v产偷v自拍小说 | 亚洲精品www久久久久久 | 一区二区国产精品 | 97精品国产97久久久久久久久久久久 | 伊人视频 | 最新av网址在线观看 | 久久久久国产精品www | 9在线观看免费高清完整版在线观看明 | 国产 日韩 在线 亚洲 字幕 中文 | 精品国产伦一区二区三区观看说明 | av在线亚洲天堂 | 国产中的精品av小宝探花 | av大全免费在线观看 | 国产高清精品在线 | 亚洲资源在线 | 日本在线观看中文字幕 | 午夜国产福利在线 | 99国产在线 | 久久精品第一页 | 久草在线最新免费 | 国产精品二区在线观看 | 成人国产一区二区 | 国产精品 国内视频 | 亚洲三级在线免费观看 | av+在线播放在线播放 | 日本乱码在线 | 亚洲视频 视频在线 | 久久成人亚洲欧美电影 | 日韩精品视频免费在线观看 | 中文字幕av播放 | 免费看的黄色录像 | 国产精品不卡在线播放 | 精品久久久久久国产偷窥 | 在线视频久 | 日韩特级黄色片 | 国产精品乱码久久久久久1区2区 | 超碰av在线| 色视频网页| 国产精品免费在线播放 | 成人黄色中文字幕 | 久久精品一区二区三区中文字幕 | 91在线播| 欧美极品久久 | 婷婷精品在线 | 久久精品一 | 人人爽人人av | 久久久久五月 | 国产69精品久久久久久久久久 | 一区 在线观看 | 中文字幕乱在线伦视频中文字幕乱码在线 | 日韩av高清| 99精品视频免费 | 国产精品四虎 | 久久香蕉电影网 | 成人久久久电影 | 久艹在线观看视频 | 91成人黄色| 欧美成人999 | 中文字幕中文中文字幕 | 四虎永久免费在线观看 | 日韩国产精品一区 | 天天干天天上 | 久久综合精品国产一区二区三区 | 久久久国内精品 | 日韩成人免费观看 | 精品亚洲免费视频 | 亚洲国产视频直播 | 精品久久一二三区 | 免费的黄色的网站 | 国产精品麻豆果冻传媒在线播放 | 日韩中文字幕免费视频 | 欧美视频不卡 | 久草电影在线观看 | japanesexxx乱女另类 | 免费久久99精品国产婷婷六月 | 国产最新视频在线观看 | www.色午夜,com| 亚洲欧美999 | 久久超级碰视频 | 国产一区精品在线观看 | 久久久精品久久日韩一区综合 | 色婷婷国产精品一区在线观看 | 亚洲欧美国产日韩在线观看 | 友田真希x88av | 五月天亚洲精品 | 国产区在线视频 | 欧美一区二区日韩一区二区 | 中文字幕在线资源 | 中文字幕免费中文 | 天天射射天天 | 国产高清亚洲 | 99久久激情 | 成人免费在线电影 | 亚洲成人黄色在线 | 欧美色综合久久 | 国产第页 | 欧美地下肉体性派对 | 国产亚洲在线观看 | 五月婷婷视频在线观看 | 日韩在线播放视频 | 欧美性高跟鞋xxxxhd | 成人精品一区二区三区电影免费 | 成年美女黄网站色大片免费看 | 亚洲一二视频 | 国产91精品看黄网站在线观看动漫 | 97国产在线播放 | 视频一区在线免费观看 | 深爱激情丁香 | 91在线精品播放 | 国产精品国产亚洲精品看不卡 | 永久免费毛片 | 欧美日韩亚洲第一 | 深爱激情久久 | 免费看的黄色小视频 | 久久91网 | 精品毛片一区二区免费看 | 午夜精品剧场 | 中文字幕乱码日本亚洲一区二区 | 国产超碰在线观看 | 国产亚洲精品美女久久 | 成人aⅴ视频 | 久久天天躁狠狠躁夜夜不卡公司 |