大模型(LLM)领域,微调是一个关键步骤,它能让通用模型适应特定任务的需求。本文将深入探讨如何利用Google Colab的强大算力,对拥有70亿参数的Mistral 7B模型进行微调,使其具备自主玩Minecraft的能力,最终目标是打造一个AI 智能体。我们将详细解析微调过程中的关键技术,例如LoRAQLoRA等,以及如何针对Minecraft任务进行数据准备和模型优化。

1. Minecraft AI智能体:微调的动机与挑战

最初的想法是创建一个能够自主在Minecraft世界中生存和探索的AI 智能体,摆脱手动挖掘钻石的繁琐。这个设想并非异想天开,已经有相关的学术研究。实现这一目标的关键在于让LLM能够理解Minecraft的游戏指令,并根据游戏环境做出决策。微调模型,使其适应Minecraft的特定指令和环境,是实现这一目标的可行途径。挑战在于如何构建高质量的训练数据,以及如何优化模型使其具备足够的推理能力,完成复杂的Minecraft任务。

2. Mistral 7B:小而强大的选择

在众多大模型中,作者最终选择了Mistral 7B,而非参数量更大的LLaMA 13B。选择的原因在于Mistral 7B在某些基准测试中表现优于LLaMA,并且模型体积更小,更适合在Google Colab等资源有限的环境下进行实验。Mistral 7B的开源特性也为微调提供了便利。实践证明,小型大模型在资源受限的情况下,通过合理的微调也能达到令人满意的效果。

3. Google Colab环境搭建:准备微调的土壤

在开始微调之前,需要在Google Colab上搭建必要的环境。首先,需要安装必要的Python库,包括:

  • transformers:Hugging Face的核心库,提供大模型、分词器和生成API。
  • accelerate:抽象硬件细节,简化分布式训练。
  • bitsandbytes:实现8-bit/4-bit量化,降低VRAM和内存使用。
  • peft:Parameter-Efficient Fine-Tuning,允许仅训练部分参数,节省时间和GPU资源。例如,LoRA
  • trl:Transformers Reinforcement Learning,方便使用RLHF(Reinforcement Learning with Human Feedback)。
  • datasets:下载、流式处理和预处理数据集。

此外,还需要在Hugging Face上创建一个账户并生成一个访问令牌(Token),用于从Hugging Face Hub下载模型和上传微调后的模型。将Token添加到Google Colab的“Secrets”中,命名为HF_TOKEN,以便程序能够安全地访问Hugging Face Hub。

4. 数据准备:Minecraft指令的语言表达

为了让Mistral 7B理解Minecraft的指令,需要构建合适的训练数据集。作者设计了一种包含[TAREFA](任务)、[INVENTÁRIO](库存)和[AÇÃO](动作)三种Token的指令格式。例如:

{"text":"[TAREFA] 创建工作台 [INVENTÁRIO] {\"橡木原木\":6} [AÇÃO] 合成 木板"}

其中,[TAREFA]描述了需要完成的任务,[INVENTÁRIO]描述了当前库存,[AÇÃO]描述了应该执行的动作。这种结构化的数据格式有助于模型理解不同Token的含义,并建立任务、库存和动作之间的联系。

作者建议使用Snake_case或Kebab-case命名物品,以减少Token数量,并使用有效的JSON格式方便后续的数据解析。使用</s>(或Tokenizers的EOS Token)结束每一个条目,以避免上下文泄露。当数据集增大时,可以考虑使用.jsonl格式,其中包含一个名为"text"的字段,包含完整的字符串。虽然作者最初使用的是TXT文件,但后来切换到了JSONL文件。

训练数据的质量直接影响微调的效果,因此需要精心设计数据格式,并准备足够数量的训练样本。作者发现,大约一千个示例之后,模型才开始产生较为合理的结果。

5. Tokenizer:理解语言的桥梁

Tokenizer 是 大模型理解语言的关键组成部分。它负责将文本转换为模型可以处理的数字 Token。 在 微调 中,需要特别注意 Tokenizer 的配置,特别是当向模型添加新的特殊 Token 时。

tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=True, trust_remote_code=True)

special = {"additional_special_tokens": [
    "[TAREFA]", "[INVENTÁRIO]", "[AÇÃO]",
    "[ITEM]", "[QUANTIDADE]", "[MATERIAL]",
    "[FERRAMENTA]", "[CRAFT]", "[COLETAR]"
]}

tokenizer.add_special_tokens(special)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"

这段代码首先加载预训练模型的 Tokenizer,然后添加自定义的特殊 Token,例如 [TAREFA][INVENTÁRIO][AÇÃO]。 这些 Token 将帮助模型更好地理解 Minecraft 指令的结构。 tokenizer.pad_token = tokenizer.eos_token 设置填充 Token 与结束 Token 相同,这是一种常见的 微调 实践。tokenizer.padding_side = "right"指定填充位置在右侧。

6. QLoRA:在有限资源下的高效微调

QLoRA (Quantization-aware Low-Rank Adaptation) 是一种参数高效的微调技术,它通过量化模型权重并引入低秩适配器来实现。 这种技术可以在不显著降低模型性能的情况下显著减少 VRAM 的使用。

bnb_cfg = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True
)

model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=bnb_cfg,
    device_map="auto",
    trust_remote_code=True,
)

model.resize_token_embeddings(len(tokenizer))

这段代码使用 BitsAndBytesConfig 配置 QLoRA。load_in_4bit=True 启用 4-bit 量化, bnb_4bit_quant_type="nf4" 指定量化类型为 NF4, bnb_4bit_compute_dtype=torch.bfloat16 指定计算数据类型为 bfloat16, bnb_4bit_use_double_quant=True 启用双重量化。 AutoModelForCausalLM.from_pretrained 加载量化后的模型, device_map="auto" 自动将模型分配到可用的 GPU 设备上。最后,model.resize_token_embeddings(len(tokenizer)) 调整模型的嵌入层大小,以匹配新添加的特殊 Token。 如果没有添加新的 Token,则不需要调整。

7. LoRA:精确调整模型的能力

LoRA (Low-Rank Adaptation) 是一种参数高效的微调技术,它通过在预训练模型的关键层中注入可训练的低秩矩阵来实现。 这种方法可以显著减少需要训练的参数数量,从而降低计算成本和内存需求。

lora_config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj"
    ],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)

model = get_peft_model(model, lora_config)
print("🔧 Lora Config Applied:")
model.print_trainable_parameters()
  • r=16:LoRA 的秩,控制可训练参数的数量。 较高的秩会增加模型的表达能力,但也需要更多的 VRAM 和训练时间。
  • lora_alpha=32:LoRA 的缩放因子。 输出的 LoRA 适配器乘以一个 lora_alpha / r 的系数。将 lora_alpha 设置为 r 的两倍是一种常见的做法,这有助于避免新信息被冻结模型的原始权重所掩盖。
  • target_modules=[...]:指定要在其中注入 LoRA 适配器的模型层。 这些层通常是 Transformer 块内的注意力和前馈网络中的线性层。
  • lora_dropout=0.05:应用于 LoRA 适配器的 dropout 概率。这有助于防止过拟合。
  • bias="none":指定是否训练偏差参数。将其设置为 "none" 可以减少需要训练的参数数量。
  • task_type="CAUSAL_LM":指定任务类型为因果语言建模。

该代码使用 LoraConfig 配置 LoRA。r=16 设置 LoRA 的秩, lora_alpha=32 设置缩放因子, target_modules 指定要注入 LoRA 适配器的目标模块。 最后,get_peft_model(model, lora_config) 将 LoRA 适配器添加到模型中, model.print_trainable_parameters() 打印可训练参数的数量。

8. 训练过程:步步为营的优化

微调 过程中,作者使用了自定义的 ValidationCallbackV03 回调函数,该函数在每个训练步骤之后评估模型在关键任务上的性能。 如果模型开始在基本任务上表现不佳,则回调函数将停止训练,以防止灾难性遗忘。

class ValidationCallbackV03(TrainerCallback):
    def __init__(self, tokenizer):
        self.tokenizer = tokenizer
        self.best_base_rate = 0
        self.valid_count= 0

    def on_evaluate(self, args, state, control, model, **kwargs):
        if state.global_step > args.warmup_steps and state.global_step % 75 == 0:
            self.valid_count += 1
            print(f"\n🧪 Validation v0.3 #{self.valid_count} - Step {state.global_step}")
            print("=" * 60)
            base_rate = self.validate_model(model)

            if self.valid_count > 2:
                if base_rate < 45 and self.best_base_rate > 70:
                    print("🛑 Regression Detected! Stopping...")
                    control.should_stop = True
                elif base_rate < 25:
                    print("🛑 Critical Regression Detected! Stopping...")
                    control.should_stop = True

            self.best_base_rate = max(self.best_base_rate, base_rate)

    def validate_model (self, model):
        CRITICAL_TESTS = [
            # Basic Tests
            ("创建 木镐", {"木板": 3, "木棍": 2}, "合成 木镐", "basic"),
            ("创建 床", {"木板": 3, "羊毛": 3}, "合成 床", "basic"),
            ("创建 铁剑", {"铁锭": 2, "木棍": 1}, "合成 铁剑", "basic"),
            ("获取 铁锭", {"粗铁矿": 5, "煤炭": 2, "熔炉": 1}, "熔炼 粗铁矿", "basic"),
            ("创建 工作台", {"橡木原木": 3}, "合成 木板", "basic"),
            # Reasoning Tests
            ("创建 铁镐", {"木棍": 2}, "获取 铁锭", "reasoning"),
            ("创建 钻石剑", {"木棍": 1}, "采集 钻石", "reasoning"),
            ("创建 熔炉", {"圆石": 7}, "采集 圆石", "reasoning"),
        ]

        basic_fails = 0
        total_basic = 0
        reasoning_fails = 0
        total_reasoning = 0

        for task, inventory, expected, category in CRITICAL_TESTS:
            inventory_str = str(inventory).replace("'", '"')
            prompt = f"[TAREFA] {task} [INVENTÁRIO] {inventory_str} [AÇÃO]"
            inputs = self.tokenizer(prompt, return_tensors="pt", add_special_tokens=False)
            inputs = {k: v.to(model.device) for k, v in inputs.items()}

            with torch.no_grad():
                outputs = model.generate(
                    **inputs,
                    max_new_tokens=25,
                    do_sample=False,
                    pad_token_id=self.tokenizer.eos_token_id,
                    eos_token_id=self.tokenizer.eos_token_id,
                    repetition_penalty=1.05
                )

            new_tokens = outputs[0][inputs['input_ids'].shape[1]:]
            response = self.tokenizer.decode(new_tokens, skip_special_tokens=True)
            response = response.split('<|endoftext|>')[0].strip()
            right = expected in response
            status = "✅" if right else "❌"

            print(f"{status} {category.upper()}: {task}")
            print(f"   Expected: {expected}")
            print(f"   Response: {response}")

            if category == "basic":
                total_basic += 1
                if not right:
                    basic_fails += 1
            else:
                total_reasoning += 1
                if not right:
                    reasoning_fails += 1

        base_basic_rate = (total_basic - basic_fails) / total_basic * 100
        base_reason_rate = (total_reasoning - reasoning_fails) / total_reasoning * 100 if total_reasoning > 0 else 0.0

        print(f"\n📊 RESULTS v0.3:")
        print(f"   Basic Hit Rate: {base_basic_rate:.1f}% ({total_basic - basic_fails}/{total_basic})")
        print(f"   Reasoning Hit Rate: {base_reason_rate:.1f}% ({total_reasoning - reasoning_fails}/{total_reasoning})")
        print(f"   Best History Rate: {self.best_base_rate:.1f}%")

        return base_basic_rate

此外,作者还使用了 SFTConfigSFTTrainer 来简化 微调 过程。 SFTConfig 用于配置训练参数,例如学习率、批量大小和训练周期数。 SFTTrainer 用于管理训练循环并保存 微调 后的模型。

作者选择按步骤进行验证,而不是按 Epoch 进行验证,这在训练大型语言模型时可能更有效。 通过每 50 步进行评估,可以快速获得有关进度的反馈并捕获最佳检查点。

training_args = SFTConfig(
    output_dir=OUTPUT_DIR,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=6,
    learning_rate=8e-5,
    bf16=True,
    tf32=True,
    num_train_epochs=15,
    save_steps=50,
    eval_steps=50,
    logging_steps=15,
    save_strategy="steps",
    eval_strategy="steps",
    warmup_steps=125,
    load_best_model_at_end=True,
    metric_for_best_model="eval_loss",
    greater_is_better=False,
    save_total_limit=4,
    warmup_ratio=0.18,
    lr_scheduler_type="cosine",
    report_to="none",
    dataloader_pin_memory=False,
    remove_unused_columns=False,
    prediction_loss_only=True,
    packing=False,
    max_seq_length=512,
    dataloader_num_workers=4,
    group_by_length=True,
    weight_decay=0.02,
    max_grad_norm=0.5
)

trainer = SFTTrainer(
    model=model,
    train_dataset=tokenized["train"],
    eval_dataset=tokenized["test"],
    peft_config=lora_config,
    args=training_args,
    processing_class = tokenizer,
    callbacks=[validation_callback]
)

9. 模型保存与评估:检验微调的成果

训练完成后,需要保存 微调 后的模型和 Tokenizer。 这将允许您在以后加载模型并将其用于 Minecraft 智能体推理。

trainer.model.save_pretrained(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)

作者还建议保存训练配置,以便以后可以重现训练过程。

最后,使用自定义的 compare_tokenizer() 函数评估 Tokenizer 的质量和效率。 此函数将测试几个示例,并打印 Token 的数量、每个 Token 的字符数以及 Token 化过程是否可逆。

10. 结论与展望:AI Steve的未来

本文详细介绍了如何在 Google Colab 上 微调 Mistral 7B 大模型,并将其应用于 Minecraft 智能体。 通过使用 QLoRA、LoRA 和自定义的回调函数,可以在资源有限的环境中有效地训练模型。 虽然作者的最终目标是创建一个能够自主玩 Minecraft 的 AI 智能体,但本文中介绍的技术和概念可以应用于广泛的其他任务。

虽然微调可以提升模型在特定任务上的性能,但更大的模型可能更适合使用Prompt工程技术。 在 Minecraft Malmo 项目的上下文中,使用 OpenAI API 或 Qwen-30B-A3B 等模型可能比训练模型更好。

未来的工作可能包括探索不同的训练策略、改进训练数据集以及将 微调 后的模型集成到 Minecraft 游戏中。 也许在下一个教程中,作者就可以用他们的 AI Steve 挖掘一些钻石。