日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

类ChatGPT的部署与微调(上):从LLaMA、Alpaca/Vicuna/BELLE、中文版

發(fā)布時(shí)間:2024/1/8 编程问答 48 豆豆
生活随笔 收集整理的這篇文章主要介紹了 类ChatGPT的部署与微调(上):从LLaMA、Alpaca/Vicuna/BELLE、中文版 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

前言?

近期,除了研究ChatGPT背后的各種技術(shù)細(xì)節(jié) 不斷看論文(至少100篇,100篇目錄見(jiàn)此:ChatGPT相關(guān)技術(shù)必讀論文100篇),還開(kāi)始研究一系列開(kāi)源模型(包括各自對(duì)應(yīng)的模型架構(gòu)、訓(xùn)練方法、訓(xùn)練數(shù)據(jù)、本地私有化部署、硬件配置要求、微調(diào)等細(xì)節(jié))?

本文一開(kāi)始是作為此文《ChatGPT技術(shù)原理解析:從RL之PPO算法、RLHF到GPT4、instructGPT》的第4部分,但隨著研究深入 為避免該文篇幅又過(guò)長(zhǎng),將把『第4部分 開(kāi)源項(xiàng)目』抽取出來(lái) 獨(dú)立成本文,然后不斷續(xù)寫(xiě)本文直至成了一個(gè)系列

畢竟我上半年的目標(biāo)之一,便是把ChatGPT涉及的所有一切關(guān)鍵技術(shù)細(xì)節(jié),以及相關(guān)的開(kāi)源項(xiàng)目都研究的透透的,故過(guò)程中會(huì)不斷產(chǎn)出一篇篇新文章出來(lái)
?

第一部分 LLaMA的代碼級(jí)解讀:RMSNorm/SwiGLU/RoPE/Transformer

1.1 Meta發(fā)布LLaMA((7B 13B 33B 65B):參數(shù)少但多數(shù)任務(wù)的效果好于GPT3

一直致力于LLM模型研究的國(guó)外TOP 3大廠除了OpenAI、Google,便是Meta(原來(lái)的Facebook)

Meta曾第一個(gè)發(fā)布了基于LLM的聊天機(jī)器人——BlenderBot 3,但輸出不夠安全,很快下線;再后來(lái),Meta發(fā)布一個(gè)專(zhuān)門(mén)為科學(xué)研究設(shè)計(jì)的模型Galactica,但用戶(hù)期望過(guò)高,發(fā)布三天后又下線

23年2.24日,Meta通過(guò)論文《LLaMA: Open and Efficient Foundation Language Models》發(fā)布了自家的大型語(yǔ)言模型LLaMA(這是解讀之一),有多個(gè)參數(shù)規(guī)模的版本(7B 13B 33B 65B)

LLaMA只使用公開(kāi)的數(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á)到甚至超過(guò)大模型的效果

  • 比如13B參數(shù)的版本在多項(xiàng)基準(zhǔn)上測(cè)試的效果好于2020年的參數(shù)規(guī)模達(dá)175B的GPT-3
  • 而對(duì)于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ǔ)言理解)上要優(yōu)于Google的指令微調(diào)模型Flan-PaLM-cont(62B)

1.2 代碼級(jí)解讀:LLaMA的模型架構(gòu)——RMSNorm/SwiGLU/RoPE/Transformer

1.2.1?項(xiàng)目環(huán)境依賴(lài):torch、fairscale、fire、sentencepiece

此項(xiàng)目給出的環(huán)境依賴(lài)有4個(gè):

  • torch
  • fairscale,fairscale是用來(lái)做GPU分布的,一般是當(dāng)使用DDP仍然遇到超顯存的問(wèn)題時(shí)使用fairscale
  • fire,fire是一個(gè)命令行工具,用或者不用他都可以
  • sentencepiece,sentencepiece是用于tokenizer的工具包 from sentencepiece import SentencePieceProcessor from logging import getLogger from typing import List import oslogger = getLogger()class Tokenizer:def __init__(self, model_path: str):# reload tokenizerassert os.path.isfile(model_path), model_pathself.sp_model = SentencePieceProcessor(model_file=model_path)logger.info(f"Reloaded SentencePiece model from {model_path}")# BOS / EOS token IDsself.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()def encode(self, s: str, bos: bool, eos: bool) -> List[int]:assert type(s) is strt = self.sp_model.encode(s)if bos:t = [self.bos_id] + tif eos:t = t + [self.eos_id]return tdef decode(self, t: List[int]) -> str:return self.sp_model.decode(t)
  • 1.2.2 RMSNorm:對(duì)每個(gè)Transformer子層的輸入進(jìn)行歸一化

    為了提高訓(xùn)練的穩(wěn)定性,對(duì)每個(gè)transformer子層的輸入進(jìn)行歸一化,而不是對(duì)輸出進(jìn)行歸一化,且使用由Zhang和Sennrich(2019)提出的RMSNorm(Root Mean Square Layer Normalization)
    RMS Norm是一般LayerNorm的一種變體,可以在梯度下降時(shí)令損失更加平滑
    與layerNorm相比,RMS Norm的主要區(qū)別在于去掉了減去均值的部分(re-centering),只保留方差部分(re-scaling)

    為一目了然,我們看下它們各自的歸一化的表達(dá)式

    • 一般的LN:

      其中

    • RMS Norm:

      其中

    至于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ù)之后分母為0self.eps = epsself.weight = nn.Parameter(torch.ones(dim))// x是輸入def _norm(self, x):// torch.rsqrt是開(kāi)平方并取倒數(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ù),即gireturn output * self.weight

    1.2.3?SwiGLU替代ReLU

    用Shazeer(2020)提出的SwiGLU替代ReLU,在維度上使用的維度是2/3*4d,而不是PaLM中的4d

    LLaMA采用SwiGLU替換了原有的ReLU,具體是采用SwiGLU的FNN,在論文中以如下公式進(jìn)行表述:

    其中

    對(duì)應(yīng)論文見(jiàn):Ramachandran et al., 2017
    代碼實(shí)現(xiàn)上:可以通過(guò)調(diào)用torch內(nèi)置方法F.silu()實(shí)現(xiàn),會(huì)在下文的FFN部分介紹

    1.2.4 位置編碼:旋轉(zhuǎn)位置嵌入(RoPE)

    在位置編碼上,刪除了絕對(duì)位置嵌入,而在網(wǎng)絡(luò)的每一層增加了蘇劍林等人(2021)提出的旋轉(zhuǎn)位置嵌入(RoPE),其思想是采用絕對(duì)位置編碼的形式,實(shí)現(xiàn)相對(duì)位置編碼

    • RoPE主要借助了復(fù)數(shù)的思想,為了引入復(fù)數(shù),首先假設(shè)了在加入位置信息之前,原有的編碼向量是二維行向量,其中是絕對(duì)位置,現(xiàn)在需要構(gòu)造一個(gè)變換,將引入到中,即尋找變換:?

      考慮到Attention的核心計(jì)算是內(nèi)積:

      所以,尋求的這個(gè)變換,應(yīng)該具有特性:
    • 這里直接說(shuō)結(jié)論,尋求的變換就是,也就是給乘以,相應(yīng)地,乘以
      做了這樣一個(gè)變換之后,根據(jù)復(fù)數(shù)的特性,有:

      也就是,如果把二維向量看做復(fù)數(shù),那么它們的內(nèi)積,等于一個(gè)復(fù)數(shù)乘以另一個(gè)復(fù)數(shù)的共軛,得到的結(jié)果再取實(shí)部,代入上面的變換,也就有:

      這樣一來(lái),內(nèi)積的結(jié)果就只依賴(lài)于,也就是相對(duì)位置了
      換言之,經(jīng)過(guò)這樣一番操作,通過(guò)給Embedding添加絕對(duì)位置信息,可以使得兩個(gè)token的編碼,經(jīng)過(guò)內(nèi)積變換(self-attn)之后,得到結(jié)果是受它們位置的差值,即相對(duì)位置影響的
    • 于是對(duì)于任意的位置為的二維向量,把它看做復(fù)數(shù),乘以,而根據(jù)歐拉公式,有:

      于是上述的相乘變換也就變成了:

      把上述式子寫(xiě)成矩陣形式:

      而這個(gè)變換的幾何意義,就是在二維坐標(biāo)系下,對(duì)向量進(jìn)行了旋轉(zhuǎn),因而這種位置編碼方法,被稱(chēng)為旋轉(zhuǎn)位置編碼
    • 根據(jù)剛才的結(jié)論,結(jié)合內(nèi)積的線性疊加性,可以將結(jié)論推廣到高維的情形。可以理解為,每?jī)蓚€(gè)維度一組,進(jìn)行了上述的“旋轉(zhuǎn)”操作,然后再拼接在一起:

      由于矩陣的稀疏性,會(huì)造成計(jì)算上的浪費(fèi),所以在計(jì)算時(shí)采用逐位相乘再相加的方式進(jìn)行:

      其中為矩陣逐位相乘操作

    原理理解了,接下來(lái)可以代碼實(shí)現(xiàn)旋轉(zhuǎn)位置編碼

    def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0):// 首先torch.arange創(chuàng)建了一個(gè)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比較容易理解,也就是絕對(duì)位置信息,它的shape是(1024)t = torch.arange(end, device=freqs.device)// torch.outer是把一個(gè)向量的轉(zhuǎn)置乘以另一個(gè)向量:torch.outer(a, b) = a^T * b// 于是根據(jù)torch.outer運(yùn)算,我們得到了一個(gè)shape為(1024, 32)的tensor。其意義也就是將每一個(gè)絕對(duì)位置,分配到對(duì)應(yīng)的角度,相乘// 直觀理解一下,就是每一個(gè)絕對(duì)位置上,都有32個(gè)角度// 為什么是這樣的呢,回顧計(jì)算的公式,對(duì)于旋轉(zhuǎn)矩陣,每?jī)蓚€(gè)元素為一組,它們乘以的角度是同一個(gè)θ,所以這個(gè)(1024, 32)// 在后續(xù)的過(guò)程中,就可以reshape成(512, 64),并且在64的那個(gè)維度上,每?jī)蓚€(gè)是相同的freqs = torch.outer(t, freqs).float()// torch.polar(abs, angle)利用一個(gè)絕對(duì)數(shù)值和一個(gè)角度值,從而在極坐標(biāo)下構(gòu)造一個(gè)復(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其實(shí)就是需要計(jì)算出來(lái)的mθ,也就是跟絕對(duì)位置相關(guān)的旋轉(zhuǎn)的角度,在極坐標(biāo)下對(duì)應(yīng)的復(fù)數(shù)tensor// 這一步就是在生成我們需要的位置信息// 直觀理解一下,像是在復(fù)平面內(nèi),以原點(diǎn)為中心,轉(zhuǎn)了1024組,每一組64個(gè)的單位向量,它的shape是(1024, 64)freqs_cis = torch.polar(torch.ones_like(freqs), freqs) # complex64return freqs_cis// 第二個(gè)函數(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])// 這個(gè)方法的作用是為了把freqs_cis變成和輸入的tensor相同的形狀// 需要注意的是,這里的freqs_cis并不是precompute_freqs_cis生成的形狀為(1024, 64)的那個(gè)tensor// 而是根據(jù)輸入的絕對(duì)位置,在(1024, 64)的tensor中,截取了長(zhǎng)度為當(dāng)前seq_len的一部分// 代碼在Transformer類(lèi)的forward方法中freqs_cis = self.freqs_cis[start_pos : start_pos + seqlen]// 也就是說(shuō),假如當(dāng)前輸入的序列長(zhǎng)度是512,那么截取出來(lái)的這個(gè)新的freqs_cis,形狀就是(512, 64)// reshape之后,形狀就變成了(1, 512, 1, 32),也就是在每一個(gè)位置上,都對(duì)應(yīng)有32個(gè)角度// 根據(jù)上面torch.polar的介紹,當(dāng)我們固定絕對(duì)值(也就是向量的模長(zhǎng))時(shí),角度就可以在笛卡爾坐標(biāo)系下唯一確定一個(gè)復(fù)數(shù)// 這樣一來(lái)也就是32個(gè)復(fù)數(shù),即64個(gè)特征維度,所以就可以對(duì)應(yīng)的將它融合到每個(gè)attention head的64個(gè)特征中去了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方法,這個(gè)方法其實(shí)就是把位置信息添加到原有的編碼結(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是把一個(gè)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))// 然后進(jìn)入到上面的第二個(gè)函數(shù)reshape_for_broadcastfreqs_cis = reshape_for_broadcast(freqs_cis, xq_)// torch.view_as_real是把復(fù)數(shù)tensor變回實(shí)數(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í)數(shù)形式,得到的shape為(2, 512, 12, 32, 2)// 然后再flatten成(2, 512, 12, 64),這樣一來(lái),就變回了和最開(kāi)始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
      每個(gè)attention_head的維度為64,那么,對(duì)于輸入到multi-head attn中的輸入的尺寸就是
      (2, 512, 12, 64)
    • 而freqs_cis其實(shí)就是需要計(jì)算出來(lái)的也就是跟絕對(duì)位置相關(guān)的旋轉(zhuǎn)的角度,在極坐標(biāo)下對(duì)應(yīng)的復(fù)數(shù)tensor

    而precompute_freqs_cis就是提前將這些旋轉(zhuǎn)角度對(duì)應(yīng)的tensor給創(chuàng)建出來(lái),并可以重復(fù)利用。因?yàn)榇_定了序列的最大長(zhǎng)度,所以這個(gè)tensor是固定死的。根據(jù)后續(xù)的數(shù)據(jù)流我們可以發(fā)現(xiàn),在調(diào)用該函數(shù)時(shí),傳入的兩個(gè)參數(shù)分別是attention_head的維度,以及最大長(zhǎng)度的兩倍,具象地,也就是64和1024

    1.2.4 Transform架構(gòu)的實(shí)現(xiàn):Attention計(jì)算、SA、FFN

    LLaMA和GPT一樣,都是基于Transformer這個(gè)架構(gòu),通常,我們?cè)跇?gòu)建transformer時(shí),是按Block構(gòu)建的,每個(gè)transformer Block包含SA和FFN兩部分,然后再通過(guò)堆疊block的形式,構(gòu)建起整個(gè)transformer網(wǎng)絡(luò),LLaMA也是這樣做的

    回顧一下Attention計(jì)算的總體過(guò)程是:

  • 輸入,分別經(jīng)過(guò)三個(gè)Linear得到
  • 在??和中加入旋轉(zhuǎn)位置編碼
  • 緩存??和??
  • 計(jì)算
  • 其中有一個(gè)細(xì)節(jié)就是緩存機(jī)制,它設(shè)計(jì)的目的是在generate時(shí)減少token的重復(fù)計(jì)算。簡(jiǎn)單解釋一下,就是在計(jì)算第n個(gè)token特征的時(shí)候,需要用到第個(gè)token,即每次生成時(shí),需要知道前面所有的過(guò)往信息,如果每次都從頭算的話,那就會(huì)造成極大的浪費(fèi),所以就沒(méi)算一個(gè)位置的信息,就把它緩存下來(lái)

    接下來(lái),我們來(lái)看下代碼實(shí)現(xiàn),首先是SA部分:

    class Attention(nn.Module):def __init__(self, args: ModelArgs):super().__init__()self.n_local_heads = args.n_heads // fs_init.get_model_parallel_world_size()self.head_dim = args.dim // args.n_headsself.wq = ColumnParallelLinear(args.dim,args.n_heads * self.head_dim,bias=False,gather_output=False,init_method=lambda x: x,)self.wk = ColumnParallelLinear(args.dim,args.n_heads * self.head_dim,bias=False,gather_output=False,init_method=lambda x: x,)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.shapexq, xk, xv = self.wq(x), self.wk(x), self.wv(x)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)xq, xk = apply_rotary_emb(xq, xk, freqs_cis=freqs_cis)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] = xkself.cache_v[:bsz, start_pos : start_pos + seqlen] = xvkeys = self.cache_k[:bsz, : start_pos + seqlen]values = self.cache_v[:bsz, : start_pos + seqlen]xq = xq.transpose(1, 2)keys = keys.transpose(1, 2)values = values.transpose(1, 2)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)output = torch.matmul(scores, values) # (bs, n_local_heads, slen, head_dim)output = output.transpose(1, 2).contiguous().view(bsz, seqlen, -1)return self.wo(output)

    然后是FFN部分,需要注意的點(diǎn)就是采用的激活函數(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))

    這里與常見(jiàn)模型中的FFN做一下簡(jiǎn)單的對(duì)比

    • BART中的FFN,用的是fc->act->fc,用了兩層全連接
    • GPT中的FFN,用的是conv1D->act->conv1D,也是只用了兩層
    • 而LLaMA中的FFN采用了三個(gè)全連接層以實(shí)現(xiàn)FFNSwiGLU,即

    然后將SA和FFN這兩部分拼在一起就是一個(gè)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進(jìn)行堆疊,拼上最前頭的embedding部分,就是一個(gè)完整的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]// 對(duì)于decoder模型,為了防止標(biāo)簽泄漏,需要mask,所以做了一個(gè)上三角的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)// 接下來(lái)就是逐層的計(jì)算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()

    接著看下生成過(guò)程,如下:

  • 對(duì)prompts進(jìn)行tokenize,得到token ids;
  • 計(jì)算當(dāng)前batch的最大長(zhǎng)度total_len,用來(lái)創(chuàng)建輸入的token tensor,最大長(zhǎng)度不能超過(guò)前文所述緩存的大小;
  • 從當(dāng)前batch中,最短的一個(gè)prompt的位置,作為生成的開(kāi)始位置,開(kāi)始生成;
  • 輸入的token tensor傳入transformer模型,計(jì)算logits,得到形狀為(batch_size, hidden_size)的logits(transformer最后一層的輸出);
  • softmax+top_p采樣,得到當(dāng)前預(yù)測(cè)的token,并更新當(dāng)前位置,準(zhǔn)備預(yù)測(cè)下一個(gè)token;
  • 解碼得到生成的文本
  • 代碼如下

    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_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),靈感來(lái)自Rabe和Staats(2021)以及Dao等人(2022),這個(gè)實(shí)現(xiàn)可在xformers庫(kù)中找到,可以有效減少內(nèi)存的使用和計(jì)算
    具體原理為通過(guò)不存儲(chǔ)注意力權(quán)重和不計(jì)算由于語(yǔ)言建模任務(wù)的因果性質(zhì)而被掩蓋的鍵/查詢(xún)分?jǐn)?shù)來(lái)實(shí)現(xiàn)的
  • 其次,為了進(jìn)一步提高訓(xùn)練效率,減少了在check point的后向傳遞中重新計(jì)算的激活量,在實(shí)現(xiàn)上,通過(guò)手動(dòng)實(shí)現(xiàn)trasnformer層的后向函數(shù)來(lái)進(jìn)行操作
    為了充分受益于這種優(yōu)化,還通過(guò)如Korthikanti等人(2022)中采用的方法,進(jìn)行使用模型和序列并行來(lái)減少模型的內(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)存,這意味著對(duì)包含1.4Ttoken的數(shù)據(jù)集進(jìn)行訓(xùn)練大約花費(fèi)了21天
  • LLaMA發(fā)布不久后,一些研究者基于它做了不少工作

    • 一開(kāi)始最小參數(shù)7B的模型也需要近30GB的GPU才能運(yùn)行,但通過(guò)比特和字節(jié)庫(kù)進(jìn)行浮點(diǎn)優(yōu)化,能夠讓模型在單個(gè)NVIDIA RTX 3060(顯存一般12G)上運(yùn)行
    • 之后,GitHub 上的一名研究人員甚至能夠在Ryzen 7900X CPU上運(yùn)行LLM的7B 版本,每秒能推斷出幾個(gè)單詞
    • 再之后,有研究者推出了llama.cpp,無(wú)需 GPU,就能運(yùn)行 LLaMA
      llama.cpp 項(xiàng)目實(shí)現(xiàn)了在MacBook上運(yùn)行 LLaMA,還有開(kāi)發(fā)者成功的在 4GB RAM 的樹(shù)莓派上運(yùn)行了 LLaMA 7B

    第二部分 各種微調(diào)LLaMA:Alpaca(self-instruct)、Vicuna(shareGPT)、BELLE(self-instruct)

    2.1 Stanford Alpaca:結(jié)合英文語(yǔ)料通過(guò)Self Instruct方式微調(diào)LLaMA 7B

    2.1.1 什么是self-instruct方式:提示GPT3/GPT3.5/GPT4的API收集數(shù)據(jù)

    3月中旬,斯坦福發(fā)布Alpaca(中文名:羊駝):號(hào)稱(chēng)只花100美元,人人都可微調(diào)Meta家70億參數(shù)的LLaMA大模型(即LLaMA 7B),具體做法是通過(guò)52k指令數(shù)據(jù),然后在8個(gè)80GB A100上訓(xùn)練3個(gè)小時(shí),使得Alpaca版的LLaMA 7B在單純對(duì)話上的性能比肩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

    可能有讀者有疑問(wèn),即52k數(shù)據(jù)都長(zhǎng)啥樣呢?這52K數(shù)據(jù)存在Alpaca項(xiàng)目的alpaca_data.json文件中,這個(gè)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對(duì)應(yīng)的API即 text-davinci-003生成的指令的答案

    而斯坦福團(tuán)隊(duì)微調(diào)LLaMA 7B所用的52K指令數(shù)據(jù),便是通過(guò)Self-Instruct『Self-Instruct是來(lái)自華盛頓大學(xué)Yizhong Wang等22年12月通過(guò)這篇論文《SELF-INSTRUCT: Aligning Language Model with Self Generated Instructions》提出的』提示GPT3的API拿到的

    ?具體而言,論文中提出

  • 人工設(shè)計(jì)175個(gè)任務(wù),每個(gè)任務(wù)都有對(duì)應(yīng)的{指令 輸入 輸出/實(shí)例}或{指令 輸出/實(shí)例},將這175個(gè)任務(wù)數(shù)據(jù)作為種子集
  • 然后提示模型比如GPT3對(duì)應(yīng)的API即 text-davinci-001 (原論文中沒(méi)用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生成指令),使用種子集作為上下文示例來(lái)生成更多新的指令
  • 對(duì)該模型生成的指令判斷是否分類(lèi)任務(wù)
  • 使用模型生成實(shí)例
  • 對(duì)上述模型生成的數(shù)據(jù){指令 輸入 輸出/實(shí)例}過(guò)濾掉低質(zhì)量或相似度高的
  • 將經(jīng)過(guò)過(guò)濾和后處理的數(shù)據(jù)添加到種子池中
    一直重復(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:輸入提示,可以是單個(gè)字符串、字符串列表、字典或字典列表decoding_args:解碼參數(shù),用于指定如何生成文本model_name:要使用的模型名稱(chēng),默認(rèn)為"text-davinci-003"sleep_time:在達(dá)到速率限制時(shí),程序暫停的時(shí)間(以秒為單位)batch_size:在單個(gè)請(qǐng)求中發(fā)送的prompts的數(shù)量max_instances:要解碼的prompts的最大數(shù)量max_batches:要解碼的批次的最大數(shù)量(此參數(shù)將在未來(lái)被棄用)return_text:如果為T(mén)rue,則返回文本而不是包含諸如logprob等信息的完整completion對(duì)象decoding_kwargs:其他解碼參數(shù),例如best_of和logit_bias"""# 函數(shù)首先檢查是否有單個(gè)prompt,如果是,則將其轉(zhuǎn)換為列表# 然后,根據(jù)最大實(shí)例數(shù)量截取prompts。接著,將prompts分成批次以便進(jìn)行批處理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進(jìn)行交互。當(dāng)遇到OpenAIError時(shí),會(huì)根據(jù)錯(cuò)誤類(lèi)型采取不同的措施如果是因?yàn)樗俾氏拗?#xff0c;程序?qū)和R欢螘r(shí)間再重試。如果提示過(guò)長(zhǎng),程序?qū)p小目標(biāo)長(zhǎng)度再重試。'''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以及是否為單個(gè)prompt的情況返回不同類(lèi)型的結(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萬(wàn)個(gè)這樣的示例微調(diào)LLaMA搞出來(lái)的,個(gè)人覺(jué)得可以取名為?instructLLaMA-7B,^_^

    值得一提的是,后來(lái)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ù)的中文翻譯,存儲(chǔ)在項(xiàng)目文件alpaca_gpt4_data_zh.json 里
    • Comparison Data ranked by GPT-4,好訓(xùn)練一個(gè)獎(jiǎng)勵(lì)模型

      存儲(chǔ)在?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時(shí)一般都會(huì)用到Hugging face實(shí)現(xiàn)的Transformers庫(kù)的Trainer類(lèi)

    可能有讀者疑問(wèn),那微調(diào)的代碼長(zhǎng)啥樣呢?實(shí)際上,微調(diào)步驟大同小異,據(jù)代碼:tatsu-lab/stanford_alpaca · GitHub,可得微調(diào)的步驟如下

  • 導(dǎo)入所需的庫(kù):包括torch,transformers等。
  • 定義一些全局變量,如特殊字符、提示模板等。
  • 定義用于處理模型、數(shù)據(jù)和訓(xùn)練參數(shù)的數(shù)據(jù)類(lèi)。
  • 定義輔助函數(shù),如:
    • safe_save_model_for_hf_trainer:安全地保存訓(xùn)練器中的模型;
    • smart_tokenizer_and_embedding_resize:調(diào)整分詞器和詞嵌入大小;
    • _tokenize_fn:將字符串序列進(jìn)行分詞;
    • preprocess:預(yù)處理數(shù)據(jù),對(duì)源數(shù)據(jù)和目標(biāo)數(shù)據(jù)進(jìn)行分詞。
  • 定義SupervisedDataset類(lèi),用于加載數(shù)據(jù)、格式化輸入、進(jìn)行分詞等操作。
  • 定義DataCollatorForSupervisedDataset類(lèi),用于將數(shù)據(jù)集的實(shí)例整理為批次。
  • 定義make_supervised_data_module函數(shù),用于創(chuàng)建監(jiān)督學(xué)習(xí)任務(wù)的數(shù)據(jù)集和整理器。
  • 定義train函數(shù),用于執(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è)用于因果語(yǔ)言建模的模型
    c.?加載分詞器:使用transformers.AutoTokenizer.from_pretrained從預(yù)訓(xùn)練的模型檢查點(diǎn)加載分詞器
    d.?為分詞器添加特殊字符:根據(jù)需要,將特殊字符添加到分詞器中
    e. 創(chuàng)建數(shù)據(jù)集和整理器:使用make_supervised_data_module函數(shù)為監(jiān)督學(xué)習(xí)任務(wù)創(chuàng)建數(shù)據(jù)集和整理器
    f. 實(shí)例化Trainer類(lèi):實(shí)例化transformers.Trainer類(lèi),并傳入模型、分詞器、訓(xùn)練參數(shù)以及數(shù)據(jù)集。Trainer類(lèi)負(fù)責(zé)管理訓(xùn)練過(guò)程
    g.?訓(xùn)練模型:調(diào)用Trainer類(lèi)的train()方法對(duì)模型進(jìn)行微調(diào),相當(dāng)于鏈路就是:transformers庫(kù) ?Trainer類(lèi) ?train函數(shù)
    h.?保存模型狀態(tài):在訓(xùn)練完成后,調(diào)用Trainer.save_state()方法保存模型的狀態(tài)
    i.?將訓(xùn)練器的模型安全地保存到磁盤(pán):使用safe_save_model_for_hf_trainer函數(shù)將訓(xùn)練器中的模型安全地保存到磁盤(pán)
  • 如果這個(gè)腳本是主程序,則調(diào)用train函數(shù)以開(kāi)始訓(xùn)練過(guò)程
  • 可能,很快便有同學(xué)疑問(wèn),怎么沒(méi)有預(yù)想中的損失計(jì)算、梯度下降、參數(shù)更新呢,實(shí)際上這三步的具體實(shí)現(xiàn)都封裝在了Hugging face社區(qū)實(shí)現(xiàn)的鼎鼎大名的transformers的Trainer類(lèi)中:transformers/trainer.py at main · huggingface/transformers · GitHub

    這個(gè)?transformers/trainer.py?文件的主要部分如下

    ????????? 導(dǎo)入:文件首先導(dǎo)入了一些必要的Python庫(kù),如os、sys、logging以及其他一些庫(kù)。它還導(dǎo)入了Hugging Face庫(kù)中的一些相關(guān)模塊,如datasets、transformers等
    ????????? TrainerState:這個(gè)類(lèi)用于保存訓(xùn)練器的狀態(tài),包括當(dāng)前的epoch、迭代步數(shù)、最佳指標(biāo)值等

    ????????? TrainOutput:這個(gè)類(lèi)用于返回訓(xùn)練過(guò)程的結(jié)果,包括訓(xùn)練損失、訓(xùn)練步數(shù)等

    ????????? TrainerControl:這個(gè)類(lèi)提供了一種用于控制訓(xùn)練循環(huán)的機(jī)制,例如,當(dāng)用戶(hù)想要在某個(gè)特定的迭代步數(shù)時(shí)停止訓(xùn)練

    ????????? Trainer:這是文件中的主要類(lèi),用于訓(xùn)練和評(píng)估Transformers模型,它包含許多方法,如train、evaluate、predict等

    更具體的,Trainer類(lèi)包括如下關(guān)鍵方法:

    __init__:初始化方法,用于創(chuàng)建訓(xùn)練器對(duì)象。它接收模型、訓(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)練過(guò)程,它包括遍歷數(shù)據(jù)集、計(jì)算損失、計(jì)算梯度、更新模型參數(shù)以及日志記錄等

    • 遍歷數(shù)據(jù)集:train方法通過(guò)使用dataloader來(lái)遍歷訓(xùn)練數(shù)據(jù)集 for step, inputs in enumerate(epoch_iterator):
    • 計(jì)算損失:損失計(jì)算在training_step方法中,接收輸入數(shù)據(jù)并產(chǎn)生預(yù)測(cè)輸出,然后,這個(gè)預(yù)測(cè)輸出會(huì)與真實(shí)輸出(標(biāo)簽)進(jìn)行比較,以計(jì)算損失 outputs = model(**inputs) 上述代碼行使用model(已經(jīng)加載了預(yù)訓(xùn)練模型)和inputs(包含輸入數(shù)據(jù)的字典)計(jì)算模型的預(yù)測(cè)輸出。這個(gè)outputs變量包含模型預(yù)測(cè)的結(jié)果
      接下來(lái),我們從outputs中獲取預(yù)測(cè)結(jié)果,并與真實(shí)標(biāo)簽(即labels)進(jìn)行比較,以計(jì)算損失 loss = outputs.loss outputs.loss是模型預(yù)測(cè)輸出和真實(shí)輸出(標(biāo)簽)之間的損失。這個(gè)損失值將用于計(jì)算梯度并更新模型參數(shù)
    • 計(jì)算梯度:loss.backward()這行代碼計(jì)算模型參數(shù)關(guān)于損失的梯度 loss.backward()
    • 梯度累積:當(dāng)gradient_accumulation_steps大于1時(shí),梯度會(huì)被累積,而不是立即更新模型參數(shù) if (step + 1) % self.args.gradient_accumulation_steps == 0:
    • 更新模型參數(shù):optimizer.step()這行代碼根據(jù)計(jì)算出的梯度來(lái)更新模型參數(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ò)程中的一些關(guān)鍵指標(biāo),例如損失、學(xué)習(xí)率等

    ?evaluate:這個(gè)方法用于評(píng)估模型在驗(yàn)證數(shù)據(jù)集上的性能,返回評(píng)估結(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ù)測(cè),返回預(yù)測(cè)結(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è)可選的類(lèi),用于支持使用混合精度和ZeRO進(jìn)行分布式訓(xùn)練


    2.1.3 Alpaca-LoRA:通過(guò)PEFT庫(kù)在消費(fèi)級(jí)GPU上微調(diào)「基于LLaMA的Alpaca」

    在神經(jīng)網(wǎng)絡(luò)模型中,模型參數(shù)通常以矩陣的形式表示。對(duì)于一個(gè)預(yù)訓(xùn)練好的模型,其參數(shù)矩陣已經(jīng)包含了很多有用的信息。為了使模型適應(yīng)特定任務(wù),我們需要對(duì)這些參數(shù)進(jìn)行微調(diào)

    LoRA的核心思想是用一種低秩的方式來(lái)調(diào)整這些參數(shù)矩陣。在數(shù)學(xué)上,低秩意味著一個(gè)矩陣可以用兩個(gè)較小的矩陣相乘來(lái)近似,通過(guò)論文《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ī)制中的查詢(xún)Q和鍵K矩陣
  • 初始化映射矩陣和逆映射矩陣:為目標(biāo)層創(chuàng)建兩個(gè)較小的矩陣A和B
    ? A是映射矩陣(隨機(jī)高斯分布初始化),維度上是升維
    ? B是逆映射矩陣(用0矩陣初始化),維度上是降維
    其中,矩陣的大小由LoRA的秩(rank)和alpha值確定
  • 參數(shù)變換:將目標(biāo)層的原始參數(shù)矩陣W通過(guò)映射矩陣A和逆映射矩陣B進(jìn)行變換。計(jì)算公式為:W' = W + A?* B。這里W'是變換后的參數(shù)矩陣
  • 微調(diào)模型:使用新的參數(shù)矩陣W'替換目標(biāo)層的原始參數(shù)矩陣W,然后在特定任務(wù)的訓(xùn)練數(shù)據(jù)上對(duì)模型進(jìn)行微調(diào)
  • 梯度更新:在微調(diào)過(guò)程中,計(jì)算損失函數(shù)關(guān)于映射矩陣A和逆映射矩陣B的梯度,并使用優(yōu)化算法(如Adam、SGD等)對(duì)A和B進(jìn)行更新
    注意,在更新過(guò)程中,原始參數(shù)矩陣W保持不變,說(shuō)白了,訓(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)過(guò)程中,模型會(huì)通過(guò)更新映射矩陣U和逆映射矩陣V來(lái)學(xué)習(xí)特定任務(wù)的知識(shí),從而提高模型在該任務(wù)上的性能。

    額外說(shuō)一下,這個(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)練,則將原始模型中名稱(chēng)含有“deoder.layers.”且為線性層修改為L(zhǎng)oRA層,具體操作為:

  • 將原始結(jié)構(gòu)的weight參數(shù)凍結(jié);
  • 新引入了2個(gè)線性層lora_right_weight和lora_left_weight,可實(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: """ 如果啟用,則對(duì)名稱(chēng)中含有“decoder.layers.”且為線性層的結(jié)構(gòu)部分引入LoRA旁路(實(shí)現(xiàn)先降維后升維的2個(gè)線性層), 這類(lèi)結(jié)構(gòu)基本都是attention、信息交互用的inner線性層, 這類(lèi)結(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):"""將名稱(chēng)中帶有"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ù)屬性名取出對(duì)應(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對(duì)應(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 = weightself.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))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

    再額外分析下 這段代碼的最后部分

    # 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)行加和”

    在代碼中體現(xiàn)為

    F.linear(input, self.weight, self.bias) + (self.lora_dropout(input) @ self.lora_right_weight @ self.lora_left_weight) * self.lora_scaling

    加號(hào)左側(cè)為原結(jié)構(gòu)支路,加號(hào)右側(cè)為新增支路,self.lora_right_weight和self.lora_left_weight分別為兩個(gè)新引入線性層的參數(shù)

    而Huggingface公司推出的PEFT(Parameter-Efficient Fine-Tuning)庫(kù)也封裝了LoRA這個(gè)方法,PEFT庫(kù)可以使預(yù)訓(xùn)練語(yǔ)言模型高效適應(yīng)各種下游任務(wù),而無(wú)需微調(diào)模型的所有參數(shù),即僅微調(diào)少量(額外)模型參數(shù),從而大大降低了計(jì)算和存儲(chǔ)成本

    ModelFull FinetuningPEFT-LoRA PyTorchPEFT-LoRA DeepSpeed with CPU Offloading
    bigscience/T0_3B (3B params)47.14GB GPU / 2.96GB CPU14.4GB GPU / 2.96GB CPU9.8GB GPU / 17.8GB CPU
    bigscience/mt0-xxl (12B params)OOM GPU56GB GPU / 3GB CPU22GB GPU / 52GB CPU
    bigscience/bloomz-7b1 (7B params)OOM GPU32GB GPU / 3.8GB CPU18.1GB GPU / 35GB CPU

    且PEFT庫(kù)支持以下流行的方法

  • LoRA:?LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS
  • 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則可以通過(guò)PEFT庫(kù)實(shí)現(xiàn)的LoRA方法在消費(fèi)級(jí)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ù)都加了注釋說(shuō)明):

    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(zhǎng)度--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.1.4 Alpaca所用的self-instruct的影響力:解決一大批模型的數(shù)據(jù)擴(kuò)展問(wèn)題

    很快,通過(guò)下文你會(huì)發(fā)現(xiàn)

  • self-instruct啟發(fā)出很多「羊駝?lì)惸P汀?/strong>
    羊駝率先帶動(dòng)的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
  • ?一下子出來(lái)這么新的模型 似乎有點(diǎn)懵,沒(méi)事,請(qǐng)看下文及下一篇文章娓娓道來(lái)..

    2.2 Vicuna/FastChat:通過(guò)ShareGPT.com的7萬(wàn)條對(duì)話數(shù)據(jù)微調(diào)LLaMA

    23年3.31日,受 Meta LLaMA 和 Stanford Alpaca 項(xiàng)目的啟發(fā),加州大學(xué)伯克利分校(UC?Berkeley)等大學(xué)的研究者根據(jù)從 ShareGPT.com (ShareGPT是一個(gè)用戶(hù)可以分享他們的 ChatGPT 對(duì)話的網(wǎng)站)收集的用戶(hù)共享對(duì)話微調(diào) LLaMA?推出了Vicuna-13B(中文稱(chēng)小羊駝,代碼地址:FastChat)。

    在數(shù)據(jù)規(guī)模上,Vicuna從ShareGPT.com 的公共 API 收集了大約 70K 用戶(hù)共享對(duì)話,且為了確保數(shù)據(jù)質(zhì)量,原作者們將 HTML 轉(zhuǎn)換回 markdown 并過(guò)濾掉一些不合適或低質(zhì)量的樣本。此外,將冗長(zhǎng)的對(duì)話分成更小的部分,以適應(yīng)模型的最大上下文長(zhǎng)度,并做了以下改進(jìn)

    • 內(nèi)存優(yōu)化:為了使 Vicuna 能夠理解長(zhǎng)上下文,將最大上下文長(zhǎng)度從羊駝中的 512 擴(kuò)展到 2048,這大大增加了 GPU 內(nèi)存需求,對(duì)此通過(guò)利用梯度檢查點(diǎn)和閃存注意力來(lái)解決內(nèi)存壓力
    • 多輪對(duì)話:調(diào)整訓(xùn)練損失以考慮多輪對(duì)話,并僅根據(jù)聊天機(jī)器人的輸出計(jì)算微調(diào)損失。
    • 通過(guò)Spot Instance 降低成本:40 倍大的數(shù)據(jù)集和 4 倍的訓(xùn)練序列長(zhǎng)度對(duì)訓(xùn)練費(fèi)用提出了相當(dāng)大的挑戰(zhàn)。原作者們使用SkyPilot managed spot 來(lái)降低成本『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)值得一提的是

  • 對(duì)于個(gè)人開(kāi)發(fā)者而言,Vicuna-13B 只需要大約 28GB 的??GPU 顯存,Vicuna-7B 大約需要14GB GPU顯存,但對(duì)于機(jī)構(gòu)而言,一般通過(guò)8個(gè)具有 80GB 顯存的 A100 GPU 進(jìn)行訓(xùn)練
  • 且Vicuna使用了和Alpaca差不多的超參數(shù) Hyperparameter

    全局批量大小

    Batch Size

    學(xué)習(xí)率

    Learning rate

    EpochsMax lengthWeight decay
    Vicuna-13B1282e-5320480
  • 最終通過(guò)直接使用GPT4評(píng)估之后,效果還不錯(cuò)

    Model NameLLaMA(駱駝)Alpaca(羊駝)Vicuna(小羊駝)Bard/ChatGPT
    DatasetPublicly available datasets
    (1.4T token)
    Self-instruct from davinci-003 API
    (52K samples)
    User-shared conversations
    (70K samples)
    N/A
    Training codeN/AAvailableAvailableN/A
    Evaluation metricsAcademic benchmarkAuthor evaluationGPT-4 assessmentMixed
    Training cost
    (7B)
    82K GPU-hours$500 (data) + $100 (training)$140 (training)N/A
    Training cost
    (13B)
    135K GPU-hoursN/A$300 (training)N/A

    2.3 BELLE:結(jié)合中文語(yǔ)料通過(guò)Self Instruct方式微調(diào)BLOOMZ-7B或LLaMA

    Stanford Alpaca的種子任務(wù)都是英語(yǔ),收集的數(shù)據(jù)也都是英文,因此訓(xùn)練出來(lái)的模型未對(duì)中文優(yōu)化。為了提升對(duì)話模型在中文上的效果,70 億參數(shù)的中文對(duì)話大模型 BELLE『Bloom-Enhanced Large Language model Engine』來(lái)了(這是項(xiàng)目地址)。

    在數(shù)據(jù)方面,結(jié)合以下兩方面的數(shù)據(jù):

    • Alpaca 的 5.2 萬(wàn)條英文數(shù)據(jù)
    • 通過(guò)Alpaca的數(shù)據(jù)收集代碼生成的約 100 萬(wàn)條中文數(shù)據(jù)『也僅使用由 GPT3.5 即模型text-davinci-003 生產(chǎn)的數(shù)據(jù),不包含任何其他數(shù)據(jù),如果想使用ChatGPT的API比如gpt-3.5-turbo模型,可通過(guò)參數(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修改而來(lái)的僅解碼器 transformer 模型架構(gòu),對(duì)應(yīng)的論文為《BLOOM: A 176B-Parameter Open-Access Multilingual Language Model》(翻譯之一,解讀之一)
    至于HuggingFace是著名開(kāi)源工具Transformers的開(kāi)發(fā)公司,很多推理工具都會(huì)支持Transformers中的模型


    截至23年3月中旬,超過(guò)100B參數(shù)量且能夠支持中文的開(kāi)源大模型只有BLOOM和GLM-130B

    該項(xiàng)目主要包含以下三部分內(nèi)容:

    • 175 個(gè)中文種子任務(wù),斯坦福Alpaca一樣,每個(gè)任務(wù)都包含對(duì)應(yīng)的指令/任務(wù)、prompt、輸出
      ? zh_seed_tasks.jsonl:樣例如下
      ?{??"id": "seed_task_20", "name": "horror_movie_opening",
      "instruction": "你需要為一部恐怖電影寫(xiě)一個(gè)創(chuàng)意的開(kāi)場(chǎng)場(chǎng)景。",
      "instances": [{"input": "","output":" 太陽(yáng)已經(jīng)落山,留下了一個(gè)黑暗的小鎮(zhèn)。微風(fēng)吹拂空蕩的街道,讓每一個(gè)冒險(xiǎn)走出門(mén)外的人感到一陣寒意。唯一的聲音是被風(fēng)吹動(dòng)的樹(shù)葉發(fā)出的輕微沙沙聲。突然,一聲令人毛骨悚然的尖叫聲劃破了寂靜,隨后是玻璃破碎的聲音。一所房子亮起了燈光,可以看到一個(gè)人影朝鎮(zhèn)中心奔跑。當(dāng)> 那個(gè)人影越來(lái)越靠近時(shí),清楚地看到那是一個(gè)年輕女子,她渾身血跡斑斑。"}],
      "is_classification": false??}
    • ? prompt_cn.txt: 生成所使用的提示語(yǔ)
      ? 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 模型,可通過(guò)參數(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:通過(guò)中文數(shù)據(jù)預(yù)訓(xùn)練/指令微調(diào)

    Chinese LLaMA(也稱(chēng)中文LLaMA,有7B和13B兩個(gè)版本,項(xiàng)目地址),相當(dāng)于在原版LLaMA的基礎(chǔ)上擴(kuò)充了中文詞表并使用了中文數(shù)據(jù)進(jìn)行二次預(yù)訓(xùn)練,進(jìn)一步提升了中文基礎(chǔ)語(yǔ)義理解能力,同時(shí),在中文LLaMA的基礎(chǔ)上,且用中文指令數(shù)據(jù)進(jìn)行指令精調(diào)得Chinese-Alpaca(也稱(chēng)中文Alpaca,同樣也有7B和13B兩個(gè)版本)

    具體而言,主要做了以下三方面的工作

    2.4.1 詞表擴(kuò)充中文數(shù)據(jù)

    在通用中文語(yǔ)料上訓(xùn)練了基于sentencepiece的20K中文詞表并與原版LLaMA模型的32K詞表進(jìn)行合并
    排除重復(fù)的token后,得到的最終中文LLaMA詞表大小為49953
    需要注意的是,在fine-tune階段Alpaca比LLaMA多一個(gè)pad token,所以中文Alpaca的詞表大小為49954

    這么做的主要原因是原版LLaMA模型的詞表大小是32K,其主要針對(duì)英語(yǔ)進(jìn)行訓(xùn)練,對(duì)多語(yǔ)種支持不是特別理想(可以對(duì)比一下多語(yǔ)言經(jīng)典模型XLM-R的詞表大小為250K)。通過(guò)初步統(tǒng)計(jì)發(fā)現(xiàn),LLaMA詞表中僅包含很少的中文字符,所以在切詞時(shí)會(huì)把中文切地更碎,需要多個(gè)byte token才能拼成一個(gè)完整的漢字,進(jìn)而導(dǎo)致信息密度降低

    其對(duì)應(yīng)的擴(kuò)充詞表的腳本代碼為(為方便大家更好的理解,我給每一行的代碼 都加上了注釋)

    # # 導(dǎo)入os模塊,用于操作系統(tǒng)相關(guān)操作 import os # 設(shè)置環(huán)境變量,使得Protocol Buffers使用Python實(shí)現(xiàn) os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"]="python" # 導(dǎo)入LlamaTokenizer類(lèi) from transformers import LlamaTokenizer # 導(dǎo)入Protocol Buffers格式的sentencepiece模型 from sentencepiece import sentencepiece_model_pb2 as sp_pb2_model # 導(dǎo)入sentencepiece模塊 import sentencepiece as spm # 導(dǎo)入argparse模塊,用于處理命令行參數(shù) import argparse # 創(chuàng)建一個(gè)命令行參數(shù)解析器實(shí)例 parser = argparse.ArgumentParser() # 添加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) # 解析命令行參數(shù) args = parser.parse_args() # 獲取llama_tokenizer_dir參數(shù)值 llama_tokenizer_dir = args.llama_tokenizer_dir # 獲取chinese_sp_model_file參數(shù)值 chinese_sp_model_file = args.chinese_sp_model_file # 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)) # 輸出LlamaTokenizer的所有特殊詞匯 print(llama_tokenizer.all_special_tokens) # 輸出LlamaTokenizer的所有特殊詞匯ID print(llama_tokenizer.all_special_ids) # 輸出LlamaTokenizer的特殊詞匯映射 print(llama_tokenizer.special_tokens_map) # 將中文詞匯添加到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 = piecenew_p.score = 0# 將中文詞匯添加到LLaMA tokenizer中l(wèi)lama_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) # 打開(kāi)合并后的sentencepiece模型文件,準(zhǔn)備寫(xiě)入 with open(output_sp_dir + '/chinese_llama.model', 'wb') as f: # 將合并后的sentencepiece模型序列化為字符串并寫(xiě)入文件 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) # 定義測(cè)試文本 text = '''白日依山盡,黃河入海流。欲窮千里目,更上一層樓。 The primary use of LLaMA is research on large language models, including''' # 輸出測(cè)試文本 print("Test text:\n", text) print # 使用原始的LLaMA tokenizer對(duì)文本進(jìn)行分詞 print(f"Tokenized by LLaMA tokenizer:{llama_tokenizer.tokenize(text)}") # 使用合并后的Chinese-LLaMA tokenizer對(duì)文本進(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è)過(guò)程包括了加載模型、合并模型、保存新的tokenizer以及進(jìn)行測(cè)試等步驟,具體如下

  • 首先,通過(guò)argparse模塊獲取命令行參數(shù),包括原始的LLaMA tokenizer的路徑和中文sentencepiece模型的路徑
  • 接著,加載這兩個(gè)模型,并將它們轉(zhuǎn)換為Protocol Buffers格式,方便進(jìn)行操作
  • 然后,從中文sentencepiece模型中提取詞匯,并將這些詞匯添加到LLaMA tokenizer中
    在這個(gè)過(guò)程中,需要檢查每個(gè)中文詞匯是否已經(jīng)存在于LLaMA tokenizer中,以避免重復(fù)添加
  • 將合并后的模型保存到指定的目錄
    即首先保存為sentencepiece模型文件,然后創(chuàng)建一個(gè)新的LlamaTokenizer實(shí)例,并將其保存為Hugging Face格式的tokenizer
  • 最后,對(duì)原始的LLaMA tokenizer和合并后的Chinese-LLaMA tokenizer進(jìn)行測(cè)試,以驗(yàn)證合并是否成功
    測(cè)試包括輸出特殊詞匯、特殊詞匯ID、特殊詞匯映射等信息,以及使用這兩個(gè)tokenizer對(duì)給定文本進(jìn)行分詞
    從測(cè)試結(jié)果可以看出,合并后的Chinese-LLaMA tokenizer能夠更好地處理中文文本
  • 此外,七月在線ChatGPT原理解析課一學(xué)員在群內(nèi)問(wèn)道:“如何擴(kuò)充詞表,訓(xùn)練embedding,然后再與llama的合并,想在自己的數(shù)據(jù)上試試
    “吹牛班的春天”答道:“我知道的方法就是直接改embedding結(jié)構(gòu):初始化參數(shù)concat到以前embedding層上,以前的權(quán)embedding權(quán)重就保留,多出來(lái)的部分就后面更新,下圖是以前BERT無(wú)損擴(kuò)詞的思路,可做參考”

    2.4.2 加入中文數(shù)據(jù)的預(yù)訓(xùn)練

    在預(yù)訓(xùn)練階段,使用約20G左右的通用中文語(yǔ)料(與中文BERT-wwmMacBERTLERTPERT中使用的語(yǔ)料一致)在原版LLaMA權(quán)重的基礎(chǔ)上進(jìn)一步進(jìn)行預(yù)訓(xùn)練。該過(guò)程又分為兩個(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ù)測(cè)時(shí)采用的都是原版Stanford Alpaca不帶input的模版。對(duì)于包含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ù)量級(jí)來(lái)源說(shuō)明
    中英翻譯數(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(暫無(wú))本項(xiàng)目使用ChatGPT接口進(jìn)行爬取,提供了一個(gè)動(dòng)態(tài)生成不同領(lǐng)域和指令類(lèi)型的prompt爬取腳本script/crawl_prompt.py。

    python script/crawl_prompt.py output-file

    思路與Stanford Alpaca中的做法基本一致,一次批量生成20組數(shù)據(jù)(可自行修改模板),以降低爬取成本
    生成的文件包含通過(guò)gpt-3.5-turbo爬取的數(shù)據(jù)(你必須擁有OpenAI API key才可以使用)
    雖然指令模板中要求輸出JSON,但系統(tǒng)并不總是會(huì)返回合法的JSON,需要自行對(duì)返回?cái)?shù)據(jù)進(jìn)行清洗
    由于爬取時(shí)間比較長(zhǎng),建議后臺(tái)運(yùn)行該腳本,且多線程運(yùn)行時(shí)注意OpenAI API的調(diào)用限制上限

    當(dāng)然,針對(duì)一些任務(wù)上效果不好!原作者也給出了幾個(gè)可能的原因,
    ????????1)本身LLaMA對(duì)中文支持不是很好,大多數(shù)相關(guān)衍生工作是直接在原版上進(jìn)行pretrain/finetune的,而我們采取了更大膽的策略——增加中文詞表,可能進(jìn)一步加劇中文訓(xùn)練不充分的問(wèn)題,但從長(zhǎng)遠(yuǎn)看是否有利于后續(xù)進(jìn)一步預(yù)訓(xùn)練就得靠時(shí)間檢驗(yàn)了;
    ????????2)指令數(shù)據(jù)的質(zhì)量有待進(jìn)一步提升;
    ????????3)訓(xùn)練時(shí)間、超參等方面還有很大調(diào)整空間;
    ????????4)沒(méi)有RLHF;
    ????????5)4-bit量化后效果可能會(huì)下降,因此可以嘗試加載FP16模型,效果相對(duì)更好一些(也更慢)

    2.5? 小結(jié):基于LLaMA微調(diào)的各模型對(duì)比:Alpaca/Vicuna/BELLE/Chinese-LLaMA

    項(xiàng)目一句話描述

    Stanford Alpaca

    結(jié)合英文語(yǔ)料通過(guò)Self Instruct方式微調(diào)LLaMA 7B

    Vicuna-13B

    通過(guò)ShareGPT.com的7萬(wàn)條對(duì)話數(shù)據(jù)微調(diào)LLaMA
    BELLE

    結(jié)合中文語(yǔ)料通過(guò)Self Instruct方式微調(diào)BLOOMZ-7B或LLaMA

    Chinese-LLaMA/Chinese-Alpaca

    通過(guò)中文數(shù)據(jù)預(yù)訓(xùn)練/指令微調(diào)LLaMA
    ChatLLaMA(英文版)

    LLaMA的RLHF版

    ColossalChat

    通過(guò)self-instruct技術(shù)指令微調(diào)LLaMA且加上RLHF

    更多請(qǐng)查看下一篇:從GLM、ChatGLM到MOSS、ChatDoctor、可商用

    總結(jié)

    以上是生活随笔為你收集整理的类ChatGPT的部署与微调(上):从LLaMA、Alpaca/Vicuna/BELLE、中文版的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。