---
title: LLaMA 2 # 簡報的名稱
tags: LLM # 簡報的標籤
slideOptions: # 簡報相關的設定
theme: black # 顏色主題
transition: 'fade' # 換頁動畫
spotlight:
enabled: true
---
# LLaMA 2
Llama 2 是一系列預先訓練和微調的生成文本模型,規模從70億到700億個參數不等,總共使用了 7 萬億 的 token 進行訓練。
其開源模型包含分為 7B、13B、70B 等不同大小的模型,並且有不同 release,hf (huggingfaces) 和 chat (專門為對話微調的模型) 版本,因此共有 4*3 = 12 種不同的模型
## Fine tune LLM
微調大模型有很多種方式
- Last Layer
- Adapter
- [prefix-tuning (P-tuning)](https://github.com/THUDM/P-tuning-v2)
- [Prompt Tuning](https://zhuanlan.zhihu.com/p/415168620)
- [LoRA](https://github.com/microsoft/LoRA) / [QLoRA](https://github.com/artidoro/qlora)
我們這裡使用 LoRA 和 QLoRA 的方式微調
## Finetune
我們使用 Alpaca+LoRA 進行微調,我們當前的任務是要讓模型學會講中文,u因此我使用 [https://github.com/LC1332/Chinese-alpaca-lora](https://github.com/LC1332/Chinese-alpaca-lora) 中文數據集,我們先介紹我們使用的資料格式
### Dataset
資料格式為 json 格式,包含 3 個主要區塊
- `instructions:` 指示詞
- `input:` 模型的輸入
- `output:` 標準答案
在訓練過程中,我們會將 instructions 和 inputs 合併為輸入放入模型中訓練,以下我們看一些簡單的資料範例。
```config=
{
"instruction": "改写下面的句子,使用主动语态。",
"input": "“新闻报道被船长阅读了。”",
"output": "船长读了新闻报道。"
},
{
"instruction": "\"评估这个句子的拼写和语法错误\"。",
"input": "他吃完饭离开了餐厅。",
"output": "他吃完饭离开了餐厅。"
},
{
"instruction": "描述一个原子的结构。",
"input": "",
"output": "一个原子由核心组成,其中包含质子和中子,周围环绕着绕核运动的电子。质子和中子带有正电荷,而电子带有负电荷,导致整个原子是中性的。每种粒子的数量确定了原子的原子序数和类型。"
},
```
### Alpaca LoRA
Alpaca LoRA 使用 LoRA 的方法訓練 Alpaca
#### Alpaca
Standford Alpaca 在 Meta 開源的 LLaMA 上面進行全參數的微調。主要的目的是要讓 AI 學會
- 強大的基礎 LM
- 高質量的 instruction data
在 Alpaca 的設計中,Standford 選用 Meta 的 LLaMA-7B 做為 base model,並使用 Self-Instruct 的方式微調,所謂的 "Self-Instruct" 指的是使用較大的模型生成數據來訓練小的模型,其概念類似於知識蒸餾。

#### LoRA

LLM 的一種看法是把數據映射到高維空間,來學習特徵的多樣化,但是在處理一個細分的下游任務時,我們通常只需要一部分的特徵,因此我們只需要微調整個特徵空間的一部分子空間即可,這些子空間所學習到的特徵相對較少,因此稱為 "Low Rank",LoRA 的做法在於,在原始的參數矩陣上加上一個 low rank matrix
$$
Wx = Wx + BAx
$$
其中 A, B 為兩個 low rank matrix,$W$ 為原始模型的參數,在這裡,他們的參數量也是固定的,我們只需要讓梯度通過 AB 矩陣即可,例如,對於一個 (n,m) 的矩陣 $W$ 而言
- $A \in \mathbb{R}^{r*m}$
- $B \in \mathbb{R}^{n*r}$
可以想像參數量的計算從 $n*m$ 降低到 $n*r+r*m$,當 $n, m$ 很大時,控制矩陣 rank $r$ 可以大幅降低參數量。另外矩陣 rank $r <= \min(n,m)$,在參數初始化階段,我們會將矩陣 $B$ 設置為 0 矩陣 $A$ 為高斯分布的抽樣
我先入為主認為 LoRA 的觀點不在於把一個高維矩陣做矩陣分解,而在於找到一個 low rank 的微調機制,用於在參數空間的子空間中調整出最適合下游任務的一個參數組合
另外,QLoRA 也就只是把 LoRA + Quantilization 降低模型的記憶體占用而已。
#### Alpaca + LoRA
那 Alpaca + LoRA 的技術自然是固定 LLaMA 的餐數,使用 LoRA 的方式訓練指令數據集囉
### Finetune config
接下來介紹我們使用的 finetune config
```yaml=
# model/data params
base_model: LLaMA-7B-hf
data_path: trans_chinese_alpaca_dataset.json
# training params
batch_size: 128
micro_batch_size: 16
num_epochs: 4
learning_rate: 3e-4
cutoff_len: 256
val_set_size: 2000
# lora params
lora_r: 8
lora_alpha: 16
lora_dropout: 0.05
lora_target_modules: ["q_proj", "v_proj"]
# llm params
train_on_inputs: True
add_eos_token: False
group_by_length: False
```
以下我們介紹每個 config 代表的意義
- Training params
- batch_size: 模型更新一次參數所看過的數據數量
- micro_batch_size: 模型 forward 一次的數據數量
- gradient_accumulation_steps = batch_size // micro_batch_size
- cutoff_len: 即 max_length,截斷文本的長度
- val_set_size: 驗證集的數據數量
- LoRA params
- lora_r: matrix rank
- lora_alpha: 調整 lora 和原先矩陣的權重
- lora_dropout: lora 層的 dropout
- lora_target_modules: 需要執行 lora
- LLM params
- train_on_inputs: 計算 loss 要不要把忽略 input
- add_eos_token: end of sequence token
- group_by_length: 是否把長度相似的文本合併
### 實現過程
這裡我們參考 [alpaca-lora](https://github.com/tloen/alpaca-lora) 的代碼做微調解析
#### Prompter
我們每次輸入給模型和的資料其實是一段有固定格式的文字,如下:
```json=
{
"description": "Template used by Alpaca-LoRA.",
"prompt_input": "Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.\n\n### Instruction:\n{instruction}\n\n### Input:\n{input}\n\n### Response:\n",
"prompt_no_input": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\n{instruction}\n\n### Response:\n",
"response_split": "### Response:"
}
```
從上面可以看到,input 是可有可無的一個輸入。在字串中,{instruction} 和 {input} 表示模型的輸入,為了方便處理,我們定義一個 Prompter 用來處理固定格式的字串如下:
```python=
class Prompter(object):
__slots__ = ("template", "_verbose")
def __init__(self, template_name: str = "", verbose: bool = False):
self._verbose = verbose
if not template_name:
# Enforce the default here, so the constructor can be called with '' and will not break.
template_name = "alpaca"
file_name = osp.join("templates", f"{template_name}.json")
if not osp.exists(file_name):
raise ValueError(f"Can't read {file_name}")
with open(file_name) as fp:
self.template = json.load(fp)
if self._verbose:
print(
f"Using prompt template {template_name}: {self.template['description']}"
)
def generate_prompt(
self,
instruction: str,
input: Union[None, str] = None,
label: Union[None, str] = None,
) -> str:
# returns the full prompt from instruction and optional input
# if a label (=response, =output) is provided, it's also appended.
if input:
res = self.template["prompt_input"].format(
instruction=instruction, input=input
)
else:
res = self.template["prompt_no_input"].format(
instruction=instruction
)
if label:
res = f"{res}{label}"
if self._verbose:
print(res)
return res
def get_response(self, output: str) -> str:
return output.split(self.template["response_split"])[1].strip()
```
在 `Prompter` 中,template 預設如上範例,我們在 generate_prompt 中,將傳入的資料使填入透過 `.format(kwargs=xxx)` 填入 Iinstruction 和 input,如果有提供 output (訓練階段),我們就將 output 也合併 (`{res}{label}`)。另外一些格式如下
- `prompt_input`: 在輸入不為 None 時使用的模板。使用 `{instruction}` 和 `{input}` 佔位符。
- `prompt_no_input`: 在輸入為 None 時使用的模板。使用 `{instruction}` 佔位符。
- `description`: 對模板的簡短描述,可能包含使用案例。
- `response_split`: 在從模型輸出中切割實際響應時使用的分隔符。
沒有使用 `{response}` 佔位符,因為響應始終是模板的最後一個元素,只需與其餘部分串聯。支援的 template 共有 alpaca, alpaca_legacy, alpace_short, vigogne,這裡就不展開討論了。
#### Tokenizer
處理完資料的串接,我們必須把字串轉成電腦看得懂的形式,也就是編碼,處理邏輯如下

其中 tokenizer 的輸出共有
- `input_ids:` 對應於上圖的 tokens_tensor,表示每個 token 對應到的 id (one-to-one)
- `token_type_ids:` 對應於上圖的 segments_tensor,表示句子分隔線,在單一句子任務中,如情感分類,默認皆為 0,在配對句子中用 0, 1 區隔
- `attention_mask:` 對應於上圖的 masks_tensor,表示注意力機制作用的範圍,1 表示作用注意力機制
```python=
tokenizer = LlamaTokenizer.from_pretrained(base_model)
tokenizer.pad_token_id = (
0 # unk. we want this to be different from the eos token
)
tokenizer.padding_side = "left" # Allow batched inference
def tokenize(prompt, add_eos_token=True):
# there's probably a way to do this with the tokenizer settings
# but again, gotta move fast
result = tokenizer(
prompt,
truncation=True,
max_length=cutoff_len,
padding=False,
return_tensors=None,
)
if (
result["input_ids"][-1] != tokenizer.eos_token_id
and len(result["input_ids"]) < cutoff_len
and add_eos_token
):
result["input_ids"].append(tokenizer.eos_token_id)
result["attention_mask"].append(1)
result["labels"] = result["input_ids"].copy()
return result
def generate_and_tokenize_prompt(data_point):
full_prompt = prompter.generate_prompt(
data_point["instruction"],
data_point["input"],
data_point["output"],
)
tokenized_full_prompt = tokenize(full_prompt)
if not train_on_inputs:
user_prompt = prompter.generate_prompt(
data_point["instruction"], data_point["input"]
)
tokenized_user_prompt = tokenize(
user_prompt, add_eos_token=add_eos_token
)
user_prompt_len = len(tokenized_user_prompt["input_ids"])
if add_eos_token:
user_prompt_len -= 1
tokenized_full_prompt["labels"] = [
-100
] * user_prompt_len + tokenized_full_prompt["labels"][
user_prompt_len:
] # could be sped up, probably
return tokenized_full_prompt
```
我們將模型指示詞 (instruction, input, output) 通過 `Prompter` 得到輸出,之後輸入 `tokenizer` 得到編碼
- `tokenize:` 進行編碼,並在句子最後一個位置加上 EOS token 表示生成句子結束,最後加上一個新的鍵 "labels" 用於紀錄句子標籤
- `generate_and_tokenize_prompt:` 輸入資料字典,並呼叫 `tokenize` 進行編碼,如果是推理階段 (train_on_inputs) 則將 instruction 和 inputs 的 labels 設置為 -100,並用 inputs_ids 補至 max_length
#### Load module
接下來,我們導入需要使用的模組,在 hf 中,我們只需要呼叫幾個簡單的 API 即可完成訓練
```python=
model = LlamaForCausalLM.from_pretrained(
base_model,
# load_in_8bit=True,
torch_dtype=torch.float16,
device_map=device_map,
)
model = prepare_model_for_int8_training(model)
config = LoraConfig(
r=lora_r,
lora_alpha=lora_alpha,
target_modules=lora_target_modules,
lora_dropout=lora_dropout,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, config)
```
- `load_in_8bit:` 是套件 bitsandbytes 提供的功能,主要將模型原本參數資料型態 fp32 (4 bytes) 轉換為 int8 (1 byte),讓記憶體占用節省 1/4 空間。具體會把 `c_attn.weight` 和 `c_proj` 資料型態轉換為 int8,其餘則是 fp16
- `prepare_model_for_int8_training:` 使用 LLM.int8() 方法使得 Attention 模塊中 LayerNorm 和輸出層保留 fp32 以保證模型在回答問題時的差異性 (這樣 sample 輸出時答案差距較大),並且設置 `gradient_checkpoint=True`,即是在 forward 過程設置 `torch.no_grad()` 在計算圖中不儲存每個節點的梯度,只記錄輸入和激活函數,只有在 backward 時才會計算梯度,所以總共要進行兩次 forward,但是減少了記憶體占用
- `get_peft_model:` 新增 LoRA 層,總共有 4 層,資料型態為 fp32
- `base_model.model.transformer.h.0.attn.c_attn.lora_A.default.weight`
- `base_model.model.transformer.h.0.attn.c_attn.lora_B.default.bias`
- `base_model.model.transformer.h.0.attn.c_attn.lora_A.default.weight`
- `base_model.model.transformer.h.0.attn.c_attn.lora_B.default.bias`
#### Training
接下來終於要進入訓練階段了
```python=
trainer = transformers.Trainer(
model=model,
train_dataset=train_data,
eval_dataset=val_data,
args=transformers.TrainingArguments(
per_device_train_batch_size=micro_batch_size,
gradient_accumulation_steps=gradient_accumulation_steps,
warmup_steps=100,
num_train_epochs=num_epochs,
learning_rate=learning_rate,
fp16=True,
logging_steps=10,
optim="adamw_torch",
evaluation_strategy="steps" if val_set_size > 0 else "no",
save_strategy="steps",
eval_steps=200 if val_set_size > 0 else None,
save_steps=200,
output_dir=output_dir,
save_total_limit=3,
load_best_model_at_end=True if val_set_size > 0 else False,
ddp_find_unused_parameters=False if ddp else None,
group_by_length=group_by_length,
report_to="wandb" if use_wandb else None,
run_name=wandb_run_name if use_wandb else None,
),
data_collator=transformers.DataCollatorForSeq2Seq(
tokenizer, pad_to_multiple_of=8, return_tensors="pt", padding=True
),
)
model.config.use_cache = False
old_state_dict = model.state_dict
model.state_dict = (
lambda self, *_, **__: get_peft_model_state_dict(
self, old_state_dict()
)
).__get__(model, type(model))
if torch.__version__ >= "2" and sys.platform != "win32":
model = torch.compile(model)
trainer.train(resume_from_checkpoint=resume_from_checkpoint)
model.save_pretrained(output_dir)
```
需要注意到的是
- `fp16=True:` 對於部分參數,在 forward 過程會以半經度 fp16 訓練,同時會複製一份 fp32 儲存,並在 backward 時使用全經度 fp32 進行更新。
- `torch.comile:` 可以優化模型訓練速度 (pytorch 版本必須 >= 2)
- `model.save_pretrained:` PeftModel 重新改寫了該方法,因此只會儲存 LoRA 的權重
預設中使用 wandb 儲存訓練相關資訊,因此我們可以使用 wandb 視覺化訓練過程,筆者的訓練節果詳見 [https://wandb.ai/ddcvlab/Alpaca-LoRA](https://wandb.ai/ddcvlab/Alpaca-LoRA)
#### Inference
訓練完成過程當然要進行推理啦
```python=
base_model = base_model or os.environ.get("BASE_MODEL", "")
assert (
base_model
), "Please specify a --base_model, e.g. --base_model='huggyllama/llama-7b'"
prompter = Prompter(prompt_template)
tokenizer = LlamaTokenizer.from_pretrained(base_model)
if device == "cuda":
model = LlamaForCausalLM.from_pretrained(
base_model,
load_in_8bit=load_8bit,
torch_dtype=torch.float16,
device_map="cuda",
)
model = PeftModel.from_pretrained(
model,
lora_weights,
torch_dtype=torch.float16,
)
# unwind broken decapoda-research config
model.config.pad_token_id = tokenizer.pad_token_id = 0 # unk
model.config.bos_token_id = 1
model.config.eos_token_id = 2
if not load_8bit:
model.half() # seems to fix bugs for some users.
model.eval()
if torch.__version__ >= "2" and sys.platform != "win32":
model = torch.compile(model)
```
基本上推理的代碼就沒有什麼需要特別說明的
- `model.half():` 在把模型放入 GPU 之後,可以使用該方法將部分參數改為半精度,加速推理和減少顯存占用
之後,我們定義 evaluate 函數
```python=
def evaluate(
instruction,
input=None,
temperature=0.1,
top_p=0.75,
top_k=40,
num_beams=4,
max_new_tokens=128,
stream_output=False,
**kwargs,
):
prompt = prompter.generate_prompt(instruction, input)
inputs = tokenizer(prompt, return_tensors="pt")
input_ids = inputs["input_ids"].to(device)
generation_config = GenerationConfig(
temperature=temperature,
top_p=top_p,
top_k=top_k,
num_beams=num_beams,
**kwargs,
)
with torch.no_grad():
generation_output = model.generate(
input_ids=input_ids,
generation_config=generation_config,
return_dict_in_generate=True,
output_scores=True,
max_new_tokens=max_new_tokens,
)
s = generation_output.sequences[0]
output = tokenizer.decode(s)
yield prompter.get_response(output)
```
- `temprature:` 模型的創造力,越高的 temprature,則有越高的創造力,具體作用是把輸出的 logits 做 softmax + temprature
$$
\sigma(z_i) = \frac{e^{z_i}}{\sum_je^{z_j}} \to \frac{e^{\frac{z_i}{T}}}{\sum_je^\frac{z_j}{T}}
$$

- `top_k:` 從機率最大的前 k 個抽樣

- `top_p:` 大過於 top_p 的 token 才會被考慮生成

Note: 如果 `top_k` 和 `top_p` 都被設定,則會先作用 `top_k`
- `num_beams:` 在每一次生成 token 時,都會保留上一次生成子句中前 num_beams 個結果,然後繼續生成下一個子句,直到所有可能性都被生成完成,保留前 `num_beams` 個結果

#### Toy example





從上述的範例中可以看到,LLaMA 中翻的程度慘不忍睹,另外,我們必須給定足夠的 information 才可以讓 AI 知道我們的需求
## Reference
- [https://docs.cohere.com/docs/the-cohere-platform](https://docs.cohere.com/docs/the-cohere-platform)
- [https://zhuanlan.zhihu.com/p/616504594](https://zhuanlan.zhihu.com/p/616504594)
- [https://github.com/tloen/alpaca-lora](https://github.com/tloen/alpaca-lora)