Qwen3 微调实践笔记

一、模型微调的意义

为什么要微调

1. 降低训练成本与门槛

  • 大模型参数量巨大,从头训练成本极高,对企业而言性价比低。
  • 微调预训练模型是更经济高效的解决方案。

2. 突破 Prompt Engineering 的限制

Prompt Engineering 虽易于上手,但存在明显缺点:

  • 长度限制:输入序列长度受限,长 Prompt 会被截断,影响输出质量。
  • 推理成本高:推理成本与 Prompt 长度的平方正相关。

3. 提升特定领域性能

  • 当企业拥有高质量的自有领域数据,且 Prompt Engineering 效果不达预期时,微调能显著提升模型在特定领域的专业能力。

4. 实现个性化服务

  • 针对不同用户的数据,训练轻量级的微调模型,是实现个性化服务的有效方案。

5. 保障数据安全

  • 当数据因安全或合规要求不能传递给第三方服务时,必须搭建自有模型。
  • 开源大模型通常需要结合自有数据进行微调,才能满足具体业务需求。

微调 vs. 检索增强生成 (RAG)

与纯RAG系统相比,微调具备以下优势:

  • 能力范围:微调几乎可以实现 RAG 的所有功能,但反之不成立。
  • 知识内化:微调将外部知识直接嵌入模型权重,使模型能独立处理特定领域查询,无需依赖外部检索系统。
  • 混合方案:即使在微调与RAG并用的混合架构中,微调后的模型也能提供可靠的后备方案,增强系统鲁棒性。

微调的优缺点与限制

参考一个案例:大模型微调,为什么99%的企业都不应该碰这个坑?

微调是通过调整预训练模型参数,使其适应特定任务或领域需求的方法。

主要优点:

  • 高效灵活:通常只需较少的数据样本即可获得良好性能,尤其适合特定领域任务(如提升对专有词汇的理解)。
  • 部署简便:模型可直接部署,无需额外外部组件,适合对实时性要求高的场景。

主要缺点与限制:

  • 资源消耗大:虽然不及全量训练,但微调也需要大量计算资源和时间,训练成本高。
  • 过拟合与灾难性遗忘:模型可能在新领域表现良好,但遗忘原有通用知识。
  • 泛化能力可能受限:在某些复杂任务上,其泛化能力可能不如RAG等方法。
  • 更新不灵活:对于需要频繁更新知识的场景,微调需重新训练,不如RAG(仅更新知识库)灵活。

二、Unsloth 工具介绍

Unsloth 是一个专为大语言模型优化的高效微调框架,具有以下核心特点:

  • 更快的训练速度:比标准 LoRA 训练快 2-5 倍。其原理是通过算子融合等技术,减少GPU间的数据传输开销。
  • 更低的内存占用:采用梯度检查点技术,在前向传播时不保存全部中间结果,需时重新计算,从而减少约 30% 的 VRAM 使用。
  • 优化的 LoRA 实现:针对当前主流的 LoRA 微调方法进行了底层深度优化。
  • 广泛的模型支持:支持 Llama、Mistral、Qwen、Phi 等多种主流开源大模型。

三、准备工作

3.1 环境准备

  1. 安装基础的 GPU 驱动、CUDA等。
  2. 在 Python 虚拟环境中安装必要的依赖包:
1
2
pip install swanlab modelscope==1.22.0 "transformers>=4.50.0" datasets==3.2.0 accelerate pandas addict
pip install "unsloth[colab-new]" -i https://pypi.tuna.tsinghua.edu.cn/simple

3.2 下载基座模型

本例选用 Qwen3-8B 模型。

下载命令:

1
2
3
4
5
6
7
8
(ai) yiyu@ubuntu:~/nfs/workspace/qwen3_08b_tunning$ git clone https://www.modelscope.cn/Qwen/Qwen3-8B.git
Cloning into 'Qwen3-8B'...
remote: Enumerating objects: 53, done.
remote: Total 53 (delta 0), reused 0 (delta 0), pack-reused 53
Receiving objects: 100% (53/53), 1.73 MiB | 11.67 MiB/s, done.
Resolving deltas: 100% (15/15), done.
Updating files: 100% (16/16), done.
Filtering content: 100% (6/6), 15.26 GiB | 21.42 MiB/s, done.

3.3 下载数据集

本例选用魔塔社区开源的 HUST-Student-Handbook(华中科技大学学生手册)数据集。

下载命令:

1
2
3
4
5
6
7
8
9
(ai) yiyu@ubuntu:~/nfs/workspace/qwen3_08b_tunning$ git clone https://www.modelscope.cn/datasets/alleyf/HUST-Student-Handbook.git
Cloning into 'HUST-Student-Handbook'...
remote: Enumerating objects: 32, done.
remote: Counting objects: 100% (32/32), done.
remote: Compressing objects: 100% (32/32), done.
remote: Total 32 (delta 12), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (32/32), 12.25 KiB | 896.00 KiB/s, done.
Resolving deltas: 100% (12/12), done.
Filtering content: 100% (2/2), 527.32 KiB | 318.00 KiB/s, done.

四、训练过程

4.1 训练脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
#!/usr/bin/env python3
"""
Qwen3-8B 微调脚本
"""

import os
import sys
import torch

# 设置环境变量 - 必须在导入之前
os.environ['UNSLOTH_NO_STATISTICS'] = '1' # 禁用Unsloth统计信息收集,减少内存占用
os.environ['HF_HUB_OFFLINE'] = '1' # 强制使用本地缓存,避免从HuggingFace Hub下载
os.environ['TRANSFORMERS_OFFLINE'] = '1' # 强制使用本地缓存,避免从HuggingFace Hub下载
os.environ['WANDB_MODE'] = 'disabled' # 禁用Weights & Biases日志记录,避免登录问题

# 现在导入其他包
import json
from datasets import load_dataset
from unsloth import FastLanguageModel
from transformers import TrainingArguments
from trl import SFTTrainer

def format_conversation(example):
"""格式化对话数据 - 适配Qwen格式"""
conversations = example["conversations"]

formatted_text = ""
for msg in conversations:
if msg["role"] == "user":
formatted_text += f"<|im_start|>user\n{msg['content']}<|im_end|>\n"
elif msg["role"] == "assistant":
formatted_text += f"<|im_start|>assistant\n{msg['content']}<|im_end|>\n"

return {"text": formatted_text}

def main():
print("=" * 60)
print("Qwen3-8B 微调")
print("=" * 60)

# 模型参数
model_path = "/home/yiyu/nfs/workspace/qwen3_08b_tunning/Qwen3-8B"
train_file = "/home/yiyu/nfs/workspace/qwen3_08b_tunning/HUST-Student-Handbook/lora_hust_student_handbookt.jsonl"
eval_file = ""
output_dir = "/home/yiyu/nfs/workspace/qwen3_08b_tunning/qwen3-8b-finetuned"

# 检查文件是否存在
if not os.path.exists(model_path):
print(f"错误: 模型路径不存在: {model_path}")
sys.exit(1)

if not os.path.exists(train_file):
print(f"错误: 训练文件不存在: {train_file}")
sys.exit(1)

# 检查GPU
print(f"\nGPU信息:")
print(f" CUDA可用: {torch.cuda.is_available()}")
if torch.cuda.is_available():
print(f" GPU名称: {torch.cuda.get_device_name(0)}")
print(f" GPU内存: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} GB")

# 1. 加载模型和分词器
print("\n1. 加载模型和分词器...")
try:
model, tokenizer = FastLanguageModel.from_pretrained(
model_name=model_path, # 预训练模型路径或HuggingFace模型ID
max_seq_length=2048, # 模型支持的最大序列长度
dtype=None, # 数据类型,None表示自动检测(通常是torch.float16)
load_in_4bit=True, # 使用4位量化加载模型,减少内存占用
token=None, # HuggingFace访问令牌,None表示不使用或已有缓存
)
print(f" ✓ 模型加载成功")
except Exception as e:
print(f" ✗ 模型加载失败: {e}")
sys.exit(1)

# 设置tokenizer
tokenizer.pad_token = tokenizer.eos_token # 使用EOS令牌作为填充令牌
tokenizer.padding_side = "right" # 在序列右侧进行填充
print(f" ✓ Tokenizer设置完成")

# 2. 应用LoRA
print("\n2. 应用LoRA配置...")
try:
model = FastLanguageModel.get_peft_model(
model, # 基础模型
r=16, # LoRA秩,控制低秩矩阵的维度,值越小参数越少
target_modules=[ # 要应用LoRA的目标模块列表
"q_proj", "k_proj", "v_proj", "o_proj", # 注意力机制投影层
"gate_proj", "up_proj", "down_proj", # FFN层投影
],
lora_alpha=32, # LoRA缩放因子,控制新权重对原始权重的贡献程度
lora_dropout=0.05, # LoRA层的Dropout率,防止过拟合
bias="none", # 偏置训练策略:"none"不训练,"all"训练所有偏置
use_gradient_checkpointing=True, # 使用梯度检查点,减少内存占用但增加计算时间
random_state=42, # 随机种子,确保可重复性
use_rslora=False, # 是否使用rsLoRA(减少梯度的LoRA变体)
loftq_config=None, # LoftQ配置,用于量化感知微调
)
print(f" ✓ LoRA应用成功 (r=16, alpha=32)")
except Exception as e:
print(f" ✗ LoRA应用失败: {e}")
sys.exit(1)

# 计算可训练参数
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
total_params = sum(p.numel() for p in model.parameters())
print(f" ✓ 可训练参数: {trainable_params:,} (占总参数 {100*trainable_params/total_params:.2f}%)")

# 3. 加载数据
print("\n3. 加载和准备数据...")
try:
# 加载训练数据
train_dataset = load_dataset("json", data_files=train_file, split="train")
train_dataset = train_dataset.map(
format_conversation,
remove_columns=["conversations"]
)
print(f" ✓ 训练数据: {len(train_dataset)} 条样本")

# 加载评估数据(如果有)
if os.path.exists(eval_file):
eval_dataset = load_dataset("json", data_files=eval_file, split="train")
eval_dataset = eval_dataset.map(
format_conversation,
remove_columns=["conversations"]
)
print(f" ✓ 评估数据: {len(eval_dataset)} 条样本")
eval_data_available = True
else:
print(f" ⓘ 评估数据不存在,跳过评估")
eval_dataset = None
eval_data_available = False

except Exception as e:
print(f" ✗ 数据加载失败: {e}")
sys.exit(1)

# 4. 设置训练参数 - 使用兼容的参数名
print("\n4. 设置训练参数...")
try:
# 创建输出目录
os.makedirs(output_dir, exist_ok=True)

# 训练参数配置
training_args = TrainingArguments(
output_dir=output_dir, # 模型和日志输出目录
num_train_epochs=3, # 训练轮数
per_device_train_batch_size=2, # 每个设备/GPU的训练批次大小
gradient_accumulation_steps=4, # 梯度累积步数,模拟更大批次
warmup_ratio=0.03, # 学习率预热比例(占总训练步数的比例)
learning_rate=2e-4, # 初始学习率
fp16=True, # 使用混合精度训练(16位浮点数)
logging_steps=10, # 每多少步记录一次日志
save_strategy="steps", # 模型保存策略:"steps"按步数保存
save_steps=100, # 每多少步保存一次模型
eval_strategy="steps" if eval_data_available else "no", # 评估策略
eval_steps=100 if eval_data_available else None, # 每多少步评估一次
gradient_checkpointing=True, # 使用梯度检查点,减少内存占用
optim="adamw_8bit", # 优化器类型,8位AdamW
lr_scheduler_type="cosine", # 学习率调度器类型:余弦退火
seed=42, # 随机种子
report_to="none", # 禁用所有日志记录器
ddp_find_unused_parameters=False, # DDP训练中不查找未使用参数
remove_unused_columns=False, # 不自动删除数据集未使用列
save_total_limit=3, # 最多保存的检查点数量
load_best_model_at_end=True if eval_data_available else False, # 训练结束时加载最佳模型
metric_for_best_model="loss" if eval_data_available else None, # 评估指标
greater_is_better=False if eval_data_available else None, # 指标是否越大越好
)
print(f" ✓ 训练参数设置完成")
except Exception as e:
print(f" ✗ 训练参数设置失败: {e}")
sys.exit(1)

# 5. 创建训练器
print("\n5. 创建训练器...")
try:
trainer = SFTTrainer(
model=model, # 要训练的模型
tokenizer=tokenizer, # 分词器
train_dataset=train_dataset, # 训练数据集
eval_dataset=eval_dataset, # 评估数据集
dataset_text_field="text", # 数据集中文本字段的名称
max_seq_length=2048, # 最大序列长度,超过部分会被截断
packing=False, # 是否将多个短序列打包成一个批次
args=training_args, # 训练参数配置
)
print(f" ✓ 训练器创建成功")
except Exception as e:
print(f" ✗ 训练器创建失败: {e}")
sys.exit(1)

# 6. 开始训练
print("\n" + "=" * 60)
print("开始训练...")
print("=" * 60)

try:
# 训练模型
train_result = trainer.train()

print("\n" + "=" * 60)
print("训练完成!")
print("=" * 60)

# 打印训练统计
print(f"\n训练统计:")
print(f" 总训练步数: {train_result.global_step}")
print(f" 训练耗时: {train_result.metrics['train_runtime']:.2f} 秒")
print(f" 每秒步数: {train_result.metrics['train_samples_per_second']:.2f}")

except Exception as e:
print(f"\n训练过程中出错: {e}")
sys.exit(1)

# 7. 保存模型
print("\n6. 保存模型...")
try:
# 保存模型和tokenizer
trainer.save_model()
tokenizer.save_pretrained(output_dir)
print(f" ✓ 模型保存到: {output_dir}")

# 保存训练参数
with open(os.path.join(output_dir, "training_args.json"), "w") as f:
json.dump(training_args.to_dict(), f, indent=2)
print(f" ✓ 训练参数保存")

except Exception as e:
print(f" ✗ 模型保存失败: {e}")
sys.exit(1)

print("\n" + "=" * 60)
print("微调流程完成!")
print("=" * 60)


if __name__ == "__main__":
main()

4.2 微调训练过程

微调训练大概占用8G显存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(base) yiyu@ubuntu:~/nfs/workspace/qwen3_08b_tunning$ nvidia-smi
Mon Dec 22 15:00:59 2025
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.274.02 Driver Version: 535.274.02 CUDA Version: 12.2 |
|-----------------------------------------+----------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+======================+======================|
| 0 Tesla V100-SXM2-32GB Off | 00000000:00:10.0 Off | 0 |
| N/A 56C P0 212W / 300W | 8486MiB / 32768MiB | 84% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+

+---------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=======================================================================================|
| 0 N/A N/A 1191093 C python 8484MiB |
+---------------------------------------------------------------------------------------+

log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
(ai) yiyu@ubuntu:~/nfs/workspace/qwen3_08b_tunning$ python finetune_qwen.py 
🦥 Unsloth: Will patch your computer to enable 2x faster free finetuning.
🦥 Unsloth Zoo will now patch everything to make training faster!
============================================================
Qwen3-8B 微调
============================================================

GPU信息:
CUDA可用: True
GPU名称: Tesla V100-SXM2-32GB
GPU内存: 31.7 GB

1. 加载模型和分词器...
==((====))== Unsloth 2025.12.8: Fast Qwen3 patching. Transformers: 4.57.3.
\\ /| Tesla V100-SXM2-32GB. Num GPUs = 1. Max memory: 31.739 GB. Platform: Linux.
O^O/ \_/ \ Torch: 2.9.1+cu128. CUDA: 7.0. CUDA Toolkit: 12.8. Triton: 3.5.1
\ / Bfloat16 = FALSE. FA [Xformers = 0.0.33.post2. FA2 = False]
"-____-" Free license: http://github.com/unslothai/unsloth
Unsloth: Fast downloading is enabled - ignore downloading bars which are red colored!
Loading checkpoint shards: 100%|█████████████████████████████████████████| 5/5 [02:30<00:00, 30.01s/it]
✓ 模型加载成功
✓ Tokenizer设置完成

2. 应用LoRA配置...
Unsloth: Dropout = 0 is supported for fast patching. You are using dropout = 0.05.
Unsloth will patch all other layers, except LoRA matrices, causing a performance hit.
Unsloth 2025.12.8 patched 36 layers with 0 QKV layers, 0 O layers and 0 MLP layers.
✓ LoRA应用成功 (r=16, alpha=32)
✓ 可训练参数: 43,646,976 (占总参数 0.92%)

3. 加载和准备数据...
Map: 100%|█████████████████████████████████████████████████| 719/719 [00:00<00:00, 11545.18 examples/s]
✓ 训练数据: 719 条样本
ⓘ 评估数据不存在,跳过评估

4. 设置训练参数...
✓ 训练参数设置完成

5. 创建训练器...
Unsloth: Tokenizing ["text"] (num_proc=20): 100%|████████████| 719/719 [00:04<00:00, 169.16 examples/s]
✓ 训练器创建成功

============================================================
开始训练...
============================================================
The model is already on multiple devices. Skipping the move to device specified in `args`.
==((====))== Unsloth - 2x faster free finetuning | Num GPUs used = 1
\\ /| Num examples = 719 | Num Epochs = 3 | Total steps = 270
O^O/ \_/ \ Batch size per device = 2 | Gradient accumulation steps = 4
\ / Data Parallel GPUs = 1 | Total batch size (2 x 4 x 1) = 8
"-____-" Trainable parameters = 43,646,976 of 8,234,382,336 (0.53% trained)
0%| | 0/270 [00:00<?, ?it/s]Unsloth: Will smartly offload gradients to save VRAM!
{'loss': 2.2591, 'grad_norm': 0.9805265665054321, 'learning_rate': 0.0002, 'epoch': 0.11}
{'loss': 1.6408, 'grad_norm': 0.6932195425033569, 'learning_rate': 0.0001992764570419069, 'epoch': 0.22}
{'loss': 1.5088, 'grad_norm': 0.5240709781646729, 'learning_rate': 0.00019711629845587164, 'epoch': 0.33}
{'loss': 1.411, 'grad_norm': 0.6144455671310425, 'learning_rate': 0.0001935507835925601, 'epoch': 0.44}
{'loss': 1.4867, 'grad_norm': 0.6005836129188538, 'learning_rate': 0.00018863150851539877, 'epoch': 0.56}
{'loss': 1.3918, 'grad_norm': 0.6643008589744568, 'learning_rate': 0.00018242965936120768, 'epoch': 0.67}
{'loss': 1.4473, 'grad_norm': 0.8665452003479004, 'learning_rate': 0.00017503498221564025, 'epoch': 0.78}
{'loss': 1.3993, 'grad_norm': 0.5923981666564941, 'learning_rate': 0.00016655448441021747, 'epoch': 0.89}
{'loss': 1.3726, 'grad_norm': 0.6304987072944641, 'learning_rate': 0.00015711088603430405, 'epoch': 1.0}
{'loss': 1.2395, 'grad_norm': 0.7031642198562622, 'learning_rate': 0.00014684084406997903, 'epoch': 1.11}
{'loss': 1.1672, 'grad_norm': 0.8705151081085205, 'learning_rate': 0.0001358929748480946, 'epoch': 1.22}
{'loss': 1.0753, 'grad_norm': 1.0293835401535034, 'learning_rate': 0.00012442570344228313, 'epoch': 1.33}
{'loss': 1.1042, 'grad_norm': 1.4802284240722656, 'learning_rate': 0.00011260497112202895, 'epoch': 1.44}
{'loss': 1.0878, 'grad_norm': 1.1789474487304688, 'learning_rate': 0.00010060183403992856, 'epoch': 1.56}
{'loss': 1.1023, 'grad_norm': 1.3673157691955566, 'learning_rate': 8.858998790219753e-05, 'epoch': 1.67}
{'loss': 1.055, 'grad_norm': 1.0307848453521729, 'learning_rate': 7.674325444256899e-05, 'epoch': 1.78}
{'loss': 1.0381, 'grad_norm': 0.9197383522987366, 'learning_rate': 6.523306607246527e-05, 'epoch': 1.89}
{'loss': 1.037, 'grad_norm': 0.9783514142036438, 'learning_rate': 5.422598510671666e-05, 'epoch': 2.0}
{'loss': 0.7927, 'grad_norm': 1.3467820882797241, 'learning_rate': 4.388129346376178e-05, 'epoch': 2.11}
{'loss': 0.7877, 'grad_norm': 1.3993027210235596, 'learning_rate': 3.4348687719438665e-05, 'epoch': 2.22}
{'loss': 0.769, 'grad_norm': 1.4056848287582397, 'learning_rate': 2.576611286891901e-05, 'epoch': 2.33}
{'loss': 0.7288, 'grad_norm': 1.5049540996551514, 'learning_rate': 1.825776614411082e-05, 'epoch': 2.44}
{'loss': 0.6902, 'grad_norm': 1.1971173286437988, 'learning_rate': 1.1932299773007228e-05, 'epoch': 2.56}
{'loss': 0.7905, 'grad_norm': 1.5358942747116089, 'learning_rate': 6.881248688597553e-06, 'epoch': 2.67}
{'loss': 0.7258, 'grad_norm': 1.0989397764205933, 'learning_rate': 3.1777059397436692e-06, 'epoch': 2.78}
{'loss': 0.7475, 'grad_norm': 0.9426413178443909, 'learning_rate': 8.752649719641848e-07, 'epoch': 2.89}
{'loss': 0.7717, 'grad_norm': 1.2142314910888672, 'learning_rate': 7.244084232338466e-09, 'epoch': 3.0}
{'train_runtime': 1060.9473, 'train_samples_per_second': 2.033, 'train_steps_per_second': 0.254, 'train_loss': 1.1343570991798684, 'epoch': 3.0}
100%|████████████████████████████████████████████████████████████████████████████| 270/270 [17:40<00:00, 3.93s/it]

============================================================
训练完成!
============================================================

训练统计:
总训练步数: 270
训练耗时: 1060.95 秒
每秒步数: 2.03

6. 保存模型...
✓ 模型保存到: /home/yiyu/nfs/workspace/qwen3_08b_tunning/qwen3-8b-finetuned
✓ 训练参数保存

============================================================
微调流程完成!
============================================================

4.3 微调后模型文件结构

训练完成后,生成的模型目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
qwen3-8b-finetuned/
├── adapter_config.json
├── adapter_model.safetensors
├── added_tokens.json
├── chat_template.jinja
├── checkpoint-100/ # 检查点目录
│ ├── adapter_config.json
│ ├── adapter_model.safetensors
│ ├── optimizer.pt
│ ├── scheduler.pt
│ ├── trainer_state.json
│ └── ...
├── checkpoint-200/ # 检查点目录
│ └── ...
├── checkpoint-270/ # 最终检查点目录
│ └── ...
├── merges.txt
├── README.md
├── special_tokens_map.json
├── test_model.py # 测试脚本
├── tokenizer_config.json
├── tokenizer.json
├── training_args.bin
├── training_args.json
└── vocab.json

4 directories, 61 files

五、模型测试

5.1 测试脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#!/usr/bin/env python3
"""
微调后的模型测试脚本 - 支持命令行参数输入模型路径
"""

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
import sys
import os
import argparse


def main():
# 创建命令行参数解析器
parser = argparse.ArgumentParser(description="测试微调后的Qwen模型")
parser.add_argument(
"--model_path",
type=str,
default="/home/yiyu/nfs/workspace/qwen3_08b_tunning/Qwen3-8B",
help="模型路径,默认为原始Qwen3-8B模型"
)
parser.add_argument(
"--max_tokens",
type=int,
default=10000,
help="最大生成token数,默认为10000"
)

# 解析命令行参数
args = parser.parse_args()

print(f"加载模型: {args.model_path}")
print(f"最大生成token数: {args.max_tokens}")

# 检查模型路径是否存在
if not os.path.exists(args.model_path):
print(f"错误: 模型路径不存在: {args.model_path}")
sys.exit(1)

# 加载tokenizer和模型
try:
# 加载tokenizer(分词器),用于文本编码和解码
tokenizer = AutoTokenizer.from_pretrained(args.model_path)

# 加载因果语言模型(用于文本生成)
model = AutoModelForCausalLM.from_pretrained(
args.model_path, # 模型路径
dtype=torch.float16, # 模型权重数据类型,float16可减少内存使用,使用dtype而非torch_dtype
device_map="auto", # 自动将模型层分配到可用GPU上,支持多GPU
trust_remote_code=True, # 信任远程代码执行(对于某些自定义模型是必需的)
)
except Exception as e:
print(f"加载模型时出错: {e}")
sys.exit(1)

# 测试问题
test_questions = [
"3年制研究生的毕业论文从开题到答辩通过最短需要多长时间?"
]

print("\n" + "=" * 60)
print("模型推理测试")
print("=" * 60)

for i, question in enumerate(test_questions, 1):
print(f"\n问题 {i}: {question}")
print("-" * 50)

# 构建提示,使用Qwen模型的特定对话格式
prompt = f"<|im_start|>user\n{question}<|im_end|>\n<|im_start|>assistant\n"

# 编码输入文本为模型可理解的token ID
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")

# 生成文本
try:
with torch.no_grad(): # 禁用梯度计算,减少内存使用
outputs = model.generate(
**inputs, # 解包输入tensors(包含input_ids, attention_mask等)
max_new_tokens=args.max_tokens, # 最大新生成的token数量(不包括输入)
temperature=0.7, # 温度参数:控制随机性,较低的值使输出更确定性
top_p=0.9, # 核采样参数:仅考虑累积概率达到top_p的token
do_sample=True, # 启用采样模式(而非贪婪解码)
pad_token_id=tokenizer.eos_token_id, # 指定填充token ID为结束token ID
)
except Exception as e:
print(f"生成时出错: {e}")
print("可能需要减少max_tokens值,或者检查GPU内存")
continue

# 解码生成的token ID为文本
response = tokenizer.decode(outputs[0], skip_special_tokens=False)

# 提取助手回复(仅保留assistant之后的文本)
if "<|im_start|>assistant" in response:
response = response.split("<|im_start|>assistant")[-1]
if "<|im_end|>" in response:
response = response.split("<|im_end|>")[0]

print(f"回答: {response.strip()}")

# 统计生成的token数量
generated_tokens = outputs[0][inputs['input_ids'].shape[1]:]
print(f"生成的token数: {len(generated_tokens)}")

print("\n" + "=" * 60)
print("测试完成!")
print("=" * 60)


if __name__ == "__main__":
main()

5.2 测试基座模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
(ai) yiyu@ubuntu:~/nfs/workspace/qwen3_08b_tunning$ python test_model.py --model_path=/home/yiyu/nfs/workspace/qwen3_08b_tunning/Qwen3-8B
加载模型: /home/yiyu/nfs/workspace/qwen3_08b_tunning/Qwen3-8B
最大生成token数: 10000
Loading checkpoint shards: 100%|█████████████████████████████████████████████████████| 5/5 [01:41<00:00, 20.35s/it]

============================================================
模型推理测试
============================================================

问题 1: 3年制研究生的毕业论文从开题到答辩通过最短需要多长时间?
--------------------------------------------------
回答: <think>
嗯,用户问的是3年制研究生的毕业论文从开题到答辩通过最短需要多长时间。首先,我需要确认用户的具体情况。可能用户是正在准备研究生学习的本科生,或者已经入学但对时间安排有疑问的研究生。也有可能用户是在考虑是否选择3年制研究生项目,想了解时间上的紧凑程度。

接下来,我得回忆一下通常的研究生流程。一般来说,3年制的研究生项目可能包括课程学习、论文研究、开题报告、中期检查、论文撰写、预审、答辩等环节。每个环节所需时间不同,但用户问的是最短时间,所以需要考虑每个步骤是否能够尽可能快速完成,没有拖延。

首先,开题报告通常在入学后的第二学期进行,但有些学校可能在第一学期就安排。如果用户想最短时间完成,可能需要在入学后尽快完成开题。不过,开题报告需要导师指导,可能需要几个月时间准备,但最短可能是一两个月。

然后是论文研究和撰写。如果开题之后立即开始研究,可能需要几个月时间收集数据、分析结果。但最短时间的话,可能需要至少6个月到一年,这取决于研究的复杂性和数据获取的难易程度。

中期检查通常是开题之后的一个阶段,用来评估研究进展。如果中期检查顺利通过,可能不需要额外时间。但最短情况下,可能只需要几个月。

论文撰写和修改可能需要几个月时间,加上预审和答辩。如果所有步骤都顺利,可能需要几个月。答辩通常在论文完成后进行,可能需要一两个月的时间准备。

不过,用户问的是最短时间,所以需要考虑每个步骤是否都能压缩到最短。例如,开题报告可能只需要几周,研究和撰写可能需要几个月,中期检查可能在开题后几个月内完成,论文修改和预审可能需要几周,答辩可能在几个月后。

不过,实际操作中,每个步骤都需要时间,尤其是研究和撰写部分,可能无法压缩太多。另外,不同学校和导师的要求可能不同,有些学校可能有严格的时间安排,而有些可能更灵活。

还需要考虑学生个人的能力和效率。如果学生非常高效,可能可以缩短时间,但通常来说,最短时间可能在1年左右,但可能需要更长时间。例如,开题到答辩可能需要至少1年,但实际可能需要更长,比如1.5年或更久。

另外,用户可能想知道是否有可能在更短时间内完成,比如半年,但这种情况非常罕见,除非研究内容非常简单,或者学生有大量资源支持,但通常不现实。

需要提醒用户,最短时间可能因学校、导师、研究课题等因素而异,建议咨询所在学校的具体要求,并合理规划时间。同时,要确保研究质量,不能为了赶时间而牺牲论文质量。
</think>

3年制研究生的毕业论文从开题到答辩通过的最短时间,通常取决于多个因素,包括学校要求、导师指导、研究课题的复杂性、学生的效率等。以下是基于一般情况的分析和时间线参考:

---

### **最短时间线(理想情况)**
假设所有环节均高效推进,且无重大延误,**最短可能需要约1年**(约12个月)。具体时间线如下:

1. **开题报告(1-2个月)**
- 入学后第1-2学期完成,需与导师讨论研究方向、确定题目、制定研究计划。

2. **论文研究与撰写(6-12个月)**
- 研究阶段(数据收集、实验、分析):3-6个月
- 论文撰写:3-6个月(需反复修改)
- 若研究内容简单(如纯理论分析、文献综述),可能缩短至3-4个月。

3. **中期检查(1-2个月)**
- 评估研究进展,通常在开题后6-12个月内进行。

4. **论文预审与修改(1-2个月)**
- 根据导师反馈调整论文,准备答辩材料。

5. **答辩(1个月)**
- 答辩准备(1-2周) + 正式答辩(1周)。

**总时间**:开题后约1年(具体可能为10-12个月)。

---

### **实际常见时间线**
在实际情况中,由于研究复杂性、数据获取难度、导师反馈周期等因素,**最短可能需要1.5-2年**。例如:

- **开题后1.5年**:研究阶段耗时更长,或需多次修改。
- **开题后2年**:若课题涉及实验、实地调查或复杂数据分析,时间可能进一步延长。

---

### **关键影响因素**
1. **研究课题性质**
- 理论研究(如纯文献分析):时间较短。
- 实验/实证研究:需更多时间收集数据、分析结果。

2. **导师与学校要求**
- 部分学校要求中期检查、预审等环节,可能延长周期。

3. **学生效率与资源**
- 高效的学生可能压缩时间,但需确保质量。

4. **答辩通过率**
- 若答辩未通过,需补充修改,可能额外耗时。

---

### **注意事项**
- **不可压缩的环节**:研究本身是核心,需保证质量,不能为赶时间牺牲成果。
- **合理规划**:建议在开题时与导师明确时间节点,预留缓冲时间。
- **学校差异**:不同高校对毕业论文的要求不同,需以所在学校的具体规定为准。

---

### **结论**
在理想情况下,**3年制研究生从开题到答辩通过的最短时间约为1年**,但实际中可能需要1.5-2年。建议学生提前规划,与导师保持沟通,确保研究进度与质量平衡。
生成的token数: 1281

============================================================
测试完成!
============================================================

5.3 测试微调模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(ai) yiyu@ubuntu:~/nfs/workspace/qwen3_08b_tunning$ python test_model.py --model_path=/home/yiyu/nfs/workspace/qwen3_08b_tunning/qwen3-8b-finetuned
加载模型: /home/yiyu/nfs/workspace/qwen3_08b_tunning/qwen3-8b-finetuned
最大生成token数: 10000
Loading checkpoint shards: 100%|████████████████████████████████████████████████████████| 5/5 [01:40<00:00, 20.09s/it]

============================================================
模型推理测试
============================================================

问题 1: 3年制研究生的毕业论文从开题到答辩通过最短需要多长时间?
--------------------------------------------------
回答: 3年制研究生的毕业论文从开题到答辩通过最短需要1年时间,即开题时间为入学后的第2学期,最迟答辩时间为入学后的第6学期。
生成的token数: 41

============================================================
测试完成!
============================================================

5.4 参考答案

1
{"conversations":[{"role":"user","content":"三年制硕士生从开题通过到申请学位论文答辩的最短时间要求是多少?"},{"role":"assistant","content":"三年制硕士生从开题答辩通过到申请学位论文答辩的最短时间要求是不得少于12个月。这一规定明确规定了开题通过后必须经过至少一年的时间才能申请论文答辩。"}]}

六、总结

从测试结果可以看出:

微调前(基座模型):

  • 回答详尽但冗余,生成1281个tokens
  • 包含大量通用知识和推理过程

微调后:

  • 回答简洁精准,仅41个tokens
  • 直接命中问题核心,给出明确的时间要求
  • 准确反映了数据集中”不得少于12个月”的规定

本次实践验证了:

  • 轻量级微调在特定领域知识注入上的有效性
  • 开源模型+领域数据是企业构建专业AI服务的可行路径
  • LoRA技术降低了模型个性化定制门槛

本次Qwen3-8B微调实践成功展示了:

  • 技术可行性:在有限资源下完成大模型领域适应
  • 效果显著性:微调后模型在特定领域回答质量显著提升
  • 流程标准化:建立了可复现的微调工作流程

通过”预训练大模型+LoRA微调+领域数据”的技术路线,企业可以经济高效地构建具备专业能力的AI系统,为垂直领域的智能化应用提供了实践参考。