<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      探秘Transformer系列之(5)--- 訓練&推理

      探秘Transformer系列之(5)--- 訓練&推理

      0x00 概述

      Transformer訓練的目的是通過對輸入源序列和模型輸出序列的學習,來擬合真正的目標序列。推理的目的則是僅通過輸入序列來產生目標序列,作為輸入傳遞給解碼器的只有輸入序列,而沒有目標序列。

      本篇依然以文本翻譯為例進行學習。

      0x01 訓練

      LLM是自回歸模型,其只能以串行方式進行預測。而為了提高訓練效率,理想的訓練方式應該是并行計算:一次性輸入整個序列,一次性并行解碼把各個位置上的預測全部輸出。Transformer通過Teacher Forcing結合掩碼來滿足這個需求。我們接下來就看看訓練中的各個要點具體如何實施。

      1.1 輸入

      訓練數據由兩部分組成:

      • 源序列,比如”我喜歡吃蘋果“。
      • 目標序列,比如”I love apple“。

      源序列會輸入給編碼器。目標序列會輸入給解碼器。同時,目標序列也被轉換為真值標簽傳遞給優化器。我們期望解碼器的輸出盡可能接近真實標簽,因此要優化器要最小化交叉熵。因為是并行操作,所以我們會把源序列拆分之后組裝成矩陣,一次性給到Transformer。這樣通過矩陣運算實現并行操作,一次即可給出所有序列的預測。但是效果等同于一個一個詞輸入到編碼器進行串行解碼。

      1.2 Dropout

      dropout(丟棄)率,即隨機丟棄的神經元比例,是一個訓練時的超參數,需要根據具體任務進行調整。因為Dropout引入了隨機性,因此在測試(或推理)階段,通常會禁用Dropout,確保所有的神經元都參與到計算中,以獲得最穩定的模型輸出。

      原理

      Dropout(正則化)是一種廣泛用于機器學習和深度學習的通過給參數增加約束項來限制參數取值范圍的方法。它的目的就是阻礙模型過度學習(過擬合),從而提升算法的泛化能力。正則化不僅可以防止模型過擬合,還可以在一定程度上緩解梯度爆炸問題。因為通過給參數增加約束項,可以限制參數在更新過程中的取值范圍,從而避免梯度因參數值過大而爆炸。

      Dropout概念是Hilton在論文“Improving neural networks by preventing co-adaptation of feature detectors”中提出。如下圖所示,實施dropout之后,原始網絡相當于變成一個更瘦更稀疏的網絡。dropout通過隨機丟棄的神經元來削弱節點彼此之間的依賴,這樣可以有效的緩解過擬合的發生,在一定程度上達到正則化的效果,有助于模型更快地收斂并提高性能,進而解決深度學習神經網絡在用小數據集訓練時常見的兩大問題:過擬合和訓練時間長。

      如果從集成學習角度來看,每做一次Dropout,相當于從原始的網絡中采樣得到一個子網絡。Dropout對于每個batch的step所優化的參數都不同,每次迭代都相當于訓練一個不同的子網絡,這些子網絡都共享原始網絡的部分參數。而且它會不斷在這個基礎上進行疊加訓練。那么,最終的網絡可以近似看作集成了若干個不同網絡的組合模型。即,Dropout的子網絡的平均,提供了一種廉價的近似的Bagging集成。

      另外,Dropout 實際上也可以被看作是一種稀疏性表現。論文“On the Effectiveness of Parameter-Efficient Fine-Tuning”就指出稀疏性在模型訓練中的兩個主要優勢:增強模型的魯棒性和降低泛化誤差。

      Dropout 可以在一定程度上達到這種稀疏性理論分析效果。

      位置

      Dropout layer 在 Transformer 結構中隨處可見,如下圖所示,具體分為四種:

      • 輸入時的dropout(對應圖上序號1)。
      • 注意力機制中對注意力權重會施加dropout(對應圖上序號2)。
      • FFN中兩個全連接層之間會施加dropout(對應圖上序號3)。
      • "Add & Norm"之間也有dropout(對應圖上序號4)。

      具體對應如下代碼片段。

      • 在 token embedding,positional encoding 求和之后,有 Dropout。
      class PositionalEncoding(nn.Module):
          "Implement the PE function."
      
          def forward(self, x):
              x = x + self.pe[:, : x.size(1)].requires_grad_(False)
              return self.dropout(x) # 這里用到Dropout
      
      • 在注意力中,\(QK^T\) 經過 scale、掩碼、softmax 得到權重之后,要經過 Dropout 才會與 V 相乘。此時隨機“丟棄”一些權重的目的是防止模型過分依賴某些特定的輸入。用數學公式展示如下。

      \[Z = Attention(Q,K,V) = Dropout(softmax(\frac{QK^T}{\sqrt d_k}))V \]

      具體代碼如下。

      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 = scores.softmax(dim=-1)
          if dropout is not None:
              p_attn = dropout(p_attn) # 這里用到Dropout
          return torch.matmul(p_attn, value), p_attn
      
      • 在 FFN 中兩個全連接層中間也有 Dropout。
      class PositionwiseFeedForward(nn.Module):
          def forward(self, x):
              return self.w_2(self.dropout(self.w_1(x).relu())) # 這里用到Dropout
      
      • 在每個注意力層和 FFN 層的輸出(殘差連接之前)都有 Dropout。
      class SublayerConnection(nn.Module):
          def forward(self, x, sublayer):
              return x + self.dropout(sublayer(self.norm(x))) # 在Norm&Add之中用到Dropout
      

      源碼

      大家可以通過下面PyTorch的源碼來了解到Dropout的內部機制。

      template<bool feature_dropout, bool alpha_dropout, bool inplace, typename T>
      Ctype<inplace> _dropout_impl(T& input, double p, bool train) {
      
        if (p == 0 || !train || input.numel() == 0) {
          return input;
        }
      
        if (p == 1) {
          return multiply<inplace>(input, at::zeros({}, input.options()));
        }
      
        at::Tensor b; // used for alpha_dropout only
        auto noise = feature_dropout ? make_feature_noise(input) : at::empty_like(input, LEGACY_CONTIGUOUS_MEMORY_FORMAT);
        noise.bernoulli_(1 - p);
        if (alpha_dropout) {
          constexpr double alpha = 1.7580993408473766;
          double a = 1. / std::sqrt((alpha * alpha * p + 1) * (1 - p));
          b = noise.add(-1).mul_(alpha * a).add_(alpha * a * p);
          noise.mul_(a);
        } else {
          noise.div_(1 - p);
        }
      
        if (!alpha_dropout) {
          return multiply<inplace>(input, noise);
        } else {
          return multiply<inplace>(input, noise).add_(b);
        }
      }
      
      ALIAS_SPECIALIZATION(_dropout,               false, false)
      ALIAS_SPECIALIZATION(_feature_dropout,       true,  false)
      ALIAS_SPECIALIZATION(_alpha_dropout,         false, true )
      ALIAS_SPECIALIZATION(_feature_alpha_dropout, true,  true )
          
      Tensor make_feature_noise(const Tensor& input) {
        auto input_sizes = input.sizes();
        std::vector<int64_t> sizes;
        sizes.reserve(input.dim());
        sizes.push_back(input_sizes[0]);
        sizes.push_back(input_sizes[1]);
        for (const auto i : c10::irange(2, input.dim())) {
          (void)i; 
          sizes.push_back(1);
        }
        return input.new_empty(sizes);
      }
      

      發展

      在小模型中可能dropout的效果比較顯著,因為小模型針對的是特定領域且少量數據的情況,容易過擬合。而在大模型時代是否需要使用dropout?答案不一。

      認為大模型不需要dropout的主要原因有如下幾點:

      • 因為大模型都是深層結構,以及在訓練過程中會使用損失低精度量化計算。使用dropout操作固然可以增加模型的泛化性,但其引入噪聲會導致模型訓練的不穩定性。
      • 使用dropout會導致計算資源的增加和效率的降低,首先要生成一個mask(需要額外顯存),然后計算結果也需要存下來(需要額外顯存)。反向傳播也需要執行額外的邏輯操作等,因此總體效率上肯定是更低的。
      • 現在的大模型都是decoder-only的結構,模型中使用了大量的如MQA、多頭、pre-norm,residual等技術,且使用到了大量的多領域的數據進行預訓練,在某種程度上也增加了泛化性,去掉一個dropout影響不大。

      然而某些大模型中也的確依然使用dropout,其作用點依然如下:

      • 對自注意力的輸出表示進行操作。
      • 對MLP輸出表示進行操作。

      當然其設置會依據大模型的特點進行調整,比如:

      • 對于輸入層的神經元,其保留率通常設為更接近1的數,使得輸入變化不會太大。這是因為對輸入層神經元進行丟棄時,相當于給數據增加噪聲,以此來提高網絡的魯棒性。
      • 對于中間隱藏層的神經元,一般來講, 設置0.5時效果最好,這對大部分的網絡和任務都比較有效。 當 = 0.5時,在訓練時有一半的神經元被丟棄,只剩余一半的神經元是可以激活的,隨機生成的網絡結構最具多樣性。
      • 輸出層一般不加dropout。

      而且近期也有把dropout進一步應用的工作,比如論文“LoRA Dropout as a Sparsity Regularizer for Overfitting Control”對 LoRA 矩陣 ?? 和 ?? 的輸入和輸出維度進行隨機 Dropout來達到更好的效果。之所以不對 ?? 的維度進行Dropout,是因為這樣會導致矩陣秩的降低,相當于在結構上使用更少的 ??,從而削弱模型的表達能力。

      1.3 損失函數

      損失函數通過評估模型預測值與真實值之間的差異來直觀地了解模型的預測性能,從而為優化算法提供明確的目標和方向,然后通過最小化損失來逐步優化模型參數。對于自回歸語言模型而言,關鍵之處是看模型能否正確預測到下一個單詞,因此優化目標是最小化交叉熵(cross entropy)。這里的交叉熵就是信息熵。或者說,預訓練階段量學習的目標就是最小化各領域的信息熵。

      在Transformer架構中,解碼器輸出后面接了一個模塊Generator。該模塊的作用是把解碼器輸出的隱向量從word embedding維度映射到詞表長度,得到logits。logits對應著該token取不同字的概率,接下來模型會依據這些概率,按照一定的采樣規則來采樣下一個token。模型的效果好壞就是看看模型是否可以把下一個token分類到真值對應的token。因此,每次預測新token都是一個分類任務,Generator就是一個分類頭。訓練會依據分類結果來計算損失。

      交叉熵

      哈佛代碼中使用交叉熵損失函數來比較模型的預測的概率分布(logits)和真實分布(targets)之間的差異。然后對損失計算梯度,用反向傳播算法來略微調整所有模型的權重,以便接下來生成更接近結果的輸出。具體代碼如下。

      self.criterion = nn.KLDivLoss(reduction="sum")
      

      我們用下圖來進行分析。假設詞表包含6個單詞,我們希望得到與預期的目標序列 "I love you"相符的概率分布。圖中上方是目標概率分布。第一個輸出詞的概率分布中,“I”的概率應該是1,而詞表中其它詞的概率都應該是0。類似的,第二個和第三個輸出詞的概率分布中,“love”和"you"的概率都應該是1,詞表中其它詞的概率都應該是0。圖下方則是模型對應預測輸出的概率分布。損失函數就是要計算兩者之間的差異。

      計算損失函數的代碼如下,傳入的參數criterion是損失函數。該類除了包含損失計算外,還包含模型generator部分的前向傳播邏輯。下面代碼有個正則化的細節,這是為了平滑。假設有兩個batch,第一個batch有6個字,則loss是這6個預測結果計算損失的和。第二個batch有60個字,則loss是這60個預測結果計算損失的和。顯然第二個損失大,這不符合邏輯。所以我們用除以有效token數目來進行平均。

      class SimpleLossCompute:
          "A simple loss compute and train function."
      
          def __init__(self, generator, criterion):
              
              self.generator = generator # Generator類對象,依據解碼器的輸出預測下一個token
              self.criterion = criterion # LabelSmoothing類對象,對標簽進行平滑和計算損失
      
          def __call__(self, x, y, norm):
              """
              x: 解碼器的輸出
              y: batch.tgt_y,要被預測的所有token,例如src為`<bos>我吃了一個蘋果<eos>`,
                 則tgt_y是"I ate an apple<eos>"
              norm: batch.ntokens, tgt_y中的有效token數  
              """
              x = self.generator(x) # 生成預測輸出
              # 首先使用KLDivLoss進行了損失計算,隨后又除以batch.ntokens對損失進行正則化。
              sloss = (
                  self.criterion(
                      x.contiguous().view(-1, x.size(-1)), y.contiguous().view(-1)
                  )
                  / norm # 對損失進行正則化
              )
              return sloss.data * norm, sloss
      

      Label Smoothing

      Transformer論文中也使用了Label Smoothing(Label Smoothing Regularization)作為正則化技術來防止過擬合。這么做的原因是因為:現實生活中得到的訓練數據是存在噪聲的,訓練得到的模型也趨向于出現多樣性的數據,所以需要在真值中添加噪聲,對模型進行約束。下面是論文中的摘錄。

      Label Smoothing During training, we employed label smoothing of value ?ls = 0.1 [36]. This hurts perplexity, as the model learns to be more unsure, but improves accuracy and BLEU score.

      Label Smoothing主要針對的是softmax層,其思路是:在真值(gound-truth)中加入噪聲,即不要把真值完全標記成非0即1,而是用一種概率的方式標記,或者說是對標簽做平滑處理,把最高值去掉一些,去掉的這些概率均分給其它人。調整之后,雖然所有類別的概率和仍然是歸一的,但是這樣可以讓模型不那么自信,從而減少過擬合。

      Label Smoothing起到的作用實際上是抑制了feature norm,損失函數值曲面上不再存在平緩區域,處處都有較大的梯度指向各個類中心,所以特征會更加聚攏。Label Smoothing的原理如下圖所示。

      我們用實例來進行演示。比如我們的標簽是2,詞典大小為6。原先的真值向量是:[0,0,1,0,0,0],我們現在取平滑因子? = 0.2,則平滑之后的標簽是:[0.2/5, 0.2/5, 1-0.2, 0.2/5, 0.2/5, 0.2/5] = [0.04, 0.04, 0.8, 0.04, 0.04, 0.04]。這樣可以即使模型預測對了,也不要太自信,而是給模型一點懲罰,防止其過度相信預測結果。

      Label Smoothing的代碼如下。該類除了負責平滑標簽外,還負責計算損失。另外,因為詞典包括填充符,而預測時候不應該預測到這個詞,所以所以公式中的 K-1也在算法中要變成 K ? 2。常見的做法是把 Pytorch CrossEntropy 中 ignore_index設置為 idx,比如loss_fn = torch.nn.CrossEntropyLoss(ignore_index=PAD_IDX)。

      class LabelSmoothing(nn.Module):
          "Implement label smoothing."
          # 該類除了平滑標簽外,還會計算損失
      
          def __init__(self, size, padding_idx, smoothing=0.0):
              """
              size: 目標語言詞典大小。
              padding_idx: <pad>在詞典中對應的序號
              smoothing: 平滑因子,0表示不做平滑處理
              """
              super(LabelSmoothing, self).__init__()
              self.criterion = nn.KLDivLoss(reduction="sum") # 最終使用的損失函數
              self.padding_idx = padding_idx
              self.confidence = 1.0 - smoothing
              self.smoothing = smoothing
              self.size = size
              self.true_dist = None # 平滑后的標簽
      
          def forward(self, x, target):
              """
              x: generator輸出的概率分布。形狀為(batch_size, voc_size)
              target: 目標真值標簽,內容是token index。形狀為(batch_size)
              """        
              # 確保generator的輸出維度和詞典大小一致,否則后面計算loss的時候就會出錯
              assert x.size(1) == self.size
              # 創建一個與x有相同形狀的張量
              true_dist = x.data.clone()
              # 將true_dist全部填充為 self.smoothing / (self.size - 2)
              """
              假設 smoothing=0.2,詞表大小為6,batch size為2
              則true_dist全部填充為 0.2 / (6-2)= 0.05,此時true_dist為:
              [[0.05, 0.05, 0.05, 0.05, 0.05, 0.05],
               [0.05, 0.05, 0.05, 0.05, 0.05, 0.05]]
              """
              true_dist.fill_(self.smoothing / (self.size - 2)) # K - 2 = 6 - 2
              """
              target.data.unsqueeze(1)會給target.data增加一維,假設target.data是[2,3],則target.data.unsqueeze(1)的結果是[[2],[3]]
              將true_dist的第一個1維度上與target.data.unsqueeze(1)對應的值變為self.confidence。
              假設此例中target.data.unsqueeze(1) 為[[2], [3]],即2個數據的標簽分別為2,3,就是把true_dist上設置為self.confidence,則true_dist執行過scatter后變為:
              [[0.05, 0.05, 0.8, 0.05, 0.05, 0.05],
               [0.05, 0.05, 0.05, 0.8, 0.05, 0.05]]
              """         
              true_dist.scatter_(1, target.data.unsqueeze(1), self.confidence) # 1代表作用到第一個維度上
              # 將<pad>所在的index填充為0
              true_dist[:, self.padding_idx] = 0
              # 找出target中為<pad>的標簽。例如target為['i', 'love', 'you', '<pad>', '<pad>'],mask則為[[3], [4]],表示第3個和第4個為空格。       
              mask = torch.nonzero(target.data == self.padding_idx)
              if mask.dim() > 0:
                  # 將"<pad>"所在的label設置為0
                  true_dist.index_fill_(0, mask.squeeze(), 0.0)
              # 保存平滑標簽后的label    
              self.true_dist = true_dist
              """
              使用平滑后的標簽計算損失
              由于對`<pad>`部分進行了mask,所以這部分不會參與損失計算
              """        
              return self.criterion(x, true_dist.clone().detach())
      

      下圖給出了上面代碼中的部分數據流程示例。

      因為訓練是并行執行,難以展示,因此下圖進行了簡化,只展示前三步中單個輸出的損失計算。

      具體調用損失函數的精簡代碼如下,因為前面提到了LabelSmoothing類分裝了損失函數,所以這里的criterion就是LabelSmoothing類的實例。

      criterion = LabelSmoothing(size=V, padding_idx=0, smoothing=0.0) 
      model = make_model(V, V, N=2)
      batch_size = 80
      for epoch in range(20):
          model.train()
          run_epoch(
              data_gen(V, batch_size, 20),
              model,
              SimpleLossCompute(model.generator, criterion),
              optimizer,
              lr_scheduler,
              mode="train",
          )
      
      # run_epoch()函數中會調用損失函數
      def run_epoch(
          data_iter,
          model,
          loss_compute,
          optimizer,
          scheduler,
          mode="train",
          accum_iter=1,
          train_state=TrainState(),
      ):
          for i, batch in enumerate(data_iter):
              out = model.forward(
                  batch.src, batch.tgt, batch.src_mask, batch.tgt_mask
              )
              # 計算損失
              loss, loss_node = loss_compute(out, batch.tgt_y, batch.ntokens)   
      
      

      下面是另一個平滑的例子,從該例子可以看到當模型非常自信的時候就會給予其一個微小的懲罰,越自信,損失反而越大。

      def loss(x, crit):
          # x是從0到100的一個不斷增大的數。 d=x+3,比x大一點。
          d = x + 3
          """
          模擬模型的輸出。
          一開始x為1,輸出為:[[0.0000, 0.2500, 0.2500, 0.2500, 0.2500]],此時模型還不太會預測
          當x到100時,輸出為:[[0.0000, 0.9706, 0.0098, 0.0098, 0.0098]],此時模型很自信的說結果就是 1
          """
          predict = torch.FloatTensor([[0, x / d, 1 / d, 1 / d, 1 / d]])
          # 計算模型損失。由于使用的是KLDivLoss,所以要對predict進行log操作
          return crit(predict.log(), torch.LongTensor([1])).data
      
      def penalization_visualization():
          crit = LabelSmoothing(5, 0, 0.1)
          loss_data = pd.DataFrame(
              {
                  # x從1開始不斷增大,模擬模型的表現越來越好
                  "Loss": [loss(x, crit) for x in range(1, 100)],
                  "Steps": list(range(99)),
              }
          ).astype("float")
      
          return (
              alt.Chart(loss_data)
              .mark_line()
              .properties(width=350)
              .encode(
                  x="Steps",
                  y="Loss",
              )
              .interactive()
          )
      
      show_example(penalization_visualization)
      
      

      1.4 學習率

      學習率決定了模型參數更新的步長。如果學習率設置得過高,那么模型參數在更新時可能會因為步長過大而跳出最優解的范圍。同時,過高的學習率會使模型在更新參數時過于激進,從而加劇梯度的波動,導致梯度爆炸。如果學習率過低,模型收斂速度可能會變慢,訓練時間變長。因此,學習率的選擇需要根據具體任務和模型結構進行調整。在實際應用中,可以使用自適應學習率算法來根據參數梯度的統計信息來調整學習率。例如,Adam、Adagrad、RMSprop等優化算法都可以根據梯度的歷史信息來動態調整學習率,從而提高訓練的穩定性和效率。

      Warmup

      Warmup(熱身)方案也屬于動態調整學習率的一種。具體是指在訓練開始階段,將學習率從 0 緩增到指定大小,而不是一開始就從一個指定大小開始訓練。如果不進行Warmup,則模型可能在訓練開始就快速學習。因為梯度消失的原因,模型對越靠后的層越敏感,也就是越靠后的層學習得越快。然而,靠后的層是以考前層的輸出為基礎進行學習,如果前面層沒有學習好,靠后層的學習就會建立在錯誤基礎上,最終導致模型崩盤。

      Noam

      Transformer論文使用了一種特殊的自適應學習率調整策略,稱為“Noam”學習率預熱策略。它包括warmup(熱身)和decay(衰減)兩個部分,總體趨勢是學習率先增加再減少。“Noam”學習率預熱策略具體如下圖所示,是一個以warmup_steps為分界點的分段函數。其中\(d_{model}\)是模型維度,step_num是當前訓練步數,armup_steps是預熱部署。

      • warmup階段:從0到warmup_steps是熱啟動階段,此時先讓學習率線性增長到某個最大的值。大型網絡在訓練初期尚不穩定,較大學習率會增加收斂難度。warmup階段用較小的學習率可以有助于模型在訓練初期快速收斂。而且大型網絡往往使用超大的批量大小(batch size),為了實現超大批量大小,需要保證“k 個 minibatch , size = n , lr = η” 和 “1 個 minibatch , size = kn , lr = kη”的梯度近似相等。但是在模型變化劇烈時,這個等式會被打破。warmup 可以有效緩解這個問題。
      • decay階段:冷卻階段,此時讓學習率按指數的方式衰減。這樣可以在訓練后期通過減小學習率來讓模型穩定訓練。常用方法有指數衰減(exponential)、分段常數衰減(piecewise-constant)、反時限衰減(inverse-time)等等。Transformer 采用了負冪形式,衰減速度先快后慢。

      Noam機制主要是受人類的學習機制啟發:每當我們學習一個新的的領域的時候,剛開始需要摸索入門,不斷嘗試,此時訓練速度很慢;隨著吸收基礎知識增多,我們學習速度會漸漸加快;當掌握了大量的比較雜的知識之后,我們一般會遇到一個瓶頸期,需要知識整合和感悟,速度又會變慢下來。總結一下,人類學習能力是一個螺旋式的漸進過程,是慢與快的交叉過程。Noam機制就是這個進程的具體體現。

      下圖給出了具體推導過程。

      哈佛代碼中rate()函數就是對下面公式的實現。

      \[lrate = d_{\text{model}}^{-0.5} \cdot \min({step\_num}^{-0.5}, {step\_num} \cdot {warmup\_steps}^{-1.5}) \]

      具體代碼如下。

      def rate(step, model_size, factor, warmup):
          """
          we have to default the step to 1 for LambdaLR function
          to avoid zero raising to negative power.
          """
          if step == 0: # 如果未提供步數,則設為1
              step = 1
          return factor * (
              model_size ** (-0.5) * min(step ** (-0.5), step * warmup ** (-1.5))
          )
      
      
      

      具體使用方式如下。

      optimizer = torch.optim.Adam(
          model.parameters(), lr=0.5, betas=(0.9, 0.98), eps=1e-9
      )
      lr_scheduler = LambdaLR(
          optimizer=optimizer,
          lr_lambda=lambda step: rate(
              step, model_size=model.src_embed[0].d_model, factor=1.0, warmup=400
          ),
      )
      
      

      在實際應用中,也可以用不同的學習率調整每一層,或者把若干層分為一組,對于不同組應用不同的學習率。這是因為 Transformer 模型中不同層通常捕獲不同類型的信息,底層通常編碼通用和基礎的信息,頂層通常編碼更接近預訓練任務的信息,因此可以對頂層應用較高學習率而對底層應用較低學習率。

      1.5 初始化

      權重初始化是神經網絡訓練的重要步驟之一。如果權重初始化過大,那么在反向傳播過程中,梯度的計算會受到很大的影響,容易導致梯度爆炸。例如,如果權重由標準正態分布初始化,其期望數量級為1,那么在多層傳播后,梯度值可能會變得非常大。

      使用合適的權重初始化策略可以有效控制梯度的大小,減少梯度爆炸的可能性。常見的權重初始化方法包括Xavier初始化(也稱為Glorot初始化)和He初始化。這些方法根據網絡的層數和激活函數的特點來設置權重的初始值,使得在反向傳播過程中梯度的變化更加平穩。

      例如,Xavier初始化方法根據輸入和輸出神經元的數量來調整權重的初始值,使得前向傳播和反向傳播中的激活值和梯度值保持相近的方差。He初始化方法則特別適用于ReLU激活函數,因為它考慮了ReLU激活函數在零點的不連續性,從而更加準確地設置了權重的初始值。

      vanilla Transformer使用的就是Xavier初始化。

      1.6 Teacher Forcing

      本質上來講,Teacher Forcing 是一種引導和加速模型學習過程的方法,在訓練的每一步都為其提供正確的輸作為指導,而不是讓訓練根據之前的輸出來生成下一步。

      問題

      前文提到過,自回歸推理有兩個問題:

      • 容易累積錯誤,導致訓練效果不佳。在訓練時,我們可以使用與推理時相同的方法,即用自回歸模式進行。然而這樣整個模式就是串行化過程,如果編碼器在某一輪預測錯了,那么這個錯誤的輸出就會作為下一輪解碼器的輸入,這樣基于錯誤輸入繼續解碼就是在錯誤道路上越走越遠,這將導致模型向全局最優收斂的速度減慢。
      • 只能以串行方式進行,這就意味著很難以并行化的方式開展訓練以提升效率。這種現象和人說話的邏輯是相似的,人也許可以在腦中構思整個句子,但是表述一定是一個詞一個詞說出來的,而且后面的詞一定會被前面的詞所影響,這就是說話的邏輯。

      我們用下面表格來看看上面兩個問題。

      • 首先,對于所提供的輸入,模型必須經過5個時間步才能完成推理,因為Decoder每一次只會預測一個單詞。但是,按照上述流程進行訓練會過于緩慢,我們應采用并行(矩陣計算)的方式去訓練。

      • 其次,推理步驟中會出現錯誤,而且容易在錯誤道路上越走越遠。

      時間步 解碼器輸入1 解碼器輸入2 解碼器輸出 真值 說明
      1 "我吃了一個蘋果"編碼后的隱向量 I I 預測正確
      2 I "我吃了一個蘋果"編碼后的隱向量 like ate 預測錯誤
      3 I like "我吃了一個蘋果"編碼后的隱向量 play an 預測錯誤
      4 I like play "我吃了一個蘋果"編碼后的隱向量 football apple 預測錯誤
      5 I like play football "我吃了一個蘋果"編碼后的隱向量 預測正確,但是沒啥用處

      概念

      為了提升訓練效率,我們需要用并行手段來保證在一次訓練中輸出一個序列中所有的單詞的預測結果。為了實現這種理想訓練方式,研究人員提出了Teacher Forcing(教師強制訓練),這種技術可以通過在訓練時向解碼器輸入整個目標序列來一次性并行解碼全部輸出。

      具體來說,Teacher Forcing就是每次推理給解碼器輸入時,不使用前次推理的輸出作為下一次推理的增加輸入,而是使用訓練標簽的真值(ground truth)作為下一次推理的增加輸入。Teacher Forcing機制保證了 Transformer 在訓練階段可以并行地輸出所有的詞,而不需要循環,這大大加快了訓練速度。這種模式具體如下圖所示,圖中簡化了輸入,實際上解碼器的輸入是一個拼接,而非單純輸入某個標簽。

      向解碼器提供目標序列實際上是給了模型一個正確指導,即使上一個詞預測錯誤,在下一時間步,它也可以用正確的第一個詞(即真值)來預測第二個詞,這就避免了錯誤的持續累加,可以保證對每次推理的監督訓練都是從正確的輸入出發,因而可以期待正確的結果。其名稱中的“Teacher”指的就是真值,自回歸模式是“靠自己”進行訓練,Teacher Forcing就是有老師帶著做訓練,即使我們計算出錯誤的答案,老師也會為我們提供問題的正確答案。我們可以知道是在哪個階段出現問題,從而很容易地分析自己的錯誤,更好更快地學習,即“靠標準答案”指導來進行訓練。

      注:與 Teacher forcing 模式相對的是 free-running 模型。free-running是直接用上一個狀態的輸出,來作為下一個狀態的輸入。

      示例

      我們假設要把“我吃了一個蘋果”翻譯成“I ate an apple”。“我吃了一個蘋果”是編碼器的輸入,“I ate an apple”是真值標簽。我們看看模型是如何利用Teacher Forcing模型在訓練中糾正錯誤,防止錯誤的累積,從而提高訓練效果。

      首先,真值標簽是目標序列,會作為解碼器的輸入。為了做更好的訓練,我們要把輸入的所有token向右移一個位置(Shifted Right),然后在最左邊放上一個表示開始的token()。與之對照,自回歸模型本時刻的輸入是上一時刻自己輸出的值,該值是上一時刻預測出來的,不一定正確;而Teacher Forcing本時刻的輸入是上一時刻的真值標簽,這肯定是正確的,可以確保解碼器本次預測是基于正確基礎上進行。

      其次,我們再看看解碼過程中的歷次推理。可以看到,如果第二步預測之后,模型接受了“like”,會導致模型在后續訓練中偏離正軌,導致學習速度變慢,模型也變得不穩定。在Teacher Forcing模式中,因為發現了錯誤,模型會丟棄這個輸出,把“ate“作為下一次的輸入。或者說,在訓練時,不管解碼器本次輸出是什么,它下次的輸入都是本次輸出對應的真值。這樣模型將更正訓練過程中的統計屬性,增加了后續單詞成功預測的幾率,從而更快地學會生成正確的序列。

      時間步 解碼器輸入1 解碼器輸入2 解碼器輸出 真值 說明
      1 "我吃了一個蘋果"編碼后的隱向量 I I 預測正確
      2 I "我吃了一個蘋果"編碼后的隱向量 like ate 預測錯誤,用真值糾正
      3 I ate "我吃了一個蘋果"編碼后的隱向量 an an 預測正確
      4 I ate an "我吃了一個蘋果"編碼后的隱向量 orange apple 預測錯誤,用真值糾正
      5 I ate an apple "我吃了一個蘋果"編碼后的隱向量 預測正確

      具體對應下圖所示。

      圖片思路來源 :解剖Transformer 第二部分:你會用注意力機制組裝出一個Transformer嗎? 大方

      原理

      實質上,Teacher Foring是在訓練過程中去掉了每次推理的序貫關系,使得原先自回歸推理先后依賴被解除,解碼器的輸入就是真值標簽,因此具備了并行推理的可能。我們可以將整個句子”I ate an apple“復制5次構成一個矩陣,使得矩陣每一行代表一個時間步的輸入,然后把矩陣作為一個批量一次性輸入給解碼器,這樣就可以利用GPU的并行能力,一次并行做5次推理來得到所有時間步的結果。然后對每個對輸出序列的每個元素都計算損失即可,這就是Transformer訓練時可以并行計算的原因。

      在執行的過程中,我們在初始輸出中添加了起始符<bos>,相當于將輸出整體右移一位(Shifted Right)。

      對應到具體數據構建,訓練代碼會先把目標句子擴展為" I ate an apple",然后向右移動一位構建標簽"I ate an apple"。再構建一個批量如下:

      <bos>I ate an apple
      <bos>I ate an apple
      <bos>I ate an apple
      <bos>I ate an apple
      <bos>I ate an apple
      
      

      最后把這個批量傳給解碼器。

      掩碼

      雖然上述的并行可以一次性計算所有時間步對應的輸出,但是卻存在一個問題,即注意力在預測某個詞時可以提前關注到其后面的單詞,從而模型學會作弊。比如上圖的每個推理步的輸入都是”I ate an apple“這整個句子。所以在預測第一個輸出”I“時,模型實際上可以關注到目標序列中”“后面的單詞,模型直接輸出”I“就可以滿足需求。這樣的偷窺行為會讓模型學會偷懶而不是學習規律。也就是說,僅僅使用Shifted Right,并不能實現teacher forcing,這是因為如果注意力模塊是沒有mask的self-attention,就會造成數據泄漏的問題。

      因此人們引入了掩碼機制來隱藏未來信息。具體做法時在計算注意力時加入一個掩碼(mask)該掩碼是一個跟輸入矩陣一樣形狀的矩陣,其作用就是遮掉輸入矩陣的一部分,讓模型只能看到目標序列的一部分(前綴):在輸出第i個元素的時候,不能看目標序列的第i個元素及其后面的部分,只能用到第i個元素之前的信息,從而切斷它從未來獲得信息的通路,不能泄露天機(把對應的注意力強制置零),這樣才能在訓練時候模擬實際推理的效果。或者說,通過掩碼可以單獨調節每一個源元素與每一個目標元素之間的注意力強度。

      在訓練時,假如Decoder當前的輸入為" I ate an apple",對于單詞an 來講,只需要讓其關注自身及I 和ate 即可,后面的apple作為我們將要預測的單詞,此時還未出現,因此不用去關注。下圖是四個單詞各自應該關注的情況。

      下圖是加入了掩碼之后的Teacher Foring示例。

      實現

      Teacher Forcing的實現相對簡單,就是傳入了目標序列,用真實目標序列和掩碼作為輸入來指導解碼器的生成過程。

      for i, batch in enumerate(data_iter):
          out = model.forward(
              batch.src, batch.tgt, batch.src_mask, batch.tgt_mask
          )
          loss, loss_node = loss_compute(out, batch.tgt_y, batch.ntokens)
      
      

      而損失函數是把所有的out放在一起,然后看損失。

      class SimpleLossCompute:
          def __call__(self, x, y, norm):
              x = self.generator(x)
              sloss = (
                  self.criterion(
                      x.contiguous().view(-1, x.size(-1)), y.contiguous().view(-1)
                  )
                  / norm
              )
              return sloss.data * norm, sloss
      
      

      優劣

      Teacher Forcing的優勢是因為模型是在“正確答案”指引下進行預測,訓練的穩定性得到大大增強,收斂速度也得以大幅提升。而且我們可以一次性的輸入全部目標序列,然后以并行的方式一次性的輸出完整的目標序列,訓練效率大幅提升。

      但是Teacher Forcing也存在一定的問題。因為訓練可以“靠老師”,推理還得“靠自己”,這樣推理時遇到的錯誤輸出對于下次推理來說就是在訓練數據分布之外(out of distribution)的異常輸入,所以會導致用Teacher Forcing模式訓練出來的模型在訓練環節和預測環節存在行為差異。這種因為訓練和推理之間數據分布存在差異,導致模型在部署中表現變差的現象叫做exposure bias(曝光誤差)。另外,因為模型生成的結果都必須和參考句一一對應。這種約束在訓練過程中減少模型發散,加快收斂速度。但是一方面也扼殺了翻譯多樣性的可能。

      因此研究人員也針對exposure bias做了一些改進工作。比如其中一個變種是Curriculum Learning,它的思路是:既然自回歸模式的全靠自身預測結果和Teacher Forcing模式的全靠真值均不可取,那么就不如折中方案,進行有計劃的學習。在訓練過程的每一步會以一定的概率隨機選擇是用模型輸出還是用真值。上述選擇概率是隨著訓練的推進不斷調整的:訓練過程會從Teacher Forcing開始,慢慢降低在訓練階段輸入真值的頻率。即一開始學生是小白,只能老師帶著學,后續隨著學生的進步,老師慢慢放手讓學生自主學。

      小結

      訓練流程是之所以是一步操作,這是因為ground-truth已知,而結合Teacher Forcing和掩碼,能夠讓第i個時間步的上下文向量只是由前i個時間步的向量計算而來,這樣就保證了第i個單詞的預測只使用了前i個時間步的信息,即提高了計算效率,又符合語言模型的內在規律。

      1.7 并行

      我們接下來看看訓練時候的并行機制。總的來說,Transformer的并行化主要體現在訓練階段,特別是在自注意力和FFN中。

      在推理階段,因為ground-truth未知,我們只知道前i-1個時間步預測的單詞,顯然只能使用自回歸迭代操作來預測所有的單詞,所以需要多步來預測序列中所有的單詞,難以并行。盡管Decoder端在推理階段的并行化存在挑戰,但通過一些先進的技術和模型變種,這個問題也可以得到一定緩解。

      邏輯維度

      我們首先從seq2seq模型維度來看看。 encoder-decoder 架構是自回歸的:通過上一步產生的token和這一步的輸入來預測這一步的輸出。我們看看Transformer對此做了哪些改進從而完成并行。

      編碼器

      編碼器天然支持并行。整個架構從原來的序列模型變成了一個全連接圖模型,每個詞之間是可以直接關聯的,這就很方便的進行矩陣計算,從而享受并行計算或者 GPU 加速帶來的運行效率的提升。

      下圖是使用“北國的特產”為例來看Transformer計算時候的信息流。注意力機制的感知域是整個句子,Transformer在計算任意一個詞的特征時,會用到所有詞的信息,即”北“的特征L(北)是由所有詞共同計算得到的。L(北)是所有單詞的加權和,這樣就沒有距離的概念,不會有長依賴的問題,即序列中任意兩個單詞之間距離都是一個固定的常數。另外,輸入序列中每個位置的單詞都各自單獨的路徑流入編碼器,所有單詞可以同時流入編碼器中,不需要排隊進入,這樣就可以進行并行處理。

      解碼器

      在自回歸模式下,解碼器需要兩種隱向量:

      • 編碼器生成的編碼隱向量。
      • 解碼器在解碼過程中產生的隱向量,即上一狀態的輸出。

      對于第一種隱向量,編碼器通過并行操作可以一次性計算出來,傳遞給交叉注意力。對于第二種隱向量,在Teacher Forcing模型下,每次推理的序貫關系被打破,原先自回歸推理先后依賴被解除,不再需要解碼過程中的隱向量。所以配合掩碼,我們可以把全量輸入和真值標簽一次性直接投入到解碼器中來完成并行訓練。

      模型維度

      模塊即指編碼器,也指解碼器。因為在訓練中,編碼器解碼器都可以并行。從模型來看,以下維度可以并行。

      • Q、K、V生成可以并行化。使用\(W^Q\)\(W^K\)\(W^V\)這三個權重矩陣的計算過程可以并行化。
      • 自注意力機制的并行化。在自注意力層中,模型計算輸入序列中所有位置的單詞之間的注意力分數,并且這些計算是相互獨立的。因此,它們可以在不同的處理單元上并行執行。
      • 多頭注意力機制的并行化。多頭注意力機制中,不同的注意力頭可以在不同的處理單元上并行計算。
      • FFN的并行化。FFN對輸入序列的每個位置執行相同的操作,并且這些操作是獨立的。因此,它們也可以在不同的處理單元上并行執行。
      自注意力

      編碼器可以并行的關鍵是在自注意力機制中,計算\(Z_i\)要依賴全部元素\(x_1,...,x_n\),而非依賴\(Z_{i-1}\)。參見下圖,以”吃了“這個token為例,自注意力機制利用輸入元素兩兩之間的相關性作為權重,然后加權求和把每一個輸入元素\(x_i\)映射到語義向量\(z_i\)\(z_i\)是考慮了全局依賴之后的產物,不需要嚴格時序依次迭代,能夠并行,因此我們可以用矩陣運算一下子把所有的\(z_i\)計算出來。

      FFN

      輸入序列中每個位置的單詞都按照各自單獨的路徑流入編碼器,即各個單詞同時流入編碼器中,不是排隊進入。
      在自注意力self-attention層中,這些路徑兩兩之間是相互依賴的,而FFN則沒有這些依賴性,所以這些路徑在流經FFN時可以并行計算。

      對于輸入序列中的每個位置 x 會使用相同的變換矩陣來計算,且每個子層使用的不用的參數。在計算完Multi-Head Attention后,FFN層的輸入矩陣為 \(X∈R^{d_{input} \times d_{model}}\),可以看作是由每個輸入位置( \(d_{input}\) 行)的attention結果( \(d_{model}\) 列)堆疊而成。這些行進行相同的線性變換后,維度改變,重新堆疊成FFN層的輸出。行與行之間無交錯,完全是“separately and identically”,按位置進行變換。論文的3.3小節Position-wise Feed-Forward Networks中,對“Position-wise”做了注解,如下圖所示。

      張量維度

      輸入到 Transformer 的 Tokens 有 batch_sizesequence_lengthembedding_dim 三個維度,而 Attention 計算的 multi head 機制把 embedding_dim 維度再拆分為 head_numhead_dim,因此從計算量角度來看一共有五個維度:

      • batch_size
      • sequence_length
      • token
      • head_num
      • head_dim

      其中 batch_size、head_num和token這三個維度本身就支持并行。而人們最近也在序列維度上進行了并行嘗試,即序列并行。序列并行首先由論文"Sequence Parallelism: Long Sequence Training from System Perspective"提出,目的是要解決序列長度過長導致內存使用量過大的問題,我們知道LLM推理主要有兩個階段:prefill和decode。前者瓶頸在于計算,而后者在于帶寬。在prefill中已經有將sequence length拆開計算再匯總的做法,序列并行則是將這個過程并行完成,具體是把輸入序列切分為多個塊,每個塊放到不同GPU上進行計算,以減少長序列輸入對顯存大小的需求。為了合并計算結果,論文也提出了環自注意力(RSA)機制。

      另外,也有一種說法叫做上下文并行 Context Parallelism,其最先出現在NVIDIA Megatron-Core中,上下文并行主要是針對self-attention(Linear,LayerNorm)進行優化,它將原本的輸入按照sequence length維度拆開,分到不同的device上,分別計算,然后通過all-gather和reduce-scatter通信操作來整合其他device上算出的結果。

      1.7 代碼

      訓練方式

      train_model()函數會依據配置選擇是進行分布式訓練還是單機訓練。

      def train_distributed_model(vocab_src, vocab_tgt, spacy_de, spacy_en, config):
          from the_annotated_transformer import train_worker
      
          ngpus = torch.cuda.device_count()
          os.environ["MASTER_ADDR"] = "localhost"
          os.environ["MASTER_PORT"] = "12356"
          print(f"Number of GPUs detected: {ngpus}")
          print("Spawning training processes ...")
          mp.spawn(
              train_worker,
              nprocs=ngpus,
              args=(ngpus, vocab_src, vocab_tgt, spacy_de, spacy_en, config, True),
          )
      
      def train_model(vocab_src, vocab_tgt, spacy_de, spacy_en, config):
          if config["distributed"]:
              train_distributed_model( # 分布式訓練
                  vocab_src, vocab_tgt, spacy_de, spacy_en, config
              )
          else:
              train_worker( # 使用0號GPU進行單機訓練
                  0, 1, vocab_src, vocab_tgt, spacy_de, spacy_en, config, False
              )
      
      
      

      單機訓練代碼

      單機訓練代碼如下,它遍歷一個 epoch 的數據,然后調用 forward()函數,接著用 loss_compute() 函數計算梯度,更新參數并且返回 loss。這里的 loss_compute() 函數的輸入是:模型的預測 out,真實的標簽序列 batch.trg_y 和 batch 中詞的個數。

      def train_worker(
          gpu,
          ngpus_per_node,
          vocab_src, # 源語言詞典
          vocab_tgt, # 目標語言詞典
          spacy_de, # 源語言分詞器
          spacy_en, # 目標語言分詞器
          config,
          is_distributed=False,
      ):
          print(f"Train worker process using GPU: {gpu} for training", flush=True)
          torch.cuda.set_device(gpu)
       
          pad_idx = vocab_tgt["<pad>"] # 得到目標語言詞典中"<pad>"所對應的索引
          d_model = 512 # 詞嵌入大小
          model = make_model(len(vocab_src), len(vocab_tgt), N=6) # 構建一個6層模型
          model.cuda(gpu)
          module = model
          is_main_process = True
          if is_distributed:
              dist.init_process_group(
                  "nccl", init_method="env://", rank=gpu, world_size=ngpus_per_node
              )
              model = DDP(model, device_ids=[gpu])
              module = model.module
              is_main_process = gpu == 0
      
          # 構建損失函數
          criterion = LabelSmoothing(
              size=len(vocab_tgt), padding_idx=pad_idx, smoothing=0.1
          )
          criterion.cuda(gpu)
      
          # 構建數據加載器
          train_dataloader, valid_dataloader = create_dataloaders(
              gpu,
              vocab_src,
              vocab_tgt,
              spacy_de,
              spacy_en,
              batch_size=config["batch_size"] // ngpus_per_node,
              max_padding=config["max_padding"],
              is_distributed=is_distributed,
          )
      
          # 構建優化器
          optimizer = torch.optim.Adam(
              model.parameters(), lr=config["base_lr"], betas=(0.9, 0.98), eps=1e-9
          )
          # 構建學習率策略,依據配置來設定warmup參數
          lr_scheduler = LambdaLR(
              optimizer=optimizer,
              lr_lambda=lambda step: rate(
                  step, d_model, factor=1, warmup=config["warmup"]
              ),
          )
          train_state = TrainState()
      
          for epoch in range(config["num_epochs"]):
              if is_distributed:
                  train_dataloader.sampler.set_epoch(epoch)
                  valid_dataloader.sampler.set_epoch(epoch)
      
              model.train()
              print(f"[GPU{gpu}] Epoch {epoch} Training ====", flush=True)
              _, train_state = run_epoch(
                  (Batch(b[0], b[1], pad_idx) for b in train_dataloader),
                  model,
                  SimpleLossCompute(module.generator, criterion),
                  optimizer,
                  lr_scheduler,
                  mode="train+log",
                  accum_iter=config["accum_iter"],
                  train_state=train_state,
              )
      
              GPUtil.showUtilization()
              if is_main_process:
                  file_path = "%s%.2d.pt" % (config["file_prefix"], epoch)
                  torch.save(module.state_dict(), file_path)
              torch.cuda.empty_cache()
      
              print(f"[GPU{gpu}] Epoch {epoch} Validation ====", flush=True)
              model.eval()
              sloss = run_epoch(
                  (Batch(b[0], b[1], pad_idx) for b in valid_dataloader),
                  model,
                  SimpleLossCompute(module.generator, criterion),
                  DummyOptimizer(),
                  DummyScheduler(),
                  mode="eval",
              )
              print(sloss)
              torch.cuda.empty_cache()
      
          if is_main_process:
              file_path = "%sfinal.pt" % config["file_prefix"]
              torch.save(module.state_dict(), file_path)
      
      

      總體代碼

      總體代碼如下所示,里面包含了訓練和使用訓練好的模型進行推理。

      def example_simple_model():
          V = 11
          criterion = LabelSmoothing(size=V, padding_idx=0, smoothing=0.0)
          model = make_model(V, V, N=2)
      
          optimizer = torch.optim.Adam(
              model.parameters(), lr=0.5, betas=(0.9, 0.98), eps=1e-9
          )
          lr_scheduler = LambdaLR(
              optimizer=optimizer,
              lr_lambda=lambda step: rate(
                  step, model_size=model.src_embed[0].d_model, factor=1.0, warmup=400
              ),
          )
      
          batch_size = 80
          for epoch in range(20):
              model.train()
              run_epoch(
                  data_gen(V, batch_size, 20),
                  model,
                  SimpleLossCompute(model.generator, criterion),
                  optimizer,
                  lr_scheduler,
                  mode="train",
              )
              model.eval()
              run_epoch(
                  data_gen(V, batch_size, 5),
                  model,
                  SimpleLossCompute(model.generator, criterion),
                  DummyOptimizer(),
                  DummyScheduler(),
                  mode="eval",
              )[0]
      
          model.eval()
          src = torch.LongTensor([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])
          max_len = src.shape[1]
          src_mask = torch.ones(1, 1, max_len)
          print(greedy_decode(model, src, src_mask, max_len=max_len, start_symbol=0))
      
      

      0x02 推理

      當我們談論大型語言模型(LLM)的推斷過程時,我們指的是使用已經訓練好的模型來對輸入文本進行處理,從而生成相應的輸出。因為訓練和推理的基本流程類似,所以我們在此處主要看兩者的差異點和推理的獨有特性。

      2.1 輸入輸出

      首先,無論是訓練和推理,編碼器模塊的輸入(要被翻譯的句子)和執行過程都相同。

      其次,對于解碼器來說,訓練和預測的執行過程存在不同。

      • 目標輸入不同。雖然都是把新詞加到之前的輸入上,拼接成解碼器的新輸入,但是新詞的來源不同。
        • 推理階段是用上一次的輸出拼接成下一次輸入。即tgt是從開始,然后每次加入上一次的輸出。
        • 訓練階段時時用真值拼接成下一次輸入。即tgt是從開始,然后每次加入下一個真值。而且是一次性把輸入序列全部傳給解碼器。
      • 輸出不同。
        • 推理:每次輸出一個新token。Decoder的并行化僅在訓練階段,在推理階段,因為我們沒有正確的目標語句,t時刻的輸入必然依賴t-1時刻的輸出,這時跟之前的seq2seq就沒什么區別了。
        • 訓練:transformer會一次輸出多個概率分布。在訓練時,得到輸出概率分布后就可以計算loss了,并不需要將概率分布再轉成對應的token。

      2.2 流程

      在推理時,因為在預測場景下不存在答案文本,只能從位置開始預測,因此預測場景的解碼器必定是串行的循環輸出。在后續步驟的預測中,會將前一個時間步的輸出序列和歷史預測單詞一齊送到下一個時間步的解碼器,直到遇到句末標記。 與Seq2Seq模型的區別在于,在每個時間步,我們重新輸入迄今為止生成的整個輸出序列,而不僅僅是最后一個單詞。

      推理過程的邏輯流程如下

      時間步 解碼器輸入1 解碼器輸入2 解碼器輸出
      1 "我吃了一個蘋果"編碼后的隱向量 I
      2 I "我吃了一個蘋果"編碼后的隱向量 ate
      3 I ate "我吃了一個蘋果"編碼后的隱向量 an
      4 I ate an "我吃了一個蘋果"編碼后的隱向量 apple
      5 I ate an apple "我吃了一個蘋果"編碼后的隱向量

      對應的邏輯圖如下。

      訓練過程就是簡單地在上述推理過程的基礎之上加上對每次推理預測的新元素的監督即可,具體見下圖。

      注意:下圖只是為了展示流程,實際上是一并輸入,并行預測。

      最后,我們總結訓練和推理在流程上的區別如下表。

      步驟 訓練 推理
      輸入 源語言序列 + 目標語言序列(真值) 源語言序列 + 目標語言序列(預測的輸出)
      1 編碼器處理 產生整個源語言序列的編碼表示 產生整個源語言序列的編碼表示
      2 解碼器處理輸入 目標序列首先加一個句首標記,被轉換成嵌入后送入解碼器。 在第一個時間步使用僅包含句子開頭標記的空序列,而非目標序列。后續時間步會輸入迄今為止生成的整個輸出序列。序列被轉換成嵌入后送入解碼器。
      3 解碼器解碼 解碼器將目標嵌入與編碼器的編碼表示一起處理,生成目標序列的解碼表示 解碼器將目標嵌入與編碼器的編碼表示一起處理,生成目標序列的解碼表示
      4 解碼器處理輸出 輸出層將目標序列的編碼表轉換為單詞概率和最終輸出序列 輸出層將目標序列的編碼表轉換為單詞概率和最終輸出序列
      5 計算損失 損失函數將此輸出序列與訓練數據中的目標序列進行比較,計算損失
      7 迭代 無迭代,一次性處理完畢 迭代運行2~4,逐步輸出token

      2.3 代碼

      下面是哈佛源碼中的推理測試代碼。

      # ## Inference:
      #
      # > Here we make a forward step to generate a prediction of the
      # model. We try to use our transformer to memorize the input. As you
      # will see the output is randomly generated due to the fact that the
      # model is not trained yet. In the next tutorial we will build the
      # training function and try to train our model to memorize the numbers
      # from 1 to 10.
      def inference_test():
          # 構建,源詞典和目標詞典大小都為11,
          # EncoderLayer和DecoderLayer的數量為2
          test_model = make_model(11, 11, 2)
          test_model.eval()
          # 輸入形狀為(1, 10),即一個句子,該句子10個單詞。
          src = torch.LongTensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]])
          # 定義源序列掩碼,即所有的詞都是有效的,沒有填充詞
          src_mask = torch.ones(1, 1, 10)
      
          # 將輸入送給編碼器,獲取輸出,記作memory
          memory = test_model.encode(src, src_mask)
          # 初始化ys為[[0]],用于保存預測結果,其中0表示'<bos>'
          ys = torch.zeros(1, 1).type_as(src)
      
          # 循環調用解碼器來預測下一個token。例如:假設我們要將“I love you”翻譯成
      	# “我愛你”,則第一次的`ys`為(<bos>),然后輸出為“I”。然后第二次`ys`為(<bos>, I)
      	# 輸出為"love",依次類推,直到decoder輸出“<eos>”或達到句子長度。
          for i in range(9): 
              # 將編碼器的輸出memory和之前解碼器的所有輸出作為參數,讓解碼器來預測下一個token
              out = test_model.decode(
                  # ys就是Decoder之前的所有輸出
                  memory, src_mask, ys, subsequent_mask(ys.size(1)).type_as(src.data)
              )
              # 將Decoder的輸出送給generator進行預測。這里只取最后一個詞的輸出進行預測。
              # 因為傳入tgt的詞數是變化的,第一次是(<bos>),第二次是(<bos>, I)
              # 所以輸出out的維度也是變化的,變化的就是(batch_size, 詞數,詞向量)中詞數這個維度
              prob = test_model.generator(out[:, -1])
              # 取出數值最大的那個token,它的index在詞典中對應的詞就是預測結果
              _, next_word = torch.max(prob, dim=1)
              # 取出預測結果
              next_word = next_word.data[0]
              # 將這一次的預測結果和之前的拼到一塊,作為之后Decoder的輸入
              ys = torch.cat(
                  [ys, torch.empty(1, 1).type_as(src.data).fill_(next_word)], dim=1
              )
      
          print("Example Untrained Model Prediction:", ys)
      
      def run_tests():
          for _ in range(10):
              inference_test()
      
      show_example(run_tests)
      
      

      0xFF 參考

      A Contrastive Framework for Neural Text Generation (Su et al., 2022)
      A Survey on Efficient Inference for Large Language Models
      Attention Is All You Need (Vaswani et al., 2017)
      Breaking the Sequential Dependency of LLM Inference Using Lookahead Decoding (Fu et al. 2023)
      ChatGPT是第一個真正意義的人工通用智能
      Fast Inference from Transformers via Speculative Decoding (Leviathan et al., 2022)
      https://arxiv.org/abs/1801.06146
      https://arxiv.org/abs/1803.05407
      https://arxiv.org/abs/2006.05987
      https://github.com/1311440131/deep_blue_writings/tree/main/2021_9_18_%E5%BE%AE%E8%B0%83Transformer%E7%9A%84%E9%AB%98%E7%BA%A7%E6%8A%80%E6%B3%95
      https://medium.com/@plienhar/llm-inference-series-1-introduction-9c78e56ef49d
      https://medium.com/@plienhar/llm-inference-series-2-the-two-phase-process-behind-llms-responses-1ff1ff021cd5
      https://pytorch.org/blog/pytorch-1.6-now-includes-stochastic-weight-averaging/
      https://pytorch.org/docs/stable/optim.html#stochastic-weight-averaging
      LaViT:這也行,微軟提出直接用上一層的注意力權重生成當前層的注意力權重 | CVPR 2024 VincentLee
      LLM Inference Unveiled: Survey and Roofline Model Insights
      LLM的幾種并行機制 認輸你就真了
      LoRA Dropout as a Sparsity Regularizer for Overfitting Control
      nn.KLDivLoss_咕嚕咕嚕day的博客-CSDN博客_kldivloss pytorch
      On the Effectiveness of Parameter-Efficient Fine-Tuning
      Pytorch:交叉熵損失(CrossEntropyLoss)以及標簽平滑(LabelSmoothing)的實現_我是大黃同學呀的博客-CSDN博客_標簽平滑交叉熵
      The Illustrated Word2vec Jay Alammar
      Towards Efficient Generative Large Language Model Serving: A Survey from Algorithms to Systems
      《Rethinking the Inception Architecture for Computer Vision》
      萬字逐行解析與實現Transformer,并進行德譯英實戰(一)
      萬字逐行解析與實現Transformer,并進行德譯英實戰(三)
      萬字逐行解析與實現Transformer,并進行德譯英實戰(二) iioSnail
      大模型參數微調:Sparsity和Dropout Chongjie
      大模型時代是否還需Dropout,一次關于GLM4-9B-Chat的分析 LeonYi
      實現 pytorch 中 torch.nn.CrossEntropyLoss_Agwave的博客-CSDN博客
      解剖Transformer 第二部分:你會用注意力機制組裝出一個Transformer嗎? 大方
      Yoshua Bengio, Rejean Ducharme, Pascal Vincent, and Christian Jauvin. A neural probabilistic language model. Journal of Machine Learning Research (JMLR), 3:1137–1155, 2003. [PDF]

      posted @ 2025-02-22 09:55  羅西的思考  閱讀(3293)  評論(4)    收藏  舉報
      主站蜘蛛池模板: 亚洲区成人综合一区二区| 在线天堂www在线| 部精品久久久久久久久| 一区二区免费高清观看国产丝瓜| 2019国产精品青青草原| 成人免费ā片在线观看| 99麻豆久久精品一区二区| 欧美白妞大战非洲大炮| 亚洲av男人电影天堂热app| 国产在线精彩自拍视频| 3d无码纯肉动漫在线观看| 久章草在线精品视频免费观看| 中文字幕日韩国产精品| 国产精品永久免费无遮挡| 国产精品视频午夜福利| 国产av不卡一区二区| 夜色福利站WWW国产在线视频| 国产免费一区二区不卡| 精品国产乱码久久久久久婷婷| 国产超高清麻豆精品传媒麻豆精品| 亚洲欭美日韩颜射在线二| 欧美国产成人久久精品| 亚洲日韩久热中文字幕| 于都县| 亚洲AV永久无码嘿嘿嘿嘿| 久久国产一区二区三区| 亚洲国产成人AⅤ片在线观看| 国模在线视频一区二区三区| 免费无码黄十八禁网站| 日韩欧美在线综合网另类 | 国产精品av中文字幕| 日韩人妻无码精品久久| 亚洲av二区三区在线| 国产成人不卡无码免费视频| 色九月亚洲综合网| 日本污视频在线观看| 97久久超碰精品视觉盛宴| 377P欧洲日本亚洲大胆| 九龙县| 4虎四虎永久在线精品免费| 色www视频永久免费|