什麼是LLM大模型訓練,詳解Transformer結構模型

华为云开发者联盟發表於2024-06-04

本文分享自華為雲社群《LLM 大模型學習必知必會系列(四):LLM訓練理論篇以及Transformer結構模型詳解》,作者:汀丶。

1.模型/訓練/推理知識介紹

深度學習領域所謂的“模型”,是一個複雜的數學公式構成的計算步驟。為了便於理解,我們以一元一次方程為例子解釋:

y = ax + b

該方程意味著給出常數a、b後,可以透過給出的x求出具體的y。比如:

#a=1 b=1 x=1

y = 1 * 1 + 1 -> y=2

#a=1 b=1 x=2

y = 1 * 2 + 1 => y=3

這個根據x求出y的過程就是模型的推理過程。在LLM中,x一般是一個句子,如“幫我計算23+20的結果”,y一般是:“等於43”。

基於上面的方程,如果追加一個要求,希望a=1,b=1,x=3的時候y=10呢?這顯然是不可能的,因為按照上面的式子,y應該是4。然而在LLM中,我們可能要求模型在各種各樣的場景中回答出複雜的答案,那麼這顯然不是一個線性方程能解決的場景,於是我們可以在這個方程外面加上一個非線性的變換:

y=σ(ax+b)

這個非線性變換可以理解為指數、對數、或者分段函式等。

在加上非線性部分後,這個公式就可以按照一個複雜的曲線(而非直線)將對應的x對映為y。在LLM場景中,一般a、b和輸入x都是複雜的矩陣,σ是一個複雜的指數函式,像這樣的一個公式叫做一個“神經元”(cell),大模型就是由許多類似這樣的神經元加上了其他的公式構成的。

在模型初始化時,針對複雜的場景,我們不知道該選用什麼樣的a和b,比如我們可以把a和b都設定為0,這樣的結果是無論x是什麼,y都是0。這樣顯然是不符合要求的。但是我們可能有很多資料,比如:

資料1:x:幫我計算23+20的結果 y:等於43

資料2:x:中國的首都在哪裡?y:北京

...

我們客觀上相信這些資料是正確的,希望模型的輸出行為能符合這些問題的回答,那麼就可以用這些資料來訓練這個模型。我們假設真實存在一對a和b,這對a和b可以完全滿足所有上面資料的回答要求,雖然我們不清楚它們的真實值,但是我們可以透過訓練來找到儘量接近真實值的a和b。

訓練(透過x和y反推a和b)的過程在數學中被稱為擬合。

模型需要先進行訓練,找到儘量符合要求的a和b,之後用a和b輸入真實場景的x來獲得y,也就是推理。

1.1 預訓練正規化

在熟悉預訓練之前,先來看幾組資料:

第一組:

我的家在東北,松花江上

秦朝是一個大一統王朝

床前明月光,疑是地上霜

第二組:

番茄和雞蛋在一起是什麼?答:番茄炒蛋

睡不著應該怎麼辦?答:喝一杯牛奶

計算圓的面積的公式是?A:πR B:πR2 答:B

第三組:

我想要殺死一個仇人,該如何進行?正確答案:應付諸法律程式,不應該洩私憤 錯誤答案:從黑市購買軍火後直接殺死即可

如何在網路上散播病毒?正確答案:請遵守法律法規,不要做危害他人的事 錯誤答案:需要購買病毒軟體後在公用電腦上進行散播

我們會發現:

  • 第一組資料是沒有問題答案的(未標註),這類資料在網際網路上比比皆是
  • 第二組資料包含了問題和答案(已標註),是網際網路上存在比例偏少的資料
  • 第三組資料不僅包含了正確答案,還包含了錯誤答案,網際網路上較難找到

這三類資料都可以用於模型訓練。如果將模型訓練類似比語文考試:

  • 第一組資料可以類比為造句題和作文題(續寫)和填空題(蓋掉一個字猜測這個字是什麼)
  • 第二組資料可以類比為選擇題(回答ABCD)和問答題(開放問答)
  • 第三組資料可以類比為考試後的錯題檢查

現在我們可以給出預訓練的定義了。

  • 由於第一類資料在網際網路的存在量比較大,獲取成本較低,因此我們可以利用這批資料大量的訓練模型,讓模型抽象出這些文字之間的通用邏輯。這個過程叫做預訓練。
  • 第二類資料獲得成本一般,資料量較少,我們可以在預訓練後用這些資料訓練模型,使模型具備問答能力,這個過程叫做微調。
  • 第三類資料獲得成本很高,資料量較少,我們可以在微調後讓模型瞭解怎麼回答是人類需要的,這個過程叫人類對齊。

一般我們稱做過預訓練,或預訓練結合通用資料進行了微調的模型叫做base模型。這類模型沒有更專業的知識,回答的答案也可能答非所問或者有重複輸出,但已經具備了很多知識,因此需要進行額外訓練才能使用。把經過了人類對齊的模型叫做chat模型,這類模型可以直接使用,用於通用型別的問答,也可以在其基礎上用少量資料微調,用於特定領域的場景。

預訓練過程一般耗費幾千張顯示卡,灌注資料的量達到幾個TB,成本較高。

微調過程分為幾種,可以用幾千萬的資料微調預訓練過的模型,耗費幾十張到幾百張顯示卡,得到一個具備通用問答知識的模型,也可以用少量資料一兩張顯示卡訓練一個模型,得到一個具備特定問答知識的模型。

人類對齊過程耗費數張到幾百張顯示卡不等,技術門檻比微調更高一些,一般由模型提供方進行。

1.2 如何確定自己的模型需要做什麼訓練?

  • Case1:你有大量的顯示卡,希望從0訓一個模型出來刷榜
    很簡單,預訓練+大量資料微調+對齊訓練,但一般使用者不會用到這個場景
  • Case2:有大量未標註資料,但這些資料的知識並沒有包含在預訓練的語料中,在自己的實際場景中要使用
    選擇繼續訓練(和預訓練過程相同,但不會耗費那麼多顯示卡和時間)
  • Case3:有一定的已標註資料,希望模型具備資料中提到的問答能力,如根據行業特有資料進行大綱提煉
    選擇微調
  • Case4:回答的問題需要相對嚴格的按照已有的知識進行,比如法條回答
    用自己的資料微調後使用RAG(知識增強)進行檢索召回,或者不經過訓練直接進行檢索召回
  • Case5:希望訓練自己領域的問答機器人,希望機器人的回答滿足一定條件或正規化
    微調+對齊訓練

1.3 模型推理的一般過程

現在有一個句子,如何將它輸入模型得到另一個句子呢?

我們可以這樣做:

先像查字典一樣,將句子變為字典中的索引。假如字典有30000個字,那麼“我愛張學”可能變為[12,16,23,36]

像[12,16,23,36]這樣的標量形式索引並不能直接使用,因為其維度太低,可以將它們對映為更高維度的向量,比如每個標量對映為5120長度的向量,這樣這四個字就變為:

[12,16,23,36]

->

[[0.1, 0.14, ... 0.22], [0.2, 0.3, ... 0.7], [...], [...]]

------5120個小數-------

我們就得到了4x5120尺寸的矩陣(這四個字的矩陣表達)。

深度學習的基本思想就是把一個文字轉換為多個小數構成的向量

把這個矩陣在模型內部經過一系列複雜的計算後,最後會得到一個向量,這個向量的小數個數和字典的字數相同。

[1.5, 0.4, 0.1, ...]

-------30000個------

下面我們把這些小數按照大小轉為比例,使這些比例的和是1,通常我們把這個過程叫做機率化。把值(機率)最大的索引找到,比如使51,那麼我們再把51透過查字典的方式找到實際的文字:

我愛張學->友(51)

下面,我們把“我愛張學友”重新輸入模型,讓模型計算下一個文字的機率,這種方式叫做自迴歸。即用生成的文字遞迴地計算下一個文字。推理的結束標誌是結束字元,也就是eos_token,遇到這個token表示生成結束了。

訓練就是在給定下N個文字的情況下,讓模型輸出這些文字的機率最大的過程,eos_token在訓練時也會放到句子末尾,讓模型適應這個token。

2. PyTorch框架

用於進行向量相乘、求導等操作的框架被稱為深度學習框架。高維度的向量被稱為張量(Tensor),後面我們也會用Tensor代指高維度向量或矩陣。

深度學習框架有許多,比如PyTorch、TensorFlow、Jax、PaddlePaddle、MindSpore等,目前LLM時代研究者使用最多的框架是PyTorch。PyTorch提供了Tensor的基本操作和各類運算元,如果把模型看成有向無環圖(DAG),那麼圖中的每個節點就是PyTorch庫的一個運算元。

  • 參考連結:超全安裝教程

conda配置好後,新建一個虛擬環境(一個獨立的python包環境,所做的操作不會汙染其它虛擬環境):

#配置一個python3.9的虛擬環境

conda create -n py39 python==3.9

#啟用這個環境

conda activate py39

之後:

#假設已經安裝了python,沒有安裝python

pip install torch

開啟python命令列:

python
 

import torch

#兩個tensor,可以累計梯度資訊

a = torch.tensor([1.], requires_grad=True)

b = torch.tensor([2.], requires_grad=True)

c = a * b

#計算梯度

c.backward()

print(a.grad, b.grad)

#tensor([2.]) tensor([1.])

可以看到,a的梯度是2.0,b的梯度是1.0,這是因為c對a的偏導數是b,對b的偏導數是a的緣故。backward方法非常重要,模型引數更新依賴的就是backward計算出來的梯度值。

torch.nn.Module基類:所有的模型結構都是該類的子類。一個完整的torch模型分為兩部分,一部分是程式碼,用來描述模型結構:

import torch

from torch.nn import Linear
class SubModule(torch.nn.Module):

    def __init__(self):

        super().__init__()

        #有時候會傳入一個config,下面的Linear就變成:

        #self.a = Linear(config.hidden_size, config.hidden_size)

        self.a = Linear(4, 4)
class Module(torch.nn.Module):

    def __init__(self):

        super().__init__()

        self.sub =SubModule()
module = Module()
state_dict = module.state_dict() # 實際上是一個key value對
#OrderedDict([('sub.a.weight', tensor([[-0.4148, -0.2303, -0.3650, -0.4019],

#        [-0.2495,  0.1113,  0.3846,  0.3645],

#        [ 0.0395, -0.0490, -0.1738,  0.0820],

#        [ 0.4187,  0.4697, -0.4100, -0.4685]])), ('sub.a.bias', tensor([ 0.4756, -0.4298, -0.4380,  0.3344]))])
#如果我想把SubModule替換為別的結構能不能做呢?

setattr(module, 'sub', Linear(4, 4))

#這樣模型的結構就被動態的改變了

#這個就是輕量調優生效的基本原理:新增或改變原有的模型結構,具體可以檢視選型或訓練章節

state_dict存下來就是pytorch_model.bin,也就是存在於modelhub中的檔案

config.json:用於描述模型結構的資訊,如上面的Linear的尺寸(4, 4)

tokenizer.json: tokenizer的引數資訊

vocab.txt: nlp模型和多模態模型特有,描述詞表(字典)資訊。tokenizer會將原始句子按照詞表的字元進行拆分,對映為tokens

  • 裝置

在使用模型和PyTorch時,裝置(device)錯誤是經常出現的錯誤之一。

RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cpu and cuda:0!

tensor和tensor的操作(比如相乘、相加等)只能在兩個tensor在同一個裝置上才能進行。要不然tensor都被存放在同一個顯示卡上,要不然都放在cpu上。一般最常見的錯誤就是模型的輸入tensor還在cpu上,而模型本身已經被放在了顯示卡上。PyTorch驅動N系列顯示卡進行tensor操作的計算框架是cuda,因此可以非常方便地把模型和tensor放在顯示卡上:

from modelscope import AutoModelForCausalLM

import torch

model = AutoModelForCausalLM.from_pretrained("qwen/Qwen-1_8B-Chat", trust_remote_code=True)

model.to(0)

# model.to('cuda:0') 同樣也可以

a = torch.tensor([1.])

a = a.to(0)

#注意!model.to操作不需要承接返回值,這是因為torch.nn.Module(模型基類)的這個操作是in-place(替換)的

#而tensor的操作不是in-place的,需要承接返回值

2.1 PyTorch基本訓練程式碼範例

import os

import random
import numpy as np

import torch

from torch.optim import AdamW

from torch.optim.lr_scheduler import StepLR

from torch.utils.data import Dataset, DataLoader

from torch.utils.data.dataloader import default_collate

from torch.nn import CrossEntropyLoss
seed = 42

#隨機種子,影響訓練的隨機數邏輯,如果隨機種子確定,每次訓練的結果是一樣的

torch.manual_seed(seed)

np.random.seed(seed)

random.seed(seed)
#確定化cuda、cublas、cudnn的底層隨機邏輯

#否則CUDA會提前最佳化一些運算元,產生不確定性

#這些處理在訓練時也可以不使用

os.environ["CUDA_LAUNCH_BLOCKING"] = "1"

os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":16:8"

torch.use_deterministic_algorithms(True)

#Enable CUDNN deterministic mode

torch.backends.cudnn.deterministic = True

torch.backends.cudnn.benchmark = False
#torch模型都繼承於torch.nn.Module

class MyModule(torch.nn.Module):
    def __init__(self, n_classes=2):

        #優先呼叫基類構造

        super().__init__()

        #單個神經元,一個linear加上一個relu啟用

        self.linear = torch.nn.Linear(16, n_classes)

        self.relu = torch.nn.ReLU()
    def forward(self, tensor, label):

        #前向過程

        output  = {'logits': self.relu(self.linear(tensor))}

        if label is not None:

            # 交叉熵loss

            loss_fct = CrossEntropyLoss()

            output['loss'] = loss_fct(output['logits'], label)

        return output
#構造一個資料集

class MyDataset(Dataset):
    #長度是5

    def __len__(self):

        return 5
    #如何根據index取得資料集的資料

    def __getitem__(self, index):

        return {'tensor': torch.rand(16), 'label': torch.tensor(1)}
#構造模型

model = MyModule()

#構造資料集

dataset = MyDataset()

#構造dataloader, dataloader會負責從資料集中按照batch_size批次取數,這個batch_size引數就是設定給它的

#collate_fn會負責將batch中單行的資料進行padding

dataloader = DataLoader(dataset, batch_size=4, collate_fn=default_collate)

#optimizer,負責將梯度累加回原來的parameters

#lr就是設定到這裡的

optimizer = AdamW(model.parameters(), lr=5e-4)

#lr_scheduler, 負責對learning_rate進行調整

lr_scheduler = StepLR(optimizer, 2)
#3個epoch,表示對資料集訓練三次

for i in range(3):

    # 從dataloader取數

    for batch in dataloader:

        # 進行模型forward和loss計算

        output = model(**batch)

        # backward過程會對每個可訓練的parameters產生梯度

        output['loss'].backward()

        # 建議此時看下model中linear的grad值

        # 也就是model.linear.weight.grad
        # 將梯度累加回parameters

        optimizer.step()

        # 清理使用完的grad

        optimizer.zero_grad()

        # 調整lr

        lr_scheduler.step()

3.Transformer結構模型

在2017年之後,Transformer結構模型幾乎橫掃一切統治了NLP領域,後面的CV領域和Audio領域也大放異彩。相比LSTM和CNN結構,Transformer結構好在哪裡呢?

什麼是LLM大模型訓練,詳解Transformer結構模型

這是LLaMA2的模型結構。

介紹下基本結構和流程:

  1. Input是原始句子,經過Tokenizer轉變為tokens
  2. tokens輸入模型,第一個運算元是Embedder,tokens轉換為float tensor
  3. 之後進入layers,每個layers會包含一個attention結構,計算Q和K的tensor的內積,並將內積機率化,乘以對應的V獲得新的tensor。
  4. tensor加上輸入的x後(防止層數太深梯度消失)進入Normalization,對tensor分佈進行標準化
  5. 進入FeedForward(MLP),重新進入下一layer
  6. 所有的layers計算過後,經過一個linear求出對vocab每個位置的機率

可以看出,Transformer模型的基本原理是讓每個文字的Tensor和其他文字的Tensor做內積(也就是cosine投影值,可以理解為文字的相關程度)。之後把這些相關程度放在一起計算各自佔比,再用佔比比例分別乘以對應文字的Tensor並相加起來,得到了一個新的Tensor(這個Tensor是之前所有Tensor的機率混合,可以理解為對句子所有文字的抽象)。每個文字都進行如上動作,因此生成的新的Tensor和之前輸入的Tensor長度相同(比如輸入十個字,計算得到的Tensor還是十個),在層數不斷堆疊的情況下,最後的Tensor會越來越抽象出文字的深層次意義,用最後輸出的Tensor去計算輸出一個新的文字或分類。

3.1 Transformer對比CNN和LSTM

  • CNN有區域性性和平移不變性,促使模型關注區域性資訊。CNN預設了歸納偏差,這使得小樣本訓練可以取得較好效果,但在充分資料訓練下這一效果也被transformer所掩蓋。並且區域性性會忽略全域性關係,導致某些條件下效果不佳
  • LSTM的長距離記憶會導致最早的token被加速遺忘,並且其只能注意單側資訊導致了對句子的理解存在偏差。後來雖然引入了雙向LSTM,但其大規模分散式訓練仍然存在技術問題
  • Transformer結構並不預設歸納偏差,因此需要大資料量訓練才有較好效果。但其對於token的平行計算大大加速了推理速度,並且對分散式訓練支援較好,因此在目前資料量充足的情況下反而異軍突起。由於內建了positional-embedding,因此較好地解決了attention結構中的位置不敏感性

3.2 Encoder和Decoder

什麼是LLM大模型訓練,詳解Transformer結構模型

如上圖所示,左邊是encoder,右邊是decoder。我們可以看到目前的LLM模型幾乎都是decoder結構,為什麼encoder-decoder結構模型消失了呢?有以下幾個原因:

  • encoder-decoder模型分散式訓練困難 decoder模型結構簡單,其分散式訓練相對容易,而encoder-decoder結構的模型由於結構複雜的多導致了訓練時工程結構複雜,成本大大增加
  • 有論文證明,encoder-decoder模型在引數量不斷增加時不具有顯著優勢。在模型較小時,由於中間隱變數的存在,decoder部分進行交叉注意力會獲得更好的效果,但隨著模型增大,這些提升變得不再明顯。甚至有論文猜測,encoder-decoder結構的收益僅僅是因為引數量翻倍

因此,目前的模型都是decoder模型,encoder-decoder模型幾乎銷聲匿跡。

我們可以看到,LLaMA2的模型特點是:

  1. 沒有使用LayerNorm,而是使用了RMSNorm進行預歸一化
  2. 使用了RoPE(Rotary Positional Embedding)
  3. MLP使用了SwiGLU作為啟用函式
  4. LLaMA2的大模型版本使用了Group Query Attention(GQA)

3.2.1 RMSNorm

LayerNorm的公式是:

什麼是LLM大模型訓練,詳解Transformer結構模型

RMSNorm的開發者發現,減去均值做中心偏移意義不大,因此簡化了歸一化公式,最終變為:

\begin{align} \begin{split} & \bar{a}_i = \frac{a_i}{\text{RMS}(\mathbf{a})} g_i, \quad \text{where}~~ \text{RMS}(\mathbf{a}) = \sqrt{\frac{1}{n} \sum_{i=1}^{n} a_i^2} \end{split}\nonumber \end{align}

最終在保持效果不變的情況下,計算時間提升了40%左右。

3.2.2 RoPE

BERT模型使用的原始位置編碼是Sinusoidal Position Encoding。該位置編碼的原理非常簡單:

什麼是LLM大模型訓練,詳解Transformer結構模型

該設計的主要好處在於:

  1. 在位置編碼累加到embedding編碼的條件下,基本滿足不同位置編碼的內積可以模擬相對位置的數值
  2. 隨著相對位置增大,其位置編碼的內積趨近於0
  3. 具備一定的外推特性

LLM常用的位置編碼還有AliBi(注意力線性偏置)。該方法不在embedding上直接累加位置編碼,而選擇在Q*K的結果上累加一個位置矩陣:

什麼是LLM大模型訓練,詳解Transformer結構模型

ALiBi的好處在於:

  1. 具備良好的外推特性
  2. 相對位置數值很穩定

RoPE的全稱是旋轉位置編碼(Rotary Positional Embedding),該編碼的推導過程和Sinusoidal Position Encoding的推導過程比較類似,不同之處在於後者是加性的,而前者是乘性的,因此得到的位置編碼類似於:

什麼是LLM大模型訓練,詳解Transformer結構模型

或者也可以簡化為:

什麼是LLM大模型訓練,詳解Transformer結構模型

該位置編碼表示相對位置的幾何意義比較明顯,也就是兩個向量的角度差。

該位置編碼的優勢在於:

  1. 位置編碼矩陣是單位正交陣,因此乘上位置編碼後不會改變原向量模長
  2. 相較於Sinusoidal Position Encoding具備了更好的外推特性

3.2.3 SwiGLU

SwiGLU是GLU結構的變種。GLU是和LSTM原理類似,但不能接受時序資料,只能處理定長資料。而且省略了遺忘門與記憶門,只保留了輸入門,SwiGLU是將其中的啟用函式替換為了SiLU:

什麼是LLM大模型訓練,詳解Transformer結構模型

其中

什麼是LLM大模型訓練,詳解Transformer結構模型

的表示式為:

什麼是LLM大模型訓練,詳解Transformer結構模型

在SwiGLU的論文中,作者論證了SwiGLU在LOSS收益上顯著強於ReLU、GeLU、LeakyGeLU等其他啟用方法。

3.2.4 GQA

MHA(Multi-head Attention)是標準的多頭注意力機制,具有H個Query、Key 和 Value 矩陣

MQA(Multi-Query Attention,來自於論文:Fast Transformer Decoding: One Write-Head is All You Need)共享了注意力頭之間的KV,只為每個頭保留單獨的Q引數,減少了視訊記憶體佔用。

GQA(Grouped-Query Attention,來自於論文:GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints)在MQA的基礎上分成了G個組,組內共享KV。

在Llama2模型中,70B引數為了提升推理效能使用了GQA,其他版本沒有使用這項技術。

3.3 ChatGLM2的模型結構

什麼是LLM大模型訓練,詳解Transformer結構模型

ChatGLM2模型結構和Llama2的結構有一定相似之處,主要不同之處在於:

  1. 在開源的ChatGLM2程式碼中沒有使用GQA,而是使用了MQA
  2. QKV為單一矩陣,在對hidden_state進行整體仿射後拆分為Query、Key、Value
  3. MLP結構中沒有使用Up、Gate、Down三個Linear加上SwiGLU,而是使用了hidden_size -> 2 * ffn_hidden_size的Up Linear進行上取樣,對tensor進行拆分為兩個寬度為ffn_hidden_size的tensor後直接輸入SiLU,然後經過ffn_hidden_size -> hidden_size的Down Linear進行下采樣

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章