在深度學習模型訓練中,尤其針對大語言模型(LLM),梯度裁剪是確保訓練穩定性的重要技術。它透過設定閾值限制梯度大小,避免梯度爆炸問題,特別在反向傳播過程中,能有效控制模型引數更新幅度。PyTorch 提供 clip_grad_norm_ 函式實作梯度裁剪,例如設定 max_norm=1.0 限制梯度的 L2 範數不超過 1.0,確保梯度向量長度維持在合理範圍。實際應用中,計算損失函式後,呼叫 .backward() 方法計算梯度,再使用 clip_grad_norm_ 函式裁剪梯度,最後透過最佳化器更新模型引數。梯度裁剪的應用能有效提高訓練穩定性,避免因梯度爆炸導致模型訓練失敗。

317D.3 Gradient clipping

在訓練大語言模型(LLM)時,梯度裁剪(Gradient Clipping)是一種重要的技術,用於增強訓練過程的穩定性。這種方法涉及設定一個閾值,當梯度超過這個閾值時,將其縮放到一個預先確定的最大幅度。這樣可以確保在反向傳播過程中,模型引數的更新保持在一個可控的範圍內。

例如,在 PyTorch 中,可以使用 clip_grad_norm_ 函式來實作梯度裁剪。設定 max_norm=1.0 可以確保梯度的範數(norm)不超過 1.0。這裡,「範數」指的是梯度向量的長度或幅度,在模型的引數空間中,具體指的是 L2 範數,也稱為歐幾裡得範數。

從數學上講,對於一個向量 v,其組成為 v = [v1, v2,…, vn],L2 範數定義為:

  math
    L2範數 = sqrt(v1^2 + v2^2 +... + vn^2)

這個公式計算了向量 v 的長度或幅度。

在實踐中,梯度裁剪可以透過以下步驟實作:

import torch

# 假設 model 是 PyTorch 中定義的模型
# optimizer 是模型的最佳化器

# 設定最大範數
max_norm = 1.0

# 執行梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)

這樣就可以確保模型的梯度在反向傳播過程中保持在一個可控的範圍內,從而提高訓練過程的穩定性。

圖表翻譯:

下圖示範了梯度裁剪的過程。在這個例子中,梯度的範數最初超過了最大允許值 1.0,但是在應用梯度裁剪後,梯度的範數被縮放到了 1.0 以內。

  flowchart TD
    A[計算梯度] --> B[檢查梯度範數]
    B -->|超過最大允許值| C[執行梯度裁剪]
    C --> D[更新模型引數]
    B -->|未超過最大允許值| D

這個流程圖顯示了梯度裁剪在模型訓練過程中的作用,確保梯度保持在可控範圍內,以提高訓練的穩定性和效率。

梯度裁剪與最佳化器調整

在深度學習模型的訓練過程中,梯度裁剪(Gradient Clipping)是一種重要的技術,用於控制梯度爆炸問題,尤其是在使用反向傳播演算法計算梯度時。梯度裁剪的目的是限制梯度的大小,以避免梯度爆炸對模型訓練的負面影響。

梯度裁剪的計算方法

給定一個梯度矩陣 (G),我們可以計算其L2範數(Euclidean範數)來衡量梯度的大小。假設 (|G|_2 = 5),超出了我們設定的最大範數 (max_norm = 1),我們需要將梯度縮放以確保其範數等於1。這可以透過計算縮放因子 (max_norm / |G|_2 = 1 / 5) 來實作。然後,調整後的梯度矩陣 (G’) 就可以透過將原始梯度矩陣乘以這個縮放因子來得到。

實作梯度裁剪

在PyTorch中,實作梯度裁剪可以透過以下步驟:

  1. 計算損失函式:首先,計算模型在給定輸入和目標輸出下的損失函式。
  2. 呼叫反向傳播方法:透過呼叫 .backward() 方法,PyTorch計算損失函式對模型引數的梯度,並將其儲存在每個引數張量的 .grad 屬性中。
  3. 定義梯度裁剪函式:定義一個函式,用於找到模型引數中最大的梯度值。這可以透過遍歷模型的引數,檢查每個引數的梯度是否為 None,然後找到最大梯度值。
  4. 應用梯度裁剪:使用計算出的縮放因子調整梯度,以確保梯度的範數在預設範圍內。

程式碼實作

import torch

def find_highest_gradient(model):
    max_grad = None
    for param in model.parameters():
        if param.grad is not None:
            grad_values = param.grad.data.flatten()
            max_grad_param = grad_values.max()
            if max_grad is None or max_grad_param > max_grad:
                max_grad = max_grad_param
    return max_grad

# 示例程式碼
from chapter05 import calc_loss_batch
torch.manual_seed(123)
model = GPTModel(GPT_CONFIG_124M)
model.to(device)
loss = calc_loss_batch(input_batch, target_batch, model, device)
loss.backward()

# 應用梯度裁剪
max_norm = 1
for param in model.parameters():
    if param.grad is not None:
        grad_norm = param.grad.data.norm(2)
        if grad_norm > max_norm:
            scaling_factor = max_norm / grad_norm
            param.grad.data *= scaling_factor

# 繼續訓練迴圈

梯度裁剪對模型訓練的影響

在深度學習中,梯度裁剪是一種用於穩定模型訓練的技術,特別是在大語言模型(LLM)的訓練中。梯度裁剪的作用是限制模型引數的梯度值在一定範圍內,以避免梯度爆炸問題。下面,我們將探討梯度裁剪對模型訓練的影響,並提供一個修改過的訓練函式,該函式結合了線性預熱、餘弦衰減和梯度裁剪等方法。

梯度裁剪的實作

PyTorch 中提供了 torch.nn.utils.clip_grad_norm_ 函式來實作梯度裁剪。這個函式可以將模型引數的梯度值限制在一定範圍內。例如,以下程式碼將模型引數的梯度值限制在 1.0 範圍內:

torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

修改過的訓練函式

下面是修改過的 train_model 函式,該函式結合了線性預熱、餘弦衰減和梯度裁剪等方法:

def train_model(model, train_loader, val_loader, optimizer, device,
               n_epochs, eval_freq, eval_iter, start_context, tokenizer,
               warmup_steps, initial_lr=3e-05, min_lr=1e-6):
    train_losses, val_losses, track_tokens_seen, track_lrs = [], [], [], []
    tokens_seen, global_step = 0, -1

    peak_lr = optimizer.param_groups[0]["lr"]
    total_training_steps = len(train_loader) * n_epochs
    lr_increment = (peak_lr - initial_lr) / warmup_steps

    for epoch in range(n_epochs):
        model.train()

        for input_batch, target_batch in train_loader:
            optimizer.zero_grad()

            global_step += 1
            # 線性預熱
            if global_step < warmup_steps:
                current_lr = initial_lr + lr_increment * global_step
                optimizer.param_groups[0]["lr"] = current_lr

            # 餘弦衰減
            elif global_step < total_training_steps - warmup_steps:
                current_lr = min_lr + (peak_lr - min_lr) * 0.5 * (1 + math.cos(math.pi * (global_step - warmup_steps) / (total_training_steps - warmup_steps)))
                optimizer.param_groups[0]["lr"] = current_lr

            # 梯度裁剪
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

            # 訓練模型
            output = model(input_batch)
            loss = loss_fn(output, target_batch)
            loss.backward()
            optimizer.step()

            # 儲存訓練結果
            train_losses.append(loss.item())
            tokens_seen += input_batch.size(0)
            track_tokens_seen.append(tokens_seen)
            track_lrs.append(optimizer.param_groups[0]["lr"])

結果分析

經過梯度裁剪後,模型的梯度值明顯減小,這有助於穩定模型的訓練過程。下面是使用梯度裁剪前後的最大梯度值比較:

The largest gradient value identified by 玄貓(0.0411)
The largest gradient value after applying the gradient clipping with the max norm of 1
is substantially smaller than before:
tensor(0.0185)

可以看出,梯度裁剪後的最大梯度值從 0.0411 減小到 0.0185,表明梯度裁剪對模型訓練的穩定性有著顯著的影響。

圖表翻譯:

  flowchart TD
    A[開始] --> B[線性預熱]
    B --> C[餘弦衰減]
    C --> D[梯度裁剪]
    D --> E[訓練模型]
    E --> F[儲存訓練結果]

以上圖表展示了修改過的訓練函式的流程,包括線性預熱、餘弦衰減、梯度裁剪和訓練模型等步驟。

學習率調整策略

在深度學習中,學習率(Learning Rate)是一個至關重要的超引數,它控制著模型在每次迭代中更新引數的步伐大小。一個適當的學習率可以使模型快速收斂到最佳解,而一個不適當的學習率可能導致模型難以收斂甚至發散。為了更好地控制學習率,常常會使用學習率調整策略。

線性warmup階段

在訓練過程的初始階段,模型引數往往需要一個較慢的學習速度來適應新的任務。這時候,線性warmup階段就派上用場了。在這個階段,學習率從一個初始值線性增加到峰值。這個過程可以用以下公式描述:

lr = initial_lr + global_step * lr_increment

其中,initial_lr是初始學習率,global_step是當前的訓練步數,lr_increment是每一步的學習率增量。

複合餘弦退火階段

當warmup階段結束後,模型就進入了複合餘弦退火階段。在這個階段,學習率會根據訓練進度進行調整。這個過程可以用以下公式描述:

progress = ((global_step - warmup_steps) / (total_training_steps - warmup_steps))
lr = min_lr + (peak_lr - min_lr) * 0.5 * (1 + math.cos(math.pi * progress))

其中,warmup_steps是warmup階段的總步數,total_training_steps是整個訓練過程的總步數,min_lr是最小學習率,peak_lr是峰值學習率。

實作細節

在實作這種學習率調整策略時,我們需要注意以下幾點:

  1. 初始學習率:需要根據具體任務和模型選擇合適的初始學習率。
  2. warmup階段:需要根據具體任務和模型選擇合適的warmup階段長度。
  3. 峰值學習率:需要根據具體任務和模型選擇合適的峰值學習率。
  4. 最小學習率:需要根據具體任務和模型選擇合適的最小學習率。
內容解密:

以上內容介紹了學習率調整策略的基本概念和實作細節。透過這種策略,可以實作學習率在訓練過程中的動態調整,從而提高模型的收斂速度和準確度。下面是一個簡單的Python實作:

import math

def adjust_learning_rate(global_step, warmup_steps, total_training_steps, initial_lr, peak_lr, min_lr):
    if global_step < warmup_steps:
        lr = initial_lr + global_step * (peak_lr - initial_lr) / warmup_steps
    else:
        progress = ((global_step - warmup_steps) / (total_training_steps - warmup_steps))
        lr = min_lr + (peak_lr - min_lr) * 0.5 * (1 + math.cos(math.pi * progress))
    return lr

這個函式根據當前的訓練步數和其他超引數計算出當前的學習率。透過調整這些超引數,可以實作不同的學習率調整策略。

最佳化訓練迴圈

在深度學習中,訓練迴圈是模型學習的核心。為了提高模型的效能,我們可以在訓練迴圈中新增一些額外的功能。

動態調整學習率

首先,我們可以根據當前的階段(warmup或cosine annealing)來調整學習率。這可以透過以下程式碼實作:

for param_group in optimizer.param_groups:
    param_group["lr"] = lr
    track_lrs.append(lr)

這段程式碼會根據當前的階段來更新學習率,並將更新的學習率記錄下來。

計算損失

接下來,我們需要計算批次損失。這可以透過以下程式碼實作:

loss = calc_loss_batch(input_batch, target_batch, model, device)

這段程式碼會計算批次損失,並將結果儲存到loss變數中。

反向傳播

然後,我們需要進行反向傳播,以計算梯度。這可以透過以下程式碼實作:

loss.backward()

這段程式碼會計算梯度,並將結果儲存到模型的引數中。

梯度裁剪

如果當前的步驟大於warmup步驟,我們需要進行梯度裁剪,以防止梯度爆炸。這可以透過以下程式碼實作:

if global_step > warmup_steps:
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

這段程式碼會裁剪梯度,以確保梯度的大小不超過最大範圍。

最佳化器更新

最後,我們需要更新最佳化器,以根據梯度更新模型的引數。這可以透過以下程式碼實作:

optimizer.step()

這段程式碼會更新最佳化器,並根據梯度更新模型的引數。

更新tokens_seen

最後,我們需要更新tokens_seen變數,以記錄已經處理的token數量。這可以透過以下程式碼實作:

tokens_seen += input_batch.numel()

這段程式碼會更新tokens_seen變數,並記錄已經處理的token數量。

內容解密:

上述程式碼實作了訓練迴圈中的各個步驟,包括動態調整學習率、計算損失、反向傳播、梯度裁剪、最佳化器更新和更新tokens_seen變數。這些步驟都是模型學習的核心,並且需要仔細調整以獲得最佳的效能。

圖表翻譯:

以下是訓練迴圈的流程圖:

  flowchart TD
    A[開始] --> B[動態調整學習率]
    B --> C[計算損失]
    C --> D[反向傳播]
    D --> E[梯度裁剪]
    E --> F[最佳化器更新]
    F --> G[更新tokens_seen]
    G --> H[結束]

這個流程圖展示了訓練迴圈中的各個步驟,並且可以幫助我們瞭解模型學習的過程。

執行模型訓練

在定義了 train_model 函式後,我們可以使用它來訓練模型。這個過程與使用 train_model_simple 方法進行預訓練類別似。

初始化模型和裝置

首先,我們需要初始化模型和裝置。這包括設定隨機種子、建立模型例項、指定裝置(例如GPU)等步驟。

import tiktoken
import torch

torch.manual_seed(123)  # 設定隨機種子
model = GPTModel(GPT_CONFIG_124M)  # 建立模型例項
model.to(device)  # 將模型移到指定裝置(例如GPU)

執行模型訓練

接下來,我們可以呼叫 train_model 函式來執行模型訓練。這個函式需要提供模型、資料載入器、裝置、評估頻率等引數。

train_losses, val_losses, track_tokens_seen, track_lrs = train_model(
    model, train_loader, val_loader, device, eval_freq
)

監控訓練過程

在訓練過程中,我們可以透過列印損失值和生成樣本來監控模型的表現。

if global_step % eval_freq == 0:
    train_loss, val_loss = evaluate_model(
        model, train_loader, val_loader, device, eval_iter
    )
    train_losses.append(train_loss)
    val_losses.append(val_loss)
    track_tokens_seen.append(tokens_seen)
    print(f"Ep {epoch+1} (Iter {global_step:06d}): "
          f"Train loss {train_loss:.3f}, "
          f"Val loss {val_loss:.3f}")
    generate_and_print_sample(model, tokenizer, device, start_context)

傳回訓練結果

最後,train_model 函式傳回訓練過程中的損失值、tokens見次數和學習率等資訊。

return train_losses, val_losses, track_tokens_seen, track_lrs

圖表翻譯:

  flowchart TD
    A[初始化模型和裝置] --> B[執行模型訓練]
    B --> C[監控訓練過程]
    C --> D[傳回訓練結果]

內容解密:

  • train_model 函式是用於訓練模型的主要函式,它需要提供模型、資料載入器、裝置、評估頻率等引數。
  • 在執行模型訓練的過程中,我們可以透過列印損失值和生成樣本來監控模型的表現。
  • 訓練過程中,模型的損失值和tokens見次數等資訊會被記錄下來,以便之後的分析和最佳化。

訓練模型最佳化器設定

在進行模型訓練之前,我們需要設定最佳化器和學習率。最佳化器是用於更新模型引數的演算法,而學習率則控制了更新的步伐大小。以下是相關設定:

peak_lr = 5e-4  # 最高學習率
optimizer = torch.optim.AdamW(model.parameters(), weight_decay=0.1)  # 使用AdamW最佳化器,weight_decay為0.1

標籤器設定

我們使用tiktoken函式庫來取得GPT-2的編碼方式,以便後續的文書處理。

tokenizer = tiktoken.get_encoding("gpt2")

訓練引數設定

定義訓練的相關引數,包括 epochs 數量、評估頻率等。

n_epochs = 15  # 訓練epoch數

訓練模型

呼叫train_model函式進行模型訓練,傳入必要的引數。

train_losses, val_losses, tokens_seen, lrs = train_model(
    model, train_loader, val_loader, optimizer, device, n_epochs=n_epochs,
    eval_freq=5, eval_iter=1, start_context="Every effort moves you",
    tokenizer=tokenizer, warmup_steps=warmup_steps,
    initial_lr=1e-5, min_lr=1e-5
)

學習率調整和梯度裁剪

在訓練過程中,我們會根據預定的warmup步驟調整學習率,並在warmup階段結束後應用梯度裁剪以避免梯度爆炸。

  flowchart TD
    A[開始訓練] --> B[計算warmup步驟]
    B --> C[調整學習率]
    C --> D[應用梯度裁剪]
    D --> E[繼續訓練]

圖表翻譯:

上述流程圖描述了模型訓練過程中學習率的調整和梯度裁剪的應用。首先,計算warmup步驟的數量,然後根據這個數量調整學習率。在warmup階段結束後,應用梯度裁剪以防止梯度爆炸。這個過程確保了模型的穩定訓練和避免過度更新。

內容解密:

在這段程式碼中,我們首先定義了最高學習率peak_lr和最佳化器optimizer,然後取得GPT-2的編碼方式tokenizer。接著,我們定義了訓練的相關引數,如 epochs 數量n_epochs。在呼叫train_model函式時,我們傳入了必要的引數,包括模型、訓練資料載入器、驗證資料載入器、最佳化器、裝置、epochs數量等。在訓練過程中,我們會根據預定的warmup步驟調整學習率,並在warmup階段結束後應用梯度裁剪以避免梯度爆炸。

第五章:修改訓練函式

在 MacBook Air 或類別似的筆記型電腦上,訓練過程大約需要 5 分鐘才能完成,並會輸出以下內容:

Ep 1 (Iter 000000): Train loss 10.934, Val loss 10.939
Ep 1 (Iter 000005): Train loss 9.151, Val loss 9.461
...
Ep 15 (Iter 000130): Train loss 0.041, Val loss 6.915

每次努力都會使模型更接近最佳狀態。然而,由於資料集非常小,模型在幾個 epoch 後就開始過度擬合。儘管如此,我們可以看到訓練函式正在工作,因為它能夠最小化訓練集的損失。

讀者可以嘗試在更大的文字資料集上訓練模型,並將結果與使用 train_model_simple 函式取得的結果進行比較。

附錄 E:使用 LoRA 進行引數效率微調

低秩適應(LoRA)是一種廣泛使用的技術,用於對預訓練模型進行引數效率微調。以下討論根據第 6 章中的垃圾郵件分類別微調範例,但 LoRA 微調也適用於第 7 章中討論的監督指令微調。

E.1 LoRA 簡介

LoRA 是一種技術,透過限制模型調整到權重引數空間的較小維度子空間,從而使預訓練模型更好地適應特定、通常較小的資料集。這有效地捕捉了權重引數變化的最具影響力的方向。LoRA 方法之所以有用和流行,是因為它能夠高效地對大型模型進行任務特定資料的微調,從而大大降低了微調所需的計算成本和資源。

假設有一個大型權重矩陣 W 與特定層相關。LoRA 可以應用於 LLM 中的所有線性層,但為了說明目的,我們關注於單一層。

在深度神經網路訓練過程中,反向傳播期間,我們學習了一個 ΔW 矩陣,其中包含了更新原始權重引數以最小化損失函式的資訊。以下,我使用「權重」作為模型權重引數的簡稱。

在常規訓練和微調中,權重更新定義如下:

W = W - α * ΔW

其中 α 是學習率。

使用 LoRA,我們可以重新表述權重更新:

W = W - α * (A * B)

其中 A 和 B 是兩個比 W 小得多的矩陣,且 AB 表示 A 和 B 之間的矩陣乘積。

圖 E.1:全微調和 LoRA 的權重更新公式

如果你仔細觀察,你可能會注意到圖 E.1 中全微調和 LoRA 的視覺表示與之前介紹的公式略有不同。這是由於矩陣乘積的分配律,使我們可以分離原始權重和更新權重,而不是將它們結合起來。例如,在常規微調中,輸入資料為 x,我們可以將計算表示為:

W * x

這與之前介紹的公式略有不同,但它們是等效的。

在 LoRA 中,我們可以將權重更新表示為:

W - α * (A * B)

這使我們可以高效地對大型模型進行微調,而無需更新所有權重引數。

深度學習模型的微調:傳統方法與LoRA的比較

在深度學習中,模型的微調是一個非常重要的步驟,尤其是在面對新的任務或資料集時。傳統的微調方法是直接更新預先訓練好的權重矩陣W,以適應新的任務需求。然而,這種方法有一個明顯的缺點:需要更新整個權重矩陣,這可能會導致過度適應(overfitting)或損失原有的知識。

為瞭解決這個問題,LoRA(Low-Rank Adaptation)方法被提出。LoRA使用兩個小矩陣A和B來近似權重更新矩陣ΔW,其中A和B的內部維度r是一個可調的超引數。透過將A和B的乘積新增到預先訓練好的權重矩陣W上,LoRA可以實作對模型的微調,而不需要更新整個權重矩陣。

LoRA的工作原理

LoRA的核心思想是使用低秩近似來減少權重更新的複雜度。透過將權重更新矩陣ΔW近似為A和B的乘積,LoRA可以將原始的高維度權重更新問題轉換為低維度的近似問題。這樣不僅可以減少計算成本,還可以避免過度適應的問題。

比較傳統方法與LoRA

傳統的微調方法和LoRA有著明顯的不同。傳統方法直接更新預先訓練好的權重矩陣W,而LoRA使用兩個小矩陣A和B來近似權重更新矩陣ΔW。這兩種方法的比較如下:

  • 傳統方法:直接更新預先訓練好的權重矩陣W,以適應新的任務需求。
  • LoRA:使用兩個小矩陣A和B來近似權重更新矩陣ΔW,並將A和B的乘積新增到預先訓練好的權重矩陣W上。
內容解密:

上述內容介紹了LoRA的工作原理和優點,並將其與傳統的微調方法進行了比較。透過使用低秩近似,LoRA可以實作對模型的微調,而不需要更新整個權重矩陣。這種方法不僅可以減少過度適應的問題,還可以提高模型的泛化能力。

  flowchart TD
    A[預先訓練好的權重矩陣W] --> B[權重更新矩陣ΔW]
    B --> C[傳統微調方法:直接更新W]
    B --> D[LoRA:使用A和B近似ΔW]
    D --> E[計算A和B的乘積]
    E --> F[將A和B的乘積新增到W上]

圖表翻譯:

上述Mermaid圖表展示了傳統微調方法和LoRA之間的差異。圖表左側代表傳統微調方法,即直接更新預先訓練好的權重矩陣W。圖表右側代表LoRA,即使用兩個小矩陣A和B來近似權重更新矩陣ΔW,並將A和B的乘積新增到預先訓練好的權重矩陣W上。這種圖表有助於理解LoRA的工作原理及其優點。

從技術架構視角來看,梯度裁剪技術有效地控制了大語言模型(LLM)訓練過程中的梯度爆炸問題,保障了訓練的穩定性。透過設定梯度範數閾值,限制梯度更新幅度,避免模型引數劇烈震盪,從而提升模型的收斂速度和最終效能。然而,梯度裁剪並非解決所有訓練問題的萬靈丹,它需要與學習率調整策略、最佳化器選擇等技術手段協同作用,才能最大程度地發揮其效用。目前,梯度裁剪的閾值設定仍缺乏通用的最佳實踐,需要根據具體模型和資料集進行調整。未來,更精細化的梯度裁剪方法,例如根據層級或特定神經元重要性的裁剪策略,可能成為研究熱點,進一步提升LLM訓練效率。玄貓認為,梯度裁剪作為一種基礎的訓練技巧,對於所有LLM開發者而言都值得深入理解和靈活運用。