案例: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 这个数据集,最主要的原因是它专为训练大语言模型玩狼人杀而设计,和你想要训练一个“策略分析”模型的目标非常匹配。简单来说,它解决了训练这类模型时最头疼的三个问题:

  1. 数据与目标高度对齐:它不是一个普通的对话数据集,而是直接源自真实的狼人杀对局。
  2. 自带“思考-行动”链路:它包含了从角色推理、策略思考到具体行动(发言、投票、夜间行动)的完整过程。
  3. 数据质量高且结构清晰:它包含了经过整理的、用于监督微调(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.safetensorspytorch_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元巨款,经费已经不够了,耗不起更多时间花不起更多的钱租算力,所以就没有尝试。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐