日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 人工智能 > pytorch >内容正文

pytorch

【深度学习】搞懂 Vision Transformer 原理和代码,看这篇技术综述就够了

發布時間:2025/3/12 pytorch 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【深度学习】搞懂 Vision Transformer 原理和代码,看这篇技术综述就够了 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

??作者丨科技猛獸

編輯丨極市平臺

導讀

?

本文對Vision Transformer的原理和代碼進行了非常全面詳細的解讀,一切從Self-attention開始、Transformer的實現和代碼以及Transformer+Detection:引入視覺領域的首創DETR。>>加入極市CV技術交流群,走在計算機視覺的最前沿

Transformer 是 Google 的團隊在 2017 年提出的一種 NLP 經典模型,現在比較火熱的 Bert 也是基于 Transformer。Transformer 模型使用了 Self-Attention 機制,不采用RNN順序結構,使得模型可以并行化訓練,而且能夠擁有全局信息。本文將對Vision Transformer的原理和代碼進行非常全面的解讀??紤]到每篇文章字數的限制,每一篇文章將按照目錄的編排包含三個小節,而且這個系列會隨著Vision Transformer的發展而長期更新。

目錄

(每篇文章對應一個Section,目錄持續更新。)

  • Section 1

1 一切從Self-attention開始
1.1 處理Sequence數據的模型
1.2 Self-attention
1.3 Multi-head Self-attention
1.4 Positional Encoding

2 Transformer的實現和代碼解讀 (NIPS2017)
(來自Google Research, Brain Team)
2.1 Transformer原理分析
2.2 Transformer代碼解讀

3 Transformer+Detection:引入視覺領域的首創DETR (ECCV2020)
(來自Facebook AI)
3.1 DETR原理分析
3.2 DETR代碼解讀

  • Section 2

4 Transformer+Detection:Deformable DETR:可變形的Transformer (ICLR2021)
(來自商湯代季峰老師組)
4.1 Deformable DETR原理分析
4.2 Deformable DETR代碼解讀

5 Transformer+Classification:用于分類任務的Transformer (ICLR2021)
(來自Google Research, Brain Team)
5.1 ViT原理分析
5.2 ViT代碼解讀

6 Transformer+Image Processing:IPT:用于底層視覺任務的Transformer
(來自北京華為諾亞方舟實驗室)
6.1 IPT原理分析

  • Section 3

7 Transformer+Segmentation:SETR:基于Transformer 的語義分割
(來自復旦大學,騰訊優圖等)
7.1 SETR原理分析

8 Transformer+GAN:VQGAN:實現高分辨率的圖像生成
(來自德國海德堡大學)
8.1 VQGAN原理分析
8.2 VQGAN代碼解讀

9 Transformer+Distillation:DeiT:高效圖像Transformer
(來自Facebook AI)
9.1 DeiT原理分析

1 一切從Self-attention開始

  • 1.1 處理Sequence數據的模型:

Transformer是一個Sequence to Sequence model,特別之處在于它大量用到了self-attention。

要處理一個Sequence,最常想到的就是使用RNN,它的輸入是一串vector sequence,輸出是另一串vector sequence,如下圖1左所示。

如果假設是一個single directional的RNN,那當輸出?時,默認?都已經看過了。如果假設是一個bi-directional的RNN,那當輸出?任意時,默認?都已經看過了。RNN非常擅長于處理input是一個sequence的狀況。

那RNN有什么樣的問題呢?它的問題就在于:RNN很不容易并行化 (hard to parallel)。

為什么說RNN很不容易并行化呢?假設在single directional的RNN的情形下,你今天要算出?,就必須要先看?再看?再看?再看?,所以這個過程很難平行處理。

所以今天就有人提出把CNN拿來取代RNN,如下圖1右所示。其中,橘色的三角形表示一個filter,每次掃過3個向量?,掃過一輪以后,就輸出了一排結果,使用橘色的小圓點表示。

這是第一個橘色的filter的過程,還有其他的filter,比如圖2中的黃色的filter,它經歷著與橘色的filter相似的過程,又輸出一排結果,使用黃色的小圓點表示。

圖1:處理Sequence數據的模型圖2:處理Sequence數據的模型

所以,用CNN,你確實也可以做到跟RNN的輸入輸出類似的關系,也可以做到輸入是一個sequence,輸出是另外一個sequence。

但是,表面上CNN和RNN可以做到相同的輸入和輸出,但是CNN只能考慮非常有限的內容。比如在我們右側的圖中CNN的filter只考慮了3個vector,不像RNN可以考慮之前的所有vector。但是CNN也不是沒有辦法考慮很長時間的dependency的,你只需要堆疊filter,多堆疊幾層,上層的filter就可以考慮比較多的資訊,比如,第二層的filter (藍色的三角形)看了6個vector,所以,只要疊很多層,就能夠看很長時間的資訊。

而CNN的一個好處是:它是可以并行化的 (can parallel),不需要等待紅色的filter算完,再算黃色的filter。但是必須要疊很多層filter,才可以看到長時的資訊。所以今天有一個想法:self-attention,如下圖3所示,目的是使用self-attention layer取代RNN所做的事情。

圖3:You can try to replace any thing that has been done by RNNwith self attention

所以重點是:我們有一種新的layer,叫self-attention,它的輸入和輸出和RNN是一模一樣的,輸入一個sequence,輸出一個sequence,它的每一個輸出?都看過了整個的輸入sequence,這一點與bi-directional RNN相同。但是神奇的地方是:它的每一個輸出?可以并行化計算。

  • 1.2 Self-attention:

那么self-attention具體是怎么做的呢?

圖4:self-attention具體是怎么做的?

首先假設我們的input是圖4的?,是一個sequence,每一個input (vector)先乘上一個矩陣?得到embedding,即向量?。接著這個embedding進入self-attention層,每一個向量?分別乘上3個不同的transformation matrix?,以向量?為例,分別得到3個不同的向量?。

圖5:self-attention具體是怎么做的?

接下來使用每個query?去對每個key?做attention,attention就是匹配這2個向量有多接近,比如我現在要對?和?做attention,我就可以把這2個向量做scaled inner product,得到?。接下來你再拿?和?做attention,得到?,你再拿?和?做attention,得到?,你再拿?和?做attention,得到?。那這個scaled inner product具體是怎么計算的呢?

式中,?是?跟?的維度。因為?的數值會隨著dimension的增大而增大,所以要除以?的值,相當于歸一化的效果。

接下來要做的事如圖6所示,把計算得到的所有?值取?操作。

圖6:self-attention具體是怎么做的?

取完?操作以后,我們得到了?,我們用它和所有的?值進行相乘。具體來講,把?乘上?,把?乘上?,把?乘上?,把?乘上?,把結果通通加起來得到?,所以,今天在產生?的過程中用了整個sequence的資訊 (Considering the whole sequence)。如果要考慮local的information,則只需要學習出相應的?,?就不再帶有那個對應分支的信息了;如果要考慮global的information,則只需要學習出相應的?,?就帶有全部的對應分支的信息了。

圖7:self-attention具體是怎么做的?

同樣的方法,也可以計算出?,如下圖8所示,?就是拿query?去對其他的?做attention,得到?,再與value值?相乘取weighted sum得到的。

圖8:self-attention具體是怎么做的?

經過了以上一連串計算,self-attention layer做的事情跟RNN是一樣的,只是它可以并行的得到layer輸出的結果,如圖9所示?,F在我們要用矩陣表示上述的計算過程。

圖9:self-attention的效果

首先輸入的embedding是?,然后用?乘以transformation matrix?得到?,它的每一列代表著一個vector?。同理,用?乘以transformation matrix?得到?,它的每一列代表著一個vector?。用?乘以transformation matrix?得到?,它的每一列代表著一個vector?。

圖10:self-attention的矩陣計算過程

接下來是?與?的attention過程,我們可以把vector?橫過來變成行向量,與列向量?做內積,這里省略了?。這樣,?就成為了?的矩陣,它由4個行向量拼成的矩陣和4個列向量拼成的矩陣做內積得到,如圖11所示。

在得到?以后,如上文所述,要得到?, 就要使用?分別與?相乘再求和得到,所以?要再左乘?矩陣。

圖11:self-attention的矩陣計算過程

到這里你會發現這個過程可以被表示為,如圖12所示:輸入矩陣?分別乘上3個不同的矩陣?得到3個中間矩陣?。它們的維度是相同的。把?轉置之后與?相乘得到Attention矩陣?,代表每一個位置兩兩之間的attention。再將它取?操作得到?,最后將它乘以?矩陣得到輸出vector?。

圖12:self-attention就是一堆矩陣乘法,可以實現GPU加速
  • 1.3 Multi-head Self-attention:

還有一種multi-head的self-attention,以2個head的情況為例:由?生成的?進一步乘以2個轉移矩陣變為?和?,同理由?生成的?進一步乘以2個轉移矩陣變為?和?,由?生成的?進一步乘以2個轉移矩陣變為?和?。接下來?再與?做attention,得到weighted sum的權重?,再與?做weighted sum得到最終的?。同理得到?。現在我們有了?和?,可以把它們concat起來,再通過一個transformation matrix調整維度,使之與剛才的?維度一致(這步如圖13所示)。

圖13:multi-head self-attention圖13:調整b的維度

從下圖14可以看到 Multi-Head Attention 包含多個 Self-Attention 層,首先將輸入?分別傳遞到 2個不同的 Self-Attention 中,計算得到 2 個輸出結果。得到2個輸出矩陣之后,Multi-Head Attention 將它們拼接在一起 (Concat),然后傳入一個Linear層,得到 Multi-Head Attention 最終的輸出???梢钥吹?Multi-Head Attention 輸出的矩陣?與其輸入的矩陣?的維度是一樣的。

圖14:multi-head self-attention

這里有一組Multi-head Self-attention的解果,其中綠色部分是一組query和key,紅色部分是另外一組query和key,可以發現綠色部分其實更關注global的信息,而紅色部分其實更關注local的信息。

圖15:Multi-head Self-attention的不同head分別關注了global和local的訊息
  • 1.4 Positional Encoding:

以上是multi-head self-attention的原理,但是還有一個問題是:現在的self-attention中沒有位置的信息,一個單詞向量的“近在咫尺”位置的單詞向量和“遠在天涯”位置的單詞向量效果是一樣的,沒有表示位置的信息(No position information in self attention)。所以你輸入"A打了B"或者"B打了A"的效果其實是一樣的,因為并沒有考慮位置的信息。所以在self-attention原來的paper中,作者為了解決這個問題所做的事情是如下圖16所示:

圖16:self-attention中的位置編碼

具體的做法是:給每一個位置規定一個表示位置信息的向量?,讓它與?加在一起之后作為新的?參與后面的運算過程,但是這個向量?是由人工設定的,而不是神經網絡學習出來的。每一個位置都有一個不同的?。

那到這里一個自然而然的問題是:為什么是?與?相加?為什么不是concatenate?加起來以后,原來表示位置的資訊不就混到?里面去了嗎?不就很難被找到了嗎?

這里提供一種解答這個問題的思路:

如圖15所示,我們先給每一個位置的?append一個one-hot編碼的向量?,得到一個新的輸入向量?,這個向量作為新的輸入,乘以一個transformation matrix?。那么:

所以,與?相加就等同于把原來的輸入?concat一個表示位置的獨熱編碼?,再做transformation。

這個與位置編碼乘起來的矩陣?是手工設計的,如圖17所示。

圖17:與位置編碼乘起來的轉移矩陣WP

Transformer 中除了單詞的 Embedding,還需要使用位置 Embedding 表示單詞出現在句子中的位置。因為 Transformer 不采用 RNN 的結構,而是使用全局信息,不能利用單詞的順序信息,而這部分信息對于 NLP 來說非常重要。所以 Transformer 中使用位置 Embedding 保存單詞在序列中的相對或絕對位置。

位置 Embedding 用 PE表示,PE 的維度與單詞 Embedding 是一樣的。PE 可以通過訓練得到,也可以使用某種公式計算得到。在 Transformer 中采用了后者,計算公式如下:

式中,?表示token在sequence中的位置,例如第一個token "我" 的?。

,或者準確意義上是?和?表示了Positional Encoding的維度,的取值范圍是:?。所以當?為1時,對應的Positional Encoding可以寫成:

式中,?。底數是10000。為什么要使用10000呢,這個就類似于玄學了,原論文中完全沒有提啊,這里不得不說說論文的readability的問題,即便是很多高引的文章,最基本的內容都討論不清楚,所以才出現像上面提問里的討論,說實話這些論文還遠遠沒有做到easy to follow。這里我給出一個假想:是一個比較接近1的數(1.018),如果用100000,則是1.023。這里只是猜想一下,其實大家應該完全可以使用另一個底數。

這個式子的好處是:

  • 每個位置有一個唯一的positional encoding。

  • 使?能夠適應比訓練集里面所有句子更長的句子,假設訓練集里面最長的句子是有 20 個單詞,突然來了一個長度為 21 的句子,則使用公式計算的方法可以計算出第 21 位的 Embedding。

  • 可以讓模型容易地計算出相對位置,對于固定長度的間距?,任意位置的?都可以被?的線性函數表示,因為三角函數特性:

接下來我們看看self-attention在sequence2sequence model里面是怎么使用的,我們可以把Encoder-Decoder中的RNN用self-attention取代掉。

圖18:Seq2seq with Attention

2 Transformer的實現和代碼解讀

  • 2.1 Transformer原理分析:

圖19:Transformer

Encoder:

這個圖19講的是一個seq2seq的model,左側為 Encoder block,右側為 Decoder block。紅色圈中的部分為Multi-Head Attention,是由多個Self-Attention組成的,可以看到 Encoder block 包含一個 Multi-Head Attention,而 Decoder block 包含兩個 Multi-Head Attention (其中有一個用到 Masked)。Multi-Head Attention 上方還包括一個 Add & Norm 層,Add 表示殘差連接 (Residual Connection) 用于防止網絡退化,Norm 表示 Layer Normalization,用于對每一層的激活值進行歸一化。比如說在Encoder Input處的輸入是機器學習,在Decoder Input處的輸入是<BOS>,輸出是machine。再下一個時刻在Decoder Input處的輸入是machine,輸出是learning。不斷重復知道輸出是句點(.)代表翻譯結束。

接下來我們看看這個Encoder和Decoder里面分別都做了什么事情,先看左半部分的Encoder:首先輸入?通過一個Input Embedding的轉移矩陣?變為了一個張量,即上文所述的?,再加上一個表示位置的Positional Encoding?,得到一個張量,去往后面的操作。

它進入了這個綠色的block,這個綠色的block會重復?次。這個綠色的block里面有什么呢?它的第1層是一個上文講的multi-head的attention。你現在一個sequence,經過一個multi-head的attention,你會得到另外一個sequence?。

下一個Layer是Add & Norm,這個意思是說:把multi-head的attention的layer的輸入?和輸出?進行相加以后,再做Layer Normalization,至于Layer Normalization和我們熟悉的Batch Normalization的區別是什么,請參考圖20和21。

圖20:不同Normalization方法的對比

其中,Batch Normalization和Layer Normalization的對比可以概括為圖20,Batch Normalization強行讓一個batch的數據的某個channel的?,而Layer Normalization讓一個數據的所有channel的?。

圖21:Batch Normalization和Layer Normalization的對比

接著是一個Feed Forward的前饋網絡和一個Add & Norm Layer。

所以,這一個綠色的block的前2個Layer操作的表達式為:

這一個綠色的block的后2個Layer操作的表達式為:

所以Transformer的Encoder的整體操作為:

Decoder:

現在來看Decoder的部分,輸入包括2部分,下方是前一個time step的輸出的embedding,即上文所述的?,再加上一個表示位置的Positional Encoding?,得到一個張量,去往后面的操作。它進入了這個綠色的block,這個綠色的block會重復?次。這個綠色的block里面有什么呢?

首先是Masked Multi-Head Self-attention,masked的意思是使attention只會attend on已經產生的sequence,這個很合理,因為還沒有產生出來的東西不存在,就無法做attention。

輸出是: 對應?位置的輸出詞的概率分布。

輸入是:?的輸出對應?位置decoder的輸出。所以中間的attention不是self-attention,它的Key和Value來自encoder,Query來自上一位置?的輸出。

解碼:這里要特別注意一下,編碼可以并行計算,一次性全部Encoding出來,但解碼不是一次把所有序列解出來的,而是像?一樣一個一個解出來的,因為要用上一個位置的輸入當作attention的query。

明確了解碼過程之后最上面的圖就很好懂了,這里主要的不同就是新加的另外要說一下新加的attention多加了一個mask,因為訓練時的output都是Ground Truth,這樣可以確保預測第?個位置時不會接觸到未來的信息。

  • 包含兩個 Multi-Head Attention 層。

  • 第一個 Multi-Head Attention 層采用了 Masked 操作。

  • 第二個 Multi-Head Attention 層的Key,Value矩陣使用 Encoder 的編碼信息矩陣?進行計算,而Query使用上一個 Decoder block 的輸出計算。

  • 最后有一個 Softmax 層計算下一個翻譯單詞的概率。

下面詳細介紹下Masked Multi-Head Self-attention的具體操作,Masked在Scale操作之后,softmax操作之前。

圖22:Masked在Scale操作之后,softmax操作之前

因為在翻譯的過程中是順序翻譯的,即翻譯完第?個單詞,才可以翻譯第?個單詞。通過 Masked 操作可以防止第?個單詞知道第?個單詞之后的信息。下面以 "我有一只貓" 翻譯成 "I have a cat" 為例,了解一下 Masked 操作。在 Decoder 的時候,是需要根據之前的翻譯,求解當前最有可能的翻譯,如下圖所示。首先根據輸入 "<Begin>" 預測出第一個單詞為 "I",然后根據輸入 "<Begin> I" 預測下一個單詞 "have"。

Decoder 可以在訓練的過程中使用 Teacher Forcing 并且并行化訓練,即將正確的單詞序列 (<Begin> I have a cat) 和對應輸出 (I have a cat <end>) 傳遞到 Decoder。那么在預測第?個輸出時,就要將第? 之后的單詞掩蓋住,注意 Mask 操作是在 Self-Attention 的 Softmax 之前使用的,下面用 0 1 2 3 4 5 分別表示 "<Begin> I have a cat <end>"。

圖23:Decoder過程

注意這里transformer模型訓練和測試的方法不同:

測試時:

  • 輸入<Begin>,解碼器輸出 I 。

  • 輸入前面已經解碼的<Begin>和 I,解碼器輸出have。

  • 輸入已經解碼的<Begin>,I, have, a, cat,解碼器輸出解碼結束標志位<end>,每次解碼都會利用前面已經解碼輸出的所有單詞嵌入信息。

  • Transformer測試時的解碼過程:

    訓練時:

    不采用上述類似RNN的方法 一個一個目標單詞嵌入向量順序輸入訓練,想采用類似編碼器中的矩陣并行算法,一步就把所有目標單詞預測出來。要實現這個功能就可以參考編碼器的操作,把目標單詞嵌入向量組成矩陣一次輸入即可。即:并行化訓練。

    但是在解碼have時候,不能利用到后面單詞a和cat的目標單詞嵌入向量信息,否則這就是作弊(測試時候不可能能未卜先知)。為此引入mask。具體是:在解碼器中,self-attention層只被允許處理輸出序列中更靠前的那些位置,在softmax步驟前,它會把后面的位置給隱去。

    Masked Multi-Head Self-attention的具體操作 如圖24所示。

    Step1: 輸入矩陣包含 "<Begin> I have a cat" (0, 1, 2, 3, 4) 五個單詞的表示向量,Mask是一個 5×5 的矩陣。在Mask可以發現單詞 0 只能使用單詞 0 的信息,而單詞 1 可以使用單詞 0, 1 的信息,即只能使用之前的信息。輸入矩陣?經過transformation matrix變為3個矩陣:Query,Key?和Value?。

    Step2:?得到 Attention矩陣?,此時先不急于做softmax的操作,而是先于一個?矩陣相乘,使得attention矩陣的有些位置 歸0,得到Masked Attention矩陣?。?矩陣是個下三角矩陣,為什么這樣設計?是因為想在計算?矩陣的某一行時,只考慮它前面token的作用。即:在計算?的第一行時,刻意地把?矩陣第一行的后面幾個元素屏蔽掉,只考慮?。在產生have這個單詞時,只考慮 I,不考慮之后的have a cat,即只會attend on已經產生的sequence,這個很合理,因為還沒有產生出來的東西不存在,就無法做attention。

    Step3: Masked Attention矩陣進行 Softmax,每一行的和都為 1。但是單詞 0 在單詞 1, 2, 3, 4 上的 attention score 都為 0。得到的結果再與?矩陣相乘得到最終的self-attention層的輸出結果?。

    Step4:?只是某一個head的結果,將多個head的結果concat在一起之后再最后進行Linear Transformation得到最終的Masked Multi-Head Self-attention的輸出結果?。

    圖24:Masked Multi-Head Self-attention的具體操作

    第1個Masked Multi-Head Self-attention的?均來自Output Embedding。

    第2個Multi-Head Self-attention的?來自第1個Self-attention layer的輸出,?來自Encoder的輸出。

    為什么這么設計? 這里提供一種個人的理解:

    來自Transformer Encoder的輸出,所以可以看做句子(Sequence)/圖片(image)內容信息(content,比如句意是:"我有一只貓",圖片內容是:"有幾輛車,幾個人等等")。

    表達了一種訴求:希望得到什么,可以看做引導信息(guide)

    通過Multi-Head Self-attention結合在一起的過程就相當于是把我們需要的內容信息指導表達出來

    Decoder的最后是Softmax 預測輸出單詞。因為 Mask 的存在,使得單詞 0 的輸出?只包含單詞 0 的信息。Softmax 根據輸出矩陣的每一行預測下一個單詞,如下圖25所示。

    圖25:Softmax 根據輸出矩陣的每一行預測下一個單詞

    如下圖26所示為Transformer的整體結構。

    圖26:Transformer的整體結構
    • 2.2 Transformer代碼解讀:

    代碼來自:

    https://github.com/jadore801120/attention-is-all-you-need-pytorch

    ScaledDotProductAttention:
    實現的是圖22的操作,先令?,再對結果按位乘以?矩陣,再做?操作,最后的結果與?相乘,得到self-attention的輸出。

    class ScaledDotProductAttention(nn.Module): ''' Scaled Dot-Product Attention '''def __init__(self, temperature, attn_dropout=0.1): super().__init__() self.temperature = temperature self.dropout = nn.Dropout(attn_dropout)def forward(self, q, k, v, mask=None):attn = torch.matmul(q / self.temperature, k.transpose(2, 3))if mask is not None: attn = attn.masked_fill(mask == 0, -1e9)attn = self.dropout(F.softmax(attn, dim=-1)) output = torch.matmul(attn, v)return output, attn

    位置編碼 PositionalEncoding:
    實現的是式(5)的位置編碼。

    class PositionalEncoding(nn.Module):def __init__(self, d_hid, n_position=200): super(PositionalEncoding, self).__init__()# Not a parameter self.register_buffer('pos_table', self._get_sinusoid_encoding_table(n_position, d_hid))def _get_sinusoid_encoding_table(self, n_position, d_hid): ''' Sinusoid position encoding table ''' # TODO: make it with torch instead of numpydef get_position_angle_vec(position): return [position / np.power(10000, 2 * (hid_j // 2) / d_hid) for hid_j in range(d_hid)]sinusoid_table = np.array([get_position_angle_vec(pos_i) for pos_i in range(n_position)]) sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2]) # dim 2i sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2]) # dim 2i+1return torch.FloatTensor(sinusoid_table).unsqueeze(0)#(1,N,d)def forward(self, x): # x(B,N,d) return x + self.pos_table[:, :x.size(1)].clone().detach()

    MultiHeadAttention:
    實現圖13,14的多頭self-attention。

    class MultiHeadAttention(nn.Module): ''' Multi-Head Attention module '''def __init__(self, n_head, d_model, d_k, d_v, dropout=0.1): super().__init__()self.n_head = n_head self.d_k = d_k self.d_v = d_vself.w_qs = nn.Linear(d_model, n_head * d_k, bias=False) self.w_ks = nn.Linear(d_model, n_head * d_k, bias=False) self.w_vs = nn.Linear(d_model, n_head * d_v, bias=False) self.fc = nn.Linear(n_head * d_v, d_model, bias=False)self.attention = ScaledDotProductAttention(temperature=d_k ** 0.5)self.dropout = nn.Dropout(dropout) self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)def forward(self, q, k, v, mask=None):d_k, d_v, n_head = self.d_k, self.d_v, self.n_head sz_b, len_q, len_k, len_v = q.size(0), q.size(1), k.size(1), v.size(1)residual = q# Pass through the pre-attention projection: b x lq x (n*dv) # Separate different heads: b x lq x n x dv q = self.w_qs(q).view(sz_b, len_q, n_head, d_k) k = self.w_ks(k).view(sz_b, len_k, n_head, d_k) v = self.w_vs(v).view(sz_b, len_v, n_head, d_v)# Transpose for attention dot product: b x n x lq x dv q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)if mask is not None: mask = mask.unsqueeze(1) # For head axis broadcasting.q, attn = self.attention(q, k, v, mask=mask)#q (sz_b,n_head,N=len_q,d_k) #k (sz_b,n_head,N=len_k,d_k) #v (sz_b,n_head,N=len_v,d_v)# Transpose to move the head dimension back: b x lq x n x dv # Combine the last two dimensions to concatenate all the heads together: b x lq x (n*dv) q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1)#q (sz_b,len_q,n_head,N * d_k) q = self.dropout(self.fc(q)) q += residualq = self.layer_norm(q)return q, attn

    前向傳播Feed Forward Network:

    class PositionwiseFeedForward(nn.Module): ''' A two-feed-forward-layer module '''def __init__(self, d_in, d_hid, dropout=0.1): super().__init__() self.w_1 = nn.Linear(d_in, d_hid) # position-wise self.w_2 = nn.Linear(d_hid, d_in) # position-wise self.layer_norm = nn.LayerNorm(d_in, eps=1e-6) self.dropout = nn.Dropout(dropout)def forward(self, x):residual = xx = self.w_2(F.relu(self.w_1(x))) x = self.dropout(x) x += residualx = self.layer_norm(x)return x

    EncoderLayer:
    實現圖26中的一個EncoderLayer,具體的結構如圖19所示。

    class EncoderLayer(nn.Module): ''' Compose with two layers '''def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1): super(EncoderLayer, self).__init__() self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout) self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)def forward(self, enc_input, slf_attn_mask=None): enc_output, enc_slf_attn = self.slf_attn( enc_input, enc_input, enc_input, mask=slf_attn_mask) enc_output = self.pos_ffn(enc_output) return enc_output, enc_slf_attn

    DecoderLayer:
    實現圖26中的一個DecoderLayer,具體的結構如圖19所示。

    class DecoderLayer(nn.Module): ''' Compose with three layers '''def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1): super(DecoderLayer, self).__init__() self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout) self.enc_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout) self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)def forward( self, dec_input, enc_output, slf_attn_mask=None, dec_enc_attn_mask=None): dec_output, dec_slf_attn = self.slf_attn( dec_input, dec_input, dec_input, mask=slf_attn_mask) dec_output, dec_enc_attn = self.enc_attn( dec_output, enc_output, enc_output, mask=dec_enc_attn_mask) dec_output = self.pos_ffn(dec_output) return dec_output, dec_slf_attn, dec_enc_attn

    Encoder:
    實現圖26,19左側的Encoder:

    class Encoder(nn.Module): ''' A encoder model with self attention mechanism. '''def __init__( self, n_src_vocab, d_word_vec, n_layers, n_head, d_k, d_v, d_model, d_inner, pad_idx, dropout=0.1, n_position=200):super().__init__()self.src_word_emb = nn.Embedding(n_src_vocab, d_word_vec, padding_idx=pad_idx) self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position) self.dropout = nn.Dropout(p=dropout) self.layer_stack = nn.ModuleList([ EncoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout) for _ in range(n_layers)]) self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)def forward(self, src_seq, src_mask, return_attns=False):enc_slf_attn_list = []# -- Forwardenc_output = self.dropout(self.position_enc(self.src_word_emb(src_seq))) enc_output = self.layer_norm(enc_output)for enc_layer in self.layer_stack: enc_output, enc_slf_attn = enc_layer(enc_output, slf_attn_mask=src_mask) enc_slf_attn_list += [enc_slf_attn] if return_attns else []if return_attns: return enc_output, enc_slf_attn_list return enc_output,

    Decoder:
    實現圖26,19右側的Decoder:

    class Decoder(nn.Module): ''' A decoder model with self attention mechanism. '''def forward(self, trg_seq, trg_mask, enc_output, src_mask, return_attns=False):dec_slf_attn_list, dec_enc_attn_list = [], []# -- Forward dec_output = self.dropout(self.position_enc(self.trg_word_emb(trg_seq))) dec_output = self.layer_norm(dec_output)for dec_layer in self.layer_stack: dec_output, dec_slf_attn, dec_enc_attn = dec_layer( dec_output, enc_output, slf_attn_mask=trg_mask, dec_enc_attn_mask=src_mask) dec_slf_attn_list += [dec_slf_attn] if return_attns else [] dec_enc_attn_list += [dec_enc_attn] if return_attns else []if return_attns: return dec_output, dec_slf_attn_list, dec_enc_attn_list return dec_output,

    整體結構:
    實現圖26,19整體的Transformer:

    class Transformer(nn.Module): ''' A sequence to sequence model with attention mechanism. '''def __init__( self, n_src_vocab, n_trg_vocab, src_pad_idx, trg_pad_idx, d_word_vec=512, d_model=512, d_inner=2048, n_layers=6, n_head=8, d_k=64, d_v=64, dropout=0.1, n_position=200, trg_emb_prj_weight_sharing=True, emb_src_trg_weight_sharing=True):super().__init__()self.src_pad_idx, self.trg_pad_idx = src_pad_idx, trg_pad_idxself.encoder = Encoder( n_src_vocab=n_src_vocab, n_position=n_position, d_word_vec=d_word_vec, d_model=d_model, d_inner=d_inner, n_layers=n_layers, n_head=n_head, d_k=d_k, d_v=d_v, pad_idx=src_pad_idx, dropout=dropout)self.decoder = Decoder( n_trg_vocab=n_trg_vocab, n_position=n_position, d_word_vec=d_word_vec, d_model=d_model, d_inner=d_inner, n_layers=n_layers, n_head=n_head, d_k=d_k, d_v=d_v, pad_idx=trg_pad_idx, dropout=dropout)self.trg_word_prj = nn.Linear(d_model, n_trg_vocab, bias=False)for p in self.parameters(): if p.dim() > 1: nn.init.xavier_uniform_(p) assert d_model == d_word_vec, \ 'To facilitate the residual connections, \ the dimensions of all module outputs shall be the same.'self.x_logit_scale = 1. if trg_emb_prj_weight_sharing: # Share the weight between target word embedding & last dense layer self.trg_word_prj.weight = self.decoder.trg_word_emb.weight self.x_logit_scale = (d_model ** -0.5)if emb_src_trg_weight_sharing: self.encoder.src_word_emb.weight = self.decoder.trg_word_emb.weightdef forward(self, src_seq, trg_seq):src_mask = get_pad_mask(src_seq, self.src_pad_idx) trg_mask = get_pad_mask(trg_seq, self.trg_pad_idx) & get_subsequent_mask(trg_seq)enc_output, *_ = self.encoder(src_seq, src_mask) dec_output, *_ = self.decoder(trg_seq, trg_mask, enc_output, src_mask) seq_logit = self.trg_word_prj(dec_output) * self.x_logit_scalereturn seq_logit.view(-1, seq_logit.size(2))

    產生Mask:

    def get_pad_mask(seq, pad_idx): return (seq != pad_idx).unsqueeze(-2)def get_subsequent_mask(seq): ''' For masking out the subsequent info. ''' sz_b, len_s = seq.size() subsequent_mask = (1 - torch.triu( torch.ones((1, len_s, len_s), device=seq.device), diagonal=1)).bool() return subsequent_mask

    src_mask = get_pad_mask(src_seq, self.src_pad_idx)
    用于產生Encoder的Mask,它是一列Bool值,負責把標點mask掉。
    trg_mask = get_pad_mask(trg_seq, self.trg_pad_idx) & get_subsequent_mask(trg_seq)
    用于產生Decoder的Mask。它是一個矩陣,如圖24中的Mask所示,功能已在上文介紹。

    3 Transformer+Detection:引入視覺領域的首創DETR

    論文名稱:End-to-End Object Detection with Transformers

    論文地址:

    https://arxiv.org/abs/2005.12872arxiv.org

    • 3.1 DETR原理分析:

    網絡架構部分解讀

    本文的任務是Object detection,用到的工具是Transformers,特點是End-to-end。

    目標檢測的任務是要去預測一系列的Bounding Box的坐標以及Label, 現代大多數檢測器通過定義一些proposal,anchor或者windows,把問題構建成為一個分類和回歸問題來間接地完成這個任務。文章所做的工作,就是將transformers運用到了object detection領域,取代了現在的模型需要手工設計的工作,并且取得了不錯的結果。在object detection上DETR準確率和運行時間上和Faster RCNN相當;將模型 generalize 到 panoptic segmentation 任務上,DETR表現甚至還超過了其他的baseline。DETR第一個使用End to End的方式解決檢測問題,解決的方法是把檢測問題視作是一個set prediction problem,如下圖27所示。

    圖27:DETR結合CNN和Transformer的結構,并行實現預測

    網絡的主要組成是CNN和Transformer,Transformer借助第1節講到的self-attention機制,可以顯式地對一個序列中的所有elements兩兩之間的interactions進行建模,使得這類transformer的結構非常適合帶約束的set prediction的問題。DETR的特點是:一次預測,端到端訓練,set loss function和二分匹配。

    文章的主要有兩個關鍵的部分。

    第一個是用transformer的encoder-decoder架構一次性生成?個box prediction。其中?是一個事先設定的、比遠遠大于image中object個數的一個整數。

    第二個是設計了bipartite matching loss,基于預測的boxex和ground truth boxes的二分圖匹配計算loss的大小,從而使得預測的box的位置和類別更接近于ground truth。

    DETR整體結構可以分為四個部分:backbone,encoder,decoder和FFN,如下圖28所示,以下分別解釋這四個部分:

    圖28:DETR整體結構

    1 首先看backbone: CNN backbone處理?維的圖像,把它轉換為維的feature map(一般來說?或),backbone只做這一件事。

    2 再看encoder: encoder的輸入是維的feature map,接下來依次進行以下過程:

    • 通道數壓縮: 先用?convolution處理,將channels數量從?壓縮到?,即得到維的新feature map。

    • 轉化為序列化數據: 將空間的維度(高和寬)壓縮為一個維度,即把上一步得到的維的feature map通過reshape成維的feature map。

    • 位置編碼: 在得到了維的feature map之后,正式輸入encoder之前,需要進行 Positional Encoding 。這一步在第2節講解transformer的時候已經提到過,因為在self-attention中需要有表示位置的信息,否則你的sequence = "A打了B" 還是sequence = "B打了A"的效果是一樣的。但是transformer encoder這個結構本身卻無法體現出位置信息。也就是說,我們需要對這個?維的feature map做positional encoding。

    進行完位置編碼以后根據paper中的圖片會有個相加的過程,如下圖問號處所示。很多讀者有疑問的地方是:論文圖示中相加的2個張量,一個是input embedding,另一個是位置編碼維度看上去不一致,是怎么相加的?后面會解答。

    圖:怎么相加的?

    原版Transformer和Vision Transformer (第4節講述)的Positional Encoding的表達式為:

    式中,?就是這個?維的feature map的第一維,?。表示token在sequence中的位置,sequence的長度是?,例如第一個token 的?。

    ,或者準確意義上是?和?表示了Positional Encoding的維度,的取值范圍是:?。所以當?為1時,對應的Positional Encoding可以寫成:

    式中,?。

    第一點不同的是,原版Transformer只考慮?方向的位置編碼,但是DETR考慮了?方向的位置編碼,因為圖像特征是2-D特征。采用的依然是?模式,但是需要考慮?兩個方向。不是類似vision transoformer做法簡單的將其拉伸為?,然后從?進行長度為256的位置編碼,而是考慮了?方向同時編碼,每個方向各編碼128維向量,這種編碼方式更符合圖像特點。

    Positional Encoding的輸出張量是:?,其中?代表位置編碼的長度,?代表張量的位置。意思是說,這個特征圖上的任意一個點?有個位置編碼,這個編碼的長度是256,其中,前128維代表?的位置編碼,后128維代表?的位置編碼。

    假設你想計算任意一個位置?的Positional Encoding,把?代入(11)式的?式和?式可以計算得到128維的向量,它代表?的位置編碼,再把?代入(11)式的?式和?式可以計算得到128維的向量,它代表?的位置編碼,把這2個128維的向量拼接起來,就得到了一個256維的向量,它代表?的位置編碼。

    計算所有位置的編碼,就得到了?的張量,代表這個batch的位置編碼。編碼矩陣的維度是?,也把它序列化成維度為?維的張量。

    準備與維的feature map相加以后輸入Encoder。

    值得注意的是,網上許多解讀文章沒有搞清楚 "轉化為序列化數據"這一步和 "位置編碼"的順序關系,以及變量的shape到底是怎樣變化的,這里我用一個圖表達,終結這個問題。

    圖29:變量的shape的變化,變量一律使用方塊表達。

    所以,了解了DETR的位置編碼之后,你應該明白了其實input embedding和位置編碼維度其實是一樣的,只是論文圖示為了突出二位編碼所以畫的不一樣罷了,如下圖所示:

    圖:input embedding與positional embedding的shape是一致的

    另一點不同的是,原版Transformer 只在Encoder之前使用了Positional Encoding,而且是在輸入上進行Positional Encoding,再把輸入經過transformation matrix變為Query,Key和Value這幾個張量。但是DETR在Encoder的每一個Multi-head Self-attention之前都使用了Positional Encoding,且只對Query和Key使用了Positional Encoding,即:只把維度為維的位置編碼與維度為維的Query和Key相加,而不與Value相加。

    如圖30所示為DETR的Transformer的詳細結構,讀者可以對比下原版Transformer的結構,如圖19所示,為了閱讀的方便我把圖19又貼在下面了。

    可以發現,除了Positional Encoding設置的不一樣外,Encoder其他的結構是一致的。每個Encoder Layer包含一個multi-head self-attention 的module和一個前饋網絡Feed Forward Network。

    Encoder最終輸出的是?維的編碼矩陣Embedding,按照原版Transformer的做法,把這個東西給Decoder。

    總結下和原始transformer編碼器不同的地方:

    • 輸入編碼器的位置編碼需要考慮2-D空間位置。

    • 位置編碼向量需要加入到每個Encoder Layer中。

    • 在編碼器內部位置編碼Positional Encoding僅僅作用于Query和Key,即只與Query和Key相加,Value不做任何處理。

    圖30:Transformer詳細結構。為了方便理解,我把每個變量的維度標在了圖上。圖19:Transformer整體結構

    3 再看decoder:

    DETR的Decoder和原版Transformer的decoder是不太一樣的,如下圖30和19所示。

    先回憶下原版Transformer,看下圖19的decoder的最后一個框:output probability,代表我們一次只產生一個單詞的softmax,根據這個softmax得到這個單詞的預測結果。這個過程我們表達為:predicts the output sequence one element at a time

    不同的是,DETR的Transformer Decoder是一次性處理全部的object queries,即一次性輸出全部的predictions;而不像原始的Transformer是auto-regressive的,從左到右一個詞一個詞地輸出。這個過程我們表達為:decodes the N objects in parallel at each decoder layer。

    DETR的Decoder主要有兩個輸入:

  • Transformer Encoder輸出的Embedding與 position encoding 之和。

  • Object queries。

  • 其中,Embedding就是上文提到的?的編碼矩陣。這里著重講一下Object queries。

    Object queries是一個維度為?維的張量,數值類型是nn.Embedding,說明這個張量是可以學習的,即:我們的Object queries是可學習的。Object queries矩陣內部通過學習建模了100個物體之間的全局關系,例如房間里面的桌子旁邊(A類)一般是放椅子(B類),而不會是放一頭大象(C類),那么在推理時候就可以利用該全局注意力更好的進行解碼預測輸出。

    Decoder的輸入一開始也初始化成維度為?維的全部元素都為0的張量,和Object queries加在一起之后充當第1個multi-head self-attention的Query和Key。第一個multi-head self-attention的Value為Decoder的輸入,也就是全0的張量。

    到了每個Decoder的第2個multi-head self-attention,它的Key和Value來自Encoder的輸出張量,維度為?,其中Key值還進行位置編碼。Query值一部分來自第1個Add and Norm的輸出,維度為?的張量,另一部分來自Object queries,充當可學習的位置編碼。所以,第2個multi-head self-attention的Key和Value的維度為?,而Query的維度為。

    每個Decoder的輸出維度為?,送入后面的前饋網絡,具體的變量維度的變化見圖30。

    到這里你會發現:Object queries充當的其實是位置編碼的作用,只不過它是可以學習的位置編碼,所以,我們對Encoder和Decoder的每個self-attention的Query和Key的位置編碼做個歸納,如圖31所示,Value沒有位置編碼:

    圖31:Transformer的位置編碼來自哪里?

    損失函數部分解讀

    得到了Decoder的輸出以后,如前文所述,應該是輸出維度為?的張量。接下來要送入2個前饋網絡FFN得到class和Bounding Box。它們會得到?個預測目標,包含類別和Bounding Box,當然這個100肯定是大于圖中的目標總數的。如果不夠100,則采用背景填充,計算loss時候回歸分支分支僅僅計算有物體位置,背景集合忽略。所以,DETR輸出張量的維度為輸出的張量的維度是?和?。對應COCO數據集來說,?,?指的是每個預測目標歸一化的?。歸一化就是除以圖片寬高進行歸一化。

    到這里我們了解了DETR的網絡架構,我們發現,它輸出的張量的維度是 分類分支:回歸分支:?,其中,前者是指100個預測框的類型,后者是指100個預測框的Bounding Box,但是讀者可能會有疑問:預測框和真值是怎么一一對應的?換句話說:你怎么知道第47個預測框對應圖片里的狗,第88個預測框對應圖片里的車?等等。

    我們下面就來聊聊這個問題。

    相比Faster R-CNN等做法,DETR最大特點是將目標檢測問題轉化為無序集合預測問題(set prediction)。論文中特意指出Faster R-CNN這種設置一大堆anchor,然后基于anchor進行分類和回歸其實屬于代理做法即不是最直接做法,目標檢測任務就是輸出無序集合,而Faster R-CNN等算法通過各種操作,并結合復雜后處理最終才得到無序集合屬于繞路了,而DETR就比較純粹了?,F在核心問題來了:輸出的?個檢測結果是無序的,如何和?計算loss?這就需要用到經典的雙邊匹配算法了,也就是常說的匈牙利算法,該算法廣泛應用于最優分配問題。

    一幅圖片,我們把第?個物體的真值表達為?,其中,?表示它的?,?表示它的?。我們定義?為網絡輸出的?個預測值。

    假設我們已經了解了什么是匈牙利算法(先假裝了解了),對于第?個?,?為匈牙利算法得到的與?對應的prediction的索引。我舉個栗子,比如?,意思就是:與第3個真值對應的預測值是第18個。

    那我能根據?匈牙利算法,找到?與每個真值對應的預測值是哪個,那究竟是如何找到呢?

    我們看看這個表達式是甚么意思,對于某一個真值?,假設我們已經找到這個真值對應的預測值?,這里的?是所有可能的排列,代表從真值索引到預測值索引的所有的映射,然后用?最小化?和?的距離。這個?具體是:

    意思是:假設當前從真值索引到預測值索引的所有的映射為?,對于圖片中的每個真值?,先找到對應的預測值?,再看看分類網絡的結果?,取反作為?的第1部分。再計算回歸網絡的結果?與真值的?的差異,即?,作為?的第2部分。

    所以,可以使得?最小的排列?就是我們要找的排列,即:對于圖片中的每個真值?來講,?就是這個真值所對應的預測值的索引。

    請讀者細品這個 尋找匹配的過程 ,這就是匈牙利算法的過程。是不是與Anchor或Proposal有異曲同工的地方,只是此時我們找的是一對一匹配。

    接下來就是使用上一步得到的排列?,計算匈牙利損失:

    式中的?具體為:

    最常用的?對于大小?會有不同的標度,即使它們的相對誤差是相似的。為了緩解這個問題,作者使用了?和廣義IoU損耗?的線性組合,它是比例不變的。

    Hungarian意思就是匈牙利,也就是前面的?,上述意思是需要計算?個?和?個輸預測出集合兩兩之間的廣義距離,距離越近表示越可能是最優匹配關系,也就是兩者最密切。廣義距離的計算考慮了分類分支和回歸分支。

    最后,再概括一下DETR的End-to-End的原理,前面那么多段話就是為了講明白這個事情,如果你對前面的論述還存在疑問的話,把下面一直到Experiments之前的這段話看懂就能解決你的困惑。

    DETR是怎么訓練的?

    訓練集里面的任何一張圖片,假設第1張圖片,我們通過模型產生100個預測框?,假設這張圖片有只3個?,它們分別是?。

    問題是:我怎么知道這100個預測框哪個是對應?,哪個是對應?,哪個是對應??

    我們建立一個?的矩陣,矩陣里面的元素就是?式的計算結果,舉個例子:比如左上角的?號元素的含義是:第1個預測框對應?的情況下的?值。我們用scipy.optimize 這個庫中的 linear_sum_assignment 函數找到最優的匹配,這個過程我們稱之為:"匈牙利算法 (Hungarian Algorithm)"

    假設linear_sum_assignment 做完以后的結果是:第?個預測框對應?,第?個預測框對應?,第?個預測框對應?。

    現在把第?個預測框挑出來,按照?式計算Loss,得到這個圖片的Loss。

    把所有的圖片按照這個模式去訓練模型。

    訓練完以后怎么用?

    訓練完以后,你的模型學習到了一種能力,即:模型產生的100個預測框,它知道某個預測框該對應什么?,比如,模型學習到:第1個?對應?,第2個?對應?,第3個?對應?,第4個?對應?,第5個?對應?,第6-100個?對應?,等等。

    以上只是我舉的一個例子,意思是說:模型知道了自己的100個預測框每個該做什么事情,即:每個框該預測什么樣的?。

    為什么訓練完以后,模型學習到了一種能力,即:模型產生的100個預測框,它知道某個預測框該對應什么??

    還記得前面說的Object queries嗎?它是一個維度為?維的張量,初始時元素全為?。實現方式是nn.Embedding(num_queries, hidden_dim),這里num_queries=100,hidden_dim=256,它是可訓練的。這里的?指的是batch size,我們考慮單張圖片,所以假設Object queries是一個維度為?維的張量。我們訓練完模型以后,這個張量已經訓練完了,那此時的Object queries究竟代表什么?

    我們把此時的Object queries看成100個格子,每個格子是個256維的向量。訓練完以后,這100個格子里面注入了不同?的位置信息和類別信息。比如第1個格子里面的這個256維的向量代表著?這種?的位置信息,這種信息是通過訓練,考慮了所有圖片的某個位置附近的?編碼特征,屬于和位置有關的全局?統計信息。

    測試時,假設圖片中有?三種物體,該圖片會輸入到編碼器中進行特征編碼,假設特征沒有丟失,Decoder的KeyValue就是編碼器輸出的編碼向量(如圖30所示),而Query就是Object queries,就是我們的100個格子。

    Query可以視作代表不同?的信息,而Key和Value可以視作代表圖像的全局信息。

    現在通過注意力模塊將QueryKey計算,然后加權Value得到解碼器輸出。對于第1個格子的Query會和Key中的所有向量進行計算,目的是查找某個位置附近有沒有?,如果有那么該特征就會加權輸出,對于第3個格子的Query會和Key中的所有向量進行計算,目的是查找某個位置附近有沒有?,很遺憾,這個沒有,所以輸出的信息里面沒有?。

    整個過程計算完成后就可以把編碼向量中的?的編碼嵌入信息提取出來,然后后面接?進行分類和回歸就比較容易,因為特征已經對齊了。

    發現了嗎?Object queries在訓練過程中對于?個格子會壓縮入對應的和位置和類別相關的統計信息,在測試階段就可以利用該Query去和某個圖像的編碼特征Key,Value計算,若圖片中剛好有Query想找的特征,比如?,則這個特征就能提取出來,最后通過2個?進行分類和回歸。所以前面才會說Object queries作用非常類似Faster R-CNN中的anchor,這個anchor是可學習的,由于維度比較高,故可以表征的東西豐富,當然維度越高,訓練時長就會越長。

    這就是DETR的End-to-End的原理,可以簡單歸結為上面的幾段話,你讀懂了上面的話,也就明白了DETR以及End-to-End的Detection模型原理。

    Experiments:

    1. 性能對比:

    圖32:DETR與Fast R-CNN的性能對比

    2. 編碼器層數對比實驗:

    圖33:編碼器數目與模型性能

    可以發現,編碼器層數越多越好,最后就選擇6。

    下圖34為最后一個Encoder Layer的attention可視化,Encoder已經分離了instances,簡化了Decoder的對象提取和定位。

    圖34:最后一個Encoder Layer的attention可視化

    3. 解碼器層數對比實驗:

    圖35:每個Decoder Layer后的AP和AP 50性能。

    可以發現,性能隨著解碼器層數的增加而提升,DETR本不需要NMS,但是作者也進行了,上圖中的NMS操作是指DETR的每個解碼層都可以輸入無序集合,那么將所有解碼器無序集合全部保留,然后進行NMS得到最終輸出,可以發現性能稍微有提升,特別是AP50。這可以通過以下事實來解釋:Transformer的單個Decoder Layer不能計算輸出元素之間的任何互相關,因此它易于對同一對象進行多次預測。在第2個和隨后的Decoder Layer中,self-attention允許模型抑制重復預測。所以NMS帶來的改善隨著Decoder Layer的增加而減少。在最后幾層,作者觀察到AP的一個小損失,因為NMS錯誤地刪除了真實的positive prediction。

    圖36:Decoder Layer的attention可視化

    類似于可視化編碼器注意力,作者在圖36中可視化解碼器注意力,用不同的顏色給每個預測對象的注意力圖著色。觀察到,解碼器的attention相當局部,這意味著它主要關注對象的四肢,如頭部或腿部。我們假設,在編碼器通過全局關注分離實例之后,解碼器只需要關注極端來提取類和對象邊界。

    • 3.2 DETR代碼解讀:

    https://github.com/facebookresearch/detr

    分析都注釋在了代碼中。

    二維位置編碼:
    DETR的二維位置編碼:
    首先構造位置矩陣x_embed和y_embed,這里用到了python函數cumsum,作用是對一個矩陣的元素進行累加,那么累加以后最后一個元素就是所有累加元素的和,省去了求和的步驟,直接用這個和做歸一化,對應x_embed[:, :, -1:]和y_embed[:, -1:, :]。
    這里我想著重強調下代碼中一些變量的shape,方便讀者掌握作者編程的思路:
    值得注意的是,tensor_list的類型是NestedTensor,內部自動附加了mask,用于表示動態shape,是pytorch中tensor新特性https://github.com/pytorch/nestedtensor。全是false。
    x:(b,c,H,W)
    mask:(b,H,W),全是False。
    not_mask:(b,H,W),全是True。
    首先出現的y_embed:(b,H,W),具體是1,1,1,1,......,2,2,2,2,......3,3,3,3,......
    首先出現的x_embed:(b,H,W),具體是1,2,3,4,......,1,2,3,4,......1,2,3,4,......
    self.num_pos_feats = 128
    首先出現的dim_t = [0,1,2,3,.....,127]
    pos_x:(b,H,W,128)
    pos_y:(b,H,W,128)
    flatten后面的數字指的是:flatten()方法應從哪個軸開始展開操作。
    torch.stack((pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4).flatten(3)
    pos_y = torch.stack((pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4)
    這一步執行完以后變成(b,H,W,2,64)通過flatten()方法從第3個軸開始展平,變為:(b,H,W,128)
    torch.cat((pos_y, pos_x), dim=3)之后變為(b,H,W,256),再最后permute為(b,256,H,W)。
    PositionEmbeddingSine類繼承nn.Module類。

    class PositionEmbeddingSine(nn.Module):def __init__(self, num_pos_feats=64, temperature=10000, normalize=False, scale=None): super().__init__() self.num_pos_feats = num_pos_feats self.temperature = temperature self.normalize = normalize if scale is not None and normalize is False: raise ValueError("normalize should be True if scale is passed") if scale is None: scale = 2 * math.pi self.scale = scaledef forward(self, tensor_list: NestedTensor):#輸入是b,c,h,w#tensor_list的類型是NestedTensor,內部自動附加了mask,#用于表示動態shape,是pytorch中tensor新特性https://github.com/pytorch/nestedtensor x = tensor_list.tensors# 附加的mask,shape是b,h,w 全是false mask = tensor_list.mask assert mask is not None not_mask = ~mask# 因為圖像是2d的,所以位置編碼也分為x,y方向# 1 1 1 1 .. 2 2 2 2... 3 3 3... y_embed = not_mask.cumsum(1, dtype=torch.float32)# 1 2 3 4 ... 1 2 3 4... x_embed = not_mask.cumsum(2, dtype=torch.float32) if self.normalize: eps = 1e-6 y_embed = y_embed / (y_embed[:, -1:, :] + eps) * self.scale x_embed = x_embed / (x_embed[:, :, -1:] + eps) * self.scale # num_pos_feats = 128# 0~127 self.num_pos_feats=128,因為前面輸入向量是256,編碼是一半sin,一半cos dim_t = torch.arange(self.num_pos_feats, dtype=torch.float32, device=x.device) dim_t = self.temperature ** (2 * (dim_t // 2) / self.num_pos_feats) # 輸出shape=b,h,w,128 pos_x = x_embed[:, :, :, None] / dim_t pos_y = y_embed[:, :, :, None] / dim_t pos_x = torch.stack((pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4).flatten(3) pos_y = torch.stack((pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4).flatten(3) pos = torch.cat((pos_y, pos_x), dim=3).permute(0, 3, 1, 2)# 每個特征圖的xy位置都編碼成256的向量,其中前128是y方向編碼,而128是x方向編碼 return pos# b,n=256,h,w

    作者定義了一種數據結構:NestedTensor,里面打包存了兩個變量:x 和mask。

    NestedTensor:
    里面打包存了兩個變量:x 和mask。
    to()函數:把變量移到GPU中。

    Backbone:

    class BackboneBase(nn.Module):def __init__(self, backbone: nn.Module, train_backbone: bool, num_channels: int, return_interm_layers: bool): super().__init__() for name, parameter in backbone.named_parameters(): if not train_backbone or 'layer2' not in name and 'layer3' not in name and 'layer4' not in name: parameter.requires_grad_(False) if return_interm_layers: return_layers = {"layer1": "0", "layer2": "1", "layer3": "2", "layer4": "3"} else: return_layers = {'layer4': "0"} #作用的模型:定義BackboneBase時傳入的nn.Moduleclass的backbone,返回的layer:來自bool變量return_interm_layers self.body = IntermediateLayerGetter(backbone, return_layers=return_layers) self.num_channels = num_channelsdef forward(self, tensor_list: NestedTensor):#BackboneBase的輸入是一個NestedTensor#xs中間層的輸出, xs = self.body(tensor_list.tensors) out: Dict[str, NestedTensor] = {} for name, x in xs.items(): m = tensor_list.mask assert m is not None#F.interpolate上下采樣,調整mask的size#to(torch.bool) 把mask轉化為Bool型變量 mask = F.interpolate(m[None].float(), size=x.shape[-2:]).to(torch.bool)[0] out[name] = NestedTensor(x, mask) return outclass Backbone(BackboneBase): """ResNet backbone with frozen BatchNorm.""" def __init__(self, name: str, train_backbone: bool, return_interm_layers: bool, dilation: bool):#根據name選擇backbone, num_channels, return_interm_layers等,傳入BackboneBase初始化 backbone = getattr(torchvision.models, name)( replace_stride_with_dilation=[False, False, dilation], pretrained=is_main_process(), norm_layer=FrozenBatchNorm2d) num_channels = 512 if name in ('resnet18', 'resnet34') else 2048 super().__init__(backbone, train_backbone, num_channels, return_interm_layers)

    把Backbone和之前的PositionEmbeddingSine連在一起:
    Backbone完以后輸出(b,c,h,w),再經過PositionEmbeddingSine輸出(b,H,W,256)。

    class Joiner(nn.Sequential): def __init__(self, backbone, position_embedding): super().__init__(backbone, position_embedding)def forward(self, tensor_list: NestedTensor): xs = self[0](tensor_list) out: List[NestedTensor] = [] pos = [] for name, x in xs.items(): out.append(x) # position encoding pos.append(self[1](x).to(x.tensors.dtype))return out, posdef build_backbone(args):#position_embedding是個nn.module position_embedding = build_position_encoding(args) train_backbone = args.lr_backbone > 0 return_interm_layers = args.masks#backbone是個nn.module backbone = Backbone(args.backbone, train_backbone, return_interm_layers, args.dilation)#nn.Sequential在一起 model = Joiner(backbone, position_embedding) model.num_channels = backbone.num_channels return model

    Transformer的一個Encoder Layer:

    class TransformerEncoderLayer(nn.Module):def __init__(self, d_model, nhead, dim_feedforward=2048, dropout=0.1, activation="relu", normalize_before=False): super().__init__() self.self_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout) # Implementation of Feedforward model self.linear1 = nn.Linear(d_model, dim_feedforward) self.dropout = nn.Dropout(dropout) self.linear2 = nn.Linear(dim_feedforward, d_model)self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) self.dropout1 = nn.Dropout(dropout) self.dropout2 = nn.Dropout(dropout)self.activation = _get_activation_fn(activation) self.normalize_before = normalize_beforedef with_pos_embed(self, tensor, pos: Optional[Tensor]): return tensor if pos is None else tensor + posdef forward_post(self, src, src_mask: Optional[Tensor] = None, src_key_padding_mask: Optional[Tensor] = None, pos: Optional[Tensor] = None): # 和標準做法有點不一樣,src加上位置編碼得到q和k,但是v依然還是src, # 也就是v和qk不一樣 q = k = self.with_pos_embed(src, pos) src2 = self.self_attn(q, k, value=src, attn_mask=src_mask, key_padding_mask=src_key_padding_mask)[0]#Add and Norm src = src + self.dropout1(src2) src = self.norm1(src)#FFN src2 = self.linear2(self.dropout(self.activation(self.linear1(src))))#Add and Norm src = src + self.dropout2(src2) src = self.norm2(src) return srcdef forward_pre(self, src, src_mask: Optional[Tensor] = None, src_key_padding_mask: Optional[Tensor] = None, pos: Optional[Tensor] = None): src2 = self.norm1(src) q = k = self.with_pos_embed(src2, pos) src2 = self.self_attn(q, k, value=src2, attn_mask=src_mask, key_padding_mask=src_key_padding_mask)[0] src = src + self.dropout1(src2) src2 = self.norm2(src) src2 = self.linear2(self.dropout(self.activation(self.linear1(src2)))) src = src + self.dropout2(src2) return srcdef forward(self, src, src_mask: Optional[Tensor] = None, src_key_padding_mask: Optional[Tensor] = None, pos: Optional[Tensor] = None): if self.normalize_before: return self.forward_pre(src, src_mask, src_key_padding_mask, pos) return self.forward_post(src, src_mask, src_key_padding_mask, pos)

    有了一個Encoder Layer的定義,再看Transformer的整個Encoder:

    class TransformerEncoder(nn.Module): def __init__(self, encoder_layer, num_layers, norm=None): super().__init__() # 編碼器copy6份 self.layers = _get_clones(encoder_layer, num_layers) self.num_layers = num_layers self.norm = normdef forward(self, src, mask: Optional[Tensor] = None, src_key_padding_mask: Optional[Tensor] = None, pos: Optional[Tensor] = None): # 內部包括6個編碼器,順序運行 # src是圖像特征輸入,shape=hxw,b,256 output = src for layer in self.layers: # 每個編碼器都需要加入pos位置編碼 # 第一個編碼器輸入來自圖像特征,后面的編碼器輸入來自前一個編碼器輸出 output = layer(output, src_mask=mask, src_key_padding_mask=src_key_padding_mask, pos=pos) return output

    Object Queries:可學習的位置編碼:
    注釋中已經注明了變量的shape的變化過程,最終輸出的是與Positional Encoding維度相同的位置編碼,維度是(b,H,W,256),只是現在這個位置編碼是可學習的了。

    class PositionEmbeddingLearned(nn.Module): """ Absolute pos embedding, learned. """ def __init__(self, num_pos_feats=256): super().__init__()]#這里使用了nn.Embedding,這是一個矩陣類,里面初始化了一個隨機矩陣,矩陣的長是字典的大小,寬是用來表示字典中每個元素的屬性向量,# 向量的維度根據你想要表示的元素的復雜度而定。類實例化之后可以根據字典中元素的下標來查找元素對應的向量。輸入下標0,輸出就是embeds矩陣中第0行。 self.row_embed = nn.Embedding(50, num_pos_feats) self.col_embed = nn.Embedding(50, num_pos_feats) self.reset_parameters()def reset_parameters(self): nn.init.uniform_(self.row_embed.weight) nn.init.uniform_(self.col_embed.weight) #輸入依舊是NestedTensor def forward(self, tensor_list: NestedTensor): x = tensor_list.tensors h, w = x.shape[-2:] i = torch.arange(w, device=x.device) j = torch.arange(h, device=x.device) #x_emb:(w, 128)#y_emb:(h, 128) x_emb = self.col_embed(i) y_emb = self.row_embed(j) pos = torch.cat([ x_emb.unsqueeze(0).repeat(h, 1, 1),#(1,w,128) → (h,w,128) y_emb.unsqueeze(1).repeat(1, w, 1),#(h,1,128) → (h,w,128) ], dim=-1).permute(2, 0, 1).unsqueeze(0).repeat(x.shape[0], 1, 1, 1)#(h,w,256) → (256,h,w) → (1,256,h,w) → (b,256,h,w) return posdef build_position_encoding(args): N_steps = args.hidden_dim // 2 if args.position_embedding in ('v2', 'sine'): # TODO find a better way of exposing other arguments position_embedding = PositionEmbeddingSine(N_steps, normalize=True) elif args.position_embedding in ('v3', 'learned'): position_embedding = PositionEmbeddingLearned(N_steps) else: raise ValueError(f"not supported {args.position_embedding}")return position_embedding

    Transformer的一個Decoder Layer:
    注意變量的命名:
    object queries(query_pos)
    Encoder的位置編碼(pos)
    Encoder的輸出(memory)

    def forward_post(self, tgt, memory, tgt_mask: Optional[Tensor] = None, memory_mask: Optional[Tensor] = None, tgt_key_padding_mask: Optional[Tensor] = None, memory_key_padding_mask: Optional[Tensor] = None, pos: Optional[Tensor] = None, query_pos: Optional[Tensor] = None): #query,key的輸入是object queries(query_pos) + Decoder的輸入(tgt),shape都是(100,b,256)#value的輸入是Decoder的輸入(tgt),shape = (100,b,256) q = k = self.with_pos_embed(tgt, query_pos) #Multi-head self-attention tgt2 = self.self_attn(q, k, value=tgt, attn_mask=tgt_mask, key_padding_mask=tgt_key_padding_mask)[0]#Add and Norm tgt = tgt + self.dropout1(tgt2) tgt = self.norm1(tgt) #query的輸入是上一個attention的輸出(tgt) + object queries(query_pos)#key的輸入是Encoder的位置編碼(pos) + Encoder的輸出(memory)#value的輸入是Encoder的輸出(memory) tgt2 = self.multihead_attn(query=self.with_pos_embed(tgt, query_pos), key=self.with_pos_embed(memory, pos), value=memory, attn_mask=memory_mask, key_padding_mask=memory_key_padding_mask)[0] #Add and Norm tgt = tgt + self.dropout2(tgt2) tgt = self.norm2(tgt) #FFN tgt2 = self.linear2(self.dropout(self.activation(self.linear1(tgt)))) tgt = tgt + self.dropout3(tgt2) tgt = self.norm3(tgt) return tgtdef forward_pre(self, tgt, memory, tgt_mask: Optional[Tensor] = None, memory_mask: Optional[Tensor] = None, tgt_key_padding_mask: Optional[Tensor] = None, memory_key_padding_mask: Optional[Tensor] = None, pos: Optional[Tensor] = None, query_pos: Optional[Tensor] = None): tgt2 = self.norm1(tgt) q = k = self.with_pos_embed(tgt2, query_pos) tgt2 = self.self_attn(q, k, value=tgt2, attn_mask=tgt_mask, key_padding_mask=tgt_key_padding_mask)[0] tgt = tgt + self.dropout1(tgt2) tgt2 = self.norm2(tgt) tgt2 = self.multihead_attn(query=self.with_pos_embed(tgt2, query_pos), key=self.with_pos_embed(memory, pos), value=memory, attn_mask=memory_mask, key_padding_mask=memory_key_padding_mask)[0] tgt = tgt + self.dropout2(tgt2) tgt2 = self.norm3(tgt) tgt2 = self.linear2(self.dropout(self.activation(self.linear1(tgt2)))) tgt = tgt + self.dropout3(tgt2) return tgtdef forward(self, tgt, memory, tgt_mask: Optional[Tensor] = None, memory_mask: Optional[Tensor] = None, tgt_key_padding_mask: Optional[Tensor] = None, memory_key_padding_mask: Optional[Tensor] = None, pos: Optional[Tensor] = None, query_pos: Optional[Tensor] = None): if self.normalize_before: return self.forward_pre(tgt, memory, tgt_mask, memory_mask, tgt_key_padding_mask, memory_key_padding_mask, pos, query_pos) return self.forward_post(tgt, memory, tgt_mask, memory_mask, tgt_key_padding_mask, memory_key_padding_mask, pos, query_pos)

    有了一個Decoder Layer的定義,再看Transformer的整個Decoder:

    class TransformerDecoder(nn.Module):#值得注意的是:在使用TransformerDecoder時需要傳入的參數有:# tgt:Decoder的輸入,memory:Encoder的輸出,pos:Encoder的位置編碼的輸出,query_pos:Object Queries,一堆mask def forward(self, tgt, memory, tgt_mask: Optional[Tensor] = None, memory_mask: Optional[Tensor] = None, tgt_key_padding_mask: Optional[Tensor] = None, memory_key_padding_mask: Optional[Tensor] = None, pos: Optional[Tensor] = None, query_pos: Optional[Tensor] = None): output = tgtintermediate = []for layer in self.layers: output = layer(output, memory, tgt_mask=tgt_mask, memory_mask=memory_mask, tgt_key_padding_mask=tgt_key_padding_mask, memory_key_padding_mask=memory_key_padding_mask, pos=pos, query_pos=query_pos) if self.return_intermediate: intermediate.append(self.norm(output))if self.norm is not None: output = self.norm(output) if self.return_intermediate: intermediate.pop() intermediate.append(output)if self.return_intermediate: return torch.stack(intermediate)return output.unsqueeze(0)

    然后是把Encoder和Decoder拼在一起,即總的Transformer結構的實現:
    此處考慮到字數限制,省略了代碼。

    實現了Transformer,還剩后面的FFN:

    class MLP(nn.Module): """ Very simple multi-layer perceptron (also called FFN)""" 代碼略,簡單的Pytorch定義layer。

    匈牙利匹配HungarianMatcher類:
    這個類的目的是計算從targets到predictions的一種最優排列。
    predictions比targets的數量多,但我們要進行1-to-1 matching,所以多的predictions將與?匹配。
    這個函數整體在構建(13)式,cost_class,cost_bbox,cost_giou,對應的就是(13)式中的幾個損失函數,它們的維度都是(b,100,m)。
    m包含了這個batch內部所有的?。

    # pred_logits:[b,100,92] # pred_boxes:[b,100,4] # targets是個長度為b的list,其中的每個元素是個字典,共包含:labels-長度為(m,)的Tensor,元素是標簽;boxes-長度為(m,4)的Tensor,元素是Bounding Box。 # detr分類輸出,num_queries=100,shape是(b,100,92)bs, num_queries = outputs["pred_logits"].shape[:2]# We flatten to compute the cost matrices in a batchout_prob = outputs["pred_logits"].flatten(0, 1).softmax(-1) # [batch_size * num_queries, num_classes] = [100b, 92]out_bbox = outputs["pred_boxes"].flatten(0, 1) # [batch_size * num_queries, 4] = [100b, 4]# 準備分類target shape=(m,)里面存儲的是類別索引,m包括了整個batch內部的所有gt bbox# Also concat the target labels and boxestgt_ids = torch.cat([v["labels"] for v in targets])# (m,)[3,6,7,9,5,9,3] # 準備bbox target shape=(m,4),已經歸一化了tgt_bbox = torch.cat([v["boxes"] for v in targets])# (m,4)#(100b,92)->(100b, m),對于每個預測結果,把目前gt里面有的所有類別值提取出來,其余值不需要參與匹配 #對應上述公式,類似于nll loss,但是更加簡單# Compute the classification cost. Contrary to the loss, we don't use the NLL,# but approximate it in 1 - proba[target class].# The 1 is a constant that doesn't change the matching, it can be ommitted. #行:取每一行;列:只取tgt_ids對應的m列cost_class = -out_prob[:, tgt_ids]# (100b, m)# Compute the L1 cost between boxes, 計算out_bbox和tgt_bbox兩兩之間的l1距離 (100b, m)cost_bbox = torch.cdist(out_bbox, tgt_bbox, p=1)# (100b, m)# Compute the giou cost betwen boxes, 額外多計算一個giou loss (100b, m)cost_giou = -generalized_box_iou(box_cxcywh_to_xyxy(out_bbox), box_cxcywh_to_xyxy(tgt_bbox))#得到最終的廣義距離(100b, m),距離越小越可能是最優匹配# Final cost matrixC = self.cost_bbox * cost_bbox + self.cost_class * cost_class + self.cost_giou * cost_giou #(100b, m)--> (b, 100, m)C = C.view(bs, num_queries, -1).cpu()#計算每個batch內部有多少物體,后續計算時候按照單張圖片進行匹配,沒必要batch級別匹配,徒增計算sizes = [len(v["boxes"]) for v in targets] #匈牙利最優匹配,返回匹配索引 #enumerate(C.split(sizes, -1))]:(b,100,image1,image2,image3,...)indices = [linear_sum_assignment(c[i]) for i, c in enumerate(C.split(sizes, -1))] return [(torch.as_tensor(i, dtype=torch.int64), torch.as_tensor(j, dtype=torch.int64)) for i, j in indices]

    在得到匹配關系后算loss就水到渠成了。loss_labels計算分類損失,loss_boxes計算回歸損失,包含?。

    PS:作者將繼續更新Section2和Section3,請保持關注~

    參考文獻:

    code:
    https://github.com/jadore801120/attention-is-all-you-need-pytorch
    https://github.com/lucidrains/vit-pytorch
    https://github.com/facebookresearch/detr

    video:
    https://www.bilibili.com/video/av71295187/?spm_id_from=333.788.videocard.8

    blog:
    https://baijiahao.baidu.com/s?id%3D1651219987457222196&wfr=spider%26for=pc
    https://zhuanlan.zhihu.com/p/308301901
    https://blog.csdn.net/your_answer/article/details/79160045

    ◎作者檔案

    作者:科技猛獸

    往期精彩回顧適合初學者入門人工智能的路線及資料下載機器學習及深度學習筆記等資料打印機器學習在線手冊深度學習筆記專輯《統計學習方法》的代碼復現專輯 AI基礎下載機器學習的數學基礎專輯 獲取本站知識星球優惠券,復制鏈接直接打開: https://t.zsxq.com/qFiUFMV 本站qq群704220115。加入微信群請掃碼:

    總結

    以上是生活随笔為你收集整理的【深度学习】搞懂 Vision Transformer 原理和代码,看这篇技术综述就够了的全部內容,希望文章能夠幫你解決所遇到的問題。

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