Skip to content

进阶教程:基于 ModelScope 本地部署 Qwen3-4B 并使用诗词数据集进行模型训练,从数据准备到效果对比的完全指南

约 11568 字大约 39 分钟

AIQwen3大语言模型LoRA微调

2026-01-13

关于 ModelScope: ModelScope 社区是一个模型开源社区及创新平台,由阿里巴巴通义实验室,联合 CCF 开源发展技术委员会,共同作为项目发起创建。(摘自官方文档)

前言

本教程基于魔搭社区(ModelScope)平台,详细讲解如何使用 Qwen3-4B 模型进行诗词生成任务的 LoRA 微调训练。从数据集下载、模型训练、到效果对比评估,覆盖完整实战流程。通过本教程,您将掌握:

  • 如何下载和准备训练数据集
  • LoRA 微调的基本原理和实践
  • Base 模型与 Instruct 模型的关键区别
  • 训练过程中常见问题的解决方案
  • 如何科学地对比评估不同模型的效果

适用场景: 大语言模型微调、诗词生成、文本生成等自然语言处理任务。

环境要求:

  • Python 3.9+
  • CUDA 支持的 GPU (推荐 8GB+ 显存)
  • Windows/Linux 系统

本文档为进阶教程,请先参考文档 从零开始:基于 ModelScope 本地部署 Qwen3-4B 大语言模型完全指南,完成 Qwen3-4B 大模型的本地部署,并掌握基础知识。该模型在本文档中称为基础版模型。


一、环境准备

前置要求: 请确保已完成基础文档中的环境配置,包括 Python、ModelScope、PyTorch(CUDA 版)、Transformers 等核心库的安装。

1.1 安装额外依赖

本教程需要额外安装以下用于模型训练的专用库:

# LoRA 微调核心库
pip install peft datasets -i https://mirrors.aliyun.com/pypi/simple/

# 数据处理和训练监控工具
pip install pandas tensorboard -i https://mirrors.aliyun.com/pypi/simple/

新增依赖说明:

  • peft: 参数高效微调库,提供 LoRA 微调实现(核心)
  • datasets: Hugging Face 数据集处理库,用于加载和预处理训练数据
  • pandas: 数据处理库,用于 CSV 转 JSON 格式转换
  • tensorboard: 训练日志可视化工具,实时监控训练过程

说明: 基础环境中已安装的 modelscopetransformerstorch 等库无需重复安装。

1.2 下载模型

由于基础文档已经完成了 Qwen3-4B 的下载,本教程额外需要使用 Qwen3-4B-Instruct-2507(指令版)模型进行训练,所以需要下载指令版模板至对应目录。

D:                          # 切换到 D 盘(如果已在 D 盘可跳过)
cd d:\wwwroot\modelscope
modelscope download --model Qwen/Qwen3-4B-Instruct-2507 --local_dir ./models/Qwen3-4B-Instruct-2507

说明: d:\wwwroot\modelscope 是本教程的示例路径,请替换成你自己的项目目录。如果 CMD 当前不在 D 盘,需要先切换盘符。输入 D: 回车即可切换到 D 盘。

1.3 项目结构

d:/wwwroot/modelscope/
├── models/                          # 模型目录
│   ├── Qwen3-4B/                   # 基础版模型
│   └── Qwen3-4B-Instruct-2507/     # 指令版模型
├── datasets/                        # 数据集目录
│   └── chinese-poetry/
│       └── train.json              # 训练数据
├── output/                          # 训练输出
│   ├── qwen3-poetry/               # 基础版输出
│   └── qwen3-poetry-instruct/      # 指令版输出
├── download_dataset_simple.py       # 数据集下载脚本
├── train.py                         # 基础版训练脚本
├── train_instruct.py                # 指令版训练脚本
├── text_trained.py                  # 基础版测试脚本
├── text_trained_instruct.py         # 指令版测试脚本
└── text_instruct_original.py        # 原始指令版测试脚本

二、数据集下载

2.1 脚本说明

使用 download_dataset_simple.py 从魔搭社区下载中文诗词数据集。

核心功能:

  • 从 ModelScope 直接下载 CSV 格式数据集
  • 自动转换为 JSON 格式
  • 保存到本地 ./datasets/chinese-poetry/ 目录

2.2 代码结构

"""简化版数据集下载脚本 - 直接从网页下载 CSV

如果 MsDataset.load() 出现路径问题,使用此脚本直接下载 CSV 文件

使用方法:
    python download_dataset_simple.py
"""

import os
import pandas as pd
import json

# 数据集直接下载链接(从 ModelScope 网页获取)
TRAIN_URL = "https://www.modelscope.cn/api/v1/datasets/modelscope/chinese-poetry-collection/repo?Revision=master&FilePath=train.csv"
TEST_URL = "https://www.modelscope.cn/api/v1/datasets/modelscope/chinese-poetry-collection/repo?Revision=master&FilePath=test.csv"

SAVE_DIR = "./datasets/chinese-poetry"
os.makedirs(SAVE_DIR, exist_ok=True)

print("=" * 60)
print("简化版数据集下载")
print("=" * 60)
print("直接从 ModelScope 下载 CSV 文件")
print("=" * 60)
print()


def download_csv(url: str, split_name: str):
    """下载 CSV 并转换为 JSON"""
    print(f"[{split_name.upper()}] 正在下载...")

    try:
        # 读取 CSV
        df = pd.read_csv(url)
        print(f"✓ 下载完成,共 {len(df)} 条数据")

        # 转换为字典列表
        data_list = df.to_dict("records")

        # 保存为 JSON
        save_path = os.path.join(SAVE_DIR, f"{split_name}.json")
        with open(save_path, "w", encoding="utf-8") as f:
            json.dump(data_list, f, ensure_ascii=False, indent=2)

        print(f"✓ 已保存到: {save_path}")
        print()

        # 显示数据样例
        if len(data_list) > 0:
            print("数据样例:")
            print(json.dumps(data_list[0], ensure_ascii=False, indent=2))
            print()

        return len(data_list)

    except Exception as e:
        print(f"✗ 下载失败: {e}")
        return 0


try:
    # 下载训练集
    train_count = download_csv(TRAIN_URL, "train")

    # 下载测试集
    test_count = download_csv(TEST_URL, "test")

    # 统计
    print("=" * 60)
    print("下载完成!")
    print("=" * 60)
    print(f"训练集: {train_count} 条")
    print(f"测试集: {test_count} 条")
    print(f"总计: {train_count + test_count} 条")
    print()
    print("下一步:")
    print("  python train.py")
    print("=" * 60)

except Exception as e:
    print(f"\n发生错误: {e}")
    print("\n如果网络下载失败,请手动下载:")
    print(
        "1. 访问: https://modelscope.cn/datasets/modelscope/chinese-poetry-collection"
    )
    print("2. 点击 Files → 下载 train.csv 和 test.csv")
    print(f"3. 放到目录: {SAVE_DIR}")

2.3 运行方式

python download_dataset_simple.py

2.4 预期输出

============================================================
简化版数据集下载
============================================================
直接从 ModelScope 下载 CSV 文件
============================================================

[TRAIN] 正在下载...
✓ 下载完成,共 388599 条数据
✓ 已保存到: ./datasets/chinese-poetry/train.json

[TEST] 正在下载...
✓ 下载完成,共 10000 条数据
✓ 已保存到: ./datasets/chinese-poetry/test.json

============================================================
下载完成!
============================================================
训练集: 388599 条
测试集: 10000 条
总计: 398599 条

下一步:
  python train.py
============================================================

2.5 常见问题

问题 1: 网络下载失败

✗ 下载失败: [网络错误]

解决方案:

  1. 检查网络连接
  2. 手动下载:
    • 访问 https://modelscope.cn/datasets/modelscope/chinese-poetry-collection
    • 点击 Files → 下载 train.csv 和 test.csv
    • 放到 ./datasets/chinese-poetry/ 目录
    • 重新运行脚本进行格式转换

三、基础版模型训练

3.1 脚本说明

使用 train.py 对 Qwen3-4B 基础版模型进行 LoRA 微调。

训练目标: 让基础版模型学会创作古诗词

3.2 完整训练脚本

"""模型训练脚本 - 使用 LoRA 微调大语言模型

功能:
1. 加载预训练模型和数据集
2. 配置 LoRA 训练参数
3. 执行训练并保存 LoRA 权重
4. 支持断点续训和多卡训练

使用方法:
    python train.py

训练完成后,LoRA 权重会保存在 OUTPUT_DIR 目录
"""

import os
from dataclasses import dataclass, field
from typing import Optional

from modelscope import AutoModelForCausalLM, AutoTokenizer
from modelscope.msdatasets import MsDataset
from transformers import TrainingArguments, Trainer
from peft import LoraConfig, get_peft_model, TaskType
import torch

# ==================== 训练配置 ====================
# 模型配置
MODEL_PATH = "d:/wwwroot/modelscope/models/Qwen3-4B"  # 本地模型路径
MODEL_TYPE = "qwen3"  # 模型类型

# 数据集配置
# DATASET_NAME = "modelscope/chinese-poetry-collection"  # 数据集名称(在线下载)
# 使用本地已下载的数据集:
DATASET_NAME = "./datasets/chinese-poetry/train.json"

# 输出配置
OUTPUT_DIR = "./output/qwen3-poetry"  # 训练输出目录
LOGGING_DIR = "./output/qwen3-poetry/logs"  # 日志目录

# 断点续训配置
RESUME_FROM_CHECKPOINT = True  # 第一次训练设为 False,之后改为 True

# LoRA 参数
LORA_R = 64  # LoRA 秩,越大参数越多,效果越好但训练越慢
LORA_ALPHA = 128  # LoRA 缩放系数,通常设为 r 的 2 倍
LORA_DROPOUT = 0.05  # Dropout 比例,防止过拟合

# 训练超参数
NUM_EPOCHS = 1  # 训练轮数 (快速测试用1轮)
BATCH_SIZE = 2  # 批次大小
GRADIENT_ACCUMULATION_STEPS = 8  # 梯度累积步数
LEARNING_RATE = 1e-4  # 学习率
MAX_LENGTH = 256  # 最大序列长度
WARMUP_RATIO = 0.1  # 预热比例
SAVE_STEPS = 100  # 每隔多少步保存一次
LOGGING_STEPS = 10  # 每隔多少步记录一次日志

# 其他配置
USE_FP16 = True  # 使用混合精度训练(推荐)
GRADIENT_CHECKPOINTING = True  # 启用梯度检查点(节省显存)
# ==================================================

print("=" * 60)
print("模型训练脚本 - LoRA 微调")
print("=" * 60)
print(f"模型路径: {MODEL_PATH}")
print(f"数据集: {DATASET_NAME}")
print(f"输出目录: {OUTPUT_DIR}")
print(f"训练轮数: {NUM_EPOCHS}")
print(f"学习率: {LEARNING_RATE}")
print(f"LoRA 秩: {LORA_R}")
print("=" * 60)
print()


def load_dataset(dataset_name: str):
    """加载数据集"""
    print(f"[1/5] 正在加载数据集: {dataset_name}")

    try:
        # 判断是否是本地文件
        if dataset_name.endswith(".json"):
            # 加载本地 JSON 文件
            import json
            from datasets import Dataset

            with open(dataset_name, "r", encoding="utf-8") as f:
                data = json.load(f)

            dataset = Dataset.from_list(data)

            # 快速测试: 使用前 20000 条数据
            if len(dataset) > 20000:
                print(f"⚠️  快速测试模式: 原数据 {len(dataset)} 条, 使用前 20000 条")
                dataset = dataset.select(range(20000))

            print(f"✓ 数据集加载成功,共 {len(dataset)} 条数据")
            return dataset
        else:
            # 尝试从 ModelScope 加载
            dataset = MsDataset.load(dataset_name, split="train")
            print(f"✓ 数据集加载成功,共 {len(dataset)} 条数据")
            return dataset
    except Exception as e:
        print(f"✗ 加载失败: {e}")
        print("提示: 检查数据集名称是否正确,或使用本地数据集路径")
        return None


def load_model_and_tokenizer(model_path: str):
    """加载模型和分词器"""
    print(f"\n[2/5] 正在加载模型: {model_path}")

    # 加载分词器
    tokenizer = AutoTokenizer.from_pretrained(
        model_path, trust_remote_code=True, padding_side="right"
    )

    # 设置 pad_token
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token

    # 加载模型
    model = AutoModelForCausalLM.from_pretrained(
        model_path,
        torch_dtype=torch.float16 if USE_FP16 else torch.float32,
        device_map="auto",
        trust_remote_code=True,
    )

    # 启用梯度检查点
    if GRADIENT_CHECKPOINTING:
        model.gradient_checkpointing_enable()

    print(f"✓ 模型加载成功")
    print(f"✓ 参数量: {sum(p.numel() for p in model.parameters()) / 1e9:.2f}B (十亿)")

    return model, tokenizer


def setup_lora(model):
    """配置 LoRA"""
    print(f"\n[3/5] 正在配置 LoRA")
    print(f"  - LoRA 秩 (r): {LORA_R}")
    print(f"  - LoRA Alpha: {LORA_ALPHA}")
    print(f"  - Dropout: {LORA_DROPOUT}")

    # LoRA 配置
    lora_config = LoraConfig(
        task_type=TaskType.CAUSAL_LM,
        r=LORA_R,
        lora_alpha=LORA_ALPHA,
        lora_dropout=LORA_DROPOUT,
        target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],  # Qwen3 的注意力层
        bias="none",
    )

    # 应用 LoRA
    model = get_peft_model(model, lora_config)

    # 打印可训练参数
    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"✓ LoRA 配置完成")
    print(f"  - 可训练参数: {trainable_params / 1e6:.2f}M")
    print(f"  - 总参数: {total_params / 1e9:.2f}B")
    print(f"  - 可训练比例: {100 * trainable_params / total_params:.2f}% (节省显存!)")

    return model


def preprocess_dataset(dataset, tokenizer):
    """预处理数据集"""
    print(f"\n[4/5] 正在预处理数据集")

    def tokenize_function(examples):
        # 检查数据集字段类型
        if "text1" in examples:
            # 纯文本数据集(如中文诗词)
            texts = examples["text1"]
        elif "instruction" in examples and "output" in examples:
            # 指令数据集
            texts = []
            for instruction, output in zip(examples["instruction"], examples["output"]):
                text = f"User: {instruction}\nAssistant: {output}"
                texts.append(text)
        elif "text" in examples:
            # 通用文本字段
            texts = examples["text"]
        else:
            # 其他格式,尝试获取第一个字段
            first_key = list(examples.keys())[0]
            texts = examples[first_key]

        # 分词
        tokenized = tokenizer(
            texts,
            truncation=True,
            max_length=MAX_LENGTH,
            padding="max_length",
            return_tensors="pt",
        )

        # 设置 labels (用于计算 loss)
        tokenized["labels"] = tokenized["input_ids"].clone()

        return tokenized

    # 应用预处理
    processed_dataset = dataset.map(
        tokenize_function,
        batched=True,
        remove_columns=dataset.column_names,
        desc="Tokenizing dataset",
    )

    print(f"✓ 数据集预处理完成")
    return processed_dataset


def train(model, tokenizer, train_dataset):
    """训练模型"""
    print(f"\n[5/5] 开始训练")

    # 训练参数
    training_args = TrainingArguments(
        output_dir=OUTPUT_DIR,
        num_train_epochs=NUM_EPOCHS,
        per_device_train_batch_size=BATCH_SIZE,
        gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS,
        learning_rate=LEARNING_RATE,
        warmup_ratio=WARMUP_RATIO,
        logging_dir=LOGGING_DIR,
        logging_steps=LOGGING_STEPS,
        save_steps=SAVE_STEPS,
        save_total_limit=3,  # 只保留最近 3 个检查点
        fp16=USE_FP16,
        gradient_checkpointing=GRADIENT_CHECKPOINTING,
        report_to="tensorboard",  # 使用 TensorBoard 记录日志
        load_best_model_at_end=False,
        remove_unused_columns=False,
    )

    # 创建 Trainer
    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=train_dataset,
        tokenizer=tokenizer,
    )

    # 开始训练
    print()
    print("-" * 60)
    print("训练开始...")
    print(
        f"总训练步数: {len(train_dataset) // (BATCH_SIZE * GRADIENT_ACCUMULATION_STEPS) * NUM_EPOCHS}"
    )
    print(f"检查点保存间隔: 每 {SAVE_STEPS} 步")
    if RESUME_FROM_CHECKPOINT:
        print(f"断点续训: 开启 (将从最新检查点续训)")
    print("-" * 60)
    print()

    trainer.train(
        resume_from_checkpoint=(
            RESUME_FROM_CHECKPOINT if RESUME_FROM_CHECKPOINT else None
        )
    )

    # 保存最终模型
    final_output_dir = os.path.join(OUTPUT_DIR, "final")
    trainer.save_model(final_output_dir)
    tokenizer.save_pretrained(final_output_dir)

    print()
    print("=" * 60)
    print("✓ 训练完成!")
    print(f"✓ LoRA 权重已保存到: {final_output_dir}")
    print("=" * 60)
    print()
    print("下一步:")
    print(f"1. 查看训练日志: tensorboard --logdir {LOGGING_DIR}")
    print(f"2. 使用训练后的模型: python text_trained.py")
    print()


def main():
    """主函数"""
    try:
        # 1. 加载数据集
        dataset = load_dataset(DATASET_NAME)
        if dataset is None:
            return

        # 2. 加载模型和分词器
        model, tokenizer = load_model_and_tokenizer(MODEL_PATH)

        # 3. 配置 LoRA
        model = setup_lora(model)

        # 4. 预处理数据集
        train_dataset = preprocess_dataset(dataset, tokenizer)

        # 5. 训练
        train(model, tokenizer, train_dataset)

    except KeyboardInterrupt:
        print("\n\n训练被用户中断")
    except Exception as e:
        print(f"\n\n训练出错: {e}")
        import traceback

        traceback.print_exc()


if __name__ == "__main__":
    main()

3.3 重要参数说明

LoRA 参数

参数默认值说明调整建议
LORA_R64LoRA 秩,决定可训练参数量简单任务用 32,复杂任务用 128
LORA_ALPHA128缩放系数,影响 LoRA 权重强度通常设为 r 的 2 倍
LORA_DROPOUT0.05Dropout 比例,防止过拟合数据少时用 0.1,数据多时用 0.05

LoRA 原理:

  • LoRA (Low-Rank Adaptation) 通过低秩矩阵分解,只训练 1.16% 的参数
  • 大幅降低显存消耗 (8GB 显存即可训练 40 亿参数模型)
  • 训练速度快,效果接近全参数微调

训练参数

参数默认值说明调整建议
NUM_EPOCHS1训练轮数快速测试用 1,完整训练用 3
BATCH_SIZE2批次大小显存足够可改为 4
GRADIENT_ACCUMULATION_STEPS8梯度累积步数实际批次 = BATCH_SIZE × 此值
LEARNING_RATE1e-4学习率过大会震荡,过小收敛慢
MAX_LENGTH256最大序列长度诗词较短,256 足够
SAVE_STEPS100检查点保存间隔越小恢复点越密集

实际批次大小计算:

实际批次 = BATCH_SIZE × GRADIENT_ACCUMULATION_STEPS
         = 2 × 8
         = 16

数据量控制

脚本中自动进行快速测试配置:

# 快速测试: 使用前 20000 条数据
if len(dataset) > 20000:
    print(f"⚠️  快速测试模式: 原数据 {len(dataset)} 条, 使用前 20000 条")
    dataset = dataset.select(range(20000))

说明:

  • 全量数据: 388599 条,训练时间约 20-25 小时
  • 快速测试: 20000 条,训练时间约 1.5-2 小时
  • 本教程使用快速测试配置

3.4 关键技术点说明

1. 数据集字段自动识别

脚本会自动识别不同格式的数据集:

  • text1 字段: 纯文本数据集(如中文诗词)
  • instruction + output: 指令数据集
  • text: 通用文本字段
  • 其他: 自动使用第一个字段

2. 混合精度训练 (FP16)

使用 torch.float16 可以:

  • 节省 50% 显存
  • 加速 2-3 倍训练速度
  • 对效果影响极小

3. 梯度检查点 (Gradient Checkpointing)

启用后可以:

  • 节省 30-50% 显存
  • 训练速度略微下降 (10-15%)
  • 适合显存不足的情况

4. 梯度累积

实际批次大小 = BATCH_SIZE × GRADIENT_ACCUMULATION_STEPS

  • 显存不足时,减小 BATCH_SIZE,增大 GRADIENT_ACCUMULATION_STEPS
  • 总批次大小保持不变,效果一致

5. 断点续训

第一次训练:

RESUME_FROM_CHECKPOINT = False

训练中断后继续:

RESUME_FROM_CHECKPOINT = True

脚本会自动从最新的检查点继续训练。

3.5 运行训练

python train.py

3.6 训练输出示例

============================================================
模型训练脚本 - LoRA 微调
============================================================
模型路径: d:/wwwroot/modelscope/models/Qwen3-4B
数据集: ./datasets/chinese-poetry/train.json
输出目录: ./output/qwen3-poetry
训练轮数: 1
学习率: 0.0001
LoRA 秩: 64
============================================================

[1/5] 正在加载数据集: ./datasets/chinese-poetry/train.json
⚠️  快速测试模式: 原数据 388599 条, 使用前 20000 条
✓ 数据集加载成功,共 20000 条数据

[2/5] 正在加载模型: d:/wwwroot/modelscope/models/Qwen3-4B
✓ 模型加载成功
✓ 参数量: 4.02B (十亿)

[3/5] 正在配置 LoRA
  - LoRA 秩 (r): 64
  - LoRA Alpha: 128
  - Dropout: 0.05
✓ LoRA 配置完成
  - 可训练参数: 47.19M
  - 总参数: 4.07B
  - 可训练比例: 1.16% (节省显存!)

[4/5] 正在预处理数据集
✓ 数据集预处理完成

[5/5] 开始训练
------------------------------------------------------------
训练开始...
总训练步数: 1250
检查点保存间隔: 每 100 步
------------------------------------------------------------

{'loss': 0.6234, 'learning_rate': 9.2e-05, 'epoch': 0.08}
{'loss': 0.5876, 'learning_rate': 8.8e-05, 'epoch': 0.16}
...
{'loss': 0.4892, 'learning_rate': 1.2e-06, 'epoch': 0.98}

============================================================
✓ 训练完成!
✓ LoRA 权重已保存到: ./output/qwen3-poetry/final
============================================================

下一步:
1. 查看训练日志: tensorboard --logdir ./output/qwen3-poetry/logs
2. 使用训练后的模型: python text_trained.py

3.7 训练时间估算

20K 数据 × 1 轮:

  • RTX 3060 (12GB): 约 1.5-2 小时
  • RTX 3080 (10GB): 约 1-1.5 小时
  • RTX 4090 (24GB): 约 30-45 分钟

全量数据 (388K) × 3 轮:

  • RTX 3060: 约 20-25 小时
  • RTX 3080: 约 15-18 小时
  • RTX 4090: 约 8-10 小时

3.8 断点续训

场景: 训练中断(电脑睡眠、手动停止等)需要继续训练

步骤:

  1. 修改 train.py 第 40 行:
RESUME_FROM_CHECKPOINT = True  # 改为 True
  1. 重新运行:
python train.py

说明:

  • 训练会从最近的检查点(如 checkpoint-300)继续
  • 检查点每 100 步保存一次
  • 最多保留最近 3 个检查点 (save_total_limit=3)

四、训练效果测试

4.1 脚本说明

使用 text_trained.py 测试训练后的基础版模型效果。

功能: 加载原始模型 + 训练后的 LoRA 权重进行对话测试

4.2 完整测试脚本

"""使用训练后的模型进行对话 - 加载 LoRA 权重

功能特性:
1. 加载原始模型 + LoRA 权重
2. 多轮对话记忆
3. 流式输出
4. 对话管理命令

使用方法:
    python text_trained.py

命令:
- quit: 退出程序
- clear: 清空对话历史
- history: 查看对话历史
"""

from modelscope import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
from transformers import TextIteratorStreamer
from threading import Thread
import os

# ==================== 配置区 ====================
# 本地模型路径
MODEL_PATH = "d:/wwwroot/modelscope/models/Qwen3-4B"

# LoRA 微调权重路径 (训练后生成的)
LORA_PATH = "d:/wwwroot/modelscope/output/qwen3-poetry/final"

# 系统提示词 (根据训练内容调整)
SYSTEM_PROMPT = "你是一位精通古诗词创作的 AI 助手,擅长创作优美的古体诗词。"

# 生成参数
MAX_NEW_TOKENS = 512  # 最大生成长度
TEMPERATURE = 0.7  # 温度,越高越随机
TOP_P = 0.8  # 核采样参数
MAX_HISTORY = 10  # 保留最近N轮对话
# ================================================

print("=" * 60)
print("加载训练后的模型...")
print("=" * 60)
print(f"基础模型: {MODEL_PATH}")
print(f"LoRA 权重: {LORA_PATH}")
print("=" * 60)
print()

# 检查 LoRA 路径是否存在
if not os.path.exists(LORA_PATH):
    print(f"❌ 错误: LoRA 权重路径不存在!")
    print(f"路径: {LORA_PATH}")
    print()
    print("请先训练模型:")
    print("  python train.py")
    print()
    exit(1)

print("[1/3] 正在加载基础模型...")

# 加载分词器和模型
tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)
model = AutoModelForCausalLM.from_pretrained(
    MODEL_PATH,
    torch_dtype="auto",
    device_map="auto",
)

print("✓ 基础模型加载完成")
print()

print("[2/3] 正在加载 LoRA 权重...")

# 加载 LoRA 权重
model = PeftModel.from_pretrained(model, LORA_PATH)

print("✓ LoRA 权重加载完成")
print()

print("[3/3] 模型合并中...")

# (可选) 合并权重以提升推理速度
# model = model.merge_and_unload()

print("✓ 准备就绪!")
print()
print("=" * 60)
print("命令: quit(退出) | clear(清空历史) | history(查看历史)")
print("=" * 60)
print()


def chat_stream(messages: list) -> str:
    """流式生成回复"""
    text = tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True,
        enable_thinking=False,  # 禁用思考模式
    )
    inputs = tokenizer([text], return_tensors="pt").to(model.device)

    # 创建流式输出器
    streamer = TextIteratorStreamer(
        tokenizer, skip_prompt=True, skip_special_tokens=True
    )

    # 生成参数
    generation_kwargs = {
        **inputs,
        "max_new_tokens": MAX_NEW_TOKENS,
        "do_sample": True,
        "temperature": TEMPERATURE,
        "top_p": TOP_P,
        "streamer": streamer,
    }

    # 在新线程中运行生成
    thread = Thread(target=model.generate, kwargs=generation_kwargs)
    thread.start()

    # 流式输出
    print("AI: ", end="", flush=True)
    response = ""
    for text in streamer:
        print(text, end="", flush=True)
        response += text
    print("\n")

    return response


def show_history(history: list):
    """显示对话历史"""
    print("\n" + "=" * 20 + " 对话历史 " + "=" * 20)
    for msg in history:
        role = "" if msg["role"] == "user" else "AI"
        content = msg["content"]
        # 截断过长内容
        if len(content) > 100:
            content = content[:100] + "..."
        print(f"[{role}]: {content}")
    print("=" * 50 + "\n")


# 对话历史 (包含系统提示词)
conversation_history = [{"role": "system", "content": SYSTEM_PROMPT}]

# 主对话循环
while True:
    try:
        user_input = input("你: ").strip()

        if not user_input:
            continue

        # 处理命令
        if user_input.lower() == "quit":
            print("再见!")
            break
        elif user_input.lower() == "clear":
            conversation_history = [{"role": "system", "content": SYSTEM_PROMPT}]
            print("[对话历史已清空]\n")
            continue
        elif user_input.lower() == "history":
            show_history(conversation_history[1:])  # 跳过系统提示词
            continue

        # 添加用户消息
        conversation_history.append({"role": "user", "content": user_input})

        # 生成回复 (流式输出)
        response = chat_stream(conversation_history)

        # 添加 AI 回复到历史
        conversation_history.append({"role": "assistant", "content": response})

        # 限制历史长度 (保留系统提示词 + 最近N轮对话)
        if len(conversation_history) > MAX_HISTORY * 2 + 1:
            conversation_history = [conversation_history[0]] + conversation_history[
                -(MAX_HISTORY * 2) :
            ]

    except KeyboardInterrupt:
        print("\n\n程序被用户中断,再见!")
        break
    except Exception as e:
        print(f"\n\n发生错误: {e}")
        import traceback

        traceback.print_exc()
        break

4.3 关键技术点

1. LoRA 权重加载

# 先加载基础模型
model = AutoModelForCausalLM.from_pretrained(MODEL_PATH)

# 再加载 LoRA 权重
model = PeftModel.from_pretrained(model, LORA_PATH)

2. 流式输出

使用 TextIteratorStreamer 实现逐字输出,提升用户体验。

3. 对话历史管理

  • 保留系统提示词
  • 限制历史长度(默认 10 轮)
  • 支持清空历史命令

4.4 运行测试

python text_trained.py

4.5 使用示例

============================================================
加载训练后的模型...
============================================================
基础模型: d:/wwwroot/modelscope/models/Qwen3-4B
LoRA 权重: d:/wwwroot/modelscope/output/qwen3-poetry/final
============================================================

[1/3] 正在加载基础模型...
✓ 基础模型加载完成

[2/3] 正在加载 LoRA 权重...
✓ LoRA 权重加载完成

[3/3] 模型合并中...
✓ 准备就绪!

============================================================
命令: quit(退出) | clear(清空历史) | history(查看历史)
============================================================

你: 写一首七言绝句,主题是春天
AI: 东风吹雨湿花枝,
   燕子双飞入画时。
   莫道江南无好景,
   绿杨深处有莺啼。

你: quit
再见!

4.6 可用命令

命令功能
quit退出程序
clear清空对话历史
history查看对话历史

五、发现的问题

5.1 问题现象

在测试基础版模型时,发现了一个严重的问题:

测试用例:

你: 写一首七言绝句,要有完整的4句

基础版输出:

东风吹雨湿花枝,
燕子双飞入画时。
[只写了2句就停止! ❌]

期望输出:

东风吹雨湿花枝,
燕子双飞入画时。
莫道江南无好景,
绿杨深处有莺啼。
[完整的4句 ✅]

5.2 更多测试

测试 2: 续写能力

你: 续写2句: 春风拂面柳丝长,燕语莺啼绿映窗。

基础版输出:
东风吹雨湿花枝,
燕子双飞入画时...
[完全不理解"续写",输出了无关诗句! ❌]

测试 3: 五言绝句

你: 写一首五言绝句,主题是秋天

基础版输出:
秋风拂面凉,
落叶满江黄。
[又是只有2句! ❌]

5.3 问题分析

根本原因: Qwen3-4B 是 Base Model (基础版),只会纯文本续写,不理解指令含义。

Base Model vs Instruct Model:

维度Base Model (基础版)Instruct Model (指令版)
训练方式纯预训练预训练 + 指令微调
主要功能文本续写指令理解 + 对话
指令理解❌ 不理解✅ 完美理解
应用场景风格迁移、文本补全对话系统、任务执行
输出控制⚠️ 容易只写 2 句就停✅ 按指令完整输出

问题统计:

  • 测试 1: "要有完整 4 句" → ❌ 只写了 2 句
  • 测试 2: "续写 2 句" → ❌ 输出无关诗句
  • 测试 3: 五言绝句 → ❌ 只写了 2 句
  • 成功率: 0/3 = 0%

5.4 解决方案

方案 1: 使用 Instruct 版本模型 (推荐)

  • 模型: Qwen3-4B-Instruct-2507
  • 优点: 天然支持指令理解,不会出现"只写 2 句"问题
  • 缺点: 需要重新下载模型

方案 2: 使用指令格式训练数据集

  • 在基础版上训练指令格式数据
  • 优点: 不需要换模型
  • 缺点: 效果不如原生 Instruct 版

我们选择方案 1,使用 Qwen3-4B-Instruct-2507 重新训练。


六、指令版模型训练

6.1 脚本说明

使用 train_instruct.py 对 Qwen3-4B-Instruct-2507 进行 LoRA 微调。

与基础版的区别: 只有模型路径和输出目录不同,其他配置完全相同。

6.2 完整训练脚本

"""Qwen3-4B-Instruct 诗词训练脚本 - LoRA 微调

功能:
1. 使用 Qwen3-4B-Instruct-2507 (指令版) 作为基础模型
2. 训练诗词 LoRA 权重
3. 快速测试配置 (20K 数据, 1 轮)

使用方法:
    python train_instruct.py

训练完成后,LoRA 权重会保存在 OUTPUT_DIR 目录
"""

import os
from dataclasses import dataclass, field
from typing import Optional

from modelscope import AutoModelForCausalLM, AutoTokenizer
from modelscope.msdatasets import MsDataset
from transformers import TrainingArguments, Trainer
from peft import LoraConfig, get_peft_model, TaskType
import torch

# ==================== 训练配置 ====================
# 模型配置
MODEL_PATH = "./models/Qwen3-4B-Instruct-2507"  # Instruct 版本模型路径
MODEL_TYPE = "qwen3"  # 模型类型

# 数据集配置
DATASET_NAME = "./datasets/chinese-poetry/train.json"

# 输出配置
OUTPUT_DIR = "./output/qwen3-poetry-instruct"  # 训练输出目录
LOGGING_DIR = "./output/qwen3-poetry-instruct/logs"  # 日志目录

# 断点续训配置
RESUME_FROM_CHECKPOINT = True  # 第一次训练设为 False,之后改为 True

# LoRA 参数
LORA_R = 64  # LoRA 秩
LORA_ALPHA = 128  # LoRA 缩放系数
LORA_DROPOUT = 0.05  # Dropout 比例

# 训练超参数 (快速测试配置)
NUM_EPOCHS = 1  # 训练轮数 (快速测试用1轮)
BATCH_SIZE = 2  # 批次大小
GRADIENT_ACCUMULATION_STEPS = 8  # 梯度累积步数
LEARNING_RATE = 1e-4  # 学习率
MAX_LENGTH = 256  # 最大序列长度
WARMUP_RATIO = 0.1  # 预热比例
SAVE_STEPS = 100  # 每隔多少步保存一次
LOGGING_STEPS = 10  # 每隔多少步记录一次日志

# 其他配置
USE_FP16 = True  # 使用混合精度训练
GRADIENT_CHECKPOINTING = True  # 启用梯度检查点
# ==================================================

print("=" * 60)
print("Qwen3-4B-Instruct 诗词训练 - LoRA 微调")
print("=" * 60)
print(f"模型路径: {MODEL_PATH}")
print(f"数据集: {DATASET_NAME}")
print(f"输出目录: {OUTPUT_DIR}")
print(f"训练轮数: {NUM_EPOCHS}")
print(f"学习率: {LEARNING_RATE}")
print(f"LoRA 秩: {LORA_R}")
print("=" * 60)
print()


def load_dataset(dataset_name: str):
    """加载数据集"""
    print(f"[1/5] 正在加载数据集: {dataset_name}")

    try:
        # 判断是否是本地文件
        if dataset_name.endswith(".json"):
            # 加载本地 JSON 文件
            import json
            from datasets import Dataset

            with open(dataset_name, "r", encoding="utf-8") as f:
                data = json.load(f)

            dataset = Dataset.from_list(data)

            # 快速测试: 使用前 20000 条数据
            if len(dataset) > 20000:
                print(f"⚠️  快速测试模式: 原数据 {len(dataset)} 条, 使用前 20000 条")
                dataset = dataset.select(range(20000))

            print(f"✓ 数据集加载成功,共 {len(dataset)} 条数据")
            return dataset
        else:
            # 尝试从 ModelScope 加载
            dataset = MsDataset.load(dataset_name, split="train")
            print(f"✓ 数据集加载成功,共 {len(dataset)} 条数据")
            return dataset
    except Exception as e:
        print(f"✗ 加载失败: {e}")
        print("提示: 检查数据集名称是否正确,或使用本地数据集路径")
        return None


def load_model_and_tokenizer(model_path: str):
    """加载模型和分词器"""
    print(f"\n[2/5] 正在加载模型: {model_path}")

    # 加载分词器
    tokenizer = AutoTokenizer.from_pretrained(
        model_path, trust_remote_code=True, padding_side="right"
    )

    # 设置 pad_token
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token

    # 加载模型
    model = AutoModelForCausalLM.from_pretrained(
        model_path,
        torch_dtype=torch.float16 if USE_FP16 else torch.float32,
        device_map="auto",
        trust_remote_code=True,
    )

    # 启用梯度检查点
    if GRADIENT_CHECKPOINTING:
        model.gradient_checkpointing_enable()

    print(f"✓ 模型加载成功")
    print(f"✓ 参数量: {sum(p.numel() for p in model.parameters()) / 1e9:.2f}B (十亿)")

    return model, tokenizer


def setup_lora(model):
    """配置 LoRA"""
    print(f"\n[3/5] 正在配置 LoRA")
    print(f"  - LoRA 秩 (r): {LORA_R}")
    print(f"  - LoRA Alpha: {LORA_ALPHA}")
    print(f"  - Dropout: {LORA_DROPOUT}")

    # LoRA 配置
    lora_config = LoraConfig(
        task_type=TaskType.CAUSAL_LM,
        r=LORA_R,
        lora_alpha=LORA_ALPHA,
        lora_dropout=LORA_DROPOUT,
        target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],  # Qwen3 的注意力层
        bias="none",
    )

    # 应用 LoRA
    model = get_peft_model(model, lora_config)

    # 打印可训练参数
    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"✓ LoRA 配置完成")
    print(f"  - 可训练参数: {trainable_params / 1e6:.2f}M")
    print(f"  - 总参数: {total_params / 1e9:.2f}B")
    print(f"  - 可训练比例: {100 * trainable_params / total_params:.2f}% (节省显存!)")

    return model


def preprocess_dataset(dataset, tokenizer):
    """预处理数据集"""
    print(f"\n[4/5] 正在预处理数据集")

    def tokenize_function(examples):
        # 检查数据集字段类型
        if "text1" in examples:
            # 纯文本数据集(如中文诗词)
            texts = examples["text1"]
        elif "instruction" in examples and "output" in examples:
            # 指令数据集
            texts = []
            for instruction, output in zip(examples["instruction"], examples["output"]):
                text = f"User: {instruction}\nAssistant: {output}"
                texts.append(text)
        elif "text" in examples:
            # 通用文本字段
            texts = examples["text"]
        else:
            # 其他格式,尝试获取第一个字段
            first_key = list(examples.keys())[0]
            texts = examples[first_key]

        # 分词
        tokenized = tokenizer(
            texts,
            truncation=True,
            max_length=MAX_LENGTH,
            padding="max_length",
            return_tensors="pt",
        )

        # 设置 labels (用于计算 loss)
        tokenized["labels"] = tokenized["input_ids"].clone()

        return tokenized

    # 应用预处理
    processed_dataset = dataset.map(
        tokenize_function,
        batched=True,
        remove_columns=dataset.column_names,
        desc="Tokenizing dataset",
    )

    print(f"✓ 数据集预处理完成")
    return processed_dataset


def train(model, tokenizer, train_dataset):
    """训练模型"""
    print(f"\n[5/5] 开始训练")

    # 训练参数
    training_args = TrainingArguments(
        output_dir=OUTPUT_DIR,
        num_train_epochs=NUM_EPOCHS,
        per_device_train_batch_size=BATCH_SIZE,
        gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS,
        learning_rate=LEARNING_RATE,
        warmup_ratio=WARMUP_RATIO,
        logging_dir=LOGGING_DIR,
        logging_steps=LOGGING_STEPS,
        save_steps=SAVE_STEPS,
        save_total_limit=3,  # 只保留最近 3 个检查点
        fp16=USE_FP16,
        gradient_checkpointing=GRADIENT_CHECKPOINTING,
        report_to="tensorboard",  # 使用 TensorBoard 记录日志
        load_best_model_at_end=False,
        remove_unused_columns=False,
    )

    # 创建 Trainer
    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=train_dataset,
        tokenizer=tokenizer,
    )

    # 开始训练
    print()
    print("-" * 60)
    print("训练开始...")
    print(
        f"总训练步数: {len(train_dataset) // (BATCH_SIZE * GRADIENT_ACCUMULATION_STEPS) * NUM_EPOCHS}"
    )
    print(f"检查点保存间隔: 每 {SAVE_STEPS} 步")
    if RESUME_FROM_CHECKPOINT:
        print(f"断点续训: 开启 (将从最新检查点续训)")
    print("-" * 60)
    print()

    trainer.train(
        resume_from_checkpoint=(
            RESUME_FROM_CHECKPOINT if RESUME_FROM_CHECKPOINT else None
        )
    )

    # 保存最终模型
    final_output_dir = os.path.join(OUTPUT_DIR, "final")
    trainer.save_model(final_output_dir)
    tokenizer.save_pretrained(final_output_dir)

    print()
    print("=" * 60)
    print("✓ 训练完成!")
    print(f"✓ LoRA 权重已保存到: {final_output_dir}")
    print("=" * 60)
    print()
    print("下一步:")
    print(f"1. 查看训练日志: tensorboard --logdir {LOGGING_DIR}")
    print(f"2. 使用训练后的模型: python text_trained_instruct.py")
    print()


def main():
    """主函数"""
    try:
        # 1. 加载数据集
        dataset = load_dataset(DATASET_NAME)
        if dataset is None:
            return

        # 2. 加载模型和分词器
        model, tokenizer = load_model_and_tokenizer(MODEL_PATH)

        # 3. 配置 LoRA
        model = setup_lora(model)

        # 4. 预处理数据集
        train_dataset = preprocess_dataset(dataset, tokenizer)

        # 5. 训练
        train(model, tokenizer, train_dataset)

    except KeyboardInterrupt:
        print("\n\n训练被用户中断")
    except Exception as e:
        print(f"\n\n训练出错: {e}")
        import traceback

        traceback.print_exc()


if __name__ == "__main__":
    main()

6.3 关键差异说明

与基础版的唯一区别:

# 基础版
MODEL_PATH = "d:/wwwroot/modelscope/models/Qwen3-4B"
OUTPUT_DIR = "./output/qwen3-poetry"
LOGGING_DIR = "./output/qwen3-poetry/logs"

# 指令版
MODEL_PATH = "./models/Qwen3-4B-Instruct-2507"  # ⚠️ 不同!
OUTPUT_DIR = "./output/qwen3-poetry-instruct"   # ⚠️ 不同!
LOGGING_DIR = "./output/qwen3-poetry-instruct/logs"  # ⚠️ 不同!

其他所有参数(LoRA 配置、训练参数等)完全相同。

6.3 模型下载

使用 modelscope 命令行(如之前已下载无需重复下载)

modelscope download --model Qwen/Qwen3-4B-Instruct-2507 --local_dir ./models/Qwen3-4B-Instruct-2507

6.4 运行训练

python train_instruct.py

6.5 训练输出

============================================================
Qwen3-4B-Instruct 诗词训练 - LoRA 微调
============================================================
模型路径: ./models/Qwen3-4B-Instruct-2507
数据集: ./datasets/chinese-poetry/train.json
输出目录: ./output/qwen3-poetry-instruct
训练轮数: 1
学习率: 0.0001
LoRA 秩: 64
============================================================

[1/5] 正在加载数据集...
⚠️  快速测试模式: 原数据 388599 条, 使用前 20000 条
✓ 数据集加载成功,共 20000 条数据

[2/5] 正在加载模型: ./models/Qwen3-4B-Instruct-2507
✓ 模型加载成功
✓ 参数量: 4.02B (十亿)

[3/5] 正在配置 LoRA
✓ LoRA 配置完成
  - 可训练参数: 47.19M
  - 总参数: 4.07B
  - 可训练比例: 1.16% (节省显存!)

[4/5] 正在预处理数据集
✓ 数据集预处理完成

[5/5] 开始训练
------------------------------------------------------------
训练开始...
总训练步数: 1250
检查点保存间隔: 每 100 步
------------------------------------------------------------

{'loss': 0.5246, 'grad_norm': 0.134, 'learning_rate': 8.36e-05, 'epoch': 0.25}
{'loss': 0.5180, 'grad_norm': 0.144, 'learning_rate': 8.27e-05, 'epoch': 0.26}
...
{'loss': 0.4892, 'grad_norm': 0.144, 'learning_rate': 3.64e-06, 'epoch': 0.98}

============================================================
✓ 训练完成!
✓ LoRA 权重已保存到: ./output/qwen3-poetry-instruct/final
============================================================

下一步:
1. 查看训练日志: tensorboard --logdir ./output/qwen3-poetry-instruct/logs
2. 使用训练后的模型: python text_trained_instruct.py

6.6 Loss 对比

基础版 vs 指令版:

初期 Loss (0-300步):
- 基础版: 0.62 → 0.56
- 指令版: 0.52 → 0.50  ✅ (收敛更快)

中期 Loss (300-800步):
- 基础版: 0.56 → 0.52
- 指令版: 0.50 → 0.49

最终 Loss (1250步):
- 基础版: 0.52
- 指令版: 0.49  ✅ (更低)

分析: 指令版模型初始 Loss 更低,收敛更快,说明其预训练质量更高。


七、指令版效果测试

7.1 脚本说明

使用 text_trained_instruct.py 测试训练后的指令版模型效果。

7.2 完整测试脚本

"""Qwen3-4B-Instruct 诗词模型对话脚本

功能:
使用训练好的 Qwen3-4B-Instruct + 诗词 LoRA 进行对话

使用方法:
    python text_trained_instruct.py
"""

from modelscope import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
from transformers import TextIteratorStreamer
import torch

# 配置
BASE_MODEL_PATH = "./models/Qwen3-4B-Instruct-2507"  # 基础模型路径
LORA_PATH = "./output/qwen3-poetry-instruct/final"  # LoRA 权重路径

# 生成参数
MAX_NEW_TOKENS = 512  # 最大生成长度
TEMPERATURE = 0.8  # 温度 (越高越随机)
TOP_P = 0.9  # Top-p 采样
TOP_K = 50  # Top-k 采样

print("=" * 60)
print()
print("加载训练后的模型...")
print()
print("=" * 60)
print(f"基础模型: {BASE_MODEL_PATH}")
print(f"LoRA 权重: {LORA_PATH}")
print("=" * 60)
print()

# 加载分词器
print("[1/3] 正在加载分词器...")
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_PATH, trust_remote_code=True)
print("✓ 分词器加载完成")
print()

# 加载基础模型
print("[2/3] 正在加载基础模型...")
base_model = AutoModelForCausalLM.from_pretrained(
    BASE_MODEL_PATH,
    torch_dtype=torch.float16,
    device_map="auto",
    trust_remote_code=True,
)
print("✓ 基础模型加载完成")
print()

# 加载 LoRA 权重
print("[3/3] 正在加载 LoRA 权重...")
model = PeftModel.from_pretrained(base_model, LORA_PATH)
model.eval()  # 设置为评估模式
print("✓ LoRA 权重加载完成")
print()

print("✓ 准备就绪!")
print()
print("=" * 60)
print("命令: quit(退出) | clear(清空历史) | history(查看历史)")
print("=" * 60)
print()

# 对话历史
conversation_history = [
    {
        "role": "system",
        "content": "你是一位精通古诗词创作的 AI 助手,擅长创作优美的古体诗词。",
    }
]


def chat_stream(messages: list):
    """流式生成回复"""
    # 应用聊天模板
    text = tokenizer.apply_chat_template(
        messages, tokenize=False, add_generation_prompt=True
    )

    # 编码
    inputs = tokenizer([text], return_tensors="pt").to(model.device)

    # 生成
    with torch.no_grad():
        streamer = TextIteratorStreamer(
            tokenizer, skip_prompt=True, skip_special_tokens=True
        )

        generation_kwargs = {
            **inputs,
            "max_new_tokens": MAX_NEW_TOKENS,
            "temperature": TEMPERATURE,
            "top_p": TOP_P,
            "top_k": TOP_K,
            "do_sample": True,
            "streamer": streamer,
        }

        from threading import Thread

        thread = Thread(target=model.generate, kwargs=generation_kwargs)
        thread.start()

        response = ""
        for text in streamer:
            print(text, end="", flush=True)
            response += text

        print()  # 换行
        return response.strip()


# 主循环
while True:
    try:
        # 获取用户输入
        user_input = input("\n你: ")

        # 处理命令
        if user_input.lower() == "quit":
            print("再见!")
            break
        elif user_input.lower() == "clear":
            conversation_history = [
                {
                    "role": "system",
                    "content": "你是一位精通古诗词创作的 AI 助手,擅长创作优美的古体诗词。",
                }
            ]
            print("✓ 对话历史已清空")
            continue
        elif user_input.lower() == "history":
            print("\n对话历史:")
            for i, msg in enumerate(conversation_history[1:], 1):  # 跳过 system
                role = "" if msg["role"] == "user" else "AI"
                print(f"{i}. {role}: {msg['content'][:50]}...")
            continue

        if not user_input.strip():
            continue

        # 添加用户消息
        conversation_history.append({"role": "user", "content": user_input})

        # 生成回复
        print("\nAI: ", end="")
        response = chat_stream(conversation_history)

        # 添加助手回复
        conversation_history.append({"role": "assistant", "content": response})

    except KeyboardInterrupt:
        print("\n\n再见!")
        break
    except Exception as e:
        print(f"\n错误: {e}")
        import traceback

        traceback.print_exc()

7.3 运行测试

python text_trained_instruct.py

7.4 测试结果

测试 1: 写完整的 4 句 (关键测试)

你: 写一首七言绝句,要有完整的4句

指令版输出:
春山翠色晓烟浮,
桃李争开百卉柔。
风暖莺啼花满树,
一川红雨落溪流。
[完整的4句! ✅✅✅]

测试 2: 续写能力

你: 续写2句: 春风拂面柳丝长,燕语莺啼绿映窗。

指令版输出:
春风拂面柳丝长,
燕语莺啼绿映窗。
花落小溪流水去,
飞花逐水流成行。
[正确续写! ✅]

测试 3: 五言绝句

你: 写一首五言绝句,主题是秋天

指令版输出:
霜天云影淡,
落叶满空林。
雁过寒砧急,
风来夜漏深。
[完整的4句,每句5字! ✅]

测试 4: 藏头诗

你: 写一首藏头诗,藏'春夏秋冬'

指令版输出:
春山新雨绿初匀,
夏树浓阴暑气匀。
秋水澄明连野阔,
冬梅缟素映寒云。
[藏头正确,完整4句! ✅]

7.5 指令效果对比

测试项训练后基础版训练后指令版结果
写完整 4 句❌ 只写 2 句✅ 完整 4 句指令版胜
续写 2 句❌ 输出无关✅ 正确续写指令版胜
五言绝句❌ 只写 2 句✅ 完整 4 句指令版胜
藏头诗❌ 只写 2 句✅ 完整 4 句指令版胜
诗词质量⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐相当
成功率0%100%指令版完胜

结论: 经过训练的指令版完美解决了同样经过训练的基础版"只写 2 句"的问题!✅


八、原始指令版与训练指令版对比

8.1 脚本说明

使用 text_instruct_original.py 测试原始(未训练)的 Qwen3-4B-Instruct 模型。

目的: 对比训练前后的差异,验证训练效果

8.2 完整对比脚本

"""Qwen3-4B-Instruct 原始模型对话脚本 (未训练诗词)

功能:
使用原始的 Qwen3-4B-Instruct (没有 LoRA) 进行对话
用于对比训练前后的差异

使用方法:
    python text_instruct_original.py
"""

from modelscope import AutoModelForCausalLM, AutoTokenizer
from transformers import TextIteratorStreamer
import torch

# 配置
MODEL_PATH = "./models/Qwen3-4B-Instruct-2507"  # 原始模型,不加载 LoRA

# 生成参数
MAX_NEW_TOKENS = 512
TEMPERATURE = 0.8
TOP_P = 0.9
TOP_K = 50

print("=" * 60)
print()
print("加载原始 Qwen3-4B-Instruct 模型 (未训练诗词)")
print()
print("=" * 60)
print(f"模型路径: {MODEL_PATH}")
print("⚠️  注意: 这是原始模型,没有加载诗词 LoRA!")
print("=" * 60)
print()

# 加载分词器
print("[1/2] 正在加载分词器...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
print("✓ 分词器加载完成")
print()

# 加载模型
print("[2/2] 正在加载模型...")
model = AutoModelForCausalLM.from_pretrained(
    MODEL_PATH, torch_dtype=torch.float16, device_map="auto", trust_remote_code=True
)
model.eval()
print("✓ 模型加载完成")
print()

print("✓ 准备就绪!")
print()
print("=" * 60)
print("命令: quit(退出) | clear(清空历史)")
print("=" * 60)
print()

# 对话历史
conversation_history = [
    {
        "role": "system",
        "content": "你是一位精通古诗词创作的 AI 助手,擅长创作优美的古体诗词。",
    }
]


def chat_stream(messages: list):
    """流式生成回复"""
    # 应用聊天模板
    text = tokenizer.apply_chat_template(
        messages, tokenize=False, add_generation_prompt=True
    )

    # 编码
    inputs = tokenizer([text], return_tensors="pt").to(model.device)

    # 生成
    with torch.no_grad():
        streamer = TextIteratorStreamer(
            tokenizer, skip_prompt=True, skip_special_tokens=True
        )

        generation_kwargs = {
            **inputs,
            "max_new_tokens": MAX_NEW_TOKENS,
            "temperature": TEMPERATURE,
            "top_p": TOP_P,
            "top_k": TOP_K,
            "do_sample": True,
            "streamer": streamer,
        }

        from threading import Thread

        thread = Thread(target=model.generate, kwargs=generation_kwargs)
        thread.start()

        response = ""
        for text in streamer:
            print(text, end="", flush=True)
            response += text

        print()  # 换行
        return response.strip()


# 主循环
while True:
    try:
        # 获取用户输入
        user_input = input("\n你: ")

        # 处理命令
        if user_input.lower() == "quit":
            print("再见!")
            break
        elif user_input.lower() == "clear":
            conversation_history = [
                {
                    "role": "system",
                    "content": "你是一位精通古诗词创作的 AI 助手,擅长创作优美的古体诗词。",
                }
            ]
            print("✓ 对话历史已清空")
            continue

        if not user_input.strip():
            continue

        # 添加用户消息
        conversation_history.append({"role": "user", "content": user_input})

        # 生成回复
        print("\nAI: ", end="")
        response = chat_stream(conversation_history)

        # 添加助手回复
        conversation_history.append({"role": "assistant", "content": response})

    except KeyboardInterrupt:
        print("\n\n再见!")
        break
    except Exception as e:
        print(f"\n错误: {e}")
        import traceback

        traceback.print_exc()

8.3 运行测试

python text_instruct_original.py

8.4 三版本对比测试

测试 1: 七言绝句 (春天主题)

原始 Instruct (版本 1):

柳眼初开燕语新,
桃腮半醉杏花匀。
风翻碧浪千山绿,
一笛吹残晓雾云。

注:本诗以"柳眼""桃腮"等拟人化意象展现春之生机...
[附带详细注释]
  • ✅ 格式完整 (4 句 7 字)
  • ⭐⭐⭐⭐ 质量良好
  • ⚠️ 输出冗长 (总带注释)

训练后 Instruct (版本 2):

春山翠色晓烟浮,
桃李争开百卉柔。
风暖莺啼花满树,
一川红雨落溪流。
  • ✅ 格式完整 (4 句 7 字)
  • ⭐⭐⭐⭐⭐ 意境流畅自然
  • ✅ 简洁纯粹

训练后基础版 (版本 3):

东风吹雨湿花枝,
燕子双飞入画时。
莫道江南无好景,
绿杨深处有莺啼。
  • ✅ 格式完整 (4 句 7 字)
  • ⭐⭐⭐⭐⭐ 江南意境清丽
  • ✅ 质量优秀

测试 2: 李白风格模仿

原始 Instruct (版本 1):

危楼高百尺,
手可摘星辰。
不敢高声语,
恐惊天上人。
  • ⚠️ 这是真的李白诗《夜宿山寺》!
  • ❌ 背诵原诗,非原创

训练后 Instruct (版本 2):

举杯邀月对孤峰,
醉倚天风落酒濛。
不向人间求富贵,
白云深处自飘蓬。
  • ✅ 完全原创
  • ⭐⭐⭐⭐⭐ 豪放飘逸,贴切

训练后基础版 (版本 3):

李白游天姥,
云生五湖水。
醉卧松风间,
山月照衣袂。
  • ✅ 原创
  • ⭐⭐⭐ 形似神不似

发现: 训练后的版本更有创造力!

测试 3: 念奴娇 (中秋主题)

原始 Instruct (版本 1):

冰轮高挂,正银汉无声,玉盘初满。
万点清辉流碧落,洒落人间千山。
桂子飘香,露华凝影,风起凉生晚。
楼台影里,一轮清彻如鉴。

谁念孤客凭阑?素娥应笑,素手曾分半。
素影徘徊今夜月,不照离愁无岸。
醉倚阑干,素光盈袖,天地皆成宴。
人间天上,共此清光一盏。
  • ✅ 约 170 字 (完整上下阕)
  • ⭐⭐⭐⭐⭐ 古典华丽

训练后 Instruct (版本 2):

念奴娇·中秋夜月

玉轮初上,望人间、清景澄澄如洗。
桂魄微明天宇阔,万古乾坤如水。
露冷蟾华,风凉桂子,凉气侵衣袂。
冰壶照影,共君今夕同醉。
  • ⚠️ 约 60 字 (只有上阕)
  • ⭐⭐⭐⭐ 清丽雅致

训练后基础版 (版本 3):

念奴娇·中秋夜思

对月凭栏,倚阑人静,清影孤圆。
凉风生袖,寒香沁襟边。
玉露凝空,银河泻水,万里共婵娟。
凭高望极,故园何处烟村。
  • ⚠️ 约 50 字 (只有上阕)
  • ⭐⭐⭐⭐ 意境优美

发现: 20K 数据中长调样本较少,训练版不如原始版写得完整。

8.5 综合评分

评估维度版本 1
(原始 Instruct)
版本 2
(训练后 Instruct)
版本 3
(训练后基础版)
指令理解⭐⭐⭐⭐⭐ (5/5)⭐⭐⭐⭐⭐ (5/5)⭐⭐ (2/5)
指令执行⭐⭐⭐⭐⭐ (5/5)⭐⭐⭐⭐⭐ (5/5)⭐⭐ (2/5)
诗词质量⭐⭐⭐⭐ (4/5)⭐⭐⭐⭐⭐ (5/5)⭐⭐⭐⭐⭐ (5/5)
意境优美⭐⭐⭐⭐ (4/5)⭐⭐⭐⭐⭐ (5/5)⭐⭐⭐⭐⭐ (5/5)
格律准确⭐⭐⭐⭐⭐ (5/5)⭐⭐⭐⭐⭐ (5/5)⭐⭐⭐⭐ (4/5)
完整性⭐⭐⭐⭐⭐ (5/5)⭐⭐⭐⭐ (4/5)⭐⭐ (2/5)
创造性⭐⭐⭐ (3/5)⭐⭐⭐⭐⭐ (5/5)⭐⭐⭐⭐ (4/5)
简洁性⭐⭐⭐ (3/5)⭐⭐⭐⭐⭐ (5/5)⭐⭐⭐⭐⭐ (5/5)
长诗能力⭐⭐⭐⭐⭐ (5/5)⭐⭐⭐ (3/5)⭐⭐⭐ (3/5)
续写能力⭐⭐⭐⭐⭐ (5/5)⭐⭐⭐⭐⭐ (5/5)⭐ (1/5)
综合得分42/50 (84%)47/50 (94%) 🏆37/50 (74%)

8.6 关键发现

1. 训练确实有效 (+10%)

版本 2 vs 版本 1:

  • 诗词质量: 4 分 → 5 分 ✅ (+25%)
  • 意境优美: 4 分 → 5 分 ✅ (+25%)
  • 创造力: 3 分 → 5 分 ✅ (+67%)
  • 综合得分: 84% → 94% ✅ (+10%)

2. Instruct 版必不可少

版本 2 vs 版本 3:

  • 指令理解: 5 分 vs 2 分 ✅
  • 指令执行: 5 分 vs 2 分 ✅
  • 完整性: 4 分 vs 2 分 ✅ (基础版经常只写 2 句)
  • 续写能力: 5 分 vs 1 分 ✅

3. 基础版的致命问题

版本 3 的问题统计:

  • 测试 1: "写完整 4 句" → ❌ 重复之前的诗
  • 测试 2: 续写 2 句 → ❌ 输出无关诗句
  • 测试 3: 五言绝句 → ❌ 只写了 2 句
  • 测试 4: 藏头诗 → ❌ 只写了 2 句
  • 成功率: 1/7 = 14%

4. 20K 数据的局限性

长调测试结果:

  • 版本 1 (原始): 170 字 (完整上下阕) ✅
  • 版本 2 (20K): 60 字 (只有上阕) ⚠️
  • 版本 3 (20K): 50 字 (只有上阕) ⚠️

分析: 20K 数据集中长调样本较少,需要更多数据 (100K+)。


九、常见问题与解决方案

9.1 训练相关问题

问题 1: 页面文件太小

错误信息:

OSError: 页面文件太小,无法完成操作。 (os error 1455)

原因: 模型加载需要大量内存,虚拟内存不足。

解决方案 A: 增加虚拟内存 (推荐)

  1. 右键"此电脑" → 属性 → 高级系统设置
  2. 性能 → 设置 → 高级 → 虚拟内存 → 更改
  3. 自定义大小(数值仅供参数,以具体硬件为准):
    • 初始大小: 16384 MB (16GB)
    • 最大值: 32768 MB (32GB)
  4. 设置 → 确定 → 重启电脑

解决方案 B: 使用量化版本

下载 FP8 量化版本(文件减半,约 4GB):

modelscope download --model Qwen/Qwen3-4B-Instruct-2507-FP8

解决方案 C: 云端训练

使用魔搭社区或租用云计算厂商的云端 GPU 平台。

问题 2: 断点续训配置遗漏

场景: 训练中断后重启,忘记设置 RESUME_FROM_CHECKPOINT = True

后果:

  • 训练会从头开始
  • 可能覆盖之前的检查点

解决方案:

如果发现及时 (训练不到 100 步):

  1. 立即停止训练 (Ctrl+C)
  2. 修改配置: RESUME_FROM_CHECKPOINT = True
  3. 重新运行: python train_instruct.py
  4. 训练会从最近检查点继续 ✅

如果训练超过 100 步:

  • 检查点每 100 步保存,会覆盖旧的
  • 最多保留 3 个检查点 (save_total_limit=3)
  • 建议重新从头训练

预防措施:

# 在代码中添加醒目注释
RESUME_FROM_CHECKPOINT = False  # ⚠️ 重要: 续训时改为 True!

问题 3: Loss 不下降

现象: 训练过程中 Loss 不下降或震荡剧烈

可能原因:

  1. 学习率太大
  2. 学习率太小
  3. 数据预处理有问题
  4. LoRA 秩太小

诊断方法:

# 查看 TensorBoard
tensorboard --logdir ./output/qwen3-poetry-instruct/logs

访问 http://localhost:6006 查看 Loss 曲线

解决方案:

如果 Loss 震荡剧烈:

LEARNING_RATE = 5e-5  # 降低学习率 (从 1e-4 降到 5e-5)

如果 Loss 下降太慢:

LEARNING_RATE = 2e-4  # 提高学习率 (从 1e-4 升到 2e-4)

如果效果不好:

LORA_R = 128          # 增大 LoRA 秩 (从 64 升到 128)
NUM_EPOCHS = 3        # 增加训练轮数 (从 1 升到 3)

问题 4: 显存不足 (CUDA OOM)

错误信息:

RuntimeError: CUDA out of memory

解决方案:

方法 1: 减小批次大小

BATCH_SIZE = 1  # 从 2 改为 1

方法 2: 减小 LoRA 秩

LORA_R = 32  # 从 64 改为 32

方法 3: 减小序列长度

MAX_LENGTH = 128  # 从 256 改为 128

方法 4: 禁用梯度检查点 (不推荐)

GRADIENT_CHECKPOINTING = False  # 会占用更多显存,但速度更快

9.2 数据集相关问题

问题 5: 数据集字段不匹配

错误信息:

KeyError: 'instruction'

原因: 数据集字段与训练脚本不匹配

解决方案:

  1. 查看数据集字段:
import json
with open('./datasets/chinese-poetry/train.json', 'r', encoding='utf-8') as f:
    data = json.load(f)
print(data[0].keys())  # 查看字段名
  1. 修改 tokenize_function 函数(train.py 第 175-206 行):
def tokenize_function(examples):
    # 根据实际字段调整
    if "your_field_name" in examples:
        texts = examples["your_field_name"]
    # ...

9.3 模型测试问题

问题 6: 模型输出不符合预期

场景: 训练后的模型表现不如预期

诊断步骤:

1. 确认使用了正确的模型:

# 推荐使用指令版
python text_trained_instruct.py

# 不要用基础版(有"只写2句"问题)
# python text_trained.py

2. 对比原始版本:

python text_instruct_original.py

3. 检查训练是否完成:

  • 查看 ./output/qwen3-poetry-instruct/final/ 是否存在
  • 查看训练日志是否显示 "✓ 训练完成!"

4. 调整生成参数:

# 修改 text_trained_instruct.py
TEMPERATURE = 0.7  # 降低随机性 (从 0.8 改为 0.7)
TOP_P = 0.8        # 降低采样范围 (从 0.9 改为 0.8)

9.4 性能优化问题

问题 7: 训练速度太慢

现状: 每步训练时间超过 30 秒

优化方案:

1. 增大批次大小 (如果显存够):

BATCH_SIZE = 4  # 从 2 改为 4

2. 减小序列长度:

MAX_LENGTH = 128  # 从 256 改为 128 (诗词较短,128 足够)

3. 减少数据量 (快速测试):

dataset = dataset.select(range(10000))  # 用 10K 测试

4. 确保已启用优化:

USE_FP16 = True                # 混合精度训练 ✅
GRADIENT_CHECKPOINTING = True  # 梯度检查点 ✅

预期效果:

  • 优化前: 30 秒/步
  • 优化后: 6-8 秒/步 ✅
  • 提速 4-5 倍! 🚀

十、总结

恭喜!到这里你已经掌握了基于 ModelScope 本地部署 Qwen3-4B 模型(含指令版)进行 LoRA 微调训练的完整流程。

本教程内容回顾

我们从零开始,依次完成了以下关键步骤:

  1. 环境准备:安装 LoRA 微调专用库(peft、datasets 等)
  2. 模型下载:使用 ModelScope 命令行工具下载 Qwen3-4B-Instruct-2507 模型
  3. 数据集准备:下载中文诗词数据集,转换为 JSON 格式
  4. 模型训练:配置 LoRA 参数,执行微调训练
  5. 效果测试:加载训练后的模型,生成诗词并评估质量
  6. 模型对比:对比 Base 版、Instruct 原始版和训练后的效果
  7. 问题解决:处理常见错误,如显存不足、训练速度慢等
  8. 参数调优:掌握 LoRA 秩、学习率、训练轮数等参数的作用

学习掌握的能力

完成本教程后,一般来说将能够:

独立部署大模型微调:掌握从环境配置到模型训练的完整流程
使用 ModelScope 平台:熟悉魔搭社区的模型下载和管理方式
配置 LoRA 微调:理解低秩矩阵分解原理,正确配置微调参数
排查常见错误:处理环境问题、显存不足、模型质量等问题
选择合适模型:理解 Base 与 Instruct 模型的本质区别
评估训练效果:掌握科学的模型对比和评估方法
调优训练参数:通过调整参数提升模型质量
优化训练性能:使用混合精度、梯度检查点等技术降低资源消耗

核心结论与建议

🏆 最佳实践:使用 Qwen3-4B-Instruct-2507 (指令版)

基于实际测试,指令版模型在诗词生成任务中表现显著优于基础版:

项目基础版指令版结论
指令理解⭐⭐⭐⭐⭐⭐⭐指令版胜
诗词完整性⭐⭐⭐⭐⭐⭐⭐指令版胜
诗词质量⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐相当
创造力⭐⭐⭐⭐⭐⭐⭐⭐⭐指令版胜
综合评分74%94%指令版完胜

训练效果总结:

  • 20K 数据 × 1 轮:快速训练,约 1.5-2 小时,综合得分 94%
  • 诗词质量提升 25%:意境更优美,语言更流畅
  • 创造力提升 67%:不再背诵原诗,完全原创
  • 解决“只写 2 句”问题:成功率从 0% 提升到 100%

推荐配置:

# 快速测试(推荐新手)
模型: Qwen3-4B-Instruct-2507
数据: 20K
轮数: 1
LoRA秩: 64
时间:1.5-2 小时
效果: 94 分 ✅

# 完整训练(追求最佳)
模型: Qwen3-4B-Instruct-2507
数据: 100K388K
轮数: 3
LoRA秩: 64
时间:8-25 小时
效果: 预计 96+

后续学习建议

如果想进一步提升,可以尝试:

  • 探索其他模型:在 ModelScope 上尝试 Qwen3-8B、DeepSeek 等其他模型
  • 学习 QLoRA 微调:使用量化技术进一步降低训练成本
  • 收集领域数据:训练客服对话、知识问答等专用模型
  • 集成到应用:将模型集成到 Web 应用、聊天机器人等实际场景
  • 探索 RAG 技术:结合知识库和检索增强生成,构建智能问答系统

感谢阅读

希望这篇教程能帮助你顺利掌握大语言模型微调技术!如果遇到问题,欢迎反馈和交流。

祝你在 AI 领域不断探索,收获满满! 🚀