类ChatGPT的部署与微调(上):从LLaMA、Alpaca/Vicuna/BELLE、中文版
前言?
近期,除了研究ChatGPT背后的各種技術(shù)細節(jié) 不斷看論文(至少100篇,100篇目錄見此:ChatGPT相關(guān)技術(shù)必讀論文100篇),還開始研究一系列開源模型(包括各自對應(yīng)的模型架構(gòu)、訓(xùn)練方法、訓(xùn)練數(shù)據(jù)、本地私有化部署、硬件配置要求、微調(diào)等細節(jié))?
本文一開始是作為此文《ChatGPT技術(shù)原理解析:從RL之PPO算法、RLHF到GPT4、instructGPT》的第4部分,但隨著研究深入 為避免該文篇幅又過長,將把『第4部分 開源項目』抽取出來 獨立成本文,然后不斷續(xù)寫本文直至成了一個系列
畢竟我上半年的目標(biāo)之一,便是把ChatGPT涉及的所有一切關(guān)鍵技術(shù)細節(jié),以及相關(guān)的開源項目都研究的透透的,故過程中會不斷產(chǎ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曾第一個發(fā)布了基于LLM的聊天機器人——BlenderBot 3,但輸出不夠安全,很快下線;再后來,Meta發(fā)布一個專門為科學(xué)研究設(shè)計的模型Galactica,但用戶期望過高,發(fā)布三天后又下線
23年2.24日,Meta通過論文《LLaMA: Open and Efficient Foundation Language Models》發(fā)布了自家的大型語言模型LLaMA(這是解讀之一),有多個參數(shù)規(guī)模的版本(7B 13B 33B 65B)
LLaMA只使用公開的數(shù)據(jù)(總計1.4T即1,400GB的token,其中CommonCrawl的數(shù)據(jù)占比67%,C4數(shù)據(jù)占比15%,Github Wikipedia Books這三項數(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)練后,也能達到甚至超過大模型的效果
- 比如13B參數(shù)的版本在多項基準(zhǔn)上測試的效果好于2020年的參數(shù)規(guī)模達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.2.1?項目環(huán)境依賴:torch、fairscale、fire、sentencepiece
此項目給出的環(huán)境依賴有4個:
1.2.2 RMSNorm:對每個Transformer子層的輸入進行歸一化
為了提高訓(xùn)練的穩(wěn)定性,對每個transformer子層的輸入進行歸一化,而不是對輸出進行歸一化,且使用由Zhang和Sennrich(2019)提出的RMSNorm(Root Mean Square Layer Normalization)
RMS Norm是一般LayerNorm的一種變體,可以在梯度下降時令損失更加平滑
與layerNorm相比,RMS Norm的主要區(qū)別在于去掉了減去均值的部分(re-centering),只保留方差部分(re-scaling)
為一目了然,我們看下它們各自的歸一化的表達式
- 一般的LN: 其中
- RMS Norm: 其中
至于RMS Norm為什么有用,需要求梯度進行分析,感興趣的同學(xué)可以閱讀RMS Norm的論文
class RMSNorm(torch.nn.Module):def __init__(self, dim: int, eps: float = 1e-6):super().__init__()// eps防止取倒數(shù)之后分母為0self.eps = epsself.weight = nn.Parameter(torch.ones(dim))// x是輸入def _norm(self, x):// torch.rsqrt是開平方并取倒數(shù)// x.pow(2)是平方/ mean(-1)是在最后一個維度(即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ù),即gireturn output * self.weight1.2.3?SwiGLU替代ReLU
用Shazeer(2020)提出的SwiGLU替代ReLU,在維度上使用的維度是2/3*4d,而不是PaLM中的4d
LLaMA采用SwiGLU替換了原有的ReLU,具體是采用SwiGLU的FNN,在論文中以如下公式進行表述:
其中
對應(yīng)論文見:Ramachandran et al., 2017
代碼實現(xiàn)上:可以通過調(diào)用torch內(nèi)置方法F.silu()實現(xiàn),會在下文的FFN部分介紹
1.2.4 位置編碼:旋轉(zhuǎn)位置嵌入(RoPE)
在位置編碼上,刪除了絕對位置嵌入,而在網(wǎng)絡(luò)的每一層增加了蘇劍林等人(2021)提出的旋轉(zhuǎn)位置嵌入(RoPE),其思想是采用絕對位置編碼的形式,實現(xiàn)相對位置編碼
- RoPE主要借助了復(fù)數(shù)的思想,為了引入復(fù)數(shù),首先假設(shè)了在加入位置信息之前,原有的編碼向量是二維行向量和,其中和是絕對位置,現(xiàn)在需要構(gòu)造一個變換,將和引入到和中,即尋找變換:? 考慮到Attention的核心計算是內(nèi)積: 所以,尋求的這個變換,應(yīng)該具有特性:
- 這里直接說結(jié)論,尋求的變換就是,也就是給乘以,相應(yīng)地,乘以
做了這樣一個變換之后,根據(jù)復(fù)數(shù)的特性,有: 也就是,如果把二維向量看做復(fù)數(shù),那么它們的內(nèi)積,等于一個復(fù)數(shù)乘以另一個復(fù)數(shù)的共軛,得到的結(jié)果再取實部,代入上面的變換,也就有: 這樣一來,內(nèi)積的結(jié)果就只依賴于,也就是相對位置了
換言之,經(jīng)過這樣一番操作,通過給Embedding添加絕對位置信息,可以使得兩個token的編碼,經(jīng)過內(nèi)積變換(self-attn)之后,得到結(jié)果是受它們位置的差值,即相對位置影響的 - 于是對于任意的位置為的二維向量,把它看做復(fù)數(shù),乘以,而根據(jù)歐拉公式,有: 于是上述的相乘變換也就變成了: 把上述式子寫成矩陣形式: 而這個變換的幾何意義,就是在二維坐標(biāo)系下,對向量進行了旋轉(zhuǎn),因而這種位置編碼方法,被稱為旋轉(zhuǎn)位置編碼
- 根據(jù)剛才的結(jié)論,結(jié)合內(nèi)積的線性疊加性,可以將結(jié)論推廣到高維的情形。可以理解為,每兩個維度一組,進行了上述的“旋轉(zhuǎn)”操作,然后再拼接在一起: 由于矩陣的稀疏性,會造成計算上的浪費,所以在計算時采用逐位相乘再相加的方式進行: 其中為矩陣逐位相乘操作
原理理解了,接下來可以代碼實現(xiàn)旋轉(zhuǎn)位置編碼
def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0):// 首先torch.arange創(chuàng)建了一個tensor,[ 0 , 2 , 4 , . . . , 60 , 62 ] [0, 2, 4, ..., 60, 62][0,2,4,...,60,62]// 然后統(tǒng)一除以64,把它變成分?jǐn)?shù),然后整體作為基礎(chǔ)角度的指數(shù),它的shape是(32)freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim))// t比較容易理解,也就是絕對位置信息,它的shape是(1024)t = torch.arange(end, device=freqs.device)// torch.outer是把一個向量的轉(zhuǎn)置乘以另一個向量:torch.outer(a, b) = a^T * b// 于是根據(jù)torch.outer運算,我們得到了一個shape為(1024, 32)的tensor。其意義也就是將每一個絕對位置,分配到對應(yīng)的角度,相乘// 直觀理解一下,就是每一個絕對位置上,都有32個角度// 為什么是這樣的呢,回顧計算的公式,對于旋轉(zhuǎn)矩陣,每兩個元素為一組,它們乘以的角度是同一個θ,所以這個(1024, 32)// 在后續(xù)的過程中,就可以reshape成(512, 64),并且在64的那個維度上,每兩個是相同的freqs = torch.outer(t, freqs).float()// torch.polar(abs, angle)利用一個絕對數(shù)值和一個角度值,從而在極坐標(biāo)下構(gòu)造一個復(fù)數(shù)張量// 即abs?cos(angle)+abs?sin(angle)j// torch.polar(torch.tensor([1], dtype=torch.float64), torch.tensor([np.pi / 2], dtype=torch.float64))// # tensor([6.1232e-17+1.j], dtype=torch.complex128)// freqs_cis其實就是需要計算出來的mθ,也就是跟絕對位置相關(guān)的旋轉(zhuǎn)的角度,在極坐標(biāo)下對應(yīng)的復(fù)數(shù)tensor// 這一步就是在生成我們需要的位置信息// 直觀理解一下,像是在復(fù)平面內(nèi),以原點為中心,轉(zhuǎn)了1024組,每一組64個的單位向量,它的shape是(1024, 64)freqs_cis = torch.polar(torch.ones_like(freqs), freqs) # complex64return freqs_cis// 第二個函數(shù)reshape_for_broadcast,是把freqs_cis變成和輸入的tensor相同的形狀 def reshape_for_broadcast(freqs_cis: torch.Tensor, x: torch.Tensor):ndim = x.ndimassert 0 <= 1 < ndimassert freqs_cis.shape == (x.shape[1], x.shape[-1])// 這個方法的作用是為了把freqs_cis變成和輸入的tensor相同的形狀// 需要注意的是,這里的freqs_cis并不是precompute_freqs_cis生成的形狀為(1024, 64)的那個tensor// 而是根據(jù)輸入的絕對位置,在(1024, 64)的tensor中,截取了長度為當(dāng)前seq_len的一部分// 代碼在Transformer類的forward方法中freqs_cis = self.freqs_cis[start_pos : start_pos + seqlen]// 也就是說,假如當(dāng)前輸入的序列長度是512,那么截取出來的這個新的freqs_cis,形狀就是(512, 64)// reshape之后,形狀就變成了(1, 512, 1, 32),也就是在每一個位置上,都對應(yīng)有32個角度// 根據(jù)上面torch.polar的介紹,當(dāng)我們固定絕對值(也就是向量的模長)時,角度就可以在笛卡爾坐標(biāo)系下唯一確定一個復(fù)數(shù)// 這樣一來也就是32個復(fù)數(shù),即64個特征維度,所以就可以對應(yīng)的將它融合到每個attention head的64個特征中去了shape = [d if i == 1 or i == ndim - 1 else 1 for i, d in enumerate(x.shape)]return freqs_cis.view(*shape)// apply_rotary_emb方法,這個方法其實就是把位置信息添加到原有的編碼結(jié)果上,在multi-head attention階段調(diào)用 def apply_rotary_emb(xq: torch.Tensor,xk: torch.Tensor,freqs_cis: torch.Tensor, ) -> Tuple[torch.Tensor, torch.Tensor]:// torch.view_as_complex是把一個tensor轉(zhuǎn)為復(fù)數(shù)形式// 比如torch.view_as_complex(torch.Tensor([[1, 2], [3, 4], [5, 6]]))// # tensor([1.+2.j, 3.+4.j, 5.+6.j])// 假設(shè)輸入x_q的尺寸就是(2, 512, 12, 64)// 那么這一句操作的reshape,就是把它變成(2, 512, 12, -1, 2),也就是(2, 512, 12, 32, 2)。x_k同理,略// 緊接著把它變成復(fù)數(shù)形式,也就是變成了(2, 512, 12, 32)的形狀。xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2))xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2))// 然后進入到上面的第二個函數(shù)reshape_for_broadcastfreqs_cis = reshape_for_broadcast(freqs_cis, xq_)// torch.view_as_real是把復(fù)數(shù)tensor變回實數(shù)// torch.view_as_real(torch.view_as_complex(torch.Tensor([[1, 2], [3, 4], [5, 6]])))// # tensor([[1., 2.],// # [3., 4.],// # [5., 6.]])// reshape之后,就是將位置信息融入query和key中// 這一步將二者相乘得到的復(fù)數(shù)tensor,重新轉(zhuǎn)換為實數(shù)形式,得到的shape為(2, 512, 12, 32, 2)// 然后再flatten成(2, 512, 12, 64),這樣一來,就變回了和最開始x_q相同的形狀,也就完成了將位置信息融入到x_q的這一操作,x_k同理xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3)xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3)return xq_out.type_as(xq), xk_out.type_as(xk)引用此文的介紹,再著重解釋下precompute_freqs_cis的作用
- 假設(shè)
batch_size為2
seq_len固定為512
attention_head的數(shù)量為12
每個attention_head的維度為64,那么,對于輸入到multi-head attn中的輸入的尺寸就是
(2, 512, 12, 64) - 而freqs_cis其實就是需要計算出來的也就是跟絕對位置相關(guān)的旋轉(zhuǎn)的角度,在極坐標(biāo)下對應(yīng)的復(fù)數(shù)tensor
而precompute_freqs_cis就是提前將這些旋轉(zhuǎn)角度對應(yīng)的tensor給創(chuàng)建出來,并可以重復(fù)利用。因為確定了序列的最大長度,所以這個tensor是固定死的。根據(jù)后續(xù)的數(shù)據(jù)流我們可以發(fā)現(xiàn),在調(diào)用該函數(shù)時,傳入的兩個參數(shù)分別是attention_head的維度,以及最大長度的兩倍,具象地,也就是64和1024
1.2.4 Transform架構(gòu)的實現(xiàn):Attention計算、SA、FFN
LLaMA和GPT一樣,都是基于Transformer這個架構(gòu),通常,我們在構(gòu)建transformer時,是按Block構(gòu)建的,每個transformer Block包含SA和FFN兩部分,然后再通過堆疊block的形式,構(gòu)建起整個transformer網(wǎng)絡(luò),LLaMA也是這樣做的
回顧一下Attention計算的總體過程是:
其中有一個細節(jié)就是緩存機制,它設(shè)計的目的是在generate時減少token的重復(fù)計算。簡單解釋一下,就是在計算第n個token特征的時候,需要用到第個token,即每次生成時,需要知道前面所有的過往信息,如果每次都從頭算的話,那就會造成極大的浪費,所以就沒算一個位置的信息,就把它緩存下來
接下來,我們來看下代碼實現(xiàn),首先是SA部分:
然后是FFN部分,需要注意的點就是采用的激活函數(shù),以及激活函數(shù)的位置
class FeedForward(nn.Module):def __init__(self,dim: int,hidden_dim: int,multiple_of: int,):super().__init__()hidden_dim = int(2 * hidden_dim / 3)hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)self.w1 = ColumnParallelLinear(dim, hidden_dim, bias=False, gather_output=False, init_method=lambda x: x)self.w2 = RowParallelLinear(hidden_dim, dim, bias=False, input_is_parallel=True, init_method=lambda x: x)self.w3 = ColumnParallelLinear(dim, hidden_dim, bias=False, gather_output=False, init_method=lambda x: x)def forward(self, x):return self.w2(F.silu(self.w1(x)) * self.w3(x))這里與常見模型中的FFN做一下簡單的對比
- BART中的FFN,用的是fc->act->fc,用了兩層全連接
- GPT中的FFN,用的是conv1D->act->conv1D,也是只用了兩層
- 而LLaMA中的FFN采用了三個全連接層以實現(xiàn)FFNSwiGLU,即
然后將SA和FFN這兩部分拼在一起就是一個transformer block
class TransformerBlock(nn.Module):def __init__(self, layer_id: int, args: ModelArgs):super().__init__()self.n_heads = args.n_headsself.dim = args.dimself.head_dim = args.dim // args.n_headsself.attention = Attention(args)self.feed_forward = FeedForward(dim=args.dim, hidden_dim=4 * args.dim, multiple_of=args.multiple_of)self.layer_id = layer_idself.attention_norm = RMSNorm(args.dim, eps=args.norm_eps)self.ffn_norm = RMSNorm(args.dim, eps=args.norm_eps)def forward(self, x: torch.Tensor, start_pos: int, freqs_cis: torch.Tensor, mask: Optional[torch.Tensor]):h = x + self.attention.forward(self.attention_norm(x), start_pos, freqs_cis, mask)out = h + self.feed_forward.forward(self.ffn_norm(h))return out最后利用torch的module list將transformer block進行堆疊,拼上最前頭的embedding部分,就是一個完整的transformer decoder結(jié)構(gòu)了
class Transformer(nn.Module):def __init__(self, params: ModelArgs):super().__init__()self.params = paramsself.vocab_size = params.vocab_sizeself.n_layers = params.n_layersself.tok_embeddings = ParallelEmbedding(params.vocab_size, params.dim, init_method=lambda x: x)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)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,先做token embedding,然后添加位置信息h = self.tok_embeddings(tokens)self.freqs_cis = self.freqs_cis.to(h.device)freqs_cis = self.freqs_cis[start_pos : start_pos + seqlen]// 對于decoder模型,為了防止標(biāo)簽泄漏,需要mask,所以做了一個上三角的mask矩陣mask = Noneif 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)// 接下來就是逐層的計算transformerfor layer in self.layers:h = layer(h, start_pos, freqs_cis, mask)h = self.norm(h)output = self.output(h[:, -1, :]) # only compute last logitsreturn output.float()接著看下生成過程,如下:
代碼如下
class LLaMA:def __init__(self, model: Transformer, tokenizer: Tokenizer):self.model = modelself.tokenizer = tokenizerdef generate(self,prompts: List[str],max_gen_len: int,temperature: float = 0.8,top_p: float = 0.95,) -> List[str]:bsz = len(prompts)params = self.model.paramsassert bsz <= params.max_batch_size, (bsz, params.max_batch_size)prompt_tokens = [self.tokenizer.encode(x, bos=True, eos=False) for x in prompts]min_prompt_size = min([len(t) for t in prompt_tokens])max_prompt_size = max([len(t) for t in prompt_tokens])total_len = min(params.max_seq_len, max_gen_len + max_prompt_size)tokens = torch.full((bsz, total_len), self.tokenizer.pad_id).cuda().long()for k, t in enumerate(prompt_tokens):tokens[k, : len(t)] = torch.tensor(t).long()input_text_mask = tokens != self.tokenizer.pad_idstart_pos = min_prompt_sizeprev_pos = 0for cur_pos in range(start_pos, total_len):logits = self.model.forward(tokens[:, prev_pos:cur_pos], prev_pos)if temperature > 0:probs = torch.softmax(logits / temperature, dim=-1)next_token = sample_top_p(probs, top_p)else:next_token = torch.argmax(logits, dim=-1)next_token = next_token.reshape(-1)# only replace token if prompt has already been generatednext_token = torch.where(input_text_mask[:, cur_pos], tokens[:, cur_pos], next_token)tokens[:, cur_pos] = next_tokenprev_pos = cur_posdecoded = []for i, t in enumerate(tokens.tolist()):# cut to max gen lent = t[: len(prompt_tokens[i]) + max_gen_len]# cut to eos tok if anytry:t = t[: t.index(self.tokenizer.eos_id)]except ValueError:passdecoded.append(self.tokenizer.decode(t))return decodeddef sample_top_p(probs, p):probs_sort, probs_idx = torch.sort(probs, dim=-1, descending=True)probs_sum = torch.cumsum(probs_sort, dim=-1)mask = probs_sum - probs_sort > pprobs_sort[mask] = 0.0probs_sort.div_(probs_sort.sum(dim=-1, keepdim=True))next_token = torch.multinomial(probs_sort, num_samples=1)next_token = torch.gather(probs_idx, -1, next_token)return next_token1.3 LLaMA的Optimizer設(shè)計、模型加速優(yōu)化與微型版本
在Optimizer設(shè)計上
- 該模型使用AdamW優(yōu)化器(Loshchilov和Hutter,2017)進行訓(xùn)練,超參數(shù)設(shè)置為β1=0.9,β2=0.95
此外,使用余弦學(xué)習(xí)率方式,使最終學(xué)習(xí)率等于最大學(xué)習(xí)率的10%,以及使用0.1的權(quán)重衰減和1.0的梯度剪裁,和2000個warm up策略,使得可以根據(jù)模型的大小改變學(xué)習(xí)率和批次大小
在模型的加速優(yōu)化方面
具體原理為通過不存儲注意力權(quán)重和不計算由于語言建模任務(wù)的因果性質(zhì)而被掩蓋的鍵/查詢分?jǐn)?shù)來實現(xiàn)的
為了充分受益于這種優(yōu)化,還通過如Korthikanti等人(2022)中采用的方法,進行使用模型和序列并行來減少模型的內(nèi)存使用
最終的優(yōu)化性能效果為:當(dāng)訓(xùn)練一個65B參數(shù)的模型時,代碼在2048A100的GPU上處理大約380個token/秒/GPU,并耗費80GB的內(nèi)存,這意味著對包含1.4Ttoken的數(shù)據(jù)集進行訓(xùn)練大約花費了21天
LLaMA發(fā)布不久后,一些研究者基于它做了不少工作
- 一開始最小參數(shù)7B的模型也需要近30GB的GPU才能運行,但通過比特和字節(jié)庫進行浮點優(yōu)化,能夠讓模型在單個NVIDIA RTX 3060(顯存一般12G)上運行
- 之后,GitHub 上的一名研究人員甚至能夠在Ryzen 7900X CPU上運行LLM的7B 版本,每秒能推斷出幾個單詞
- 再之后,有研究者推出了llama.cpp,無需 GPU,就能運行 LLaMA
llama.cpp 項目實現(xiàn)了在MacBook上運行 LLaMA,還有開發(fā)者成功的在 4GB RAM 的樹莓派上運行了 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 什么是self-instruct方式:提示GPT3/GPT3.5/GPT4的API收集數(shù)據(jù)
3月中旬,斯坦福發(fā)布Alpaca(中文名:羊駝):號稱只花100美元,人人都可微調(diào)Meta家70億參數(shù)的LLaMA大模型(即LLaMA 7B),具體做法是通過52k指令數(shù)據(jù),然后在8個80GB A100上訓(xùn)練3個小時,使得Alpaca版的LLaMA 7B在單純對話上的性能比肩GPT-3.5(text-davinci-003),這便是指令調(diào)優(yōu)LLaMA的意義所在
- 論文《Alpaca: A Strong Open-Source Instruction-Following Model》
- 代碼地址:https://github.com/tatsu-lab/stanford_alpaca
可能有讀者有疑問,即52k數(shù)據(jù)都長啥樣呢?這52K數(shù)據(jù)存在Alpaca項目的alpaca_data.json文件中,這個JSON文件是一個字典列表,每個字典包含以下字段:
- instruction: str,描述了模型應(yīng)該執(zhí)行的任務(wù),52K 條指令中的每一條都是唯一的
- input: str,要么是上下文,要么直接輸入(optional context or input for the task),例如,當(dāng)指令是“總結(jié)以下文章”時,輸入就是文章,大約 40% 的示例有輸入
- output: str,由GPT3.5對應(yīng)的API即 text-davinci-003生成的指令的答案
而斯坦福團隊微調(diào)LLaMA 7B所用的52K指令數(shù)據(jù),便是通過Self-Instruct『Self-Instruct是來自華盛頓大學(xué)Yizhong Wang等22年12月通過這篇論文《SELF-INSTRUCT: Aligning Language Model with Self Generated Instructions》提出的』提示GPT3的API拿到的
?具體而言,論文中提出
一直重復(fù)上述2-6步直到種子池有足夠多的數(shù)據(jù)
關(guān)鍵代碼如下: def openai_completion(prompts: Union[str, Sequence[str], Sequence[dict[str, str]], dict[str, str]],decoding_args: OpenAIDecodingArguments,model_name="text-davinci-003",sleep_time=2,batch_size=1,max_instances=sys.maxsize,max_batches=sys.maxsize,return_text=False,**decoding_kwargs, ) -> Union[Union[StrOrOpenAIObject], Sequence[StrOrOpenAIObject], Sequence[Sequence[StrOrOpenAIObject]],]:"""prompts:輸入提示,可以是單個字符串、字符串列表、字典或字典列表decoding_args:解碼參數(shù),用于指定如何生成文本model_name:要使用的模型名稱,默認(rèn)為"text-davinci-003"sleep_time:在達到速率限制時,程序暫停的時間(以秒為單位)batch_size:在單個請求中發(fā)送的prompts的數(shù)量max_instances:要解碼的prompts的最大數(shù)量max_batches:要解碼的批次的最大數(shù)量(此參數(shù)將在未來被棄用)return_text:如果為True,則返回文本而不是包含諸如logprob等信息的完整completion對象decoding_kwargs:其他解碼參數(shù),例如best_of和logit_bias"""# 函數(shù)首先檢查是否有單個prompt,如果是,則將其轉(zhuǎn)換為列表# 然后,根據(jù)最大實例數(shù)量截取prompts。接著,將prompts分成批次以便進行批處理is_single_prompt = isinstance(prompts, (str, dict))if is_single_prompt:prompts = [prompts]if max_batches < sys.maxsize:logging.warning("`max_batches` will be deprecated in the future, please use `max_instances` instead.""Setting `max_instances` to `max_batches * batch_size` for now.")max_instances = max_batches * batch_sizeprompts = prompts[:max_instances]num_prompts = len(prompts)prompt_batches = [prompts[batch_id * batch_size : (batch_id + 1) * batch_size]for batch_id in range(int(math.ceil(num_prompts / batch_size)))]'''函數(shù)遍歷這些批次,并嘗試與OpenAI API進行交互。當(dāng)遇到OpenAIError時,會根據(jù)錯誤類型采取不同的措施如果是因為速率限制,程序?qū)和R欢螘r間再重試。如果提示過長,程序?qū)p小目標(biāo)長度再重試。'''completions = []for batch_id, prompt_batch in tqdm.tqdm(enumerate(prompt_batches),desc="prompt_batches",total=len(prompt_batches),):batch_decoding_args = copy.deepcopy(decoding_args) # cloning the decoding_argswhile True:try:shared_kwargs = dict(model=model_name,**batch_decoding_args.__dict__,**decoding_kwargs,)completion_batch = openai.Completion.create(prompt=prompt_batch, **shared_kwargs)choices = completion_batch.choicesfor choice in choices:choice["total_tokens"] = completion_batch.usage.total_tokenscompletions.extend(choices)breakexcept openai.error.OpenAIError as e:logging.warning(f"OpenAIError: {e}.")if "Please reduce your prompt" in str(e):batch_decoding_args.max_tokens = int(batch_decoding_args.max_tokens * 0.8)logging.warning(f"Reducing target length to {batch_decoding_args.max_tokens}, Retrying...")else:logging.warning("Hit request rate limit; retrying...")time.sleep(sleep_time) # Annoying rate limit on requests.# 最后,函數(shù)根據(jù)return_text、decoding_args.n以及是否為單個prompt的情況返回不同類型的結(jié)果if return_text:completions = [completion.text for completion in completions]if decoding_args.n > 1:# make completions a nested list, where each entry is a consecutive decoding_args.n of original entries.completions = [completions[i : i + decoding_args.n] for i in range(0, len(completions), decoding_args.n)]if is_single_prompt:# Return non-tuple if only 1 input and 1 generation.(completions,) = completionsreturn completions
而斯坦福的Alpaca,就是花了不到500美元使用OpenAI API生成了5.2萬個這樣的示例微調(diào)LLaMA搞出來的,個人覺得可以取名為?instructLLaMA-7B,^_^
值得一提的是,后來23年4月有微軟的研究者提示GPT4的API進行指令微調(diào)「論文地址:INSTRUCTION TUNING WITH GPT-4、GitHub地址:instruction-Tuning-with-GPT-4、項目地址:使用GPT4進行指令調(diào)優(yōu)」,從而生成以下數(shù)據(jù)
- English Instruction-Following Data,generated by GPT-4 using Alpaca prompts
這部分?jǐn)?shù)據(jù)在項目文件?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ù)的中文翻譯,存儲在項目文件alpaca_gpt4_data_zh.json 里
- Comparison Data ranked by GPT-4,好訓(xùn)練一個獎勵模型
存儲在?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.2 微調(diào)LLM時一般都會用到Hugging face實現(xiàn)的Transformers庫的Trainer類
可能有讀者疑問,那微調(diào)的代碼長啥樣呢?實際上,微調(diào)步驟大同小異,據(jù)代碼:tatsu-lab/stanford_alpaca · GitHub,可得微調(diào)的步驟如下
- safe_save_model_for_hf_trainer:安全地保存訓(xùn)練器中的模型;
- smart_tokenizer_and_embedding_resize:調(diào)整分詞器和詞嵌入大小;
- _tokenize_fn:將字符串序列進行分詞;
- preprocess:預(yù)處理數(shù)據(jù),對源數(shù)據(jù)和目標(biāo)數(shù)據(jù)進行分詞。
a. 解析命令行參數(shù):使用transformers.HfArgumentParser解析命令行參數(shù),將它們分為模型參數(shù)、數(shù)據(jù)參數(shù)和訓(xùn)練參數(shù)
b.?加載預(yù)訓(xùn)練模型:使用transformers.AutoModelForCausalLM.from_pretrained從預(yù)訓(xùn)練的模型檢查點加載一個用于因果語言建模的模型
c.?加載分詞器:使用transformers.AutoTokenizer.from_pretrained從預(yù)訓(xùn)練的模型檢查點加載分詞器
d.?為分詞器添加特殊字符:根據(jù)需要,將特殊字符添加到分詞器中
e. 創(chuàng)建數(shù)據(jù)集和整理器:使用make_supervised_data_module函數(shù)為監(jiān)督學(xué)習(xí)任務(wù)創(chuàng)建數(shù)據(jù)集和整理器
f. 實例化Trainer類:實例化transformers.Trainer類,并傳入模型、分詞器、訓(xùn)練參數(shù)以及數(shù)據(jù)集。Trainer類負責(zé)管理訓(xùn)練過程
g.?訓(xùn)練模型:調(diào)用Trainer類的train()方法對模型進行微調(diào),相當(dāng)于鏈路就是:transformers庫 ?Trainer類 ?train函數(shù)
h.?保存模型狀態(tài):在訓(xùn)練完成后,調(diào)用Trainer.save_state()方法保存模型的狀態(tài)
i.?將訓(xùn)練器的模型安全地保存到磁盤:使用safe_save_model_for_hf_trainer函數(shù)將訓(xùn)練器中的模型安全地保存到磁盤
可能,很快便有同學(xué)疑問,怎么沒有預(yù)想中的損失計算、梯度下降、參數(shù)更新呢,實際上這三步的具體實現(xiàn)都封裝在了Hugging face社區(qū)實現(xiàn)的鼎鼎大名的transformers的Trainer類中:transformers/trainer.py at main · huggingface/transformers · GitHub
這個?transformers/trainer.py?文件的主要部分如下
????????? 導(dǎo)入:文件首先導(dǎo)入了一些必要的Python庫,如os、sys、logging以及其他一些庫。它還導(dǎo)入了Hugging Face庫中的一些相關(guān)模塊,如datasets、transformers等
????????? TrainerState:這個類用于保存訓(xùn)練器的狀態(tài),包括當(dāng)前的epoch、迭代步數(shù)、最佳指標(biāo)值等
????????? TrainOutput:這個類用于返回訓(xùn)練過程的結(jié)果,包括訓(xùn)練損失、訓(xùn)練步數(shù)等
????????? TrainerControl:這個類提供了一種用于控制訓(xùn)練循環(huán)的機制,例如,當(dāng)用戶想要在某個特定的迭代步數(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:這個方法負責(zé)整個訓(xùn)練過程,它包括遍歷數(shù)據(jù)集、計算損失、計算梯度、更新模型參數(shù)以及日志記錄等
- 遍歷數(shù)據(jù)集:train方法通過使用dataloader來遍歷訓(xùn)練數(shù)據(jù)集 for step, inputs in enumerate(epoch_iterator):
- 計算損失:損失計算在training_step方法中,接收輸入數(shù)據(jù)并產(chǎn)生預(yù)測輸出,然后,這個預(yù)測輸出會與真實輸出(標(biāo)簽)進行比較,以計算損失 outputs = model(**inputs) 上述代碼行使用model(已經(jīng)加載了預(yù)訓(xùn)練模型)和inputs(包含輸入數(shù)據(jù)的字典)計算模型的預(yù)測輸出。這個outputs變量包含模型預(yù)測的結(jié)果
接下來,我們從outputs中獲取預(yù)測結(jié)果,并與真實標(biāo)簽(即labels)進行比較,以計算損失 loss = outputs.loss outputs.loss是模型預(yù)測輸出和真實輸出(標(biāo)簽)之間的損失。這個損失值將用于計算梯度并更新模型參數(shù) - 計算梯度:loss.backward()這行代碼計算模型參數(shù)關(guān)于損失的梯度 loss.backward()
- 梯度累積:當(dāng)gradient_accumulation_steps大于1時,梯度會被累積,而不是立即更新模型參數(shù) if (step + 1) % self.args.gradient_accumulation_steps == 0:
- 更新模型參數(shù):optimizer.step()這行代碼根據(jù)計算出的梯度來更新模型參數(shù) self.optimizer.step()
- 學(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:這個方法用于評估模型在驗證數(shù)據(jù)集上的性能,返回評估結(jié)果
def evaluate(self, eval_dataset: Optional[Dataset] = None, ignore_keys: Optional[List[str]] = None ) -> Dict[str, float]:predict:這個方法用于在給定的數(shù)據(jù)集上進行預(yù)測,返回預(yù)測結(jié)果
def predict(self, test_dataset: Dataset, ignore_keys: Optional[List[str]] = None ) -> PredictionOutput:save_model:這個方法用于將訓(xùn)練好的模型保存到指定的目錄
def save_model(self, output_dir: Optional[str] = None):????????? ShardedDDPOption:這是一個可選的類,用于支持使用混合精度和ZeRO進行分布式訓(xùn)練
2.1.3 Alpaca-LoRA:通過PEFT庫在消費級GPU上微調(diào)「基于LLaMA的Alpaca」
在神經(jīng)網(wǎng)絡(luò)模型中,模型參數(shù)通常以矩陣的形式表示。對于一個預(yù)訓(xùn)練好的模型,其參數(shù)矩陣已經(jīng)包含了很多有用的信息。為了使模型適應(yīng)特定任務(wù),我們需要對這些參數(shù)進行微調(diào)
LoRA的核心思想是用一種低秩的方式來調(diào)整這些參數(shù)矩陣。在數(shù)學(xué)上,低秩意味著一個矩陣可以用兩個較小的矩陣相乘來近似,通過論文《LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS》可知(這是解讀之一)
? A是映射矩陣(隨機高斯分布初始化),維度上是升維
? B是逆映射矩陣(用0矩陣初始化),維度上是降維
其中,矩陣的大小由LoRA的秩(rank)和alpha值確定
注意,在更新過程中,原始參數(shù)矩陣W保持不變,說白了,訓(xùn)練的時候固定原始PLM的參數(shù),只訓(xùn)練降維矩陣A與升維矩陣B
總之,LoRA的詳細步驟包括選擇目標(biāo)層、初始化映射矩陣和逆映射矩陣、進行參數(shù)變換和模型微調(diào)。在微調(diào)過程中,模型會通過更新映射矩陣U和逆映射矩陣V來學(xué)習(xí)特定任務(wù)的知識,從而提高模型在該任務(wù)上的性能。
額外說一下,這個LoRA的應(yīng)用還是挺廣的,比如后續(xù)微軟推出的DeepSpeed-Chat便用了這個方法
DeepSpeed-Chat的實現(xiàn)中,當(dāng)設(shè)置LoRA的低秩維度lora_dim(如lora_dim=128)時,即認(rèn)為啟用了LoRA訓(xùn)練,則將原始模型中名稱含有“deoder.layers.”且為線性層修改為LoRA層,具體操作為:
再額外分析下 這段代碼的最后部分
# 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é)果進行加和”
在代碼中體現(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分別為兩個新引入線性層的參數(shù)
而Huggingface公司推出的PEFT(Parameter-Efficient Fine-Tuning)庫也封裝了LoRA這個方法,PEFT庫可以使預(yù)訓(xùn)練語言模型高效適應(yīng)各種下游任務(wù),而無需微調(diào)模型的所有參數(shù),即僅微調(diào)少量(額外)模型參數(shù),從而大大降低了計算和存儲成本
| 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庫支持以下流行的方法
而Alpaca-LoRA則可以通過PEFT庫實現(xiàn)的LoRA方法在消費級GPU微調(diào)「基于LLaMA的Alpaca」,比如項目中的這個文件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ù)(為方便大家理解,我給每個參數(shù)都加了注釋說明):
python finetune.py \ # 運行微調(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è)置每個批次的樣本數(shù)量--micro_batch_size 4 \ # 設(shè)置每個小批次的樣本數(shù)量--num_epochs 3 \ # 設(shè)置訓(xùn)練的輪次(epoch)--learning_rate 1e-4 \ # 設(shè)置學(xué)習(xí)速率--cutoff_len 512 \ # 設(shè)置截斷長度--val_set_size 2000 \ # 設(shè)置驗證集的大小--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進行微調(diào)的模型模塊--train_on_inputs # 指示模型在訓(xùn)練時使用輸入文本2.1.4 Alpaca所用的self-instruct的影響力:解決一大批模型的數(shù)據(jù)擴展問題
很快,通過下文你會發(fā)現(xiàn)
羊駝率先帶動的self-instruct,啟發(fā)后續(xù)很多人/團隊也用這個方式去采集『提示ChatGPT API』的數(shù)據(jù),比如BELLE、ChatLLaMA、ColossalChat
然后還有一批模型各種疊加組合比如『Alpaca/BELLE』,又用于微調(diào)一批批模型
比如ChatDoctor 有用到Alpaca的數(shù)據(jù)進行微調(diào),再比如有人拿BELLE數(shù)據(jù)tuning去調(diào)chatglm
?一下子出來這么新的模型 似乎有點懵,沒事,請看下文及下一篇文章娓娓道來..
2.2 Vicuna/FastChat:通過ShareGPT.com的7萬條對話數(shù)據(jù)微調(diào)LLaMA
23年3.31日,受 Meta LLaMA 和 Stanford Alpaca 項目的啟發(fā),加州大學(xué)伯克利分校(UC?Berkeley)等大學(xué)的研究者根據(jù)從 ShareGPT.com (ShareGPT是一個用戶可以分享他們的 ChatGPT 對話的網(wǎng)站)收集的用戶共享對話微調(diào) LLaMA?推出了Vicuna-13B(中文稱小羊駝,代碼地址:FastChat)。
在數(shù)據(jù)規(guī)模上,Vicuna從ShareGPT.com 的公共 API 收集了大約 70K 用戶共享對話,且為了確保數(shù)據(jù)質(zhì)量,原作者們將 HTML 轉(zhuǎn)換回 markdown 并過濾掉一些不合適或低質(zhì)量的樣本。此外,將冗長的對話分成更小的部分,以適應(yīng)模型的最大上下文長度,并做了以下改進
- 內(nèi)存優(yōu)化:為了使 Vicuna 能夠理解長上下文,將最大上下文長度從羊駝中的 512 擴展到 2048,這大大增加了 GPU 內(nèi)存需求,對此通過利用梯度檢查點和閃存注意力來解決內(nèi)存壓力
- 多輪對話:調(diào)整訓(xùn)練損失以考慮多輪對話,并僅根據(jù)聊天機器人的輸出計算微調(diào)損失。
- 通過Spot Instance 降低成本:40 倍大的數(shù)據(jù)集和 4 倍的訓(xùn)練序列長度對訓(xùn)練費用提出了相當(dāng)大的挑戰(zhàn)。原作者們使用SkyPilot managed spot 來降低成本『SkyPilot是加州大學(xué)伯克利分校構(gòu)建的一個框架,用于在各種云上輕松且經(jīng)濟高效地運行 ML 工作負載』,方法是利用更便宜的spot instances以及auto-recovery for preemptions and auto zone switch
該解決方案將 7B 模型的訓(xùn)練成本從 500 美元削減至 140 美元左右,將 13B 模型的訓(xùn)練成本從 1000 美元左右削減至 300 美元
有兩點值得一提的是
| Vicuna-13B | 128 | 2e-5 | 3 | 2048 | 0 |
最終通過直接使用GPT4評估之后,效果還不錯
| 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 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』來了(這是項目地址)。
在數(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實例實現(xiàn)調(diào)優(yōu)的模型:BELLE-LLAMA-7B-2M,BELLE-LLAMA-13B-2M
BLOOM是由HuggingFace于2022年3月中旬推出的大模型,規(guī)模最大版本的參數(shù)量達到176B(GPT-3是175B),基于從 Megatron-LM GPT-2修改而來的僅解碼器 transformer 模型架構(gòu),對應(yīng)的論文為《BLOOM: A 176B-Parameter Open-Access Multilingual Language Model》(翻譯之一,解讀之一)
至于HuggingFace是著名開源工具Transformers的開發(fā)公司,很多推理工具都會支持Transformers中的模型
截至23年3月中旬,超過100B參數(shù)量且能夠支持中文的開源大模型只有BLOOM和GLM-130B
該項目主要包含以下三部分內(nèi)容:
- 175 個中文種子任務(wù),斯坦福Alpaca一樣,每個任務(wù)都包含對應(yīng)的指令/任務(wù)、prompt、輸出
? zh_seed_tasks.jsonl:樣例如下
?{??"id": "seed_task_20", "name": "horror_movie_opening",
"instruction": "你需要為一部恐怖電影寫一個創(chuàng)意的開場場景。",
"instances": [{"input": "","output":" 太陽已經(jīng)落山,留下了一個黑暗的小鎮(zhèn)。微風(fēng)吹拂空蕩的街道,讓每一個冒險走出門外的人感到一陣寒意。唯一的聲音是被風(fēng)吹動的樹葉發(fā)出的輕微沙沙聲。突然,一聲令人毛骨悚然的尖叫聲劃破了寂靜,隨后是玻璃破碎的聲音。一所房子亮起了燈光,可以看到一個人影朝鎮(zhèn)中心奔跑。當(dāng)> 那個人影越來越靠近時,清楚地看到那是一個年輕女子,她渾身血跡斑斑。"}],
"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兩個版本,項目地址),相當(dāng)于在原版LLaMA的基礎(chǔ)上擴充了中文詞表并使用了中文數(shù)據(jù)進行二次預(yù)訓(xùn)練,進一步提升了中文基礎(chǔ)語義理解能力,同時,在中文LLaMA的基礎(chǔ)上,且用中文指令數(shù)據(jù)進行指令精調(diào)得Chinese-Alpaca(也稱中文Alpaca,同樣也有7B和13B兩個版本)
具體而言,主要做了以下三方面的工作
2.4.1 詞表擴充中文數(shù)據(jù)
在通用中文語料上訓(xùn)練了基于sentencepiece的20K中文詞表并與原版LLaMA模型的32K詞表進行合并
排除重復(fù)的token后,得到的最終中文LLaMA詞表大小為49953
需要注意的是,在fine-tune階段Alpaca比LLaMA多一個pad token,所以中文Alpaca的詞表大小為49954
這么做的主要原因是原版LLaMA模型的詞表大小是32K,其主要針對英語進行訓(xùn)練,對多語種支持不是特別理想(可以對比一下多語言經(jīng)典模型XLM-R的詞表大小為250K)。通過初步統(tǒng)計發(fā)現(xiàn),LLaMA詞表中僅包含很少的中文字符,所以在切詞時會把中文切地更碎,需要多個byte token才能拼成一個完整的漢字,進而導(dǎo)致信息密度降低
其對應(yīng)的擴充詞表的腳本代碼為(為方便大家更好的理解,我給每一行的代碼 都加上了注釋)
這段代碼的主要目的是將一個中文的sentencepiece模型與一個已經(jīng)預(yù)訓(xùn)練好的LLaMA tokenizer進行合并,以便在處理中文文本時,LLaMA tokenizer能更好地進行分詞
整個過程包括了加載模型、合并模型、保存新的tokenizer以及進行測試等步驟,具體如下
在這個過程中,需要檢查每個中文詞匯是否已經(jīng)存在于LLaMA tokenizer中,以避免重復(fù)添加
即首先保存為sentencepiece模型文件,然后創(chuàng)建一個新的LlamaTokenizer實例,并將其保存為Hugging Face格式的tokenizer
測試包括輸出特殊詞匯、特殊詞匯ID、特殊詞匯映射等信息,以及使用這兩個tokenizer對給定文本進行分詞
從測試結(jié)果可以看出,合并后的Chinese-LLaMA tokenizer能夠更好地處理中文文本
此外,七月在線ChatGPT原理解析課一學(xué)員在群內(nèi)問道:“如何擴充詞表,訓(xùn)練embedding,然后再與llama的合并,想在自己的數(shù)據(jù)上試試”
“吹牛班的春天”答道:“我知道的方法就是直接改embedding結(jié)構(gòu):初始化參數(shù)concat到以前embedding層上,以前的權(quán)embedding權(quán)重就保留,多出來的部分就后面更新,下圖是以前BERT無損擴詞的思路,可做參考”
2.4.2 加入中文數(shù)據(jù)的預(yù)訓(xùn)練
在預(yù)訓(xùn)練階段,使用約20G左右的通用中文語料(與中文BERT-wwm、MacBERT、LERT、PERT中使用的語料一致)在原版LLaMA權(quán)重的基礎(chǔ)上進一步進行預(yù)訓(xùn)練。該過程又分為兩個階段:
第一階段:凍結(jié)transformer參數(shù),僅訓(xùn)練embedding,在盡量不干擾原模型的情況下適配新增的中文詞向量
第二階段:使用LoRA技術(shù),為模型添加LoRA權(quán)重(adapter),訓(xùn)練embedding的同時也更新LoRA參數(shù)
2.4.3 指令精調(diào)
指令精調(diào)階段的任務(wù)形式基本與Stanford Alpaca相同,訓(xùn)練方案同樣采用了LoRA進行高效精調(diào),并進一步增加了可訓(xùn)練參數(shù)數(shù)量
在prompt設(shè)計上,精調(diào)以及預(yù)測時采用的都是原版Stanford Alpaca不帶input的模版。對于包含input字段的數(shù)據(jù),采用f"{instruction}+\n+{input}"的形式進行拼接
且指令精調(diào)階段使用了以下數(shù)據(jù),其中7B模型約2M數(shù)據(jù)、13B模型約3M數(shù)據(jù)。基本構(gòu)成如下:
| 中英翻譯數(shù)據(jù) | 500K | 外部鏈接 | 在原數(shù)據(jù)集的基礎(chǔ)上進行了采樣+規(guī)則篩選 |
| pCLUE數(shù)據(jù) | 300K | 外部鏈接 | 在原數(shù)據(jù)集的基礎(chǔ)上進行了采樣+規(guī)則篩選 |
| Alpaca數(shù)據(jù)(英) | 50K | 外部鏈接 | 斯坦福原版Alpaca訓(xùn)練數(shù)據(jù) |
| Alpaca數(shù)據(jù)(中) | 50K | 本地鏈接 | 本項目使用ChatGPT接口將英文版翻譯為中文(篩掉一部分) |
| Self-instruction數(shù)據(jù) | 1~2M | (暫無) | 本項目使用ChatGPT接口進行爬取,提供了一個動態(tài)生成不同領(lǐng)域和指令類型的prompt爬取腳本script/crawl_prompt.py。 python script/crawl_prompt.py output-file |
當(dāng)然,針對一些任務(wù)上效果不好!原作者也給出了幾個可能的原因,
????????1)本身LLaMA對中文支持不是很好,大多數(shù)相關(guān)衍生工作是直接在原版上進行pretrain/finetune的,而我們采取了更大膽的策略——增加中文詞表,可能進一步加劇中文訓(xùn)練不充分的問題,但從長遠看是否有利于后續(xù)進一步預(yù)訓(xùn)練就得靠時間檢驗了;
????????2)指令數(shù)據(jù)的質(zhì)量有待進一步提升;
????????3)訓(xùn)練時間、超參等方面還有很大調(diào)整空間;
????????4)沒有RLHF;
????????5)4-bit量化后效果可能會下降,因此可以嘗試加載FP16模型,效果相對更好一些(也更慢)
2.5? 小結(jié):基于LLaMA微調(diào)的各模型對比:Alpaca/Vicuna/BELLE/Chinese-LLaMA
| 項目 | 一句話描述 |
| 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 |
| ChatLLaMA(英文版) | LLaMA的RLHF版 |
| ColossalChat | 通過self-instruct技術(shù)指令微調(diào)LLaMA且加上RLHF |
更多請查看下一篇:從GLM、ChatGLM到MOSS、ChatDoctor、可商用
總結(jié)
以上是生活随笔為你收集整理的类ChatGPT的部署与微调(上):从LLaMA、Alpaca/Vicuna/BELLE、中文版的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 201509281125_《为什么移动a
- 下一篇: angular复用路由组件_Angula