smartTranscend commited on
Commit
e51c601
·
verified ·
1 Parent(s): 8490a59

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +2161 -0
  2. llama_requirements.txt +25 -0
  3. readme.txt +254 -0
app.py ADDED
@@ -0,0 +1,2161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import torch
4
+ from datasets import Dataset, DatasetDict
5
+ from transformers import (
6
+ AutoTokenizer,
7
+ AutoModelForSequenceClassification,
8
+ TrainingArguments,
9
+ Trainer,
10
+ DataCollatorWithPadding
11
+ )
12
+ from peft import (
13
+ LoraConfig,
14
+ AdaLoraConfig,
15
+ AdaptionPromptConfig,
16
+ PromptTuningConfig,
17
+ PrefixTuningConfig,
18
+ get_peft_model,
19
+ TaskType,
20
+ PeftModel
21
+ )
22
+ from sklearn.model_selection import train_test_split
23
+ from sklearn.metrics import accuracy_score, precision_recall_fscore_support
24
+ from sklearn.utils import resample
25
+ import numpy as np
26
+ import json
27
+ from datetime import datetime
28
+ import os
29
+ import gc
30
+ from huggingface_hub import login
31
+
32
+ # ==================== 全域變數 ====================
33
+ LAST_MODEL_PATH = None
34
+ LAST_TOKENIZER = None
35
+ MAX_LENGTH = 512
36
+
37
+ # ==================== HF Token 登入 ====================
38
+ print("🔐 檢查 Hugging Face Token...")
39
+ if "HF_TOKEN" in os.environ:
40
+ try:
41
+ login(token=os.environ["HF_TOKEN"])
42
+ print("✅ 已使用 HF Token 登入")
43
+ except Exception as e:
44
+ print(f"⚠️ Token 登入失敗: {e}")
45
+ else:
46
+ print("⚠️ 未找到 HF_TOKEN,可能無法下載 Llama 模型")
47
+
48
+ # 檢測設備
49
+ device = "cuda" if torch.cuda.is_available() else "cpu"
50
+ print(f"🖥️ 使用設備: {device}")
51
+
52
+ # ==================== 核心訓練函數(你的原始邏輯 - 完全不動) ====================
53
+ def run_llama_training(
54
+ file_path,
55
+ model_name,
56
+ target_samples,
57
+ use_class_weights,
58
+ num_epochs,
59
+ batch_size,
60
+ learning_rate,
61
+ tuning_method,
62
+ lora_r,
63
+ lora_alpha,
64
+ lora_dropout,
65
+ lora_target_modules,
66
+ adalora_init_r,
67
+ adalora_target_r,
68
+ adalora_alpha,
69
+ adalora_tinit,
70
+ adalora_tfinal,
71
+ adalora_delta_t,
72
+ adapter_reduction_factor,
73
+ prompt_tuning_num_tokens,
74
+ prefix_tuning_num_tokens,
75
+ best_metric,
76
+ # 【新增】二次微調參數
77
+ is_second_finetuning=False,
78
+ base_model_path=None
79
+ ):
80
+ """
81
+ 你的原始 Llama 訓練邏輯
82
+ """
83
+
84
+ global LAST_MODEL_PATH, LAST_TOKENIZER
85
+
86
+ # ==================== 清空記憶體(訓練前) ====================
87
+ torch.cuda.empty_cache()
88
+ gc.collect()
89
+ print("🧹 記憶體已清空")
90
+
91
+ # ==================== 1. 載入數據 ====================
92
+ training_type = "二次微調" if is_second_finetuning else "第一次微調"
93
+
94
+ print("\n" + "="*80)
95
+ print(f"🦙 Llama NBCD {training_type} - {tuning_method} 方法")
96
+ print("="*80)
97
+ print(f"開始時間: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
98
+ print(f"訓練類型: {training_type}")
99
+ print(f"微調方法: {tuning_method}")
100
+ if is_second_finetuning:
101
+ print(f"基礎模型: {base_model_path}")
102
+ print("="*80)
103
+
104
+ print("📂 載入訓練數據...")
105
+ df = pd.read_csv(file_path)
106
+ print(f"✅ 成功載入 {len(df)} 筆數據")
107
+
108
+ # 自動偵測文本和標籤欄位
109
+ text_col = None
110
+ label_col = None
111
+
112
+ # 支持的文本欄位名稱
113
+ if 'Text' in df.columns:
114
+ text_col = 'Text'
115
+ elif 'text' in df.columns:
116
+ text_col = 'text'
117
+
118
+ # 支持的標籤欄位名稱
119
+ if 'Label' in df.columns:
120
+ label_col = 'Label'
121
+ elif 'label' in df.columns:
122
+ label_col = 'label'
123
+
124
+ if text_col is None or label_col is None:
125
+ raise ValueError(
126
+ f"❌ 無法偵測到正確的欄位名稱!\n"
127
+ f"📋 您的 CSV 欄位: {list(df.columns)}\n\n"
128
+ f"✅ 請使用以下欄位名稱:\n"
129
+ f" 文本欄位: 'Text' 或 'text'\n"
130
+ f" 標籤欄位: 'Label' 或 'label'"
131
+ )
132
+
133
+ print(f" ✅ 偵測到文本欄位: '{text_col}'")
134
+ print(f" ✅ 偵測到標籤欄位: '{label_col}'")
135
+
136
+ # 統一重命名為標準欄位名
137
+ df = df.rename(columns={text_col: 'Text', label_col: 'nbcd'})
138
+
139
+ print(f" 原始 Class 0: {(df['nbcd']==0).sum()} 筆")
140
+ print(f" 原始 Class 1: {(df['nbcd']==1).sum()} 筆")
141
+
142
+ # ==================== 2. 資料平衡處理 ====================
143
+ print("\n⚖️ 執行資料平衡...")
144
+
145
+ df_class_0 = df[df['nbcd'] == 0]
146
+ df_class_1 = df[df['nbcd'] == 1]
147
+
148
+ target_n = int(target_samples)
149
+
150
+ # 欠採樣 Class 0
151
+ if len(df_class_0) > target_n:
152
+ df_class_0_balanced = resample(df_class_0, n_samples=target_n, random_state=42, replace=False)
153
+ print(f"✅ Class 0 欠採樣: {len(df_class_0)} → {len(df_class_0_balanced)} 筆")
154
+ else:
155
+ df_class_0_balanced = df_class_0
156
+ print(f"⚠️ Class 0 樣本數不足,保持 {len(df_class_0)} 筆")
157
+
158
+ # 過採樣 Class 1
159
+ if len(df_class_1) < target_n:
160
+ df_class_1_balanced = resample(df_class_1, n_samples=target_n, random_state=42, replace=True)
161
+ print(f"✅ Class 1 過採樣: {len(df_class_1)} → {len(df_class_1_balanced)} 筆")
162
+ else:
163
+ df_class_1_balanced = df_class_1
164
+ print(f"⚠️ Class 1 樣本數充足,保持 {len(df_class_1)} 筆")
165
+
166
+ df_balanced = pd.concat([df_class_0_balanced, df_class_1_balanced])
167
+ df_balanced = df_balanced.sample(frac=1, random_state=42).reset_index(drop=True)
168
+
169
+ print(f"\n📊 平衡後數據:")
170
+ print(f" 總樣本數: {len(df_balanced)} 筆")
171
+ print(f" Class 0: {(df_balanced['nbcd']==0).sum()} 筆")
172
+ print(f" Class 1: {(df_balanced['nbcd']==1).sum()} 筆")
173
+
174
+ # ==================== 3. 計算類別權重 ====================
175
+ if use_class_weights:
176
+ print("\n⚖️ 計算類別權重...")
177
+ class_counts = df_balanced['nbcd'].value_counts().sort_index()
178
+ total = len(df_balanced)
179
+ num_classes = 2
180
+
181
+ class_weight_0 = total / (num_classes * class_counts[0])
182
+ class_weight_1 = total / (num_classes * class_counts[1])
183
+ class_weights = torch.tensor([class_weight_0, class_weight_1], dtype=torch.float32)
184
+
185
+ print(f"✅ 類別權重計算完成:")
186
+ print(f" Class 0 權重: {class_weight_0:.4f}")
187
+ print(f" Class 1 權重: {class_weight_1:.4f}")
188
+
189
+ if device == "cuda":
190
+ class_weights = class_weights.to(device)
191
+ else:
192
+ class_weights = None
193
+ print("\n⚠️ 未使用類別權重")
194
+
195
+ # ==================== 4. 分割數據 ====================
196
+ print("\n✂️ 分割訓練集和測試集...")
197
+ train_df, test_df = train_test_split(
198
+ df_balanced,
199
+ test_size=0.2,
200
+ stratify=df_balanced['nbcd'],
201
+ random_state=42
202
+ )
203
+ print(f"✅ 訓練集: {len(train_df)} 筆 (Class 0: {(train_df['nbcd']==0).sum()}, Class 1: {(train_df['nbcd']==1).sum()})")
204
+ print(f"✅ 測試集: {len(test_df)} 筆 (Class 0: {(test_df['nbcd']==0).sum()}, Class 1: {(test_df['nbcd']==1).sum()})")
205
+
206
+ dataset = DatasetDict({
207
+ 'train': Dataset.from_pandas(train_df[['Text', 'nbcd']]),
208
+ 'test': Dataset.from_pandas(test_df[['Text', 'nbcd']])
209
+ })
210
+
211
+ # ==================== 5. 載入模型和 Tokenizer ====================
212
+ print("\n🤖 載入 Llama 模型和 Tokenizer...")
213
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
214
+ if tokenizer.pad_token is None:
215
+ tokenizer.pad_token = tokenizer.eos_token
216
+ tokenizer.pad_token_id = tokenizer.eos_token_id
217
+
218
+ # ==================== 6. 載入未微調的基礎模型 (Baseline) ====================
219
+ print("\n📦 載入未微調的基礎模型 (Baseline)...")
220
+ baseline_model = AutoModelForSequenceClassification.from_pretrained(
221
+ model_name,
222
+ num_labels=2,
223
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32,
224
+ device_map="auto" if device == "cuda" else None
225
+ )
226
+ baseline_model.config.pad_token_id = tokenizer.pad_token_id
227
+ print("✅ Baseline 模型載入完成")
228
+
229
+ # ==================== 7. 載入要微調的模型 ====================
230
+ print("\n🔧 載入用於微調的模型...")
231
+
232
+ # 【新增】二次微調邏輯
233
+ if is_second_finetuning and base_model_path:
234
+ print(f"📦 載入第一次微調模型: {base_model_path}")
235
+
236
+ # 讀取第一次模型資訊
237
+ with open('./saved_llama_models_list.json', 'r') as f:
238
+ models_list = json.load(f)
239
+
240
+ base_model_info = None
241
+ for model_info in models_list:
242
+ if model_info['model_path'] == base_model_path:
243
+ base_model_info = model_info
244
+ break
245
+
246
+ if base_model_info is None:
247
+ raise ValueError(f"找不到基礎模型資訊: {base_model_path}")
248
+
249
+ base_tuning_method = base_model_info['tuning_method']
250
+ print(f" 第一次微調方法: {base_tuning_method}")
251
+
252
+ # 根據第一次的方法載入模型
253
+ if base_tuning_method in ["LoRA", "AdaLoRA", "Adapter", "Prompt Tuning"]:
254
+ # 載入 PEFT 模型
255
+ base_bert = AutoModelForSequenceClassification.from_pretrained(
256
+ model_name,
257
+ num_labels=2,
258
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32
259
+ )
260
+ base_model = PeftModel.from_pretrained(base_bert, base_model_path)
261
+ print(f" ✅ 已載入 {base_tuning_method} 模型")
262
+ else:
263
+ # 載入一般模型 (BitFit)
264
+ base_model = AutoModelForSequenceClassification.from_pretrained(
265
+ base_model_path,
266
+ num_labels=2,
267
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32
268
+ )
269
+ print(f" ✅ 已載入 BitFit 模型")
270
+
271
+ if device == "cuda":
272
+ base_model = base_model.to(device)
273
+
274
+ print(f" ⚠️ 注意:二次微調將使用與第一次相同的方法 ({base_tuning_method})")
275
+
276
+ # 二次微調時強制使用相同方法
277
+ tuning_method = base_tuning_method
278
+
279
+ else:
280
+ # 【原始邏輯】第一次微調:從純 Llama 開始
281
+ base_model = AutoModelForSequenceClassification.from_pretrained(
282
+ model_name,
283
+ num_labels=2,
284
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32,
285
+ device_map="auto" if device == "cuda" else None
286
+ )
287
+
288
+ base_model.config.pad_token_id = tokenizer.pad_token_id
289
+ print("✅ 基礎模型載入完成")
290
+
291
+ # ==================== 8. 配置微調方法 ====================
292
+ print(f"\n🔧 配置 {tuning_method}...")
293
+
294
+ if tuning_method == "LoRA":
295
+ # LoRA 配置 - 使用完整參數
296
+ target_modules_map = {
297
+ "query,value": ["q_proj", "v_proj"],
298
+ "query,key,value": ["q_proj", "k_proj", "v_proj"],
299
+ "all": ["q_proj", "k_proj", "v_proj", "o_proj"]
300
+ }
301
+
302
+ peft_config = LoraConfig(
303
+ task_type=TaskType.SEQ_CLS,
304
+ r=int(lora_r),
305
+ lora_alpha=int(lora_alpha),
306
+ lora_dropout=float(lora_dropout),
307
+ target_modules=target_modules_map.get(lora_target_modules, ["q_proj", "v_proj"]),
308
+ bias="none"
309
+ )
310
+ print(f"✅ LoRA 配置完成")
311
+ print(f" LoRA rank (r): {lora_r}")
312
+ print(f" LoRA alpha: {lora_alpha}")
313
+ print(f" LoRA dropout: {lora_dropout}")
314
+ print(f" 目標模組: {lora_target_modules}")
315
+
316
+ elif tuning_method == "AdaLoRA":
317
+ # AdaLoRA 配置 - 使用獨立參數
318
+ try:
319
+ peft_config = AdaLoraConfig(
320
+ task_type=TaskType.SEQ_CLS,
321
+ inference_mode=False,
322
+ r=int(adalora_target_r),
323
+ lora_alpha=int(adalora_alpha),
324
+ lora_dropout=0.1,
325
+ target_modules=["q_proj", "v_proj"],
326
+ # AdaLoRA 特定參數
327
+ init_r=int(adalora_init_r),
328
+ target_r=int(adalora_target_r),
329
+ tinit=int(adalora_tinit),
330
+ tfinal=int(adalora_tfinal),
331
+ deltaT=int(adalora_delta_t),
332
+ )
333
+ print(f"✅ AdaLoRA 配置完成")
334
+ print(f" 初始 rank: {adalora_init_r}")
335
+ print(f" 目標 rank: {adalora_target_r}")
336
+ print(f" Alpha: {adalora_alpha}")
337
+ print(f" Tinit: {adalora_tinit}, Tfinal: {adalora_tfinal}")
338
+ print(f" Delta T: {adalora_delta_t}")
339
+ print(f" 自適應秩調整: 啟用")
340
+ except Exception as e:
341
+ print(f"⚠️ AdaLoRA 配置失敗,回退到 LoRA: {e}")
342
+ peft_config = LoraConfig(
343
+ task_type=TaskType.SEQ_CLS,
344
+ r=int(adalora_target_r),
345
+ lora_alpha=int(adalora_alpha),
346
+ lora_dropout=0.1,
347
+ target_modules=["q_proj", "v_proj"],
348
+ bias="none"
349
+ )
350
+
351
+ elif tuning_method == "Adapter":
352
+ # Adapter (Bottleneck Adapters)
353
+ peft_config = AdaptionPromptConfig(
354
+ task_type=TaskType.SEQ_CLS,
355
+ adapter_len=10,
356
+ adapter_layers=30,
357
+ reduction_factor=int(adapter_reduction_factor)
358
+ )
359
+ print(f"✅ Adapter 配置完成")
360
+ print(f" Reduction factor: {adapter_reduction_factor}")
361
+
362
+ elif tuning_method == "Prompt Tuning":
363
+ # Soft Prompt Tuning
364
+ peft_config = PromptTuningConfig(
365
+ task_type=TaskType.SEQ_CLS,
366
+ num_virtual_tokens=int(prompt_tuning_num_tokens),
367
+ prompt_tuning_init="TEXT",
368
+ prompt_tuning_init_text="Classify if the following text indicates NBCD:",
369
+ tokenizer_name_or_path=model_name
370
+ )
371
+ print(f"✅ Prompt Tuning 配置完成")
372
+ print(f" Virtual tokens: {prompt_tuning_num_tokens}")
373
+
374
+ elif tuning_method == "Prefix Tuning":
375
+ # Prefix Tuning - 可能有兼容性問題,但仍然嘗試
376
+ print(f"⚠️ Prefix Tuning 在某些環境可能有兼容性問題")
377
+ print(f" 如果遇到錯誤,建議使用 Prompt Tuning 替代")
378
+
379
+ try:
380
+ # 先禁用模型的緩存功能
381
+ base_model.config.use_cache = False
382
+
383
+ peft_config = PrefixTuningConfig(
384
+ task_type=TaskType.SEQ_CLS,
385
+ num_virtual_tokens=int(prefix_tuning_num_tokens),
386
+ prefix_projection=False,
387
+ inference_mode=False
388
+ )
389
+ print(f"✅ Prefix Tuning 配置完成")
390
+ print(f" Virtual tokens: {prefix_tuning_num_tokens}")
391
+ print(f" 已禁用緩存")
392
+ except Exception as e:
393
+ print(f"❌ Prefix Tuning 配置失敗: {e}")
394
+ raise ValueError(
395
+ f"Prefix Tuning 配置失敗,原因: {e}\n"
396
+ f"建議使用 Prompt Tuning 作為替代方案"
397
+ )
398
+
399
+ elif tuning_method == "BitFit":
400
+ # BitFit: 只訓練 bias 參數 - 完全修復版
401
+ model = base_model
402
+
403
+ # 凍結所有參數
404
+ for param in model.parameters():
405
+ param.requires_grad = False
406
+
407
+ # 只解凍 bias 和 分類頭
408
+ trainable_params_list = []
409
+ for name, param in model.named_parameters():
410
+ if 'bias' in name or 'score' in name or 'classifier' in name:
411
+ param.requires_grad = True
412
+ trainable_params_list.append(name)
413
+
414
+ print(f"✅ BitFit 配置完成")
415
+ print(f" 僅訓練 bias 和分類頭參數")
416
+ print(f" 可訓練參數: {', '.join(trainable_params_list[:5])}...")
417
+
418
+ # 應用 PEFT 配置(BitFit 除外)
419
+ if tuning_method != "BitFit":
420
+ model = get_peft_model(base_model, peft_config)
421
+
422
+ # Prefix Tuning 額外設置
423
+ if tuning_method == "Prefix Tuning":
424
+ model.config.use_cache = False
425
+
426
+ # 計算可訓練參數
427
+ trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
428
+ total_params = sum(p.numel() for p in model.parameters())
429
+ print(f" 可訓練參數: {trainable_params:,} / {total_params:,} ({trainable_params/total_params*100:.2f}%)")
430
+
431
+ # ==================== 9. 預處理數據 ====================
432
+ print("\n📄 預處理數據...")
433
+
434
+ def preprocess_function(examples):
435
+ return tokenizer(
436
+ examples['Text'],
437
+ truncation=True,
438
+ padding='max_length',
439
+ max_length=MAX_LENGTH
440
+ )
441
+
442
+ tokenized_dataset = dataset.map(preprocess_function, batched=True, remove_columns=['Text'])
443
+ tokenized_dataset = tokenized_dataset.rename_column("nbcd", "labels")
444
+ print("✅ 數據預處理完成")
445
+
446
+ # ==================== 10. 評估指標函數 ====================
447
+ def compute_metrics(eval_pred):
448
+ predictions, labels = eval_pred
449
+ predictions = np.argmax(predictions, axis=1)
450
+
451
+ accuracy = accuracy_score(labels, predictions)
452
+ precision, recall, f1, _ = precision_recall_fscore_support(
453
+ labels, predictions, average='binary', zero_division=0
454
+ )
455
+
456
+ # 計算混淆矩陣以得到 sensitivity 和 specificity
457
+ from sklearn.metrics import confusion_matrix
458
+ cm = confusion_matrix(labels, predictions)
459
+
460
+ if cm.shape == (2, 2):
461
+ tn, fp, fn, tp = cm.ravel()
462
+ sensitivity = tp / (tp + fn) if (tp + fn) > 0 else 0 # 敏感度 = Recall
463
+ specificity = tn / (tn + fp) if (tn + fp) > 0 else 0 # 特異性
464
+ else:
465
+ sensitivity = 0
466
+ specificity = 0
467
+
468
+ return {
469
+ 'accuracy': accuracy,
470
+ 'precision': precision,
471
+ 'recall': recall,
472
+ 'f1': f1,
473
+ 'sensitivity': sensitivity,
474
+ 'specificity': specificity
475
+ }
476
+
477
+ # ==================== 11. 評估 Baseline 模型 ====================
478
+ # 【僅第一次微調時執行】
479
+ if not is_second_finetuning:
480
+ print("\n" + "="*70)
481
+ print("📊 評估未微調的 Baseline 模型...")
482
+ print("="*70)
483
+
484
+ baseline_trainer = Trainer(
485
+ model=baseline_model,
486
+ args=TrainingArguments(
487
+ output_dir="./temp_baseline_llama",
488
+ per_device_eval_batch_size=int(batch_size),
489
+ bf16=(device == "cuda"),
490
+ report_to="none"
491
+ ),
492
+ tokenizer=tokenizer,
493
+ data_collator=DataCollatorWithPadding(tokenizer=tokenizer),
494
+ compute_metrics=compute_metrics
495
+ )
496
+
497
+ baseline_test_results = baseline_trainer.evaluate(eval_dataset=tokenized_dataset['test'])
498
+
499
+ print("\n📋 Baseline 模型 - 測試集結果:")
500
+ print(f" Accuracy: {baseline_test_results['eval_accuracy']:.4f}")
501
+ print(f" Precision: {baseline_test_results['eval_precision']:.4f}")
502
+ print(f" Recall: {baseline_test_results['eval_recall']:.4f}")
503
+ print(f" F1 Score: {baseline_test_results['eval_f1']:.4f}")
504
+ print(f" Sensitivity: {baseline_test_results['eval_sensitivity']:.4f}")
505
+ print(f" Specificity: {baseline_test_results['eval_specificity']:.4f}")
506
+
507
+ # 清空 baseline 模型記憶體
508
+ del baseline_model
509
+ del baseline_trainer
510
+ torch.cuda.empty_cache()
511
+ gc.collect()
512
+ else:
513
+ # 二次微調不評估 baseline
514
+ baseline_test_results = None
515
+ del baseline_model
516
+ torch.cuda.empty_cache()
517
+ gc.collect()
518
+
519
+ # ==================== 12. 自定義 Trainer ====================
520
+ if use_class_weights:
521
+ class WeightedTrainer(Trainer):
522
+ def __init__(self, *args, class_weights=None, **kwargs):
523
+ super().__init__(*args, **kwargs)
524
+ self.class_weights = class_weights
525
+
526
+ def compute_loss(self, model, inputs, return_outputs=False, **kwargs):
527
+ labels = inputs.pop("labels")
528
+ outputs = model(**inputs)
529
+ logits = outputs.logits
530
+
531
+ loss_fct = torch.nn.CrossEntropyLoss(weight=self.class_weights)
532
+ loss = loss_fct(logits.view(-1, self.model.config.num_labels), labels.view(-1))
533
+
534
+ return (loss, outputs) if return_outputs else loss
535
+
536
+ TrainerClass = WeightedTrainer
537
+ else:
538
+ TrainerClass = Trainer
539
+
540
+ # ==================== 13. 訓練配置 ====================
541
+ print("\n" + "="*70)
542
+ print("⚙️ 配置微調訓練器...")
543
+ print("="*70)
544
+
545
+ # 指標映射
546
+ metric_map = {
547
+ "f1": "f1",
548
+ "accuracy": "accuracy",
549
+ "precision": "precision",
550
+ "recall": "recall",
551
+ "sensitivity": "sensitivity",
552
+ "specificity": "specificity"
553
+ }
554
+
555
+ training_label = "second" if is_second_finetuning else "first"
556
+ output_dir = f'./llama_nbcd_{tuning_method.lower().replace(" ", "_")}_{training_label}_{datetime.now().strftime("%Y%m%d_%H%M%S")}'
557
+
558
+ training_args = TrainingArguments(
559
+ output_dir=output_dir,
560
+ num_train_epochs=int(num_epochs),
561
+ per_device_train_batch_size=int(batch_size),
562
+ per_device_eval_batch_size=int(batch_size),
563
+ learning_rate=float(learning_rate),
564
+ weight_decay=0.01,
565
+ eval_strategy="epoch",
566
+ save_strategy="epoch",
567
+ load_best_model_at_end=True,
568
+ metric_for_best_model=metric_map.get(best_metric, "recall"),
569
+ logging_dir=f"{output_dir}/logs",
570
+ logging_steps=10,
571
+ bf16=(device == "cuda"),
572
+ gradient_accumulation_steps=2,
573
+ warmup_steps=50,
574
+ report_to="none",
575
+ seed=42
576
+ )
577
+
578
+ if use_class_weights:
579
+ trainer = TrainerClass(
580
+ model=model,
581
+ args=training_args,
582
+ train_dataset=tokenized_dataset['train'],
583
+ eval_dataset=tokenized_dataset['test'],
584
+ tokenizer=tokenizer,
585
+ data_collator=DataCollatorWithPadding(tokenizer=tokenizer),
586
+ compute_metrics=compute_metrics,
587
+ class_weights=class_weights
588
+ )
589
+ else:
590
+ trainer = TrainerClass(
591
+ model=model,
592
+ args=training_args,
593
+ train_dataset=tokenized_dataset['train'],
594
+ eval_dataset=tokenized_dataset['test'],
595
+ tokenizer=tokenizer,
596
+ data_collator=DataCollatorWithPadding(tokenizer=tokenizer),
597
+ compute_metrics=compute_metrics
598
+ )
599
+
600
+ # ==================== 14. 開始訓練 ====================
601
+ print("\n" + "="*70)
602
+ print(f"🚀 開始{training_type}訓練...")
603
+ print("="*70 + "\n")
604
+
605
+ start_time = datetime.now()
606
+ train_result = trainer.train()
607
+ end_time = datetime.now()
608
+ duration = (end_time - start_time).total_seconds() / 60
609
+
610
+ print("\n" + "="*70)
611
+ print(f"✅ 訓練完成!")
612
+ print(f" 耗時: {duration:.1f} 分鐘")
613
+ print("="*70)
614
+
615
+ # ==================== 15. 評估微調後的模型 ====================
616
+ print("\n" + "="*70)
617
+ print(f"📊 評估{training_type}後的模型...")
618
+ print("="*70)
619
+
620
+ finetuned_test_results = trainer.evaluate(eval_dataset=tokenized_dataset['test'])
621
+
622
+ print(f"\n📋 {training_type}模型 - 測試集結果:")
623
+ print(f" Accuracy: {finetuned_test_results['eval_accuracy']:.4f}")
624
+ print(f" Precision: {finetuned_test_results['eval_precision']:.4f}")
625
+ print(f" Recall: {finetuned_test_results['eval_recall']:.4f}")
626
+ print(f" F1 Score: {finetuned_test_results['eval_f1']:.4f}")
627
+ print(f" Sensitivity: {finetuned_test_results['eval_sensitivity']:.4f}")
628
+ print(f" Specificity: {finetuned_test_results['eval_specificity']:.4f}")
629
+
630
+ # ==================== 16. 保存模型和結果 ====================
631
+ print("\n💾 保存模型和結果...")
632
+ trainer.save_model()
633
+ tokenizer.save_pretrained(output_dir)
634
+
635
+ # 儲存模型資訊到 JSON 檔案
636
+ model_info = {
637
+ 'model_path': output_dir,
638
+ 'model_name': model_name,
639
+ 'tuning_method': tuning_method,
640
+ 'training_type': training_type,
641
+ 'best_metric': best_metric,
642
+ 'best_metric_value': float(finetuned_test_results[f'eval_{metric_map.get(best_metric, "recall")}']),
643
+ 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
644
+ 'target_samples': target_samples,
645
+ 'epochs': num_epochs,
646
+ 'batch_size': batch_size,
647
+ 'learning_rate': learning_rate,
648
+ 'lora_r': lora_r if tuning_method in ["LoRA", "AdaLoRA"] else None,
649
+ 'lora_alpha': lora_alpha if tuning_method in ["LoRA", "AdaLoRA"] else None,
650
+ 'is_second_finetuning': is_second_finetuning,
651
+ 'base_model_path': base_model_path if is_second_finetuning else None
652
+ }
653
+
654
+ # 讀取現有的模型列表
655
+ models_list_file = './saved_llama_models_list.json'
656
+ if os.path.exists(models_list_file):
657
+ with open(models_list_file, 'r') as f:
658
+ models_list = json.load(f)
659
+ else:
660
+ models_list = []
661
+
662
+ # 加入新模型資訊
663
+ models_list.append(model_info)
664
+
665
+ # 儲存更新後的列表
666
+ with open(models_list_file, 'w') as f:
667
+ json.dump(models_list, f, indent=2)
668
+
669
+ # 更新全域變數
670
+ LAST_MODEL_PATH = output_dir
671
+ LAST_TOKENIZER = tokenizer
672
+
673
+ print(f"✅ 模型已儲存至: {output_dir}")
674
+
675
+ # ==================== 清空記憶體(訓練後) ====================
676
+ del model
677
+ del trainer
678
+ torch.cuda.empty_cache()
679
+ gc.collect()
680
+ print("🧹 訓練後記憶體已清空")
681
+
682
+ # 準備返回結果
683
+ results = {
684
+ 'baseline_results': baseline_test_results,
685
+ 'finetuned_results': finetuned_test_results,
686
+ 'model_path': output_dir,
687
+ 'duration': duration,
688
+ 'best_metric': best_metric,
689
+ 'model_name': model_name,
690
+ 'tuning_method': tuning_method,
691
+ 'training_type': training_type,
692
+ 'is_second_finetuning': is_second_finetuning
693
+ }
694
+
695
+ return results
696
+
697
+ # ==================== Gradio Wrapper 函數 ====================
698
+ def train_first_wrapper(
699
+ file,
700
+ model_name,
701
+ target_samples,
702
+ use_class_weights,
703
+ num_epochs,
704
+ batch_size,
705
+ learning_rate,
706
+ tuning_method,
707
+ lora_r,
708
+ lora_alpha,
709
+ lora_dropout,
710
+ lora_target_modules,
711
+ adalora_init_r,
712
+ adalora_target_r,
713
+ adalora_alpha,
714
+ adalora_tinit,
715
+ adalora_tfinal,
716
+ adalora_delta_t,
717
+ adapter_reduction_factor,
718
+ prompt_tuning_num_tokens,
719
+ prefix_tuning_num_tokens,
720
+ best_metric
721
+ ):
722
+ """第一次微調的包裝函數"""
723
+
724
+ if file is None:
725
+ return "請上傳 CSV 檔案", "", ""
726
+
727
+ try:
728
+ # 呼叫訓練函數
729
+ results = run_llama_training(
730
+ file_path=file.name,
731
+ model_name=model_name,
732
+ target_samples=target_samples,
733
+ use_class_weights=use_class_weights,
734
+ num_epochs=num_epochs,
735
+ batch_size=batch_size,
736
+ learning_rate=learning_rate,
737
+ tuning_method=tuning_method,
738
+ lora_r=lora_r,
739
+ lora_alpha=lora_alpha,
740
+ lora_dropout=lora_dropout,
741
+ lora_target_modules=lora_target_modules,
742
+ adalora_init_r=adalora_init_r,
743
+ adalora_target_r=adalora_target_r,
744
+ adalora_alpha=adalora_alpha,
745
+ adalora_tinit=adalora_tinit,
746
+ adalora_tfinal=adalora_tfinal,
747
+ adalora_delta_t=adalora_delta_t,
748
+ adapter_reduction_factor=adapter_reduction_factor,
749
+ prompt_tuning_num_tokens=prompt_tuning_num_tokens,
750
+ prefix_tuning_num_tokens=prefix_tuning_num_tokens,
751
+ best_metric=best_metric,
752
+ is_second_finetuning=False
753
+ )
754
+
755
+ baseline_results = results['baseline_results']
756
+ finetuned_results = results['finetuned_results']
757
+
758
+ # 第一格:資料資訊
759
+ data_info = f"""
760
+ # 📊 資料資訊 (第一次微調)
761
+
762
+ ## 🔧 訓練配置
763
+ - **模型**: {results['model_name']}
764
+ - **微調方法**: {results['tuning_method']}
765
+ - **最佳化指標**: {results['best_metric']}
766
+ - **訓練時長**: {results['duration']:.1f} 分鐘
767
+
768
+ ## ⚙️ 訓練參數
769
+ - **目標樣本數**: {target_samples} 筆/類別
770
+ - **使用類別權重**: {'是' if use_class_weights else '否'}
771
+ - **訓練輪數**: {num_epochs}
772
+ - **批次大小**: {batch_size}
773
+ - **學習率**: {learning_rate}
774
+
775
+ ✅ 第一次微調完成!可進行二次微調或預測!
776
+ """
777
+
778
+ # 第二格:未微調 Llama
779
+ baseline_output = f"""
780
+ # 🔵 未微調 Llama (Baseline)
781
+ ## 未經訓練
782
+
783
+ ### 📈 評估指標
784
+
785
+ | 指標 | 數值 |
786
+ |------|------|
787
+ | **Accuracy** | {baseline_results['eval_accuracy']:.4f} |
788
+ | **Precision** | {baseline_results['eval_precision']:.4f} |
789
+ | **Recall** | {baseline_results['eval_recall']:.4f} |
790
+ | **F1 Score** | {baseline_results['eval_f1']:.4f} |
791
+ | **Sensitivity** | {baseline_results['eval_sensitivity']:.4f} |
792
+ | **Specificity** | {baseline_results['eval_specificity']:.4f} |
793
+ """
794
+
795
+ # 第三格:微調後 Llama
796
+ finetuned_output = f"""
797
+ # 🟢 第一次微調 Llama
798
+ ## {results['tuning_method']}
799
+
800
+ ### 📈 評估指標
801
+
802
+ | 指標 | 數值 |
803
+ |------|------|
804
+ | **Accuracy** | {finetuned_results['eval_accuracy']:.4f} |
805
+ | **Precision** | {finetuned_results['eval_precision']:.4f} |
806
+ | **Recall** | {finetuned_results['eval_recall']:.4f} |
807
+ | **F1 Score** | {finetuned_results['eval_f1']:.4f} |
808
+ | **Sensitivity** | {finetuned_results['eval_sensitivity']:.4f} |
809
+ | **Specificity** | {finetuned_results['eval_specificity']:.4f} |
810
+ """
811
+
812
+ return data_info, baseline_output, finetuned_output
813
+
814
+ except Exception as e:
815
+ import traceback
816
+ error_msg = f"❌ 錯誤:{str(e)}\n\n詳細錯誤訊息:\n{traceback.format_exc()}"
817
+ return error_msg, "", ""
818
+
819
+ def train_second_wrapper(
820
+ base_model_choice,
821
+ file,
822
+ target_samples,
823
+ use_class_weights,
824
+ num_epochs,
825
+ batch_size,
826
+ learning_rate,
827
+ best_metric
828
+ ):
829
+ """二次微調的包裝函數"""
830
+
831
+ if base_model_choice == "請先進行第一次微調":
832
+ return "請先在「第一次微調」頁面訓練模型", ""
833
+
834
+ if file is None:
835
+ return "請上傳新的訓練數據 CSV 檔案", ""
836
+
837
+ try:
838
+ # 解析基礎模型路徑
839
+ base_model_path = base_model_choice
840
+
841
+ # 讀取第一次模型資訊
842
+ with open('./saved_llama_models_list.json', 'r') as f:
843
+ models_list = json.load(f)
844
+
845
+ base_model_info = None
846
+ for model_info in models_list:
847
+ if model_info['model_path'] == base_model_path:
848
+ base_model_info = model_info
849
+ break
850
+
851
+ if base_model_info is None:
852
+ return "找不到基礎模型資訊", ""
853
+
854
+ # 使用第一次的參數(二次微調不更改方法)
855
+ tuning_method = base_model_info['tuning_method']
856
+ model_name = base_model_info['model_name']
857
+
858
+ # 獲取第一次的 PEFT 參數
859
+ lora_r = base_model_info.get('lora_r', 16)
860
+ lora_alpha = base_model_info.get('lora_alpha', 32)
861
+ lora_dropout = 0.1
862
+ lora_target_modules = "query,value"
863
+ adalora_init_r = 12
864
+ adalora_target_r = 8
865
+ adalora_alpha = 32
866
+ adalora_tinit = 0
867
+ adalora_tfinal = 0
868
+ adalora_delta_t = 1
869
+ adapter_reduction_factor = 16
870
+ prompt_tuning_num_tokens = 20
871
+ prefix_tuning_num_tokens = 30
872
+
873
+ results = run_llama_training(
874
+ file_path=file.name,
875
+ model_name=model_name,
876
+ target_samples=target_samples,
877
+ use_class_weights=use_class_weights,
878
+ num_epochs=num_epochs,
879
+ batch_size=batch_size,
880
+ learning_rate=learning_rate,
881
+ tuning_method=tuning_method,
882
+ lora_r=lora_r,
883
+ lora_alpha=lora_alpha,
884
+ lora_dropout=lora_dropout,
885
+ lora_target_modules=lora_target_modules,
886
+ adalora_init_r=adalora_init_r,
887
+ adalora_target_r=adalora_target_r,
888
+ adalora_alpha=adalora_alpha,
889
+ adalora_tinit=adalora_tinit,
890
+ adalora_tfinal=adalora_tfinal,
891
+ adalora_delta_t=adalora_delta_t,
892
+ adapter_reduction_factor=adapter_reduction_factor,
893
+ prompt_tuning_num_tokens=prompt_tuning_num_tokens,
894
+ prefix_tuning_num_tokens=prefix_tuning_num_tokens,
895
+ best_metric=best_metric,
896
+ is_second_finetuning=True,
897
+ base_model_path=base_model_path
898
+ )
899
+
900
+ finetuned_results = results['finetuned_results']
901
+
902
+ data_info = f"""
903
+ # 📊 二次微調結果
904
+
905
+ ## 🔧 訓練配置
906
+ - **基礎模型**: {base_model_path}
907
+ - **微調方法**: {results['tuning_method']} (繼承自第一次)
908
+ - **最佳化指標**: {results['best_metric']}
909
+ - **最佳指標值**: {finetuned_results[f"eval_{results['best_metric']"]:.4f}
910
+ - **訓練時長**: {results['duration']:.1f} 分鐘
911
+
912
+ ## ⚙️ 訓練參數
913
+ - **目標樣本數**: {target_samples} 筆/類別
914
+ - **使用類別權重**: {'是' if use_class_weights else '否'}
915
+ - **訓練輪數**: {num_epochs}
916
+ - **批次大小**: {batch_size}
917
+ - **學習率**: {learning_rate}
918
+
919
+ ✅ 二次微調完成!可進行預測!
920
+ """
921
+
922
+ finetuned_output = f"""
923
+ # 🟢 二次微調 Llama
924
+ ## {results['tuning_method']}
925
+
926
+ ### 📈 評估指標
927
+
928
+ | 指標 | 數值 |
929
+ |------|------|
930
+ | **Accuracy** | {finetuned_results['eval_accuracy']:.4f} |
931
+ | **Precision** | {finetuned_results['eval_precision']:.4f} |
932
+ | **Recall** | {finetuned_results['eval_recall']:.4f} |
933
+ | **F1 Score** | {finetuned_results['eval_f1']:.4f} |
934
+ | **Sensitivity** | {finetuned_results['eval_sensitivity']:.4f} |
935
+ | **Specificity** | {finetuned_results['eval_specificity']:.4f} |
936
+ """
937
+
938
+ return data_info, finetuned_output
939
+
940
+ except Exception as e:
941
+ import traceback
942
+ error_msg = f"❌ 錯誤:{str(e)}\n\n詳細錯誤訊息:\n{traceback.format_exc()}"
943
+ return error_msg, ""
944
+
945
+ # ==================== 新增:新數據測試函數 ====================
946
+
947
+ def test_on_new_data(test_file_path, baseline_choice, first_choice, second_choice):
948
+ """
949
+ 在新測試數據上比較三個模型的表現:
950
+ 1. 純 Llama (baseline)
951
+ 2. 第一次微調模型
952
+ 3. 第二次微調模型
953
+ """
954
+
955
+ print("\n" + "=" * 80)
956
+ print("📊 新數據測試 - 三模型比較")
957
+ print("=" * 80)
958
+
959
+ # 載入測試數據
960
+ df_test = pd.read_csv(test_file_path)
961
+
962
+ # 自動偵測欄位
963
+ text_col = 'Text' if 'Text' in df_test.columns else 'text'
964
+ label_col = 'Label' if 'Label' in df_test.columns else 'label'
965
+
966
+ df_clean = pd.DataFrame({
967
+ 'text': df_test[text_col],
968
+ 'label': df_test[label_col]
969
+ })
970
+ df_clean = df_clean.dropna()
971
+
972
+ print(f"\n測試數據:")
973
+ print(f" 總筆數: {len(df_clean)}")
974
+ print(f" Class 0: {sum(df_clean['label']==0)} 筆")
975
+ print(f" Class 1: {sum(df_clean['label']==1)} 筆")
976
+
977
+ # 準備測試數據
978
+ test_dataset = Dataset.from_pandas(df_clean[['text', 'label']])
979
+
980
+ # 評估函數
981
+ def evaluate_model(model, tokenizer, model_name_str, dataset_name):
982
+ model.eval()
983
+
984
+ def preprocess_function(examples):
985
+ return tokenizer(examples['text'], truncation=True, padding='max_length', max_length=MAX_LENGTH)
986
+
987
+ test_tokenized = test_dataset.map(preprocess_function, batched=True)
988
+
989
+ trainer_args = TrainingArguments(
990
+ output_dir='./temp_test',
991
+ per_device_eval_batch_size=32,
992
+ report_to="none"
993
+ )
994
+
995
+ def compute_metrics_test(eval_pred):
996
+ predictions, labels = eval_pred
997
+ predictions = np.argmax(predictions, axis=1)
998
+
999
+ accuracy = accuracy_score(labels, predictions)
1000
+ precision, recall, f1, _ = precision_recall_fscore_support(
1001
+ labels, predictions, average='binary', zero_division=0
1002
+ )
1003
+
1004
+ from sklearn.metrics import confusion_matrix
1005
+ cm = confusion_matrix(labels, predictions)
1006
+
1007
+ if cm.shape == (2, 2):
1008
+ tn, fp, fn, tp = cm.ravel()
1009
+ sensitivity = tp / (tp + fn) if (tp + fn) > 0 else 0
1010
+ specificity = tn / (tn + fp) if (tn + fp) > 0 else 0
1011
+ else:
1012
+ sensitivity = 0
1013
+ specificity = 0
1014
+ tn = fp = fn = tp = 0
1015
+
1016
+ return {
1017
+ 'accuracy': accuracy,
1018
+ 'precision': precision,
1019
+ 'recall': recall,
1020
+ 'f1': f1,
1021
+ 'sensitivity': sensitivity,
1022
+ 'specificity': specificity,
1023
+ 'tp': int(tp),
1024
+ 'tn': int(tn),
1025
+ 'fp': int(fp),
1026
+ 'fn': int(fn)
1027
+ }
1028
+
1029
+ trainer = Trainer(
1030
+ model=model,
1031
+ args=trainer_args,
1032
+ compute_metrics=compute_metrics_test
1033
+ )
1034
+
1035
+ predictions_output = trainer.predict(test_tokenized)
1036
+
1037
+ results = {
1038
+ 'accuracy': predictions_output.metrics['test_accuracy'],
1039
+ 'precision': predictions_output.metrics['test_precision'],
1040
+ 'recall': predictions_output.metrics['test_recall'],
1041
+ 'f1': predictions_output.metrics['test_f1'],
1042
+ 'sensitivity': predictions_output.metrics['test_sensitivity'],
1043
+ 'specificity': predictions_output.metrics['test_specificity'],
1044
+ 'tp': predictions_output.metrics['test_tp'],
1045
+ 'tn': predictions_output.metrics['test_tn'],
1046
+ 'fp': predictions_output.metrics['test_fp'],
1047
+ 'fn': predictions_output.metrics['test_fn']
1048
+ }
1049
+
1050
+ print(f"\n✅ {dataset_name} 評估完成")
1051
+
1052
+ del trainer
1053
+ torch.cuda.empty_cache()
1054
+ gc.collect()
1055
+
1056
+ return results
1057
+
1058
+ all_results = {}
1059
+
1060
+ # 1. 評估純 Llama
1061
+ if baseline_choice == "評估純 Llama":
1062
+ print("\n" + "-" * 80)
1063
+ print("1️⃣ 評估純 Llama (Baseline)")
1064
+ print("-" * 80)
1065
+
1066
+ # 獲取模型名稱
1067
+ if first_choice != "請選擇":
1068
+ with open('./saved_llama_models_list.json', 'r') as f:
1069
+ models_list = json.load(f)
1070
+ for model_info in models_list:
1071
+ if model_info['model_path'] == first_choice:
1072
+ model_name = model_info['model_name']
1073
+ break
1074
+ else:
1075
+ model_name = "meta-llama/Llama-3.2-1B"
1076
+
1077
+ baseline_tokenizer = AutoTokenizer.from_pretrained(model_name)
1078
+ if baseline_tokenizer.pad_token is None:
1079
+ baseline_tokenizer.pad_token = baseline_tokenizer.eos_token
1080
+ baseline_tokenizer.pad_token_id = baseline_tokenizer.eos_token_id
1081
+
1082
+ baseline_model = AutoModelForSequenceClassification.from_pretrained(
1083
+ model_name,
1084
+ num_labels=2,
1085
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32,
1086
+ device_map="auto" if device == "cuda" else None
1087
+ )
1088
+ baseline_model.config.pad_token_id = baseline_tokenizer.pad_token_id
1089
+
1090
+ all_results['baseline'] = evaluate_model(baseline_model, baseline_tokenizer, model_name, "純 Llama")
1091
+ del baseline_model, baseline_tokenizer
1092
+ torch.cuda.empty_cache()
1093
+ else:
1094
+ all_results['baseline'] = None
1095
+
1096
+ # 2. 評估第一次微調模型
1097
+ if first_choice != "請選擇":
1098
+ print("\n" + "-" * 80)
1099
+ print("2️⃣ 評估第一次微調模型")
1100
+ print("-" * 80)
1101
+
1102
+ # 讀取模型資訊
1103
+ with open('./saved_llama_models_list.json', 'r') as f:
1104
+ models_list = json.load(f)
1105
+
1106
+ first_model_info = None
1107
+ for model_info in models_list:
1108
+ if model_info['model_path'] == first_choice:
1109
+ first_model_info = model_info
1110
+ break
1111
+
1112
+ if first_model_info:
1113
+ tuning_method = first_model_info['tuning_method']
1114
+ model_name = first_model_info['model_name']
1115
+
1116
+ first_tokenizer = AutoTokenizer.from_pretrained(first_choice)
1117
+ if first_tokenizer.pad_token is None:
1118
+ first_tokenizer.pad_token = first_tokenizer.eos_token
1119
+ first_tokenizer.pad_token_id = first_tokenizer.eos_token_id
1120
+
1121
+ if tuning_method in ["LoRA", "AdaLoRA", "Adapter", "Prompt Tuning"]:
1122
+ base_model = AutoModelForSequenceClassification.from_pretrained(
1123
+ model_name,
1124
+ num_labels=2,
1125
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32
1126
+ )
1127
+ first_model = PeftModel.from_pretrained(base_model, first_choice)
1128
+ if device == "cuda":
1129
+ first_model = first_model.to(device)
1130
+ else:
1131
+ first_model = AutoModelForSequenceClassification.from_pretrained(
1132
+ first_choice,
1133
+ num_labels=2,
1134
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32,
1135
+ device_map="auto" if device == "cuda" else None
1136
+ )
1137
+
1138
+ all_results['first'] = evaluate_model(first_model, first_tokenizer, model_name, "第一次微調模型")
1139
+ del first_model, first_tokenizer
1140
+ torch.cuda.empty_cache()
1141
+ else:
1142
+ all_results['first'] = None
1143
+ else:
1144
+ all_results['first'] = None
1145
+
1146
+ # 3. 評估第二次微調模型
1147
+ if second_choice != "請選擇":
1148
+ print("\n" + "-" * 80)
1149
+ print("3️⃣ 評估第二次微調模型")
1150
+ print("-" * 80)
1151
+
1152
+ # 讀取模型資訊
1153
+ with open('./saved_llama_models_list.json', 'r') as f:
1154
+ models_list = json.load(f)
1155
+
1156
+ second_model_info = None
1157
+ for model_info in models_list:
1158
+ if model_info['model_path'] == second_choice:
1159
+ second_model_info = model_info
1160
+ break
1161
+
1162
+ if second_model_info:
1163
+ tuning_method = second_model_info['tuning_method']
1164
+ model_name = second_model_info['model_name']
1165
+
1166
+ second_tokenizer = AutoTokenizer.from_pretrained(second_choice)
1167
+ if second_tokenizer.pad_token is None:
1168
+ second_tokenizer.pad_token = second_tokenizer.eos_token
1169
+ second_tokenizer.pad_token_id = second_tokenizer.eos_token_id
1170
+
1171
+ if tuning_method in ["LoRA", "AdaLoRA", "Adapter", "Prompt Tuning"]:
1172
+ base_model = AutoModelForSequenceClassification.from_pretrained(
1173
+ model_name,
1174
+ num_labels=2,
1175
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32
1176
+ )
1177
+ second_model = PeftModel.from_pretrained(base_model, second_choice)
1178
+ if device == "cuda":
1179
+ second_model = second_model.to(device)
1180
+ else:
1181
+ second_model = AutoModelForSequenceClassification.from_pretrained(
1182
+ second_choice,
1183
+ num_labels=2,
1184
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32,
1185
+ device_map="auto" if device == "cuda" else None
1186
+ )
1187
+
1188
+ all_results['second'] = evaluate_model(second_model, second_tokenizer, model_name, "第二次微調模型")
1189
+ del second_model, second_tokenizer
1190
+ torch.cuda.empty_cache()
1191
+ else:
1192
+ all_results['second'] = None
1193
+ else:
1194
+ all_results['second'] = None
1195
+
1196
+ print("\n" + "=" * 80)
1197
+ print("✅ 新數據測試完成")
1198
+ print("=" * 80)
1199
+
1200
+ return all_results
1201
+
1202
+ def test_new_data_wrapper(test_file, baseline_choice, first_choice, second_choice):
1203
+ """新數據測試的包裝函數"""
1204
+
1205
+ if test_file is None:
1206
+ return "請上傳測試數據 CSV 檔案", "", ""
1207
+
1208
+ try:
1209
+ all_results = test_on_new_data(
1210
+ test_file.name,
1211
+ baseline_choice,
1212
+ first_choice,
1213
+ second_choice
1214
+ )
1215
+
1216
+ # 格式化輸出
1217
+ outputs = []
1218
+
1219
+ # 1. 純 Llama
1220
+ if all_results['baseline']:
1221
+ r = all_results['baseline']
1222
+ baseline_output = f"""
1223
+ # 🔵 純 Llama (Baseline)
1224
+
1225
+ | 指標 | 數值 |
1226
+ |------|------|
1227
+ | **F1 Score** | {r['f1']:.4f} |
1228
+ | **Accuracy** | {r['accuracy']:.4f} |
1229
+ | **Precision** | {r['precision']:.4f} |
1230
+ | **Recall** | {r['recall']:.4f} |
1231
+ | **Sensitivity** | {r['sensitivity']:.4f} |
1232
+ | **Specificity** | {r['specificity']:.4f} |
1233
+
1234
+ ### 混淆矩陣
1235
+ | | 預測:Class 0 | 預測:Class 1 |
1236
+ |---|-----------|-----------|
1237
+ | **實際:Class 0** | TN={r['tn']} | FP={r['fp']} |
1238
+ | **實際:Class 1** | FN={r['fn']} | TP={r['tp']} |
1239
+ """
1240
+ else:
1241
+ baseline_output = "未選擇評估純 Llama"
1242
+ outputs.append(baseline_output)
1243
+
1244
+ # 2. 第一次微調
1245
+ if all_results['first']:
1246
+ r = all_results['first']
1247
+ first_output = f"""
1248
+ # 🟢 第一次微調模型
1249
+
1250
+ | 指標 | 數值 |
1251
+ |------|------|
1252
+ | **F1 Score** | {r['f1']:.4f} |
1253
+ | **Accuracy** | {r['accuracy']:.4f} |
1254
+ | **Precision** | {r['precision']:.4f} |
1255
+ | **Recall** | {r['recall']:.4f} |
1256
+ | **Sensitivity** | {r['sensitivity']:.4f} |
1257
+ | **Specificity** | {r['specificity']:.4f} |
1258
+
1259
+ ### 混淆矩陣
1260
+ | | 預測:Class 0 | 預測:Class 1 |
1261
+ |---|-----------|-----------|
1262
+ | **實際:Class 0** | TN={r['tn']} | FP={r['fp']} |
1263
+ | **實際:Class 1** | FN={r['fn']} | TP={r['tp']} |
1264
+ """
1265
+ else:
1266
+ first_output = "未選擇第一次微調模型"
1267
+ outputs.append(first_output)
1268
+
1269
+ # 3. 第二次微調
1270
+ if all_results['second']:
1271
+ r = all_results['second']
1272
+ second_output = f"""
1273
+ # 🟡 第二次微調模型
1274
+
1275
+ | 指標 | 數值 |
1276
+ |------|------|
1277
+ | **F1 Score** | {r['f1']:.4f} |
1278
+ | **Accuracy** | {r['accuracy']:.4f} |
1279
+ | **Precision** | {r['precision']:.4f} |
1280
+ | **Recall** | {r['recall']:.4f} |
1281
+ | **Sensitivity** | {r['sensitivity']:.4f} |
1282
+ | **Specificity** | {r['specificity']:.4f} |
1283
+
1284
+ ### 混淆矩陣
1285
+ | | 預測:Class 0 | 預測:Class 1 |
1286
+ |---|-----------|-----------|
1287
+ | **實際:Class 0** | TN={r['tn']} | FP={r['fp']} |
1288
+ | **實際:Class 1** | FN={r['fn']} | TP={r['tp']} |
1289
+ """
1290
+ else:
1291
+ second_output = "未選擇第二次微調模型"
1292
+ outputs.append(second_output)
1293
+
1294
+ return outputs[0], outputs[1], outputs[2]
1295
+
1296
+ except Exception as e:
1297
+ import traceback
1298
+ error_msg = f"❌ 錯誤:{str(e)}\n\n詳細錯誤訊息:\n{traceback.format_exc()}"
1299
+ return error_msg, "", ""
1300
+
1301
+ # ==================== 預測函數 ====================
1302
+ def predict_text(model_choice, text_input):
1303
+ """
1304
+ 預測功能 - 支持選擇已訓練的模型,並同時顯示未微調和微調的預測結果
1305
+ """
1306
+
1307
+ if not text_input or text_input.strip() == "":
1308
+ return "請輸入文本", "請輸入文本"
1309
+
1310
+ try:
1311
+ # ==================== 未微調的 Llama 預測 ====================
1312
+ print("\n使用未微調 Llama 預測...")
1313
+
1314
+ # 載入 tokenizer
1315
+ if model_choice != "請先訓練模型":
1316
+ # 從選擇中解析模型名稱
1317
+ model_path = model_choice.split(" | ")[0].replace("路徑: ", "")
1318
+
1319
+ # 從 JSON 讀取模型資訊
1320
+ with open('./saved_llama_models_list.json', 'r') as f:
1321
+ models_list = json.load(f)
1322
+
1323
+ selected_model_info = None
1324
+ for model_info in models_list:
1325
+ if model_info['model_path'] == model_path:
1326
+ selected_model_info = model_info
1327
+ break
1328
+
1329
+ if selected_model_info is None:
1330
+ return "找不到模型資訊", "找不到模型資訊"
1331
+
1332
+ model_name = selected_model_info['model_name']
1333
+ baseline_tokenizer = AutoTokenizer.from_pretrained(model_name)
1334
+ else:
1335
+ baseline_tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.2-1B")
1336
+ model_name = "meta-llama/Llama-3.2-1B"
1337
+
1338
+ if baseline_tokenizer.pad_token is None:
1339
+ baseline_tokenizer.pad_token = baseline_tokenizer.eos_token
1340
+ baseline_tokenizer.pad_token_id = baseline_tokenizer.eos_token_id
1341
+
1342
+ baseline_model = AutoModelForSequenceClassification.from_pretrained(
1343
+ model_name,
1344
+ num_labels=2,
1345
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32,
1346
+ device_map="auto" if device == "cuda" else None
1347
+ )
1348
+ baseline_model.config.pad_token_id = baseline_tokenizer.pad_token_id
1349
+ baseline_model.eval()
1350
+
1351
+ # Tokenize 輸入(未微調)
1352
+ baseline_inputs = baseline_tokenizer(
1353
+ text_input,
1354
+ return_tensors="pt",
1355
+ truncation=True,
1356
+ max_length=MAX_LENGTH
1357
+ )
1358
+ if device == "cuda":
1359
+ baseline_inputs = {k: v.to(baseline_model.device) for k, v in baseline_inputs.items()}
1360
+
1361
+ # 預測(未微調)
1362
+ with torch.no_grad():
1363
+ baseline_outputs = baseline_model(**baseline_inputs)
1364
+ baseline_probs = torch.nn.functional.softmax(baseline_outputs.logits, dim=-1)
1365
+ baseline_pred_class = torch.argmax(baseline_probs, dim=-1).item()
1366
+ baseline_confidence = baseline_probs[0][baseline_pred_class].item()
1367
+
1368
+ baseline_result = "NBCD = 0" if baseline_pred_class == 0 else "NBCD = 1"
1369
+ baseline_prob_class0 = baseline_probs[0][0].item()
1370
+ baseline_prob_class1 = baseline_probs[0][1].item()
1371
+
1372
+ baseline_output = f"""
1373
+ # 🔵 未微調 Llama 預測結果
1374
+
1375
+ ## 預測類別: **{baseline_result}**
1376
+
1377
+ ## 信心度: **{baseline_confidence:.1%}**
1378
+
1379
+ ## 機率分布:
1380
+ - **Class 0 機率**: {baseline_prob_class0:.2%}
1381
+ - **Class 1 機率**: {baseline_prob_class1:.2%}
1382
+
1383
+ ---
1384
+ **說明**: 此為原始 Llama 模型,未經任何領域資料訓練
1385
+ """
1386
+
1387
+ # 清空記憶體
1388
+ del baseline_model
1389
+ del baseline_tokenizer
1390
+ torch.cuda.empty_cache()
1391
+
1392
+ # ==================== 微調後的 Llama 預測 ====================
1393
+
1394
+ if model_choice == "請先訓練模型":
1395
+ finetuned_output = """
1396
+ # 🟢 微調 Llama 預測結果
1397
+
1398
+ ❌ 尚未訓練任何模型,請先在「模型訓練」頁面訓練模型
1399
+ """
1400
+ return baseline_output, finetuned_output
1401
+
1402
+ print(f"\n使用微調模型: {model_path}")
1403
+
1404
+ # 載入 tokenizer
1405
+ finetuned_tokenizer = AutoTokenizer.from_pretrained(model_path)
1406
+ if finetuned_tokenizer.pad_token is None:
1407
+ finetuned_tokenizer.pad_token = finetuned_tokenizer.eos_token
1408
+ finetuned_tokenizer.pad_token_id = finetuned_tokenizer.eos_token_id
1409
+
1410
+ # 載入 PEFT 模型(根據微調方法)
1411
+ base_model = AutoModelForSequenceClassification.from_pretrained(
1412
+ model_name,
1413
+ num_labels=2,
1414
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32,
1415
+ device_map="auto" if device == "cuda" else None
1416
+ )
1417
+
1418
+ # 根據微調方法載入模型
1419
+ tuning_method = selected_model_info.get('tuning_method', 'LoRA')
1420
+
1421
+ if tuning_method == "BitFit":
1422
+ # BitFit 直接載入完整模型
1423
+ finetuned_model = AutoModelForSequenceClassification.from_pretrained(
1424
+ model_path,
1425
+ num_labels=2,
1426
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32,
1427
+ device_map="auto" if device == "cuda" else None
1428
+ )
1429
+ else:
1430
+ # 其他方法使用 PEFT
1431
+ finetuned_model = PeftModel.from_pretrained(base_model, model_path)
1432
+
1433
+ # Prefix Tuning 需要禁用緩存
1434
+ if tuning_method == "Prefix Tuning":
1435
+ finetuned_model.config.use_cache = False
1436
+
1437
+ finetuned_model.config.pad_token_id = finetuned_tokenizer.pad_token_id
1438
+ finetuned_model.eval()
1439
+
1440
+ # Tokenize 輸入(微調)
1441
+ finetuned_inputs = finetuned_tokenizer(
1442
+ text_input,
1443
+ return_tensors="pt",
1444
+ truncation=True,
1445
+ max_length=MAX_LENGTH
1446
+ )
1447
+ if device == "cuda":
1448
+ finetuned_inputs = {k: v.to(finetuned_model.device) for k, v in finetuned_inputs.items()}
1449
+
1450
+ # 預測(微調)
1451
+ with torch.no_grad():
1452
+ finetuned_outputs = finetuned_model(**finetuned_inputs)
1453
+ finetuned_probs = torch.nn.functional.softmax(finetuned_outputs.logits, dim=-1)
1454
+ finetuned_pred_class = torch.argmax(finetuned_probs, dim=-1).item()
1455
+ finetuned_confidence = finetuned_probs[0][finetuned_pred_class].item()
1456
+
1457
+ finetuned_result = "NBCD = 0" if finetuned_pred_class == 0 else "NBCD = 1"
1458
+ finetuned_prob_class0 = finetuned_probs[0][0].item()
1459
+ finetuned_prob_class1 = finetuned_probs[0][1].item()
1460
+
1461
+ training_type_label = "二次微調" if selected_model_info.get('is_second_finetuning', False) else "第一次微調"
1462
+
1463
+ finetuned_output = f"""
1464
+ # 🟢 微調 Llama 預測結果
1465
+
1466
+ ## 預測類別: **{finetuned_result}**
1467
+
1468
+ ## 信心度: **{finetuned_confidence:.1%}**
1469
+
1470
+ ## 機率分布:
1471
+ - **Class 0 機率**: {finetuned_prob_class0:.2%}
1472
+ - **Class 1 機率**: {finetuned_prob_class1:.2%}
1473
+
1474
+ ---
1475
+ ### 模型資訊:
1476
+ - **訓練類型**: {training_type_label}
1477
+ - **模型名稱**: {selected_model_info['model_name']}
1478
+ - **微調方法**: {selected_model_info['tuning_method']}
1479
+ - **最佳化指標**: {selected_model_info['best_metric']}
1480
+ - **訓練時間**: {selected_model_info['timestamp']}
1481
+ - **模型路徑**: {model_path}
1482
+
1483
+ ---
1484
+ **注意**: 此預測僅供參考。
1485
+ """
1486
+
1487
+ # 清空記憶體
1488
+ del finetuned_model
1489
+ del finetuned_tokenizer
1490
+ torch.cuda.empty_cache()
1491
+
1492
+ return baseline_output, finetuned_output
1493
+
1494
+ except Exception as e:
1495
+ import traceback
1496
+ error_msg = f"❌ 預測錯誤:{str(e)}\n\n詳細錯誤訊息:\n{traceback.format_exc()}"
1497
+ return error_msg, error_msg
1498
+
1499
+ def get_available_models():
1500
+ """
1501
+ 取得所有已訓練的模型���表
1502
+ """
1503
+ models_list_file = './saved_llama_models_list.json'
1504
+ if not os.path.exists(models_list_file):
1505
+ return ["請先訓練模型"]
1506
+
1507
+ with open(models_list_file, 'r') as f:
1508
+ models_list = json.load(f)
1509
+
1510
+ if len(models_list) == 0:
1511
+ return ["請先訓練模型"]
1512
+
1513
+ # 格式化模型選項
1514
+ model_choices = []
1515
+ for i, model_info in enumerate(models_list, 1):
1516
+ training_type = model_info.get('training_type', '第一次微調')
1517
+ choice = f"路徑: {model_info['model_path']} | 類型: {training_type} | 方法: {model_info['tuning_method']} | 時間: {model_info['timestamp']}"
1518
+ model_choices.append(choice)
1519
+
1520
+ return model_choices
1521
+
1522
+ def get_first_finetuning_models():
1523
+ """
1524
+ 取得所有第一次微調的模型(用於二次微調選擇)
1525
+ """
1526
+ models_list_file = './saved_llama_models_list.json'
1527
+ if not os.path.exists(models_list_file):
1528
+ return ["請先進行第一次微調"]
1529
+
1530
+ with open(models_list_file, 'r') as f:
1531
+ models_list = json.load(f)
1532
+
1533
+ # 只返回第一次微調的模型
1534
+ first_models = [m for m in models_list if not m.get('is_second_finetuning', False)]
1535
+
1536
+ if len(first_models) == 0:
1537
+ return ["請先進行第一次微調"]
1538
+
1539
+ model_choices = []
1540
+ for model_info in first_models:
1541
+ choice = f"{model_info['model_path']}"
1542
+ model_choices.append(choice)
1543
+
1544
+ return model_choices
1545
+
1546
+ # ==================== Gradio 介面 (參考第四個文件的視覺化) ====================
1547
+ with gr.Blocks(title="🦙 Llama NBCD 二次微調平台", theme=gr.themes.Soft()) as demo:
1548
+
1549
+ gr.Markdown("""
1550
+ # 🦙 Llama NBCD 二次微調完整平台
1551
+
1552
+ ### 🌟 功能特色:
1553
+ - 🎯 第一次微調:從純 Llama 開始訓練
1554
+ - 🔄 第二次微調:基於第一次模型用新數據繼續訓練
1555
+ - 📊 自動比較有/無微調的表現差異
1556
+ - 🎨 可選擇最佳化指標(F1、Accuracy、Precision、Recall)
1557
+ - 🔮 訓練後可直接預測新樣本
1558
+ - 💾 自動儲存最佳模型
1559
+ - 🧹 自動記憶體管理
1560
+
1561
+ ✅ **支持的微調方法**: LoRA, AdaLoRA, Adapter, BitFit, Prompt Tuning
1562
+ ⚠️ **暫不支持**: Prefix Tuning (版本兼容性問題,請使用 Prompt Tuning 替代)
1563
+ """)
1564
+
1565
+ # Tab 1: 第一次微調
1566
+ with gr.Tab("1️⃣ 第一次微調"):
1567
+ with gr.Row():
1568
+ with gr.Column(scale=1):
1569
+ gr.Markdown("### 📤 資料上傳")
1570
+
1571
+ file_input = gr.File(
1572
+ label="上傳 CSV 檔案",
1573
+ file_types=[".csv"]
1574
+ )
1575
+
1576
+ gr.Markdown("### 🤖 模型選擇")
1577
+
1578
+ model_name_input = gr.Textbox(
1579
+ value="meta-llama/Llama-3.2-1B",
1580
+ label="Hugging Face 模型名稱",
1581
+ info="例如: meta-llama/Llama-3.2-1B"
1582
+ )
1583
+
1584
+ gr.Markdown("### 🔧 微調方法選擇")
1585
+
1586
+ tuning_method = gr.Radio(
1587
+ choices=["LoRA", "AdaLoRA", "Adapter", "BitFit", "Prompt Tuning"],
1588
+ value="LoRA",
1589
+ label="選擇微調方法",
1590
+ info="不同的參數效率微調方法 (Prefix Tuning 暫不支持)"
1591
+ )
1592
+
1593
+ gr.Markdown("### 🎯 最佳模型選擇")
1594
+
1595
+ best_metric = gr.Dropdown(
1596
+ choices=["f1", "accuracy", "precision", "recall", "sensitivity", "specificity"],
1597
+ value="recall",
1598
+ label="選擇最佳化指標",
1599
+ info="模型會根據此指標選擇最佳檢查點"
1600
+ )
1601
+
1602
+ gr.Markdown("### ⚙️ 資料平衡參數")
1603
+
1604
+ target_samples_input = gr.Number(
1605
+ value=700,
1606
+ label="目標樣本數(每類別)"
1607
+ )
1608
+
1609
+ use_weights_checkbox = gr.Checkbox(
1610
+ value=True,
1611
+ label="使用類別權重",
1612
+ info="在損失函數中使用類別權重"
1613
+ )
1614
+
1615
+ gr.Markdown("### ⚙️ 訓練參數")
1616
+
1617
+ epochs_input = gr.Number(
1618
+ value=3,
1619
+ label="訓練輪數 (Epochs)"
1620
+ )
1621
+
1622
+ batch_size_input = gr.Number(
1623
+ value=4,
1624
+ label="批次大小 (Batch Size)"
1625
+ )
1626
+
1627
+ lr_input = gr.Number(
1628
+ value=1e-4,
1629
+ label="學習率 (Learning Rate)"
1630
+ )
1631
+
1632
+ gr.Markdown("---")
1633
+
1634
+ # LoRA 參數
1635
+ with gr.Column(visible=True) as lora_params:
1636
+ gr.Markdown("### 🔷 LoRA 參數")
1637
+
1638
+ lora_r_input = gr.Slider(
1639
+ minimum=4,
1640
+ maximum=64,
1641
+ value=16,
1642
+ step=4,
1643
+ label="LoRA Rank (r)",
1644
+ info="低秩分解的秩"
1645
+ )
1646
+
1647
+ lora_alpha_input = gr.Slider(
1648
+ minimum=8,
1649
+ maximum=128,
1650
+ value=32,
1651
+ step=8,
1652
+ label="LoRA Alpha",
1653
+ info="LoRA 縮放參數"
1654
+ )
1655
+
1656
+ lora_dropout_input = gr.Slider(
1657
+ minimum=0.0,
1658
+ maximum=0.5,
1659
+ value=0.1,
1660
+ step=0.05,
1661
+ label="LoRA Dropout",
1662
+ info="Dropout 率"
1663
+ )
1664
+
1665
+ lora_target_input = gr.Dropdown(
1666
+ choices=["query,value", "query,key,value", "all"],
1667
+ value="query,value",
1668
+ label="目標模組",
1669
+ info="用逗號分隔"
1670
+ )
1671
+
1672
+ # AdaLoRA 參數
1673
+ with gr.Column(visible=False) as adalora_params:
1674
+ gr.Markdown("### 🔶 AdaLoRA 參數")
1675
+
1676
+ adalora_init_r_input = gr.Slider(
1677
+ minimum=4,
1678
+ maximum=64,
1679
+ value=12,
1680
+ step=4,
1681
+ label="初始 Rank",
1682
+ info="訓練開始時的秩"
1683
+ )
1684
+
1685
+ adalora_target_r_input = gr.Slider(
1686
+ minimum=4,
1687
+ maximum=64,
1688
+ value=8,
1689
+ step=4,
1690
+ label="目標 Rank",
1691
+ info="訓練結束時的目標秩"
1692
+ )
1693
+
1694
+ adalora_alpha_input = gr.Slider(
1695
+ minimum=8,
1696
+ maximum=128,
1697
+ value=32,
1698
+ step=8,
1699
+ label="LoRA Alpha",
1700
+ info="縮放參數"
1701
+ )
1702
+
1703
+ adalora_tinit_input = gr.Number(
1704
+ value=0,
1705
+ label="Tinit",
1706
+ info="開始剪枝的步數"
1707
+ )
1708
+
1709
+ adalora_tfinal_input = gr.Number(
1710
+ value=0,
1711
+ label="Tfinal",
1712
+ info="結束剪枝的步數"
1713
+ )
1714
+
1715
+ adalora_delta_t_input = gr.Number(
1716
+ value=1,
1717
+ label="Delta T",
1718
+ info="剪枝頻率"
1719
+ )
1720
+
1721
+ # Adapter 參數
1722
+ with gr.Column(visible=False) as adapter_params:
1723
+ gr.Markdown("### 🔶 Adapter 參數")
1724
+
1725
+ adapter_reduction_input = gr.Slider(
1726
+ minimum=2,
1727
+ maximum=64,
1728
+ value=16,
1729
+ step=2,
1730
+ label="Reduction Factor",
1731
+ info="降維因子,越大參數越少"
1732
+ )
1733
+
1734
+ # Prompt Tuning 參數
1735
+ with gr.Column(visible=False) as prompt_tuning_params:
1736
+ gr.Markdown("### 🔷 Prompt Tuning 參數")
1737
+
1738
+ prompt_tokens_input = gr.Slider(
1739
+ minimum=1,
1740
+ maximum=100,
1741
+ value=20,
1742
+ step=1,
1743
+ label="Virtual Tokens 數量"
1744
+ )
1745
+
1746
+ # Prefix Tuning 參數
1747
+ with gr.Column(visible=False) as prefix_tuning_params:
1748
+ gr.Markdown("### 🔶 Prefix Tuning 參數")
1749
+ gr.Markdown("⚠️ **注意**: 目前版本可能有兼容性問題,建議使用 Prompt Tuning")
1750
+
1751
+ prefix_tokens_input = gr.Slider(
1752
+ minimum=1,
1753
+ maximum=100,
1754
+ value=30,
1755
+ step=1,
1756
+ label="Virtual Tokens 數量"
1757
+ )
1758
+
1759
+ train_button = gr.Button(
1760
+ "🚀 開始第一次微調",
1761
+ variant="primary",
1762
+ size="lg"
1763
+ )
1764
+
1765
+ with gr.Column(scale=2):
1766
+ gr.Markdown("### 📊 第一次微調結果與比較")
1767
+
1768
+ # 第一格:資料資訊
1769
+ data_info_output = gr.Markdown(
1770
+ value="### 等待訓練...\n\n訓練完成後會顯示資料資訊和訓練配置",
1771
+ label="資料資訊"
1772
+ )
1773
+
1774
+ # 第二和第三格:並排顯示
1775
+ with gr.Row():
1776
+ # 第二格:未微調 Llama
1777
+ baseline_output = gr.Markdown(
1778
+ value="### 未微調 Llama\n等待訓練完成...",
1779
+ label="未微調 Llama"
1780
+ )
1781
+
1782
+ # 第三格:微調後 Llama
1783
+ finetuned_output = gr.Markdown(
1784
+ value="### 第一次微調 Llama\n等待訓練完成...",
1785
+ label="第一次微調 Llama"
1786
+ )
1787
+
1788
+ # Tab 2: 二次微調
1789
+ with gr.Tab("2️⃣ 二次微調"):
1790
+ with gr.Row():
1791
+ with gr.Column(scale=1):
1792
+ gr.Markdown("### 🔄 選擇基礎模型")
1793
+ base_model_dropdown = gr.Dropdown(
1794
+ label="選擇第一次微調的模型",
1795
+ choices=["請先進行第一次微調"],
1796
+ value="請先進行第一次微調"
1797
+ )
1798
+ refresh_base_models = gr.Button("🔄 重新整理模型列表", size="sm")
1799
+
1800
+ gr.Markdown("### 📤 上傳新訓練數據")
1801
+ file_input_second = gr.File(label="上傳新的訓練數據 CSV", file_types=[".csv"])
1802
+
1803
+ gr.Markdown("### ⚙️ 訓練參數")
1804
+ gr.Markdown("⚠️ 微調方法將自動繼承第一次微調的方法")
1805
+ best_metric_second = gr.Dropdown(
1806
+ choices=["f1", "accuracy", "precision", "recall", "sensitivity", "specificity"],
1807
+ value="f1",
1808
+ label="選擇最佳化指標"
1809
+ )
1810
+
1811
+ target_samples_second = gr.Number(
1812
+ value=700,
1813
+ label="目標樣本數(每類別)"
1814
+ )
1815
+
1816
+ use_weights_second = gr.Checkbox(
1817
+ value=True,
1818
+ label="使用類別權重"
1819
+ )
1820
+
1821
+ epochs_input_second = gr.Number(value=3, label="訓練輪數", info="建議比第一次少")
1822
+ batch_size_input_second = gr.Number(value=4, label="批次大小")
1823
+ lr_input_second = gr.Number(value=5e-5, label="學習率", info="建議比第一次小")
1824
+
1825
+ train_button_second = gr.Button("🚀 開始二次微調", variant="primary", size="lg")
1826
+
1827
+ with gr.Column(scale=2):
1828
+ gr.Markdown("### 📊 二次微調結果")
1829
+ data_info_output_second = gr.Markdown(value="等待訓練...")
1830
+ finetuned_output_second = gr.Markdown(value="### 二次微調\n等待訓練...")
1831
+
1832
+ # Tab 3: 新數據測試
1833
+ with gr.Tab("3️⃣ 新數據測試"):
1834
+ with gr.Row():
1835
+ with gr.Column(scale=1):
1836
+ gr.Markdown("### 📤 上傳測試數據")
1837
+ test_file_input = gr.File(label="上傳測試數據 CSV", file_types=[".csv"])
1838
+
1839
+ gr.Markdown("### 🎯 選擇要比較的模型")
1840
+ gr.Markdown("可選擇 1-3 個模型進行比較")
1841
+
1842
+ baseline_test_choice = gr.Radio(
1843
+ choices=["評估純 Llama", "跳過"],
1844
+ value="評估純 Llama",
1845
+ label="純 Llama (Baseline)"
1846
+ )
1847
+
1848
+ first_model_test_dropdown = gr.Dropdown(
1849
+ label="第一次微調模型",
1850
+ choices=["請選擇"],
1851
+ value="請選擇"
1852
+ )
1853
+
1854
+ second_model_test_dropdown = gr.Dropdown(
1855
+ label="第二次微調模型",
1856
+ choices=["請選擇"],
1857
+ value="請選擇"
1858
+ )
1859
+
1860
+ refresh_test_models = gr.Button("🔄 重新整理模型列表", size="sm")
1861
+ test_button = gr.Button("📊 開始測試", variant="primary", size="lg")
1862
+
1863
+ with gr.Column(scale=2):
1864
+ gr.Markdown("### 📊 新數據測試結果 - 三模型比較")
1865
+ with gr.Row():
1866
+ baseline_test_output = gr.Markdown(value="### 純 Llama\n等待測試...")
1867
+ first_test_output = gr.Markdown(value="### 第一次微調\n等待測試...")
1868
+ second_test_output = gr.Markdown(value="### 二次微調\n等待測試...")
1869
+
1870
+ # Tab 4: 模型預測
1871
+ with gr.Tab("4️⃣ 模型預測"):
1872
+ gr.Markdown("""
1873
+ ### 使用訓練好的模型進行預測
1874
+
1875
+ 選擇已訓練的模型,輸入文本進行預測。會同時顯示未微調和微調模型的預測結果以供比較。
1876
+ """)
1877
+
1878
+ with gr.Row():
1879
+ with gr.Column():
1880
+ # 模型選擇下拉選單
1881
+ model_dropdown = gr.Dropdown(
1882
+ label="選擇模型",
1883
+ choices=["請先訓練模型"],
1884
+ value="請先訓練模型",
1885
+ info="選擇要使用的已訓練模型"
1886
+ )
1887
+
1888
+ refresh_button = gr.Button(
1889
+ "🔄 重新整理模型列表",
1890
+ size="sm"
1891
+ )
1892
+
1893
+ text_input = gr.Textbox(
1894
+ label="輸入文本",
1895
+ placeholder="請輸入要預測的文本...",
1896
+ lines=10
1897
+ )
1898
+
1899
+ predict_button = gr.Button(
1900
+ "🔮 開始預測",
1901
+ variant="primary",
1902
+ size="lg"
1903
+ )
1904
+
1905
+ with gr.Column():
1906
+ gr.Markdown("### 預測結果比較")
1907
+
1908
+ # 上框:未微調 Llama 預測結果
1909
+ baseline_prediction_output = gr.Markdown(
1910
+ label="未微調 Llama",
1911
+ value="等待預測..."
1912
+ )
1913
+
1914
+ # 下框:微調 Llama 預測結果
1915
+ finetuned_prediction_output = gr.Markdown(
1916
+ label="微調 Llama",
1917
+ value="等待預測..."
1918
+ )
1919
+
1920
+ # Tab 5: 使用說明
1921
+ with gr.Tab("📖 使用說明"):
1922
+ gr.Markdown("""
1923
+ ## 🔄 二次微調流程說明
1924
+
1925
+ ### 步驟 1: 第一次微調
1926
+ 1. 上傳訓練數據 A (CSV 格式: Text, label)
1927
+ 2. 選擇微調方法 (LoRA / AdaLoRA / Adapter / BitFit / Prompt Tuning)
1928
+ 3. 調整訓練參數
1929
+ 4. 開始訓練
1930
+ 5. 系統會自動比較純 Llama vs 第一次微調的表現
1931
+
1932
+ ### 步驟 2: 二次微調
1933
+ 1. 選擇已訓練的第一次微調模型
1934
+ 2. 上傳新的訓練數據 B
1935
+ 3. 調整訓練參數 (建議 epochs 更小, learning rate 更小)
1936
+ 4. 開始訓練 (方法自動繼承第一次)
1937
+ 5. 模型會基於第一次的權重繼續學習
1938
+
1939
+ ### 步驟 3: 預測
1940
+ 1. 選擇任一已訓練模型
1941
+ 2. 輸入文本
1942
+ 3. 查看預測結果
1943
+
1944
+ ## 🎯 微調方法說明
1945
+
1946
+ | 方法 | 參數量 | 記憶體 | 訓練速度 | 適用場景 |
1947
+ |------|--------|--------|----------|----------|
1948
+ | **LoRA** | 很少 (~1%) | 低 | 快 | 通用,效果好 |
1949
+ | **AdaLoRA** | 很少 (~1%) | 低 | 快 | 自適應,效果更優 |
1950
+ | **Adapter** | 少 (~2-5%) | 低 | 中 | 多任務學習 |
1951
+ | **BitFit** | 極少 (~0.1%) | 極低 | 極快 | 快速微調 |
1952
+ | **Prompt Tuning** | 極少 (可調) | 極低 | 快 | 小數據集 |
1953
+
1954
+ ## 💡 二次微調建議
1955
+
1956
+ ### 訓練參數調整:
1957
+ - **Epochs**: 第二次建議 3-5 輪 (第一次通常 8-10 輪)
1958
+ - **Learning Rate**: 第二次建議 5e-5 (第一次通常 1e-4)
1959
+ - **Warmup Steps**: 第二次建議減半
1960
+
1961
+ ### 適用場景:
1962
+ 1. **領域適應**: 第一次用通用醫療數據,第二次用特定醫院數據
1963
+ 2. **增量學習**: 隨時間增加新病例數據
1964
+ 3. **數據稀缺**: 先用大量相關數據預訓練,再用少量目標數據微調
1965
+
1966
+ ## ⚠️ 注意事項
1967
+
1968
+ - CSV 格式必須包含 `Text` 和 `label` 欄位
1969
+ - 第二次微調會自動使用第一次的微調方法
1970
+ - 建議第二次的學習率比第一次小,避免破壞已學習的知識
1971
+ - 訓練時間依資料量和硬體而定(10-30 分鐘)
1972
+ - 需要 Hugging Face Token 才能下載 Llama 模型
1973
+ - GPU 訓練效果最佳,CPU 會非常慢
1974
+
1975
+ ## 📊 指標說明
1976
+
1977
+ - **F1 Score**: 精確率和召回率的調和平均,平衡指標
1978
+ - **Accuracy**: 整體準確率
1979
+ - **Precision**: 預測為正類中的準確率
1980
+ - **Recall/Sensitivity**: 實際正類中被正確識別的比例
1981
+ - **Specificity**: 實際負類中被正確識別的比例
1982
+
1983
+ ## 🔧 已修復的問題
1984
+
1985
+ - ✅ **AdaLoRA**: 簡化配置參數,避免版本兼容性問題
1986
+ - ✅ **BitFit**: 正確處理 gradient 設置,包含分類頭訓練
1987
+ - ✅ **參數顯示**: AdaLoRA 現在會正確顯示專屬參數界面
1988
+ - ❌ **Prefix Tuning**: 因 PEFT 版本問題暫時移除,請用 Prompt Tuning 替代
1989
+
1990
+ ## 🔐 設定 HF Token
1991
+
1992
+ 在環境變數中設定:
1993
+ ```
1994
+ export HF_TOKEN=your_token_here
1995
+ ```
1996
+ """)
1997
+
1998
+ # ==================== 事件綁定 ====================
1999
+
2000
+ # 根據選擇的微調方法顯示/隱藏相應參數
2001
+ def update_params_visibility(method):
2002
+ if method == "LoRA":
2003
+ return (
2004
+ gr.update(visible=True), # lora_params
2005
+ gr.update(visible=False), # adalora_params
2006
+ gr.update(visible=False), # adapter_params
2007
+ gr.update(visible=False), # prompt_tuning_params
2008
+ gr.update(visible=False) # prefix_tuning_params
2009
+ )
2010
+ elif method == "AdaLoRA":
2011
+ return (
2012
+ gr.update(visible=False), # lora_params
2013
+ gr.update(visible=True), # adalora_params
2014
+ gr.update(visible=False), # adapter_params
2015
+ gr.update(visible=False), # prompt_tuning_params
2016
+ gr.update(visible=False) # prefix_tuning_params
2017
+ )
2018
+ elif method == "Adapter":
2019
+ return (
2020
+ gr.update(visible=False),
2021
+ gr.update(visible=False),
2022
+ gr.update(visible=True),
2023
+ gr.update(visible=False),
2024
+ gr.update(visible=False)
2025
+ )
2026
+ elif method == "Prompt Tuning":
2027
+ return (
2028
+ gr.update(visible=False),
2029
+ gr.update(visible=False),
2030
+ gr.update(visible=False),
2031
+ gr.update(visible=True),
2032
+ gr.update(visible=False)
2033
+ )
2034
+ elif method == "Prefix Tuning":
2035
+ return (
2036
+ gr.update(visible=False),
2037
+ gr.update(visible=False),
2038
+ gr.update(visible=False),
2039
+ gr.update(visible=False),
2040
+ gr.update(visible=True)
2041
+ )
2042
+ else: # BitFit
2043
+ return (
2044
+ gr.update(visible=False),
2045
+ gr.update(visible=False),
2046
+ gr.update(visible=False),
2047
+ gr.update(visible=False),
2048
+ gr.update(visible=False)
2049
+ )
2050
+
2051
+ tuning_method.change(
2052
+ fn=update_params_visibility,
2053
+ inputs=[tuning_method],
2054
+ outputs=[lora_params, adalora_params, adapter_params, prompt_tuning_params, prefix_tuning_params]
2055
+ )
2056
+
2057
+ # 設定第一次微調按鈕動作
2058
+ train_button.click(
2059
+ fn=train_first_wrapper,
2060
+ inputs=[
2061
+ file_input,
2062
+ model_name_input,
2063
+ target_samples_input,
2064
+ use_weights_checkbox,
2065
+ epochs_input,
2066
+ batch_size_input,
2067
+ lr_input,
2068
+ tuning_method,
2069
+ lora_r_input,
2070
+ lora_alpha_input,
2071
+ lora_dropout_input,
2072
+ lora_target_input,
2073
+ adalora_init_r_input,
2074
+ adalora_target_r_input,
2075
+ adalora_alpha_input,
2076
+ adalora_tinit_input,
2077
+ adalora_tfinal_input,
2078
+ adalora_delta_t_input,
2079
+ adapter_reduction_input,
2080
+ prompt_tokens_input,
2081
+ prefix_tokens_input,
2082
+ best_metric
2083
+ ],
2084
+ outputs=[data_info_output, baseline_output, finetuned_output]
2085
+ )
2086
+
2087
+ # 重新整理基礎模型列表按鈕
2088
+ def refresh_base_models_list():
2089
+ choices = get_first_finetuning_models()
2090
+ return gr.update(choices=choices, value=choices[0])
2091
+
2092
+ refresh_base_models.click(
2093
+ fn=refresh_base_models_list,
2094
+ outputs=[base_model_dropdown]
2095
+ )
2096
+
2097
+ # 二次微調按鈕
2098
+ train_button_second.click(
2099
+ fn=train_second_wrapper,
2100
+ inputs=[
2101
+ base_model_dropdown,
2102
+ file_input_second,
2103
+ target_samples_second,
2104
+ use_weights_second,
2105
+ epochs_input_second,
2106
+ batch_size_input_second,
2107
+ lr_input_second,
2108
+ best_metric_second
2109
+ ],
2110
+ outputs=[data_info_output_second, finetuned_output_second]
2111
+ )
2112
+
2113
+ # 重新整理測試模型列表
2114
+ def refresh_test_models_list():
2115
+ all_models = get_available_models()
2116
+ first_models = get_first_finetuning_models()
2117
+
2118
+ # 篩選第二次微調模型
2119
+ with open('./saved_llama_models_list.json', 'r') as f:
2120
+ models_list = json.load(f)
2121
+ second_models = [m['model_path'] for m in models_list if m.get('is_second_finetuning', False)]
2122
+
2123
+ if len(second_models) == 0:
2124
+ second_models = ["請選擇"]
2125
+
2126
+ return (
2127
+ gr.update(choices=first_models if first_models[0] != "請先進行第一次微調" else ["請選擇"], value="請選擇"),
2128
+ gr.update(choices=second_models, value="請選擇")
2129
+ )
2130
+
2131
+ refresh_test_models.click(
2132
+ fn=refresh_test_models_list,
2133
+ outputs=[first_model_test_dropdown, second_model_test_dropdown]
2134
+ )
2135
+
2136
+ # 測試按鈕
2137
+ test_button.click(
2138
+ fn=test_new_data_wrapper,
2139
+ inputs=[test_file_input, baseline_test_choice, first_model_test_dropdown, second_model_test_dropdown],
2140
+ outputs=[baseline_test_output, first_test_output, second_test_output]
2141
+ )
2142
+
2143
+ # 重新整理模型列表按鈕
2144
+ def refresh_models():
2145
+ return gr.update(choices=get_available_models(), value=get_available_models()[0])
2146
+
2147
+ refresh_button.click(
2148
+ fn=refresh_models,
2149
+ inputs=[],
2150
+ outputs=[model_dropdown]
2151
+ )
2152
+
2153
+ # 預測按鈕動作
2154
+ predict_button.click(
2155
+ fn=predict_text,
2156
+ inputs=[model_dropdown, text_input],
2157
+ outputs=[baseline_prediction_output, finetuned_prediction_output]
2158
+ )
2159
+
2160
+ if __name__ == "__main__":
2161
+ demo.launch()
llama_requirements.txt ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gradio for web interface
2
+ gradio
3
+
4
+ # Transformers and related packages
5
+ transformers
6
+ accelerate
7
+ bitsandbytes
8
+
9
+ # PEFT for LoRA, AdaLoRA, and other methods
10
+ peft
11
+
12
+ # Dataset and model utilities
13
+ datasets
14
+ huggingface_hub
15
+
16
+ # Machine learning libraries
17
+ torch
18
+ scikit-learn
19
+
20
+ # Data processing
21
+ pandas
22
+ numpy
23
+
24
+ # For evaluation
25
+ scipy
readme.txt ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Llama NBCD Second Fine-tuning Platform
3
+ emoji: 🦙
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 4.44.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # 🦙 Llama NBCD 二次微調完整平台
14
+
15
+ 互動式 Llama 模型二次微調和預測平台,支持多種參數高效微調方法 (LoRA, AdaLoRA, Adapter, BitFit, Prompt Tuning)。
16
+
17
+ ## 🌟 功能特色
18
+
19
+ - 🎯 **第一次微調**: 從純 Llama 開始訓練,支持 5 種 PEFT 方法
20
+ - 🔄 **二次微調**: 基於第一次模型用新數據繼續訓練
21
+ - 📊 **Baseline 比較**: 自動比較未微調 vs 微調模型的效果
22
+ - 🧪 **新數據測試**: 同時比較 3 個模型在新數據上的表現
23
+ - 🎨 **指標選擇**: 可選擇最佳化指標(F1、Accuracy、Precision、Recall、Sensitivity、Specificity)
24
+ - 🔮 **即時預測**: 訓練後可直接預測新樣本
25
+ - 💾 **模型管理**: 自動儲存和管理多個訓練模型
26
+ - 🧹 **記憶體管理**: 自動清理 GPU 記憶體,避免 OOM
27
+
28
+ ## 📋 使用方式
29
+
30
+ ### 📑 頁面結構 (5個Tab)
31
+
32
+ #### 1️⃣ 第一次微調
33
+
34
+ 1. **上傳資料**: CSV 檔案需包含 `Text` 和 `label` 欄位
35
+ 2. **選擇模型**: 設定 Llama 模型(預設: meta-llama/Llama-3.2-1B)
36
+ 3. **選擇微調方法**:
37
+ - ✅ **LoRA**: 通用,效果好
38
+ - ✅ **AdaLoRA**: 自適應,效果更優
39
+ - ✅ **Adapter**: 適合多任務
40
+ - ✅ **BitFit**: 極快速,參數最少
41
+ - ✅ **Prompt Tuning**: 適合小數據集
42
+ - ❌ **Prefix Tuning**: 暫不支持(兼容性問題)
43
+ 4. **設定參數**: 調整資料平衡、訓練參數和 PEFT 參數
44
+ 5. **開始訓練**: 點擊「開始第一次微調」按鈕
45
+ 6. **查看結果**: 比較未微調和微調模型的表現
46
+
47
+ #### 2️⃣ 二次微調
48
+
49
+ 1. **選擇基礎模型**: 從下拉選單選擇已訓練的第一次微調模型
50
+ 2. **上傳新資料**: 上傳新的訓練數據 CSV
51
+ 3. **調整參數**:
52
+ - ⚠️ 微調方法自動繼承第一次
53
+ - 建議 Epochs 更少 (3-5 輪)
54
+ - 建議 Learning Rate 更小 (5e-5)
55
+ 4. **開始訓練**: 點擊「開始二次微調」按鈕
56
+ 5. **查看結果**: 查看二次微調後的表現
57
+
58
+ #### 3️⃣ 新數據測試
59
+
60
+ 1. **上傳測試數據**: 上傳測試用的 CSV 檔案
61
+ 2. **選擇要比較的模型**:
62
+ - 純 Llama (Baseline) - 可選
63
+ - 第一次微調模型 - 可選
64
+ - 第二次微調模型 - 可選
65
+ 3. **開始測試**: 點擊「開始測試」按鈕
66
+ 4. **查看結果**: 並排比較所有選擇的模型在新數據上的表現
67
+
68
+ #### 4️⃣ 模型預測
69
+
70
+ 1. **選擇模型**: 從下拉選單選擇已訓練的模型
71
+ 2. **輸入文本**: 輸入要預測的文本
72
+ 3. **查看結果**: 同時顯示未微調和微調模型的預測結果
73
+
74
+ #### 5️⃣ 使用說明
75
+
76
+ - 完整的操作流程說明
77
+ - 微調方法詳細比較
78
+ - 參數調整建議
79
+ - 注意事項和常見問題
80
+
81
+ ## 🔐 重要設定
82
+
83
+ ### Hugging Face Token
84
+
85
+ 如果要使用 Llama 模型,需要:
86
+
87
+ 1. 在 [Hugging Face Settings](https://huggingface.co/settings/tokens) 創建 Token
88
+ 2. 在 Space 的 Settings → Repository secrets 中加入:
89
+ - Name: `HF_TOKEN`
90
+ - Value: 你的 token
91
+
92
+ 或在本地設定環境變數:
93
+ ```bash
94
+ export HF_TOKEN=your_token_here
95
+ ```
96
+
97
+ ## ⚙️ 預設參數
98
+
99
+ ### 第一次微調
100
+ - **訓練輪數**: 3
101
+ - **批次大小**: 4
102
+ - **學習率**: 1e-4
103
+ - **LoRA rank**: 16
104
+ - **LoRA alpha**: 32
105
+ - **目標樣本數**: 700 筆/類別
106
+ - **類別權重**: 啟用
107
+
108
+ ### 二次微調(建議)
109
+ - **訓練輪數**: 3-5(比第一次少)
110
+ - **批次大小**: 4
111
+ - **學習率**: 5e-5(比第一次小)
112
+ - **其他參數**: 自動繼承第一次
113
+
114
+ ## 📊 資料格式
115
+
116
+ CSV 檔案需包含以下欄位:
117
+
118
+ ```csv
119
+ Text,label
120
+ "Patient data text here...",0
121
+ "Another patient data...",1
122
+ ```
123
+
124
+
125
+
126
+ ```csv
127
+ text,Label
128
+ "Patient data text here...",0
129
+ "Another patient data...",1
130
+ ```
131
+
132
+ - `Text`/`text`: 文本資料
133
+ - `Label`/`label`: 標籤 (0 或 1)
134
+
135
+ ## 🎯 微調方法比較
136
+
137
+ | 方法 | 參數量 | 記憶體 | 訓練速度 | 效果 | 適用場景 |
138
+ |------|--------|--------|----------|------|----------|
139
+ | **LoRA** | 很少 (~1%) | 低 | 快 | 良好 | 通用,效果好 |
140
+ | **AdaLoRA** | 很少 (~1%) | 低 | 快 | 優秀 | 自適應,效果更優 |
141
+ | **Adapter** | 少 (~2-5%) | 低 | 中 | 良好 | 多任務學習 |
142
+ | **BitFit** | 極少 (~0.1%) | 極低 | 極快 | 可接受 | 快速微調 |
143
+ | **Prompt Tuning** | 極少 (可調) | 極低 | 快 | 良好 | 小數據集 |
144
+
145
+ ## 💡 二次微調建議
146
+
147
+ ### 適用場景
148
+
149
+ 1. **領域適應**: 第一次用通用醫療數據,第二次用特定醫院數據
150
+ 2. **增量學習**: 隨時間增加新病例數據
151
+ 3. **數據稀缺**: 先用大量相關數據預訓練,再用少量目標數據微調
152
+
153
+ ### 參數調整原則
154
+
155
+ - **Epochs**: 第二次建議 3-5 輪(第一次通常 5-8 輪)
156
+ - **Learning Rate**: 第二次建議 5e-5(第一次通常 1e-4)
157
+ - **避免**: 第二次不要用太大的學習率,會破壞已學習的知識
158
+
159
+ ## 📈 評估指標說明
160
+
161
+ - **F1 Score**: 精確率和召回率的調和平均,平衡指標
162
+ - **Accuracy**: 整體準確率
163
+ - **Precision**: 預測為正類中的準確率
164
+ - **Recall**: 實際正類中被正確識別的比例
165
+ - **Sensitivity**: 敏感度,等同於 Recall
166
+ - **Specificity**: 特異性,正確識別負類的能力
167
+
168
+ ## ⚠️ 注意事項
169
+
170
+ - 訓練時間依資料量和硬體而定(通常 10-30 分鐘)
171
+ - 需要 Hugging Face Token 才能下載 Llama 模型
172
+ - **GPU 訓練強烈建議**: CPU 訓練會非常慢
173
+ - 資料量建議: 每個類別至少 500 筆資料
174
+ - 二次微調自動繼承第一次的微調方法,無法更改
175
+ - Prefix Tuning 因 PEFT 庫兼容性問題暫不支持,請使用 Prompt Tuning 替代
176
+
177
+ ## 🔧 已知問題與解決方案
178
+
179
+ ### ✅ 已修復
180
+ - **AdaLoRA**: 簡化配置參數,避免版本兼容性問題
181
+ - **BitFit**: 正確處理 gradient 設置,包含分類頭訓練
182
+ - **參數顯示**: 各方法現在會正確顯示專屬參數界面
183
+
184
+ ### ❌ 暫不支持
185
+ - **Prefix Tuning**: 因 PEFT 版本與 transformers 的 DynamicCache 不兼容
186
+ - **錯誤**: `'DynamicCache' object has no attribute 'key_cache'`
187
+ - **替代方案**: 使用 Prompt Tuning,功能類似且更穩定
188
+ - **預計修復**: 等待 PEFT 庫更新
189
+
190
+ ## 🚀 快速開始
191
+
192
+ ```bash
193
+ # 1. 安裝依賴
194
+ pip install -r requirements.txt
195
+
196
+ # 2. 設定 HF Token (可選,但建議設定)
197
+ export HF_TOKEN=your_token_here
198
+
199
+ # 3. 啟動應用
200
+ python app.py
201
+
202
+ # 4. 打開瀏覽器訪問
203
+ # http://localhost:7860
204
+ ```
205
+
206
+ ## 📁 專案結構
207
+
208
+ ```
209
+ .
210
+ ├── app.py # 主程式
211
+ ├── requirements.txt # 依賴套件
212
+ ├── README.md # 說明文件
213
+ ├── saved_llama_models_list.json # 模型列表(自動生成)
214
+ └── llama_nbcd_*/ # 訓練模型目錄(自動生成)
215
+ ```
216
+
217
+ ## 💻 系統需求
218
+
219
+ ### 最低需求
220
+ - **CPU**: 4 核心以上
221
+ - **RAM**: 16GB 以上
222
+ - **硬碟**: 20GB 可用空間
223
+
224
+ ### 建議配置
225
+ - **GPU**: NVIDIA GPU with 16GB+ VRAM (如 V100, A100, RTX 3090/4090)
226
+ - **RAM**: 32GB 以上
227
+ - **硬碟**: 50GB 可用空間(用於儲存多個模型)
228
+
229
+ ### 無 GPU 訓練
230
+ - 可以使用 CPU 訓練,但速度會非常慢(可能需要數小時)
231
+ - 建議使用 Google Colab 或 HuggingFace Spaces 的免費 GPU
232
+
233
+ ## 🤝 貢獻
234
+
235
+ 歡迎提交 Issue 和 Pull Request!
236
+
237
+ ## 📝 License
238
+
239
+ MIT License
240
+
241
+ ## 🙏 致謝
242
+
243
+ - [Hugging Face Transformers](https://github.com/huggingface/transformers)
244
+ - [PEFT](https://github.com/huggingface/peft)
245
+ - [Gradio](https://github.com/gradio-app/gradio)
246
+ - [Meta Llama](https://ai.meta.com/llama/)
247
+
248
+ ## 📧 聯繫方式
249
+
250
+ 如有問題或建議,請開 Issue 討論。
251
+
252
+ ---
253
+
254
+ **⚡ 提示**: 首次使用建議先閱讀「使用說明」頁面,了解完整的操作流程!