CV攻城獅入門VIT(vision transformer)之旅——近年超火的Transformer你再不瞭解就晚了!

語言: CN / TW / HK

theme: fancy

本文為稀土掘金技術社區首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!

🍊作者簡介:禿頭小蘇,致力於用最通俗的語言描述問題

🍊往期回顧:對抗生成網絡GAN系列——GAN原理及手寫數字生成小案例   對抗生成網絡GAN系列——DCGAN簡介及人臉圖像生成案例   對抗生成網絡GAN系列——AnoGAN原理及缺陷檢測實戰   對抗生成網絡GAN系列——EGBAD原理及缺陷檢測實戰

🍊近期目標:寫好專欄的每一篇文章

🍊支持小蘇:點贊👍🏼、收藏⭐、留言📩

 

CV攻城獅入門VIT(vision transformer)之旅——近年超火的Transformer你再不瞭解就晚了!

寫在前面

​  近年來,VIT模型真是屠戮各項榜單啊,就像是15年的resnet,不管是物體分類,目標檢測還是語義分割的榜單前幾名基本都是用VIT實現的!!!朋友,相信你點進來了也是瞭解了VIT的強大,想一睹VIT的風采。🌼🌼🌼正如我的標題所説,作為一名CV程序員,沒有接觸過NLP(自然語言處理)的內容,這給理解VIT帶來了一定的難度,但是為了緊跟時代潮流,我們還是得硬着頭皮往transformer的浪潮裏衝一衝。那麼這裏我準備做一個VIT的入門系列,打算一共分為三篇來講述,計劃如下:

  • 第一篇:介紹NLP領域的transformer,這是我們入門VIT的必經之路,我認為這也是最艱難的一步。當然我會盡可能從一個CV程序員的角度來幫助大家理解,也會秉持我寫文章的宗旨——通俗易懂,相信你耐心看完會有所收穫。🌾🌾🌾
  • 第二篇:介紹VIT,即transformer模型在視覺領域的應用,當你對第一篇transformer瞭解透徹後,這部分難度不大,所謂先苦 後甜,所以大家還是要多花些功夫在第一篇文章理解上。🌾🌾🌾
  • 第三篇:梳理VIT的代碼,讓大家對VIT有一個更加清晰的認識。大家遇到代碼也不要有畏難情緒,對於不明白的地方我們大可以 調試看看輸出的變化或者查閲文檔,總之方法總比困難多!🌾🌾🌾

​那麼下面我們就要開始了,給大家詳細的嘮嘮transformer!!!準備發車🚖🚖🚖

 

整體框架

​  在介紹transformer的整體框架之前,我先來簡單説説我們為什麼採用transformer結構,即transformer結構有什麼優勢呢?在NLP中,在transformer出現之前,主流的框架是RNN和LSTM,但這些框架都有一個共同的缺陷,就是程序難以並行化。舉個例子,我們期望用RNN來進行語言的翻譯任務,即輸入I Love China,輸出我愛中國。對於RNN來説,要是現在我們要輸出中國,就必須先輸出,這個過程是難以並行的,即我們必須先得到一些東西才能進行下一步。【注:這裏不知大家能否聽懂哈,但只要知道傳統架構有難以並行化的缺陷即可】

​  這樣的話,就可以順理成章的提出transformer了,其最主要就是解決了類似RNN框架難以並行的特點。後文我也會詳細介紹transformer是如何進行並行處理數據的。

​  現在就讓我們來看看transformer的整體框架,如下圖所示:【注:下圖圖片公式皆為論文中所截,這裏整理到了一起】

transformer-第 2 頁.drawio

​  看了上圖,不用想太多,你就是不理解,我想任誰第一眼看到這堆玩意都是懵逼的,但是沒關係,後面我會慢慢的解析這個圖。

🌷🌷🌷🌷🌷🌷🌷🌷🌷🌷

​  這一部分我想大致介紹一下這篇文章的行文安排,這樣大家應該就不會有很亂的感覺。首先我會介紹self Attention模塊和Multi-Head Attention模塊。這兩部分是transformer的核心,可以這麼説,搞懂了這兩個部分transformer你基本就掌握大部分了。接着我會講解encoder和decoderr模塊,明白的Multi-Head Attention後,其實encoder和decoder模塊就非常簡單了。最後,我會做一個總結,提出我的一些思考和看法。

🌷🌷🌷🌷🌷🌷🌷🌷🌷🌷

 

self Attention✨✨✨

​  在寫這部分之前呢,我覺得有必要提醒一下大家,對於我下面講述的內容你可能會很難理解self Attention為什麼會這麼做,我給的意見是大家先不用過多的在意,而是先了解self Attention的過程,這個過程理解後,你可能就會對self Attention產生自己獨特的認識,當然這部分介紹完後我也會給出自己的理解供大家參考。此外,這部分我會先給出self Attention的執行步驟,然後會結合代碼幫大家更深入的理解這個過程,大家務必耐心看完!!!🌱🌱🌱

​  【注:執行步驟部分的圖都為自己所畫,一方面希望能用自己的思路表述清楚這部分,另一方面也想在鍛鍊一下自己的作圖水平,作圖不易,懇請大家點贊支持,轉載請附鏈接。代碼演示部分參考這篇文章🍋🍋🍋】

執行步驟🧨🧨🧨

step1:獲取$q^i、k^i、v^i$

​  下面我就來介紹self Attention的步驟了。首先,需要有一系列的輸入,以三個輸入$a_1$、$a_2$、$a_3$ 為例,我們分別將$a_1$、$a_2$、$a_3$ 乘以$W_q$、$W_k$、$W_v$ 矩陣得到對應的$q$、$k$、$v$ ,如下圖所示:

image-20220803101912828

​  需要注意的是這裏的$W_q$、$W_k$、$W_v$ 是共享的。【注:或許你還不明白$a_1$、$a_2$、$a_3$ 怎麼通過乘一個矩陣變成$q$、$k$、$v$ 的,不用擔心,在執行步驟介紹完後,我會舉一些特例結合代碼幫大家理解這些過程,所以還是像我先前説到那樣對不理解的點先不用着急,耐心的看完你可能會有所收穫!!!】

​  在每給出一個執行步驟後,我都會列出這部分執行的圖解公式,其實這些都是一些矩陣運算,如下圖所示:

image-20220803104237300

step2:計算attention score

​  得到這些$q$、$k$、$v$ 後,我們會分別用q去乘每一個$k^T$得到一個數值$a_{ij}$,即用$q_1分別乘k_1^T、k_2^T、k_3^T$;$q_2分別乘k_1^T、k_2^T、k_3^T$;$q_3分別乘k_1^T、k_2^T、k_3^T$,如下圖所示:【注:為方便表示,先使用$q_1分別乘k_1^T、k_2^T、k_3^T$得到$a_{1,1}、a_{1,2}、a_{1,3}$】

​  $a_{1,1}、a_{1,2}、a_{1,3}$是一個數值,我們稱為attention score,其表示的是每個輸入的重要程度。這部分的圖解公式如下:

image-20220803110735729

step3:通過softmax層

​  這步就比較簡單了,即把上步得到的$a_{1,1}、a_{1,2}、a_{1,3}$經過一個softmax層得到輸出$a_{1,1}^{'}、a_{1,2}^{'}、a_{1,3}^{'}$,如下圖所示:

image-20220803111435913

​  這裏有一點我需要説明,如果你看attention的論文或者一些文章解讀,在經過softmax層前會除了一個$\sqrt {{{\rm{d}}_k}}$,起到了一個歸一化的作用,我這裏沒有除, 因為後面代碼舉例時不除這個$\sqrt {{{\rm{d}}_k}}$會更方便大家理解,至於這裏除不除$\sqrt {{{\rm{d}}_k}}$對大家理解是沒有任何影響的,而且不除$\sqrt {{{\rm{d}}_k}}$其實也是一種方法。

​  這裏在給出此步驟的圖解公式:

image-20220803112628057

step4:得到輸出$b^i$

​  得到$a_{1,1}^{'}、a_{1,2}^{'}、a_{1,3}^{'}$後,會讓其分別乘$v_1、v_2、v_3$ 再相加得到$b^1$,過程如下:

image-20220803113618444

​  這部分的圖解公式如下:

image-20220803114807030



​  上文通過$q_1分別乘k_1^T、k_2^T、k_3^T$最終得到$b^1$ ,同理我們可以通過$q_2分別乘k_1^T、k_2^T、k_3^T$和$q_3分別乘k_1^T、k_2^T、k_3^T$得到$b^2和b^3$。如下圖所示:

image-20220803115531297

​  在上述step2、step3和step4中,由於沒有介紹$b^2和b^3$的生成過程,因此只給出了有關 $b^1$的圖解公式。這裏再補充上完整的圖解公式,如下:

step2:

image-20220803144402744

step3:

image-20220803144707651

step4:

image-20220803144737742


​  最後,為讓大家理解此過程是並行的,我將步驟1到步驟4的過程整合在一起,其中$I$表示輸入的向量,通過下圖可以很明顯的看出這些矩陣運算是可以並行的,即我們把所有的輸入$a_{i}$拼在一起成為$I$,將I輸入網絡進行一系列的矩陣運算。

image-20220803145318664

 

代碼演示🧨🧨🧨

​  這部分會根據上述的理論過程結合代碼加深各位的理解。此外,這部分我也會分步驟介紹,但會細化理論部分的步驟,這樣大家理解起來會更舒服,但整體的步驟是沒有變的。

step1:準備輸入

​  我們定義的輸入有三個,它們的維度都是1×4的,將它們放在一起構成一個3×4的輸入張量,代碼如下:

```python import torch

x = [ [1, 0, 1, 0], # Input 1 [0, 2, 0, 2], # Input 2 [1, 1, 1, 1] # Input 3 ] x = torch.tensor(x, dtype=torch.float32) ```

​ 我們來看看輸入x的結果:

```python

輸出結果

tensor([[1., 0., 1., 0.], [0., 2., 0., 2.], [1., 1., 1., 1.]]) ```

image-20220803152935402

step2:初始化權重矩陣

​  我們知道要拿輸入x和權重矩陣$W_q$、$W_k$、$W_v$分別相乘得到$q$、$k$、$v$,而x的維度是3×4,為保證矩陣可乘,可設$W_q$、$W_k$、$W_v$的維度都為4×3,這樣得到的$q$、$k$、$v$都為3×3維。

```python w_query = [ [1, 0, 1], [1, 0, 0], [0, 0, 1], [0, 1, 1] ]

w_key = [ [0, 0, 1], [1, 1, 0], [0, 1, 0], [1, 1, 0] ]

w_value = [ [0, 2, 0], [0, 3, 0], [1, 0, 3], [1, 1, 0] ]

將w_query、w_key、w_value變成張量形式

w_query = torch.tensor(w_query, dtype=torch.float32) w_key = torch.tensor(w_key, dtype=torch.float32) w_value = torch.tensor(w_value, dtype=torch.float32) ```

step3:生成$Q、K、V$

​  這步就是矩陣的乘法,注意@表示矩陣的乘法,*表示矩陣按位相乘。代碼如下:

python querys = x @ w_query keys = x @ w_key values = x @ w_value

​  同樣的,我們可以看看此步得到的$Q、K、V$結果:

```python

Q

tensor([[1., 0., 2.], [2., 2., 2.], [2., 1., 3.]])

K

tensor([[0., 1., 1.], [4., 4., 0.], [2., 3., 1.]])

V

tensor([[1., 2., 3.], [2., 8., 0.], [2., 6., 3.]])
```

image-20220803155303324

step4:計算attention score

​  計算attention score其實就是計算$Q \cdot K^T$ ,代碼如下:

python attn_scores = querys @ keys.T

​  計算得到的attn_scores結果如下:

```python

attn_scores

tensor([[ 2., 4., 4.], [ 4., 16., 12.], [ 4., 12., 10.]]) ```

image-20220803161655930

​  注意,上圖只畫出了$q_1 \cdot K^T$的計算結果,為$[2., 4., 4.]$ ,同理你可以得到$q_2 \cdot K^T$ 和$q_1 \cdot K^T$的結果,分別為$[4., 16., 12.]$ 和$[4., 12., 10.]$ ,將它們組合在一起即得到了attn_scores矩陣,其維度為3×3。

step5:attn_score矩陣通過softmax層

​  將上步得到的attn_scores輸入softmax層,代碼如下:

```python from torch.nn.functional import softmax

attn_scores_softmax = softmax(attn_scores, dim=-1) ```

​  我們可以來看看attn_scores_softmax的結果:

python tensor([[6.3379e-02, 4.6831e-01, 4.6831e-01], [6.0337e-06, 9.8201e-01, 1.7986e-02], [2.9539e-04, 8.8054e-01, 1.1917e-01]])

​  上面的結果有效數字太多了,後文不好教學展示,因此我們對attn_scores_softmax的結果取小數點後一位,即attn_scores_softmax變成下列形式:

```python attn_scores_softmax = [ [0.0, 0.5, 0.5], [0.0, 1.0, 0.0], [0.0, 0.9, 0.1] ]

轉換為tensor格式

attn_scores_softmax = torch.tensor(attn_scores_softmax)

輸出attn_scores_softmax結果

tensor([[0.0000, 0.5000, 0.5000],

[0.0000, 1.0000, 0.0000],

[0.0000, 0.9000, 0.1000]])

```

step6:將attn_scores_softmax與矩陣V相乘

​  這部分代碼如下:

python outputs = attn_scores_softmax@values

​  這裏可以看一下這部分的輸出:

```python

outputs結果

tensor([[2.0000, 7.0000, 1.5000], [2.0000, 8.0000, 0.0000], [2.0000, 7.8000, 0.3000]]) ```

image-20220803170334553

​  注意:這部分不是按照參考鏈接所給代碼寫的,參考鏈接中把這步拆分成了兩個部分,還涉及到了三維矩陣的乘法,我認為是不好理解的,感興趣的可以自己去看看。

 

特別注意

🌷🌷🌷🌷🌷🌷🌷🌷🌷🌷

​  代碼演示這部分的代碼和圖是參考Illustrated: Self-Attention 這篇文章,我覺得寫的非常好,圖文並茂的展現了self Attention的過程。但是我認為這個例子似乎是有一些缺陷的,當然了,這裏所説的缺陷並沒有針對作者對self Ateention的解釋,而是這個例子不能對應我們下文提出的encoder和decoder模塊,我現在説encoder 和decoder 模塊你肯定還不明白説的是什麼,但是我這裏先提出這個例子的缺陷,大家有個印象就好。

​  那到底是什麼缺陷呢?我們可以直接來看上文step7中圖片,可以發現我們輸入的是3個4維向量,即維度為3×4;而輸出為3個三維向量,即維度為3×3。這裏的維度是不同的,這主要是由於我們在由輸入生成$Q、K、V$時所乘的權重矩陣$W_q$、$W_k$、$W_v$維度導致的。那麼輸入輸出的維度不一致為什麼會在encoder 和 decoder 出現問題呢?其實啊,在Attention操作後都會接上一個殘差模塊,這就要求Attention 操作前後輸入輸出的維度一致。

​  講到這裏,我相信大家已經知道問題就出在輸入輸出的維度上的,那麼後文我們就會默認經過Attention模塊後輸入輸出的維度保持不變。

​  這部分我沒有修改這部分代碼及圖片一方面是偷了個懶,另一方面是想讓大家更加深刻的意識到這個輸入輸出維度的問題。還有一點需要注意,在下文介紹Multi-Head Attention時是最後通過乘一個$W^o$矩陣實現的,在相關部分我也會介紹。

🌷🌷🌷🌷🌷🌷🌷🌷🌷🌷

 

小結

​  最後我們來對照整體框架的第一張圖來看看self Attention的過程,如下圖:

​  對於上圖其實有兩點和我們上文講述的有所差異,第一點是紅色底框中的Mask是可選的(opt.),我們並沒有採用,關於這個Mask我會在後文講述decoder模塊部分進行講解;還有一點是上圖採用的是Scaled Dot-Product Attention,而我們採用的是Dot-Product Attention,這兩個有什麼區別呢?其實區別我們在step3:通過softmax層有提到,即沒有除以$\sqrt {{{\rm{d}}_k}}$。 🍚🍚🍚

​  到這裏,self Attention的內容就介紹完了。我自認為講解得算是比較清楚的了,希望能對大家有所幫助。🍵🍵🍵

   

Multi-Head Attention✨✨✨

​  Multi-Head Attention稱為多頭注意力機制,其實你理解了上文的自注意力機制(self Attention),再來看這部分其實就很簡單了,下面就跟着我一起來學學吧!!!🚀🚀🚀

step1:獲取$q^i、k^i、v^i$

​  首先第一步和self Attention一模一樣,獲取$q^i、k^i、v^i$,如下圖所示:

image-20220803101912828

step2:分裂產生多個$q^{i,j}、k^{i,j}、v^{i,j}$

​  以下以兩個head為例進行闡述,即將$q^1$分裂成兩個$q^{1,1}和q^{1,2}$,將$q^2$分裂成兩個$q^{2,1}和q^{2,2}$,將$q^3$分裂成兩個$q^{3,1}和q^{3,2}$如下圖所示:

image-20220803221758211

​  那麼這個過程是怎麼進行的呢,其實也很簡單,只需要分別乘上兩個矩陣$W_1^Q$和$W_2^Q$即可。【注意:$q_1、q_2、q_3乘W_1^Q$會分別得到$q^{1,1}、q^{2,1}、q^{3,1};q_1、q_2、q_3乘W_2^Q$會分別得到$q^{1,2}、q^{2,2}、q^{3,2}$】

​  為了方便大家理解,結合特例作圖如下:即我們只需有$W_1^Q$和$W_2^Q$矩陣即可將$q$分成多個。

image-20220803224314390

​  同理,我們可以將$k和v$採用同樣的方法,即都相應的乘以兩個矩陣進行分裂,結果如下圖所示:

image-20220803234051146

step3:對所有head使用self Attention

​  我們可以將上述結果分成兩個head進行處理,如下圖所示:

image-20220803234320583

​  你會發現head1和head2就是我們前面所説的self Attention裏面的元素,這樣會從head1和head2得到對應輸出,如下圖所示:

image-20220803234902388

step4:拼接所有head輸出的結果

​  這一步我們會將上一步不同head輸出的結果進行Concat拼接,如下圖所示:

image-20220803235213939

step5:Concat後的結果乘上$W^o$矩陣

​  這一步會乘上$W^o$矩陣,其作用主要是融合之前多個head的結果,並使我們的輸出和輸入時維度保持一致,如下圖所示:【注:這裏是不是和我們介紹Self Attention模塊時講的特別注意部分很像呢,即Multi-Head Attention是通過$W^o$矩陣控制輸入輸出維度一致的】

image-20220803235704428

 

小結

​  同樣的,這裏我們也來對照整體框架中的圖片來看看Multi-Head Attenton的過程,如下圖所示:

​  你會發現這副圖畫的比較抽象,用虛影表示出多個head的情景,我想大家是能夠理解的。需要注意的一點是上圖中的Linear操作其實就是指我們對原數據乘一個矩陣進行變換。🍚🍚🍚

​  那麼到這裏,Multi-Head Attention的內容就介紹完了,希望能對大家有所幫助。🍵🍵🍵

   

encoder

​  encoder模塊結構如下圖黃色虛線框內所示:

​  首先我們要先介紹以下輸入,即上圖Input Embedding + Positional Encoding 部分,因為這部分我認為內容還是挺多的,因此放在了附錄部分,大家可先點擊查看。

​  瞭解了輸入,其實就剩下了灰色框部分,其實這部分還蠻簡單的,其主要由兩個小部分組成,其一是Multi-Head Attention+Add&Norm,其二是Feed Forward+Add&Norm。

​  我們先來介紹第一小部分,假設輸入是維度為(N,d)的矩陣,用 $I$ 來表示,首先會進入一個Multi-Head Attention模塊,這部分我們上文已經詳細介紹過了,這裏不再闡述,通過Multi-Head Attention模塊後得到輸出$B$ ,其維度同樣是(N,d)。接着使用一個殘差模塊將 $I$ 和 $B$ 加到一起得到 ${{\rm{B}}^} , 最後對{{\rm{B}}^}$ 進行Layer Normalization操作得到輸出$O_1$,其維度同樣是(N,d)。【關於Layer Normalization不瞭解的可以參考我的這篇文章:Batch_Normalization 、Layer_Normalization 、Group_Normalization你分的清楚嗎 🍤🍤🍤

​  這部分操作的表達式如下:

​      $$O_1=Layer \ Normalization(I + Multi\text{-}Head Attention(I))$$

​  是不是發現這種表達式一下子就把上圖的結構都展現出來了呢,所以數學真的很奇妙!!!🌼🌼🌼


​  接下來來介紹第二小部分。這回的輸入即為$O_1$,維度為(N,d)。首先會進入一個Feed Forward網絡,這是什麼呢,其實很簡單,就是兩個全連接層,如下圖所示:

​  經過Feed Forward層後,我們的輸出為$O_1^1$,前後尺寸保持不變。接着我們同樣會進行Add和Layer Normalization操作,最後得到輸出$O_2$,此時$O_2$的維度同樣為(N,d)。

​  這部分操作的表達式如下:

​    $$O_2=Layer \ Nomalization(O_1+Feed \ Forward \ Network(O_1))$$


​  這樣我們就算是把一個encoder網絡介紹完了,細心的同學可能會發現encoder結構圖傍邊寫了個$N×$,沒錯啦,和大家想的一樣,我們會將這個結構重複N次。重複N次就不要我講了叭,但需要強調一點,一個網絡結構要能夠重複堆疊,那麼它的輸入輸出的維度應該是一致的,很顯然我們上面介紹的結構滿足這已條件。

​  這部分是不是發現還蠻簡單滴,同樣,希望大家都有所收穫!!!🌾🌾🌾

 

decoder

​  decoder的結構如下圖黃色虛線框內所示:

​  decoder的結構相較於encoder就難多了,一共包含四個子結構(灰色框中三個),分別為Masked Multi-head Attention+Add&Norm 、Multi-Head Attention+Add&Norm 、 Feed Forward+Add&Norm 和 Linear+Softmax。

​  我覺得這部分最難理解的就是訓練和測試是不同的,下面我將分為訓練階段和測試階段來為大家講解這個decoder模塊。💐💐💐  

訓練階段

​  我們先來講講decoder的訓練階段是如何運行的。首先要明確我們的任務——將“我有一隻貓”翻譯成“I have a cat”。選用這個例子也是我看網上資料基本都是這個例子,圖片都是於此相關的,這部分我實在是不想再畫圖了,這篇文章確實寫的太久了,也太累了,所以也就偷個懶,就借用一下別人的圖啦!!!【這裏的參考鏈接我放在最後那部分,因為我看評論區博主説這些圖片是一篇英文博客上的,不過我沒找到原始博客🍋🍋🍋】

​  接着我們來看看decoder的輸入和輸出是什麼:

  • 輸入:encoder的輸出和decoder自身的輸出
  • 輸出:輸出詞的概率分佈

​  對於這個輸入輸出你現在可能還不是很理解,接下來我會慢慢分析。🥂🥂🥂

​  我覺得很有必要的一點是讓大家清楚decoder結構主要做了什麼?——decoder會根據之前的翻譯,求得目前最有可能的翻譯結果。例如輸入“”預測出第一個單詞為“I”,輸入“ I”預測下一個單詞為“have”。如下圖所示:【注:這裏的是開始的標誌,是要加在我們的輸入中的。】

img

​  這裏不知道大家能否明白,我當時看這部分時還是有所困惑的,即我們的任務不是將“我有一隻貓”翻譯成“I have a cat”嘛,為什麼這裏輸入和輸出都是英文啊?這塊我沒看到相關的解釋,可能時我們CV程序員對NLP的理解有所欠缺,我談談自己的看法——我認為大家和我進入了一個誤區,即decoder的輸入到底是什麼?通過我上文的我們可以知道decoder輸入為encoder的輸出和decoder自身的輸出。可以看到,decoder根本就沒有把“我是一隻貓”作為輸入,它會先輸入一個開始標誌,這樣會輸出“I”;接着這個“I”又反過來加到後,形成“ I”,這時將“ I”作為輸入,會得到輸出“have”。這樣描述大家是否能明白了呢?其實啊,“我是一隻貓”這個輸入只存在encoder的輸入中,在decoder中可沒有用到喔。🍜🍜🍜

​  如果大家覺得自己明白了這一部分,先給自己點個贊!!!然後我再來問大家一個問題看看你是否是真的明白了呢——為什麼我們輸入輸出的會是“I”,輸入“ I”輸出會是“have”?仔細想想喔,下自然段為大家解答。🍚🍚🍚

​  傻瓜!!!這當然是我們訓練的結果啦!!!不然這傻瓜機器怎麼會這麼智能。我簡單的畫個圖為大家解釋解釋。

​  上圖展示了我們訓練的大致過程,即我們輸入經Decoder會得到輸出,然後這個輸出會和我們期望的真實值比較,接着就是更新各種參數使這個輸出更加接近“I”。然後我們將輸出放在後構成新的輸入送入Decoder網絡得到輸出,此時再拿輸出和期望的輸出“have”比較,使兩者相似。依此類推......

​  這會大家是不是對Decoder的理解更近一步了呢?如果是的話,我就再來問大家一個問題——我們輸入得到輸出,儘管我們期望這個輸出與真實值“I”儘可能接近,但很可能我們訓練的結果不那麼準確,比如最後輸出的不是“I”而是“L”,接着我們將“L”拼在後面形成“ L”,再將其作為輸入,此時輸入都有偏差,大概率會導致此時的輸出離預期結果差距更大,這樣下去,最後的結果就更加離譜了,這就像是一步錯步步錯。那麼這應該用什麼方法解決呢?不賣關子了,這裏我們會每次都把正確的單詞序列作為輸入,即不管你一步輸出的是“I”還是“L”,我們都會將真實結果“I”拼在後形成下一步輸入,後面都是這樣。這種方式被稱為teacher-forcing,就像是一個老師在看着你,讓你每次都強制輸入正確的結果。【注:這部分只在訓練部分使用,因為我們在測試階段是沒有真實值的】


​  到這裏,我相信大家對decoder整體的訓練已經有了一個較清晰的認識。下面我就來結合decoder的結構圖來看看decoder裏到底都有些什麼。

​  首先是輸入部分,這部分我在上文中講述的已經夠清楚了。在訓練階段我們會將“ I have a cat”這五個單詞的詞向量作為輸入,需要注意的是這裏同樣加上了位置編碼,但是加了位置編碼後的維度還是一樣的,後文就不再特別強調是否加入了位置編碼。接下來會將輸入送到Masked Multi-Head Attention中,是不是發現和前面講的Multi-Head Attention有些不一樣呢,多了一個Masked。那為什麼要採用這個Masked呢,這是因為訓練時我們輸入的是所有的GT(Ground Truth),即“ I have a cat”五個詞向量,但是在測試時並不會這樣做,而是一個一個的輸入,因為此時的輸入必須包含上一步的輸出,而不全是GT。採用Masked會在訓練時掩蓋某個單詞後面的詞向量,即預測第 i 個輸出時,就要將第 i+1 之後的單詞掩蓋住,這樣就防止了訓練時某個單詞接觸了未來的信息,導致和測試時不一致。下面我將一步步帶大家看看Masked Multi-Head Attention的過程。【注:下面使用0 1 2 3 4分佈代表“ I have a cat ”,是結束標誌】

  1. 得到輸入矩陣和Mask矩陣,兩者維度一致。圖中顯示遮擋位置的值為0。可以發現單詞0只能使用單詞0的信息,單詞1可以使用單詞0和單詞1的信息。

img

  1. 通過輸入矩陣X計算得到$Q、K、V$並計算$Q \cdot K^T$

img

  1. $Q \cdot K^T$ 與Mask矩陣按位相乘,得到$Mask \ \ Q \cdot K^T$

img

  1. 對$Mask \ \ Q \cdot K^T$ 進行Softmax操作,使$Mask \ \ Q \cdot K^T$矩陣的每一行都為相加為1

  2. $Mask \ \ Q \cdot K^T$與矩陣V相乘,得到輸出Z

img

​  上述過程只展示的是一個Head的情況,輸出了Z,最後應該把所有Head的結果拼接,使最終的Z和輸入X的維度一致。

​  Masked Multi-Head Attention結束後使一個Add&LayerNormalization層,這個我在encoder中已經講述的很清楚了,這裏不再贅述。經過Add&LayerNormalization層後的輸出維度仍和輸入X維度一致。

​  接着會進入第二個Multi-Head Attention層,注意此時的$K、V$來自於encoder,而$Q$來自decoder。這樣做的好處使在decoder時,每一個詞都可以利用encoder中所有單詞的信息。接着同樣是一個Add&LayerNormalization層。

​  然後會進入Feed Forward+Add&Norm層,接着會將整個結構重複N次。

​  最後會進入Linear+Softmax層,最終輸出預測的單詞,因為 Mask 的存在,使得單詞 0 的輸出 Z(0,) 只包含單詞 0 的信息,如下:

img

​  Softmax 根據輸出矩陣的每一行預測下一個單詞,如下圖所示:

img

​  這部分我推薦大家聽聽李宏毅老師的課程:台大李宏毅21年機器學習課程 self-attention和transformer🍁🍁🍁

測試階段

​  明白了上文訓練階段decoder是怎麼工作的,那麼測試階段就很容易理解了。其實我在測試階段也有提及,主要區別就是此時我們不是一次將“ I have a cat”一起作為輸入,而是一個一個詞的輸入,並把輸出加到下一次輸入中,過程如下:

  1. 輸入,decoder輸出 I 。
  2. 輸入前面已經解碼的和 I,decoder輸出have。
  3. 輸入已經解碼的“ I have a cat”,decoder輸出解碼結束標誌位,每次解碼都會利用前面已經解碼輸出的所有單詞嵌入信息。

​ 那麼很明顯測試階段我們是無法做並行化處理的!!!🍷🍷🍷

 

總結

​  終於算是把transformer的內容講完了,這裏我給出一張Transformer的整體結構圖,我覺得畫的非常好,如下圖所示:【圖片來源於此篇文章

img

​  另外,作為CV程序員的我們,往往對CNN網絡是更加熟悉的。那麼CNN和Transformer中的self-Attention是否有什麼聯繫呢?大家可以去網上找找資料,其實CNN可以看作是一種簡化版的self-Attention,或者説self-Attention是一種複雜化的CNN,它們的大致關係如下:

img

​  我們知道越複雜的模型,往往就需要更多的參數來訓練,因此在訓練Transformer時就需要更多的數據,關於這一點在後面講述的VIT模型中會有體現,敬請期待吧!!!

​  最後的最後,還是希望大家有所收穫!!!另外,如果文章對你有所幫助,希望得到你小小的贊,這是對創作最大的支持🌹🌹🌹

 

論文下載地址

Attention Is All You Need 🍁🍁🍁

參考連接

1、Transformer中Self-Attention以及Multi-Head Attention詳解🍁🍁🍁

2、台大李宏毅21年機器學習課程 self-attention和transformer🍁🍁🍁

3、Transformer論文逐段精讀【論文精讀】🍁🍁🍁

4、ViT論文逐段精讀【論文精讀】🍁🍁🍁

5、shusheng wang 講解 Transformer模型🍁🍁🍁

6、Illustrated: Self-Attention🍁🍁🍁

7、Vision Transformer 超詳細解讀 (原理分析+代碼解讀) (一)🍁🍁🍁

8、Transformer Decoder詳解🍁🍁🍁

9、Transformer模型詳解(圖解最完整版)

 

 

附錄

input輸入解析

​  這部分來談談encoderr的輸入部分,其結構示意圖如下:

image-20220805104056605

​  上圖主要包含兩個概念,一個是Input Embedding ,一個是Positional Encoding。下面就來逐一的進行介紹。🥂🥂🥂


Input Embedding

​  我們先來看Input Embedding,何為Input Embedding呢?這裏我先賣個關子,先不介紹這個概念,而是先從我們的輸入一點點談起。現假設我們要實現一個文本翻譯任務,假設具體任務為將漢字“禿 頭 小 蘇 ”翻譯成拼音“tu tou xiao su”,這裏我們只關注輸入,此時的輸入應該是“禿 頭 小 蘇”四個漢字,但是作為程序猿的我們應該知道,這四個漢字計算機是不認識的,那麼就需要將“禿 頭 小 蘇”轉化為計算機認識的語言,一種常見的做法是獨熱編碼(one-hot編碼),如下圖所示:【對於獨熱編碼不熟悉的自行百度,這裏不再介紹】

image-20220805151216621

​  可以看出,上圖可以用一串數字表示出“禿 頭 小 蘇”這四個漢字,如用1 0 0 0表示“禿”,用0 1 0 0表示“頭”......

​  但是這種表示方法是否存在缺陷呢?大家都可以思考思考,我給出兩點如下:

  1. 這種編碼方式對於我這個案例來説貌似是還蠻不錯的,但是大家有沒有想過,對於一個文本翻譯任務來説,往往裏面有大量大量的漢字,假設有10000個,那麼一個單獨的字,如“禿”就需要一個1×10000維的矩陣來表示,而且矩陣中有9999個0,這無疑是對空間的一種浪費。
  2. 這種編碼方式無法表示兩個相關單詞的關係,如“禿”和“頭”這兩個單詞明顯是有某種內在的關係的,但是獨熱編碼卻無法表示這種關係。

​  那麼我們採用什麼方法來緩解這種問題呢?答案就是Embedding!!!🌿🌿🌿那麼何為Embedding呢,我的理解就是改變原來輸入input的維度,。比如我們現在分別先用“1”,“2”,“ 3”,“ 4” 分別代表“禿”,“頭”,“小”,“蘇”這四個字,然後將“1”,“2”,“ 3”,“ 4”送入embedding層,代碼如下:

import torch import torch.nn as nn embedding = nn.Embedding(5, 3) input = torch.IntTensor([[1,2,3,4]])

​  上文代碼(5,3)中的3就代表我們輸出每個單詞的維度,可以看一下輸出結果,如下圖所示:

image-20220805155028504

​  輸出矩陣的每一行都代表了一個詞,如第一行[0.2095 -0.6338 0.5679]代表1,即代表“禿”。

我們可以修改一下Embedding的參數,將(5,3)換成(5,4),如下:

import torch import torch.nn as nn embedding = nn.Embedding(5, 4) input = torch.IntTensor([[1,2,3,4]])

​  這時我們在來看看輸出結果,此時每個詞就是一個4維向量:

image-20220805155503600

​  通過上面代碼的演示,不知大家有沒有體會到Embedding可以控制輸入維度的作用呢。有關Embedding函數的使用請參照pytorch官網對此部分的解讀,點擊☞☞☞瞭解詳情。

​  最後我們來大致看看通過Embedding後會達到怎樣的效果:

image-20220805160030426

​  可以看出,“禿”和“頭”在某個空間中離的比較近,説明這兩個詞的相關性較大。即Embedding不僅可以控制我們輸入的維度,還可以從較高的維度去考慮一些詞,那麼會發現一些詞之前存在某種關聯。🍤🍤🍤



Positional Encoding(位置編碼)

​  首先談談我們為什麼要採用位置編碼,還記得我們前文所説的Attention操作嘛,其採用的是並行化的操作,即會將輸入一同輸入Attention,這種並行化就會導致在輸入是沒有是沒有順序的。同樣拿輸入“禿 頭 小 蘇”為列,沒有加入位置編碼時,我們不管時輸入“禿 頭 小 蘇”、“小 頭 蘇 禿”或其它等等,對我們的輸出結果是沒有任何影響的,這部分此篇文章還簡單的做了個小實驗,大家可以

參考一下。

​  通過上文的介紹,我們知道沒有位置編碼會導致不管我們的輸入順序如何變換,對於最後的結果是沒有影響的,這肯定不是我們期望看到的。那我們就給它整個位置編碼唄!可是我們應該採用什麼方式的位置編碼呢?我想大家可以很自然的想到一個,那就是一個詞標一個數字就得了唄,如下表所示:

| 詞 | 編碼 | | :--: | :--: | | 禿 | 0 | | 頭 | 1 | | 小 | 2 | | 蘇 | 3 |

​  這種編碼操作簡單,但是編碼長度是不可控的,即詞的個數越多,後面編碼詞越大,這樣的方式其實不是理想的。

​  那我們還可以使用什麼編碼方式呢?既然上述所述編碼規則是編碼長度不可控,那麼就可以通過除以詞的長度將其控制在0-1的範圍內呀,如下表所示:

| 詞 | 編碼 | | :--: | :----------------: | | 禿 | $\frac{0}{4}=0.00$ | | 頭 | $\frac{1}{4}=0.25$ | | 小 | $\frac{2}{4}=0.50$ | | 蘇 | $\frac{3}{4}=0.75$ |

​  你或許覺得這種編碼方式還是蠻不錯的,但是呢這種方式會導致結果的尺寸會隨着詞的長度變換而不斷變換,即上例中我們每個詞編碼結果的間距是0.25,但是要是我們有100個詞,有100個詞時,這個間距又會變成多少呢?這種尺度的不統一,對模型的訓練是不友好的。

​  “你一會介紹這個方法,這個方法不行;一會介紹那個方法,那個方法不行。那到底行不行!!!”,~~嗚嗚😭😭😭,大佬們別噴啊,我這是想讓大家看看有哪些思路,況且論文中所給的編碼方式也不一定是最好的,大家都可以多想想嘛。那麼下面就給各位老大爺帶來論文中關於此部分的位置編碼方式,公式如下:

​ $$PE_{pos,2i}=sin(pos/(10000^{2i/d_{model}}))$$

​ $$PE_{pos,2i+1}=cos(pos/(10000^{2i/d_{model}}))$$

​  不知道大家看到這個公式做何感想呢?反正對我來説我是懵的。下面就為大家來介紹介紹。首先來解釋一下公式中符號的含義:pos表示詞的位置,同樣拿“禿 頭 小 蘇”為例,pos=0表示第一個詞“禿”,pos=1表示第二個詞“頭”。2i2i+1表示Positional Encoding(位置編碼)的維度,這個怎麼理解呢,我們知道2i是偶數位,2i+1是奇數位,假設我們現在對“禿”字進行位置編碼,那麼位置編碼向量的第0個位置,即偶數位採用的是$PE_{pos,2i}=sin(pos/(10000^{2i/d_{model}}))$這個公式,而位置編碼向量的第1個位置,即奇數位採用的公式為$PE_{pos,2i+1}=cos(pos/(10000^{2i/d_{model}}))$ 。$d_{model}$表示輸入的維度大小,即我們上小節所述的Input Embedding。【注id的取值範圍為$[0,...,d_{mode/2}]$】

​  知道了這些符號含義,不知道大家是否有所感悟。如果感覺還差一點的話也沒關係,我相信我再舉兩個例子大家就明白了。首先還是“禿 頭 小 蘇”這個例子,我們先來看看第一個詞“禿”的位置編碼:【注:設$ d_{model}$=512】

​  再來看看“頭”的編碼,如下:

image-20220806234750519

​  我相信通過上面的例子你應該已經對這種方式的位置編碼有所瞭解了,即你知道了如何用這種方式來對某個詞進行編碼。但是你可能會問,為什麼用這個方式來進行位置編碼呢?即這種位置編碼的優勢在哪裏呢?這裏我為大家呈現3點:

  1. 每個位置都有唯一的一個位置編碼
  2. 能夠適應比訓練集裏面所有句子更長的句子,假設訓練集裏面最長的句子是有 100 個單詞,突然來了一個長度為101 的句子,則使用公式計算的方法可以計算出第101位的 Embedding。
  3. 可以讓模型很容易的計算出相對位置。【這一點似乎比較難理解,我詳細的為大家説説】

🔑🔑🔑🔑🔑🔑

第3點説明:

​  第3點説可以讓模型很容易的計算出相對位置,怎麼理解呢,其實就是説任意位置的$PE_{pos+k}$都可以被$PE_{pos}$和$PE_{k}$表示。這時候很多資料就給大家列出了一個誰都知道的三角公式,如下:

​ $$sin(\alpha + \beta)=sin(\alpha)cos(\beta)+cos(\alpha)sin(\beta)$$

​ $$cos(\alpha + \beta)=cos(\alpha)cos(\beta)-sin(\alpha)sin(\beta)$$

​  後面就沒有解釋了,這可能就是專家視角吧!!!認為誰都知道,可是我卻不認為大家都能明白其中的含義,至少我當時就沒明白。【大佬請忽略】下面我就為大家解釋解釋為什麼通過這兩個三角公式就會使任意位置的$PE_{pos+k}$都可以被$PE_{pos}$和$PE_{k}$表示,如下圖所示:【注:為方便公式書寫,這裏令${10000^{2i/{d_{model}}}} = M$】

image-20220807114609514

​  通過上圖可以看出,對於pos+k位置的位置編碼可以表示位pos位置和k位置的線性組合。這樣的線性組合意味着某個位置向量藴含了其它位置向量的信息。

​  【注:可能很多人會問為什麼這個M,即${10000^{2i/{d_{model}}}} $中的10000有什麼講究嘛,其實吧,也沒必要選用這個10000,之前看過一篇英文文章,就對這個數進行過分析,但是我現在找不着鏈接了,總之大家不用特別糾結這個10000】

🔑🔑🔑🔑🔑🔑

小結

​  最後,我們再來看看這張圖:

image-20220805104056605

​  可以看出我們最後的輸入會將Input Embedding 和Positional Encoding進行相加,那麼這就要求Input Embedding 和Positional Encoding的維度使一致的。這裏大家會不會有這樣的疑問呢,我們將Input Embedding 和Positional Encoding相加,不是會將原來表示位置信息的Positional Encoding混入到Input Embedding中了,這樣不就感覺很難再找到Positional Encoding的信息了嘛?似乎採用concat(拼接)更加合適吧!!!這裏給出一種解釋,參考的是這篇文章【Positional Encoding用$e^i$表示,Input Embedding 用$a^i$表示】

​  我們先給每一個位置的 $x^i \in R(1,d)$ append一個位置編碼的向量 $p^i \in R(1,N) $,得到一個新的輸入向量 $p^i\in R(1,d+N) $,這個向量作為新的輸入,乘以一個transformation matrix $W=\left[ \begin{array}{} {W^I}\ {{\rm{W}}^p} \end{array} \right]\in R(d+N,d)$ 。那麼:

​ $$x_p^i⋅ W =[x^ip^i]⋅\left[ \begin{array}{} {W^I}\ {{\rm{W}}^p} \end{array} \right]=x^i⋅W^I+p^i⋅W^P=a^i+e^i$$

所以,$e^i $與$ a^i $相加就等同於把原來的輸入 $x^i $concat一個表示位置的位置編碼$ p^i$ ,再做transformation。

​  大家覺得這個解釋怎麼樣呢?我當時看到就覺得這實在是太妙了。那麼這部分就為大家呈現這麼多了,同樣希望大家都收穫滿滿喔!!!🌾🌾🌾


   

如若文章對你有所幫助,那就🛴🛴🛴

         一鍵三連 (1).gif