1. 项目概述
作为一名长期从事AI技术实践的开发者,我最近完成了一个有趣的实验:在普通笔记本电脑上训练了一个8.41M参数的医疗领域大模型。这个项目完整走完了从数据准备、预训练、监督微调(SFT)到直接偏好优化(DPO)的全流程,虽然最终模型规模不大,但整个实践过程对于理解大模型训练的核心机制非常有价值。
这个项目的特别之处在于:
- 完全在消费级硬件(2019款MacBook Pro)上完成
- 使用开源工具链(PyTorch+Transformers)
- 完整复现了工业级大模型的训练流程
- 发现了小模型训练中的"灾难性遗忘"现象
- 提供了可直接运行的完整代码
2. 技术选型与环境准备
2.1 工具链选择
在开始项目前,我仔细评估了各种技术方案,最终确定的工具栈如下:
| 组件类型 | 具体工具 | 版本要求 | 选择理由 |
|---|---|---|---|
| 编程语言 | Python | 3.10 | 3.10版本在稳定性和库支持上达到最佳平衡 |
| 深度学习框架 | PyTorch | 2.0+ | 动态图机制更适合研究场景,社区支持完善 |
| 模型库 | HuggingFace Transformers | 最新版 | 提供现成的模型实现和训练接口 |
| 数据处理 | HuggingFace Datasets | 最新版 | 简化数据加载和预处理流程 |
| 分词器 | Tokenizers | 最新版 | 高性能分词实现,支持BPE等主流算法 |
| 开发环境 | Jupyter Notebook | - | 方便实验和调试 |
提示:Python版本不宜过高,某些库可能尚未适配最新Python版本。经测试Python 3.10能完美兼容所有所需库。
2.2 硬件配置
我的训练设备配置如下:
- 处理器:六核Intel Core i7(2019款MacBook Pro)
- 内存:16GB DDR4
- 存储:512GB SSD
- 显卡:仅集成显卡(未使用)
由于MacBook的GPU性能有限,且PyTorch对Apple Metal的支持尚不完善,我决定全程使用CPU进行训练。这带来两个直接影响:
- 训练速度大幅降低
- 模型规模必须严格控制
2.3 模型参数设计
基于硬件限制,我设计了如下模型架构:
| 参数项 | 取值 | 设计考虑 |
|---|---|---|
| 参数量 | 8.41M | 在可接受训练时间内能达到的最大规模 |
| 向量维度 | 256 | 平衡表达能力和计算开销 |
| Transformer层数 | 4 | 深层网络需要更多显存,4层是CPU能承载的合理值 |
| 词表大小 | 20,000 | 覆盖医疗领域常见术语,同时控制内存占用 |
| 注意力头数 | 8 | 每个头32维(256/8),符合常规设计 |
这个配置相比主流大模型(通常有Billion级参数)确实很小,但足以验证训练流程的可行性。
3. 数据准备与预处理
3.1 数据集选择
优质的数据集是模型成功的基础。经过调研,我选择了以下两个数据集:
-
预训练数据:shibing624/medical(来自HuggingFace)
- 数据量:630MB(约370,000条医疗相关知识)
- 格式:纯文本
- 特点:已经过清洗和格式化,开箱即用
-
微调数据:自构建的Alpaca格式数据集
- 数据量:3.5MB(约5,000条问答对)
- 格式:JSON
- 样例:
json复制{ "instruction": "请描述口腔黏膜吸收的历史", "input": "", "output": "1847年,阿斯坎尼欧·索布雷罗等首先报导了硝酸甘油可以经口腔黏膜吸收进入人体血液循环系统..." }
注意:工业级训练通常会使用TB级数据,但对我们的小规模实验来说,百MB级数据已经足够。
3.2 数据预处理流程
完整的预处理包括以下步骤:
-
数据清洗:
- 去除HTML标签
- 统一标点符号
- 纠正明显错别字
- 过滤非中文字符占比过高的样本
-
分词训练:
python复制from tokenizers import ByteLevelBPETokenizer tokenizer = ByteLevelBPETokenizer() tokenizer.train(files=["medical_data.txt"], vocab_size=20000, min_frequency=2, special_tokens=["<pad>", "<unk>", "<bos>", "<eos>"]) tokenizer.save_model("tokenizer") -
数据集构建:
python复制from datasets import load_dataset dataset = load_dataset("text", data_files={"train": "medical_data.txt"}) def tokenize_function(examples): return tokenizer(examples["text"], truncation=True, max_length=256) tokenized_datasets = dataset.map(tokenize_function, batched=True)
预处理耗时约10分钟,最终得到一个包含20,000个token的词表。
4. 模型训练全流程
4.1 预训练阶段
预训练是让模型学习通用语言表示的关键阶段。我采用标准的自回归语言建模目标,即预测下一个token。
4.1.1 模型初始化
python复制from transformers import GPT2Config, GPT2LMHeadModel
config = GPT2Config(
vocab_size=20000,
n_positions=512,
n_embd=256,
n_layer=4,
n_head=8,
bos_token_id=tokenizer.bos_token_id,
eos_token_id=tokenizer.eos_token_id
)
model = GPT2LMHeadModel(config)
4.1.2 训练配置
python复制from transformers import Trainer, TrainingArguments
training_args = TrainingArguments(
output_dir="./pretrain",
overwrite_output_dir=True,
num_train_epochs=1,
per_device_train_batch_size=4,
save_steps=500,
save_total_limit=2,
logging_steps=50,
prediction_loss_only=True,
learning_rate=5e-4,
fp16=False, # CPU上需关闭
optim="adamw_torch"
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_datasets["train"],
)
4.1.3 训练过程监控
训练过程中我主要关注以下指标:
- Loss曲线:从初始的8+下降到4.7
- 生成样本质量:每隔50步让模型生成文本观察进步
- 资源占用:CPU利用率保持在90%以上,内存占用约12GB
训练耗时约10.5小时完成92,474步。以下是训练过程中模型生成的文本演变:
| 训练步数 | 生成文本示例 (输入:"我今天有点头疼") | Loss值 |
|---|---|---|
| 0 | "今 天 有 点 头 疼 糖 尿 病 患 者" | 8.2 |
| 20,000 | "我今天有点头疼可能是血压高引起的" | 5.1 |
| 92,474 | "我今天有点头疼,建议测量体温并观察..." | 4.7 |
4.2 监督微调(SFT)
预训练后的模型需要进行指令微调才能进行对话。我使用5,000条医疗问答对进行微调。
4.2.1 数据格式转换
python复制def format_sft_example(example):
prompt = f"Instruction: {example['instruction']}\nInput: {example['input']}\nOutput: {example['output']}"
return {"text": prompt}
sft_dataset = load_dataset("json", data_files="sft_data.json")
sft_dataset = sft_dataset.map(format_sft_example)
4.2.2 微调配置
python复制training_args = TrainingArguments(
output_dir="./sft",
per_device_train_batch_size=4,
num_train_epochs=3,
learning_rate=1e-5, # 比预训练更小的学习率
save_steps=100,
logging_steps=10
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=sft_dataset["train"],
)
微调耗时较短(约2小时),最终train_loss为4.517。但测试发现模型出现了明显的"灾难性遗忘"现象——在学会对话格式的同时,丢失了部分医学知识。
4.3 直接偏好优化(DPO)
为了进一步优化回答质量,我尝试了DPO方法。相比传统的RLHF,DPO更节省资源。
4.3.1 数据准备
DPO需要成对的偏好数据,格式如下:
json复制{
"question": "术后肌痛的高危因素有些什么?",
"response_chosen": "琥珀胆碱",
"response_rejected": "手术后疼痛是常见的并发症之一。"
}
4.3.2 DPO训练
python复制from trl import DPOTrainer
dpo_trainer = DPOTrainer(
model=model,
ref_model=model, # 初始参考模型
args=training_args,
train_dataset=dpo_dataset,
beta=0.1, # 控制偏离参考模型的程度
)
dpo_trainer.train()
DPO训练非常耗时(13.5小时仅完成31%),且CPU负载极高。最终在141步时提前终止,reward/accuracy达到0.9375。
5. 结果分析与经验总结
5.1 模型表现对比
我对比了三个阶段的模型表现:
| 模型类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 预训练模型 | 医学知识丰富 | 只会文本接龙,不会问答 | 知识检索类应用 |
| SFT模型 | 能进行结构化问答 | 部分知识遗忘,回答模式化 | 简单问答系统 |
| DPO模型 | 回答更符合人类偏好 | 过度礼貌,信息量下降 | 客服类应用 |
意外发现:基础预训练模型在实际医疗问答中表现最好,SFT和DPO反而降低了实用性。
5.2 关键经验教训
-
小模型的局限性:
- 8M参数难以同时存储知识和对话模式
- 微调容易导致"灾难性遗忘"
- 解决方案:要么增大模型,要么采用RAG架构
-
硬件限制的应对策略:
- 使用checkpoint保存中间结果
- 调整batch_size防止OOM
- 耐心等待CPU训练完成
-
数据质量的重要性:
- 预训练数据质量直接影响最终效果
- 微调数据需要多样化
- 偏好数据需要精心设计
5.3 改进方向
基于这次实践,我总结了几点改进方向:
-
参数效率优化:
- 尝试LoRA等参数高效微调方法
- 使用量化技术减小模型尺寸
-
架构改进:
- 引入检索增强生成(RAG)
- 尝试混合专家(MoE)架构
-
训练策略优化:
- 采用课程学习策略
- 实验对比学习等新方法
6. 完整代码获取
本项目的完整代码已开源在GitHub:
https://github.com/Chacha-Bing/med_model_learning
仓库包含:
- 数据预处理脚本
- 模型训练代码
- 测试评估代码
- 预训练权重(部分)
对于想要复现或进一步开发的同学,建议:
- 先从小规模数据开始实验
- 逐步增加模型复杂度
- 关注硬件资源使用情况
这个项目虽然规模不大,但完整展示了大模型训练的核心流程和技术要点。在资源有限的情况下,通过合理的策略设计,我们仍然能够获得有实用价值的模型成果。
