前言
近期,除了研究ChatGPT背后的各種技術(shù)細(xì)節(jié) 不斷看論文(至少100篇,100篇目錄見此:ChatGPT相關(guān)技術(shù)必讀論文100篇),還開始研究一系列開源模型(包括各自對應(yīng)的模型架構(gòu)、訓(xùn)練方法、訓(xùn)練數(shù)據(jù)、本地私有化部署、硬件配置要求、微調(diào)等細(xì)節(jié))?
本文一開始是作為此文《ChatGPT技術(shù)原理解析:從RL之PPO算法、RLHF到GPT4、instructGPT》的第4部分,但隨著研究深入 為避免該文篇幅又過長,將把『第4部分 開源項(xiàng)目』抽取出來 獨(dú)立成本文,然后不斷續(xù)寫本文直至成了一個(gè)系列
畢竟我上半年的目標(biāo)之一,便是把ChatGPT涉及的所有一切關(guān)鍵技術(shù)細(xì)節(jié),以及相關(guān)的開源項(xiàng)目都研究的透透的,故過程中會不斷產(chǎn)出一篇篇新文章、新課程(比如七月類ChatGPT微調(diào)實(shí)戰(zhàn)課)出來
?
第一部分 LLaMA的代碼級解讀:RMSNorm/SwiGLU/RoPE/Transformer
1.1 Meta發(fā)布LLaMA((7B 13B 33B 65B):參數(shù)少但多數(shù)任務(wù)的效果好于GPT3
一直致力于LLM模型研究的國外TOP 3大廠除了OpenAI、Google,便是Meta(原來的Facebook)
Meta曾第一個(gè)發(fā)布了基于LLM的聊天機(jī)器人——BlenderBot 3,但輸出不夠安全,很快下線;再后來,Meta發(fā)布一個(gè)專門為科學(xué)研究設(shè)計(jì)的模型Galactica,但用戶期望過高,發(fā)布三天后又下線
23年2.24日,Meta通過論文《LLaMA: Open and Efficient Foundation Language Models》發(fā)布了自家的大型語言模型LLaMA(這是LLaMA的GitHub代碼地址,這是解讀之一),有多個(gè)參數(shù)規(guī)模的版本(7B 13B 33B 65B),并于次月3.8日被迫開源
LLaMA只使用公開的數(shù)據(jù)(總計(jì)1.4T即1,400GB的token,其中CommonCrawl的數(shù)據(jù)占比67%,C4數(shù)據(jù)占比15%,Github、Wikipedia、Books這三項(xiàng)數(shù)據(jù)均都各自占比4.5%,ArXiv占比2.5%,StackExchange占比2%),論文中提到
When training a 65B-parameter model, our code processes around 380 tokens/sec/GPU on 2048 A100 GPU with 80GB of RAM.
This means that training over our dataset containing 1.4T tokens takes approximately 21 days
且試圖證明小模型在足夠多的的數(shù)據(jù)上訓(xùn)練后,也能達(dá)到甚至超過大模型的效果
- 比如13B參數(shù)的版本在多項(xiàng)基準(zhǔn)上測試的效果好于2020年的參數(shù)規(guī)模達(dá)175B的GPT-3
- 而對于65B參數(shù)的LLaMA,則可與DeepMind的Chinchilla(70B參數(shù))和谷歌的PaLM(540B參數(shù))旗鼓相當(dāng)
- 且Meta還嘗試使用了論文「Scaling Instruction-Finetuned Language Models」中介紹的指令微調(diào)方法,由此產(chǎn)生的模型LLaMA-I,在MMLU(Massive Multitask Language Understanding,大型多任務(wù)語言理解)上要優(yōu)于Google的指令微調(diào)模型Flan-PaLM-cont(62B)
1.2 LLaMA的模型架構(gòu)——RMSNorm/SwiGLU/RoPE/Transformer/1-1.4T tokens
1.2.1?項(xiàng)目環(huán)境依賴:torch、fairscale、fire、sentencepiece
此項(xiàng)目給出的環(huán)境依賴有4個(gè):
- torch
- fairscale,fairscale是用來做GPU分布的,一般是當(dāng)使用DDP仍然遇到超顯存的問題時(shí)使用fairscale
- fire,fire是一個(gè)命令行工具,用或者不用他都可以
- sentencepiece,sentencepiece是用于tokenizer的工具包
「?SentencePiece 實(shí)現(xiàn)了subword單元(例如,字節(jié)對編碼(BPE)和 unigram語言模型),并可以直接從原始句子訓(xùn)練字詞模型(subword model),這是對SentencePiece的解讀:大模型詞表擴(kuò)充必備工具SentencePiece?」# 引入 sentencepiece 庫的 SentencePieceProcessor 模塊,用于進(jìn)行分詞操作 from sentencepiece import SentencePieceProcessor # 引入 logging 庫的 getLogger 模塊,用于生成日志 from logging import getLogger # 引入 typing 庫的 List 模塊,用于注釋函數(shù)參數(shù)或返回值的類型 from typing import List # 引入 os 庫,提供了大量與操作系統(tǒng)進(jìn)行交互的接口 import os # 創(chuàng)建一個(gè)日志記錄器 logger = getLogger() # 定義一個(gè) Tokenizer 類 class Tokenizer: # 初始化函數(shù),參數(shù)為 SentencePiece 模型的路徑 def __init__(self, model_path: str): # 判斷指定的模型文件是否存在 assert os.path.isfile(model_path), model_path # 加載 SentencePiece 模型 self.sp_model = SentencePieceProcessor(model_file=model_path) # 記錄日志,提示模型加載成功 logger.info(f"Reloaded SentencePiece model from {model_path}") # 獲取模型的詞匯量、開始標(biāo)記 ID、結(jié)束標(biāo)記 ID、填充標(biāo)記 ID self.n_words: int = self.sp_model.vocab_size() self.bos_id: int = self.sp_model.bos_id() self.eos_id: int = self.sp_model.eos_id() self.pad_id: int = self.sp_model.pad_id() # 記錄日志,顯示獲取的信息 logger.info( f"#words: {self.n_words} - BOS ID: {self.bos_id} - EOS ID: {self.eos_id}" ) # 確保模型的詞匯量與詞片段大小一致 assert self.sp_model.vocab_size() == self.sp_model.get_piece_size() # 編碼函數(shù),將輸入的字符串編碼為 token id 列表 def encode(self, s: str, bos: bool, eos: bool) -> List[int]: # 檢查輸入的是否是字符串 assert type(s) is str # 使用 SentencePiece 模型將字符串編碼為 token id 列表 t = self.sp_model.encode(s) # 如果需要在開頭添加開始標(biāo)記,就將開始標(biāo)記 id 添加到列表的開頭 if bos: t = [self.bos_id] + t # 如果需要在結(jié)尾添加結(jié)束標(biāo)記,就將結(jié)束標(biāo)記 id 添加到列表的結(jié)尾 if eos: t = t + [self.eos_id] # 返回 token id 列表 return t # 解碼函數(shù),將 token id 列表解碼為字符串 def decode(self, t: List[int]) -> str: # 使用 SentencePiece 模型將 token id 列表解碼為字符串 return self.sp_model.decode(t)
1.2.2 RMSNorm:對每個(gè)Transformer子層的輸入進(jìn)行歸一化
為了提高訓(xùn)練的穩(wěn)定性,對每個(gè)transformer子層的輸入進(jìn)行歸一化,而不是對輸出進(jìn)行歸一化,且使用由Zhang和Sennrich(2019)提出的RMSNorm(Root Mean Square Layer Normalization)
RMS Norm是一般LayerNorm的一種變體,可以在梯度下降時(shí)令損失更加平滑,與layerNorm相比,RMS Norm的主要區(qū)別在于去掉了減去均值的部分(re-centering),只保留方差部分(re-scaling)
為一目了然,我們看下它們各自的歸一化的表達(dá)式
-
LayerNorm
在給定一個(gè)輸入特征向量后,先計(jì)算 x 的均值 μ 和標(biāo)準(zhǔn)差 σ 然后進(jìn)行歸一化操作:
其中的是可學(xué)習(xí)的縮放參數(shù),來調(diào)整每個(gè)特征在歸一化后的尺度或權(quán)重,最終作用是恢復(fù)歸一化操作可能損失的信息,如數(shù)據(jù)的比例和分布等
而是偏移因子,可以對歸一化并放縮后的數(shù)據(jù)進(jìn)行偏移,使模型可以學(xué)習(xí)到一個(gè)最優(yōu)的數(shù)值范圍,比如在ReLU激活函數(shù)中,我們可能希望值在0以上 -
RMS Norm
首先,計(jì)算輸入特征向量 a 的平方根均值 (其中,n是向量a的元素?cái)?shù)量) 然后,對輸入特征向量 a 進(jìn)行歸一化 此外,可選地,RMSNorm 還可以引入可學(xué)習(xí)的放縮參數(shù)? 和偏移參數(shù) :
其代碼實(shí)現(xiàn)為
?至于RMS Norm為什么有用,需要求梯度進(jìn)行分析,感興趣的同學(xué)可以閱讀RMS Norm的論文class RMSNorm(torch.nn.Module): def __init__(self, dim: int, eps: float = 1e-6): super().__init__() // eps防止取倒數(shù)之后分母為0 self.eps = eps self.weight = nn.Parameter(torch.ones(dim)) // x是輸入 def _norm(self, x): // torch.rsqrt是開平方并取倒數(shù) // x.pow(2)是平方 / mean(-1)是在最后一個(gè)維度(即hidden特征維度)上取平均 return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) def forward(self, x): output = self._norm(x.float()).type_as(x) // weight是末尾乘的可訓(xùn)練參數(shù),即gi return output * self.weight
1.2.3?SwiGLU替代ReLU
為了更好的理解SwiGLU,首先你得先了解什么是ReLU和GLU
- ReLU的函數(shù)表達(dá)式為,這意味著對于所有負(fù)的輸入值,ReLU函數(shù)的輸出都是0,對于所有正的輸入值,ReLU函數(shù)的輸出等于輸入值本身
- GLU 的基本思想是引入一種稱為“門”機(jī)制,該機(jī)制可以動態(tài)地控制信息的流動
這個(gè)公式意味著,對于每個(gè)輸入 x,都會有一個(gè)相應(yīng)的門值,這個(gè)門值由 sigmoid 函數(shù)產(chǎn)生,其范圍在 0 到 1 之間(在正數(shù)區(qū)域接近于1,負(fù)數(shù)區(qū)域接近于0),這個(gè)門值用于調(diào)節(jié)相應(yīng)的輸入值
? 如果 接近 1,那么“門”就幾乎完全開啟,輸入 x 的信息能夠自由流動,于是 GLU 的輸出接近于 x
? 如果 接近 0,意味著“門”幾乎完全關(guān)閉,即輸入 x 的大部分或全部信息被阻止通過,于是 GLU 的輸出接近 0
而LLaMA采用Shazeer(2020)提出的SwiGLU替換了原有的ReLU,SwiGLU的作用機(jī)制是根據(jù)輸入數(shù)據(jù)的特性,通過學(xué)習(xí)到的參數(shù)自動調(diào)整信息流動的路徑,具體是采用SwiGLU的Feedforward Neural Network (簡稱FNN,這是一種使用可學(xué)習(xí)的門控機(jī)制的前饋神經(jīng)網(wǎng)絡(luò))
其在論文中以如下公式進(jìn)行表述:
解釋下這個(gè)公式
- 該公式先是通過Swish非線性激活函數(shù)處理 “輸入和權(quán)重矩陣的乘積”
- 上面步驟1得到的結(jié)果和 “輸入與權(quán)重矩陣的乘積” 進(jìn)行逐元素的乘法
這個(gè)操作相當(dāng)于在 Swish 激活的輸出和第二個(gè)線性變換的輸出之間引入了一個(gè)類似于GLU的“門”,這個(gè)門的值是由原始輸入 通過線性變換 計(jì)算得到的,因此,它可以動態(tài)地控制 Swish 激活的輸出 - 最后乘以權(quán)重矩陣
至于Swish激活函數(shù)可表示為
表示sigmoid函數(shù),但其輸入被縮放了?倍,是一個(gè)可以學(xué)習(xí)的參數(shù),比如下圖,不同,Swish激活函數(shù)的形狀則各異
- 當(dāng) 趨近于 0 時(shí),Swish 函數(shù)趨近于線性函數(shù) y = x
- 當(dāng)? 趨近于無窮大時(shí),Swish 函數(shù)趨近于 ReLU 函數(shù)
對應(yīng)論文見:Ramachandran et al., 2017
代碼實(shí)現(xiàn)上:可以通過調(diào)用torch內(nèi)置方法F.silu()實(shí)現(xiàn),會在下文的FFN部分介紹
為增進(jìn)大家對SwiGLU的理解,我還是舉個(gè)簡單的例子來說明這個(gè)過程
假設(shè)我們的輸入 x 是一個(gè)二維向量
[2,3]
,權(quán)重矩陣 W 和 V 都是 2x2 矩陣,且我們簡化問題,令 β =1
x[2,3]乘以權(quán)重矩陣 W
得到新的向量z,假設(shè)z是[5, 4]
- 對 xW的結(jié)果 z =?
[5, 4]
應(yīng)用 Swish 激活函數(shù),即Swish_1(z) = z ⊙ σ(z) =[5σ(5), 4σ(4)]
- 然后,我們計(jì)算
xV
以得到“門”控制值
計(jì)算xV
得到新的向量 y,假設(shè) y =[1,0]
- 接著,我們將
Swish_1(xW)
和xV
做元素級別的乘法,也就是實(shí)施"門控":(Swish_1(xW) ⊙ xV)
=[5σ(5)*1, 4σ(4)*0]
=[5σ(5), 0]
在這個(gè)例子中,我們可以看到
xV
的輸出[1,0]
在元素級別上控制了Swish_1(xW)
的輸出
- 第一個(gè)維度的門值為 1,因此
Swish_1(xW)
的第一個(gè)維度的輸出能夠“通過”,得到進(jìn)入門控之前的結(jié)果5σ(5)
- 第二個(gè)維度的門值為 0,因此
Swish_1(xW)
的第二個(gè)維度的輸出被“阻止”了,結(jié)果為 0這就是“門”的動態(tài)控制作用:它根據(jù)
xV
的輸出調(diào)整Swish_1(xW)
的輸出,通過這種方式,模型可以根據(jù)輸入 x 的不同,動態(tài)地調(diào)整信息流動
1.2.4 位置編碼:RoPE
關(guān)于旋轉(zhuǎn)位置編碼的理解,請參看此文《一文通透位置編碼:從標(biāo)準(zhǔn)位置編碼到旋轉(zhuǎn)位置編碼RoPE》
接下來,我們來看下LLaMA里是怎么實(shí)現(xiàn)這個(gè)旋轉(zhuǎn)位置編碼的,具體而言,LLaMA 的model.py文件里面實(shí)現(xiàn)了旋轉(zhuǎn)位置編碼(為方便大家理解,我給相關(guān)代碼 加了下注釋)
首先,逐一實(shí)現(xiàn)這三個(gè)函數(shù)
precompute_freqs_cis
reshape_for_broadcast
apply_rotary_emb
# 預(yù)計(jì)算頻率和復(fù)數(shù)的函數(shù)
def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0):
? ? freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim)) ? ?# 計(jì)算頻率
? ? t = torch.arange(end, device=freqs.device) ? ?# 根據(jù)結(jié)束位置生成序列
? ? freqs = torch.outer(t, freqs).float() ? ?# 計(jì)算外積得到新的頻率
? ? freqs_cis = torch.polar(torch.ones_like(freqs), freqs) ? ?# 計(jì)算復(fù)數(shù)
? ? return freqs_cis ? ?# 返回復(fù)數(shù)
# 重塑的函數(shù)
def reshape_for_broadcast(freqs_cis: torch.Tensor, x: torch.Tensor):
? ? ndim = x.ndim ? ?# 獲取輸入張量的維度
? ? assert 0 <= 1 < ndim ? ?# 檢查維度的合理性
? ? assert freqs_cis.shape == (x.shape[1], x.shape[-1]) ? ?# 檢查復(fù)數(shù)的形狀
? ? shape = [d if i == 1 or i == ndim - 1 else 1 for i, d in enumerate(x.shape)] ? ?# 計(jì)算新的形狀
? ? return freqs_cis.view(*shape) ? ?# 重塑復(fù)數(shù)的形狀并返回
# 應(yīng)用旋轉(zhuǎn)嵌入的函數(shù)
def apply_rotary_emb(
? ? xq: torch.Tensor,
? ? xk: torch.Tensor,
? ? freqs_cis: torch.Tensor,
) -> Tuple[torch.Tensor, torch.Tensor]:
? ? xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2)) ? ?# 將xq視為復(fù)數(shù)
? ? xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2)) ? ?# 將xk視為復(fù)數(shù)
? ? freqs_cis = reshape_for_broadcast(freqs_cis, xq_) ? ?# 重塑復(fù)數(shù)的形狀
? ? xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3) ? ?# 計(jì)算xq的輸出
? ? xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3) ? ?# 計(jì)算xk的輸出
? ? return xq_out.type_as(xq), xk_out.type_as(xk) ? ?# 返回xq和xk的輸出
之后,在注意力機(jī)制的前向傳播函數(shù)中調(diào)用上面實(shí)現(xiàn)的第三個(gè)函數(shù) apply_rotary_emb,賦上位置信息 (詳見下文1.2.5節(jié))
# 對Query和Key應(yīng)用旋轉(zhuǎn)嵌入
xq, xk = apply_rotary_emb(xq, xk, freqs_cis=freqs_cis)
1.2.5 Transform架構(gòu)的實(shí)現(xiàn):Attention計(jì)算、SA、FFN
LLaMA和GPT一樣,都是基于Transformer這個(gè)架構(gòu),通常,我們在構(gòu)建transformer時(shí),是按Block構(gòu)建的,每個(gè)transformer Block包含SA和FFN兩部分,然后再通過堆疊block的形式,構(gòu)建起整個(gè)transformer網(wǎng)絡(luò),LLaMA也是這樣做的
回顧一下Attention計(jì)算的總體過程是:
- 輸入,分別經(jīng)過三個(gè)Linear得到
- 在??和中加入旋轉(zhuǎn)位置編碼
- 緩存??和??
- 計(jì)算
其中有一個(gè)細(xì)節(jié)就是緩存機(jī)制,它設(shè)計(jì)的目的是在generate時(shí)減少token的重復(fù)計(jì)算。簡單解釋一下,就是在計(jì)算第n個(gè)token特征的時(shí)候,需要用到第個(gè)token,即每次生成時(shí),需要知道前面所有的過往信息,如果每次都從頭算的話,那就會造成極大的浪費(fèi),所以就沒算一個(gè)位置的信息,就把它緩存下來
接下來,我們來看下代碼實(shí)現(xiàn),首先是SA(self-attention)部分:
class Attention(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
# 設(shè)置本地注意力頭的數(shù)量
self.n_local_heads = args.n_heads // fs_init.get_model_parallel_world_size()
# 每個(gè)注意力頭的維度
self.head_dim = args.dim // args.n_heads
# Query投影層
self.wq = ColumnParallelLinear(
args.dim,
args.n_heads * self.head_dim,
bias=False,
gather_output=False,
init_method=lambda x: x,
)
# Key投影層
self.wk = ColumnParallelLinear(
args.dim,
args.n_heads * self.head_dim,
bias=False,
gather_output=False,
init_method=lambda x: x,
)
# Value投影層
self.wv = ColumnParallelLinear(
args.dim,
args.n_heads * self.head_dim,
bias=False,
gather_output=False,
init_method=lambda x: x,
)
# 輸出投影層
self.wo = RowParallelLinear(
args.n_heads * self.head_dim,
args.dim,
bias=False,
input_is_parallel=True,
init_method=lambda x: x,
)
# 使用零初始化鍵緩存
self.cache_k = torch.zeros(
(args.max_batch_size, args.max_seq_len, self.n_local_heads, self.head_dim)
).cuda()
# 使用零初始化值緩存
self.cache_v = torch.zeros(
(args.max_batch_size, args.max_seq_len, self.n_local_heads, self.head_dim)
).cuda()
def forward(self, x: torch.Tensor, start_pos: int, freqs_cis: torch.Tensor, mask: Optional[torch.Tensor]):
bsz, seqlen, _ = x.shape
# 進(jìn)行Query投影
xq, xk, xv = self.wq(x), self.wk(x), self.wv(x)
# 將形狀調(diào)整為[bsz, seqlen, n_local_heads, head_dim]
xq = xq.view(bsz, seqlen, self.n_local_heads, self.head_dim)
xk = xk.view(bsz, seqlen, self.n_local_heads, self.head_dim)
xv = xv.view(bsz, seqlen, self.n_local_heads, self.head_dim)
# 對Query和Key應(yīng)用旋轉(zhuǎn)嵌入
xq, xk = apply_rotary_emb(xq, xk, freqs_cis=freqs_cis)
# 將緩存鍵和值轉(zhuǎn)換為xq的設(shè)備類型
self.cache_k = self.cache_k.to(xq)
self.cache_v = self.cache_v.to(xq)
# 更新緩存鍵和值
self.cache_k[:bsz, start_pos : start_pos + seqlen] = xk
self.cache_v[:bsz, start_pos : start_pos + seqlen] = xv
# 獲取鍵和值
keys = self.cache_k[:bsz, : start_pos + seqlen]
values = self.cache_v[:bsz, : start_pos + seqlen]
# 轉(zhuǎn)置xq、鍵和值的維度
xq = xq.transpose(1, 2)
keys = keys.transpose(1, 2)
values = values.transpose(1, 2)
# 計(jì)算注意力分?jǐn)?shù)
scores = torch.matmul(xq, keys.transpose(2, 3)) / math.sqrt(self.head_dim)
if mask is not None:
scores = scores + mask # (bs, n_local_heads, slen, cache_len + slen)
scores = F.softmax(scores.float(), dim=-1).type_as(xq)
# 使用注意力分?jǐn)?shù)加權(quán)求和得到輸出
output = torch.matmul(scores, values) # (bs, n_local_heads, slen, head_dim)
output = output.transpose(
1, 2
).contiguous().view(bsz, seqlen, -1)
# 應(yīng)用輸出投影
return self.wo(output)
然后是前饋網(wǎng)絡(luò)FFN部分,需要注意的點(diǎn)就是采用的激活函數(shù),以及激活函數(shù)的位置
import torch.nn as nn
import torch.nn.functional as F
class FeedForward(nn.Module):
def __init__(
self,
dim: int,
hidden_dim: int,
multiple_of: int,
):
super().__init__()
# 初始化隱藏層的維度為輸入維度的2/3
hidden_dim = int(2 * hidden_dim / 3)
# 調(diào)整隱藏層維度為multiple_of的倍數(shù)
hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)
# 第一個(gè)線性層
self.w1 = ColumnParallelLinear(
dim, hidden_dim, bias=False, gather_output=False, init_method=lambda x: x
)
# 第二個(gè)線性層
self.w2 = RowParallelLinear(
hidden_dim, dim, bias=False, input_is_parallel=True, init_method=lambda x: x
)
# 第三個(gè)線性層
self.w3 = ColumnParallelLinear(
dim, hidden_dim, bias=False, gather_output=False, init_method=lambda x: x
)
def forward(self, x):
# 前向傳播函數(shù)
return self.w2(F.silu(self.w1(x)) * self.w3(x))
這里與常見模型中的FFN做一下簡單的對比
- BART中的FFN,用的是fc->act->fc,用了兩層全連接
- GPT中的FFN,用的是conv1D->act->conv1D,也是只用了兩層
- 而LLaMA中的FFN采用了三個(gè)全連接層以實(shí)現(xiàn)FFNSwiGLU,即
然后將SA和FFN這兩部分拼在一起就是一個(gè)transformer block
import torch
import torch.nn as nn
from typing import Optional
class TransformerBlock(nn.Module):
def __init__(self, layer_id: int, args: ModelArgs):
super().__init__()
# 初始化參數(shù)
self.n_heads = args.n_heads # 注意力頭的數(shù)量
self.dim = args.dim # 模型維度
self.head_dim = args.dim // args.n_heads # 每個(gè)注意力頭的維度
self.attention = Attention(args) # 注意力機(jī)制模塊
self.feed_forward = FeedForward(
dim=args.dim, hidden_dim=4 * args.dim, multiple_of=args.multiple_of
) # 前饋神經(jīng)網(wǎng)絡(luò)模塊
self.layer_id = layer_id # 當(dāng)前層的ID
self.attention_norm = RMSNorm(args.dim, eps=args.norm_eps) # 注意力模塊的歸一化
self.ffn_norm = RMSNorm(args.dim, eps=args.norm_eps) # 前饋神經(jīng)網(wǎng)絡(luò)模塊的歸一化
def forward(self, x: torch.Tensor, start_pos: int, freqs_cis: torch.Tensor, mask: Optional[torch.Tensor]):
# 輸入x經(jīng)過self-attention之后,做Add&Norm
h = x + self.attention.forward(self.attention_norm(x), start_pos, freqs_cis, mask)
# 上一步的輸出h作為輸入,經(jīng)過前饋神經(jīng)網(wǎng)絡(luò)Feed forward之后,做Add&Norm
out = h + self.feed_forward.forward(self.ffn_norm(h))
return out
最后利用torch的module list將transformer block進(jìn)行堆疊,拼上最前頭的embedding部分,就是一個(gè)完整的transformer decoder結(jié)構(gòu)了
import torch
import torch.nn as nn
from typing import Optional
class Transformer(nn.Module):
def __init__(self, params: ModelArgs):
super().__init__()
# 初始化參數(shù)
self.params = params
self.vocab_size = params.vocab_size # 詞匯表大小
self.n_layers = params.n_layers # Transformer模型的層數(shù)
# 詞嵌入層
self.tok_embeddings = ParallelEmbedding(
params.vocab_size, params.dim, init_method=lambda x: x
)
# Transformer的各個(gè)層
self.layers = torch.nn.ModuleList()
for layer_id in range(params.n_layers):
self.layers.append(TransformerBlock(layer_id, params))
# 歸一化層
self.norm = RMSNorm(params.dim, eps=params.norm_eps)
# 輸出層
self.output = ColumnParallelLinear(
params.dim, params.vocab_size, bias=False, init_method=lambda x: x
)
# 預(yù)計(jì)算的頻率矩陣
self.freqs_cis = precompute_freqs_cis(
self.params.dim // self.params.n_heads, self.params.max_seq_len * 2
)
@torch.inference_mode()
def forward(self, tokens: torch.Tensor, start_pos: int):
_bsz, seqlen = tokens.shape
# Token嵌入和位置編碼
h = self.tok_embeddings(tokens)
self.freqs_cis = self.freqs_cis.to(h.device)
freqs_cis = self.freqs_cis[start_pos : start_pos + seqlen]
# 生成上三角的mask矩陣(為decoder模型防止標(biāo)簽泄漏)
mask = None
if seqlen > 1:
mask = torch.full((1, 1, seqlen, seqlen), float("-inf"), device=tokens.device)
mask = torch.triu(mask, diagonal=start_pos + 1).type_as(h)
# 逐層計(jì)算Transformer
for layer in self.layers:
h = layer(h, start_pos, freqs_cis, mask)
h = self.norm(h)
output = self.output(h[:, -1, :]) # 只計(jì)算最后一個(gè)位置的logits
return output.float()
接著看下生成過程,如下:
- 對prompts進(jìn)行tokenize,得到token ids;
- 計(jì)算當(dāng)前batch的最大長度total_len,用來創(chuàng)建輸入的token tensor,最大長度不能超過前文所述緩存的大??;
- 從當(dāng)前batch中,最短的一個(gè)prompt的位置,作為生成的開始位置,開始生成;
- 輸入的token tensor傳入transformer模型,計(jì)算logits,得到形狀為(batch_size, hidden_size)的logits(transformer最后一層的輸出);
- softmax+top_p采樣,得到當(dāng)前預(yù)測的token,并更新當(dāng)前位置,準(zhǔn)備預(yù)測下一個(gè)token;
- 解碼得到生成的文本
代碼如下
class LLaMA:
def __init__(self, model: Transformer, tokenizer: Tokenizer):
self.model = model
self.tokenizer = tokenizer
def generate(
self,
prompts: List[str],
max_gen_len: int,
temperature: float = 0.8,
top_p: float = 0.95,
) -> List[str]:
# 獲取批處理大小
bsz = len(prompts)
# 獲取模型參數(shù)
params = self.model.params
# 檢查批處理大小是否在允許的最大批處理大小范圍內(nèi)
assert bsz <= params.max_batch_size, (bsz, params.max_batch_size)
# 使用分詞器對提示進(jìn)行編碼為標(biāo)記
prompt_tokens = [self.tokenizer.encode(x, bos=True, eos=False) for x in prompts]
# 查找提示標(biāo)記的最小和最大大小
min_prompt_size = min([len(t) for t in prompt_tokens])
max_prompt_size = max([len(t) for t in prompt_tokens])
# 計(jì)算要生成的標(biāo)記的總長度
total_len = min(params.max_seq_len, max_gen_len + max_prompt_size)
# 創(chuàng)建一個(gè)張量來存儲生成的標(biāo)記,填充為填充標(biāo)記
tokens = torch.full((bsz, total_len), self.tokenizer.pad_id).cuda().long()
# 將提示標(biāo)記復(fù)制到標(biāo)記張量中
for k, t in enumerate(prompt_tokens):
tokens[k, : len(t)] = torch.tensor(t).long()
# 創(chuàng)建一個(gè)掩碼以識別輸入文本
input_text_mask = tokens != self.tokenizer.pad_id
# 設(shè)置生成的起始位置
start_pos = min_prompt_size
prev_pos = 0
# 逐個(gè)生成標(biāo)記
for cur_pos in range(start_pos, total_len):
# 通過模型進(jìn)行前向傳遞以獲取logits
logits = self.model.forward(tokens[:, prev_pos:cur_pos], prev_pos)
if temperature > 0:
# 對logits應(yīng)用溫度并計(jì)算概率
probs = torch.softmax(logits / temperature, dim=-1)
# 使用top-p采樣抽樣下一個(gè)標(biāo)記
next_token = sample_top_p(probs, top_p)
else:
# 選擇概率最高的標(biāo)記
next_token = torch.argmax(logits, dim=-1)
next_token = next_token.reshape(-1)
# 只有在已經(jīng)生成了提示的情況下才替換標(biāo)記
next_token = torch.where(
input_text_mask[:, cur_pos], tokens[:, cur_pos], next_token
)
tokens[:, cur_pos] = next_token
prev_pos = cur_pos
# 將生成的標(biāo)記解碼為文本
decoded = []
for i, t in enumerate(tokens.tolist()):
# 將標(biāo)記截?cái)嗟阶畲笊砷L度
t = t[: len(prompt_tokens[i]) + max_gen_len]
# 將標(biāo)記截?cái)嗟饺绻嬖诮Y(jié)束標(biāo)記
try:
t = t[: t.index(self.tokenizer.eos_id)]
except ValueError:
pass
# 將標(biāo)記解碼為文本
decoded.append(self.tokenizer.decode(t))
return decoded
def sample_top_p(probs, p):
# 按降序?qū)Ω怕蔬M(jìn)行排序
probs_sort, probs_idx = torch.sort(probs, dim=-1, descending=True)
# 計(jì)算概率的累積和
probs_sum = torch.cumsum(probs_sort, dim=-1)
# 創(chuàng)建一個(gè)掩碼以過濾累積概率超過p的標(biāo)記
mask = probs_sum - probs_sort > p
# 將被過濾的標(biāo)記的概率設(shè)置為0
probs_sort[mask] = 0.0
# 歸一化概率
probs_sort.div_(probs_sort.sum(dim=-1, keepdim=True))
# 使用修改后的概率進(jìn)行抽樣下一個(gè)標(biāo)記
next_token = torch.multinomial(probs_sort, num_samples=1)
# 收集抽樣標(biāo)記的原始索引
next_token = torch.gather(probs_idx, -1, next_token)
return next_token
1.3 LLaMA的Optimizer設(shè)計(jì)、模型加速優(yōu)化與微型版本
在Optimizer設(shè)計(jì)上
- 該模型使用AdamW優(yōu)化器(Loshchilov和Hutter,2017)進(jìn)行訓(xùn)練,超參數(shù)設(shè)置為β1=0.9,β2=0.95
此外,使用余弦學(xué)習(xí)率方式,使最終學(xué)習(xí)率等于最大學(xué)習(xí)率的10%,以及使用0.1的權(quán)重衰減和1.0的梯度剪裁,和2000個(gè)warm up策略,使得可以根據(jù)模型的大小改變學(xué)習(xí)率和批次大小
在模型的加速優(yōu)化方面
- 首先,使用一個(gè)高效的因果多頭注意力方式的實(shí)現(xiàn),靈感來自Rabe和Staats(2021)以及Dao等人(2022),這個(gè)實(shí)現(xiàn)可在xformers庫中找到,可以有效減少內(nèi)存的使用和計(jì)算
具體原理為通過不存儲注意力權(quán)重和不計(jì)算由于語言建模任務(wù)的因果性質(zhì)而被掩蓋的鍵/查詢分?jǐn)?shù)來實(shí)現(xiàn)的 - 其次,為了進(jìn)一步提高訓(xùn)練效率,減少了在check point的后向傳遞中重新計(jì)算的激活量,在實(shí)現(xiàn)上,通過手動實(shí)現(xiàn)trasnformer層的后向函數(shù)來進(jìn)行操作
為了充分受益于這種優(yōu)化,還通過如Korthikanti等人(2022)中采用的方法,進(jìn)行使用模型和序列并行來減少模型的內(nèi)存使用 - 最后,該工作還盡可能地重疊激活的計(jì)算和GPU之間在網(wǎng)絡(luò)上的通信
最終的優(yōu)化性能效果為:當(dāng)訓(xùn)練一個(gè)65B參數(shù)的模型時(shí),代碼在2048A100的GPU上處理大約380個(gè)token/秒/GPU,并耗費(fèi)80GB的內(nèi)存,這意味著對包含1.4Ttoken的數(shù)據(jù)集進(jìn)行訓(xùn)練大約花費(fèi)了21天
LLaMA發(fā)布不久后,一些研究者基于它做了不少工作
- 一開始最小參數(shù)7B的模型也需要近30GB的GPU才能運(yùn)行,但通過比特和字節(jié)庫進(jìn)行浮點(diǎn)優(yōu)化,能夠讓模型在單個(gè)NVIDIA RTX 3060(顯存一般12G)上運(yùn)行
- 之后,GitHub 上的一名研究人員甚至能夠在Ryzen 7900X CPU上運(yùn)行LLM的7B 版本,每秒能推斷出幾個(gè)單詞
- 再之后,有研究者推出了llama.cpp,無需 GPU,就能運(yùn)行 LLaMA
llama.cpp 項(xiàng)目實(shí)現(xiàn)了在MacBook上運(yùn)行 LLaMA,還有開發(fā)者成功的在 4GB RAM 的樹莓派上運(yùn)行了 LLaMA 7B
第二部分 各種微調(diào)LLaMA:Alpaca(self-instruct)、Vicuna(shareGPT)、BELLE(self-instruct)
2.1 Stanford Alpaca:結(jié)合英文語料通過Self Instruct方式微調(diào)LLaMA 7B
2.1.1 Stanford Alpaca簡介:論文、代碼、數(shù)據(jù)
3月中旬,斯坦福的Rohan Taori等人發(fā)布Alpaca(中文名:羊駝):號稱只花100美元,人人都可微調(diào)Meta家70億參數(shù)的LLaMA大模型(即LLaMA 7B),具體做法是通過52k指令數(shù)據(jù),然后在8個(gè)80GB A100上訓(xùn)練3個(gè)小時(shí),使得Alpaca版的LLaMA 7B在單純對話上的性能比肩GPT-3.5(text-davinci-003),這便是指令調(diào)優(yōu)LLaMA的意義所在
- 論文《Alpaca: A Strong Open-Source Instruction-Following Model》
- GitHub地址:https://github.com/tatsu-lab/stanford_alpaca
- 數(shù)據(jù)地址 (即斯坦福團(tuán)隊(duì)微調(diào)LLaMA 7B所用的52K英文指令數(shù)據(jù)):https://raw.githubusercontent.com/tatsu-lab/stanford_alpaca/main/alpaca_data.json
有意思的是,后來不斷有人把這52K的英文指令數(shù)據(jù)翻譯了下,比如:
單純翻譯的斯坦福52K中文指令數(shù)據(jù)
斯坦福52K中文指令數(shù)據(jù)(語句上做了中文表達(dá)風(fēng)格的意譯)
這52K數(shù)據(jù)所對應(yīng)的alpaca_data.json文件是一個(gè)字典列表,每個(gè)字典包含以下字段:
- instruction: str,描述了模型應(yīng)該執(zhí)行的任務(wù),52K 條指令中的每一條都是唯一的
- input: str,要么是上下文,要么直接輸入(optional context or input for the task),例如,當(dāng)指令是“總結(jié)以下文章”時(shí),輸入就是文章,大約 40% 的示例有輸入
- output: str,由GPT3.5對應(yīng)的API即 text-davinci-003生成的指令的答案
2.1.2 什么是self-instruct方式:提示GPT3/GPT3.5/GPT4的API收集數(shù)據(jù)
而這52K數(shù)據(jù)是怎么來的呢?實(shí)際上,是通過Self-Instruct『Self-Instruct是來自華盛頓大學(xué)Yizhong Wang等人于22年12月通過這篇論文《SELF-INSTRUCT: Aligning Language Model with Self Generated Instructions》提出的,這是其論文地址、代碼地址』提示GPT3的API拿到的
?具體而言,論文中提出
- 人工設(shè)計(jì)175個(gè)任務(wù),每個(gè)任務(wù)都有對應(yīng)的{指令 輸入 輸出/實(shí)例}或{指令 輸出/實(shí)例},將這175個(gè)任務(wù)數(shù)據(jù)作為種子集
比如這是斯坦福Alpaca的175個(gè)種子數(shù)據(jù):stanford_alpaca/seed_tasks.jsonl at main · tatsu-lab/stanford_alpaca · GitHub
{"id": "seed_task_0", "name": "breakfast_suggestion",
"instruction": "Is there anything I can eat for a breakfast that doesn't include eggs, yet includes protein, and has roughly 700-1000 calories?",
"instances": [{"input": "", "output": "Yes, you can have 1 oatmeal banana protein shake and 4 strips of bacon. The oatmeal banana protein shake may contain 1/2 cup oatmeal, 60 grams whey protein powder, 1/2 medium banana, 1tbsp flaxseed oil and 1/2 cup watter, totalling about 550 calories. The 4 strips of bacon contains about 200 calories."}],
"is_classification": false}
{"id": "seed_task_1", "name": "antonym_relation",
"instruction": "What is the relation between the given pairs?",
"instances": [{"input": "Night : Day :: Right : Left", "output": "The relation between the given pairs is that they are opposites."}],
"is_classification": false} - 然后提示模型比如GPT3對應(yīng)的API即 text-davinci-001 (原論文中沒用text-davinci-003,because their newer engines are trained with the latest user data and are likely to already see the SUPERNI evaluation set,但實(shí)際應(yīng)用時(shí)比如斯坦福Alpaca指定的GPT3.5的API即 text-davinci-003生成指令,包括很快你將看到,23年4月還有微軟的研究者指定GPT4的API生成指令),使用種子集作為上下文示例來生成更多新的指令
- 對該模型生成的指令判斷是否分類任務(wù)
- 使用模型生成實(shí)例
- 對上述模型生成的數(shù)據(jù){指令 輸入 輸出/實(shí)例}過濾掉低質(zhì)量或相似度高的
- 將經(jīng)過過濾和后處理的數(shù)據(jù)添加到種子池中
一直重復(fù)上述2-6步直到種子池有足夠多的數(shù)據(jù)
2.1.3 生成微調(diào)LLaMA的52K數(shù)據(jù)的完整代碼
斯坦福的Alpaca在實(shí)際生成52K數(shù)據(jù)時(shí),在上節(jié)self-instruct方式的基礎(chǔ)上,還考慮到了多重過濾機(jī)制,防止生成過于相似、過長或含有特定關(guān)鍵詞的指令,以此保證生成的指令集的質(zhì)量和多樣性,且在每一輪生成指令后,都會保存當(dāng)前的結(jié)果,方便隨時(shí)跟蹤進(jìn)度,此外,還采用了多進(jìn)程處理,提高了效率
故最終完整生成52K數(shù)據(jù)的完整代碼如下(來源于:https://github.com/tatsu-lab/stanford_alpaca/blob/main/generate_instruction.py,且為方便理解,給每一行代碼都逐行加上了中文注釋)
"""
batch_selfinstruct_generate.py
運(yùn)行:
python -m generate_instruction generate_instruction_following_data \
--output_dir ./ \
--num_instructions_to_generate 10 \
--model_name="text-davinci-003" \
"""
import time # 引入時(shí)間模塊
import json # 引入json模塊
import os # 引入os模塊
import random # 引入隨機(jī)數(shù)模塊
import re # 引入正則表達(dá)式模塊
import string # 引入字符串模塊
from functools import partial # 引入偏函數(shù)模塊
from multiprocessing import Pool # 引入多進(jìn)程模塊
import numpy as np # 引入Numpy庫
import tqdm # 引入tqdm庫,用于進(jìn)度條顯示
from rouge_score import rouge_scorer # 引入rouge評分器,用于文本相似度計(jì)算
import utils # 引入自定義的工具模塊
import fire # 引入fire庫,用于命令行參數(shù)解析
# 定義一個(gè)將多個(gè)提示指令編碼成單一字符串的函數(shù)
def encode_prompt(prompt_instructions):
prompt = open("./prompt.txt").read() + "\n" # 打開并讀取提示文本文件
# 遍歷提示指令,將其格式化并附加到提示字符串中
for idx, task_dict in enumerate(prompt_instructions):
(instruction, input, output) = task_dict["instruction"], task_dict["input"], task_dict["output"]
instruction = re.sub(r"\s+", " ", instruction).strip().rstrip(":") # 對指令進(jìn)行清洗
input = "<noinput>" if input.lower() == "" else input # 若無輸入則標(biāo)注為"<noinput>"
# 格式化并添加指令、輸入和輸出到提示中
prompt += f"###\n"
prompt += f"{idx + 1}. Instruction: {instruction}\n"
prompt += f"{idx + 1}. Input:\n{input}\n"
prompt += f"{idx + 1}. Output:\n{output}\n"
prompt += f"###\n"
prompt += f"{idx + 2}. Instruction:" # 添加下一個(gè)指令的前綴
return prompt # 返回提示字符串
# 定義一個(gè)對GPT-3響應(yīng)進(jìn)行后處理的函數(shù),抽取生成的新指令
def post_process_gpt3_response(num_prompt_instructions, response):
if response is None: # 如果響應(yīng)為空,則返回空列表
return []
raw_instructions = f"{num_prompt_instructions+1}. Instruction:" + response["text"] # 獲取原始的指令文本
raw_instructions = re.split("###", raw_instructions) # 根據(jù)"###"切分原始指令
instructions = [] # 初始化指令列表
# 對每個(gè)切分出的原始指令進(jìn)行處理
for idx, inst in enumerate(raw_instructions):
# 如果解碼由于長度停止,最后一個(gè)示例可能被截?cái)?,因此我們丟棄它
if idx == len(raw_instructions) - 1 and response["finish_reason"] == "length":
continue
idx += num_prompt_instructions + 1
# 根據(jù)索引和"Instruction", "Input", "Output"關(guān)鍵字進(jìn)行切分
splitted_data = re.split(f"{idx}\.\s+(Instruction|Input|Output):", inst)
if len(splitted_data) != 7: # 如果切分結(jié)果不等于7,則繼續(xù)下一輪循環(huán)
continue
else:
# 提取指令、輸入、輸出
inst = splitted_data[2].strip()
input = splitted_data[4].strip()
input = "" if input.lower() == "<noinput>" else input # 對輸入進(jìn)行處理,如果是"<noinput>",則替換為空字符串
output = splitted_data[6].strip()
# 過濾掉太短或太長的指令
if len(inst.split()) <= 3 or len(inst.split()) > 150:
continue
# 根據(jù)不適合語言模型的關(guān)鍵詞進(jìn)行過濾
blacklist = [
"image",
"images",
"graph",
"graphs",
"picture",
"pictures",
"file",
"files",
"map",
"maps",
"draw",
"plot",
"go to",
"video",
"audio",
"music",
"flowchart",
"diagram",
]
# 如果指令中存在黑名單中的詞,則忽略該指令
if any(find_word_in_string(word, inst) for word in blacklist):
continue
# 模型傾向于為一些現(xiàn)有指令添加"編寫程序",這會導(dǎo)致很多這樣的指令。
# 這里過濾掉這類指令
if inst.startswith("Write a program"):
continue
# 過濾那些以標(biāo)點(diǎn)符號開始的指令
if inst[0] in string.punctuation:
continue
# 過濾那些以非英語字符開始的指令
if not inst[0].isascii():
continue
# 將處理后的指令添加到指令列表中
instructions.append({"instruction": inst, "input": input, "output": output})
return instructions # 返回指令列表
# 定義一個(gè)在字符串中查找單詞的函數(shù)
def find_word_in_string(w, s):
return re.compile(r"\b({0})\b".format(w), flags=re.IGNORECASE).search(s)
# 定義一個(gè)生成指令的函數(shù)
def generate_instruction_following_data(
output_dir="./",
seed_tasks_path="./seed_tasks.jsonl",
num_instructions_to_generate=100,
model_name="text-davinci-003",
num_prompt_instructions=3,
request_batch_size=5,
temperature=1.0,
top_p=1.0,
num_cpus=16,
):
seed_tasks = [json.loads(l) for l in open(seed_tasks_path, "r")] # 讀取并解析種子任務(wù)
# 從種子任務(wù)中提取指令、輸入和輸出
seed_instruction_data = [
{"instruction": t["instruction"], "input": t["instances"][0]["input"], "output": t["instances"][0]["output"]}
for t in seed_tasks
]
print(f"Loaded {len(seed_instruction_data)} human-written seed instructions") # 打印加載的人工編寫的種子指令的數(shù)量
os.makedirs(output_dir, exist_ok=True) # 創(chuàng)建輸出目錄
request_idx = 0
# 加載LM生成的指令
machine_instruction_data = []
if os.path.exists(os.path.join(output_dir, "regen.json")):
machine_instruction_data = utils.jload(os.path.join(output_dir, "regen.json"))
print(f"Loaded {len(machine_instruction_data)} machine-generated instructions") # 打印加載的機(jī)器生成的指令的數(shù)量
# 初始化Rouge得分計(jì)算器
scorer = rouge_scorer.RougeScorer(["rougeL"], use_stemmer=False)
# 進(jìn)度條,總數(shù)為要生成的指令數(shù)量
progress_bar = tqdm.tqdm(total=num_instructions_to_generate)
if machine_instruction_data:
progress_bar.update(len(machine_instruction_data)) # 如果已有機(jī)器生成的指令,則更新進(jìn)度條
# 首先,我們對所有的種子指令和生成的機(jī)器指令進(jìn)行標(biāo)記
all_instructions = [d["instruction"] for d in seed_instruction_data] + [
d["instruction"] for d in machine_instruction_data
]
all_instruction_tokens = [scorer._tokenizer.tokenize(inst) for inst in all_instructions]
# 當(dāng)機(jī)器指令數(shù)據(jù)的數(shù)量小于需要生成的指令數(shù)量時(shí),持續(xù)生成
while len(machine_instruction_data) < num_instructions_to_generate:
request_idx += 1 # 請求索引增加
batch_inputs = []
for _ in range(request_batch_size):
# 只從種子任務(wù)中采樣
prompt_instructions = random.sample(seed_instruction_data, num_prompt_instructions)
# 將多個(gè)提示指令編碼成一個(gè)字符串
prompt = encode_prompt(prompt_instructions)
batch_inputs.append(prompt) # 將編碼的指令添加到批輸入列表中
decoding_args = utils.OpenAIDecodingArguments(
temperature=temperature,
n=1,
max_tokens=3072, # 硬編碼以最大化長度。請求將自動調(diào)整
top_p=top_p,
stop=["\n20", "20.", "20."], # 當(dāng)出現(xiàn)這些字符串時(shí),生成停止
)
# 記錄請求開始的時(shí)間
request_start = time.time()
# 調(diào)用OpenAI API進(jìn)行批量生成
results = utils.openai_completion(
prompts=batch_inputs,
model_name=model_name,
batch_size=request_batch_size,
decoding_args=decoding_args,
logit_bias={"50256": -100}, # 阻止特定token被生成
)
request_duration = time.time() - request_start # 計(jì)算請求的時(shí)間
# 開始后處理生成的結(jié)果
process_start = time.time()
instruction_data = []
for result in results:
# 對每個(gè)結(jié)果進(jìn)行后處理,并獲取新的指令
new_instructions = post_process_gpt3_response(num_prompt_instructions, result)
instruction_data.extend(new_instructions)
process_duration = time.time() - process_start # 計(jì)算后處理的時(shí)間
# 更新進(jìn)度條
progress_bar.update(len(instruction_data))
print(
f"\nRequest {request_idx} took {request_duration:.2f} seconds, post-processing took {process_duration:.2f} seconds"
)
# 對每一條新指令進(jìn)行處理
for data in instruction_data:
inst = data["instruction"]
# 使用Rouge得分器對指令進(jìn)行標(biāo)記
inst_tokens = scorer._tokenizer.tokenize(inst)
# 計(jì)算新指令與已有指令的最大RougeL得分
max_rougeL = max(
[scorer.score(inst_tokens, old_inst_tokens)["rougeL"].fmeasure for old_inst_tokens in all_instruction_tokens]
)
# 如果RougeL得分大于0.5,則認(rèn)為該指令與已有指令過于相似,不予采納
if max_rougeL > 0.5:
continue
# 將新指令添加到已有指令列表和已有指令標(biāo)記列表中
all_instructions.append(inst)
all_instruction_tokens.append(inst_tokens)
# 將新指令添加到機(jī)器生成的指令數(shù)據(jù)中
machine_instruction_data.append(data)
# 將機(jī)器生成的指令數(shù)據(jù)保存到文件中
utils.jdump(machine_instruction_data, os.path.join(output_dir, "regen.json"))
progress_bar.close() # 關(guān)閉進(jìn)度條
print(f"Generated {len(machine_instruction_data)} instructions") # 打印生成的指令數(shù)量
# 隨機(jī)化并截取生成的指令數(shù)據(jù)
random.shuffle(machine_instruction_data)
machine_instruction_data = machine_instruction_data[:num_instructions_to_generate]
# 將指令數(shù)據(jù)轉(zhuǎn)化為任務(wù)格式
machine_tasks = []
for data in machine_instruction_data:
task = {
"id": utils.random_id(),
"input": data["input"],
"output": data["output"],
"rating": np.random.uniform(1, 5), # 給指令一個(gè)隨機(jī)的評分,代表指令的質(zhì)量
"instruction": data["instruction"],
}
machine_tasks.append(task)
# 保存機(jī)器生成的任務(wù)到文件中
utils.jdump(machine_tasks, os.path.join(output_dir, "regen_tasks.json"))
# 使用fire庫解析命令行參數(shù),并調(diào)用函數(shù)
if __name__ == "__main__":
fire.Fire(generate_instruction_following_data)
所以Alpaca,就是花了不到500美元使用OpenAI API生成了5.2萬個(gè)這樣的示例微調(diào)LLaMA搞出來的,個(gè)人覺得可以取名為?instructLLaMA-7B,^_^
2.1.4 微軟研究者提示GPT4的API生成指令數(shù)據(jù)
值得一提的是,后來23年4月有微軟的研究者提示GPT4的API進(jìn)行指令微調(diào)「論文地址:INSTRUCTION TUNING WITH GPT-4、GitHub地址:instruction-Tuning-with-GPT-4、項(xiàng)目地址:使用GPT4進(jìn)行指令調(diào)優(yōu)」,從而生成以下數(shù)據(jù)
- English Instruction-Following Data,generated by GPT-4 using Alpaca prompts
這部分?jǐn)?shù)據(jù)在項(xiàng)目文件?alpaca_gpt4_data.json?里,contains 52K instruction-following data generated by GPT-4 with prompts in Alpaca. This JSON file has the same format as Alpaca data, except the output is generated by GPT-4:
instruction: str, describes the task the model should perform. Each of the 52K instructions is unique.
input: str, optional context or input for the task.
output: str, the answer to the instruction as generated by GPT-4. - Chinese Instruction-Following Data,即上面英文數(shù)據(jù)的中文翻譯,存儲在項(xiàng)目文件alpaca_gpt4_data_zh.json 里
- Comparison Data ranked by GPT-4,好訓(xùn)練一個(gè)獎勵(lì)模型
存儲在?comparision_data.json?文件里,ranked responses from three models, including GPT-4, GPT-3.5 and OPT-IML by asking GPT-4 to rate the quality.
user_input: str, prompts used for quering LLMs.
completion_a: str, a model completion which is ranked higher than completion_b.
completion_b: str, a different model completion which has a lower quality score. - Answers on Unnatural Instructions Data,該數(shù)據(jù)用于大規(guī)模量化 GPT-4 與我們的指令調(diào)整模型(即LLaMA by instruction tuning with GPT4)之間的差距,而縮小與GPT4的差距便是本次指令調(diào)優(yōu)的目標(biāo)
2.1.5 手把手實(shí)戰(zhàn):Self-Instruct: Aligning LM with Self Generated Instructions
之前已說過,Self-Instruct是來自華盛頓大學(xué)Yizhong Wang等人于22年12月通過這篇論文《SELF-INSTRUCT: Aligning Language Model with Self Generated Instructions》提出的,這是其論文地址、代碼地址
為進(jìn)一步理解self-instruct這個(gè)方式的原理與實(shí)現(xiàn)細(xì)節(jié),我司杜老師把這個(gè)self-instruct的方式生成語料的過程實(shí)踐了下(這是該教程地址),具體而言,先理清如下4個(gè)步驟
-
Step1:通過模型生成新的指令
根據(jù)人工設(shè)計(jì)的175個(gè)任務(wù),每個(gè)任務(wù)都有對應(yīng)的(指令,輸入,輸出)或(指令,輸出);使用模型生成新的指令;
執(zhí)行的代碼文件為# 1. Generate instructions from the seed tasks ???????./scripts/generate_instructions.sh
-
Step2:對模型生成的指令進(jìn)行判斷(指令是否是一個(gè)分類任務(wù))
而判斷指令是否屬于分類任務(wù)的操作如下:在種子池中隨機(jī)挑選12條分類指令和19條非分類指令,然后加上新生成的指令
執(zhí)行的代碼文件為# 2. Identify whether the instruction represents a classification task or not ./scripts/is_clf_or_not.sh
-
Step3:根據(jù)Step2的判斷結(jié)果,給出不同的輸出
如果是分類任務(wù),就通過模型輸出 Class_label 和 Input(Output-first,即先輸出分類的標(biāo)簽,再輸出Input內(nèi)容)
如果不是分類任務(wù),就通過模型輸出 Input 和 Output(Input-first,即先輸出Input,再輸出Output)
執(zhí)行的代碼文件為# 3. Generate instances for each instruction ./scripts/generate_instances.sh
-
Step4:過濾及后處理
對上述模型生成的數(shù)據(jù)進(jìn)行過濾和后處理,將經(jīng)過過濾和后處理的數(shù)據(jù)添加到種子池中
且為了數(shù)據(jù)的多樣性,新生成的指令只有與種子池中的指令的 ROUGE-L 小于0.7時(shí)才會添加進(jìn)入種子池;
排除一些無法被語言模型處理的指令,比如涉及圖像、圖片、圖形的指令;
在給指令生成實(shí)例時(shí),會過濾掉輸入相同但是輸出不同的實(shí)例
執(zhí)行的代碼文件為# 4. Filtering, processing, and reformatting ./scripts/prepare_for_finetuning.sh
對于以上4個(gè)步驟進(jìn)行不斷循環(huán),直到種子池有足夠多的數(shù)據(jù)(通常會設(shè)定一個(gè)具體的參數(shù),比如:52000),生成過程停止
接下來,我們逐一寫代碼實(shí)現(xiàn)
正式編碼之前的一些準(zhǔn)備工作
1、首先將代碼下載到本地,下面兩種方式均可
- 使用 Download 下載zip文件
- git clone https://github.com/yizhongw/self-instruct.git
// 因在windows上操作的,所以無法執(zhí)行bash命令,故直接用python命令運(yùn)行
2、進(jìn)入conda環(huán)境(用的pytorch這個(gè)環(huán)境) ,安裝相關(guān)的包
cd self-instruct-main
pip install -r requirements.txt
-
Step1 通過模型生成新的指令
先看下原始人工標(biāo)注的175種子數(shù)據(jù)的樣式,共包含4個(gè)部分,id,name,instruction,is_classification
本次只是實(shí)驗(yàn),故將scripts/generate_instructions.sh中的50000改為100(這樣產(chǎn)生的費(fèi)用也較少){ ?? ?"id": "seed_task_0",? ?? ?"name": "breakfast_suggestion",? ?? ?"instruction": "Is there anything I can eat for a breakfast that doesn't include eggs, yet includes protein, and has roughly 700-1000 calories?", "instances": [{"input": "", "output": "Yes, you can have 1 oatmeal banana protein shake and 4 strips of bacon. The oatmeal banana protein shake may contain 1/2 cup oatmeal, 60 grams whey protein powder, 1/2 medium banana, 1tbsp flaxseed oil and 1/2 cup watter, totalling about 550 calories. The 4 strips of bacon contains about 200 calories."}],? ?? ?"is_classification": false ?? ?}
運(yùn)行命令如下:
大概需要4分半的時(shí)間,生成100條數(shù)據(jù),它們會寫入data/ceishi/machine_generated_instructions.jsonl中,最終生成了122條,這些數(shù)據(jù)是通過LLM生成的與種子任務(wù)關(guān)聯(lián)度比較弱的一些任務(wù)描述(一些相似度高的就刪除了)python self_instruct/bootstrap_instructions.py --batch_dir data/ceshi --num_instructions_to_generate 100 --seed_tasks_path data/seed_tasks.jsonl --engine "davinci" --api_key "自己的openai API"
從下面的代碼中可以看出,最后寫入文件時(shí),一共包含了以下5個(gè)部分:instruction、most_similar、avg_similarity_score、metadata、request_idx
實(shí)際生成指令時(shí),分兩步:fout.write(json.dumps({ "instruction": inst, "most_similar": most_similar_instructions, "avg_similarity_score": float(np.mean(rouge_scores)), "metadata": metadata, "request_idx": request_idx }) + "\n")
? 第一步 先從種子池中隨機(jī)抽取6個(gè)人工編寫的指令,再隨機(jī)抽取2個(gè)模型生成的指令,總共8個(gè)指令,為何是8?其實(shí)可以自定義,比如默認(rèn)為8:https://github.com/yizhongw/self-instruct/blob/0b26ccaa415992100fa32df62d41b994cf928e23/self_instruct/bootstrap_instructions.py#L106(最開始的時(shí)候,是沒有模型生成的指令,因此是會直接從種子池中隨機(jī)抽取8條人工編寫的指令)
? 第二步 按照指定模版格式組織之后,輸入給模型,讓模型輸出一個(gè)新的指令parser.add_argument( "--num_prompt_instructions", type=int, default=8, help="The number of instructions to use in the prompt." )
最終,生成數(shù)據(jù)的核心代碼如下:
其中,對不同類型的數(shù)據(jù)需要構(gòu)建不同的 prompt 數(shù)據(jù)(如:是分類數(shù)據(jù),不是分類數(shù)據(jù)),構(gòu)建方式在函數(shù)encode_prompt中# load the LM-generated instructions,使用生成模型得到新的100條 instruction 提示 machine_instructions = [] # 開始生成100條instruction提示數(shù)據(jù) # 使用文件操作打開一個(gè)文件,該文件位于指定的批處理目錄中 # 文件名為"machine_generated_instructions.jsonl",以追加模式打開,然后把文件對象賦值給fout with open(os.path.join(args.batch_dir, "machine_generated_instructions.jsonl"), "a") as fout: # 進(jìn)入循環(huán),當(dāng)生成模型產(chǎn)生的指令數(shù)量未達(dá)到用戶指定的數(shù)量時(shí),繼續(xù)產(chǎn)生新的指令 while len(machine_instructions) < args.num_instructions_to_generate: # 初始化一個(gè)列表,用于保存批處理的輸入數(shù)據(jù) batch_inputs = [] # args.request_batch_size為5 # 循環(huán)指定的批處理大小的次數(shù),每次循環(huán)都會產(chǎn)生一條新的指令 for _ in range(args.request_batch_size): # 調(diào)用函數(shù)從生成模型中抽樣生成指令,這里選擇的指令數(shù)量為2,然后將生成的指令保存到變量prompt_instructions prompt_instructions = sample_machine_instructions( machine_instructions, similarities=None, n=2) ''' sample human instructions from the pool 從默認(rèn)的175條中選再選6條seed_instructions,加上上面使用LLM最初生成的2條prompt_instructions,相當(dāng)于一共選了8條 (最開始的時(shí)候,machine_instructions為空,因此會直接從175條中直接選8條) ''' prompt_instructions += random.sample(seed_instructions, args.num_prompt_instructions - len(prompt_instructions)) # 對這8條指令進(jìn)行隨機(jī)排序 random.shuffle(prompt_instructions) # 將這8條指令編碼成模型可以接收的輸入格式,然后保存到變量prompt prompt = encode_prompt(prompt_instructions, classification=args.use_clf_seed_tasks_only) # 將編碼后的輸入添加到批處理的輸入數(shù)據(jù)列表中 batch_inputs.append(prompt) # 調(diào)用函數(shù)使用GPT-3引擎對批處理的輸入數(shù)據(jù)進(jìn)行處理,處理的參數(shù)包括最大的輸出詞匯數(shù)量、輸出的隨機(jī)性、輸出結(jié)果的頂部概率等 results = make_gpt3_requests( engine=args.engine, prompts=batch_inputs, max_tokens=1024, temperature=0.7, top_p=0.5, frequency_penalty=0, presence_penalty=2, stop_sequences=["\n\n", "\n16", "16.", "16 ."], logprobs=1, n=1, best_of=1, api_key=args.api_key, organization=args.organization, )
# 構(gòu)建prompt數(shù)據(jù),針對是否分類分別構(gòu)建不同的prompt數(shù)據(jù) # 定義一個(gè)函數(shù),該函數(shù)用于將多個(gè)提示指令編碼成一個(gè)字符串 # 該函數(shù)接受兩個(gè)參數(shù),第一個(gè)參數(shù)是提示指令列表,第二個(gè)參數(shù)表示是否是分類任務(wù),是=>輸出優(yōu)先,否=>輸入優(yōu)先,對應(yīng)的 prompt_instructions/prompt_instances 不一樣 def encode_prompt(prompt_instructions, classification=False): """Encode multiple prompt instructions into a single string.""" # 如果當(dāng)前任務(wù)是分類任務(wù),那么設(shè)置提示信息為一個(gè)固定的字符串 if classification: # 這個(gè)提示信息是引導(dǎo)用戶生成一系列的分類任務(wù),如果可能的話,要求用戶明確指定可能的輸出標(biāo)簽 prompt = "Referring to a series of classification tasks, generate 8 more new tasks. Try to specify the possible output labels when possible.\n" # 如果當(dāng)前任務(wù)不是分類任務(wù),那么設(shè)置提示信息為另一個(gè)固定的字符串 else: # 這個(gè)提示信息是引導(dǎo)用戶生成一系列的任務(wù) prompt = "Referring to these eight tasks, generate 8 more new tasks:\n" # 循環(huán)處理每一條提示指令 for idx, instruction in enumerate(prompt_instructions): # 使用正則表達(dá)式將指令中的多余空格替換為單個(gè)空格,并去掉前后的空格以及末尾的冒號 instruction = re.sub(r"\s+", " ", instruction).strip().rstrip(":") # 將處理后的指令添加到提示信息中,注意指令前面需要添加序號 prompt += f"{idx+1}. {instruction}\n" # 在所有指令之后添加一個(gè)空白的序號,這個(gè)序號是接下來用戶需要填寫的新任務(wù)的序號 prompt += f"{len(prompt_instructions) + 1}." # 返回編碼后的提示信息 return prompt
-
Step2 對模型生成的指令進(jìn)行判斷
判斷是否是分類任務(wù)
會寫入data/ceishi/is_clf_or_not_davinci_template_1.jsonl中 (如上說的122條)python self_instruct/identify_clf_or_not.py --batch_dir data/ceshi --engine "davinci" --request_batch_size 5 --api_key "自己的openai API"
內(nèi)容包括:
核心代碼如下:{"instruction": "Find the largest number in this list.", "is_classification": " Yes"} {"instruction": "What is the first name of your favorite actor?", "is_classification": " No"} {"instruction": "Give me the number of distinct elements in this set.", "is_classification": " Yes"} {"instruction": "Give me the top 5 countries that are exporting tea.", "is_classification": " Yes"}
# 執(zhí)行輸出過程 # 使用文件操作打開一個(gè)輸出文件,然后把文件對象賦值給fout with open(output_path, "w") as fout: # 迭代輸入的數(shù)據(jù)行,步長為request_batch_size for batch_idx in range(0, len(lines), args.request_batch_size): # 對每個(gè)批次,將批次中的數(shù)據(jù)行轉(zhuǎn)換為JSON對象 batch = [json.loads(line) for line in lines[batch_idx: batch_idx + args.request_batch_size]] # 檢查批次中的所有指令是否都在已存在的請求中 if all(d["instruction"] in existing_requests for d in batch): # 如果都在,則直接從已存在的請求中獲取數(shù)據(jù),并寫入到輸出文件中 for d in batch: data = existing_requests[d["instruction"]] data = OrderedDict( (k, data[k]) for k in \ ["instruction", "is_classification"] ) fout.write(json.dumps(data, ensure_ascii=False) + "\n") else: # 如果不都在,那么需要使用GPT-3引擎生成數(shù)據(jù) # 首先構(gòu)造一個(gè)提示,這個(gè)提示包含前綴和指令 # prefix = compose_prompt_prefix(human_written_tasks, batch[0]["instruction"], 8, 2) prefix = templates[args.template] prompts = [prefix + " " + d["instruction"].strip() + "\n" + "Is it classification?" for d in batch] # 調(diào)用函數(shù)使用GPT-3引擎對批處理的輸入數(shù)據(jù)進(jìn)行處理 # 處理的參數(shù)包括最大的輸出詞匯數(shù)量、輸出的隨機(jī)性、輸出結(jié)果的頂部概率等 results = make_gpt3_requests( engine=args.engine, prompts=prompts, max_tokens=3, temperature=0, top_p=0, frequency_penalty=0, presence_penalty=0, stop_sequences=["\n", "Task"], logprobs=1, n=1, best_of=1, api_key=args.api_key, organization=args.organization) # 將結(jié)果寫入到輸出文件中 for i in range(len(batch)): data = batch[i] # 如果結(jié)果存在,則將結(jié)果中的"is_classification"字段保存到數(shù)據(jù)中 if results[i]["response"] is not None: data["is_classification"] = results[i]["response"]["choices"][0]["text"] else: # 如果結(jié)果不存在,則將"is_classification"字段設(shè)置為空 data["is_classification"] = "" # 構(gòu)造一個(gè)字典,包含指令和"is_classification"字段 data = { "instruction": data["instruction"], "is_classification": data["is_classification"] } # 對字典進(jìn)行排序,然后將字典轉(zhuǎn)換為JSON字符串,并寫入到輸出文件中 data = OrderedDict( (k, data[k]) for k in \ ["instruction", "is_classification"] ) fout.write(json.dumps(data, ensure_ascii=False) + "\n")
-
Step3:根據(jù)Step2的判斷結(jié)果,給出不同的輸出
如果遇到以下報(bào)錯(cuò):python self_instruct/generate_instances.py --batch_dir data/ceshi --input_file machine_generated_instructions.jsonl --output_file machine_generated_instances.jsonl --max_instances_to_gen 5 --engine "davinci" --request_batch_size 5 --api_key "自己的openai API"
UnicodeDecodeError: ‘gbk’ codec can’t decode byte 0x9d in position 6169: illegal multibyte sequence
解決方法:
在open函數(shù)中添加encoding='utf-8’即可
運(yùn)行后會將結(jié)果寫入 data/ceishi/machine_generated_instances.jsonl中。每條數(shù)據(jù)包含5部分:“instruction”, “raw_instances”, “instance_metadata”, “instruction_metadata”, “most_similar”, “avg_similarity_score”
核心代碼如下『這段核心代碼與上面step2最后的核心代碼的區(qū)別在于:上段代碼的重點(diǎn)是確定任務(wù)是否為分類任務(wù),這段代碼的重點(diǎn)是根據(jù)任務(wù)類型(分類或生成)生成任務(wù)實(shí)例?』:# 使用文件操作打開一個(gè)輸出文件,以utf-8的編碼格式,然后把文件對象賦值給fout with open(output_path, "w", encoding='utf-8') as fout: # 迭代任務(wù)數(shù)據(jù),步長為request_batch_size for batch_idx in range(0, len(tasks), args.request_batch_size): # 獲取當(dāng)前批次的任務(wù) batch = tasks[batch_idx: batch_idx + args.request_batch_size] # 檢查批次中的所有指令是否都在已存在的請求中 if all(d["instruction"] in existing_requests for d in batch): # 如果都在,則直接從已存在的請求中獲取數(shù)據(jù),并寫入到輸出文件中 for d in batch: data = existing_requests[d["instruction"]] # 只選擇關(guān)鍵字段創(chuàng)建有序字典 data = OrderedDict( (k, data[k]) for k in \ ["instruction", "raw_instances", "instance_metadata", "instruction_metadata", "most_similar", "avg_similarity_score"] ) # 寫入數(shù)據(jù)到輸出文件 fout.write(json.dumps(data, ensure_ascii=False) + "\n") else: # 如果不都在,那么需要構(gòu)建請求的prompts prompts = [] for task in batch: # 根據(jù)任務(wù)的類型,使用不同的模板構(gòu)建prompt if task_clf_types[task["instruction"]]: prompt = output_first_template_for_clf + " " + task["instruction"].strip() + "\n" prompts.append(prompt) else: prompt = input_first_template_for_gen + " " + task["instruction"].strip() + "\n" prompts.append(prompt) # 使用GPT-3引擎發(fā)送請求 results = make_gpt3_requests( engine=args.engine, prompts=prompts, # 根據(jù)任務(wù)類型調(diào)整最大token數(shù) max_tokens=300 if any(task_clf_types[task["instruction"]] for task in batch) else 350, temperature=0, top_p=0, frequency_penalty=0, presence_penalty=1.5, stop_sequences=[f"Example {args.max_instances_to_generate + 1}", "Task:"], logprobs=1, n=1, best_of=1, api_key=args.api_key, organization=args.organization) # 將結(jié)果寫入到輸出文件中 for i in range(len(batch)): data = batch[i] # 保存請求的元數(shù)據(jù) data["instance_metadata"] = results[i] # 如果結(jié)果存在,則保存生成的實(shí)例 if results[i]["response"] is not None: data["raw_instances"] = results[i]["response"]["choices"][0]["text"] else: # 如果結(jié)果不存在,則設(shè)置為空 data["raw_instances"] = "" # 構(gòu)建有序字典 data = OrderedDict( (k, data[k]) for k in \ ["instruction", "raw_instances", "instance_metadata", "instruction_metadata", "most_similar", "avg_similarity_score"] ) # 寫入數(shù)據(jù)到輸出文件 fout.write(json.dumps(data, ensure_ascii=False) + "\n") # 更新進(jìn)度條 progress_bar.update(len(batch))
-
Step4:過濾及后處理
運(yùn)行后會生成兩個(gè)數(shù)據(jù)文件,均在data/ceshi/finetuning_data目錄下:python self_instruct/prepare_for_finetuning.py --instance_files data/ceshi/machine_generated_instances.jsonl --classification_type_files data/ceshi/is_clf_or_not_davinci_template_1.jsonl --output_dir data/ceshi/finetuning_data --include_seed_tasks --seed_tasks_path data/seed_tasks.jsonl
all_generated_instances.jsonl 和 gpt3_finetuning_data_336.jsonl
其中,all_generated_instances.jsonl中包含的是 instruction,input,output
gpt3_finetuning_data_336.jsonl中包含的是prompt,completion
另,考慮到七月類ChatGPT微調(diào)實(shí)戰(zhàn)課上有一學(xué)員對這兩個(gè)數(shù)據(jù)集為何各自的輸出一個(gè)是output,一個(gè)是completion,故特再解釋下
區(qū)別在于:在第4步之前是output,經(jīng)過第4步處理后是completion。而第4步就是一些過濾以及一些限制條件的處理,比如為了數(shù)據(jù)的多樣性,新生成的指令只有與種子池中的指令的 ROUGE-L 小于0.7時(shí)才會保留下來;還會排除一些無法被語言模型處理的指令,比如涉及圖像、圖片、圖形的指令等等
核心代碼為# 使用tqdm模塊,這是一個(gè)快速,可擴(kuò)展的Python進(jìn)度條,遍歷生成的任務(wù) for task in tqdm.tqdm(generated_tasks): # 從任務(wù)中提取出指令 instruction = task["instruction"] # 根據(jù)指令判斷任務(wù)是否為分類任務(wù),并存儲結(jié)果 task["is_classification"] = task_clf_types[instruction] # 根據(jù)任務(wù)類型,解析并獲取對應(yīng)的實(shí)例 if task["is_classification"]: task_instances = parse_instances_for_classification_task(task["raw_instances"], instruction, task["instance_metadata"]) else: task_instances = parse_instances_for_generation_task(task["raw_instances"], instruction, task["instance_metadata"]) # 每個(gè)任務(wù)最多取5個(gè)實(shí)例,如果實(shí)例數(shù)少于5,則取全部 task_instances = random.sample(task_instances, min(len(task_instances), 5)) # 如果任務(wù)沒有實(shí)例,則跳過當(dāng)前循環(huán) if not task_instances: continue # 將實(shí)例添加到訓(xùn)練實(shí)例列表中 training_instances += task_instances # 初始化GPT-3實(shí)例列表 gpt3_instances = [] # 遍歷訓(xùn)練實(shí)例 for instance in training_instances: # 獲取輸入 inst_input = instance[1] # 對輸入進(jìn)行預(yù)處理,可能會去除冒號前的部分,或替換連續(xù)的兩個(gè)新行符為一個(gè)新行符 if random.random() < 0.5: colon_words = re.findall(r"(\w+):", inst_input) if len(set(colon_words)) == 1: inst_input = inst_input.split(":", 1)[1].strip() else: inst_input = inst_input.strip() inst_input = inst_input.replace("\n\n", "\n") # 對實(shí)例進(jìn)行編碼,并添加到GPT-3實(shí)例列表 gpt3_instances.append(encode_instance(instance[0], inst_input, instance[2])) # 初始化過濾實(shí)例列表和實(shí)例集合,用于移除重復(fù)實(shí)例 filtered_instances = [] prompt_completion_set = set() # 遍歷GPT-3實(shí)例 for instance in gpt3_instances: # 創(chuàng)建實(shí)例對 instance_pair = (instance["prompt"], instance["completion"]) # 如果實(shí)例對不在集合中,添加到集合和過濾實(shí)例列表中 if instance_pair not in prompt_completion_set: prompt_completion_set.add((instance["prompt"], instance["completion"])) filtered_instances.append(instance) # 使用過濾后的實(shí)例替換原來的GPT-3實(shí)例 gpt3_instances = filtered_instances # 打亂GPT-3實(shí)例順序 random.shuffle(gpt3_instances) # 打開文件,準(zhǔn)備將GPT-3實(shí)例寫入文件 with open(os.path.join(args.output_dir, f"gpt3_finetuning_data_{len(gpt3_instances)}.jsonl"), "w") as fout: # 遍歷GPT-3實(shí)例 for instance in gpt3_instances: # 將實(shí)例轉(zhuǎn)化為json格式并寫入文件 fout.write(json.dumps({ "prompt": instance["prompt"], "completion": instance["completion"], }) + "\n")
2.2 Stanford Alpaca的微調(diào)拆解——見證LLM微調(diào)的一般模式
2.2.1 stanford_alpaca/train.py:微調(diào)代碼的逐行分析
可能有讀者疑問,那微調(diào)的代碼長啥樣呢?實(shí)際上,微調(diào)步驟大同小異,具體而言,一般直接用的Hugging Face的transformer標(biāo)準(zhǔn)庫中的微調(diào)代碼?(We fine-tune our models using standard Hugging Face training code)
- 首先安裝pip install -r requirements.txt
- 下面的命令在具有 4 個(gè) A100 80G GPU 且處于 FSDP 模式的機(jī)器上使用52K的數(shù)據(jù)集對 LLaMA-7B 進(jìn)行微調(diào)
???????full_shard
替換<your_random_port>為你自己的端口
<your_path_to_hf_converted_llama_ckpt_and_tokenizer>轉(zhuǎn)換后的檢查點(diǎn)和分詞器的路徑
以及<your_output_dir>你想要存儲輸出的位置torchrun --nproc_per_node=4 --master_port=<your_random_port> train.py \ --model_name_or_path <your_path_to_hf_converted_llama_ckpt_and_tokenizer> \ --data_path ./alpaca_data.json \ --bf16 True \ --output_dir <your_output_dir> \ --num_train_epochs 3 \ --per_device_train_batch_size 4 \ --per_device_eval_batch_size 4 \ --gradient_accumulation_steps 8 \ --evaluation_strategy "no" \ --save_strategy "steps" \ --save_steps 2000 \ --save_total_limit 1 \ --learning_rate 2e-5 \ --weight_decay 0. \ --warmup_ratio 0.03 \ --lr_scheduler_type "cosine" \ --logging_steps 1 \ --fsdp "full_shard auto_wrap" \ --fsdp_transformer_layer_cls_to_wrap 'LlamaDecoderLayer' \ --tf32 True
- 當(dāng)然,以上腳本也適用于對 OPT 微調(diào),這是微調(diào) OPT-6.7B 的示例
torchrun --nproc_per_node=4 --master_port=<your_random_port> train.py \ --model_name_or_path "facebook/opt-6.7b" \ --data_path ./alpaca_data.json \ --bf16 True \ --output_dir <your_output_dir> \ --num_train_epochs 3 \ --per_device_train_batch_size 4 \ --per_device_eval_batch_size 4 \ --gradient_accumulation_steps 8 \ --evaluation_strategy "no" \ --save_strategy "steps" \ --save_steps 2000 \ --save_total_limit 1 \ --learning_rate 2e-5 \ --weight_decay 0. \ --warmup_ratio 0.03 \ --lr_scheduler_type "cosine" \ --logging_steps 1 \ --fsdp "full_shard auto_wrap" \ --fsdp_transformer_layer_cls_to_wrap 'OPTDecoderLayer' \ --tf32 True
好,現(xiàn)在問題來了,上面腳本中的微調(diào)代碼即train.py到底長啥樣呢?
據(jù)Stanford Alpaca于Mar 17, 2023發(fā)布的231行的訓(xùn)練代碼stanford_alpaca/train.py at aa65c492bb788e144712daab42bc5d11c2761591 · tatsu-lab/stanford_alpaca · GitHub?(注:后代碼于Apr 16, 2023有更新,代碼略微縮小至222行代碼,詳見:https://github.com/tatsu-lab/stanford_alpaca/blob/main/train.py),可得微調(diào)的步驟如下
- 導(dǎo)入所需的庫:包括torch,transformers等
import copy import logging from dataclasses import dataclass, field from typing import Optional, Dict, Sequence import torch import transformers from torch.utils.data import Dataset from transformers import Trainer import utils
- 定義一些全局變量,如特殊字符、提示模板等
- 定義用于處理模型、數(shù)據(jù)和訓(xùn)練參數(shù)的數(shù)據(jù)類
# 這是Python中的裝飾器,用于指示該類是一個(gè)數(shù)據(jù)類。數(shù)據(jù)類是一個(gè)專門用于存儲數(shù)據(jù)的類 # 它為我們自動實(shí)現(xiàn)了一些基礎(chǔ)方法,如__init__,__repr__,__eq__等 @dataclass # 定義一個(gè)名為ModelArguments的數(shù)據(jù)類 class ModelArguments: # 定義一個(gè)名為model_name_or_path的實(shí)例變量,類型為Optional[str],默認(rèn)值為"facebook/opt-125m" model_name_or_path: Optional[str] = field(default="facebook/opt-125m") @dataclass # 定義一個(gè)名為DataArguments的數(shù)據(jù)類 class DataArguments: # 定義一個(gè)名為data_path的實(shí)例變量,類型為str,默認(rèn)值為None,額外的metadata提供了該變量的幫助信息 data_path: str = field(default=None, metadata={"help": "Path to the training data."}) @dataclass # 定義一個(gè)名為TrainingArguments的數(shù)據(jù)類,這個(gè)類繼承了transformers庫的TrainingArguments類 class TrainingArguments(transformers.TrainingArguments): # 定義一個(gè)名為cache_dir的實(shí)例變量,類型為Optional[str],默認(rèn)值為None cache_dir: Optional[str] = field(default=None) # 定義一個(gè)名為optim的實(shí)例變量,類型為str,默認(rèn)值為"adamw_torch" optim: str = field(default="adamw_torch") # 定義一個(gè)名為model_max_length的實(shí)例變量,類型為int model_max_length: int = field( default=512, metadata={"help": "Maximum sequence length. Sequences will be right padded (and possibly truncated)."}, )
- 定義輔助函數(shù),如:
-
safe_save_model_for_hf_trainer?:安全地保存訓(xùn)練器中的模型
def safe_save_model_for_hf_trainer(trainer: transformers.Trainer, output_dir: str): """Collects the state dict and dump to disk.""" state_dict = trainer.model.state_dict() if trainer.args.should_save: cpu_state_dict = {key: value.cpu() for key, value in state_dict.items()} del state_dict trainer._save(output_dir, state_dict=cpu_state_dict) # noqa
-
smart_tokenizer_and_embedding_resize?:調(diào)整分詞器和詞嵌入大小
# 定義一個(gè)函數(shù),函數(shù)名為 smart_tokenizer_and_embedding_resize,輸入包括一個(gè)字典(用于定義特殊詞匯),一個(gè)分詞器和一個(gè)預(yù)訓(xùn)練模型 def smart_tokenizer_and_embedding_resize( special_tokens_dict: Dict, tokenizer: transformers.PreTrainedTokenizer, model: transformers.PreTrainedModel, ): """Resize tokenizer and embedding. Note: This is the unoptimized version that may make your embedding size not be divisible by 64. """ # 向分詞器添加特殊詞匯,返回新添加的詞匯數(shù)量 num_new_tokens = tokenizer.add_special_tokens(special_tokens_dict) # 將模型的嵌入層大小調(diào)整為與新的詞匯表大小一致 model.resize_token_embeddings(len(tokenizer)) # 如果添加了新的詞匯 if num_new_tokens > 0: # 獲取模型輸入嵌入的權(quán)重?cái)?shù)據(jù) input_embeddings = model.get_input_embeddings().weight.data # 獲取模型輸出嵌入的權(quán)重?cái)?shù)據(jù) output_embeddings = model.get_output_embeddings().weight.data # 計(jì)算輸入嵌入中舊詞匯的平均向量 input_embeddings_avg = input_embeddings[:-num_new_tokens].mean(dim=0, keepdim=True) # 計(jì)算輸出嵌入中舊詞匯的平均向量 output_embeddings_avg = output_embeddings[:-num_new_tokens].mean(dim=0, keepdim=True) # 將新添加的詞匯的輸入嵌入向量設(shè)置為舊詞匯的平均輸入嵌入向量 input_embeddings[-num_new_tokens:] = input_embeddings_avg # 將新添加的詞匯的輸出嵌入向量設(shè)置為舊詞匯的平均輸出嵌入向量 output_embeddings[-num_new_tokens:] = output_embeddings_avg
-
_tokenize_fn?:將字符串序列進(jìn)行分詞
# 函數(shù)定義,接受一個(gè)字符串序列和一個(gè)預(yù)訓(xùn)練的分詞器,返回一個(gè)字典 def _tokenize_fn(strings: Sequence[str], tokenizer: transformers.PreTrainedTokenizer) -> Dict: """Tokenize a list of strings.""" tokenized_list = [ tokenizer( text, # 對每個(gè)字符串進(jìn)行分詞處理 return_tensors="pt", # 返回PyTorch tensors padding="longest", # padding策略為 "longest",即填充到最長序列的長度 max_length=tokenizer.model_max_length, # 最大長度為分詞器的最大長度 truncation=True, # 如果序列超過最大長度,則進(jìn)行截?cái)? ) for text in strings # 遍歷輸入的每個(gè)字符串 ] # 從分詞結(jié)果中提取輸入的ids和標(biāo)簽 input_ids = labels = [tokenized.input_ids[0] for tokenized in tokenized_list] # 計(jì)算輸入ids和標(biāo)簽的長度(不包括padding) input_ids_lens = labels_lens = [ tokenized.input_ids.ne(tokenizer.pad_token_id).sum().item() for tokenized in tokenized_list ] # 返回一個(gè)字典,包含輸入的ids、標(biāo)簽、輸入的長度和標(biāo)簽的長度 return dict( input_ids=input_ids, labels=labels, input_ids_lens=input_ids_lens, labels_lens=labels_lens, )
-
preprocess :預(yù)處理數(shù)據(jù),對源數(shù)據(jù)和目標(biāo)數(shù)據(jù)進(jìn)行分詞
# 函數(shù)定義,接受源字符串、目標(biāo)字符串和一個(gè)預(yù)訓(xùn)練的分詞器,返回一個(gè)字典 def preprocess( sources: Sequence[str], targets: Sequence[str], tokenizer: transformers.PreTrainedTokenizer, ) -> Dict: # 將源字符串和目標(biāo)字符串組合在一起 examples = [s + t for s, t in zip(sources, targets)] # 對組合后的字符串和源字符串分別進(jìn)行分詞處理 examples_tokenized, sources_tokenized = [_tokenize_fn(strings, tokenizer) for strings in (examples, sources)] input_ids = examples_tokenized["input_ids"] # 從組合后的分詞結(jié)果中提取輸入ID labels = copy.deepcopy(input_ids) # 復(fù)制一份輸入ID作為標(biāo)簽 for label, source_len in zip(labels, sources_tokenized["input_ids_lens"]): # 對于標(biāo)簽,將源字符串部分的ID設(shè)置為忽略索引(IGNORE_INDEX) label[:source_len] = IGNORE_INDEX # 返回一個(gè)字典,包含輸入ID和標(biāo)簽 return dict(input_ids=input_ids, labels=labels)
-
safe_save_model_for_hf_trainer?:安全地保存訓(xùn)練器中的模型
- 定義SupervisedDataset 類(用于監(jiān)督微調(diào)的數(shù)據(jù)集),用于加載數(shù)據(jù)、格式化輸入、進(jìn)行分詞等操作
# 定義一個(gè)用于監(jiān)督學(xué)習(xí)微調(diào)的數(shù)據(jù)集類 class SupervisedDataset(Dataset): def __init__(self, data_path: str, tokenizer: transformers.PreTrainedTokenizer): super(SupervisedDataset, self).__init__() # 初始化父類 logging.warning("Loading data...") # 記錄開始加載數(shù)據(jù)的日志 list_data_dict = utils.jload(data_path) # 加載數(shù)據(jù) logging.warning("Formatting inputs...") # 記錄開始格式化輸入的日志 # 從字典中獲取輸入提示和無輸入提示 prompt_input, prompt_no_input = PROMPT_DICT["prompt_input"], PROMPT_DICT["prompt_no_input"] sources = [ prompt_input.format_map(example) if example.get("input", "") != "" else prompt_no_input.format_map(example) # 遍歷每個(gè)例子,如果有輸入則使用輸入提示,否則使用無輸入提示 for example in list_data_dict ] # 構(gòu)造目標(biāo),每個(gè)目標(biāo)是輸出加上結(jié)束標(biāo)記 targets = [f"{example['output']}{tokenizer.eos_token}" for example in list_data_dict] # 記錄開始分詞輸入的日志 logging.warning("Tokenizing inputs... This may take some time...") data_dict = preprocess(sources, targets, tokenizer) # 預(yù)處理源和目標(biāo) self.input_ids = data_dict["input_ids"] # 保存輸入ID self.labels = data_dict["labels"] # 保存標(biāo)簽 # 返回?cái)?shù)據(jù)集的大小 def __len__(self): return len(self.input_ids) # 返回第i個(gè)樣本,包含輸入ID和標(biāo)簽 def __getitem__(self, i) -> Dict[str, torch.Tensor]: return dict(input_ids=self.input_ids[i], labels=self.labels[i])
- 定義DataCollatorForSupervisedDataset 類,用于將數(shù)據(jù)集的實(shí)例整理為批次
# 定義一個(gè)用于監(jiān)督學(xué)習(xí)微調(diào)的數(shù)據(jù)整理類 @dataclass class DataCollatorForSupervisedDataset(object): # 預(yù)訓(xùn)練的分詞器 tokenizer: transformers.PreTrainedTokenizer # 從實(shí)例中提取輸入ID和標(biāo)簽 def __call__(self, instances: Sequence[Dict]) -> Dict[str, torch.Tensor]: input_ids, labels = tuple([instance[key] for instance in instances] for key in ("input_ids", "labels")) # 對輸入ID進(jìn)行填充,使它們具有相同的長度,填充值為分詞器的填充標(biāo)記ID input_ids = torch.nn.utils.rnn.pad_sequence( input_ids, batch_first=True, padding_value=self.tokenizer.pad_token_id ) # 對標(biāo)簽進(jìn)行填充,使它們具有相同的長度,填充值為忽略索引(IGNORE_INDEX) labels = torch.nn.utils.rnn.pad_sequence(labels, batch_first=True, padding_value=IGNORE_INDEX) # 返回一個(gè)字典,包含輸入ID、標(biāo)簽和注意力掩碼。注意力掩碼用于指示哪些元素應(yīng)該被模型關(guān)注(在這里是非填充的元素) return dict( input_ids=input_ids, labels=labels, attention_mask=input_ids.ne(self.tokenizer.pad_token_id), )
- 定義make_supervised_data_module 函數(shù),用于創(chuàng)建監(jiān)督學(xué)習(xí)任務(wù)的數(shù)據(jù)集和整理器
# 函數(shù)定義,接受一個(gè)預(yù)訓(xùn)練的分詞器和數(shù)據(jù)參數(shù),返回一個(gè)字典 def make_supervised_data_module(tokenizer: transformers.PreTrainedTokenizer, data_args) -> Dict: """Make dataset and collator for supervised fine-tuning.""" # 創(chuàng)建一個(gè)監(jiān)督學(xué)習(xí)的微調(diào)數(shù)據(jù)集 train_dataset = SupervisedDataset(tokenizer=tokenizer, data_path=data_args.data_path) # 創(chuàng)建一個(gè)數(shù)據(jù)整理器 data_collator = DataCollatorForSupervisedDataset(tokenizer=tokenizer) # 返回一個(gè)字典,包含訓(xùn)練數(shù)據(jù)集、評估數(shù)據(jù)集和數(shù)據(jù)整理器。在這里,評估數(shù)據(jù)集為None return dict(train_dataset=train_dataset, eval_dataset=None, data_collator=data_collator)
- 定義train函數(shù)def train() :,用于執(zhí)行以下操作:
a. 解析命令行參數(shù):使用transformers.HfArgumentParser 解析命令行參數(shù),將它們分為模型參數(shù)、數(shù)據(jù)參數(shù)和訓(xùn)練參數(shù)
b.?加載預(yù)訓(xùn)練模型:使用transformers.AutoModelForCausalLM.from_pretrained 從預(yù)訓(xùn)練的模型檢查點(diǎn)加載一個(gè)用于因果語言建模的模型???????parser = transformers.HfArgumentParser((ModelArguments, DataArguments, TrainingArguments)) model_args, data_args, training_args = parser.parse_args_into_dataclasses()
c.?加載分詞器:使用transformers.AutoTokenizer.from_pretrained 從預(yù)訓(xùn)練的模型檢查點(diǎn)加載分詞器???????model = transformers.AutoModelForCausalLM.from_pretrained( model_args.model_name_or_path, cache_dir=training_args.cache_dir, )
d.?為分詞器添加特殊字符:根據(jù)需要,將特殊字符添加到分詞器中???????# 從預(yù)訓(xùn)練模型創(chuàng)建一個(gè)自動化的分詞器,其中包含了模型的名稱或路徑,緩存目錄,模型的最大長度,填充的位置以及是否使用快速分詞器 tokenizer = transformers.AutoTokenizer.from_pretrained( model_args.model_name_or_path, cache_dir=training_args.cache_dir, model_max_length=training_args.model_max_length, padding_side="right", use_fast=False, ) # 如果分詞器沒有pad token,那么添加一個(gè),并重新設(shè)置模型的嵌入大小 if tokenizer.pad_token is None: smart_tokenizer_and_embedding_resize( special_tokens_dict=dict(pad_token=DEFAULT_PAD_TOKEN), tokenizer=tokenizer, model=model, )
e. ???????創(chuàng)建數(shù)據(jù)集和整理器:使用make_supervised_data_module 函數(shù)為監(jiān)督學(xué)習(xí)任務(wù)創(chuàng)建數(shù)據(jù)集和整理器???????# 如果模型名包含"llama",則為分詞器添加特殊的token,包括eos_token,bos_token以及unk_token if "llama" in model_args.model_name_or_path: tokenizer.add_special_tokens( { "eos_token": DEFAULT_EOS_TOKEN, "bos_token": DEFAULT_BOS_TOKEN, "unk_token": DEFAULT_UNK_TOKEN, } )
f. ???????實(shí)例化Trainer類:實(shí)例化transformers.Trainer?類,并傳入模型、分詞器、訓(xùn)練參數(shù)以及數(shù)據(jù)集。Trainer類負(fù)責(zé)管理訓(xùn)練過程???????data_module = make_supervised_data_module(tokenizer=tokenizer, data_args=data_args)
g.?訓(xùn)練模型:調(diào)用???????Trainer???????trainer = Trainer(model=model, tokenizer=tokenizer, args=training_args, **data_module)
類的train()?
方法對模型進(jìn)行微調(diào),相當(dāng)于鏈路就是:transformers庫 ?Trainer類 ?train函數(shù)???????
h.?保存模型狀態(tài):在訓(xùn)練完成后,調(diào)用Trainer.save_state() 方法保存模型的狀態(tài)trainer.train()
i.?將訓(xùn)練器的模型安全地保存到磁盤:使用safe_save_model_for_hf_trainer 函數(shù)將訓(xùn)練器中的模型安全地保存到磁盤trainer.save_state()
trainer.save_model(output_dir=training_args.output_dir)
- 如果這個(gè)腳本是主程序,則調(diào)用train 函數(shù)以開始訓(xùn)練過程
if __name__ == "__main__": train()
這份訓(xùn)練/微調(diào)代碼很經(jīng)典,包括像此文《從GLM、ChatGLM-6B、MOSS、baichuan-7B到垂類醫(yī)療/金融/法律模型、可商用模型》第七部分“各種醫(yī)療類ChatGPT:或中英文數(shù)據(jù)微調(diào)LLaMA、或中文數(shù)據(jù)微調(diào)ChatGLM”里的chatdoctor (基于醫(yī)療語料微調(diào)LLaMA),也用的這份標(biāo)準(zhǔn)代碼
這是chatdoctor代碼庫中的微調(diào)代碼:https://github.com/Kent0n-Li/ChatDoctor/blob/main/train.py#L192,一模一樣,沒有任何不同
![]()
2.2.2?Hugging face社區(qū)實(shí)現(xiàn)的鼎鼎大名的transformers庫
但,可能很快便有同學(xué)疑問,怎么沒有預(yù)想中的損失計(jì)算、梯度下降、參數(shù)更新呢,實(shí)際上這三步的具體實(shí)現(xiàn)都封裝在了Hugging face社區(qū)實(shí)現(xiàn)的鼎鼎大名的transformers庫的Trainer類中:transformers/trainer.py at main · huggingface/transformers · GitHub
這個(gè)?transformers/trainer.py?文件的主要部分如下
????????? 導(dǎo)入:文件首先導(dǎo)入了一些必要的Python庫,如os、sys、logging以及其他一些庫。它還導(dǎo)入了Hugging Face庫中的一些相關(guān)模塊,如datasets、transformers等
????????? TrainerState:這個(gè)類用于保存訓(xùn)練器的狀態(tài),包括當(dāng)前的epoch、迭代步數(shù)、最佳指標(biāo)值等
????????? TrainOutput:這個(gè)類用于返回訓(xùn)練過程的結(jié)果,包括訓(xùn)練損失、訓(xùn)練步數(shù)等
????????? TrainerControl:這個(gè)類提供了一種用于控制訓(xùn)練循環(huán)的機(jī)制,例如,當(dāng)用戶想要在某個(gè)特定的迭代步數(shù)時(shí)停止訓(xùn)練
????????? Trainer:這是文件中的主要類,用于訓(xùn)練和評估Transformers模型,它包含許多方法,如train、evaluate、predict等
更具體的,Trainer類包括如下關(guān)鍵方法:
__init__:初始化方法,用于創(chuàng)建訓(xùn)練器對象。它接收模型、訓(xùn)練參數(shù)、數(shù)據(jù)集等作為輸入,并設(shè)置相關(guān)屬性
def __init__( self, model: PreTrainedModel, args: TrainingArguments, train_dataset: Optional[Dataset] = None, eval_dataset: Optional[Dataset] = None, tokenizer: Optional[PreTrainedTokenizerBase] = None, data_collator: Optional[DataCollator] = None, train_iterator: Optional[DataLoader] = None, eval_iterator: Optional[DataLoader] = None, ... ):
train:這個(gè)方法負(fù)責(zé)整個(gè)訓(xùn)練過程,它包括遍歷數(shù)據(jù)集、計(jì)算損失、計(jì)算梯度、更新模型參數(shù)以及日志記錄等
遍歷數(shù)據(jù)集:train方法通過使用dataloader來遍歷訓(xùn)練數(shù)據(jù)集
for step, inputs in enumerate(epoch_iterator):
- 計(jì)算損失:損失計(jì)算在training_step方法中,接收輸入數(shù)據(jù)并產(chǎn)生預(yù)測輸出,然后,這個(gè)預(yù)測輸出會與真實(shí)輸出(標(biāo)簽)進(jìn)行比較,以計(jì)算損失???????
outputs = model(**inputs)
上述代碼行使用model(已經(jīng)加載了預(yù)訓(xùn)練模型)和inputs(包含輸入數(shù)據(jù)的字典)計(jì)算模型的預(yù)測輸出。這個(gè)outputs變量包含模型預(yù)測的結(jié)果
接下來,我們從outputs中獲取預(yù)測結(jié)果,并與真實(shí)標(biāo)簽(即labels)進(jìn)行比較,以計(jì)算損失outputs.lossloss = outputs.loss
是模型預(yù)測輸出和真實(shí)輸出(標(biāo)簽)之間的損失。這個(gè)損失值將用于計(jì)算梯度并更新模型參數(shù)
計(jì)算梯度:loss.backward()這行代碼計(jì)算模型參數(shù)關(guān)于損失的梯度
loss.backward()
梯度累積:且當(dāng)gradient_accumulation_steps大于1時(shí),梯度會被累積,而不是立即更新模型參數(shù)
if (step + 1) % self.args.gradient_accumulation_steps == 0:
更新模型參數(shù):optimizer.step()這行代碼根據(jù)計(jì)算出的梯度來更新模型參數(shù)
且慢,暫停解釋下為何會有梯度累積這個(gè)操作呢?原因在于batch size越大,局部數(shù)據(jù)求得的梯度方向越接近全局的梯度優(yōu)化方向,那怎么增大batch size呢?一方面可以增加硬件資源,二方面可以通過梯度累積self.optimizer.step()
舉個(gè)例子,
假定我們有1000個(gè)樣本的數(shù)據(jù)集,我們可以將其分成10個(gè)小批次,每個(gè)小批次包含100個(gè)樣本
? 梯度累積:在每個(gè)小批次的訓(xùn)練中,我們會計(jì)算出模型參數(shù)的梯度,然后將這些梯度累加起來(可以設(shè)定一個(gè)參數(shù)gradient_accumulation_steps,以指定我們想要累積多少個(gè)小批次的梯度,比如5),而不是立即用它們來更新模型參數(shù)
? 參數(shù)更新:當(dāng)我們處理完gradient_accumulation_steps個(gè)小批次后,我們就使用累積的梯度來更新模型的參數(shù)
? 梯度清零:在每次參數(shù)更新后,我們都會將累積的梯度清零,以便于開始下一個(gè)梯度累積和參數(shù)更新的周期,最終處理完剩下的5個(gè)批次
值得一提的是,通常情況下,我們會進(jìn)行多個(gè)epoch的訓(xùn)練(每次進(jìn)行新的epoch時(shí),數(shù)據(jù)打亂),每個(gè)epoch后都會對模型的性能進(jìn)行評估,并根據(jù)評估結(jié)果來調(diào)整學(xué)習(xí)率等超參數(shù)學(xué)習(xí)率調(diào)整:lr_scheduler.step()根據(jù)預(yù)定義的學(xué)習(xí)率調(diào)度策略更新學(xué)習(xí)率
self.lr_scheduler.step()
日志記錄:log方法用于記錄訓(xùn)練過程中的一些關(guān)鍵指標(biāo),例如損失、學(xué)習(xí)率等
?evaluate
:這個(gè)方法用于評估模型在驗(yàn)證數(shù)據(jù)集上的性能,返回評估結(jié)果def evaluate( self, eval_dataset: Optional[Dataset] = None, ignore_keys: Optional[List[str]] = None ) -> Dict[str, float]:
predict
:這個(gè)方法用于在給定的數(shù)據(jù)集上進(jìn)行預(yù)測,返回預(yù)測結(jié)果def predict( self, test_dataset: Dataset, ignore_keys: Optional[List[str]] = None ) -> PredictionOutput:
save_model
:這個(gè)方法用于將訓(xùn)練好的模型保存到指定的目錄def save_model(self, output_dir: Optional[str] = None):
????????? ShardedDDPOption:這是一個(gè)可選的類,用于支持使用混合精度和ZeRO進(jìn)行分布式訓(xùn)練
2.2.3 Alpaca-LoRA:通過PEFT庫在消費(fèi)級GPU上微調(diào)「基于LLaMA的Alpaca」
在神經(jīng)網(wǎng)絡(luò)模型中,模型參數(shù)通常以矩陣的形式表示。對于一個(gè)預(yù)訓(xùn)練好的模型,其參數(shù)矩陣已經(jīng)包含了很多有用的信息。為了使模型適應(yīng)特定任務(wù),我們需要對這些參數(shù)進(jìn)行微調(diào)
LoRA的核心思想是用一種低秩的方式來調(diào)整這些參數(shù)矩陣。在數(shù)學(xué)上,低秩意味著一個(gè)矩陣可以用兩個(gè)較小的矩陣相乘來近似,通過論文《LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS》可知(這是解讀之一)
- 選擇目標(biāo)層:首先,在預(yù)訓(xùn)練神經(jīng)網(wǎng)絡(luò)模型中選擇要應(yīng)用LoRA的目標(biāo)層。這些層通常是與特定任務(wù)相關(guān)的,如自注意力機(jī)制中的查詢Q和鍵K矩陣
- 初始化映射矩陣和逆映射矩陣:為目標(biāo)層創(chuàng)建兩個(gè)較小的矩陣A和B
? A是映射矩陣(一般用隨機(jī)高斯分布初始化,當(dāng)然實(shí)際代碼實(shí)現(xiàn)時(shí),比如微軟的deepspeed chat在用到LoRA時(shí),一開始通過0矩陣占位,然后調(diào)用搭配ReLU激活函數(shù)的kaiming均勻分布初始化,雖與LoRA原始定義所用的正態(tài)分布初始化不同,但此兩種初始化方式都可以工作,更多介紹見下面deepspeed chat的代碼?),維度上是降維
? B是逆映射矩陣(用0矩陣初始化),維度上是升維
其中,矩陣的大小由LoRA的秩(rank)和alpha值確定 - 參數(shù)變換:將目標(biāo)層的原始參數(shù)矩陣W通過映射矩陣A和逆映射矩陣B進(jìn)行變換。計(jì)算公式為:
,這里W'是變換后的參數(shù)矩陣
- 微調(diào)模型:使用新的參數(shù)矩陣替換目標(biāo)層的原始參數(shù)矩陣,然后在特定任務(wù)的訓(xùn)練數(shù)據(jù)上對模型進(jìn)行微調(diào)
- 梯度更新:在微調(diào)過程中,計(jì)算損失函數(shù)關(guān)于映射矩陣A和逆映射矩陣B的梯度,并使用優(yōu)化算法(如Adam、SGD等)對A和B進(jìn)行更新
注意,在更新過程中,原始參數(shù)矩陣W保持不變,說白了,訓(xùn)練的時(shí)候固定原始PLM的參數(shù),只訓(xùn)練降維矩陣A與升維矩陣B - 重復(fù)更新:在訓(xùn)練的每個(gè)批次中,重復(fù)步驟3-5,直到達(dá)到預(yù)定的訓(xùn)練輪次(epoch)或滿足收斂條件
總之,LoRA的詳細(xì)步驟包括選擇目標(biāo)層、初始化映射矩陣和逆映射矩陣、進(jìn)行參數(shù)變換和模型微調(diào)。在微調(diào)過程中,模型會通過更新映射矩陣U和逆映射矩陣V來學(xué)習(xí)特定任務(wù)的知識,從而提高模型在該任務(wù)上的性能
繼續(xù)說一下,這個(gè)LoRA的應(yīng)用還是挺廣的,比如后續(xù)微軟推出的DeepSpeed-Chat便用了這個(gè)方法
DeepSpeed-Chat的實(shí)現(xiàn)中,當(dāng)設(shè)置LoRA的低秩維度lora_dim(如lora_dim=128)時(shí),即認(rèn)為啟用了LoRA訓(xùn)練,則將原始模型中名稱含有“deoder.layers.”且為線性層修改為LoRA層,具體操作為:
- 將原始結(jié)構(gòu)的weight參數(shù)凍結(jié);
- 新引入了2個(gè)線性層lora_right_weight和lora_left_weight (分別對應(yīng)上圖中的降維矩陣A、升維矩陣B?),可實(shí)現(xiàn)先降維至lora_dim再升維回原維度;
- LoRA層主要實(shí)現(xiàn)了兩分支通路,一條分支為已被凍結(jié)weight參數(shù)的原始結(jié)構(gòu)、另一條分支為新引入的降維再升維線性層組
# applications/DeepSpeed-Chat/training/step1_supervised_finetuning/main.py # 判斷是否啟用LoRA模式 if args.lora_dim > 0: """ 如果啟用,則對名稱中含有“decoder.layers.”且為線性層的結(jié)構(gòu)部分引入LoRA旁路(實(shí)現(xiàn)先降維后升維的2個(gè)線性層), 這類結(jié)構(gòu)基本都是attention、信息交互用的inner線性層, 這類結(jié)構(gòu)的Weight參數(shù)將被凍結(jié),轉(zhuǎn)而優(yōu)化LoRA旁路的參數(shù)。 """ args.lora_module_name = "decoder.layers." model = convert_linear_layer_to_lora(model, args.lora_module_name, args.lora_dim) # applications/DeepSpeed-Chat/training/utils/module/lora.py def convert_linear_layer_to_lora(model, part_module_name, lora_dim=0, lora_scaling=1, lora_droppout=0): """ 將名稱中帶有"decoder.layers."的線性層轉(zhuǎn)換為lora層 """ """取出模型中參數(shù)名含有decoder.layers.的線性層""" repalce_name = [] for name, module in model.named_modules(): if isinstance(module, nn.Linear) and part_module_name in name: repalce_name.append(name) for name in repalce_name: """recursive_getattr實(shí)現(xiàn)了從model中根據(jù)屬性名取出對應(yīng)原始結(jié)構(gòu)""" module = recursive_getattr(model, name) """納入原始結(jié)構(gòu)的參數(shù),實(shí)例化lora層""" tmp = LinearLayer_LoRA( module.weight, lora_dim, lora_scaling, lora_droppout, module.bias).to(module.weight.device).to(module.weight.dtype) """recursive_getattr實(shí)現(xiàn)了將model對應(yīng)屬性的結(jié)構(gòu)換成lora層實(shí)例""" recursive_setattr(model, name, tmp) return model # applications/DeepSpeed-Chat/training/utils/module/lora.py class LinearLayer_LoRA(nn.Module): """具體的lora層""" def __init__(...): ... """此處的weight和bias即為原始結(jié)構(gòu)中的參數(shù)""" self.weight = weight self.bias = bias ··· """凍結(jié)weight部分的參數(shù)""" self.weight.requires_grad = False ··· self.lora_right_weight = nn.Parameter(torch.zeros(columns, lora_dim)) self.lora_left_weight = nn.Parameter(torch.zeros(lora_dim, rows)) ... """初始化LoRA線性層的參數(shù)""" self.reset_parameters() # 調(diào)用reset_parameters(self)做初始化 def reset_parameters(self): # 降維矩陣與LoRA原始定義所用的(0,\sigma^2)正態(tài)分布初始化不同,而是使用的kaiming均勻分布初始化 # kaiming服從均勻分布U(-\sqrt{1/in_feature}, +\sqrt{1/in_feature}) # f_i是矩陣的輸入維度,就是nn.Linear(in_features, out_features)中的in_features # 對應(yīng)上面代碼中的columns,而這個(gè)columns相當(dāng)于基座模型的hidden_size nn.init.kaiming_uniform_(self.lora_right_weight, a=math.sqrt(5)) # 升維矩陣使用全0初始化 nn.init.zeros_(self.lora_left_weight) def forward(self, input): """LoRA的正向傳播""" ··· else: # F.linear(input, self.weight, self.bias)是使用給定的權(quán)重self.weight和偏差self.bias對輸入數(shù)據(jù)input進(jìn)行線性變換 # 這個(gè)操作等價(jià)于input @ self.weight.t() + self.bias,其中@表示矩陣乘法,.t()表示矩陣轉(zhuǎn)置 return F.linear(input, self.weight, self.bias) # 1,self.lora_dropout(input)對輸入進(jìn)行了隨機(jī)的dropout操作,這是一種正則化手段 # 2,對結(jié)果進(jìn)行兩次線性變換,一次是@ self.lora_right_weight,然后是@ self.lora_left_weight # 3,乘法部分* self.lora_scaling是對加號后面部分的結(jié)果進(jìn)行縮放 + (self.lora_dropout(input) @ self.lora_right_weight @ self.lora_left_weight) * self.lora_scaling
再額外分析下 這段代碼的最后部分
# applications/DeepSpeed-Chat/training/utils/module/lora.py class LinearLayer_LoRA(nn.Module): """具體的lora層""" ··· def forward(self, input): """LoRA的正向傳播""" ··· else: return F.linear( input, self.weight, self.bias) + (self.lora_dropout(input) @ self.lora_right_weight @ self.lora_left_weight) * self.lora_scaling
常規(guī)部分的正向傳播由transformers所定義,而LoRA部分的正向傳播則由LinearLayer_LoRA(nn.Module)的forward()所定義,即“LoRA層的兩條分支結(jié)果進(jìn)行加和”,如下圖所示『圖源:https://huggingface.co/docs/peft/conceptual_guides/lora,相當(dāng)于在訓(xùn)練期間,較小的權(quán)重矩陣(下圖中的A和B)是分開的,但一旦訓(xùn)練完成,權(quán)重可以合并到一個(gè)新權(quán)重矩陣中?』
?在代碼中體現(xiàn)為
F.linear(input, self.weight, self.bias) + (self.lora_dropout(input) @ self.lora_right_weight @ self.lora_left_weight) * self.lora_scaling
加號左側(cè)為原結(jié)構(gòu)支路,加號右側(cè)為新增支路,self.lora_right_weight?和self.lora_left_weight?分別為兩個(gè)新引入線性層的參數(shù)
而Huggingface公司推出的PEFT(Parameter-Efficient Fine-Tuning)庫也封裝了LoRA這個(gè)方法,PEFT庫可以使預(yù)訓(xùn)練語言模型高效適應(yīng)各種下游任務(wù),而無需微調(diào)模型的所有參數(shù),即僅微調(diào)少量(額外)模型參數(shù),從而大大降低了計(jì)算和存儲成本
Model | Full Finetuning | PEFT-LoRA PyTorch | PEFT-LoRA DeepSpeed with CPU Offloading |
---|---|---|---|
bigscience/T0_3B (3B params) | 47.14GB GPU / 2.96GB CPU | 14.4GB GPU / 2.96GB CPU | 9.8GB GPU / 17.8GB CPU |
bigscience/mt0-xxl (12B params) | OOM GPU | 56GB GPU / 3GB CPU | 22GB GPU / 52GB CPU |
bigscience/bloomz-7b1 (7B params) | OOM GPU | 32GB GPU / 3.8GB CPU | 18.1GB GPU / 35GB CPU |
且PEFT庫?(peft/src/peft/peft_model.py at main · huggingface/peft · GitHub)支持以下流行的方法
- LoRA,PEFT對LoRA的實(shí)現(xiàn)封裝見:peft/src/peft/tuners/lora.py at main · huggingface/peft · GitHub,比如對權(quán)重的合并代碼 (和上面DSC對LoRA權(quán)重合并的實(shí)現(xiàn),在本質(zhì)上是一致的)
def merge(self): # 檢查當(dāng)前激活的適配器是否在lora_A的鍵中,如果不在則終止函數(shù) if self.active_adapter not in self.lora_A.keys(): return if self.merged: warnings.warn("Already merged. Nothing to do.") return # 如果激活適配器的r值大于0,表示有可以合并的權(quán)重 if self.r[self.active_adapter] > 0: # 在當(dāng)前的權(quán)重上加上計(jì)算得到的新權(quán)重 self.weight.data += ( # 轉(zhuǎn)置運(yùn)算 transpose( # 通過矩陣乘法計(jì)算新的權(quán)重 self.lora_B[self.active_adapter].weight @ self.lora_A[self.active_adapter].weight, # 這是轉(zhuǎn)置運(yùn)算的維度參數(shù) self.fan_in_fan_out, ) # 然后將計(jì)算得到的權(quán)重乘以對應(yīng)的縮放因子 * self.scaling[self.active_adapter] ) self.merged = True
- Prefix Tuning:?Prefix-Tuning: Optimizing Continuous Prompts for Generation,?P-Tuning v2: Prompt Tuning Can Be Comparable to Fine-tuning Universally Across Scales and Tasks
- P-Tuning:?GPT Understands, Too
- Prompt Tuning:?The Power of Scale for Parameter-Efficient Prompt Tuning
故而,Alpaca-LoRA即可以通過PEFT庫實(shí)現(xiàn)的LoRA方法在消費(fèi)級GPU微調(diào)「基于LLaMA的Alpaca」,比如項(xiàng)目中的這個(gè)文件finetune.py 包含了PEFT在LLaMA上的直接應(yīng)用,以及一些與prompt construction和tokenization相關(guān)的代碼,以下是用法示例:
python finetune.py \
--base_model 'decapoda-research/llama-7b-hf' \
--data_path 'yahma/alpaca-cleaned' \
--output_dir './lora-alpaca'
我們還可以調(diào)整我們的超參數(shù)(為方便大家理解,我給每個(gè)參數(shù)都加了注釋說明):
python finetune.py \ # 運(yùn)行微調(diào)腳本
--base_model 'decapoda-research/llama-7b-hf' \ # 選擇預(yù)訓(xùn)練的基礎(chǔ)模型
--data_path 'yahma/alpaca-cleaned' \ # 用于微調(diào)的數(shù)據(jù)集路徑
--output_dir './lora-alpaca' \ # 微調(diào)后模型的輸出目錄
--batch_size 128 \ # 設(shè)置每個(gè)批次的樣本數(shù)量
--micro_batch_size 4 \ # 設(shè)置每個(gè)小批次的樣本數(shù)量
--num_epochs 3 \ # 設(shè)置訓(xùn)練的輪次(epoch)
--learning_rate 1e-4 \ # 設(shè)置學(xué)習(xí)速率
--cutoff_len 512 \ # 設(shè)置截?cái)嚅L度
--val_set_size 2000 \ # 設(shè)置驗(yàn)證集的大小
--lora_r 8 \ # 設(shè)置LoRA方法中的秩
--lora_alpha 16 \ # 設(shè)置LoRA方法中的alpha值
--lora_dropout 0.05 \ # 設(shè)置LoRA方法中的dropout率
--lora_target_modules '[q_proj,v_proj]' \ # 設(shè)置使用LoRA進(jìn)行微調(diào)的模型模塊
--train_on_inputs # 指示模型在訓(xùn)練時(shí)使用輸入文本
2.3 Alpaca所用的self-instruct的影響力:解決一大批模型的數(shù)據(jù)擴(kuò)展問題
很快,通過下文你會發(fā)現(xiàn)
-
self-instruct啟發(fā)出很多「羊駝?lì)惸P汀?/strong>
羊駝率先帶動的self-instruct,啟發(fā)后續(xù)很多人/團(tuán)隊(duì)也用這個(gè)方式去采集『提示ChatGPT API』的數(shù)據(jù),比如BELLE、ChatLLaMA、ColossalChat -
很多「羊駝?lì)惸P汀沟臄?shù)據(jù)被用于微調(diào)新一批模型
然后還有一批模型各種疊加組合比如『Alpaca/BELLE』,又用于微調(diào)一批批模型
比如ChatDoctor 有用到Alpaca的數(shù)據(jù)進(jìn)行微調(diào),再比如有人拿BELLE數(shù)據(jù)tuning去調(diào)chatglm
?一下子出來這么新的模型 似乎有點(diǎn)懵,沒事,請看下文及下一篇文章娓娓道來..
2.3.1 UC?Berkeley的Vicuna/FastChat:通過ShareGPT.com的7萬條對話數(shù)據(jù)微調(diào)LLaMA
23年3.31日,受 Meta LLaMA 和 Stanford Alpaca 項(xiàng)目的啟發(fā),加州大學(xué)伯克利分校(UC?Berkeley)等大學(xué)的研究者根據(jù)從 ShareGPT.com (ShareGPT是一個(gè)用戶可以分享他們的 ChatGPT 對話的網(wǎng)站)收集的用戶共享對話微調(diào) LLaMA?推出了Vicuna-13B(中文稱小羊駝,GitHub地址:FastChat)
在數(shù)據(jù)規(guī)模上,Vicuna從ShareGPT.com 的公共 API 收集了大約 70K 用戶共享對話,且為了確保數(shù)據(jù)質(zhì)量,原作者們將 HTML 轉(zhuǎn)換回 markdown 并過濾掉一些不合適或低質(zhì)量的樣本。此外,將冗長的對話分成更小的部分,以適應(yīng)模型的最大上下文長度,并做了以下改進(jìn)
- 內(nèi)存優(yōu)化:為了使 Vicuna 能夠理解長上下文,將最大上下文長度從羊駝Alpaca中的 512 擴(kuò)展到 2048,這大大增加了 GPU 內(nèi)存需求,對此通過利用梯度檢查點(diǎn)和閃存注意力來解決內(nèi)存壓力 (We tackle the memory pressure by utilizing gradient checkpointing and flash attention)
- 多輪對話:調(diào)整訓(xùn)練損失以考慮多輪對話,并僅根據(jù)聊天機(jī)器人的輸出計(jì)算微調(diào)損失
- 通過Spot Instance 降低成本:40 倍大的數(shù)據(jù)集和 4 倍的訓(xùn)練序列長度對訓(xùn)練費(fèi)用提出了相當(dāng)大的挑戰(zhàn)。原作者們使用SkyPilot managed spot 來降低成本『SkyPilot是加州大學(xué)伯克利分校構(gòu)建的一個(gè)框架,用于在各種云上輕松且經(jīng)濟(jì)高效地運(yùn)行 ML 工作負(fù)載』,方法是利用更便宜的spot instances以及auto-recovery for preemptions and auto zone switch
該解決方案將 7B 模型的訓(xùn)練成本從 500 美元削減至 140 美元左右,將 13B 模型的訓(xùn)練成本從 1000 美元左右削減至 300 美元
有兩點(diǎn)值得一提的是
- Vicuna的預(yù)訓(xùn)練是一天之內(nèi)通過8個(gè)具有 80GB 顯存的 A100 GPU 進(jìn)行訓(xùn)練的,預(yù)訓(xùn)練好之后單純部署的話,Vicuna-13B 需要大約 28GB 的??GPU 顯存,Vicuna-7B 大約需要14GB GPU顯存
- 且Vicuna使用了和Alpaca差不多的超參數(shù)
Hyperparameter 全局批量大小
Batch Size
學(xué)習(xí)率
Learning rate
Epochs Max length Weight decay Vicuna-13B 128 2e-5 3 2048 0
最終通過直接使用GPT4評估之后(基于 GPT-4 的評估框架來自動評估聊天機(jī)器人的性能),效果還不錯(cuò)
Model Name | LLaMA(駱駝) | Alpaca(羊駝) | Vicuna(小羊駝) | Bard/ChatGPT |
Dataset | Publicly available datasets (1.4T token) |
Self-instruct from davinci-003 API (52K samples) |
User-shared conversations (70K samples) |
N/A |
Training code | N/A | Available | Available | N/A |
Evaluation metrics | Academic benchmark | Author evaluation | GPT-4 assessment | Mixed |
Training cost (7B) |
82K GPU-hours | $500 (data) + $100 (training) | $140 (training) | N/A |
Training cost (13B) |
135K GPU-hours | N/A | $300 (training) | N/A |
2.3.2 鏈家BELLE:結(jié)合中文語料通過Self Instruct方式微調(diào)BLOOMZ-7B或LLaMA
Stanford Alpaca的種子任務(wù)都是英語,收集的數(shù)據(jù)也都是英文,因此訓(xùn)練出來的模型未對中文優(yōu)化。為了提升對話模型在中文上的效果,70 億參數(shù)的中文對話大模型 BELLE『Bloom-Enhanced Large Language model Engine』來了(這是項(xiàng)目地址)。
在數(shù)據(jù)方面,結(jié)合以下兩方面的數(shù)據(jù):
- Alpaca 的 5.2 萬條英文數(shù)據(jù)
- 通過Alpaca的數(shù)據(jù)收集代碼生成的約 100 萬條中文數(shù)據(jù)『也僅使用由 GPT3.5 即模型text-davinci-003 生產(chǎn)的數(shù)據(jù),不包含任何其他數(shù)據(jù),如果想使用ChatGPT的API比如gpt-3.5-turbo模型,可通過參數(shù)控制』
模型訓(xùn)練上,有
- 基于BLOOMZ-7B1-mt優(yōu)化后的模型:BELLE-7B-0.2M,BELLE-7B-0.6M,BELLE-7B-1M,BELLE-7B-2M
- 基于huggingface的LLaMA實(shí)例實(shí)現(xiàn)調(diào)優(yōu)的模型:BELLE-LLAMA-7B-2M,BELLE-LLAMA-13B-2M
BLOOM是由HuggingFace于2022年3月中旬推出的大模型,規(guī)模最大版本的參數(shù)量達(dá)到176B(GPT-3是175B),基于從 Megatron-LM GPT-2修改而來的僅解碼器 transformer 模型架構(gòu)
? 對應(yīng)的論文為《BLOOM: A 176B-Parameter Open-Access Multilingual Language Model》(翻譯之一,解讀之一)
? 此外,這里有篇不錯(cuò)的文章(重點(diǎn)講了下Megatron-DeepSpeed):千億參數(shù)開源大模型 BLOOM 背后的技術(shù)
至于HuggingFace是著名開源工具Transformers的開發(fā)公司,很多推理工具都會支持Transformers中的模型
截至23年3月中旬,超過100B參數(shù)量且能夠支持中文的開源大模型只有BLOOM和GLM-130B
該項(xiàng)目主要包含以下三部分內(nèi)容:
- 175 個(gè)中文種子任務(wù),斯坦福Alpaca一樣,每個(gè)任務(wù)都包含對應(yīng)的指令/任務(wù)、prompt、輸出
? zh_seed_tasks.jsonl:樣例如下
?{??"id": "seed_task_20", "name": "horror_movie_opening",
"instruction": "你需要為一部恐怖電影寫一個(gè)創(chuàng)意的開場場景。",
"instances": [{"input": "","output":" 太陽已經(jīng)落山,留下了一個(gè)黑暗的小鎮(zhèn)。微風(fēng)吹拂空蕩的街道,讓每一個(gè)冒險(xiǎn)走出門外的人感到一陣寒意。唯一的聲音是被風(fēng)吹動的樹葉發(fā)出的輕微沙沙聲。突然,一聲令人毛骨悚然的尖叫聲劃破了寂靜,隨后是玻璃破碎的聲音。一所房子亮起了燈光,可以看到一個(gè)人影朝鎮(zhèn)中心奔跑。當(dāng)> 那個(gè)人影越來越靠近時(shí),清楚地看到那是一個(gè)年輕女子,她渾身血跡斑斑。"}],
"is_classification": false??} -
? prompt_cn.txt: 生成所使用的提示語
? 0.5M 生成的數(shù)據(jù) - 生成數(shù)據(jù)及其代碼
沿用 Alpaca 的方式:
pip install -r requirements.txt
export OPENAI_API_KEY=YOUR_API_KEY
python generate_instruction.py generate_instruction_following_data
默認(rèn)使用 Completion API,模型 text-davinci-003。如果想使用 Chat API 并使用 gpt-3.5-turbo 模型,可通過參數(shù)控制:
python generate_instruction.py generate_instruction_following_data \
??--api=chat --model_name=gpt-3.5-turbo
輸出文件在 Belle.train.json,可以人工篩選后再使用 - 基于 BLOOMZ-7B1-mt 模型和 Belle.train.json 訓(xùn)練模型
2.4 Chinese-LLaMA/Chinese-Alpaca:通過中文數(shù)據(jù)預(yù)訓(xùn)練/指令微調(diào)
Chinese LLaMA(也稱中文LLaMA,有7B和13B兩個(gè)版本,項(xiàng)目地址),相當(dāng)于在原版LLaMA的基礎(chǔ)上擴(kuò)充了中文詞表并使用了中文數(shù)據(jù)進(jìn)行二次預(yù)訓(xùn)練,進(jìn)一步提升了中文基礎(chǔ)語義理解能力,同時(shí),在中文LLaMA的基礎(chǔ)上,且用中文指令數(shù)據(jù)進(jìn)行指令精調(diào)得Chinese-Alpaca(也稱中文Alpaca,同樣也有7B和13B兩個(gè)版本)
具體而言,主要做了以下三方面的工作
2.4.1 詞表擴(kuò)充中文數(shù)據(jù)
在通用中文語料上訓(xùn)練了基于SentencePiece的20K中文詞表并與原版LLaMA模型的32K詞表進(jìn)行合并
排除重復(fù)的token后,得到的最終中文LLaMA詞表大小為49953
需要注意的是,在fine-tune階段Alpaca比LLaMA多一個(gè)pad token,所以中文Alpaca的詞表大小為49954
這么做的主要原因是原版LLaMA模型的詞表大小是32K,其主要針對英語進(jìn)行訓(xùn)練,對多語種支持不是特別理想(可以對比一下多語言經(jīng)典模型XLM-R的詞表大小為250K)。通過初步統(tǒng)計(jì)發(fā)現(xiàn),LLaMA詞表中僅包含很少的中文字符,所以在切詞時(shí)會把中文切地更碎,需要多個(gè)byte token才能拼成一個(gè)完整的漢字,進(jìn)而導(dǎo)致信息密度降低
其對應(yīng)的擴(kuò)充詞表的腳本代碼為 (代碼地址在:merge_tokenizers.py???????,為方便大家更好的理解,我給每一行的代碼 都加上了注釋)
import os # 導(dǎo)入os模塊,用于操作系統(tǒng)相關(guān)操作
# 設(shè)置環(huán)境變量,使得Protocol Buffers使用Python實(shí)現(xiàn)
os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"]="python"
from transformers import LlamaTokenizer # 導(dǎo)入LlamaTokenizer類
# 導(dǎo)入Protocol Buffers格式的sentencepiece模型
from sentencepiece import sentencepiece_model_pb2 as sp_pb2_model
import sentencepiece as spm # 導(dǎo)入sentencepiece模塊
import argparse # 導(dǎo)入argparse模塊,用于處理命令行參數(shù)
parser = argparse.ArgumentParser() # 創(chuàng)建一個(gè)命令行參數(shù)解析器實(shí)例
# 添加llama_tokenizer_dir參數(shù),必需
parser.add_argument('--llama_tokenizer_dir', default=None, type=str, required=True)
# 添加chinese_sp_model_file參數(shù),可選
parser.add_argument('--chinese_sp_model_file', default='./chinese_sp.model', type=str)
args = parser.parse_args() # 解析命令行參數(shù)
llama_tokenizer_dir = args.llama_tokenizer_dir # 獲取llama_tokenizer_dir參數(shù)值
chinese_sp_model_file = args.chinese_sp_model_file # 獲取chinese_sp_model_file參數(shù)值
# load, 加載預(yù)訓(xùn)練LlamaTokenizer實(shí)例
llama_tokenizer = LlamaTokenizer.from_pretrained(llama_tokenizer_dir)
# 創(chuàng)建SentencePieceProcessor實(shí)例
chinese_sp_model = spm.SentencePieceProcessor()
# 加載中文sentencepiece模型
chinese_sp_model.Load(chinese_sp_model_file)
# 將LlamaTokenizer和中文sentencepiece模型轉(zhuǎn)換為Protocol Buffers格式
llama_spm = sp_pb2_model.ModelProto()
llama_spm.ParseFromString(llama_tokenizer.sp_model.serialized_model_proto())
chinese_spm = sp_pb2_model.ModelProto()
chinese_spm.ParseFromString(chinese_sp_model.serialized_model_proto())
# print number of tokens
# 輸出LlamaTokenizer和中文sentencepiece模型的詞匯數(shù)量
print(len(llama_tokenizer),len(chinese_sp_model))
print(llama_tokenizer.all_special_tokens) # 輸出LlamaTokenizer的所有特殊詞匯
print(llama_tokenizer.all_special_ids) # 輸出LlamaTokenizer的所有特殊詞匯ID
print(llama_tokenizer.special_tokens_map) # 輸出LlamaTokenizer的特殊詞匯映射
'''
將中文詞匯添加到LLaMA tokenizer中
# 提取LLaMA tokenizer中的詞匯
'''
llama_spm_tokens_set=set(p.piece for p in llama_spm.pieces)
print(len(llama_spm_tokens_set))
print(f"Before:{len(llama_spm_tokens_set)}")
for p in chinese_spm.pieces:
piece = p.piece
# 如果中文詞匯不存在于LLaMA tokenizer中
if piece not in llama_spm_tokens_set:
new_p = sp_pb2_model.ModelProto().SentencePiece()
new_p.piece = piece
new_p.score = 0
# 將中文詞匯添加到LLaMA tokenizer中
llama_spm.pieces.append(new_p)
print(f"New model pieces: {len(llama_spm.pieces)}")
# Save, 設(shè)置輸出目錄,用于保存合并后的sentencepiece模型
output_sp_dir = 'merged_tokenizer_sp'
# 設(shè)置輸出目錄,用于保存合并后的Chinese-LLaMA tokenizer
output_hf_dir = 'merged_tokenizer_hf'
# 創(chuàng)建輸出目錄(如果不存在)
os.makedirs(output_sp_dir, exist_ok=True)
# 打開合并后的sentencepiece模型文件,準(zhǔn)備寫入
with open(output_sp_dir + '/chinese_llama.model', 'wb') as f:
# 將合并后的sentencepiece模型序列化為字符串并寫入文件
f.write(llama_spm.SerializeToString())
# 從合并后的sentencepiece模型文件中創(chuàng)建LlamaTokenizer實(shí)例
tokenizer = LlamaTokenizer(vocab_file=output_sp_dir + '/chinese_llama.model')
# 保存合并后的Chinese-LLaMA tokenizer到指定目錄
tokenizer.save_pretrained(output_hf_dir)
# 輸出保存信息
print(f"Chinese-LLaMA tokenizer has been saved to {output_hf_dir}")
# Test
# 重新加載原始的LLaMA tokenizer
llama_tokenizer = LlamaTokenizer.from_pretrained(llama_tokenizer_dir)
# 加載合并后的Chinese-LLaMA tokenizer
chinese_llama_tokenizer = LlamaTokenizer.from_pretrained(output_hf_dir)
# 輸出合并后的tokenizer的所有特殊詞匯
print(tokenizer.all_special_tokens)
# 輸出合并后的tokenizer的所有特殊詞匯ID
print(tokenizer.all_special_ids)
# 輸出合并后的tokenizer的特殊詞匯映射
print(tokenizer.special_tokens_map)
# 定義測試文本
text = '''白日依山盡,黃河入海流。欲窮千里目,更上一層樓。
The primary use of LLaMA is research on large language models, including'''
# 輸出測試文本
print("Test text:\n", text)
print
# 使用原始的LLaMA tokenizer對文本進(jìn)行分詞
print(f"Tokenized by LLaMA tokenizer:{llama_tokenizer.tokenize(text)}")
# 使用合并后的Chinese-LLaMA tokenizer對文本進(jìn)行分詞
print(f"Tokenized by Chinese-LLaMA tokenizer:{chinese_llama_tokenizer.tokenize(text)}")
這段代碼的主要目的是將一個(gè)中文的sentencepiece模型與一個(gè)已經(jīng)預(yù)訓(xùn)練好的LLaMA tokenizer進(jìn)行合并,以便在處理中文文本時(shí),LLaMA tokenizer能更好地進(jìn)行分詞
整個(gè)過程包括了加載模型、合并模型、保存新的tokenizer以及進(jìn)行測試等步驟,具體如下
- 首先,通過argparse模塊獲取命令行參數(shù),包括原始的LLaMA tokenizer的路徑和中文sentencepiece模型的路徑
- 接著,加載這兩個(gè)模型,并將它們轉(zhuǎn)換為Protocol Buffers格式,方便進(jìn)行操作
- 然后,從中文sentencepiece模型中提取詞匯,并將這些詞匯添加到LLaMA tokenizer中
在這個(gè)過程中,需要檢查每個(gè)中文詞匯是否已經(jīng)存在于LLaMA tokenizer中,以避免重復(fù)添加 - 將合并后的模型保存到指定的目錄
即首先保存為sentencepiece模型文件,然后創(chuàng)建一個(gè)新的LlamaTokenizer實(shí)例,并將其保存為Hugging Face格式的tokenizer - 最后,對原始的LLaMA tokenizer和合并后的Chinese-LLaMA tokenizer進(jìn)行測試,以驗(yàn)證合并是否成功
測試包括輸出特殊詞匯、特殊詞匯ID、特殊詞匯映射等信息,以及使用這兩個(gè)tokenizer對給定文本進(jìn)行分詞
從測試結(jié)果可以看出,合并后的Chinese-LLaMA tokenizer能夠更好地處理中文文本
此外,七月在線ChatGPT原理解析課一學(xué)員在群內(nèi)問道:“如何擴(kuò)充詞表、訓(xùn)練embedding,然后再與llama的合并,想在自己的數(shù)據(jù)上試試”
“吹牛班的春天”答道:“我知道的方法就是直接改embedding結(jié)構(gòu):初始化參數(shù)concat到以前embedding層上,以前的權(quán)embedding權(quán)重就保留,多出來的部分就后面更新,下圖是以前BERT無損擴(kuò)詞的思路,可做參考”
2.4.2 加入中文數(shù)據(jù)的預(yù)訓(xùn)練
在預(yù)訓(xùn)練階段,使用約20G左右的通用中文語料(與中文BERT-wwm、MacBERT、LERT、PERT中使用的語料一致)在原版LLaMA權(quán)重的基礎(chǔ)上進(jìn)一步進(jìn)行預(yù)訓(xùn)練。該過程又分為兩個(gè)階段:
第一階段:凍結(jié)transformer參數(shù),僅訓(xùn)練embedding,在盡量不干擾原模型的情況下適配新增的中文詞向量
第二階段:使用LoRA技術(shù),為模型添加LoRA權(quán)重(adapter),訓(xùn)練embedding的同時(shí)也更新LoRA參數(shù)
2.4.3 指令精調(diào)
指令精調(diào)階段的任務(wù)形式基本與Stanford Alpaca相同,訓(xùn)練方案同樣采用了LoRA進(jìn)行高效精調(diào),并進(jìn)一步增加了可訓(xùn)練參數(shù)數(shù)量
在prompt設(shè)計(jì)上,精調(diào)以及預(yù)測時(shí)采用的都是原版Stanford Alpaca不帶input的模版。對于包含input字段的數(shù)據(jù),采用"?f{instruction}+\n+{input}?"的形式進(jìn)行拼接
且指令精調(diào)階段使用了以下數(shù)據(jù),其中7B模型約2M數(shù)據(jù)、13B模型約3M數(shù)據(jù)?;緲?gòu)成如下:
數(shù)據(jù) | 量級 | 來源 | 說明 |
---|---|---|---|
中英翻譯數(shù)據(jù) | 500K | 外部鏈接 | 在原數(shù)據(jù)集的基礎(chǔ)上進(jìn)行了采樣+規(guī)則篩選 |
pCLUE數(shù)據(jù) | 300K | 外部鏈接 | 在原數(shù)據(jù)集的基礎(chǔ)上進(jìn)行了采樣+規(guī)則篩選 |
Alpaca數(shù)據(jù)(英) | 50K | 外部鏈接 | 斯坦福原版Alpaca訓(xùn)練數(shù)據(jù) |
Alpaca數(shù)據(jù)(中) | 50K | 本地鏈接 | 本項(xiàng)目使用ChatGPT接口將英文版翻譯為中文(篩掉一部分) |
Self-instruction數(shù)據(jù) | 1~2M | (暫無) | 本項(xiàng)目使用ChatGPT接口進(jìn)行爬取,提供了一個(gè)動態(tài)生成不同領(lǐng)域和指令類型的prompt爬取腳本script/crawl_prompt.py。 python script/crawl_prompt.py output-file |
當(dāng)然,針對一些任務(wù)上效果不好!原作者也給出了幾個(gè)可能的原因,
????????1)本身LLaMA對中文支持不是很好,大多數(shù)相關(guān)衍生工作是直接在原版上進(jìn)行pretrain/finetune的,而我們采取了更大膽的策略——增加中文詞表,可能進(jìn)一步加劇中文訓(xùn)練不充分的問題,但從長遠(yuǎn)看是否有利于后續(xù)進(jìn)一步預(yù)訓(xùn)練就得靠時(shí)間檢驗(yàn)了;
????????2)指令數(shù)據(jù)的質(zhì)量有待進(jìn)一步提升;
????????3)訓(xùn)練時(shí)間、超參等方面還有很大調(diào)整空間;
????????4)沒有RLHF;
????????5)4-bit量化后效果可能會下降,因此可以嘗試加載FP16模型,效果相對更好一些(也更慢)
2.5 姜子牙系列模型Ziya-LLaMA-13B-v1
2.5.1 基本信息:軟件依賴/模型分類
姜子牙通用大模型V1是基于LLaMa的130億參數(shù)的大規(guī)模預(yù)訓(xùn)練模型 (論文地址:Zero-Shot Learners for Natural Language Understanding via a Unified Multiple Choice Perspective),具備翻譯,編程,文本分類,信息抽取,摘要,文案生成,常識問答和數(shù)學(xué)計(jì)算等能力。目前姜子牙通用大模型已完成大規(guī)模預(yù)訓(xùn)練、多任務(wù)有監(jiān)督微調(diào)和人類反饋學(xué)習(xí)三階段的訓(xùn)練過程「large-scale continual pre-training (PT), multi-task supervised fine-tuning (SFT), and human feedback learning (RM, PPO)」
軟件依賴
pip install torch==1.12.1 tokenizers==0.13.3 git+https://github.com/huggingface/transformers
模型分類 Model Taxonomy
需求 Demand | 任務(wù) Task | 系列 Series | 模型 Model | 參數(shù) Parameter | 額外 Extra |
---|---|---|---|---|---|
通用 General | AGI模型 | 姜子牙 Ziya | LLaMA | 13B | English&Chinese |
2.5.2 模型的預(yù)訓(xùn)練與微調(diào):預(yù)訓(xùn)練、SFT、HFFT
繼續(xù)預(yù)訓(xùn)練 Continual pretraining
- 數(shù)據(jù)方面
原始數(shù)據(jù)包含英文和中文,其中英文數(shù)據(jù)來自openwebtext、Books、Wikipedia和Code,中文數(shù)據(jù)來自清洗后的悟道數(shù)據(jù)集、自建的中文數(shù)據(jù)集。在對原始數(shù)據(jù)進(jìn)行去重、模型打分、數(shù)據(jù)分桶、規(guī)則過濾、敏感主題過濾和數(shù)據(jù)評估后,最終得到125B tokens的有效數(shù)據(jù)「After deduplication, model scoring, data bucketing, rule filtering, sensitive topic filtering, and data evaluation, we finally obtained 125 billion tokens of valid data」 - 分詞方面
為了解決LLaMA原生分詞對中文編解碼效率低下的問題,我們在LLaMA詞表的基礎(chǔ)上增加了7k+個(gè)常見中文字,通過和LLaMA原生的詞表去重,最終得到一個(gè)39410大小的詞表,并通過復(fù)用Transformers里L(fēng)lamaTokenizer來實(shí)現(xiàn)了這一效果「We achieved this by reusing the LlamaTokenizer in Transformers」 - 訓(xùn)練過程
在增量訓(xùn)練過程中,我們使用了160張40GB的A100,采用2.6M tokens的訓(xùn)練集樣本數(shù)量和FP 16的混合精度,吞吐量達(dá)到118 TFLOP per GPU per second。因此我們能夠在8天的時(shí)間里在原生的LLaMA-13B模型基礎(chǔ)上,增量訓(xùn)練110B tokens的數(shù)據(jù)
訓(xùn)練期間,雖然遇到了機(jī)器宕機(jī)、底層框架bug、loss spike等各種問題,但我們通過快速調(diào)整,保證了增量訓(xùn)練的穩(wěn)定性。我們也放出訓(xùn)練過程的loss曲線,讓大家了解可能出現(xiàn)的問題
多任務(wù)有監(jiān)督微調(diào) Supervised finetuning
在多任務(wù)有監(jiān)督微調(diào)階段,采用了課程學(xué)習(xí)(curiculum learning)和增量訓(xùn)練( incremental training)的策略,用大模型輔助劃分已有的數(shù)據(jù)難度,然后通過“Easy To Hard”的方式,分多個(gè)階段進(jìn)行SFT訓(xùn)練
SFT訓(xùn)練數(shù)據(jù)包含多個(gè)高質(zhì)量的數(shù)據(jù)集,均經(jīng)過人工篩選和校驗(yàn):
- Self-Instruct構(gòu)造的數(shù)據(jù)(約2M):BELLE、Alpaca、Alpaca-GPT4等多個(gè)數(shù)據(jù)集
- 內(nèi)部收集Code數(shù)據(jù)(300K):包含leetcode、多種Code任務(wù)形式
- 內(nèi)部收集推理/邏輯相關(guān)數(shù)據(jù)(500K):推理、申論、數(shù)學(xué)應(yīng)用題、數(shù)值計(jì)算等
- 中英平行語料(2M):中英互譯語料、COT類型翻譯語料、古文翻譯語料等
- 多輪對話語料(500K):Self-Instruct生成、任務(wù)型多輪對話、Role-Playing型多輪對話等
人類反饋學(xué)習(xí) Human-Feedback training
為了進(jìn)一步提升模型的綜合表現(xiàn),使其能夠充分理解人類意圖、減少“幻覺”和不安全的輸出,基于指令微調(diào)后的模型,進(jìn)行了人類反饋訓(xùn)練(Human-Feedback Training,HFT)。在訓(xùn)練中,我們采用了以人類反饋強(qiáng)化學(xué)習(xí)(RM、PPO)為主,結(jié)合多種其他手段聯(lián)合訓(xùn)練的方法用來彌補(bǔ)PPO方法的短板、加速訓(xùn)練,具體包括
- 人類反饋微調(diào)(Human-Feedback Fine-tuning,HFFT)
- 后見鏈微調(diào)(Chain-of-Hindsight Fine-tuning,COHFT)
- AI反饋(AI Feedback)
- 基于規(guī)則的獎勵(lì)系統(tǒng)(Rule-based Reward System,RBRS)等
我們在內(nèi)部自研的框架上實(shí)現(xiàn)了HFT的訓(xùn)練流程,該框架可以利用最少8張40G的A100顯卡完成Ziya-LLaMA-13B-v1的全參數(shù)訓(xùn)練。在PPO訓(xùn)練中,我們沒有限制生成樣本的長度,以確保長文本任務(wù)的獎勵(lì)準(zhǔn)確性。每次訓(xùn)練的總經(jīng)驗(yàn)池尺寸超過100k樣本,確保了訓(xùn)練的充分性
2.5.3 效果評估、使用說明、微調(diào)示例
使用 Usage
考慮到LLaMA權(quán)重的許可限制,我們無法直接發(fā)布完整的模型權(quán)重。因此,我們使用了開源的FastChat工具,并進(jìn)一步優(yōu)化它以計(jì)算Ziya-LLaMA-13B-v1權(quán)重和原始LLaMA權(quán)重之間的差異(?we utilized?the open-source FastChat tool?and further optimized it to calculate the differences between Ziya-LLaMA-13B-v1 weights and the original?LLaMA weights)。用戶可以按照以下步驟獲得Ziya-LLaMA-13B-v1的完整權(quán)重:
- Step 1:獲取LLaMA權(quán)重并轉(zhuǎn)成Hugging Face Transformers模型格式,可參考轉(zhuǎn)換腳本(若已經(jīng)有huggingface權(quán)重則跳過)
python src/transformers/models/llama/convert_llama_weights_to_hf.py \ --input_dir /path/to/downloaded/llama/weights --model_size 13B --output_dir /output/path
- Step 2:下載Ziya-LLaMA-13B-v1的delta權(quán)重以及step 1中轉(zhuǎn)換好的原始LLaMA權(quán)重,使用如下腳本轉(zhuǎn)換:https://github.com/IDEA-CCNL/Fengshenbang-LM/blob/main/fengshen/utils/apply_delta.py
python3 -m apply_delta --base ~/model_weights/llama-13b --target ~/model_weights/Ziya-LLaMA-13B --delta ~/model_weights/Ziya-LLaMA-13B-v1
- Step 3: 加載step 2得到的模型推理
from transformers import AutoTokenizer from transformers import LlamaForCausalLM import torch device = torch.device("cuda") ckpt = '基于delta參數(shù)合并后的完整模型權(quán)重' query="幫我寫一份去西安的旅游計(jì)劃" model = LlamaForCausalLM.from_pretrained(ckpt, torch_dtype=torch.float16, device_map="auto") tokenizer = AutoTokenizer.from_pretrained(ckpt, use_fast=False) inputs = '<human>:' + query.strip() + '\n<bot>:' input_ids = tokenizer(inputs, return_tensors="pt").input_ids.to(device) generate_ids = model.generate( input_ids, max_new_tokens=1024, do_sample = True, top_p = 0.85, temperature = 1.0, repetition_penalty=1., eos_token_id=2, bos_token_id=1, pad_token_id=0) output = tokenizer.batch_decode(generate_ids)[0] print(output)
微調(diào)示例 Finetune Example
Refer to?ziya_finetune
推理量化示例 Inference & Quantization Example
Refer to?ziya_inference
2.6? 基于LLaMA微調(diào)的各模型對比:Alpaca/Vicuna/BELLE/Chinese-LLaMA/Ziya-LLaMA-13B
項(xiàng)目 | 一句話描述 |
Stanford Alpaca |
結(jié)合英文語料通過Self Instruct方式微調(diào)LLaMA 7B |
Vicuna-13B |
通過ShareGPT.com的7萬條對話數(shù)據(jù)微調(diào)LLaMA |
BELLE | 結(jié)合中文語料通過Self Instruct方式微調(diào)BLOOMZ-7B或LLaMA |
Chinese-LLaMA/Chinese-Alpaca |
通過中文數(shù)據(jù)預(yù)訓(xùn)練/指令微調(diào)LLaMA |
姜子牙系列模型Ziya-LLaMA-13B-v1 | 基于LLaMA-13B的中英文模型 |
ChatLLaMA(英文版) | LLaMA的RLHF版 |
ColossalChat |
通過self-instruct技術(shù)指令微調(diào)LLaMA且加上RLHF |
第三部分?更強(qiáng)的LLaMA 2開源,可直接商用
3.1 LLaMA2簡介:相比LLaMA1代——1.4倍token,2倍上下文
23年7月份,Meta發(fā)布LLAMA 2 (項(xiàng)目地址、論文地址、論文解讀之一),是 LLAMA 1 的更新版本
-
模型結(jié)構(gòu)
采用了 Llama 1 的大部分預(yù)訓(xùn)練設(shè)置和模型架構(gòu),比如使用標(biāo)準(zhǔn)Transformer 架構(gòu),使用 RMSNorm 應(yīng)用預(yù)歸一化、使用 SwiGLU 激活函數(shù)和旋轉(zhuǎn)位置嵌入RoPE -
訓(xùn)練數(shù)據(jù)
使用一種新的混合的公開可用數(shù)據(jù)進(jìn)行訓(xùn)練,訓(xùn)練數(shù)據(jù)規(guī)模是2T個(gè)token,相比1代的1.4T多出了40% -
上下文長度
上下文長度達(dá)到了4096,相比1代的2048直接翻了一倍 -
模型種類
目前 LLAMA 2 的系列模型有 7B、13B、70B 三種(34B的后續(xù)發(fā)布)
值得特別注意的是,其中的70B模型采用了分組查詢注意力(grouped-query attention,簡稱GQA)「Transformer原始論文中用的多頭注意力(MHA)、ChatGLM2-6B則用的多查詢注意力(Multi-query attention,簡稱MQA)」
同時(shí) Meta 還發(fā)布了 LLaMA 2-CHAT,其是基于 LLAMA 2 針對對話場景微調(diào)的版本,同樣 7B、13B 和 70B 參數(shù)三個(gè)版本,具體的訓(xùn)練方法與ChatGPT類似
- 先是監(jiān)督微調(diào)LLaMA2得到SFT版本?(接受了成千上萬個(gè)人類標(biāo)注數(shù)據(jù)的訓(xùn)練,本質(zhì)是問題-答案對?)
- 然后使用人類反饋強(qiáng)化學(xué)習(xí)(RLHF)進(jìn)行迭代優(yōu)化
先訓(xùn)練一個(gè)獎勵(lì)模型
然后在獎勵(lì)模型/優(yōu)勢函數(shù)的指引下,通過拒絕抽樣(rejection sampling)和近端策略優(yōu)化(PPO)的方法迭代模型的生成策略
LLAMA 2 的性能表現(xiàn)更加接近 GPT-3.5,Meta 也承認(rèn)距離 GPT-4 和 PaLM 2 等領(lǐng)先非開源模型還有差距
Meta 在技術(shù)報(bào)告中詳細(xì)列出了 LLAMA 2 的性能、測評數(shù)據(jù),以及分享了重要的訓(xùn)練方法,具體詳見原論文
3.2 LLaMA2之分組查詢注意力——Grouped-Query Attention
自回歸解碼的標(biāo)準(zhǔn)做法是緩存序列中先前標(biāo)記的鍵 (K) 和值 (V) 對,從而加快注意力計(jì)算速度
然而,隨著上下文窗口或批量大小的增加,多頭注意力 (MHA)模型中與 KV 緩存大小相關(guān)的內(nèi)存成本顯著增長
???????對于較大的模型,KV 緩存大小成為瓶頸,鍵和值投影可以在多個(gè)頭之間共享,而不會大幅降低性能,可以使用
- 具有單個(gè) KV 投影的原始多查詢格式(MQA)
ChatGLM2-6B即用的這個(gè),詳見此文《ChatGLM兩代的部署/微調(diào)/實(shí)現(xiàn):從基座GLM、ChatGLM的LoRA/P-Tuning微調(diào)、6B源碼解讀到ChatGLM2的微調(diào)與實(shí)現(xiàn)》的3.1.2節(jié)
不過,多查詢注意(Multi-query attention,簡稱MQA)只使用一個(gè)鍵值頭,雖大大加快了解碼器推斷的速度,但MQA可能導(dǎo)致質(zhì)量下降,而且僅僅為了更快的推理而訓(xùn)練一個(gè)單獨(dú)的模型可能是不可取的 - 或具有多個(gè) KV 投影的分組查詢注意力(grouped-query attention,簡稱GQA),速度快 質(zhì)量高
23年,還是Google的研究者們提出了一種新的方法,即分組查詢注意(GQA,論文地址為:GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints???????),這是一種多查詢注意的泛化,它通過折中(多于一個(gè)且少于查詢頭的數(shù)量,比如4個(gè))鍵值頭的數(shù)量,使得經(jīng)過強(qiáng)化訓(xùn)練的GQA以與MQA相當(dāng)?shù)乃俣冗_(dá)到接近多頭注意力的質(zhì)量
經(jīng)實(shí)驗(yàn)論證,GQA 變體在大多數(shù)評估任務(wù)上的表現(xiàn)與 MHA 基線相當(dāng),并且平均優(yōu)于 MQA 變體
更多請參見《一文通透各種注意力:從多頭注意力MHA到分組查詢注意力GQA、多查詢注意力MQA》
3.3 Llama 2-Chat中的RLHF:依然是三階段訓(xùn)練方式
3.3.1?監(jiān)督微調(diào)(SFT)
在SFT的數(shù)據(jù)上
- 他們先是重點(diǎn)收集了幾千個(gè)高質(zhì)量 SFT 數(shù)據(jù)示例 (注意:很多新聞稿會說SFT的數(shù)據(jù)達(dá)到百萬以上,這就是沒仔細(xì)看論文的結(jié)果,論文之意是勝過百萬低質(zhì)量的數(shù)據(jù),As a result, we focused first on collecting several thousand examples of high-quality SFT data. By setting aside millions of examples from third-party datasets and using fewer buthigher-quality examples from our own vendor-based annotation efforts, our results notably improved)
- 之后發(fā)現(xiàn)幾萬次的SFT標(biāo)注就足以獲得高質(zhì)量的結(jié)果,最終總共收集了27540條用于SFT的標(biāo)注數(shù)據(jù) (We found that SFT annotations in the order of tens ofthousands was enough to achieve a high-quality result. We stopped annotating SFT after collecting a total of 27,540 annotations.)
在微調(diào)過程中
- 每個(gè)樣本都包括一個(gè)prompt和一個(gè)response(說白了,就是問題-答案對,和instructGPT/ChatGPT本身的監(jiān)督微調(diào)是一個(gè)本質(zhì)),且為確保模型序列長度得到正確填充,Meta 將訓(xùn)練集中的所有prompt和response連接起來。他們使用一個(gè)特殊的 token 來分隔prompt和response片段,利用自回歸目標(biāo),將來自用戶提示的 token 損失歸零,因此只對答案 token 進(jìn)行反向傳播,最后對模型進(jìn)行了 2 次微調(diào)
- 微調(diào)過程中的參數(shù)則如此設(shè)置:we use a cosine learning rate schedule with an initiallearning rate of?2?×10?5?, a weight decay of 0.1, a batch size of 64, and a sequence length of 4096 token
3.3.2 訓(xùn)練兩個(gè)獎勵(lì)模型:一個(gè)偏實(shí)用 一個(gè)偏安全
下表 6 報(bào)告了 Meta 長期以來收集到的獎勵(lì)建模數(shù)據(jù)的統(tǒng)計(jì)結(jié)果,并將其與多個(gè)開源偏好數(shù)據(jù)集進(jìn)行了對比。他們收集了超過 100 萬個(gè)基于人類應(yīng)用指定準(zhǔn)則的二元比較的大型數(shù)據(jù)集,也就是獎勵(lì)建模數(shù)據(jù)
關(guān)于獎勵(lì)數(shù)據(jù)
- prompt和response中的標(biāo)記數(shù)因文本領(lǐng)域而異,比如摘要和在線論壇數(shù)據(jù)的prompt通常較長,而對話式的prompt通常較短。與現(xiàn)有的開源數(shù)據(jù)集相比,本文的偏好數(shù)據(jù)具有更多的對話回合,平均長度也更長
- 獎勵(lì)模型將模型響應(yīng)及其相應(yīng)的提示(包括前一輪的上下文)作為輸入,并輸出一個(gè)標(biāo)量分?jǐn)?shù)來表示模型生成的質(zhì)量(例如有用性和安全性),利用這種作為獎勵(lì)的響應(yīng)得分,Meta 在 RLHF 期間優(yōu)化了 Llama 2-Chat,以更好地與人類偏好保持一致,并提高有用性和安全性
在每一批用于獎勵(lì)建模的人類偏好標(biāo)注中,Meta 都拿出 1000 個(gè)樣本作為測試集來評估模型,并將相應(yīng)測試集的所有prompt的集合分別稱為實(shí)用性和安全性 (很多新聞稿會翻譯成元實(shí)用、元安全,其實(shí)沒必要加個(gè)“元”字,你理解為是Meta內(nèi)部定義的“實(shí)用”與“安全”兩個(gè)概念即可)
故為了兼顧和平衡模型的實(shí)用性和安全性,LLaMA 2團(tuán)隊(duì)訓(xùn)練了兩個(gè)獨(dú)立的獎勵(lì)模型
- 一個(gè)針對實(shí)用性(稱為實(shí)用性RM)進(jìn)行了優(yōu)化,在內(nèi)部所有偏實(shí)用的獎勵(lì)數(shù)據(jù)集上進(jìn)行訓(xùn)練,并結(jié)合從內(nèi)部偏安全的獎勵(lì)數(shù)據(jù)集和開源安全性數(shù)據(jù)集中統(tǒng)一采樣的同等部分剩余數(shù)據(jù)
theHelpfulness reward model is eventually trained on all Meta Helpfulness data, combined with an equalparts of the remaining data uniformly sampled from Meta Safety and from the open-source datasets - 另一個(gè)針對安全性(安全性RM)進(jìn)行了優(yōu)化,在內(nèi)部所有偏安全的獎勵(lì)數(shù)據(jù)和人類無害數(shù)據(jù)上進(jìn)行訓(xùn)練,并以90/10的比例混合內(nèi)部偏實(shí)用的獎勵(lì)數(shù)據(jù)和開源實(shí)用性數(shù)據(jù)
TheMeta Safety reward model is trained on all Meta Safety and Anthropic Harmless data, mixed with MetaHelpfulness and open-source helpfulness data in a 90/10 proportion.
We found that the setting with 10% helpfulness data is especially beneficial for the accuracy on samples where both the chosen and rejectedresponses were deemed safe
并通過預(yù)訓(xùn)練的LLaMA 2初始化獎勵(lì)模型(意味著獎勵(lì)模型的架構(gòu)與參數(shù)與預(yù)訓(xùn)練模型一致,只是用于下一個(gè)token預(yù)測的分類頭被替換為用于輸出標(biāo)量獎勵(lì)的回歸頭),因?yàn)樗_保了兩個(gè)模型都能從預(yù)訓(xùn)練中獲得的知識中受益
「雖然論文中的原文是:We initialize our reward models from pretrained chat model checkpoints, as it ensures that both modelsbenefit from knowledge acquired in pretraining,以及The model architecture and hyper-parameters are identical to thoseof the pretrained language models, except that the classification head for next-token prediction is replacedwith a regression head for outputting a scalar reward,但為何沒有類似ChatGPT那樣,通過微調(diào)過的SFT初始化RM模型,該點(diǎn)存疑?」
為了使模型行為與人類偏好相一致,Meta 收集了代表了人類偏好經(jīng)驗(yàn)采樣的數(shù)據(jù),通過針對同一個(gè)prompt模型給出的兩個(gè)不同的response,人類標(biāo)注者選擇他們更喜歡的模型輸出。這種人類偏好被用于訓(xùn)練獎勵(lì)模型
其中,是標(biāo)注者選擇的首選response,是被拒絕的對應(yīng)response
且為了讓模型可以更好的體會到不同response質(zhì)量之間的差異,作者團(tuán)隊(duì)將偏好評級被分為4層評級,且考慮到根據(jù)這些評級信息使得獎勵(lì)模型對有更多差異的生成,有著不同分?jǐn)?shù)且這些分?jǐn)?shù)上彼此之間的差距盡可能拉開是有用的「Given that our preference ratings is decomposed as a scale of four points (e.g.,significantly better), it can be useful to leverage this information to explicitlyteach the reward model to assign more discrepant scores to the generations that have more differences」,為此,我們在損失中進(jìn)一步添加一個(gè)邊際成分
其中邊際是偏好評級的離散函數(shù),他們發(fā)現(xiàn)這個(gè)邊際成分可以提高幫助性獎勵(lì)模型的準(zhǔn)確性,特別是在兩個(gè)response更好區(qū)分的的樣本上「where the margin?m(r)?is a discrete function of the preference rating.?We found this margin component can improve Helpfulness reward model accuracy especially on sampleswhere two responses are more separable」
具體而言,為了衡量不同response好壞的程度,劃分為4個(gè)等級(比如很好、好、較好、一般好或不確定),那這4個(gè)等級是需要有一定的間隔的,彼此之間不能模棱兩可(模棱兩可就容易把模型搞糊涂),而這個(gè)間隔大小是個(gè)超參數(shù),可以人為設(shè)定,比如為小點(diǎn)的間隔1/3或大點(diǎn)的間隔1,如下圖所示
文章來源:http://www.zghlxwxcb.cn/news/detail-476613.html
3.3.3 具體的策略迭代:PPO與拒絕采樣
此處使用兩種主要算法對 RLHF 進(jìn)行了微調(diào):文章來源地址http://www.zghlxwxcb.cn/news/detail-476613.html
- 近端策略優(yōu)化(PPO)
PPO在之前的《ChatGPT技術(shù)原理解析》或《強(qiáng)化學(xué)習(xí)極簡入門》等文章中講的很詳細(xì)了,可以參見 - 拒絕采樣(Rejection Sampling)
即在模型生成多個(gè)回復(fù)后,選擇最佳的回復(fù)作為模型的輸出,過程中,如果生成的回復(fù)不符合預(yù)期,就會被拒絕,直到找到最佳回復(fù)
從而幫助提高模型的生成質(zhì)量,使其更符合人類的期望
At each iterative stage, we sample K answers for each prompt from the most recent model. We score eachsample given the best reward model accessible at the time of the experiment, and then select the best answerfor a given promp
到了這里,關(guān)于類ChatGPT模型LLaMA的解讀與其微調(diào):Alpaca-LoRA/Vicuna/BELLE的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!