【微调】(案例)SpeedWolf 狼人杀策略分析模型
案例:SpeedWolf 狼人杀策略分析模型
准备 LLaMaFactory
安装过程已经在上一章描述过了,本章节不再赘述,按照以下方法执行即可:
git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
pip install -e ".[torch,metrics]" -i https://mirrors.aliyun.com/pypi/simple/
pip uninstall torch torchvision torchaudio -y
pip install --pre torch torchvision torchaudio --index-urlhttps://download.pytorch.org/whl/nightly/cu132 # 注意一定要下载与自己的 CUDA 兼容的版本,要不然会报错
如果说显存不够,或者配置环境麻烦,推荐租赁 AutoDL 服务器,选择单卡 RTX 4090,镜像选择 hiyouga/LLaMA-Factory/LLaMA-Factory:v6 即可。
目录说明:
| 目录名 | 性质 | 速度 | 备注 |
|---|---|---|---|
/ |
系统盘 | 中 | 实例关机数据不会丢失,可存放代码等。会随保存镜像一起保存。 |
/root/autodl-tmp |
数据盘 | 快 | 实例关机数据不会丢失,可存放读写IO要求高的数据。但不会随保存镜像一起保存 |
/root/autodl-fs |
文件存储 | 中 | 可以实现多实例间的文件同步共享,不受实例开关机和保存镜像的影响。 |
一般下载模型在/root/autodl-tmp,执行以下命令可以下载模型
curl -LsSf https://hf.co/cli/install.sh | bash
hf auth login
hf download Qwen/Qwen3-0.6B
hf download 命令支持多种参数,方便你灵活控制下载行为。
| 参数 | 说明 | 示例 |
|---|---|---|
--local-dir |
将模型下载到指定的本地文件夹,而非缓存目录。 | hf download Qwen/Qwen3-0.6B --local-dir /root/autodl-tmp/Qwen3-0.6B |
--repo-type |
下载其他类型的仓库,如数据集(dataset)或空间(space),默认为模型(model)。 |
hf download username/dataset-name --repo-type dataset |
--include/--exclude |
使用通配符模式仅下载或排除特定文件,例如只下载 .safetensors 格式的模型文件。 |
hf download gpt2 --include "*.safetensors" |
--revision |
下载特定版本,如分支名、标签名或提交哈希值。 | hf download fffiloni/zeroscope --revision=refs/pr/78 |
--token |
在非交互式环境中,直接指定Token进行认证。 | hf download private/model --token hf_xxxxx |
数据集处理
首先,我们从 Hugging Face 上下载 ReneeYe/werewolf_game_reasoning 数据集
powershell -ExecutionPolicy ByPass -c "irm https://hf.co/cli/install.ps1 | iex"
hf download ReneeYe/werewolf_game_reasoning --repo-type=dataset --local-dir ./wolf_ds
cd wolf_ds/process_script
选择 ReneeYe/werewolf_game_reasoning 这个数据集,最主要的原因是它专为训练大语言模型玩狼人杀而设计,和你想要训练一个“策略分析”模型的目标非常匹配。简单来说,它解决了训练这类模型时最头疼的三个问题:
- 数据与目标高度对齐:它不是一个普通的对话数据集,而是直接源自真实的狼人杀对局。
- 自带“思考-行动”链路:它包含了从角色推理、策略思考到具体行动(发言、投票、夜间行动)的完整过程。
- 数据质量高且结构清晰:它包含了经过整理的、用于监督微调(SFT)的高质量指令数据,便于直接使用。
下载完成后,需要使用 PyCharm 打开这个文件夹,然后安装必要的依赖
pip install tqdm pyarrow pandas -i https://mirrors.aliyun.com/pypi/simple/
以下是这个项目的目录:
你的工作目录/
│
├── werewolf_game_reasoning/ # 克隆下来的原始仓库
│ ├── raw/ # 原始游戏数据
│ │ ├── train/ # 训练集
│ │ │ ├── 7_player_game/ # 7人局
│ │ │ │ ├── seer_guard/ # 预言家+守卫组合
│ │ │ │ │ ├── game_1/
│ │ │ │ │ │ ├── event.json
│ │ │ │ │ │ └── note.json
│ │ │ │ │ └── ...
│ │ │ │ └── seer_witch/ # 预言家+女巫组合
│ │ │ └── 9_player_game/ # 9人局
│ │ │ ├── guard_witch_seer/
│ │ │ └── hunter_witch_seer/
│ │ └── test/ # 测试集(结构类似)
│ ├── process_script/ # 官方处理脚本
│ │ ├── process_data.py # ⭐ 核心脚本
│ │ └── csv_to_parquet.py
│ └── README.md
然后运行脚本,处理以下数据:
# 训练数据处理
python process_script/process_data.py --read_path './raw/train/7_player_game/seer_guard','./raw/train/7_player_game/seer_witch','./raw/train/9_player_game/guard_witch_seer','./raw/train/9_player_game/hunter_witch_seer' --language zh --save_path ./game_behavior --add_rolepred True
# 测试数据处理
python process_script/process_data.py --read_path './raw/test/7_player_game/seer_guard','./raw/test/7_player_game/seer_witch','./raw/test/9_player_game/guard_witch_seer','./raw/test/9_player_game/hunter_witch_seer' --language zh --save_path ./test --add_rolepred True
然后在根目录中生成一个game_behavior文件夹。就说明这一步成功了。
如果没有成功,有可能是编码集的问题。
修改前(第147行附近):
with open(event_path, 'r') as f:
event = json.load(f)
修改后:
with open(event_path, 'r', encoding='utf-8') as f:
event = json.load(f)
同样修改 note.json 的读取(大约在155行附近):
with open(note_path, 'r', encoding='utf-8') as f:
note = json.load(f)
执行成功后:在项目根目录创建一个新文件 convert_to_llamafactory.py:
import pandas as pd
import json
import os
from pathlib import Path
def convert_csv_to_alpaca(csv_path, output_path, task_type):
"""
将CSV转换为LLaMA-Factory的Alpaca格式
Args:
csv_path: CSV文件路径
output_path: 输出JSON文件路径
task_type: 任务类型 (speech/action/vote/role_pred)
"""
print(f"📊 正在处理 {task_type} 数据...")
# 读取CSV
df = pd.read_csv(csv_path, encoding='utf-8')
print(f" 读取到 {len(df)} 条样本")
# 查看列名(调试用)
print(f" 列名: {df.columns.tolist()}")
alpaca_data = []
for idx, row in df.iterrows():
# 根据实际的列名调整
# 常见的列名:instruction, input, output, context, response 等
# 尝试不同的列名组合
instruction = row.get('instruction', row.get('prompt', row.get('task', '')))
input_text = row.get('input', row.get('context', row.get('situation', '')))
output_text = row.get('output', row.get('response', row.get('answer', '')))
# 如果这些列都不存在,尝试使用其他策略
if not instruction and not input_text and not output_text:
# 可能数据在别的列中,这里根据实际情况调整
# 例如,可能是 'speech' 列作为output
if 'speech' in df.columns:
output_text = row.get('speech', '')
instruction = "请根据游戏局势生成发言"
input_text = f"当前游戏状态: {row.get('context', '')}"
elif 'action' in df.columns:
output_text = row.get('action', '')
instruction = "请决定你的游戏行动"
input_text = f"当前游戏状态: {row.get('context', '')}"
# 构建Alpaca格式
alpaca_item = {
"instruction": str(instruction) if instruction else "",
"input": str(input_text) if input_text else "",
"output": str(output_text) if output_text else ""
}
alpaca_data.append(alpaca_item)
# 保存为JSON
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(alpaca_data, f, ensure_ascii=False, indent=2)
print(f"✅ 转换完成!保存到: {output_path}")
print(f" 共 {len(alpaca_data)} 条样本\n")
return alpaca_data
def main():
# 输入输出路径
base_dir = "./game_behavior"
output_dir = "./llamafactory_data"
# 创建输出目录
os.makedirs(output_dir, exist_ok=True)
# 要处理的CSV文件列表
csv_files = [
{"name": "speech", "path": f"{base_dir}/speech.csv", "output": f"{output_dir}/werewolf_speech.json"},
{"name": "action", "path": f"{base_dir}/action.csv", "output": f"{output_dir}/werewolf_action.json"},
{"name": "vote", "path": f"{base_dir}/vote.csv", "output": f"{output_dir}/werewolf_vote.json"},
]
# 如果有role_pred.csv也加上
if os.path.exists(f"{base_dir}/role_pred.csv"):
csv_files.append({"name": "role_pred", "path": f"{base_dir}/role_pred.csv", "output": f"{output_dir}/werewolf_role_pred.json"})
# 转换所有文件
all_data = []
for csv_file in csv_files:
try:
data = convert_csv_to_alpaca(csv_file["path"], csv_file["output"], csv_file["name"])
all_data.extend(data)
except Exception as e:
print(f"❌ 处理 {csv_file['name']} 失败: {e}")
# 合并所有数据为一个文件(可选)
if all_data:
combined_path = f"{output_dir}/werewolf_combined.json"
with open(combined_path, 'w', encoding='utf-8') as f:
json.dump(all_data, f, ensure_ascii=False, indent=2)
print(f"📦 合并所有数据: {combined_path} ({len(all_data)} 条)")
print("\n🎉 所有转换完成!")
if __name__ == "__main__":
main()
然后生成了一个llamafactory_data文件夹,以下就是我们需要的数据。我们把这几个 json 文件直接放入 llamafactory 项目下的 data 文件夹。
然后在这个文件夹里编辑一个 dataset_info.json 文件,添加这几个字典:
{
"werewolf_speech": {
"file_name": "werewolf_speech.json",
"columns": {
"prompt": "instruction",
"query": "input",
"response": "output"
}
},
"werewolf_action": {
"file_name": "werewolf_action.json",
"columns": {
"prompt": "instruction",
"query": "input",
"response": "output"
}
},
"werewolf_vote": {
"file_name": "werewolf_vote.json",
"columns": {
"prompt": "instruction",
"query": "input",
"response": "output"
}
},
"werewolf_combined": {
"file_name": "werewolf_combined.json",
"columns": {
"prompt": "instruction",
"query": "input",
"response": "output"
}
}
}
至此,数据处理完毕
微调阶段
首先我们打开 WebUI:
set HF_ENDPOINT=https://hf-mirror.com # 配置镜像
llamafactory-cli webui
然后我们配置这些参数:
# 模型配置
model_name_or_path: /root/autodl-tmp/Qwen3.5-4B-Thinking
template: qwen3_5
# 微调方法
finetuning_type: lora
# LoRA 参数
lora_rank: 32
lora_alpha: 64
lora_dropout: 0.05
loraplus_lr_ratio: 16
# 数据集配置
dataset:
- werewolf_combined
- werewolf_speech
dataset_dir: /root/LLaMA-Factory/data
cutoff_len: 2048
validation_split_percentage: 1
max_samples: 10000
# 训练参数
stage: sft # Supervised Fine-Tuning
do_train: true
finetuning_type: lora
# 优化器配置
learning_rate: 5.0e-5
num_train_epochs: 3.0
lr_scheduler_type: cosine
warmup_ratio: 0.1
optimizer: adamw_torch
# 梯度配置
gradient_accumulation_steps: 1024
per_device_train_batch_size: 2
per_device_eval_batch_size: 2
max_grad_norm: 0.75
# 精度配置
fp16: true
bf16: false
# 加速方式
ddp_timeout: 180000000
# 其他参数
logging_steps: 10
save_steps: 100
eval_steps: 100
save_total_limit: 3
plot_loss: true
# 输出目录
output_dir: ./output
为什么选择这些参数呢:
| 参数 | 数值 | 一句话理由 |
|---|---|---|
| 学习率 | 5e-5 | LoRA标准起步价 |
| 训练轮数 | 3 | 够用就行,防过拟合 |
| 批次大小 | 2 | 显存有限,保守点 |
| 梯度累积 | 1024 | 等效batch=2048,稳 |
| 截断长度 | 2048 | 模型原生长度 |
| LoRA秩 | 16 | 复杂度适中 |
| LoRA缩放 | 32 | 常规2倍配比 |
| LoRA丢弃 | 0.05 | 轻微正则化 |
然后点击开始训练即可进行训练流程。
这里注意一下,由于我们选择了 Qwen3.5-4B-Thinking,40亿的参数训练出来的结果一定是不尽人意,所以如果说想要训练更好的模型,就要选择更高参数的基础模型,还有更高的算力,使用的参数一定也不一样。因为作者名字里带兔,那么有句古话说的好,RabbitMQ(兔子没钱),所以我就不租用那么高的算力了,我们能看出来这个模型训练有效就算完成学习。
如果不想使用 WebUI,那么将以上配置写在 train_werewolf.yaml,然后执行:
set HF_ENDPOINT=https://hf-mirror.com # 配置镜像
llamafactory-cli train train_werewolf.yaml
然后就是漫长的等待…整个过程需要数几个小时
验证模型
训练完成后,找到对应的 Checkpoint 文件夹
以上面的 hiyouga/LLaMA-Factory/LLaMA-Factory:v6 镜像为例,这个路径一般在 /root/LLaMA-Factory/src/saves/Qwen3.5-4B-Thinking/lora/train_日期/checkpoint-训练次数
复制路径,写在 WebUI 的 检查点路径 参数里面。然后选择 Chat 模式,点击加载模型,验证这个模型即可。
模型导出
在LLaMA Factory中使用llamafactory-cli export命令或通过WebUI的"Export"标签页完成。导出后的目录结构和普通的Hugging Face模型一致:
config.json:模型的配置文件。tokenizer.json/tokenizer_config.json:分词器文件。model.safetensors或pytorch_model.bin:完整的模型权重文件(如果模型较大,可能会被分割成多个文件,如model-00001-of-00002.safetensors)。generation_config.json:生成文本时的默认配置。
导出模型后,我们就可以使用 vLLM 等工具做部署了。
微调中遇到的问题
问题一:训练卡住
- 训练过程中,CPU占用率降至0%,CUDA使用率也降至0%
- 进程未退出,但无任何进度输出
- 卡住时间点:第一个epoch开始时 / 某个epoch结束后的数据加载阶段
解决方法:直接 Ctrl + C 停止进程,然后清除显存,从检查点恢复,重新训练
问题二:训练后模型输出异常
- 询问“守卫第一天怎么发言”,模型回答:“直接起跳守卫然后干掉女巫”
- 该回答违背狼人杀基本常识,因为守卫是防御型角色,不应攻击队友女巫。而且第一天起跳守卫的行为就给狼人很多信息了,所以这就是典型的欠拟合
欠拟合的情况需要用更高参数的模型、增加 Batch Size 和梯度累计值、减少正则化参数等策略。
相反的,过拟合的表现就是不懂得变通了,换句话说模型压根就没有学会推理,而是记住了训练集里的高频话术。
举个例子,狼人杀中有个常见的错误的玩法叫“位置学”(比如认为边角位必出狼)。过拟合的模型会放大这种偏差。模型会严重依赖玩家编号来下判断,而不是听发言内容。比如,只要3号玩家发言,模型就本能地投3号,因为在训练集里3号当狼的次数多。
此时我们应该调低他的学习率,调低训练轮次,调低 LoRA Rank,调高 LoRA Dropout。并且增加正则化的权重衰减。
过拟合这个地方作者没有尝试继续微调。老话说的好,RabbitMQ(兔子没钱)。兔子为了学习微调,花了100元巨款,经费已经不够了,耗不起更多时间花不起更多的钱租算力,所以就没有尝试。
更多推荐
所有评论(0)