成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

從模型原理到代碼實踐,深入淺出上手 Transformer,叩開大模型世界的大門

人工智能 開發
Transformer 主要是設計用來做翻譯的,分兩大塊編碼器和解碼器,GPT基本可以認為就是Transformer的解碼器部分。

作者 | Plus

一、序言

作為非算法同學,最近被Cursor、DeepSeek搞的有點焦慮,同時也非常好奇這里的原理,所以花了大量業余時間自學了Transformer并做了完整的工程實踐。希望自己心得和理解可以幫到大家~

如有錯漏,歡迎指出~

本文都會以用Transformer做中英翻譯的具體實例進行闡述。 

二、從宏觀邏輯看Transformer

讓我們先從宏觀角度解釋一下這個架構。

首先 Transformer也是一個神經網絡,神經網絡的本質是模擬人腦神經元的思考過程,數學上是一種擬合,當然,人腦內部的信號處理是否連續或者可擬合我們不得而知,但Transformer在我的機器上實實在在地思考并輸出了正確的答案。

Transformer 主要是設計用來做翻譯的,分兩大塊,如上圖,左邊的編碼器和右邊的解碼器。

編碼器負責提取原文的特征, 解碼器負責提取當前已有譯文序列的特征,并結合原文特征(編碼器解碼器的連線部分),給出下一個詞的預測。

GPT基本可以認為就是Transformer的解碼器部分。

接下來我們分幾個部分逐步講解并附上代碼實踐,很長 得慢慢看....

三、輸入和輸出

我認為大部分文章都沒有把輸入和輸出講得很細,其實理解了輸入輸出,你就基本可以理解Transformer的大半了。

1. 模型的輸入

以中英翻譯為例,Transformer的輸入分兩部分:

  • 源文序列(中文) 即圖1左側部分,輸入到編碼器。 舉例: 我愛00700
  • 目標譯文(英文)即圖1右側部分,輸入到解碼器。 一開始只有一個,僅用于告訴模型開始翻譯

2. 模型的輸出

很顯然,對于上述輸入,我們期待的輸出是 I love 00700

的作用是表示翻譯結束,因為翻譯的英文不一定和輸入的中文等長,所以需要有一個結束符。

模型并不能一下子輸出完整的句子,他是一個詞一個詞( /token/ )吐出來的,并且每一個詞都需要作為下一詞的輸入,這也是為什么大模型都是打字機交互的原因。  具體例子:

第一個循環 
 編碼器輸入 我 愛 00700  
 解碼器輸入 <bos>
輸出 I

第二個循環 
 編碼器輸入 我 愛 00700 
 解碼器輸入 <bos> I
輸出 love

第三個循環 
 編碼器輸入 我 愛 00700 
 解碼器輸入 <bos> I love
輸出 00700
 
第四個循環 
 編碼器輸入 我 愛 00700 
 解碼器輸入 <bos> I love 00700
輸出 <eos>
// 輸出了結束符,翻譯完成

請注意上述是為了簡化理解的一個陳述,實際上模型真正的輸出并不是一個詞,而是整個 /詞表/ 內任意一個詞可能的概率,也就是圖2所示的probabilities。

具體說,就是一個數組[0.2, 0.7, 0.1],  序號為0的詞的概率是0.2,序號為1的詞的概率是0.7,序號為2的詞的概率是0.1。 假設這是第二輪循環,詞表是[I, love, 00700] 取最大概率0.7的詞,然后得到第二輪輸出是love, 和第一輪輸出拼起來就是 I love。

3. 詞表 & token

劃線的兩個詞  /token/   /詞表/  你可能仍有疑惑。這正是我們真正弄清楚整個輸入輸出的關鍵。

顯然,計算機只能理解二進制數據,我們說輸入"我愛00700"的時候,實際上輸入的是處理好的二進制數據。

如何把句子轉化成模型可以理解的二進制數據呢? 不妨先想想我們怎么學英語的, 沒錯,背單詞??! 我們需要先認識詞,然后理解句子,模型也是一樣的。

我們背的英文單詞表,和這里的模型 /詞表/ 其實是一個意思,當然,形式略有不同。  詞表里的每一個詞,就是我們說的 /token/ ,請注意 token 不一定是一個英語單詞。上述例子的英文詞表可能是: [I, lo, ve, 00, 7] 。 顯然,token不是按照空格分的。 原因是算力難以覆蓋,英文單詞有幾十萬個(不權威),老黃聽了都搖頭??。 而更低維度的分詞,可以壓縮詞表的數量,[I, lo, ve, 00, 7]是我瞎寫的,理解意思就行,讓DS給我們舉一個更好的例子:

假設語料庫包含以下高頻單詞:

  • play (10次)
  • player (8次)
  • playing (6次)
  • plays (5次)
  • replay (7次)
  • replaying (4次)

直接按空格分詞會生成獨立詞表:

空格分詞詞表大?。?
[play, player, playing, plays, replay, replaying]
# bpe分詞算法最終詞表(目標大小=4)
# ?表示這個token只會出現在開始,想一想 還原句子的時候你需要知道兩個token是拼起來還是插入空格
[?play, re, ing, er]

對比空格分詞, /bpe分詞算法/ (想了解的自行ds,限于篇幅不贅述)可以有效壓縮詞表大小,在大量語料的情況下會更明顯,40GB數據的GPT2也僅5w詞表大小

4. 嵌入(embedding)

現在,我們有了詞表,很容易就可以得到二進制的序列了

還是老例子:

我 愛 00700 被編碼為 [1, 2, 3]    (為了方便,這里假設分詞就這樣。)

我們可以直接輸入到網絡訓練了嗎? 答案是否定的,不過我們也終于來到了有意思的地方。 在在訓練之前需要做一個 /嵌入/

來一個youtobe的動圖看下 /嵌入/ 大概是啥。

再來一個DS的靈魂解釋:

嵌入(Embedding)的核心思想是 將復雜、高維的數據(如文字、圖像)轉化為低維、連續的數值向量,同時保留其內在的語義或關系 。這種轉化讓計算機能像人類一樣“理解”數據的含義,并用數學方式計算相似性、分類或生成。

額... 有點抽象,似乎講了些什么,又似乎什么都沒講......

沒關系,讓我來舉一個形象的例子 觀察下面的句子:

紅色
當紅明星
生意好紅火啊

注意"紅"這個字,發現了嗎,在不同語境(或者說維度)它有不同的意思,如果我們直接輸入token編碼,這些信息是缺失的,模型就不太可能理解句子的意思。

那么,一個字或者說一個token到底有多少維度的語義呢?不知道啊??,但是沒關系,猜一個, 512。 沒錯,就是這么樸實無華!

于是我們就有了嵌入矩陣  (self.weight)

: 詞表大小(num_embeddings,如32000)

: 嵌入維度(embedding_dim,如512)

即E是一個32000x512的二維矩陣 (代碼里就是數組)

E[1,1]  E[1,2] ...... E[1,512]   的值表示詞表中序號為1的詞在維度1、2、512的語義, 由此同樣一個“紅”的token,就可以有多種語義,這樣模型就可以理解詞和句子了。

當我們輸入 我 愛 00700 被編碼為 [1, 2, 3] , 實際上我們應該輸入:

請注意請注意,所謂一個字有不同維度的語義,是我現編的比喻,如果它幫到你理解這個東西那最最好,如果覺得比喻得不太對,大可一笑了之。

繼續

E[1,1]  E[1,2] ...... E[1,512] 我們可以把它看作是以原點為起點的 512維的向量 ,一般就叫做詞的嵌入向量,也就是平時可能聽到的向量化,沒什么高大上的對吧。 向量化之后可以做什么呢?看圖

(以三維舉例,512維可以發揮想象力自行腦補)

為了讓模型理解詞和詞的關系,我們用數學語言去描述這種關系,就是向量的內積:

  •  是向量a的長度(范數)
  • 是向量b的長度(范數)
  • 是兩個向量之間的夾角

就算忘記了向量內積怎么算也沒關系,我們只需要理解,夾角越小,向量內積越大, 兩個詞的關聯越緊密就行了。 實際上大部分復雜公式的計算都是封裝好了的。 這里的內積,在自注意力的時候我們也會用到。

到了這里就可以解釋一下 的元素值是怎么來的了,我們定義了每一個詞有512個維度,那么每一個維度的值是什么呢?

答案是模型自己學習出來的,E是模型的參數的一部分,一個詞每個維度的值,初始化是一個隨機值,模型訓練的時候會被更新。

怎么學出來的? 試想一下: 語料里面有無數的詞,但是他們是有一定的關聯的,比如 I love 這兩個詞經常同時出現,那么他們的內積就應該較大,反之也一樣。實際上有點像聚類,怎么理解都行,模型學得差不多的時候,token的分布或者說嵌入向量的關系,一定是有規律的。

讓我們來用數學語言總結一下嵌入:

就是我們的查表或者說嵌入操作:輸入張量 

  • B: 批次大小(batch_size)
  • L: 序列長度(sequence_length)
  • D: 嵌入維度

嵌入查找操作上述例子 就是  ,L是句子長度也就是3

為什么這里也會叫查表? 實際上對索引為1的token做嵌入,就是在嵌入矩陣里到找第一行然后拿出來,就是 E[1,1]  E[1,2] ...... E[1,512] 。這個過程不就是查表么。

5. Batch 處理

細心的你可能發現了問題,這里多了一個維度B,輸入的結果 ,是三維的。這里解釋一下,我們的例子只有一句話,但是在模型的訓練中,其實是一次性輸入多句話并行計算的,batch_size 的值就是你一次性輸入多少句子來訓練,一般來說,這取決于你的顯存大小,自行嘗試調整就可以了。

假設batch 為2,一次輸入兩句:

兩句話的embedding矩陣合并,得到一個 2x3x512的三維矩陣 就是我們給編碼器的輸入了。 此時 B=2  L=3  D=512

6. padding掩碼矩陣

還沒完,讓我們來一點刁鉆的問題。 上述例子L=3,兩個句子長度都一樣=3。 如果句子長度不一樣呢?batch矩陣豈不是拼不出來? 當然不會,實際寫代碼的時候,L一般都是取一個較大值,比如語料中最長句子的值。L=100。 不夠長的句子怎么辦呢? 補padding,注意不是0,因為神經網絡的某些中間計算如softmax輸入0也是有值的。 一般是用一個特殊的token作為padding。

在自注意力計算的時候(下文會講)會根據padding的位置,生成mask 矩陣,計算時候padding替換為一個極小值比如-1e9,就可以不影響計算了。

7. PositionalEncoding

現在這個帶padding的矩陣可以輸入模型開始訓練了嗎? 還是不行.....  我們還缺少一個重要的信息,位置。 舉一個例子:

[ [0.3, 0.5, 0.1, 0.4]
[貓,吃,魚]  => Transformer =>    [0.1, -0.6, -0.2, 0.3], 
                                 [0.3, 0.5, 0.3, -0.1] ]

                                 [ [0.3, 0.5, 0.1, 0.4]
[魚,吃,貓]  => Transformer =>     [0.1, -0.6, -0.2, 0.3], 
                                   [0.3, 0.5, 0.3, -0.1] ]

很明顯,雖然只是交換了一個字的順序,但其實是兩個完全不同意義的句子, 而 Transformer輸出的分布卻是不變的,說明網絡沒有辦法識別位置信息。

為了解決這個問題,Transformer的論文里的方案是添加位置編碼,也就是PositionalEncoding。

其實沒有那么神秘,我們把 Transformer 看成函數T , 現在的問題是:

我們可以加上一個位置編碼P去規避這個問題:

: 當前的位置序號

來一個簡單粗暴易理解的 P(i) = i

貓 0
 吃 1
 魚 2

然后呢?就是簡單的直接加上去

沒錯,這樣其實我們的輸入就包含了位置信息了,只要信息在那里,模型總能學明白

當然,這里只是為了方便說明位置編碼本身,所以用了最簡單的方案,你說它能不能跑,那肯定也是能跑的,就是會有不少問題,實際上論文里實現是正弦torch.sin(position * div_term)和余弦函數torch.cos(position * div_term)來生成位置編碼, 解釋起來就篇幅太長,大家可以自己探索。

位置編碼不改變矩陣的維度,只是改變了數值, 我們給編碼器的最終輸入總結如下:

我愛我愛分詞嵌入

但是對于解碼器的輸入,還需要額外處理,繼續往下。

8. 并行計算 & Teacher Forcing

回顧下一開始的例子

第一個循環 
 編碼器輸入 我 愛 00700  
 解碼器輸入 <bos>
輸出 I

第二個循環 
 編碼器輸入 我 愛 00700 
 解碼器輸入 <bos> I
輸出 love

第三個循環 
 編碼器輸入 我 愛 00700 
 解碼器輸入 <bos> I love
輸出 00700
 
第四個循環 
 編碼器輸入 我 愛 00700 
 解碼器輸入 <bos> I love 00700
輸出 <eos>
// 輸出了結束符,翻譯完成

我們給編碼器的輸入始終是整個句子序列的embedding。而給解碼器的輸入,則是當前已經預測出來的 n-1個詞的序列的embedding。

(1) 并行計算

當你真的上手去寫代碼的時候,你就會發現, 在訓練階段,代碼里我們給解碼器直接輸入完整的句子I love 00700 ,且模型輸出直接就是整個句子。 只有一個循環。你現在肯定滿腦子問號,直接輸入正確答案了,還學什么?

別急,實際上我們輸入不是全部的ground truth,是 n-1序列的ground truth。怎么理解呢?真正的ground truth 是  I love 00700 eos 。  我們的輸入是I love 00700。 當然這樣僅僅是少了最后一個token的答案而已。

實際上解碼的輸入(姑且叫trg_input)在做計算的時候還需要做一個causal mask,叫做因果掩碼,字面意思就是為了防止計算的時候知道未來的信息。

>>> trg_input # 假設我們的input長這樣   1234代表 <bos> I love 00700
tensor([[1, 2, 3, 4],
        [1, 2, 3, 4],
        [1, 2, 3, 4],
        [1, 2, 3, 4]])
# 因果掩碼矩陣是這樣的,對角線上移一位的上三角矩陣,上三角的元素是極小值
>>> mask = torch.triu(torch.ones(4, 4), diagonal=1)
>>> mask = mask.float().masked_fill(mask == 1, float(-1e9))
>>> mask
tensor([[0., -inf, -inf, -inf],
        [0., 0., -inf, -inf],
        [0., 0., 0., -inf],
        [0., 0., 0., 0.]])
>>> 

#執行 causal mask
>>> trg_input + mask
tensor([[1., -inf, -inf, -inf],
        [1., 2., -inf, -inf],
        [1., 2., 3., -inf],
        [1., 2., 3., 4.]])

看看causal mask結果的第一行,[1., -inf, -inf, -inf] 其實就是相當于第一輪循環的輸入 bos, 因為無限小在計算的時候可以忽略。 后續的2、3、4行剛好也就是 對應的2、3、4輪循環。 get到了嗎, 直接作為一個矩陣輸入,在模型里并行計算,4個循環優化成了一個!能夠并行計算,這正是transformer一個重要特性。

(2) Teacher Forcing

想一想為什么我們可以實現并行計算? 因為訓練的時候,我們提前知道了正確答案,在生產環境推理過程中,我們不可能知道第一個token應該輸出I,第二個token應該輸出love ...., 所以推理的時候,只能順序執行循環。

再多想一點,在訓練的時候,模型還并不成熟,第一個字符輸出的未必是I,假設輸出了 He 呢? 然后第二輪循環我們的輸入就是 bos He, 這樣繼續循環下去只會越來越錯,非常不利于學習的收斂。 所以,我們第二輪循環不輸入He, 而是用正確答案 I 來繼續下一個token預測,實驗表明這樣學習的速度會更快。  這種訓練方法,我們就稱之為Teacher Forcing。

關于并行計算和Teacher Forcing感覺大部分文章都是一筆帶過,而我感覺這里很重要,包括理解架構本身或者是去理解為什么transformer帶來了技術的爆發,并行計算是一個很重要的因素。

9. 輸入輸出的代碼實現

作為一個嚴謹的工程師,最后我貼一下我自己的實現,并做簡單講解:

# 關于語料 我的數據集來自 wmt17 v13 zh-en  大約100m 30+w行長句子
# 從csv讀出來, 第一列是中文 第0列是英文
train_data = read_tsv_file(opt.train_tsv, 1, 0, opt.delimiter) 
valid_data = read_tsv_file(opt.valid_tsv, 1, 0, opt.delimiter) if opt.valid_tsv else []
test_data = read_tsv_file(opt.test_tsv, 1, 0, opt.delimiter) if opt.test_tsv else []
 
# 分詞器初始化 我這里是bpe bytelevel  32000 詞表大小,具體實現太長就不貼了
src_tokenizer_path = os.path.join(opt.data_dir, "tokenizer_zh.json")
src_tokenizer = train_tokenizer([src for src, _ in train_data], opt.vocab_size, src_tokenizer_path)
    
trg_tokenizer_path = os.path.join(opt.data_dir, "tokenizer_en.json")
trg_tokenizer = train_tokenizer([trg for _, trg in train_data], opt.vocab_size, trg_tokenizer_path)
 
# 有了分詞器之后,對訓練數據句子分詞 打包成pkl格式,方便模型訓練的時候讀取
processed_train = process_data(train_data, src_tokenizer, trg_tokenizer, opt.max_len)
 
# train階段
# 從pkl加載數據 略

# 訓練輸入
for i, batch in enumerate(dataloader):
        # 準備數據 每次循環處理一個batch的數據
        src = batch['src'].to(device) # 取出語料原文。我 愛 00700 (舉例,實際上batch是多句的數組,但是矩陣運算的時候都是一起并行算的)
        trg = batch['trg'].to(device) # 取出語料目標譯文。bos I love 00700 eos
        trg_input = trg[:, :-1]  # 去掉最后一個token, bos I love 00700  n-1的Teacher Forcing序列
        trg_output = trg[:, 1:]  # 去掉第一個token, I love 00700 eos 真實的groud truth,用于計算損失
        
        # 前向傳播 清空梯度
        optimizer.zero_grad()

        # 輸入模型計算一次
        # teacher forcing: trg_input 實際上就是ground truth(正確的值)
        # train 模式下,output是整個句子的預測值
        output = model(src, trg_input)
        ......
 

# 嵌入
"""初始化嵌入矩陣"""
        # 使用正態分布(高斯分布)初始化張量
        # 均值為0,標準差為0.02
        # 在數學上,512維的隨機向量,可以認為是互相正交的(就是任意兩個token之間一點關系都沒有)
        nn.init.normal_(self.weight, mean=0, std=0.02)
 
     # 基本的嵌入查找 
     # weight 是模型參數,自動參與學習
        embeddings = self.weight[x]
        

# mask
 """創建源序列和目標序列的掩碼"""
        src_padding_mask = (src == self.pad_idx) #  src padding掩碼
        trg_padding_mask = (trg == self.pad_idx) #  trg padding掩碼
        
        # 因果掩碼
        seq_len = trg.size(1)
        trg_mask = torch.triu(
            torch.ones((seq_len, seq_len), device=src.device), diagonal=1
        ).bool()
        
        return src_padding_mask, trg_padding_mask, trg_mask
 
 # 應用mask   我這里的mask是bool矩陣,true表示需要mask
 # 如果mask是true,scores對應位置的元素就填充為極小值 -1e9
        if mask is not None:
            scores = scores.masked_fill(mask == True, -1e9)

輸入輸出終于寫完 看到這里就真的不容易 感謝~!

四、自注意力機制

到這里我們已經完成了輸入的所有前置處理,訓練數據開始進入到網絡訓練。 而理解Transformer網絡的關鍵就在于自注意力機制,如下圖所示的三個模塊,他們是整個網絡的核心。

1. 注意力機制

注意力其實非常好理解, 如下圖: 萬綠叢中一點紅,你首先看到了紅,這就是注意力。

在訓練網絡中,注意力的作用是什么呢?

老例子:“ 我 愛 00700 ”  翻譯為    “ I love () ”    () 為當前正在預測的詞。

如上圖, 前面我們介紹過Transformer預測下一個詞的時候,會結合整個上下文,即“我 愛 00700  I love”,假如沒有注意力機制,那么所有詞對預測結果的貢獻是一樣的!  假設語料的言情文偏多,那輸出很可能是 I love you, 很明顯不合理, 此時我們希望網絡把注意力集中在“00700”這個詞上,其他詞應該忽略掉。

那么在數學上注意力是什么呢? 非常簡單,就是權重系數寫出來就是:

  • x ∈ R^(seq_len, d_model)  seq_len是句子長度,d_model=512是embdedding的緯度,忘了的話往回翻復習下吧
  • W ∈ R^(512, 512) 是權重矩陣

???  解釋了這么久 就這? 沒錯是的,取個高大上的名字而已。

2. 自注意力機制

自注意力畢竟多了一個字,肯定還是有點不一樣的,不一樣在哪呢,他乘的不是權重矩陣,它乘它自己!

  • 即權重 
  • x ∈ R^(seq_len, d_model)  seq_len是句子長度,d_model=512是embdedding的緯度,忘了的話往回翻復習下吧

重點看一下這個  

如果你對矩陣乘法熟,你馬上就會意識到,這不就是句子的詞和詞之間的內積嗎?

回憶一下嵌入的內容,我們說過詞向量的內積反應的是詞的關系遠近。

本質是,Transformer通過句子本身的詞和詞之間的關系計算注意力權重! 所以我們叫它 - 自注意力 !還算自洽吧~

當然,直接相乘沒有參數可以訓練啊,來個線性變換吧 ,于是

不好記,得起個名字, 第一個式子是原始請求 就叫Query吧,簡寫為Q

感覺不用糾結這個命名,反正論文沒提命名的道理 自行想象吧!


第二個式子是被用來計算關系的,就叫Key吧,簡寫為K

于是:

按照論文所提,實驗發現,QK點積可能會過大,數據方差太大,導致梯度不太穩定,所以需要把分布均勻一下,于是:

我們需要的是權重,所以需要轉化為 0.0~1.0 這樣的值,這正是 /softmax/ 函數的能力 于是:

代入 , y改寫為Attention,更高大上

另外為了增加網路的復雜度,我們不直接乘x,老辦法,線性變換一下, 記為V

最終

最終我們得到了和論文一模一樣的公式~

3. 多頭注意力機制

先看一個圖,這是一個自注意力計算的示例:

問題是: 我 - 我  愛-愛  00700-00700  的分數最高, 自己最關注自己,聽起來是合理的,但論文作者應該是覺得這不利于收斂。 所以提出了多頭機制。

其實這里論文并沒有太多解釋,我理解是一種實驗性的經驗。

那么什么是多頭? 其實也很簡單,就是分塊計算注意力,好比原來是一個頭看整個矩陣, 現在是變身哪吒三頭六臂,一個頭看一小塊,然后信息合并。 畫圖說明:

這種分頭其實本質上計算量是一樣的,只是注意力分塊了,直接看公式就好了。

  • Q, K, V 是輸入矩陣,形狀為 (batch_size, seq_len, d_model)
  • W_i^Q ∈ R^(d_model × d_k)
  • W_i^K ∈ R^(d_model × d_k)
  • W_i^V ∈ R^(d_model × d_v)
  • head_i 的形狀為 (batch_size, seq_len, d_v)

論文中 i=8, 8個頭, dk =dv = d_model/8 = 64

因果掩碼-多頭自注意力機制:

其實輸入輸出的時候就講了,就是解碼器做多頭計算的時候 使用teacher forcing,需要加上因果掩碼mask。 下面代碼說明。

4. 交叉-多頭自注意力機制

解碼器如何結合編碼器的上下文就在這里了,如果是交叉-多頭自注意力機制,那么 Q = x, K=V=編碼器的輸出,直接看代碼最直觀。

# 1. 線性變換 這里的 query=key=value 就是x 
     # 如果是解碼器的 交叉-多頭自注意力機制,那么 query = x, key=value=編碼器的輸出
        Q = self.q_linear(query)  # 這里的linear就是線性變換 wx+b,下同
        K = self.k_linear(key)
        V = self.v_linear(value)
        
        # 2. 分割成多頭 [batch_size, seq_len, d_model] -> [batch_size, seq_len, num_heads, d_k]
     # 實際上 torch的張量(矩陣) 是用一緯數組存的,所謂分割多頭、轉置,最終就是改一下數組元素的順序
        Q = Q.view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        K = K.view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        V = V.view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        
        # 3. 計算注意力
     # K.transpose(-2, -1) 就是 k的轉置
        scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k) 
        
        # 4. 應用mask  因果掩碼 或者 padding 掩碼
        if mask is not None:
            scores = scores.masked_fill(mask == True, -1e9)
        
        # 5. softmax獲取注意力權重
        attn = F.softmax(scores, dim=-1)
     # dropout是隨機丟棄一些值(寫0),為了增加隨機性,訓練時才會開啟,避免過擬合
        attn = self.dropout(attn)
        
        # 6. 注意力加權求和  就是乘v
        out = torch.matmul(attn, V)
        
        # 7. 重新拼起來多頭 把維度轉置回來
        out = out.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
        
        # 8. 最后又加了一個線性變換 wx+b, 不要為問我為什么  ...... 實驗性經驗
        out = self.out_linear(out)
        
        return out

五、前向傳播

到這里就很簡單了,核心的部分都講完了。 還剩下幾個小塊簡單講一下。

1. Add & Norm

Add 叫殘差,超級簡單,直接上代碼:

# self_attn 就是算注意力,tgt_mask是因果掩碼。  
 # 本來應該是  x = self.dropout1(self.self_attn(x2, x2, x2, tgt_mask)) 
 # 殘差就是多加了 x,    x = x + self.dropout1(self.self_attn(x2, x2, x2, tgt_mask)) 
 # 簡單解釋就是 加這個x,防止梯度下降太快到后面沒了。詳細就不展開了,可自行gpt
 x = x + self.dropout1(self.self_attn(x2, x2, x2, tgt_mask))

Norm 是歸一化,簡單解釋一下,歸一化是讓數據分布更均衡,一般是消除一些離譜值、不同數據量綱的影響。 比如分析年齡和資產對相親成功率的影響,年齡只有0-100, 資產的范圍就很大,計算的時候就不好弄,需要把資產也縮放到一個合理的數值范圍 比如 0-100 萬。

歸一化有很多種,這里用的是  /Layernorm/ ,這個公式還挺復雜,有興趣可以自己研究,這里我是偷懶的:

# 直接使用 pytorch的自帶公式
 self.norm1 = nn.LayerNorm(d_model)
 # 調包就完事
 x2 = self.norm1(x)

2. FeedForward

前饋全連接層(feed-forward linear layer) ,好多文章試圖解釋這個意義,其實沒啥意義,就是普通的兩層網絡, 一般來說就是一層神經網絡就是 線性變換+激活函數。

線性變換,無非就是升維和降維。舉例:升維是類似于你看圖片的時候,雙指放大,然后滑來滑去找到你需要的,是特征放大。降維么,就是截圖保存放大部分,然后輸出,是特征提取。

一升一降就是FeedForward啦,直接看代碼:

class FeedForward(nn.Module):
    def __init__(self, d_model: int, d_ff: int, dropout: float = 0.1):
        super().__init__()
     # 從dmodel升維到d_ff = 2048 論文的值  
        self.linear1 = nn.Linear(d_model, d_ff)
 
     # 降回來提取特征  
        self.linear2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, x: torch.Tensor) -> torch.Tensor:
     # 怎么找到關注特征的 那不就是激活函數 F.relu ,很明顯,神經元被激活的地方就是感興趣的特征啊。
        return self.linear2(self.dropout(F.relu(self.linear1(x))))

3. 解碼器

到這里就沒啥了,每個塊我們都理解了,后續就是實現邏輯了。 直接上代碼,太長了好像沒必要,文末尾放git地址把。

4. 編碼器

同上。

六、反向傳播

前向傳播計算出一次訓練的結果,反向傳播就是根據結果的好or壞,更新參數,循環往復,最終得到一個滿意的模型。

自己實現 實在是有點累,調包只需要一句:

# 前向傳播
        output = model(src, trg_input)
        
     ......

        # 計算損失
        loss = criterion(output_flat, trg_output_flat)
        
        # 反向傳播
        loss.backward()

pytorch框架會記錄你整個模型前向傳播的圖,然后根據損失,使用 /鏈式法則 / & /梯度下降算法/ 幫你回溯計算更新每一個參數,太舒服了~

責任編輯:趙寧寧 來源: 騰訊技術工程
相關推薦

2024-12-12 09:00:28

2024-03-27 10:14:48

2021-07-20 15:20:02

FlatBuffers阿里云Java

2018-12-25 08:00:00

2022-02-25 08:54:50

setState異步React

2024-07-07 21:49:22

2023-01-06 12:50:46

ChatGPT

2021-03-16 08:54:35

AQSAbstractQueJava

2020-07-29 10:10:37

HTTP緩存前端

2021-08-10 14:10:02

Nodejs后端開發

2020-11-06 09:24:09

node

2011-07-04 10:39:57

Web

2023-05-18 08:54:22

OkHttp源碼解析

2009-11-30 16:46:29

學習Linux

2019-11-11 14:51:19

Java數據結構Properties

2022-12-02 09:13:28

SeataAT模式

2025-05-09 01:30:00

JavaScript事件循環基石

2022-05-06 07:19:11

DOMDiff算法

2019-11-21 09:16:14

OpenStack安全組MAC

2019-01-07 15:29:07

HadoopYarn架構調度器
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 日韩免费福利视频 | 久久精品视频亚洲 | 亚洲一区二区三区久久 | 久久久久久久99 | 久久天堂网 | 精品在线播放 | 久久精品欧美一区二区三区麻豆 | 亚洲成人中文字幕 | 99re| 日韩精品一区中文字幕 | 日韩欧美成人一区二区三区 | 国产精品久久久久久久久久久久久 | 在线天堂免费中文字幕视频 | 国产精品久久久久久久久久久久久久 | 极品的亚洲 | 久久久免费少妇高潮毛片 | www.伊人.com| 国产精品.xx视频.xxtv | 女同videos另类 | 九九热在线观看视频 | 欧美福利三区 | 久久久久久国产精品免费免费狐狸 | 福利网站在线观看 | 久久久久国产 | 精品日韩一区二区 | 免费日韩av网站 | 99久久精品免费看国产高清 | 九九亚洲 | 欧美日韩在线视频一区二区 | 欧美h | 一区二区三区在线观看视频 | 欧美v免费 | 99久久精品一区二区成人 | 日韩精品久久久久久 | 在线免费观看黄a | 97国产一区二区精品久久呦 | 四虎最新视频 | 黄色欧美 | 国产99精品 | 欧美三级在线 | 亚洲国产网 |