目錄
1、指令微調的訓練過程
2、指令微調數據
2.1、“指令輸入”
2.2、“答案輸出”
3、指令微調數據的構建方法
3.1、手動構建:純人工 “出題 + 寫答案”
3.1.1、構建流程
3.1.1.1、定義任務類型
3.1.1.2、設計指令模板
3.1.1.3、人工標注響應
3.1.2、工具支持
3.1.3、 優缺點
3.2、現有數據集轉換:給 “舊材料” 換個 “新包裝”
3.2.1、常見轉換來源
3.2.1.1、問答數據集
3.2.1.2、摘要數據集
3.2.1.3、多輪對話數據集
3.2.1.4、專業領域數據
3.2.2、 轉換技巧
3.2.3、優缺點
3.3、自動構建:讓機器自己 “編題 + 答題”
3.3.1、基于大模型生成
3.3.1.1、自我指導(Self-Instruct)
3.3.1.2、指令模板填充
3.3.2、基于規則生成
3.3.2.1、指令變體生成
3.3.2.2、響應合成
3.3.3、混合方法
3.3.4、優缺點
3.4、三種方法對比與結合
4、模型微調
4.1、先搞懂:為什么需要模型微調?
4.2、參數高效微調:給大模型 “輕量補課”
4.3、逐個拆解:LoRA、AdaLoRA、QLoRA
4.3.1、 LoRA(Low-Rank Adaptation):給模型加 “臨時支路”
4.3.2、?AdaLoRA(Adaptive LoRA):給 “重要支路” 加寬
4.3.3、QLoRA(Quantized LoRA):給支路加 “壓縮包”
4.4、三者對比:怎么選?
4.5、通俗總結
5、完整代碼
6、實驗結果
指令微調,是指在預訓練大預言模型的基礎上,通過使用有標注的自然語言形式的數據,對模型參數進行微調,使模型具備指令遵循能力,能夠完成各類預先設計的任務,并可以在零樣本情況下處理諸多下游任務。
1、指令微調的訓練過程
分為三個步驟:
(1) 針對每一項任務明確地定義相應的自然語言形式的指令或者指示,這些指令或提示對任務目標及輸出要求進行清晰描述。
(2)將訓練數據調整成包含指令及與之對應的響應的形式
(3)使用包含指令和響應的訓練數據對預訓練模型進行微調操作。
2、指令微調數據
指令微調數據通常由文本構成,包含“指令輸入”與“答案輸出”兩個關鍵部分。
2.1、“指令輸入”
是指人們向模型提出的各類請求,包含定義精準、清晰的指令或者提示信息,其核心作用在于詳細闡釋任務的目標究竟是什么,以及明確規定輸出需要滿足的各項要求。指令涵蓋的范疇極為廣泛,包括問題回答、信息分類、內容總結、文本改寫等。
2.2、“答案輸出”
則是指期望模型依據所接收的指令響應內容,這些響應需要符合人們預先設定的期望。答案輸出的內容,可以使用人工手段或借助自動化方法構建。
3、指令微調數據的構建方法
指令微調數據的構建方法主要分為手動構建、現有數據集轉換和自動構建三種,以下是對這三種方法的詳細解析:
3.1、手動構建:純人工 “出題 + 寫答案”
通過人工設計指令和標注響應,適合高質量、特定領域數據。
3.1.1、構建流程
3.1.1.1、定義任務類型
- 明確覆蓋的任務類別(如問答、摘要、翻譯、推理等)。
- 示例任務清單:
- 開放式問答(如解釋科學概念) - 封閉式問答(如選擇題) - 文本摘要(如新聞摘要) - 指令生成(如寫郵件、代碼) - 推理任務(如數學問題、邏輯推理)
3.1.1.2、設計指令模板
- 為每個任務類型創建多樣化的指令模板,避免單一表述。
- 示例模板:
- 解釋類:"請解釋{概念}的原理" - 摘要類:"用{X}個字總結以下文本" - 翻譯類:"將{源語言}翻譯成{目標語言}:{文本}" - 改寫類:"用{風格}改寫以下句子:{文本}"
3.1.1.3、人工標注響應
- 標注者根據指令生成高質量響應,確保:
- 準確性:事實正確,無錯誤信息。
- 完整性:回答完整,不遺漏關鍵細節。
- 一致性:格式和風格統一(如使用項目符號、分段等)。
3.1.2、工具支持
- 標注平臺:LabelStudio、Prodigy、Amazon SageMaker Ground Truth。
- 協作管理:使用 Notion、Google Sheets 管理任務分配和進度。
3.1.3、 優缺點
- 優點:數據質量高,符合特定領域需求,可控性強。
- 缺點:成本高(人力 + 時間),規模受限,難以覆蓋長尾場景。
3.2、現有數據集轉換:給 “舊材料” 換個 “新包裝”
將公開數據集或私有數據轉換為指令 - 響應格式,適合快速構建大規模數據。
很多領域早就有現成的數據集了(比如以前做翻譯的雙語數據、做分類的文本 + 標簽數據),把這些 “舊數據” 改寫成 “指令 + 答案” 的形式。
3.2.1、常見轉換來源
3.2.1.1、問答數據集
- 示例:SQuAD、Natural Questions → 轉換為指令格式。
- 轉換方法:
# 原始數據:{"context": "巴黎是法國首都", "question": "法國首都在哪里", "answer": "巴黎"} # 轉換后: {"instruction": "回答以下問題:法國首都在哪里", "output": "巴黎"}
3.2.1.2、摘要數據集
- 示例:CNN/Daily Mail、XSum → 轉換為 “總結文本” 指令。
- 轉換方法:
# 原始數據:{"article": "……", "summary": "……"} # 轉換后: {"instruction": "總結以下新聞:{article}", "output": "{summary}"}
3.2.1.3、多輪對話數據集
- 示例:CoQA、DialogSum → 轉換為多輪指令交互。
- 轉換方法:
# 原始對話:[{"user": "今天天氣如何?", "assistant": "晴天,25℃"}, {"user": "適合穿什么?", "assistant": "穿短袖和薄外套"}] # 轉換后: {"instruction": "根據對話回答問題:今天適合穿什么?對話歷史:今天天氣如何?晴天,25℃", "output": "穿短袖和薄外套"}
3.2.1.4、專業領域數據
- 示例:醫療病歷、法律文書 → 轉換為領域特定指令。
- 轉換方法:
# 原始病歷:{"symptoms": "頭痛、發熱", "diagnosis": "流感"} # 轉換后: {"instruction": "根據癥狀診斷疾病:頭痛、發熱", "output": "流感"}
3.2.2、 轉換技巧
- 增加多樣性:對同一原始數據,使用多個指令模板生成不同版本。
示例:# 原始數據:{"text": "蘋果公司成立于1976年"} # 轉換版本1: {"instruction": "提取關鍵信息:蘋果公司成立于1976年", "output": "蘋果公司,成立時間:1976年"} # 轉換版本2: {"instruction": "將以下句子轉換為問答形式:蘋果公司成立于1976年", "output": "問題:蘋果公司何時成立?答案:1976年"}
3.2.3、優缺點
- 優點:快速獲取大規模數據,成本低,保留原始數據的領域知識。
- 缺點:格式適配可能復雜,需處理數據噪聲,領域可能受限。
3.3、自動構建:讓機器自己 “編題 + 答題”
利用模型或算法自動生成指令 - 響應數據,適合低成本擴展數據規模。
用已經訓練好的模型(比如大模型本身)自動生成指令和答案,相當于讓 “學生” 自己出題自己做。
3.3.1、基于大模型生成
3.3.1.1、自我指導(Self-Instruct)
- 流程:
1. 使用少量人工標注數據訓練初始模型。 2. 用初始模型生成新的指令-響應樣本。 3. 人工篩選高質量樣本,擴充訓練集。 4. 重復步驟2-3迭代優化。
- 工具:Hugging Face 的
self-instruct
庫。
3.3.1.2、指令模板填充
- 方法:使用預訓練模型填充指令模板中的變量。
- 示例:
運行
# 模板:"解釋{概念}的{方面}在{領域}中的應用" # 填充后: {"instruction": "解釋注意力機制的原理在自然語言處理中的應用", "output": "注意力機制……"}
3.3.2、基于規則生成
3.3.2.1、指令變體生成
- 方法:對現有指令進行語法改寫、同義詞替換等。
- 示例:
# 原始指令:"將這段文本翻譯成英文" # 變體指令: ["請把這段文字轉為英文", "翻譯以下內容到英文", "用英文表達這段文本"]
3.3.2.2、響應合成
- 方法:從知識庫或 API 獲取信息,自動生成響應。
- 示例:
# 指令:"查詢特斯拉公司2023年Q1營收" # 響應:通過調用財務API獲取數據后生成。
3.3.3、混合方法
- 流程:
1. 使用規則生成基礎指令-響應模板。 2. 用大模型對模板進行多樣化擴展。 3. 通過人工或自動化篩選機制過濾低質量樣本。
3.3.4、優缺點
- 優點:低成本、高效率,可大規模擴展數據。
- 缺點:生成質量可能參差不齊,需嚴格篩選機制,可能引入模型偏見。
3.4、三種方法對比與結合
方法 | 成本 | 質量 | 規模 | 領域適配性 |
---|---|---|---|---|
手動構建 | 高 | 高 | 小 | 強 |
數據集轉換 | 中 | 中 | 大 | 依賴原始數據 |
自動構建 | 低 | 中 - 低 | 極大 | 需驗證 |
推薦組合策略:
- 冷啟動階段:手動構建小規模高質量種子數據(如 1000 條)。
- 擴展階段:
- 使用種子數據訓練初始模型,通過自動構建生成大量候選數據。
- 將現有公開數據集轉換為指令格式,補充多樣性。
- 優化階段:
- 人工篩選自動生成的高質量樣本,加入訓練集。
- 針對薄弱領域(如低資源語言、專業領域)補充手動構建數據。
4、模型微調
模型微調是讓預訓練大模型(比如 GPT、LLaMA 等)“專項進修” 的過程 —— 就像一個學了基礎知識的大學生,通過針對性訓練成為某領域專家(比如從 “全科生” 變成 “法律顧問” 或 “代碼助手”)。
4.1、先搞懂:為什么需要模型微調?
預訓練大模型(比如 GPT-3.5)已經通過海量數據學會了語言規律、常識等 “通用能力”,但直接用它做具體任務(比如公司內部的客服問答、特定行業的數據分析)往往不夠精準。
比如:用通用大模型回答 “我們公司的退款政策是什么”,它可能瞎編;但如果用公司歷史退款記錄微調后,就能準確回答。
傳統微調的問題:大模型參數太多(動輒幾十億、上千億),直接訓練所有參數就像 “重新教一遍”,又慢又費錢(需要頂級 GPU),還容易 “學歪”(忘記原有知識)。
4.2、參數高效微調:給大模型 “輕量補課”
為了解決傳統微調的痛點,研究者想出了參數高效微調(PEFT)?方法:不碰原模型的大部分參數,只訓練少量 “新增參數”,效果卻能接近全量微調。
LoRA、AdaLoRA、QLoRA 都是 PEFT 的代表,核心思路類似 “給原模型加小插件,只訓練插件”。
4.3、逐個拆解:LoRA、AdaLoRA、QLoRA
4.3.1、 LoRA(Low-Rank Adaptation):給模型加 “臨時支路”
核心思想:不改動原模型的 “主干道”(大參數矩陣),而是新增兩條 “臨時支路”(小矩陣),讓模型在訓練時主要走支路,推理時再把支路合并回主干道。
-
打個比方:原模型的參數像一條寬馬路,LoRA 在旁邊修了兩條窄巷子(A 和 B)。訓練時,數據主要從巷子走(只優化 A 和 B);訓練完,把巷子的 “流量” 合并到主馬路,不影響原馬路結構。
-
具體做法:
大模型里有很多 “注意力矩陣”(負責計算文字間的關聯,比如 “貓” 和 “抓” 更相關),這些矩陣很大(比如 1024x1024)。
LoRA 把這些大矩陣拆成兩個小矩陣(比如 1024x8 和 8x1024,“8” 是秩,可調整),只訓練這兩個小矩陣(參數從百萬級降到萬級),原矩陣凍結不動。 -
優點:
- 訓練速度快(參數少)、省顯存(不用存原模型的梯度);
- 訓練完合并參數后,推理速度和原模型一樣(不增加額外計算)。
-
適用場景:大部分微調任務(比如文本分類、對話機器人),尤其是資源中等的情況(有一塊較好的 GPU)。
4.3.2、?AdaLoRA(Adaptive LoRA):給 “重要支路” 加寬
核心思想:LoRA 的 “支路寬度”(秩)是固定的,但模型不同層的重要性不一樣(比如有的層負責理解語義,有的層作用不大)。AdaLoRA 讓 “重要的層” 支路寬一點(秩大),“不重要的層” 支路窄一點(秩小),更省資源。
-
打個比方:LoRA 給所有路段都修了同樣寬的巷子,AdaLoRA 則根據路段的車流量(重要性)調整巷子寬度 —— 市中心(重要層)巷子寬(秩 = 16),郊區(次要層)巷子窄(秩 = 4)。
-
具體做法:
訓練時動態計算每個層的 “貢獻度”(比如該層對任務的影響多大),貢獻高的層分配更大的秩(小矩陣更大),貢獻低的層縮小秩甚至關掉支路。 -
優點:比 LoRA 更高效,同樣效果下參數更少(省 10%-30% 資源)。
-
適用場景:資源緊張但任務復雜的情況(比如多輪對話、長文本理解),需要精打細算用資源。
4.3.3、QLoRA(Quantized LoRA):給支路加 “壓縮包”
核心思想:在 LoRA 基礎上,給原模型參數 “瘦身”(量化),再訓練支路。比如把原模型的參數從 “32 位浮點數” 壓成 “4 位整數”(類似把高清圖轉成壓縮圖),大幅節省顯存。
-
打個比方:原模型是一個 100GB 的大文件,QLoRA 先把它壓縮成 10GB(但信息基本保留),再在壓縮后的文件上修 LoRA 支路,訓練時電腦只需要裝下 10GB 文件 + 支路,普通電腦也能跑。
-
具體做法:
用 “4 位量化” 存儲原模型參數(顯存占用降為原來的 1/8),同時用 LoRA 訓練新增的小矩陣。訓練時通過 “量化感知訓練” 保證精度不下降,最后合并參數。 -
優點:資源要求極低,比如用消費級顯卡(如 RTX 3090)就能微調 70 億甚至 130 億參數的大模型(傳統方法需要幾十塊頂級 GPU)。
-
適用場景:個人或小團隊微調大模型(比如用 LLaMA-7B 做私人助手),顯存有限的情況(只有一塊普通 GPU)。
4.4、三者對比:怎么選?
方法 | 核心改進 | 顯存需求 | 適用場景 | 一句話總結 |
---|---|---|---|---|
LoRA | 固定秩的低秩分解 | 中 | 中等資源,通用任務 | 基礎款 “輕量微調”,平衡速度和效果 |
AdaLoRA | 動態調整秩(按需分配) | 中低 | 資源緊張,復雜任務 | 智能款 “按需分配”,更省參數 |
QLoRA | 4 位量化 + LoRA | 極低 | 個人 / 小團隊,大模型微調 | 平民款 “壓縮微調”,普通電腦能跑 |
4.5、通俗總結
- 傳統微調:給大模型 “全身體檢 + 重訓”,貴且麻煩;
- LoRA:只給大模型 “局部小手術”,快又省;
- AdaLoRA:“智能小手術”,哪里重要修哪里;
- QLoRA:“壓縮后小手術”,普通設備也能做。
5、完整代碼
# 導入必要的庫
import json # 用于數據的序列化和反序列化
import random # 用于數據打亂,保證訓練隨機性
import torch # PyTorch核心庫,用于張量計算和模型訓練
import os # 用于文件路徑操作和驗證
import warnings # 用于屏蔽無關警告,保持輸出整潔
from tqdm import tqdm # 用于顯示進度條,直觀展示訓練/評估進度
from datasets import Dataset # Hugging Face的數據集類,用于數據格式轉換
from transformers import (AutoTokenizer, # 自動加載預訓練模型的分詞器AutoModelForCausalLM, # 自動加載因果語言模型(如GPT-2)TrainingArguments, # 訓練參數配置類Trainer, # 訓練器類,封裝了訓練邏輯BitsAndBytesConfig # 量化配置(當前代碼禁用,保留為擴展接口)
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training # PEFT庫,用于參數高效微調(LoRA)
from nltk.translate.bleu_score import sentence_bleu # 用于計算BLEU分數,評估生成文本質量# 屏蔽無關警告(可選,根據需要開啟)
# 屏蔽bitsandbytes庫的警告(當前禁用量化,可注釋)
warnings.filterwarnings("ignore", category=UserWarning, module="bitsandbytes")
# 屏蔽torch.checkpoint的reentrant參數警告
warnings.filterwarnings("ignore", category=UserWarning, message="torch.utils.checkpoint: the use_reentrant parameter")# 設置隨機種子,確保實驗可復現
random.seed(42) # Python原生隨機數種子
torch.manual_seed(42) # PyTorch隨機數種子# ----------------------
# 一、數據構建模塊:生成三種類型的指令微調數據
# (解釋類、寫作類、分析類,覆蓋典型微調場景)
# ----------------------def manual_construction():"""手動構建三類指令數據,作為基礎樣本"""manual_data = [# 1. 解釋類:解釋概念/技術{"instruction": "解釋什么是機器學習","input": "", # 無額外輸入"output": "機器學習是人工智能的一個分支,它通過算法讓計算機從數據中學習模式,而不需要明確的編程指令。常見的機器學習任務包括分類、回歸、聚類等。"},# 2. 寫作類:生成特定格式文本(郵件){"instruction": "寫一封申請實習的郵件","input": "申請崗位:AI研究員,公司:ABC科技,附學歷證明", # 提供關鍵信息"output": "尊敬的招聘經理:\n\n您好!我是XX大學計算機專業的本科生,預計2025年畢業。我對貴公司的AI研究方向非常感興趣,特此申請AI研究員實習崗位。\n\n隨信附上我的簡歷和學歷證明,期待您的回復。\n\n此致\n敬禮\n申請人:張三"},# 3. 翻譯類(屬于寫作類變體):語言轉換{"instruction": "將以下文本翻譯成英文","input": "深度學習已經徹底改變了自然語言處理領域。", # 待翻譯文本"output": "Deep learning has revolutionized the field of natural language processing."},# 4. 分析類:情感傾向分析{"instruction": "分析下面這段文字的情感傾向","input": "這款新手機的電池續航太差了,完全達不到宣傳的效果!", # 待分析文本"output": "負面。文本中使用了'太差了'這樣的負面評價詞,并且指出產品'完全達不到宣傳的效果',表達了對手機電池續航的不滿。"},# 5. 建議類(屬于分析類變體):提供解決方案{"instruction": "給出三個提高代碼可讀性的建議","input": "", # 無額外輸入"output": "1. 使用有意義的變量名和函數名,避免使用無意義的縮寫;2. 添加適當的注釋,特別是復雜算法或關鍵邏輯部分;3. 遵循一致的代碼風格和縮進規則,保持代碼結構清晰。"}]return manual_datadef dataset_conversion():"""跳過外部數據集加載(如需擴展,可在此處添加其他數據集轉換邏輯)"""print("已跳過外部數據集加載")return []def auto_construction(num_samples=50):"""自動生成50條樣本,補充三類指令數據(保持數據多樣性)"""print("自動生成數據(本地模式,不加載外部模型)")auto_data = []# 用于生成"解釋類"指令的概念列表concepts = ["神經網絡", "區塊鏈", "量子計算", "自然語言處理", "大數據", "云計算"]# 用于生成"寫作類"指令的主題列表themes = ["人工智能的發展趨勢", "環境保護的重要性", "元宇宙的未來", "可再生能源的應用"]# 用于生成"分析類"指令的文本列表analyze_texts = ["這家餐廳的服務特別好,菜品也很美味,下次還會再來","這個軟件頻繁崩潰,客服也不解決問題,非常失望","這部電影劇情緊湊,演員演技出色,強烈推薦"]for i in range(num_samples):if i % 5 == 0: # 每5條樣本生成1條"解釋類"concept = concepts[i % len(concepts)]instruction = f"解釋{concept}的工作原理"output = f"{concept}是一種重要的技術,廣泛應用于多個領域,通過特定的機制實現其功能。其核心原理包括數據輸入、處理邏輯和結果輸出三個環節。"elif i % 5 == 1: # 每5條樣本生成1條"寫作類"theme = themes[i % len(themes)]instruction = f"寫一篇關于{theme}的短文(100字左右)"output = f"{theme}是當前社會關注的熱點話題。隨著技術進步和認知提升,其在經濟、環境和社會層面的影響日益顯著。深入研究其發展規律,對未來規劃具有重要意義。"elif i % 5 == 2: # 每5條樣本生成1條"翻譯類"(寫作類變體)instruction = "將以下句子改寫成正式的表達方式"input_text = "這個技術特別好用,大家都覺得不錯"output = "該技術具有較高的實用性,獲得了廣泛的認可與好評。"elif i % 5 == 3: # 每5條樣本生成1條"解釋類"(補充)instruction = "回答以下問題:什么是人工智能?"output = "人工智能是研究如何使計算機模擬人類智能行為的科學與技術,涵蓋機器學習、自然語言處理、計算機視覺等多個分支。"else: # 剩余樣本生成"分析類"text = analyze_texts[i % len(analyze_texts)]instruction = f"分析這段文字的情感傾向:{text}"input_text = text# 根據文本內容生成對應情感分析結果if "好" in text or "美味" in text or "推薦" in text:output = "正面。文本中使用了'好'、'美味'等積極詞匯,表達了對事物的滿意和推薦態度。"else:output = "負面。文本中使用了'崩潰'、'失望'等消極詞匯,表達了對事物的不滿情緒。"auto_data.append({"instruction": instruction,"input": input_text if i % 5 == 2 or i % 5 == 4 else "", # 僅特定樣本需要input"output": output})return auto_datadef build_full_dataset():"""組合手動和自動生成的數據,構建完整數據集"""print("開始構建數據集...")manual_data = manual_construction()print(f"手動構建完成: {len(manual_data)} 個樣本(含解釋類、寫作類、分析類)")converted_data = dataset_conversion()print(f"數據集轉換完成: {len(converted_data)} 個樣本")auto_data = auto_construction()print(f"自動構建完成: {len(auto_data)} 個樣本(補充三類指令數據)")# 合并數據并打亂順序(避免同類樣本集中)full_data = manual_data + converted_data + auto_datarandom.shuffle(full_data)# 保存數據集到本地(方便后續查看和復用)with open("instruction_tuning_data.json", "w", encoding="utf-8") as f:json.dump(full_data, f, ensure_ascii=False, indent=2)print(f"數據集構建完成,共{len(full_data)}個樣本(含三類指令)")return full_data# ----------------------
# 二、模型微調模塊:使用LoRA進行參數高效微調
# ----------------------
def finetune_model(dataset):print("開始模型微調(本地模式)")# 本地預訓練模型路徑(需提前下載GPT-2中文模型)model_name = r"E:\WH\data\gpt2-chinese-cluecorpussmall"# 驗證模型路徑是否存在(避免路徑錯誤導致加載失敗)print(f"正在驗證模型路徑: {model_name}")if not os.path.exists(model_name):raise FileNotFoundError(f"模型路徑不存在: {model_name}")# 檢查路徑下是否包含必要的模型文件(確保模型完整)required_files = ["config.json", "pytorch_model.bin", "tokenizer_config.json", "vocab.txt"]missing_files = [f for f in required_files if not os.path.exists(os.path.join(model_name, f))]if missing_files:raise FileNotFoundError(f"模型路徑缺少必要文件: {', '.join(missing_files)}")print(f"模型路徑驗證通過: {model_name}")print(f"正在加載本地模型...")# 加載分詞器(將文本轉換為模型可識別的token)try:tokenizer = AutoTokenizer.from_pretrained(model_name)# GPT-2默認無pad_token,需手動設置為eos_token(確保批量處理時填充有效)if tokenizer.pad_token is None:tokenizer.pad_token = tokenizer.eos_tokenprint(f"已將填充標記設置為:{tokenizer.pad_token}(與結束標記一致)")except Exception as e:raise Exception(f"加載分詞器失敗,請檢查路徑是否正確:{model_name}\n錯誤:{e}")# 格式化數據集:將instruction/input/output轉換為模型訓練的prompt格式def format_dataset(dataset):formatted_data = []for example in dataset:instruction = example["instruction"]input_text = example["input"]output = example["output"]# 區分有/無input的情況,保持prompt格式統一if input_text:prompt = f"### Instruction: {instruction}\n### Input: {input_text}\n### Response: {output}"else:prompt = f"### Instruction: {instruction}\n### Response: {output}"formatted_data.append({"text": prompt}) # 用"text"字段統一存儲return formatted_data# 轉換為Hugging Face Dataset格式(方便后續分詞處理)formatted_data = format_dataset(dataset)hf_dataset = Dataset.from_list(formatted_data)# 分詞函數:將文本轉換為token ID,并添加labels用于計算損失def tokenize_function(examples):# 分詞時自動填充/截斷到固定長度(512,根據模型最大序列長度設置)tokenized = tokenizer(examples["text"],padding="max_length", # 不足512則填充truncation=True, # 超過512則截斷max_length=512)# GPT-2是自回歸模型,labels與input_ids一致(用輸入預測輸出)tokenized["labels"] = tokenized["input_ids"].copy()return tokenized# 批量分詞處理(加速處理效率)tokenized_dataset = hf_dataset.map(tokenize_function, batched=True)print(f"數據集分詞完成,共{len(tokenized_dataset)}個樣本(每個樣本已轉換為512長度的token)")# 配置LoRA(參數高效微調方法,只訓練部分參數,減少資源消耗)lora_config = LoraConfig(r=8, # LoRA注意力維度(控制參數量,越大能力越強但訓練越慢)lora_alpha=32, # 縮放因子(通常為r的4倍)target_modules=["c_attn", "c_proj"], # GPT-2中需要微調的注意力模塊lora_dropout=0.1, # Dropout概率(防止過擬合)bias="none", # 不微調偏置參數task_type="CAUSAL_LM" # 任務類型:因果語言模型)# 加載預訓練模型(不使用量化,適合CPU/GPU環境)try:model = AutoModelForCausalLM.from_pretrained(model_name,device_map="auto" # 自動分配設備(CPU/GPU))except Exception as e:raise Exception(f"加載模型失敗,請檢查路徑是否正確:{model_name}\n錯誤:{e}")# 準備模型訓練(關閉緩存,適配LoRA)model.config.use_cache = False # 關閉緩存,避免與梯度檢查點沖突# 根據PEFT版本選擇是否添加use_reentrant參數(兼容新舊版本)import peftif hasattr(peft, '__version__') and peft.__version__ >= "0.7.0":model = prepare_model_for_kbit_training(model, use_reentrant=False)else:model = prepare_model_for_kbit_training(model)# 應用LoRA配置(將LoRA適配器注入模型)model = get_peft_model(model, lora_config)# 打印可訓練參數比例(驗證LoRA是否生效)print(f"LoRA配置完成,可訓練參數占比: {model.print_trainable_parameters()}")# 配置訓練參數(根據硬件調整,CPU訓練需減小批次)training_args = TrainingArguments(output_dir="./results", # 訓練結果保存路徑learning_rate=3e-4, # 學習率(LoRA通常用較大學習率)per_device_train_batch_size=2, # 單設備批次大小(CPU設為2,GPU可增大)gradient_accumulation_steps=8, # 梯度累積步數(等效批次=2*8=16)num_train_epochs=3, # 訓練輪數(3輪足夠小數據集)weight_decay=0.01, # 權重衰減(防止過擬合)logging_dir="./logs", # 日志保存路徑logging_steps=1, # 每1步打印一次損失save_strategy="epoch", # 每輪結束保存模型fp16=False, # CPU模式關閉混合精度訓練dataloader_pin_memory=False, # CPU關閉內存鎖定disable_tqdm=False, # 啟用進度條report_to="none" # 不使用外部日志工具(如W&B))# 創建訓練器(封裝訓練邏輯)trainer = Trainer(model=model, # 待訓練的模型args=training_args, # 訓練參數train_dataset=tokenized_dataset # 訓練數據集)# 開始訓練print(f"開始訓練(共{training_args.num_train_epochs}輪,每輪{len(tokenized_dataset) // training_args.per_device_train_batch_size}步)")train_result = trainer.train()# 訓練結束提示(展示最終損失,判斷是否收斂)print("\n" + "=" * 50)print(f"訓練已全部完成!共訓練{training_args.num_train_epochs}輪")print(f"最終訓練損失:{train_result.training_loss:.4f}(損失越低說明擬合越好)")print("=" * 50 + "\n")# 保存微調后的模型(僅保存LoRA適配器,體積小)model_save_path = "instruction_tuned_model"model.save_pretrained(model_save_path)tokenizer.save_pretrained(model_save_path)# 驗證模型保存結果print(f"驗證微調模型保存路徑: {model_save_path}")if not os.path.exists(model_save_path):raise FileNotFoundError(f"模型保存失敗,路徑不存在: {model_save_path}")saved_files = os.listdir(model_save_path)print(f"保存的模型文件: {', '.join(saved_files)}(應包含adapter_config.json和adapter_model.bin)")print(f"模型微調完成,已保存至 {model_save_path}")return model_save_path, tokenizer# ----------------------
# 三、測試評估模塊:分別測試三類指令微調效果
# ----------------------def test_model(model_path, tokenizer):"""測試三類指令的微調效果:解釋類、寫作類、分析類"""print("開始模型測試...")# 驗證模型路徑if not os.path.exists(model_path):raise FileNotFoundError(f"測試模型路徑不存在: {model_path}")# 加載微調后的模型model = AutoModelForCausalLM.from_pretrained(model_path,device_map="auto" # 自動分配設備)# 生成回答的函數(封裝生成邏輯,方便復用)def generate_response(model, tokenizer, instruction, input_text="", max_length=200):# 構造與訓練時一致的prompt格式prompt = f"### Instruction: {instruction}\n### Input: {input_text}\n### Response:"# 轉換為模型輸入格式inputs = tokenizer(prompt, return_tensors="pt").to(model.device)# 生成回答(控制隨機性和長度)outputs = model.generate(inputs.input_ids,max_length=max_length, # 最大長度temperature=0.7, # 溫度(0.7表示中等隨機性)num_return_sequences=1 # 生成1個結果)# 解碼并提取回答部分response = tokenizer.decode(outputs[0], skip_special_tokens=True)return response.split("### Response:")[-1].strip() # 只保留回答內容# 三類指令測試案例(覆蓋微調場景)test_instructions = [# 1. 解釋類(新概念,測試知識遷移能力){"instruction": "解釋什么是邊緣計算","input": "","type": "解釋類","expected": "應說明邊緣計算的定義、特點(如靠近數據源、低延遲)和應用場景"},# 2. 寫作類(新場景,測試格式生成能力){"instruction": "寫一封產品退款申請郵件","input": "產品名稱:無線耳機,問題:無法充電,購買時間:2025年6月1日","type": "寫作類","expected": "應包含禮貌稱呼、退款原因、關鍵信息(產品/時間)和訴求,格式符合郵件規范"},# 3. 分析類(新文本,測試情感判斷能力){"instruction": "分析下面這段文字的情感傾向","input": "這款智能手表續航超預期,功能豐富,性價比很高!","type": "分析類","expected": "應判斷為正面情感,并說明依據(如'超預期'、'性價比高'等積極詞匯)"}]# 執行測試并打印結果for i, test_case in enumerate(test_instructions, 1):instruction = test_case["instruction"]input_text = test_case["input"]case_type = test_case["type"]expected = test_case["expected"]response = generate_response(model, tokenizer, instruction, input_text)print(f"\n=== 測試案例 {i}({case_type}): {instruction} ===")if input_text:print(f"輸入信息: {input_text}")print(f"模型回答: \n{response}")print(f"預期表現: {expected}")print("-" * 80)return model, tokenizerdef evaluate_model(model, tokenizer, test_data, num_samples=20):"""用BLEU分數自動評估模型生成質量(數值越高越好,0-1之間)"""print(f"開始自動評估(使用前{num_samples}個樣本,計算BLEU-1分數)...")bleu_scores = []# 生成回答的函數(與測試函數一致,保證評估邏輯統一)def generate_response(model, tokenizer, instruction, input_text="", max_length=200):prompt = f"### Instruction: {instruction}\n### Input: {input_text}\n### Response:"inputs = tokenizer(prompt, return_tensors="pt").to(model.device)outputs = model.generate(inputs.input_ids,max_length=max_length,temperature=0.7,num_return_sequences=1)return tokenizer.decode(outputs[0], skip_special_tokens=True).split("### Response:")[-1].strip()# 遍歷樣本計算BLEU分數(用進度條顯示)for example in tqdm(test_data[:num_samples], desc="評估進度"):instruction = example["instruction"]input_text = example["input"]reference = example["output"] # 參考回答(人工標注)response = generate_response(model, tokenizer, instruction, input_text) # 模型回答# 計算BLEU-1分數(單字匹配度,適合中文)reference_tokens = list(reference) # 參考回答分詞(按字)response_tokens = list(response) # 模型回答分詞(按字)bleu_score = sentence_bleu([reference_tokens], response_tokens, weights=(1, 0, 0, 0)) # 只關注單字匹配bleu_scores.append(bleu_score)# 計算平均分數avg_bleu = sum(bleu_scores) / len(bleu_scores) if bleu_scores else 0print(f"平均BLEU-1分數: {avg_bleu:.4f}(>0.3表示微調有效,>0.5表示效果較好)")return avg_bleu# ----------------------
# 主程序:串聯數據構建→微調→測試→評估全流程
# ----------------------if __name__ == "__main__":print("完全本地模式運行,無任何網絡依賴")# 1. 構建數據集(包含三類指令數據)dataset = build_full_dataset()# 2. 微調模型(用LoRA適配三類指令)model_path, tokenizer = finetune_model(dataset)# 3. 測試模型(分別驗證解釋類、寫作類、分析類指令)model, tokenizer = test_model(model_path, tokenizer)# 4. 評估模型(用BLEU分數量化微調效果)evaluate_model(model, tokenizer, dataset)print("\n=== 指令微調流程全部完成 ===")
6、實驗結果
?