diff --git a/models/Gemma4/.keep b/models/Gemma4/.keep new file mode 100644 index 0000000..e69de29 diff --git a/models/Gemma4/01-gemma-4-E4B-it FastApi 部署调用.md b/models/Gemma4/01-gemma-4-E4B-it FastApi 部署调用.md new file mode 100644 index 0000000..f014318 --- /dev/null +++ b/models/Gemma4/01-gemma-4-E4B-it FastApi 部署调用.md @@ -0,0 +1,409 @@ +# gemma-4-E4B-it FastApi 部署调用 + +> Gemma 4 E4B-it 为 Google 开源的多模态指令模型(文本 / 图像 / 音频,小尺寸型号),上下文 **128K**,在 Transformers 中推荐使用 `AutoProcessor` + `AutoModelForMultimodalLM`。概述与生态见 [Welcome Gemma 4](https://huggingface.co/blog/gemma4),加载与聊天模板细节见模型卡 [google/gemma-4-E4B-it](https://huggingface.co/google/gemma-4-E4B-it)。 + +## 环境准备 + +本文基础环境如下(与 2026 年本地实测的 CUDA 12.4 + PyTorch 2.6 亦可兼容,请按你本机驱动选择合适 wheel): + +``` +---------------- +ubuntu 22.04(或同类 Linux) +python 3.12 +cuda 12.4+(与 PyTorch 所带 cu12 运行时一致即可) +pytorch 2.5+ / 2.6+(需带 CUDA 的构建) +transformers ≥ 4.51(推荐 5.x,含 AutoModelForMultimodalLM) +torchvision(与 torch 同 CUDA 版本,供 Gemma4Processor 使用) +---------------- +``` + +> 请先安装好 **带 CUDA 的 PyTorch**,再安装其余依赖。若仅使用 CPU,多模态推理速度会极慢,且部分配置可能不可用,本文不展开。 + +首先升级 `pip`,并视网络情况选择镜像(若清华源偶发「找不到 numpy 等包」,可改用 **阿里云** `https://mirrors.aliyun.com/pypi/simple/` 或 **官方 PyPI**): + +```shell +python -m pip install --upgrade pip +# 可选:pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple + +pip install requests==2.32.3 +pip install fastapi==0.115.8 +pip install uvicorn==0.34.0 +pip install huggingface-hub==0.28.1 +pip install "transformers>=4.51.0" accelerate +# PyTorch:请按 https://pytorch.org/get-started/locally/ 选择与你 CUDA 匹配的命令,例如 cu124: +# pip install torch --index-url https://download.pytorch.org/whl/cu124 +pip install torchvision --index-url https://download.pytorch.org/whl/cu124 +``` + +> 考虑到部分同学配置环境可能会遇到一些问题,我们在 AutoDL 平台准备了 gemma-4-E4B-it 的环境镜像,点击下方链接并直接创建实例即可。 +> ***https://www.codewithgpu.com/i/datawhalechina/self-llm/self-llm-gemma4*** + +## 模型下载 + +权重仓库:**`google/gemma-4-E4B-it`**(Hugging Face)。网络受限时可设置镜像,例如: + +```shell +export HF_ENDPOINT=https://hf-mirror.com +``` + +**方式一:Hugging Face** — 使用 `huggingface_hub` 的 `snapshot_download`: + +```python +from huggingface_hub import snapshot_download + +model_dir = snapshot_download("google/gemma-4-E4B-it", cache_dir="/root/autodl-tmp") +``` + +**方式二:ModelScope(国内常用)** — 例如: + +```shell +modelscope download --model google/gemma-4-E4B-it --local_dir /dataset/gemma-4-E4B-it +``` + +下载完成后,目录内应包含 `config.json`、`model.safetensors`、`tokenizer.json`、`processor_config.json` 等。启动 API 前设置环境变量 **`GEMMA_MODEL_PATH`** 指向该目录(例如 `/dataset/gemma-4-E4B-it`);若不设置,[api.py](./api.py) 默认使用该路径。 + +> 注意:`cache_dir` / `--local_dir` 请改为你本机实际路径;单文件 `model.safetensors` 体积较大,请预留磁盘与显存。 + +## 代码准备 + +本仓库已在同目录提供可运行的 **[api.py](./api.py)**(经实测可在本地权重目录下正常推理)。你也可以新建 `api.py` 并粘贴下方代码,或直接使用仓库内文件。 + +> **环境版本提示(2026 实测)** +> - 需 **较新的 `transformers`**(例如 **≥ 4.51**;当前常见发行版为 **5.x**),才导出 **`AutoModelForMultimodalLM`**。 +> - 加载 **`AutoProcessor`** / `Gemma4VideoProcessor` 需安装与 PyTorch CUDA 版本一致的 **`torchvision`**(例如 CUDA 12.4 对应:`pip install torchvision --index-url https://download.pytorch.org/whl/cu124`)。 +> - **`fastapi==0.115.8`** 依赖 **Pydantic v2**,请勿再使用已废弃的 `@validator`、`min_items`、`BaseModel.dict()`;路由参数勿命名为 `request`,以免与 Starlette 的 `Request` 冲突。 +> - 若 `pip config` 使用清华源出现「No matching distribution」,可改用阿里云镜像或官方 PyPI。 + +```python +# api.py — 与教程 01 一致,已适配 Pydantic v2 / FastAPI,并支持本地模型路径 +from __future__ import annotations + +import logging +import os +import time +from contextlib import asynccontextmanager +from typing import List, Literal, Optional + +import torch +import uvicorn +from fastapi import Body, FastAPI, HTTPException +from pydantic import BaseModel, Field, model_validator +from transformers import AutoModelForMultimodalLM, AutoProcessor + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +DEVICE = "cuda" +DEVICE_ID = os.environ.get("CUDA_DEVICE_ID", "0") +CUDA_DEVICE = f"{DEVICE}:{DEVICE_ID}" if DEVICE_ID else DEVICE + +MODEL_PATH = os.environ.get("GEMMA_MODEL_PATH", "/dataset/gemma-4-E4B-it") + +model = None +processor = None +DEFAULT_SYSTEM_PROMPT = "You are a helpful assistant." + + +def torch_gc(): + if torch.cuda.is_available(): + with torch.cuda.device(CUDA_DEVICE): + torch.cuda.empty_cache() + torch.cuda.ipc_collect() + + +class ContentItem(BaseModel): + type: Literal["text", "image"] + text: Optional[str] = Field(None, description="文本内容(当 type 为 text 时必填)") + image: Optional[str] = Field(None, description="图片 URL 或 base64(当 type 为 image 时必填)") + + @model_validator(mode="after") + def validate_content(self): + if self.type == "text": + if not self.text or not str(self.text).strip(): + raise ValueError("文本类型必须提供 text 字段") + elif self.type == "image": + img = self.image or "" + if not str(img).startswith(("http://", "https://", "data:image")): + raise ValueError("图片必须是有效的 URL 或 base64 编码字符串") + return self + + +class Message(BaseModel): + role: Literal["system", "user", "assistant"] + content: List[ContentItem] + + +class ProcessRequest(BaseModel): + messages: List[Message] = Field(..., min_length=1, description="对话历史记录") + max_new_tokens: int = Field(1000, ge=10, le=4096, description="生成的最大 token 数") + + +class ProcessResponse(BaseModel): + response: str + status: int + time: int + processing_time: float + tokens_generated: int + + +def load_models(): + global model, processor + if not os.path.isdir(MODEL_PATH): + raise FileNotFoundError(f"模型目录不存在: {MODEL_PATH}") + try: + logger.info("正在加载模型: %s", MODEL_PATH) + model = AutoModelForMultimodalLM.from_pretrained( + MODEL_PATH, + dtype="auto", + device_map="auto", + trust_remote_code=True, + ).eval() + logger.info("正在加载处理器...") + processor = AutoProcessor.from_pretrained(MODEL_PATH, trust_remote_code=True) + logger.info("模型加载完成 device=%s", getattr(model, "device", "?")) + except Exception as e: + logger.error("模型加载失败: %s", e) + raise + + +@asynccontextmanager +async def lifespan(app: FastAPI): + try: + load_models() + yield + except Exception as e: + logger.error("服务初始化失败: %s", e) + raise + finally: + torch_gc() + + +app = FastAPI(lifespan=lifespan) + + +def _normalize_content_items(items): + out = [] + for it in items: + if it.get("type") == "text": + out.append({"type": "text", "text": it.get("text") or ""}) + elif it.get("type") == "image": + img = it.get("image") or it.get("url") + if not img: + continue + if str(img).startswith(("http://", "https://")): + out.append({"type": "image", "url": img}) + else: + out.append({"type": "image", "image": img}) + return out + + +@app.post("/chat/completions", response_model=ProcessResponse) +async def generate_response(payload: ProcessRequest = Body(...)): + start_time = time.time() + try: + processed_messages = [] + system_prompt = DEFAULT_SYSTEM_PROMPT + + for msg in payload.messages: + if msg.role == "system": + system_prompt = " ".join( + [item.text or "" for item in msg.content if item.type == "text"] + ) + else: + d = msg.model_dump() + d["content"] = _normalize_content_items(d["content"]) + processed_messages.append(d) + + messages = [ + {"role": "system", "content": [{"type": "text", "text": system_prompt}]}, + *processed_messages, + ] + + inputs = processor.apply_chat_template( + messages, + add_generation_prompt=True, + tokenize=True, + return_tensors="pt", + return_dict=True, + ).to(model.device) + + input_len = inputs["input_ids"].shape[-1] + max_token_num = min(4096, int(payload.max_new_tokens)) + with torch.inference_mode(): + generation = model.generate( + **inputs, + max_new_tokens=max_token_num, + do_sample=False, + ) + response_ids = generation[0][input_len:] + raw = processor.decode(response_ids, skip_special_tokens=False) + try: + parsed = processor.parse_response(raw) + decoded = parsed.get("content", raw) if isinstance(parsed, dict) else raw + except Exception: + decoded = processor.decode(response_ids, skip_special_tokens=True) + + ntok = int(response_ids.numel()) if hasattr(response_ids, "numel") else len(response_ids) + return ProcessResponse( + response=str(decoded), + status=200, + time=int(time.time()), + processing_time=time.time() - start_time, + tokens_generated=ntok, + ) + except HTTPException: + raise + except Exception as e: + logger.error("处理请求时出错: %s", e) + raise HTTPException(status_code=500, detail=str(e)) from e + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=6006) +``` + +> **配置说明** +> - **`GEMMA_MODEL_PATH`**:模型所在目录,默认 `/dataset/gemma-4-E4B-it`。示例:`export GEMMA_MODEL_PATH=/你的路径/gemma-4-E4B-it`。 +> - **`CUDA_DEVICE_ID`**:CUDA 设备号,默认 `0`。 + + +## Api 部署 + +在终端输入以下命令启动api服务: + +```shell +python api.py +``` + +加载完毕后出现如下信息说明成功。 + +![](./images/01-1.png) + +默认部署在 **6006** 端口,通过 **POST** `/chat/completions` 调用。 + +**建议先做一次纯文本小额生成**(`max_new_tokens` 较小便于确认服务与权重正常;4090 级以下显卡上首次推理可能需数十秒级): + +```shell +curl -sS -X POST "http://127.0.0.1:6006/chat/completions" \ + -H 'Content-Type: application/json' \ + -d '{"messages":[{"role":"user","content":[{"type":"text","text":"用一句话自我介绍。"}]}],"max_new_tokens":64}' +``` + +**图文(与下图同一公开图床 URL 的示例,实测可通过)**: + +```shell +curl -X POST "http://127.0.0.1:6006/chat/completions" \ + -H 'Content-Type: application/json' \ + -d '{"messages": [{"role": "user","content": [{"type": "image", "image": "http://gips3.baidu.com/it/u=1821127123,1149655687&fm=3028&app=3028&f=JPEG&fmt=auto?w=720&h=1280"},{"type": "text", "text": "请描述图片中的详情信息"}]}],"max_new_tokens": 4096}' +``` + +也可以使用 python 中的 requests 库进行调用,如下所示: + +```python +import requests +import json + +def get_completion(): + headers = {'Content-Type': 'application/json'} + data = { + "messages": [ + { + "role": "user", + "content": [ + {"type": "image", "image": "http://gips3.baidu.com/it/u=1821127123,1149655687&fm=3028&app=3028&f=JPEG&fmt=auto?w=720&h=1280"}, + {"type": "text", "text": "请描述图片中的详情信息"} + ] + } + ], + "max_new_tokens": 4096 + } + response = requests.post(url='http://127.0.0.1:6006/chat/completions', headers=headers, data=json.dumps(data)) + return response.json()['response'] + +if __name__ == '__main__': + print(get_completion()) +``` + +返回 JSON 字段说明:`response` 为模型输出;`status` 固定为 200 表示业务成功;`processing_time` 为耗时(秒);`tokens_generated` 为生成 token 数。**具体文案与耗时因机器、驱动、`max_new_tokens` 及是否多模态而异。** + +实测示例(**RTX 4060 Ti 16GB**,本地 `GEMMA_MODEL_PATH` 指向已下好的权重;仅作格式参考): + +```json +{ + "response": "我是一个由 Google 训练的大型语言模型,旨在为您提供帮助和信息。", + "status": 200, + "time": 1743648000, + "processing_time": 62.5, + "tokens_generated": 19 +} +``` + +图文同类请求在相同环境下约 **40s 级** 可完成(`max_new_tokens` 较小时会更短),以本机实测为准。 +![](./images/01-2.png) + +## message 支持的几种类型 + +### 01.纯文本对话 +```json +{ + "messages": [ + { + "role": "user", + "content": [ + {"type": "text", "text": "帮我写一份科幻小说的大纲!"} + ] + } + ], + "max_new_tokens": 4096 +} +``` + +### 02.纯文本+图片URL +```json +{ + "messages": [ + { + "role": "user", + "content": [ + {"type": "image", "image": "http://gips3.baidu.com/it/u=1821127123,1149655687&fm=3028&app=3028&f=JPEG&fmt=auto?w=720&h=1280"}, + {"type": "text", "text": "请描述图片中的详情信息"} + ] + } + ], + "max_new_tokens": 4096 +} +``` + +### 03.纯文本+图片Base64信息 +> 说明:下面使用 **1×1 透明 PNG** 的最短 Base64 作为占位示例;实际使用时请将你本地的图片完整编码为 `data:image/png;base64,...`(或 jpeg)。不建议在文档或日志中粘贴极长 Base64。 + +```json +{ + "messages": [ + { + "role": "user", + "content": [ + {"type": "image", "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="}, + {"type": "text", "text": "请描述图片的详情信息"} + ] + } + ], + "max_new_tokens": 256 +} +``` + +### 04.纯文本+多张图片(混合) +```json +{ + "messages": [ + { + "role": "user", + "content": [ + {"type": "image", "image": "data:image/png;base64,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"}, + {"type": "text", "text": "请分别描述所有的图片的详情"} + ] + } + ], + "max_new_tokens": 4096 +} +``` \ No newline at end of file diff --git a/models/Gemma4/03-gemma-4-E4B-it-ollama + open-webui部署.md b/models/Gemma4/03-gemma-4-E4B-it-ollama + open-webui部署.md new file mode 100644 index 0000000..7eb66a8 --- /dev/null +++ b/models/Gemma4/03-gemma-4-E4B-it-ollama + open-webui部署.md @@ -0,0 +1,112 @@ +# ollama + open-webui 部署 Gemma 4 E4B-it 模型 + +> 权重与能力说明见 [google/gemma-4-E4B-it](https://huggingface.co/google/gemma-4-E4B-it) 与 [Gemma 4 博文](https://huggingface.co/blog/gemma4)。本地推理也可使用 llama.cpp:`llama-server -hf ggml-org/gemma-4-E4B-it-GGUF`。 + +Ollama 是一个开源的大语言模型服务工具,旨在帮助用户快速在**本地**运行大模型。 + +Open WebUI 是一个**可扩展、功能丰富、用户友好的自托管WebUI**,旨在完全离线操作。它支持各种LLM运行程序,包括Ollama和OpenAI兼容的API。 + +本教程使用 Ollama **本地部署**与 Hugging Face 上 `google/gemma-4-E4B-it` 对应的 **`gemma4:e4b`** 标签(以 [Ollama Library](https://ollama.com/library/gemma4) 为准),并用 Open WebUI 部署 Web 界面。 + +## 环境准备 + +``` +ubuntu 22.04 +python 3.12 +pytorch 2.5.1 +cuda 12.4 +``` + +本文默认学习者已安装好如上环境,如未安装请自行安装。 + +## 安装 ollama + +### 1. macOS和Windows系统安装 + +macOS用户通过[此安装包链接](https://ollama.com/download/Ollama-darwin.zip)下载安装ollama + +Windows用户通过[此安装包链接](https://ollama.com/download/OllamaSetup.exe)下载安装ollama + +### 2. Linux系统安装 + +方案一:在终端输入以下命令,**自动安装ollama** + +```bash +curl -fsSL https://ollama.com/install.sh | sh +``` + +方案二:在终端输入以下命令,**手动安装ollama** + +```bash +curl -L https://ollama.com/download/ollama-linux-amd64.tgz -o ollama-linux-amd64.tgz +sudo tar -C /usr -xzf ollama-linux-amd64.tgz +``` +如果出现无法下载安装包的情况,修改GitHub镜像源之后再下载安装 + +```bash +curl -L https://git.886.be/https://github.com/ollama/ollama/releases/download/v0.6.0/ollama-linux-amd64.tgz -o ollama-linux-amd64.tgz +sudo tar -C /usr -xzf ollama-linux-amd64.tgz +``` + +> 考虑到部分同学配置环境可能会遇到一些问题,我们在 AutoDL 平台准备了 gemma-4-E4B-it 的环境镜像,点击下方链接并直接创建 Autodl 示例即可。 +> ***https://www.codewithgpu.com/i/datawhalechina/self-llm/self-llm-gemma4*** + + +## 运行 ollama + +```bash +ollama serve +``` + +## 下载并运行 Gemma 4 E4B(Ollama:`gemma4:e4b`) + +```bash +ollama run gemma4:e4b +``` + +## 查看模型运行状态,以检测是否运行模型 + +```bash +ollama ps +``` + +## 下载 open-webui + +```bash +# 升级 pip +python -m pip install --upgrade pip +# 更换 pypi 源加速库的安装 +pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple + +pip install open-webui==0.5.20 +``` + +## 运行 open-webui + +```bash +open-webui serve +``` + +openwebui默认在8080端口运行,如需修改服务端口,请输入如下命令: + +```bash +open-webui serve --port 6006 +``` + +如果出现 `Connection to huggingface.co timed out` 等报错,添加环境变量修改镜像源后再运行服务: + +```bash +export HF_ENDPOINT=https://hf-mirror.com +``` + +## 访问 open-webui + +打开浏览器,访问 http://localhost:6006 即可访问 open-webui。 + +在开启 Ollama 服务并运行 `gemma4:e4b` 后,Open WebUI 会**自动检测**本地 Ollama 并选用该模型。 + +![03-1](./images/03-1.png) + +## 测试 Gemma 4 E4B-it 可用性 + +![03-2](./images/03-2.png) \ No newline at end of file diff --git a/models/Gemma4/04-Gemma4-E4B-it evalscope智商情商评测.md b/models/Gemma4/04-Gemma4-E4B-it evalscope智商情商评测.md new file mode 100644 index 0000000..b687299 --- /dev/null +++ b/models/Gemma4/04-Gemma4-E4B-it evalscope智商情商评测.md @@ -0,0 +1,107 @@ +# 04-Gemma4-E4B-it evalscope智商情商评测 + +> 评测对象可与 Ollama 模型 **`gemma4:e4b`**(对应 Hugging Face **[google/gemma-4-E4B-it](https://huggingface.co/google/gemma-4-E4B-it)**)对齐;概述见 [Gemma 4 博文](https://huggingface.co/blog/gemma4)。 + +## 大模型评测是什么 +- 大语言模型评测是指对大语言模型(LLM)在多种任务和场景下的性能进行全面评估的过程。评测的目的是衡量模型的通用能力、特定领域表现、效率、鲁棒性、安全性等多方面性能,以便优化模型设计、指导技术选型和推动模型在实际应用中的部署。 +- 评测的主要内容 +通用能力:评估模型在语言理解、生成、推理等方面的基础能力。 +特定领域表现:针对特定任务(如数学推理、代码生成、情感分析等)的性能评估。 +效率与资源消耗:包括模型的训练和推理时间、计算资源需求等。 +鲁棒性与可靠性:评估模型在面对噪声、对抗攻击或输入扰动时的稳定性。 +伦理与安全性:检测模型是否会产生有害内容、是否存在偏见或歧视。 +- EvalScope是魔搭社区官方推出的模型评测与性能基准测试框架,内置多个常用测试基准和评测指标,如MMLU、CMMLU、C-Eval、GSM8K、ARC、HellaSwag、TruthfulQA、MATH和HumanEval等;支持多种类型的模型评测,包括LLM、多模态LLM、embedding模型和reranker模型。EvalScope还适用于多种评测场景,如端到端RAG评测、竞技场模式和模型推理性能压测等。此外,通过ms-swift训练框架的无缝集成,可一键发起评测,实现了模型训练到评测的全链路支持。 +官网地址:https://evalscope.readthedocs.io/zh-cn/latest/get_started +# evalscope评测使用方法 +## 环境准备 +本文基础环境如下: + +``` +---------------- +ubuntu 22.04 +python 3.12 +Cuda 12.4 +PyTorch 2.5.1 +---------------- +``` +2. **pip安装evalscope:** +``` +pip install evalscope # 安装 Native backend (默认) +# 额外选项 +pip install evalscope[opencompass] # 安装 OpenCompass backend +pip install evalscope[vlmeval] # 安装 VLMEvalKit backend +pip install evalscope[rag] # 安装 RAGEval backend +pip install evalscope[perf] # 安装 模型压测模块 依赖 +pip install evalscope[all] # 安装所有 backends (Native, OpenCompass, VLMEvalKit, RAGEval) +``` + +> 考虑到部分同学配置环境可能会遇到一些问题,我们在 AutoDL 平台准备了 gemma-4-E4B-it 的环境镜像,点击下方链接并直接创建 Autodl 示例即可。 +> ***https://www.codewithgpu.com/i/datawhalechina/self-llm/self-llm-gemma4*** + + +## 模型评测方法 +1. **创建ollama服务器** +这里首先使用ollama创建兼容 OpenAI API 接口的服务器,然后使用evalscope进行评测。当然接入其他的api也是可以的。 +```bash +curl -L https://git.886.be/https://github.com/ollama/ollama/releases/download/v0.6.0/ollama-linux-amd64.tgz -o ollama-linux-amd64.tgz +sudo tar -C /usr -xzf ollama-linux-amd64.tgz +``` +```bash +ollama serve #运行ollama服务器 +``` + +新建一个bash窗口 +```bash +ollama run gemma4:e4b +``` +此时可以在控制台直接与模型对话。 + +1. **执行评测** +新建eval_api.py文件,并输入以下代码: +``` +# 导入执行任务的函数和任务配置类 +from evalscope.run import run_task +from evalscope.config import TaskConfig + +""" +以下为多个AI服务的API端点地址,用于配置任务: +- siliconflow: https://api.siliconflow.cn/v1/chat/completions +- dashscope: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions +- modelscope: https://api-inference.modelscope.cn/v1/chat/completions +- xunfei: https://maas-api.cn-huabei-1.xf-yun.com/v1/chat/completions +""" + +# 配置任务参数 +task_cfg = TaskConfig( + model='gemma4:e4b', # 指定使用的模型 + api_url='http://localhost:11434/v1/chat/completions', # 指定API端点,这里使用的是ollama默认的api接口 + api_key='sk-xxxxxxx', # API密钥(需替换为实际密钥,ollama 的api_key) + eval_type='service', # 指定评估类型为服务模式 + datasets=['iquiz'], # 指定使用的数据集(这个测试集可以快速测试模型的智商和情商) + generation_config={ # 文本生成配置 + 'max_tokens': 4096, # 最大令牌数 + 'max_new_tokens': 4096, # 最大新生成令牌数 + 'temperature': 1.0, # 温度参数,这里设置为1.0,模型的输出随机性较大,所以可能会有些实验误差 + }, + work_dir='outputs/Gemma4-E4B-it', # 输出目录 +) + +# 执行任务 +run_task(task_cfg=task_cfg) +``` + +新建一个bash窗口,也就是控制台中执行。 +控制台运行`python eval_api.py`命令即可。 +等待3分钟左右评测就完成啦,控制台输出的结果如下图所示: +![](./images/04-01.png) +实验结果可能有误差,因为在评测任务配置中我们把temperature调到了1.0,如果调小一些,可能会得到更精确的结果。 +可以看到模型的得分还是不错的,模型评测的文件保存在`/root/outputs/Gemma4-E4B-it/20250315_164601/reviews/Gemma4-E4B-it`目录下。 +![](./images/04-05.png) +## evalscope简介: +- 支持多种模型评测backend,包括OpenAI API、OpenCompass、VLMEvalKit、RAGEval等。 +![](./images/04-02.png) +- 支持自定义评测任务和数据集,支持多种评测指标。 +![](./images/04-03.png) + +模型评测对于验证和优化大模型如Gemma4-E4B-it至关重要。通过评测,我们可以全面了解模型的性能、能力边界及潜在问题,确保其在实际应用中的表现符合预期,并推动持续改进。此外,评测还能检测模型的公平性和安全性,提升用户体验,并为不同模型间的对比分析提供客观依据。最终,评测结果为后续版本迭代提供了关键数据支持,保障模型在实际场景中的可靠性和有效性。 + diff --git a/models/Gemma4/05-gemma-4-E4B-it LoRA.ipynb b/models/Gemma4/05-gemma-4-E4B-it LoRA.ipynb new file mode 100644 index 0000000..8058103 --- /dev/null +++ b/models/Gemma4/05-gemma-4-E4B-it LoRA.ipynb @@ -0,0 +1,1957 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "20d2d16a-7f75-42e9-9745-e08ee8ccb309", + "metadata": {}, + "source": [ + "# 导入环境" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "12ddcb7d-b41c-4e68-bcfd-dc192e39d19e", + "metadata": {}, + "outputs": [], + "source": [ + "from datasets import Dataset\n", + "import pandas as pd\n", + "from transformers import AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq, TrainingArguments, Trainer, GenerationConfig" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "53c06dd1-8a7f-4bb9-a0d6-a96a4ccd3898", + "metadata": {}, + "outputs": [], + "source": [ + "# 读取json数据集文件\n", + "df = pd.read_json('dataset/huanhuan.json')\n", + "ds = Dataset.from_pandas(df)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "9b146696-f7c1-45e9-9160-6609427cd182", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'instruction': ['小姐,别的秀女都在求中选,唯有咱们小姐想被撂牌子,菩萨一定记得真真儿的——',\n", + " '这个温太医啊,也是古怪,谁不知太医不得皇命不能为皇族以外的人请脉诊病,他倒好,十天半月便往咱们府里跑。',\n", + " '嬛妹妹,刚刚我去府上请脉,听甄伯母说你来这里进香了。'],\n", + " 'input': ['', '', ''],\n", + " 'output': ['嘘——都说许愿说破是不灵的。', '你们俩话太多了,我该和温太医要一剂药,好好治治你们。', '出来走走,也是散心。']}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds[:3] # 展示前三组数据" + ] + }, + { + "cell_type": "markdown", + "id": "ac65caa6-2e8c-4794-aadf-2a59e6a0bde7", + "metadata": {}, + "source": [ + "# 处理数据集" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "bbd74576-8329-4f8c-80b2-8959b7a25214", + "metadata": {}, + "outputs": [], + "source": [ + "model_path = 'autodl-tmp/google/gemma-4-E4B-it'\n", + "\n", + "tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False, trust_remote_code=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "db3cf8c9-8899-4da9-8f48-2dc5813420fe", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "user\n", + "You are a helpful assistant.\n", + "\n", + "你好呀\n", + "model\n", + "有什么可以帮你的?\n", + "model\n", + "\n" + ] + } + ], + "source": [ + "# 使用tokenizer构建messages并打印, 查看chat_template的输出格式\n", + "messages = [\n", + " {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n", + " {\"role\": \"user\", \"content\": '你好呀'},\n", + " {\"role\": \"assistant\", \"content\": '有什么可以帮你的?'}\n", + " ]\n", + "# 使用chat_template将messages格式化并打印\n", + "print(tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True))" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "325b08dc-7d5d-4c4d-9dfd-af097ee1647f", + "metadata": {}, + "outputs": [], + "source": [ + "system_prompt = '现在你要扮演皇帝身边的女人--甄嬛'\n", + "\n", + "def process_func(example):\n", + " MAX_LENGTH = 384 # 分词器会将一个中文字切分为多个token,因此需要放开一些最大长度,保证数据的完整性\n", + " input_ids, attention_mask, labels = [], [], []\n", + " # 构建指令部分的输入, 可参考上面的输出格式进行调整和补充\n", + " instruction = tokenizer(\n", + " f\"<|im_start|>system\\n{system_prompt}<|im_end|>\\n\" \n", + " f\"<|im_start|>user\\n{example['instruction'] + example['input']}<|im_end|>\\n\" \n", + " f\"<|im_start|>assistant\\n\", \n", + " add_special_tokens=False \n", + " )\n", + " # 构建模型回复部分的输入\n", + " response = tokenizer(\n", + " f\"{example['output']}\",\n", + " add_special_tokens=False \n", + " )\n", + " # 拼接指令和回复部分的 input_ids\n", + " input_ids = instruction[\"input_ids\"] + response[\"input_ids\"] + [tokenizer.pad_token_id]\n", + " # 拼接指令和回复部分的 attention_mask\n", + " attention_mask = instruction[\"attention_mask\"] + response[\"attention_mask\"] + [1] # 因为 EOS token 也需要关注,所以补充为 1\n", + " # 构建标签\n", + " # 对于指令部分,使用 -100 忽略其损失计算;对于回复部分,保留其 input_ids 作为标签\n", + " labels = [-100] * len(instruction[\"input_ids\"]) + response[\"input_ids\"] + [tokenizer.pad_token_id] \n", + " # 如果总长度超过最大长度,进行截断\n", + " if len(input_ids) > MAX_LENGTH: \n", + " input_ids = input_ids[:MAX_LENGTH]\n", + " attention_mask = attention_mask[:MAX_LENGTH]\n", + " labels = labels[:MAX_LENGTH]\n", + " return {\n", + " \"input_ids\": input_ids,\n", + " \"attention_mask\": attention_mask,\n", + " \"labels\": labels\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "1429b7d8-a41a-4ea3-b26c-d45cdb2fdd2b", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9d6209c945154d919cb78b38d50c99c5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map: 0%| | 0/3729 [00:00<|im_start|>system\n", + "现在你要扮演皇帝身边的女人--甄嬛<|im_end|>\n", + "<|im_start|>user\n", + "这个温太医啊,也是古怪,谁不知太医不得皇命不能为皇族以外的人请脉诊病,他倒好,十天半月便往咱们府里跑。<|im_end|>\n", + "<|im_start|>assistant\n", + "你们俩话太多了,我该和温太医要一剂药,好好治治你们。\n" + ] + } + ], + "source": [ + "# 解码输入\n", + "print(tokenizer.decode(tokenized_id[1]['input_ids']))" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "1bb9fc43-69f9-4ab4-86e1-51c95a4eac0e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "你们俩话太多了,我该和温太医要一剂药,好好治治你们。\n" + ] + } + ], + "source": [ + "# 解码标签, 过滤掉-100\n", + "print(tokenizer.decode(list(filter(lambda x: x != -100, tokenized_id[1][\"labels\"]))))" + ] + }, + { + "cell_type": "markdown", + "id": "37a0fc7f-7adc-4926-b55f-3cb7dd932be6", + "metadata": {}, + "source": [ + "# 创建模型" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "2e34e258-9f38-4b49-9716-2b1d358c3aa1", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4a36c1b2c3b345cb9a09290a10564aec", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Loading checkpoint shards: 0%| | 0/2 [00:00', attn_implementation='eager')`.\n", + "`use_cache=True` is incompatible with gradient checkpointing. Setting `use_cache=False`.\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + "
\n", + " \n", + " \n", + " [2796/2796 52:40, Epoch 2/3]\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
StepTraining Loss
1027.133600
2021.347000
3018.589100
4018.701000
5016.447900
6015.465600
7015.476800
8014.833600
9014.242100
10013.849500
11015.143100
12013.934600
13013.579500
14012.739000
15014.071300
16013.429800
17013.409500
18014.614900
19013.298300
20013.242100
21013.760000
22013.903900
23014.041100
24013.384900
25014.652700
26012.969400
27012.975600
28013.404200
29013.863100
30014.716900
31014.040800
32012.587900
33012.495100
34014.416900
35013.454700
36014.047200
37013.050200
38012.059400
39012.534500
40014.332300
41013.280900
42014.451200
43013.236300
44013.571700
45013.524600
46013.190800
47013.185000
48012.262100
49012.489400
50012.732000
51013.114500
52014.342300
53014.097600
54012.669000
55012.153500
56013.363600
57013.394200
58012.186100
59013.366800
60013.428600
61013.010300
62014.218500
63012.775500
64013.784900
65012.800500
66013.242400
67012.458800
68013.079300
69012.395200
70011.744200
71012.563100
72013.779400
73012.900400
74012.184500
75011.759400
76011.909800
77013.100600
78013.054700
79011.948200
80013.018200
81014.420800
82012.306400
83012.885100
84011.820400
85011.164000
86013.010700
87013.241200
88013.301700
89012.702000
90013.442800
91012.595400
92013.190700
93013.797800
94011.768800
95012.767500
96012.375300
97011.610600
98011.844600
99011.325000
100012.058900
101010.899700
102011.994500
103011.704900
104011.676200
105010.593400
106011.973100
107012.000600
108011.815400
109011.436800
110012.153700
111012.267900
112011.660400
113011.483900
114013.132900
115013.530200
116010.713300
117010.883700
118011.058600
119012.374100
120012.718100
121012.706600
122012.324000
123010.218300
124011.259100
125012.191900
126010.984400
127012.198800
128011.847800
129012.257800
130011.900000
131011.586500
132012.289600
133011.941100
134010.703200
135012.183900
136011.044900
137011.332500
138013.122500
139012.043300
140011.035300
141011.430100
142011.944500
143011.218100
144012.226100
145011.405300
146012.749600
147011.846700
148011.604900
149012.057800
150011.820400
151012.523700
152011.595200
153011.262300
154012.352900
155011.983900
156012.054000
157010.725700
158010.493100
159011.607800
160011.746800
161011.440200
162012.634300
163012.863500
164012.078900
165012.261400
166012.390000
167011.103300
168011.621900
169012.873800
170011.474200
171010.678300
172011.178000
173011.302600
174010.563000
175011.708500
176012.005800
177010.724100
178010.598100
179011.424600
180011.857900
181012.254100
182010.849400
183011.654700
184011.842200
185011.841300
186011.610000
18709.994800
188011.234700
18909.631100
19009.725600
191011.134200
19209.617600
19309.195400
194011.525700
195010.249100
196011.692700
197010.598200
19809.821100
199010.650600
200010.869800
201011.417500
20209.355600
203010.887300
204010.432100
205011.677800
206010.629700
207010.937500
20809.539100
209010.302700
210010.111600
21109.845500
212010.648300
213010.955500
214010.503900
215011.879500
216011.138200
217011.775000
218010.980700
219010.989400
220010.217800
221011.486300
222011.496200
223010.566100
224010.561700
225010.774600
226011.621200
22709.948400
228010.848400
229010.021900
230011.530900
23109.752300
23209.879400
233010.337600
23408.862600
235011.082500
236011.376600
237010.513500
238010.111700
239011.432600
240010.896400
241010.551400
242012.013800
243010.691200
244011.052300
24509.853400
246011.384000
247010.882000
248010.998500
24909.820900
250011.692100
251011.007100
252010.614900
253010.080400
254010.767100
255010.712200
256010.911600
257010.830000
258010.686100
259010.838800
260010.170300
261011.887700
262011.243100
26309.874100
264011.052900
265011.489300
266012.466200
267011.467800
26809.634000
26909.034700
270010.596800
27109.692200
272010.336700
273010.506600
274010.627500
275010.666700
27609.632100
277010.286400
27809.931700
279011.270800

" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "TrainOutput(global_step=2796, training_loss=12.025420244841786, metrics={'train_runtime': 3162.7596, 'train_samples_per_second': 3.537, 'train_steps_per_second': 0.884, 'total_flos': 2.321808952845744e+16, 'train_loss': 12.025420244841786, 'epoch': 2.997586484312148})" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "trainer.train()" + ] + }, + { + "cell_type": "markdown", + "id": "6d725d46-6dc3-428b-870b-5275a0838457", + "metadata": {}, + "source": [ + "# 合并加载模型\n", + "\n", + "> 这里推荐大家在**训练结束重启一下notebook, 释放微调占用的GPU显存**, 否则容易出现如下警告\"Some parameters are on the meta device because they were offloaded to the cpu.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "0b484a15-a300-4be8-ab08-8722c1dc3b50", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4f177d8f03bf4de8bfd9fba99946e991", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Loading checkpoint shards: 0%| | 0/2 [00:00 基座模型:**[google/gemma-4-E4B-it](https://huggingface.co/google/gemma-4-E4B-it)**(Gemma 4 多模态指令版,详见 [Gemma 4 发布博文](https://huggingface.co/blog/gemma4))。以下 LoRA 示例以**纯文本对话**数据为主,与官方模型卡中的 `AutoModelForCausalLM` 文本路径一致;若需图像/音频管线请改用 `AutoModelForMultimodalLM`。 + +本节我们简要介绍如何基于 transformers、peft 等框架,使用由笔者合作开源的 [Chat-甄嬛](https://github.com/KMnO4-zx/huanhuan-chat) 项目中的**嬛嬛数据集**作为微调数据集,对 gemma-4-E4B-it 模型进行 LoRA 微调, 以构建一个能够模拟甄嬛对话风格的个性化 LLM , 数据集路径为[`../../dataset/huanhuan.json`](../../dataset/huanhuan.json)。 + +> **LoRA** 是一种高效微调方法,深入了解其原理可参见博客:[知乎|深入浅出 LoRA](https://zhuanlan.zhihu.com/p/650197598)。 + +> 本教程会在同目录下给大家提供一个 [**notebook** 文件 (05-gemma-4-E4B-it LoRA.ipynb)](05-gemma-4-E4B-it%20LoRA.ipynb) ,来帮助大家更好的学习。 + +## 环境配置 + +实验所依赖的基础开发环境如下: + +``` +---------------- +ubuntu 22.04 +Python 3.12.3 +cuda 12.4 +pytorch 2.5.1 +---------------- +``` +> 本文默认学习者已安装好以上 Pytorch(cuda) 环境,如未安装请自行安装。 + +首先 `pip` 换源加速下载并安装依赖包: + +```shell +# 升级pip +python -m pip install --upgrade pip +# 更换 pypi 源加速库的安装 +pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple + +# LoRA微调 相关依赖 +pip install peft==0.14.0 # 用于 LoRA 微调 + +# 通用依赖(Gemma 4 需较新 transformers,见模型卡) +pip install -U transformers accelerate +pip install huggingface_hub>=0.28.0 +pip install sentencepiece==0.2.0 # 用于处理文本数据 +pip install accelerate==1.5.1 # 用于分布式训练和混合精度训练 +pip install datasets==3.3.2 # 用于加载和处理数据集 +``` + +> 考虑到部分同学配置环境可能会遇到一些问题,我们在 AutoDL 平台准备了 gemma-4-E4B-it 的环境镜像,点击下方链接并直接创建 Autodl 示例即可。 +> ***https://www.codewithgpu.com/i/datawhalechina/self-llm/self-llm-gemma4*** + +## 模型下载 + +在 `/root/autodl-tmp` 路径下新建 `model_download.py` 文件并在其中粘贴以下代码,并保存文件。 + +```python +from huggingface_hub import snapshot_download + +model_dir = snapshot_download("google/gemma-4-E4B-it", cache_dir="./") +``` + +> 注意:记得修改 `cache_dir` 为你的模型下载路径;若访问 hub 较慢可设置环境变量 `HF_ENDPOINT`(如镜像站)。 + +在终端运行 `python /root/autodl-tmp/model_download.py` 执行下载,权重体积以 Hugging Face 模型页显示为准(Safetensors 约 8B 量级参数),下载时间取决于带宽。 + + +## 指令集构建 + +LLM 的微调一般指指令微调过程。所谓指令微调,是说我们使用的微调数据形如: + +```json +{ + "instruction": "回答以下用户问题,仅输出答案。", + "input": "1+1等于几?", + "output": "2" +} +``` + +其中,`instruction` 是用户指令,告知模型其需要完成的任务;`input` 是用户输入,是完成用户指令所必须的输入内容;`output` 是模型应该给出的输出。 + +即我们的核心训练目标是让模型具有理解并遵循用户指令的能力。因此,在指令集构建时,我们应针对我们的目标任务,针对性构建任务指令集。 + +例如,在本节我们使用由笔者合作开源的 [**Chat-甄嬛**](https://github.com/KMnO4-zx/huanhuan-chat) 项目作为示例,我们的目标是构建一个能够模拟甄嬛对话风格的个性化 LLM,因此我们构造的指令形如: + +```json +{ + "instruction": "你是谁?", + "input": "", + "output": "家父是大理寺少卿甄远道。" +} +``` + +我们所构造的全部指令数据集会被保存在根目录下。 + +## 数据格式化 + +`LoRA` 训练的数据是需要经过格式化、编码之后再输入给模型进行训练的,如果是熟悉 `Pytorch` 模型训练流程的同学会知道,我们一般需要将输入文本编码为 `input_ids`,将输出文本编码为 `labels`,编码之后的结果都是多维的向量。 + +为了得到 InternLM3-8b-Instruct 的 Prompt Template,使用 tokenizer 构建 messages 并打印, 查看 chat_template 的输出格式 + +```python +messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": '你好呀'}, + {"role": "assistant", "content": '有什么可以帮你的?'} + ] +# 使用chat_template将messages格式化并打印 +print(tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)) + + +## 得到输出结果如下 + +#user +#You are a helpful assistant. + +#你好呀 +#model +#有什么可以帮你的? +#model +``` + +然后我们就可以定义预处理函数 `process_func`,这个函数用于对每一个样本,编码其输入、输出文本并返回一个编码后的字典,方便模型使用: + +```python +system_prompt = '现在你要扮演皇帝身边的女人--甄嬛' + +def process_func(example): + MAX_LENGTH = 384 # 分词器会将一个中文字切分为多个token,因此需要放开一些最大长度,保证数据的完整性 + input_ids, attention_mask, labels = [], [], [] + # 构建指令部分的输入, 可参考上面的输出格式进行调整和补充 + instruction = tokenizer( + f"<|im_start|>system\n{system_prompt}<|im_end|>\n" + f"<|im_start|>user\n{example['instruction'] + example['input']}<|im_end|>\n" + f"<|im_start|>assistant\n", + add_special_tokens=False + ) + # 构建模型回复部分的输入 + response = tokenizer( + f"{example['output']}", + add_special_tokens=False + ) + # 拼接指令和回复部分的 input_ids + input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id] + # 拼接指令和回复部分的 attention_mask + attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1] # 因为 EOS token 也需要关注,所以补充为 1 + # 构建标签 + # 对于指令部分,使用 -100 忽略其损失计算;对于回复部分,保留其 input_ids 作为标签 + labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id] + # 如果总长度超过最大长度,进行截断 + if len(input_ids) > MAX_LENGTH: + input_ids = input_ids[:MAX_LENGTH] + attention_mask = attention_mask[:MAX_LENGTH] + labels = labels[:MAX_LENGTH] + return { + "input_ids": input_ids, + "attention_mask": attention_mask, + "labels": labels + } +``` + +> 补充: gemma-4-E4B-it 采用的 `Prompt Template`格式如下: + +```text +user +You are a helpful assistant. + +你好呀 +model +有什么可以帮你的? +model +``` + +## 加载 tokenizer 和半精度模型 (model) + +`tokenizer` 是将文本转换为模型 (`model`) 能理解的数字的工具,`model` 是根据这些数字生成文本的核心部分。 + +以半精度形式加载 `model`, 如果你的显卡比较新的话,可以用 `torch.bfolat` 形式加载。对于自定义模型,必须指定 `trust_remote_code=True` ,以确保加载自定义代码时不会报错。 + +```python +model_path = '/root/autodl-tmp/google/gemma-4-E4B-it' + +tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False, trust_remote_code=True) + +model = AutoModelForCausalLM.from_pretrained( + model_path, + device_map="auto", + torch_dtype=torch.bfloat16, + attn_implementation="eager", +) +``` + +> 注意:此处要记得修改为自己的模型路径哦~ + +如果想要查看模型结构,可以打印模型: + +```python +print(model) + +# 输出结果(结构名称随 transformers 版本可能略有不同,以本地打印为准)例如: +''' +Gemma4ForConditionalGeneration( + (vision_tower): SiglipVisionModel( + (vision_model): SiglipVisionTransformer( + (embeddings): SiglipVisionEmbeddings( + (patch_embedding): Conv2d(3, 1152, kernel_size=(14, 14), stride=(14, 14), padding=valid) + (position_embedding): Embedding(4096, 1152) + ) + (encoder): SiglipEncoder( + (layers): ModuleList( + (0-26): 27 x SiglipEncoderLayer( + (self_attn): SiglipSdpaAttention( + (k_proj): Linear(in_features=1152, out_features=1152, bias=True) + (v_proj): Linear(in_features=1152, out_features=1152, bias=True) + (q_proj): Linear(in_features=1152, out_features=1152, bias=True) + (out_proj): Linear(in_features=1152, out_features=1152, bias=True) + ) + (layer_norm1): LayerNorm((1152,), eps=1e-06, elementwise_affine=True) + (mlp): SiglipMLP( + (activation_fn): PytorchGELUTanh() + (fc1): Linear(in_features=1152, out_features=4304, bias=True) + (fc2): Linear(in_features=4304, out_features=1152, bias=True) + ) + (layer_norm2): LayerNorm((1152,), eps=1e-06, elementwise_affine=True) + ) + ) + ) + (post_layernorm): LayerNorm((1152,), eps=1e-06, elementwise_affine=True) + ) + ) + (multi_modal_projector): Gemma4MultiModalProjector( + (mm_soft_emb_norm): Gemma4RMSNorm((1152,), eps=1e-06) + (avg_pool): AvgPool2d(kernel_size=4, stride=4, padding=0) + ) + (language_model): Gemma4ForCausalLM( + (model): Gemma4TextModel( + (embed_tokens): Gemma4TextScaledWordEmbedding(262208, 2560, padding_idx=0) + (layers): ModuleList( + (0-33): 34 x Gemma4DecoderLayer( + (self_attn): Gemma4Attention( + (q_proj): Linear(in_features=2560, out_features=2048, bias=False) + (k_proj): Linear(in_features=2560, out_features=1024, bias=False) + (v_proj): Linear(in_features=2560, out_features=1024, bias=False) + (o_proj): Linear(in_features=2048, out_features=2560, bias=False) + (q_norm): Gemma4RMSNorm((256,), eps=1e-06) + (k_norm): Gemma4RMSNorm((256,), eps=1e-06) + ) + (mlp): Gemma4MLP( + (gate_proj): Linear(in_features=2560, out_features=10240, bias=False) + (up_proj): Linear(in_features=2560, out_features=10240, bias=False) + (down_proj): Linear(in_features=10240, out_features=2560, bias=False) + (act_fn): PytorchGELUTanh() + ) + (input_layernorm): Gemma4RMSNorm((2560,), eps=1e-06) + (post_attention_layernorm): Gemma4RMSNorm((2560,), eps=1e-06) + (pre_feedforward_layernorm): Gemma4RMSNorm((2560,), eps=1e-06) + (post_feedforward_layernorm): Gemma4RMSNorm((2560,), eps=1e-06) + ) + ) + (norm): Gemma4RMSNorm((2560,), eps=1e-06) + (rotary_emb): Gemma4RotaryEmbedding() + (rotary_emb_local): Gemma4RotaryEmbedding() + ) + (lm_head): Linear(in_features=2560, out_features=262208, bias=False) + ) +) +''' +``` + +上面打印了 `Gemma4Model` 的模型结构, 可以看到里面的 `self_attn` 和 `mlp` 是两个主要的模块, 因此可以考虑将这两个模块作为 **LoRA** 微调 的 `target_modules` , 包括 `q_proj`, `k_proj`, `v_proj`, `o_proj` 以及 `gate_proj`、`up_proj` 和 `down_proj` 。 + +通常我们只对 `self_attn` 模块中的 `q_proj`, `k_proj`, `v_proj`, `o_proj`进行微调, 本教程里我们也将对这四个模块进行微调演示, 感兴趣的同学可以自行尝试添加对 `mlp` 中的三个 `proj` 模块进行微调。 + +## 定义 LoraConfig + +`LoraConfig`类用于设置 LoRA 微调参数,虽然可以设置很多参数,但主要的参数没多少,简单讲一讲,感兴趣的同学可以直接看源码。 + +- `task_type`:模型类型 +- `target_modules`:需要训练的模型层的名字,主要就是 `attention`部分的层,不同的模型对应的层的名字不同,可以传入数组,也可以字符串,也可以正则表达式。 +- `r`:`LoRA`的秩,具体可以看 `LoRA`原理。 +- `lora_alpha`:`LoRA alaph` ,具体作用参见 `LoRA` 原理。 +- `lora_dropout`: `LoRA` 层的 `Dropout` 比例,用于防止过拟合,具体作用参见 `LoRA` 原理。 + +`LoRA`的缩放是啥嘞?当然不是 `r`(秩),这个缩放就是 `lora_alpha/r`, 在这个 `LoraConfig`中缩放就是 4 倍。 + +```python +from peft import LoraConfig, TaskType, get_peft_model + +config = LoraConfig( + task_type=TaskType.CAUSAL_LM, + target_modules=["q_proj", "k_proj","v_proj", "o_proj"], # 可以自行添加更多微调的target_modules + inference_mode=False, # 训练模式 + r=8, # LoRA 秩 + lora_alpha=32, # LoRA alaph,具体作用参见 LoRA 原理 + lora_dropout=0.1 # Dropout 比例 +) +``` + +## 自定义 TrainingArguments 参数 + +`TrainingArguments`类用于设置微调训练过程中的配置参数,这个类的源码也介绍了每个参数的具体作用,当然大家可以来自行探索,这里就简单说几个常用的。 + +- `output_dir`:模型的输出路径 +- `per_device_train_batch_size`:顾名思义 `batch_size`,批量大小 +- `gradient_accumulation_steps`: 梯度累加,如果你的显存比较小,那可以把 `batch_size` 设置小一点,梯度累加增大一些。 +- `logging_steps`:多少步,输出一次 `log` +- `num_train_epochs`:顾名思义 `epoch`,训练轮次 +- `gradient_checkpointing`:梯度检查,这个一旦开启,模型就必须执行 `model.enable_input_require_grads()`,这个原理大家可以自行探索,这里就不细说了。 + +```python +args = TrainingArguments( + output_dir="/root/autodl-tmp/gemma-4-E4B-it_lora_output", + per_device_train_batch_size=1, + gradient_accumulation_steps=4, + logging_steps=10, + num_train_epochs=3, + save_steps=100, + learning_rate=1e-4, + save_on_each_node=True, + gradient_checkpointing=True +) +``` + +## 使用 Trainer 训练 + +我们使用 `Trainer` 类来管理训练过程。`TrainingArguments` 用于设置训练参数,`Trainer` 则负责实际的训练逻辑。 + +```python +trainer = Trainer( + model=model, # 要训练的模型 + args=args, # 训练参数 + train_dataset=tokenized_id, # 训练数据集 + data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True), # 数据整理器 +) +trainer.train() # 开始训练 +``` +## 加载 LoRA 权重推理 + +训练好了之后可以使用如下方式加载 `LoRA`权重进行推理: + +```python +from transformers import AutoModelForCausalLM, AutoTokenizer +import torch +from peft import PeftModel + +model_path = '/root/autodl-tmp/google/gemma-4-E4B-it' +lora_path = '/root/autodl-tmp/google/gemma-4-E4B-it_lora_output/checkpoint-2790' # 这里改成 LoRA 输出对应 checkpoint 地址和最终的 epoch 数值 2796 + +# 加载tokenizer +tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) + +# 加载模型 +model = AutoModelForCausalLM.from_pretrained(model_path, + device_map="auto", + torch_dtype=torch.bfloat16, + trust_remote_code=True).eval() + +# 加载lora权重 +model = PeftModel.from_pretrained(model, model_id=lora_path) + +prompt = "你是谁?" +system_prompt = "现在你要扮演皇帝身边的女人--甄嬛" +print("prompt: ", prompt) +print("system_prompt: ", system_prompt) + +inputs = tokenizer.apply_chat_template([{"role": "system", "content": system_prompt}, + {"role": "user", "content": prompt}], + add_generation_prompt=True, + tokenize=True, + return_tensors="pt", + return_dict=True + ).to(model.device) # 将 inputs 移动到模型所在的设备,确保设备一致性 + + +gen_kwargs = {"max_length": 2500, "do_sample": True, "top_k": 1} +with torch.no_grad(): + outputs = model.generate(**inputs, **gen_kwargs) + outputs = outputs[:, inputs['input_ids'].shape[1]:] + print("output: ", tokenizer.decode(outputs[0], skip_special_tokens=True)) +``` + + +> 注意修改为自己的模型路径哦~‘ + +> 如果显示 `Some parameters are on the meta device because they were offloaded to the cpu.` 的报错,需要将实例关机,重启后单独运行本条代码。 diff --git a/models/Gemma4/6-gemma4-E4B-itGRPO微调及通过swanlab可视化.ipynb b/models/Gemma4/6-gemma4-E4B-itGRPO微调及通过swanlab可视化.ipynb new file mode 100644 index 0000000..61bcecd --- /dev/null +++ b/models/Gemma4/6-gemma4-E4B-itGRPO微调及通过swanlab可视化.ipynb @@ -0,0 +1,2894 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "## 步骤1: 安装必要的依赖包\n", + "\n", + "首先,我们需要安装Unsloth和vLLM。这些是进行高效模型微调所必需的工具:\n", + "\n", + "- **Unsloth**: 一个专门用于快速微调大语言模型的库,支持LoRA和QLoRA\n", + "- **vLLM**: 一个高性能的大语言模型推理引擎\n", + "\n", + "注意:`--no-deps`参数用于避免依赖冲突,确保安装指定版本的包。\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# pip install --no-deps unsloth vllm==0.8.5.post1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤2: 加载预训练模型和分词器\n", + "\n", + "在这一步中,我们将:\n", + "\n", + "1. **导入必要的库**: FastModel是Unsloth提供的快速模型加载接口\n", + "2. **设置参数**: 定义最大序列长度,这影响模型能处理的文本长度\n", + "3. **加载模型**: 从本地路径加载预训练的Gemma 4 E4B模型\n", + "4. **配置精度**: 使用16位精度进行训练,平衡效果和显存使用\n", + "\n", + "**重要参数说明**:\n", + "- `max_seq_length=1024`: 模型能处理的最大token数量\n", + "- `load_in_4bit=False, load_in_8bit=False`: 不使用量化加载,保持全精度\n", + "- `full_finetuning=False`: 使用LoRA而不是全参数微调" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==((====))== Unsloth 2025.6.11: Fast Gemma4 patching. Transformers: 4.52.4. vLLM: 0.8.5.post1.\n", + " \\\\ /| NVIDIA A800-SXM4-80GB. Num GPUs = 1. Max memory: 79.325 GB. Platform: Linux.\n", + "O^O/ \\_/ \\ Torch: 2.6.0+cu124. CUDA: 8.0. CUDA Toolkit: 12.4. Triton: 3.2.0\n", + "\\ / Bfloat16 = TRUE. FA [Xformers = None. FA2 = True]\n", + " \"-____-\" Free license: http://github.com/unslothai/unsloth\n", + "Unsloth: Fast downloading is enabled - ignore downloading bars which are red colored!\n", + "Unsloth: QLoRA and full finetuning all not selected. Switching to 16bit LoRA.\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "36ab76cf579349e6823ea88e6136316d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Loading checkpoint shards: 0%| | 0/2 [00:00>24 clips in May.\\nNatalia sold 48+24 = <<48+24=72>>72 clips altogether in April and May.\\n#### 72'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# 查看第一个样例的答案\n", + "# 注意答案格式:包含推理过程和最终答案(####后面是最终答案)\n", + "print(\"答案示例:\")\n", + "print(dataset[0][\"answer\"])\n", + "print(\"\\n可以看到:\")\n", + "print(\"1. 答案包含详细的推理步骤\")\n", + "print(\"2. 最终答案在####符号后面\")\n", + "print(\"3. 这种格式有助于模型学习推理过程\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'72'" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# 定义函数提取最终答案\n", + "# GSM8K数据集中,最终答案位于####符号之后\n", + "def extract_hash_answer(text):\n", + " \"\"\"\n", + " 从GSM8K答案中提取最终数值答案\n", + " \n", + " Args:\n", + " text (str): 包含推理过程和最终答案的完整文本\n", + " \n", + " Returns:\n", + " str or None: 提取的最终答案,如果没有####标记则返回None\n", + " \"\"\"\n", + " if \"####\" not in text: \n", + " return None\n", + " # 分割文本,取####后面的部分并去除空格\n", + " return text.split(\"####\")[1].strip()\n", + "\n", + "# 测试提取函数\n", + "final_answer = extract_hash_answer(dataset[0][\"answer\"])\n", + "print(f\"提取的最终答案: {final_answer}\")\n", + "\n", + "# 验证提取结果\n", + "final_answer" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "## 步骤5: 设计输出格式和系统提示词\n", + "\n", + "为了让模型学会按照特定格式输出答案,我们需要:\n", + "\n", + "1. **定义格式标记**: 设置开始和结束标记来界定不同部分\n", + "2. **创建系统提示词**: 指导模型如何结构化输出\n", + "3. **确保格式一致性**: 训练过程中检查格式合规性\n", + "\n", + "我们设计的格式包含两个部分:\n", + "- **推理过程**: 放在``和``之间\n", + "- **最终答案**: 放在``和``之间\n", + "\n", + "这种结构化输出有助于:\n", + "- 评估模型的推理质量\n", + "- 方便提取最终答案\n", + "- 提高输出的可读性\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'You are given a problem.\\nThink about the problem and provide your working out.\\nPlace it between and .\\nThen, provide your solution between '" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# 定义输出格式的标记符号\n", + "# 这些标记帮助我们识别和评估模型输出的不同部分\n", + "reasoning_start = \"\" # 推理过程开始标记\n", + "reasoning_end = \"\" # 推理过程结束标记\n", + "solution_start = \"\" # 最终答案开始标记\n", + "solution_end = \"\" # 最终答案结束标记\n", + "\n", + "# 创建系统提示词\n", + "# 这个提示词指导模型按照我们期望的格式输出答案\n", + "system_prompt = f\"\"\"You are given a problem.\n", + "Think about the problem and provide your working out.\n", + "Place it between {reasoning_start} and {reasoning_end}.\n", + "Then, provide your solution between {solution_start}{solution_end}\"\"\"\n", + "\n", + "print(\"系统提示词内容:\")\n", + "print(system_prompt)\n", + "print(\"\\n这个提示词告诉模型:\")\n", + "print(\"1. 需要思考问题\")\n", + "print(\"2. 将推理过程放在指定标记之间\")\n", + "print(\"3. 将最终答案放在SOLUTION标记之间\")\n", + "\n", + "system_prompt" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "## 步骤6: 转换数据集格式\n", + "\n", + "现在我们需要将原始的GSM8K数据转换为适合GRPO训练的格式:\n", + "\n", + "1. **创建对话格式**: 将每个问题转换为系统消息+用户消息的对话格式\n", + "2. **提取标准答案**: 使用之前定义的函数提取最终答案\n", + "3. **构建训练样本**: 每个样本包含提示(prompt)和标准答案(answer)\n", + "\n", + "转换后的格式:\n", + "- `prompt`: 包含系统消息和用户问题的对话列表\n", + "- `answer`: 提取的数值答案,用于评估模型输出的正确性\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "eec34d39908e4e48a57db4c1a1ada812", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map: 0%| | 0/7473 [00:00 and .\\nThen, provide your solution between ',\n", + " 'role': 'system'},\n", + " {'content': 'Natalia sold clips to 48 of her friends in April, and then she sold half as many clips in May. How many clips did Natalia sell altogether in April and May?',\n", + " 'role': 'user'}]}" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# 转换数据集格式\n", + "# 将原始数据转换为对话格式,便于模型训练\n", + "dataset = dataset.map(lambda x: {\n", + " # 构建对话prompt,包含系统提示和用户问题\n", + " \"prompt\" : [\n", + " {\"role\": \"system\", \"content\": system_prompt}, # 系统消息:指导输出格式\n", + " {\"role\": \"user\", \"content\": x[\"question\"]}, # 用户消息:具体的数学问题\n", + " ],\n", + " # 提取标准答案,用于后续的奖励计算\n", + " \"answer\": extract_hash_answer(x[\"answer\"]),\n", + "})\n", + "\n", + "print(\"转换后的数据格式示例:\")\n", + "print(\"1. prompt包含系统提示和用户问题\")\n", + "print(\"2. answer是提取的数值答案\")\n", + "print(f\"3. 数据集大小保持不变: {len(dataset)} 条\")\n", + "\n", + "# 查看转换后的第一个样本\n", + "dataset[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "## 步骤7: 设计奖励函数系统\n", + "\n", + "GRPO的核心是通过奖励函数来指导模型学习。我们将设计4个奖励函数来评估模型输出的不同方面:\n", + "\n", + "1. **格式完全匹配** (`match_format_exactly`): 检查输出是否严格遵循格式\n", + "2. **格式近似匹配** (`match_format_approximately`): 检查格式标记的出现情况\n", + "3. **答案正确性** (`check_answer`): 验证提取的答案是否正确\n", + "4. **数值提取** (`check_numbers`): 检查是否能正确提取数值\n", + "\n", + "### 7.1 首先定义正则表达式来匹配期望的格式\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "# 导入正则表达式库\n", + "import re\n", + "\n", + "# 定义正则表达式来匹配期望的输出格式\n", + "# 这个正则表达式确保模型输出包含所有必需的标记并按正确顺序排列\n", + "match_format = re.compile(\n", + " rf\"^[\\s]{{0,}}\" # 开头可以有任意数量的空白字符\n", + " rf\"{reasoning_start}.+?{reasoning_end}.*?\" # 推理过程部分(非贪婪匹配)\n", + " rf\"{solution_start}(.+?){solution_end}\" # 解决方案部分(捕获组获取答案)\n", + " rf\"[\\s]{{0,}}$\", # 结尾可以有任意数量的空白字符\n", + " flags = re.MULTILINE | re.DOTALL # 多行模式,.匹配换行符\n", + ")\n", + "\n", + "print(\"正则表达式说明:\")\n", + "print(\"1. 匹配从的推理过程\")\n", + "print(\"2. 匹配从的最终答案\")\n", + "print(\"3. 捕获SOLUTION标记内的内容作为答案\")\n", + "print(\"4. 允许前后有空白字符\")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + " 0.34 \"\n", + "extracted_numbers = match_numbers.findall(test_solution)\n", + "\n", + "print(f\"测试文本: {test_solution}\")\n", + "print(f\"提取的数字: {extracted_numbers}\")\n", + "print(\"✓ 数字提取正则表达式工作正常\" if extracted_numbers else \"✗ 数字提取失败\")\n", + "\n", + "extracted_numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "def check_numbers(prompts, completions, answer, **kwargs):\n", + " \"\"\"\n", + " 奖励函数4: 检查数值提取能力\n", + " \n", + " 这个函数专门检查模型是否能在SOLUTION标记内输出有效的数值,\n", + " 并与标准答案进行精确数值比较。\n", + " \n", + " Args:\n", + " prompts: 输入提示列表\n", + " completions: 模型生成的完成文本列表\n", + " answer: 标准答案列表\n", + " **kwargs: 其他参数\n", + " \n", + " Returns:\n", + " list: 每个完成文本的奖励分数列表\n", + " \"\"\"\n", + " question = prompts[0][-1][\"content\"]\n", + " responses = [completion[0][\"content\"] for completion in completions]\n", + "\n", + " # 使用数字提取正则表达式从响应中提取数值\n", + " extracted_responses = [\n", + " guess.group(1)\n", + " if (guess := match_numbers.search(r)) is not None else None \\\n", + " for r in responses\n", + " ]\n", + "\n", + " scores = []\n", + " \n", + " # 打印调试信息(训练时会显示)\n", + " print('*'*20, f\"Question:\\n{question}\", \n", + " f\"\\nAnswer:\\n{answer[0]}\", \n", + " f\"\\nResponse:\\n{responses[0]}\", \n", + " f\"\\nExtracted:\\n{extracted_responses[0]}\")\n", + " \n", + " for guess, true_answer in zip(extracted_responses, answer):\n", + " # 如果无法提取数字,得分为0\n", + " if guess is None:\n", + " scores.append(0)\n", + " continue\n", + " \n", + " # 尝试将提取的答案和标准答案转换为数值进行比较\n", + " try:\n", + " true_answer_num = float(true_answer.strip())\n", + " guess_num = float(guess.strip())\n", + " # 数值完全匹配时给予奖励,否则为0\n", + " scores.append(1.5 if guess_num == true_answer_num else 0.0)\n", + " except:\n", + " # 转换失败时得分为0\n", + " scores.append(0)\n", + " continue\n", + " \n", + " return scores\n", + "\n", + "print(\"奖励函数4说明:\")\n", + "print(\"- 专门检查SOLUTION标记内的数值提取\")\n", + "print(\"- 数值完全匹配: +1.5分\")\n", + "print(\"- 无法提取数值或不匹配: 0分\")\n", + "print(\"- 用于确保模型输出包含有效数字\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "## 步骤8: 配置GRPO训练参数\n", + "\n", + "现在我们配置GRPO训练的各种参数。这些参数控制训练过程的方方面面:\n", + "\n", + "### 关键参数说明:\n", + "\n", + "**学习率相关**:\n", + "- `learning_rate=5e-6`: 较小的学习率,确保稳定训练\n", + "- `warmup_ratio=0.1`: 学习率预热,前10%步骤逐渐增加学习率\n", + "\n", + "**批次和生成**:\n", + "- `per_device_train_batch_size=1`: 每个设备的批次大小\n", + "- `num_generations=4`: 每个提示生成4个候选答案进行比较\n", + "\n", + "**序列长度**:\n", + "- `max_prompt_length=256`: 输入提示的最大长度\n", + "- `max_completion_length`: 输出完成文本的最大长度\n", + "\n", + "**训练控制**:\n", + "- `max_steps=50`: 快速演示训练(实际训练建议更多步骤)\n", + "- `report_to=\"swanlab\"`: 使用SwanLab进行可视化监控\n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Unsloth: We now expect `per_device_train_batch_size` to be a multiple of `num_generations`.\n", + "We will change the batch size of 1 to the `num_generations` of 4\n" + ] + } + ], + "source": [ + "# 设置提示词的最大长度\n", + "max_prompt_length = 256\n", + "\n", + "# 导入GRPO相关的配置和训练器\n", + "from trl import GRPOConfig, GRPOTrainer\n", + "\n", + "# 创建GRPO训练配置\n", + "training_args = GRPOConfig(\n", + " # 优化器参数\n", + " learning_rate = 5e-6, # 学习率:GRPO通常使用较小的学习率\n", + " adam_beta1 = 0.9, # Adam优化器的beta1参数\n", + " adam_beta2 = 0.99, # Adam优化器的beta2参数\n", + " weight_decay = 0.1, # 权重衰减,防止过拟合\n", + " optim = \"adamw_torch_fused\", # 使用融合的AdamW优化器,更高效\n", + " \n", + " # 学习率调度\n", + " warmup_ratio = 0.1, # 学习率预热比例\n", + " lr_scheduler_type = \"cosine\", # 余弦学习率调度\n", + " \n", + " # 训练批次设置\n", + " per_device_train_batch_size = 1, # 每个设备的批次大小\n", + " gradient_accumulation_steps = 1, # 梯度累积步数(可以增加到4获得更平滑的训练)\n", + " num_generations = 4, # 每个提示生成的候选数量(显存不足时可减少)\n", + " \n", + " # 序列长度控制\n", + " max_prompt_length = max_prompt_length, # 提示的最大长度\n", + " max_completion_length = max_seq_length - max_prompt_length, # 完成文本的最大长度\n", + " \n", + " # 训练控制\n", + " max_steps = 50, # 最大训练步数(演示用,实际训练建议更多)\n", + " # num_train_epochs = 1, # 或者使用训练轮数而非步数\n", + " save_steps = 50, # 保存模型的步数间隔\n", + " max_grad_norm = 0.1, # 梯度裁剪阈值\n", + " \n", + " # 日志和监控\n", + " logging_steps = 1, # 日志记录间隔\n", + " report_to = \"swanlab\", # 实验跟踪工具(也可以使用\"wandb\")\n", + " output_dir = \"outputs\", # 输出目录\n", + ")\n", + "\n", + "print(\"GRPO训练配置已设置完成!\")\n", + "print(f\"- 最大训练步数: {training_args.max_steps}\")\n", + "print(f\"- 每步生成候选数: {training_args.num_generations}\")\n", + "print(f\"- 学习率: {training_args.learning_rate}\")\n", + "print(f\"- 使用SwanLab进行可视化监控\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "## 步骤9: 执行GRPO训练\n", + "\n", + "现在我们创建GRPO训练器并开始训练过程。训练器会:\n", + "\n", + "1. **初始化训练器**: 设置模型、奖励函数和训练参数\n", + "2. **开始训练**: 循环执行以下步骤:\n", + " - 生成多个候选答案\n", + " - 使用奖励函数评估每个候选\n", + " - 根据奖励信号更新模型参数\n", + "3. **监控训练**: 通过SwanLab实时查看训练进度\n", + "\n", + "### 训练过程中会看到:\n", + "- 训练进度条和损失值\n", + "- 每个奖励函数的得分统计\n", + "- SwanLab的可视化界面链接\n", + "- 样例问题和模型输出\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Detected kernel version 5.4.143, which is below the recommended minimum of 5.5.0; this can cause the process to hang. It is recommended to upgrade the kernel to the minimum version or higher.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "==((====))== Unsloth - 2x faster free finetuning | Num GPUs used = 1\n", + " \\\\ /| Num examples = 7,473 | Num Epochs = 1 | Total steps = 50\n", + "O^O/ \\_/ \\ Batch size per device = 4 | Gradient accumulation steps = 1\n", + "\\ / Data Parallel GPUs = 1 | Total batch size (4 x 1 x 1) = 4\n", + " \"-____-\" Trainable parameters = 14,901,248 of 4,314,980,720 (0.35% trained)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[34mswanlab\u001b[0m\u001b[0m: Tracking run with swanlab version 0.6.4 \n", + "\u001b[1m\u001b[34mswanlab\u001b[0m\u001b[0m: Run data will be saved locally in \u001b[35m\u001b[1m/opt/tiger/test0/swanlog/run-20250701_195941-0e8cd89d\u001b[0m\u001b[0m\n", + "\u001b[1m\u001b[34mswanlab\u001b[0m\u001b[0m: 👋 Hi \u001b[1m\u001b[39mtwosugar\u001b[0m\u001b[0m, welcome to swanlab!\n", + "\u001b[1m\u001b[34mswanlab\u001b[0m\u001b[0m: Syncing run \u001b[33moutputs\u001b[0m to the cloud\n", + "\u001b[1m\u001b[34mswanlab\u001b[0m\u001b[0m: 🏠 View project at \u001b[34m\u001b[4mhttps://swanlab.cn/@twosugar/test0\u001b[0m\u001b[0m\n", + "\u001b[1m\u001b[34mswanlab\u001b[0m\u001b[0m: 🚀 View run at \u001b[34m\u001b[4mhttps://swanlab.cn/@twosugar/test0/runs/cmax5v7at0zpzpqk94cbg\u001b[0m\u001b[0m\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " Show Iframe\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "******************** Question:\n", + "A concert ticket costs $40. Mr. Benson bought 12 tickets and received a 5% discount for every ticket bought that exceeds 10. How much did Mr. Benson pay in all? \n", + "Answer:\n", + "476 \n", + "Response:\n", + " \n", + "\n", + " \n", + "\n", + " \n", + "\n", + " **1.** Calculate the number of tickets bought that exceed 10. Mr. Benson bought 12 tickets, so 12 - 10 = 2 tickets exceed 10.\n", + "\n", + " **2.** Calculate the discount per ticket bought that exceeds 10. The discount is 5% of $40. 0.05 * $40 = $2. However, the discount is calculated *only* for those tickets exceeding 10. Therefore, the discount per ticket is $2.\n", + "\n", + " **3.** Calculate the total discount. Mr. Benson bought 2 tickets that exceed 10, so the total discount is 2 * $2 = $4.\n", + "\n", + " **4.** Calculate the total cost before discount. Mr. Benson bought 12 tickets at $40 each, so the total cost is 12 * $40 = $480.\n", + "\n", + " **5.** Calculate the total cost after discount. The total cost after discount is $480 - $4 = $476.\n", + "\n", + " **6.** Mr. Benson bought 12 tickets and received a 5% discount for every ticket bought that exceeds 10. The number of tickets bought that exceed 10 is 2. Therefore, the discount is 2 * $2 = $4. Mr. Benson paid in all $480 - $4 = $476.\n", + "\n", + " **7.** The problem states that Mr. Benson bought 12 tickets and received a 5% discount for every ticket bought that exceeds 10. The discount is given for every ticket bought that exceeds 10. So, the discount is given for 2 tickets (12-10 = 2). The discount is 2 * $2 = $4. Mr. Benson paid in all $480 - $4 = $476.\n", + "\n", + " **8.** The problem is to calculate how much Mr. Benson paid in all. Mr. Benson bought 12 tickets at $40 each, so the cost is $480. He received a 5% discount for every ticket bought that exceeds 10. There are 2 tickets that exceed 10 (12-10 = 2). The discount is 2 * 5% = 10%. The discount is $480 * 10% = $480 * 0.10 = $48. The total cost is $480 - $48 = $432.\n", + "\n", + " **9.** The problem states Mr. Benson bought 12 tickets and received a 5% discount for every ticket bought that exceeds 10. Tickets bought that exceed 10 is 2. 5% of the price is $40 is $2. The total discount is 2 * $2 = $4. The amount paid is $480 - $4 = $476.\n", + "\n", + " **10.** Re-reading the problem: Mr. Benson bought 12 tickets at $40 each. A 5% discount is given for every ticket bought that exceeds 10. This means for every ticket *beyond* 10, there is a 5% discount. So only 2 tickets are eligible for the discount. The discount is 2 * 5% = 10%. The total discount is $480 * 0.10 = $48. Therefore the final \n", + "Extracted:\n", + "1.\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + "
\n", + " \n", + " \n", + " [16/50 20:14 < 49:10, 0.01 it/s, Epoch 0.00/1]\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
StepTraining Lossrewardreward_stdcompletions / mean_lengthcompletions / min_lengthcompletions / max_lengthcompletions / clipped_ratiocompletions / mean_terminated_lengthcompletions / min_terminated_lengthcompletions / max_terminated_lengthklrewards / match_format_exactly / meanrewards / match_format_exactly / stdrewards / match_format_approximately / meanrewards / match_format_approximately / stdrewards / check_answer / meanrewards / check_answer / stdrewards / check_numbers / meanrewards / check_numbers / std
1-0.0000000.5000001.000000768.000000768.000000768.0000001.0000000.0000000.0000000.0000000.0000000.0000000.0000000.5000001.0000000.0000000.0000000.0000000.000000
2-0.000000-0.6250001.108678768.000000768.000000768.0000001.0000000.0000000.0000000.0000000.0000000.7500001.500000-1.2500000.957427-0.1250000.2500000.0000000.000000
30.0000000.6250002.625992768.000000768.000000768.0000001.0000000.0000000.0000000.0000000.0001770.7500001.5000000.0000001.414214-0.1250000.2500000.0000000.000000
40.0000003.6250003.944933768.000000768.000000768.0000001.0000000.0000000.0000000.0000000.0006200.7500001.5000001.7500000.5000000.7500001.5000000.3750000.750000
50.0000003.7500002.020726768.000000768.000000768.0000001.0000000.0000000.0000000.0000000.0014471.5000001.7320512.0000000.0000000.2500000.2886750.0000000.000000
60.0000005.0000001.683251768.000000768.000000768.0000001.0000000.0000000.0000000.0000000.0006812.2500001.5000001.7500000.500000-0.1250000.4787141.1250000.750000
70.0000000.0000001.154701768.000000768.000000768.0000001.0000000.0000000.0000000.0000000.0004220.0000000.0000000.0000001.1547010.0000000.0000000.0000000.000000
80.0000000.2500001.500000768.000000768.000000768.0000001.0000000.0000000.0000000.0000000.0005130.0000000.0000000.2500001.5000000.0000000.0000000.0000000.000000
90.0000001.3750002.688711768.000000768.000000768.0000001.0000000.0000000.0000000.0000000.0006460.7500001.5000000.7500001.892969-0.1250000.2500000.0000000.000000
100.0000001.7500002.020726768.000000768.000000768.0000001.0000000.0000000.0000000.0000000.0007392.2500001.500000-0.5000001.914854-0.3750000.2500000.3750000.750000
110.0000004.1875001.179248768.000000768.000000768.0000001.0000000.0000000.0000000.0000000.0010382.2500001.5000001.7500000.500000-0.1875000.3750000.3750000.750000
120.0000000.8750002.719528768.000000768.000000768.0000001.0000000.0000000.0000000.0000000.0002310.7500001.5000000.2500001.707825-0.1250000.2500000.0000000.000000
130.0000002.1875003.091487768.000000768.000000768.0000001.0000000.0000000.0000000.0000000.0012410.7500001.5000001.0000002.0000000.0625000.1250000.3750000.750000
140.0000001.2500002.598076768.000000768.000000768.0000001.0000000.0000000.0000000.0000000.0003280.7500001.5000000.2500001.500000-0.1250000.2500000.3750000.750000

" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "******************** Question:\n", + "Jane is trying to decide whether to buy a house or a trailer. A house costs $480,000 and a trailer costs $120,000. Each loan will be paid in monthly installments over 20 years. How much more is the monthly payment on the house compared to the trailer? \n", + "Answer:\n", + "1500 \n", + "Response:\n", + "Here's the working out, placed between and .\n", + "\n", + "\n", + "\n", + "Jane is trying to decide whether to buy a house or a trailer. A house costs $480,000 and a trailer costs $120,000. Each loan will be paid in monthly installments over 20 years. How much more is the monthly payment on the house compared to the trailer?\n", + "\n", + "1. Calculate the monthly payment for the house.\n", + " * Loan amount: $480,000\n", + " * Number of months: 20 years * 12 months/year = 240 months\n", + " * Monthly payment: $480,000 / 240 months = $2,000/month\n", + "\n", + "2. Calculate the monthly payment for the trailer.\n", + " * Loan amount: $120,000\n", + " * Number of months: 20 years * 12 months/year = 240 months\n", + " * Monthly payment: $120,000 / 240 months = $500/month\n", + "\n", + "3. Find the difference between the house and trailer payment.\n", + " * Difference: $2,000 - $500 = $1,500\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "$1,500\n", + "\n", + "model \n", + "Extracted:\n", + "1\n", + "Unsloth: Will smartly offload gradients to save VRAM!\n", + "******************** Question:\n", + "Janet pays $40/hour for 3 hours per week of clarinet lessons and $28/hour for 5 hours a week of piano lessons. How much more does she spend on piano lessons than clarinet lessons in a year? \n", + "Answer:\n", + "1040 \n", + "Response:\n", + "Here's the breakdown of the problem and solution:\n", + "\n", + "\n", + "\n", + "**1. Clarinet Lesson Cost:**\n", + "\n", + "* **Annual Hours:** 3 hours/week * 52 weeks/year = 156 hours/year\n", + "* **Annual Cost:** 156 hours/year * $28/hour = $4008\n", + "\n", + "**2. Piano Lesson Cost:**\n", + "\n", + "* **Annual Hours:** 5 hours/week * 52 weeks/year = 260 hours/year\n", + "* **Annual Cost:** 260 hours/year * $40/hour = $10400\n", + "\n", + "**3. Cost Difference:**\n", + "\n", + "* **Difference:** $10400 - $4008 = $6392\n", + "\n", + "**4. Solution**\n", + "\n", + "\n", + "\n", + "\n", + "$6392\n", + " \n", + "Extracted:\n", + "6392\n", + "******************** Question:\n", + "Sabrina is collecting herbs to make a poultice for her grandmother. She needs twice as many basil leaves as sage leaves and 5 fewer sage leaves than verbena leaves. If she needs 12 basil leaves, how many leaves total does she need? \n", + "Answer:\n", + "29 \n", + "Response:\n", + "Here's the working out:\n", + "\n", + "\n", + "\n", + "Sabrina needs 12 basil leaves. \n", + "The number of basil leaves is twice the number of sage leaves, so 12 = 2 * sage leaves leaves. \n", + "Therefore, sage leaves need = 12 / 2 = 6 leaves.\n", + "Sage leaves need 5 fewer leaves than verbena leaves, so verbena leaves need = 6 + 5 = 11 leaves.\n", + "Verbena leaves need 5 fewer leaves than verbena leaves, so she needs = 11 + 5 = 16 leaves.\n", + "Total leaves = basil leaves + sage leaves + verbena leaves + sage leaves= 12+6+11+5=32 leaves.\n", + " Total leaves = 12 + 6 + 11 + 5 = 32 leaves.\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "32\n", + "Categorization: 32.\n", + "\n", + " \n", + "Extracted:\n", + "32\n", + "******************** Question:\n", + "Over the past five years, on July 4th, the high temperature for Washington, DC has been: 90 degrees in 2020, 90 degrees in 2019, 90 degrees in 2018, 79 degrees in 2017 and 71 degrees in 2016. What is the average temperature for July 4th in Washington, DC over the past 5 years? \n", + "Answer:\n", + "84 \n", + "Response:\n", + "\n", + "\n", + "Okay, let's work this out. We need to calculate the average temperature for July 4th in Washington, DC, over the past 5 years. \n", + "\n", + "Here's the breakdown of the temperatures:\n", + "\n", + "* 2020: 90 degrees\n", + "* 2019: 90 degrees\n", + "* 2018: 90 degrees\n", + "* 2017: 79 degrees\n", + "* 2016: 71 degrees\n", + "\n", + "Now let's calculate the average:\n", + "\n", + "1. Add all the temperatures: 90 + 90 + 90 + 79 + 71 = 399\n", + "2. Divide the sum by the number of temperatures (5): 399 / 5 = 79.8\n", + "\n", + "Therefore, the average temperature for July 4th in Washington, DC over the past 5 years is 79.8 degrees.\n", + "\n", + "\n", + "\n", + "79.8 \n", + "Extracted:\n", + "79.8\n", + "******************** Question:\n", + "Rene can finish reading 30 pages in 60 minutes. Lulu can read 27 pages in 60 minutes and Cherry can read 25 pages in 60 minutes. If they have been reading for 240 minutes now, how many pages have they finished reading in total? \n", + "Answer:\n", + "328 \n", + "Response:\n", + "Here's the breakdown of the problem, the solution and the final answer.\n", + "\n", + "\n", + "\n", + "**Problem Breakdown:**\n", + "\n", + "1. **Individual Rates:** We are given the individual pages read rates per 60 minutes interval.\n", + "\n", + "2. **Combined Rates (Intervals):** The example gives how many pages are read in 60 minutes. Since we are provided the time interval, we can calculate the combined read rate.\n", + "\n", + "3. **Time Interval**: We are provided with the time interval of 240 minutes. We have to convert this to hours and minutes. Also the given time interval that is thet 240 minutes is not provided and has to be calculated for.\n", + "\n", + "4. **Calculate Total Pages Read:** Combine the values to calculate the total pages read for 240 minutes.\n", + "\n", + "**Solution:**\n", + "\n", + "1. **Calculate 240 minutes interval** :\n", + " 240 minutes = 4 hours.\n", + "\n", + "2. **Calculate the total pages read:**\n", + " * **Rene**: Reads 30 pages in 60 minutes. Therefore reads 30 pages / 60 min * 240 min = 120 pages.\n", + " * **Lulu**: Reads 27 pages in 60 minutes. Therefore reads 27 pages / 60 min * 240 min = 108 pages.\n", + " * **Cherry**: Reads 25 pages in 60 minutes. Therefore reads 25 pages / 60 min * 240 min = 120 pages.\n", + "\n", + " **Total Pages Read:** 120 + 108 + 120 = 348 pages.\n", + "\n", + "3. **Answer**\n", + " * **Total Pages Read** = 348 pages.\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "348\n", + "\n", + "\n", + "\n", + " \n", + "Extracted:\n", + "348\n", + "******************** Question:\n", + "Martin rings the small bell 4 times more than 1/3 as often as the big bell. If he rings both of them a combined total of 52 times, how many times does he ring the big bell? \n", + "Answer:\n", + "36 \n", + "Response:\n", + " \n", + "\n", + "Martin rings the small bell 4 times more than 1/3 as often as the big bell. If he rings both of them a combined total of 52 times, how many times does he ring the big bell?\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "< \n", + "Extracted:\n", + "None\n", + "******************** Question:\n", + "Bert fills out the daily crossword puzzle in the newspaper every day. He uses up a pencil to fill out the puzzles every two weeks. On average, it takes him 1050 words to use up a pencil. How many words are in each crossword puzzle on average? \n", + "Answer:\n", + "75 \n", + "Response:\n", + "Here's the solution to the crossword puzzle problem:\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + " \n", + "Extracted:\n", + "None\n", + "******************** Question:\n", + "Matt can make a batch of a dozen cookies using 2 pounds of flour. He uses 4 bags of flour each weighing 5 pounds. If Jim eats 15 cookies how many cookies are left? \n", + "Answer:\n", + "105 \n", + "Response:\n", + "Okay, let's tackle this problem step-by-step.\n", + "\n", + "\n", + "\n", + "**1. Understand the problem:****\n", + "* **Matt:** Uses 2 pounds of flour for 1 dozen cookies (12).\n", + "* **Jim:** Eats 15 cookies.\n", + "* **We need to know:** How many cookies are left?\n", + "\n", + "**2. Calculate the amount of flour used by Jim:**\n", + "* **1 dozen cookies:** = 12 cookies\n", + "* **15 cookies:** = 15 cookies/ 12 cookies/ 1 dozen = 1.25 dozens\n", + "* **Flour required:** 1.25 * 2 = 2.5 pounds\n", + "\n", + "**3. Find the number of cookies left**\n", + "* **Number of cookies left:** =12 -15 =-3.\n", + "* **Since number can't be negative** We assume 0 cookies.\n", + "\n", + "**4. Find the number of cookies left.**\n", + "* **Since number can't be negative** We assume 0 cookies.\n", + "\n", + "**5. Find the number of cookies left.**\n", + "* **Since number can't be negative** We assume 0 cookies.\n", + "\n", + "**6. Find the number of cookies left.**\n", + "* **Since number can't be negative** We assume 0 cookies.\n", + "\n", + "**7. Find the number of cookies left.**\n", + "* **Since number can't be negative** We assume 0 cookies.\n", + "\n", + "**8. Find the number of cookies left.**\n", + "* **Since number can't be negative** We assume 0 cookies.\n", + "\n", + "**9. Find the number of cookies left.**\n", + "* **Since number can't be negative** We assume 0 cookies.\n", + "\n", + "**10. Find the number of cookies left.**\n", + "* **Since number can't be negative** We assume 0 cookies.\n", + "\n", + "
\n", + "\n", + "\n", + "\n", + "There are 0 cookies left.\n", + "\n", + " \n", + "Extracted:\n", + "0\n", + "******************** Question:\n", + "James decides to build a tin house by collecting 500 tins in a week. On the first day, he collects 50 tins. On the second day, he manages to collect 3 times that number. On the third day, he collects 50 tins fewer than the number he collected on the second day. If he collects an equal number of tins on the remaining days of the week, what's the number of tins he collected each day for the rest of the week? \n", + "Answer:\n", + "50 \n", + "Response:\n", + "Here's the solution to the James tin house problem, including the working out and the solution between the and tags.\n", + "\n", + "\n", + "\n", + "First, let's calculate the number of tins James collected on each day:\n", + "\n", + "* **Day 1:** 50 tins\n", + "* **Day 2:** 3 * 50 = 150 tins\n", + "* **Day 3:** 150 - 50 = 100 tins\n", + "* **Days 4-7:** Let 'x' be the number of tins collected each of these days.\n", + "\n", + "Total number of days in a week is 7.\n", + "James collected 50 tins on day 1.\n", + "James collected 50 tins on day 3.\n", + "So, James collected a total of 50+50=100 tins on days 1 and 3.\n", + "So, James collected 500-100=400 tins on days 2,4,5,6,7.\n", + "Since James collected ‘x’ number of tins on each of days 2,4,5,6,7.\n", + "So, 5x=400.\n", + "Therefore, x=80.\n", + "So, James collected 80 tins each on day 2,4,5,6,7.\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "**Solution:**\n", + "\n", + "James collected 80 tins each on days 2, 4, 5, 6, and 7.\n", + "\n", + "\n", + "\n", + " \n", + "Extracted:\n", + "None\n", + "******************** Question:\n", + "A jar of jellybeans has 14 blue jellybeans, 26 purple jellybeans and 40 orange jellybeans. If there are 200 jellybeans in the jar, how many are there of the red color? \n", + "Answer:\n", + "120 \n", + "Response:\n", + "Here's the breakdown of the problem and solution:\n", + "\n", + "\n", + "\n", + "The problem states that there is 14 blue jellybeans, 26 purple jellybeans and 40 orange jellybeans. Also, there are 200 jellybeans in the jar total and we need to find out how many are red. We know that they are for a total of 200 jellybeans and already know how many blue, purple and orange there are. So the equation will be:\n", + "\n", + "14 (blue) + 26 (purple) + 40 (orange) + x (red) = 200 (total)\n", + "\n", + "Where x is the red jellybeans. Let’s solve for x:\n", + "\n", + "14 + 26 + 40 + x = 200\n", + "\n", + "100 + x = 200\n", + "\n", + "x = 200 - 100\n", + "\n", + "x = 100\n", + "\n", + "So, there are 100 red jellybeans.\n", + "\n", + "\n", + "\n", + "100 \n", + "Extracted:\n", + "100\n", + "******************** Question:\n", + "Five adults and two children go to see a movie and buy $12 worth of concessions. The total cost of their trip is $76. If each child's ticket is $7, how much, in dollars, are the adult tickets? \n", + "Answer:\n", + "10 \n", + "Response:\n", + "Here's the working out:\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Here’s the solution:\n", + "\n", + "\n", + "\n", + "\n", + " \n", + "Extracted:\n", + "None\n", + "******************** Question:\n", + "Janelle had 26 green marbles. Then she bought 6 bags of blue marbles. There were 10 marbles in each bag. She created a gift of 6 green marbles and 8 blue marbles and gave it to a friend. How many marbles does Janelle have now? \n", + "Answer:\n", + "72 \n", + "Response:\n", + "Janelle started with 26 green marbles. She also bought 6 bags of blue marbles, and in each bag there were 10 marbles. The total number of blue marbles is 6 * 10 = 60. \n", + "\n", + "The total number of marbles Janelle has is 26 + 60 = 86. \n", + "\n", + "Janelle created a gift of 6 green marbles and 8 blue marbles.\n", + "\n", + "The number of green marbles left is 26 - 6 = 20.\n", + "The number of blue marbles left is 60 - 8 = 52.\n", + "\n", + "Total number of marbles Janelle has is 20 + 52 = 72. \n", + "\n", + "The solution is between \n", + "\n", + "Janelle started with 26 green marbles. She also bought 6 bags of blue marbles, and in each bag there were 10 marbles. The total number of blue marbles is 6 * 10 = 60. \n", + "\n", + "The total number of marbles Janelle has is 26 + 60 = 86. \n", + "\n", + "Janelle created a gift of 6 green marbles and 8 blue marbles.\n", + "\n", + "The number of green marbles left is 26 - 6 = 20.\n", + "The number of blue marbles left is 60 - 8 = 52.\n", + "\n", + "Total number of marbles Janelle has is 20 + 52 = 72. \n", + "\n", + "The answer is 72. \n", + "Extracted:\n", + "26\n", + "******************** Question:\n", + "Every day in a week, Siena bookmarks 30 website pages from the research she does on her browser. If Siena has 400 bookmarked pages on her bookmarks library now, how many pages will she have in her bookmarks library at the end of March? \n", + "Answer:\n", + "1330 \n", + "Response:\n", + "Here's a breakdown of the solution, as requested:\n", + "\n", + "\n", + "\n", + "**1. Determine the number of days from the beginning of March to the end of March:** March has 31 days.\n", + "\n", + "**2. Determine the number of days from the beginning of March to the end of March** which means the # of days from the beginning of March to the end of March is 31 days (since March has 31 days).\n", + "\n", + "**3. Calculate the total number of booksmarked pages that will be on her bookmarks library at the end of March:** \n", + "* She will have on her bookmark library. 400 bookmarked pages.\n", + "* On a given day. Sien will have 30 new bookmarked pages.\n", + "* During the month of March. Sien will have 30 new bookmarked pages.\n", + "* So at the end of March. Sien will have 400 + 30 = 430 bookmarked pages.\n", + "* She will have on her bookmark library. 400 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark library. 30 pages.\n", + "* She will have on her bookmark \n", + "Extracted:\n", + "None\n", + "******************** Question:\n", + "An old pirate wants to leave his treasure on an island. He has 3500 gold coins. He wants to spread this equally across 5 chests. Additionally, he will put a total of 500 silver coins and twice as many bronze coins as silver, all distributed equally across the chests. How many coins in total will be in each chest? \n", + "Answer:\n", + "1000 \n", + "Response:\n", + "Here's the breakdown of the problem and the solution:\n", + "\n", + "\n", + "\n", + "**1. Calculate the Gold Coins per Chest:**\n", + "\n", + "* Gold Coins: 3500\n", + "* Number of Chests: 5\n", + "* Gold Coins per Chest: 3500 / 5 = 700\n", + "\n", + "**2. Calculate the Silver Coins per Chest:**\n", + "\n", + "* Silver Coins: 500\n", + "* Number of Chests: 5\n", + "* Silver Coins per Chest: 500 / 5 = 100\n", + "\n", + "**3. Calculate the Bronze Coins per Chest:**\n", + "\n", + "* Bronze Coins: 500 * 2 = 1000\n", + "* Number of Chests: 5\n", + "* Bronze Coins per Chest: 1000 / 5 = 200\n", + "\n", + "**4. Calculate the Total Coins per Chest:**\n", + "\n", + "* Gold Coins per Chest: 700\n", + "* Silver Coins per Chest: 100\n", + "* Bronze Coins per Chest: 200\n", + "* Total Coins per Chest: 700 + 100 + 200 = 1000\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Each chest will contain 1000 coins in total.\n", + " \n", + "Extracted:\n", + "1000\n" + ] + } + ], + "source": [ + "# 创建GRPO训练器\n", + "# 训练器整合了模型、奖励函数、训练参数和数据集\n", + "trainer = GRPOTrainer(\n", + " model = model, # 要训练的模型\n", + " processing_class = tokenizer, # 分词器(用于文本处理)\n", + " \n", + " # 奖励函数列表:这些函数将评估模型输出质量\n", + " reward_funcs = [\n", + " match_format_exactly, # 奖励函数1:严格格式匹配\n", + " match_format_approximately, # 奖励函数2:近似格式匹配\n", + " check_answer, # 奖励函数3:答案正确性\n", + " check_numbers, # 奖励函数4:数值提取\n", + " ],\n", + " \n", + " args = training_args, # 训练配置参数\n", + " train_dataset = dataset, # 训练数据集\n", + ")\n", + "\n", + "print(\"GRPO训练器创建完成!\")\n", + "print(\"包含的奖励函数:\")\n", + "print(\"1. match_format_exactly - 检查完整格式\")\n", + "print(\"2. match_format_approximately - 检查标记使用\")\n", + "print(\"3. check_answer - 检查答案正确性\")\n", + "print(\"4. check_numbers - 检查数值提取\")\n", + "print(\"\\n开始训练...\")\n", + "\n", + "# 开始GRPO训练\n", + "# 注意:训练过程中会显示大量调试信息,包括问题、答案和模型输出\n", + "trainer.train()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "## 步骤10: 测试训练后的模型\n", + "\n", + "训练完成后,让我们测试一下模型是否学会了按照我们期望的格式回答问题。我们将:\n", + "\n", + "1. **构建测试消息**: 使用系统提示词和一个新的数学问题\n", + "2. **生成回答**: 让微调后的模型回答问题\n", + "3. **观察输出**: 检查模型是否遵循了我们定义的格式\n", + "\n", + "这个测试将帮助我们验证GRPO训练的效果。\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 构建测试消息\n", + "# 使用训练时相同的系统提示词,但提出一个新问题\n", + "messages = [\n", + " {\"role\": \"system\", \"content\": system_prompt}, # 使用相同的格式指导\n", + " {\"role\": \"user\", \"content\": \"What is the sqrt of 101?\"}, # 新的数学问题\n", + "]\n", + "\n", + "# 将消息转换为模型输入格式\n", + "text = tokenizer.apply_chat_template(\n", + " messages,\n", + " add_generation_prompt = True, # 添加生成提示,告诉模型开始回答\n", + " tokenize = False, # 先不分词,保持文本格式\n", + ")\n", + "\n", + "print(\"测试问题: What is the sqrt of 101?\")\n", + "print(\"期望输出格式:\")\n", + "print(\"- 包含 ... 的推理过程\")\n", + "print(\"- 包含 ... 的最终答案\")\n", + "print(\"\\n模型输出:\")\n", + "\n", + "# 导入文本流输出器,用于实时显示生成过程\n", + "from transformers import TextStreamer\n", + "\n", + "# 生成回答\n", + "_ = model.generate(\n", + " **tokenizer(text, return_tensors = \"pt\").to(\"cuda\"), # 将输入转换为张量并移到GPU\n", + " max_new_tokens = 64, # 限制输出长度(可以根据需要增加)\n", + " \n", + " # Gemma-4推荐的生成参数\n", + " temperature = 1.0, # 控制输出的随机性\n", + " top_p = 0.95, # 核采样参数\n", + " top_k = 64, # top-k采样参数\n", + " \n", + " # 实时输出流\n", + " streamer = TextStreamer(tokenizer, skip_prompt = True), # 跳过输入提示,只显示生成内容\n", + ")\n", + "\n", + "# \n", + "# We want to find the square root of 101, which is written as √101.\n", + "\n", + "# Since 101 is a prime number, its only factors are 1 and 101. Therefore, its square root is not an integer." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "## 步骤11: 保存训练后的模型\n", + "\n", + "训练完成并验证效果后,我们需要保存模型以便后续使用。有几种保存方式:\n", + "\n", + "1. **LoRA适配器保存**: 只保存训练的LoRA权重(文件小,推荐)\n", + "2. **完整模型保存**: 将LoRA权重合并到原模型中保存\n", + "3. **GGUF格式保存**: 保存为量化的GGUF格式,便于部署\n", + "\n", + "### LoRA适配器保存(推荐)\n", + "\n", + "这种方式只保存训练过程中新增的LoRA权重,文件很小(通常几十MB),使用时需要配合原始模型一起加载。\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 保存LoRA适配器(推荐方式)\n", + "# 这种方式只保存训练过程中新增的LoRA权重,文件很小\n", + "print(\"正在保存LoRA适配器...\")\n", + "\n", + "model.save_pretrained(\"gemma-4\") # 保存模型(包含LoRA权重)\n", + "tokenizer.save_pretrained(\"gemma-4\") # 保存分词器\n", + "\n", + "print(\"✓ LoRA适配器和分词器已保存到 'gemma-4' 目录\")\n", + "print(\"保存内容:\")\n", + "print(\"- adapter_config.json: LoRA配置文件\")\n", + "print(\"- adapter_model.safetensors: LoRA权重文件\")\n", + "print(\"- tokenizer相关文件\")\n", + "print(\"\\n使用方法:\")\n", + "print(\"1. 先加载原始Gemma 4 E4B模型\")\n", + "print(\"2. 再加载这个LoRA适配器\")\n", + "print(\"3. 即可获得微调后的模型\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "### 可选:保存完整模型\n", + "\n", + "如果你希望将LoRA权重合并到原模型中并保存完整的模型文件,可以使用以下代码:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 可选:保存完整的微调模型\n", + "# 将LoRA权重合并到原模型中,生成一个完整的模型文件\n", + "if False: # 设置为True以执行保存\n", + " print(\"正在保存完整的微调模型...\")\n", + " model.save_pretrained_merged(\"gemma-4-finetune\", tokenizer)\n", + " print(\"✓ 完整模型已保存到 'gemma-4-finetune' 目录\")\n", + " print(\"注意:完整模型文件很大(几GB),但使用时不需要原始模型\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "### 可选:保存GGUF格式\n", + "\n", + "GGUF是一种优化的模型格式,支持量化压缩,适合部署到资源受限的环境:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 可选:保存为GGUF格式\n", + "# GGUF格式支持量化,文件更小,推理速度更快\n", + "if False: # 设置为True以执行保存\n", + " print(\"正在保存GGUF格式模型...\")\n", + " model.save_pretrained_gguf(\n", + " \"gemma-4-finetune\",\n", + " quantization_type = \"Q8_0\", # 量化类型:目前支持Q8_0, BF16, F16\n", + " )\n", + " print(\"✓ GGUF格式模型已保存\")\n", + " print(\"特点:\")\n", + " print(\"- 文件更小(通过量化压缩)\")\n", + " print(\"- 推理速度更快\")\n", + " print(\"- 适合部署到边缘设备\")\n", + " print(\"- 可以用llama.cpp等工具加载\")" + ] + } + ], + "metadata": { + "fileId": "280337d0-7469-42d6-a465-e0acc4f9fe07", + "filePath": "/opt/tiger/test0/gemma.ipynb", + "kernelspec": { + "display_name": "uni", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/models/Gemma4/6-gemma4-E4B-itGRPO微调及通过swanlab可视化.md b/models/Gemma4/6-gemma4-E4B-itGRPO微调及通过swanlab可视化.md new file mode 100644 index 0000000..cbe098b --- /dev/null +++ b/models/Gemma4/6-gemma4-E4B-itGRPO微调及通过swanlab可视化.md @@ -0,0 +1,726 @@ +# Gemma 4 E4B GRPO微调教程 + +> 基座模型:[google/gemma-4-E4B-it](https://huggingface.co/google/gemma-4-E4B-it);Gemma 4 系列说明见 [Hugging Face 博文](https://huggingface.co/blog/gemma4)。Unsloth 与训练脚本请以当前库文档为准。 + +> 话不多说,直接开始! + +本文使用的测试环境为单张 A100,显存 80GB,可根据需求切换不同参数量的模型;Gemma 4 **E4B-it** 在 24GB 级显存上可按量化与序列长度实测调整。 +使用的框架为 Unsloth +![06-01](./images/06-01.png) +Unsloth 是一个极其强调资源节省的框架,把所有的资源节省做到了极致,具体来讲Unsloth能够将 Llama-3、Mistral、Phi-4 和 Gemma 等大型语言模型的微调速度提升 2 倍,内存占用减少 70%,并且准确率没有任何下降! +官方文档非常全面,详细指导了如何训练自己的定制模型。其中涵盖了安装和更新 Unsloth、创建数据集、运行和部署模型等基本要素。 Unsloth 让大家在本地或在 Google Colab 和 Kaggle 等平台上训练像 Llama 3 这样的模型变得极其简单。Unsloth简化了整个训练工作流程,包括模型加载、量化、训练、评估、运行、保存、导出,以及与 Ollama、llama.cpp 和 vLLM 等推理引擎的集成。 +Unsloth定期与 Hugging Face、Google 和 Meta 的团队合作,以修复 LLM 训练和模型中的错误。因此,当使用 Unsloth 进行训练或使用模型时,可以期待获得最准确的结果。 Unsloth 具有高度可定制性,允许更改聊天模板或数据集格式等内容。Unsloth还为视觉、文本转语音 (TTS)、BERT、强化学习 (RL) 等提供了预构建的脚本!此外,Unsloth支持所有训练方法和所有基于 Transformer 的模型。 + +Unsloth 可显著加快 Gemma 系列微调并降低显存占用;使用 Unsloth 时,Gemma 4 **E4B-it** 可在约 24GB VRAM 环境下结合量化与配置进行尝试(请以 Unsloth 与 transformers 实际支持为准)。 +unsloth为Gemma4提供了Dynamic 2.0量化方法,在5-shot MMLU和KL散度基准测试中提供最佳性能。这意味着可以运行和微调量化后的Gemma4 LLM,同时保持最小的精度损失。unsloth还上传了支持原生长上下文的Gemma4版本。 + +## 教程概览 + +本教程将指导您完成 **Gemma 4 E4B 模型的 GRPO(Group Relative Policy Optimization)微调**,这是一种先进的强化学习技术,专门用于提升大语言模型在特定任务上的表现。 + +### 什么是GRPO? + +GRPO(Group Relative Policy Optimization)是一种强化学习优化技术,通过设计多个奖励函数来评估模型输出的不同方面,从而指导模型学习期望的行为模式。在数学推理任务中,GRPO可以帮助模型: + +- 学会按照特定格式输出答案 +- 提高推理过程的逻辑性 +- 增强答案的准确性 +- 改善输出的结构化程度 + +### 本教程的学习内容 + +1. **环境设置**: 安装Unsloth和相关依赖 +2. **模型加载**: 加载Gemma 4 E4B预训练模型 +3. **LoRA配置**: 设置高效的参数微调 +4. **数据处理**: 处理GSM8K数学推理数据集 +5. **格式设计**: 定义结构化的输出格式 +6. **奖励函数**: 设计多维度评估体系 +7. **GRPO训练**: 执行强化学习微调 +8. **效果验证**: 测试微调后的模型 +9. **模型保存**: 保存训练结果 +10. **可视化监控**: 使用SwanLab跟踪训练过程 + + +## 步骤1: 安装必要的依赖包 + +**安装软件包** + +```Python +# pip install --no-deps unsloth vllm==0.8.5.post1 +``` + +## 步骤2: 加载预训练模型和分词器 + +**准备模型,设置参数** + +```Python +# 导入必要的库 +from unsloth import FastModel # Unsloth的快速模型加载接口 +import torch # PyTorch深度学习框架 + +# 设置最大序列长度 +# 这个参数决定了模型能处理的最大文本长度(以token为单位) +max_seq_length = 1024 + +# 加载预训练模型和分词器 +# 这里我们加载Gemma 4 E4B的指令微调版本 +model, tokenizer = FastModel.from_pretrained( + model_name = "google/gemma-4-E4B-it", # 模型路径 + max_seq_length = max_seq_length, # 最大序列长度 + load_in_4bit = False, # 不使用4位量化,保持精度 + load_in_8bit = False, # 不使用8位量化,保持精度 + full_finetuning = False, # 使用LoRA微调,不进行全参数微调 +) +``` + +## 步骤3: 配置LoRA(Low-Rank Adaptation) + +```Python +# 配置LoRA(Low-Rank Adaptation)参数 +# 将基础模型转换为PEFT(Parameter Efficient Fine-Tuning)模型 +model = FastModel.get_peft_model( + model, + # 层级配置:决定哪些层参与微调 + finetune_vision_layers = False, # 关闭视觉层微调(仅文本任务) + finetune_language_layers = True, # 开启语言层微调(必须) + finetune_attention_modules = True, # 开启注意力模块微调(对GRPO很重要) + finetune_mlp_modules = True, # 开启MLP模块微调(建议保持开启) + + # LoRA核心参数 + r = 8, # LoRA的秩:控制适应层大小,值越大精度越高但可能过拟合 + lora_alpha = 8, # LoRA的缩放因子:建议设置为r的值或略大 + lora_dropout = 0, # LoRA的dropout率:防止过拟合,这里设为0 + bias = "none", # 偏置项设置:不训练偏置项 + random_state = 3407, # 随机种子:确保结果可复现 +) +``` + +## 步骤4: 加载和探索GSM8K数据集 + +**设置CoT思考模版【让模型具备思考能力的必经之路】** + +```Python +# 加载GSM8K数据集 +from datasets import load_dataset + +# 从本地路径加载GSM8K数据集的训练集 +# GSM8K是一个包含小学数学推理问题的数据集 +dataset = load_dataset("openai/gsm8k", "main", split = "train") + +# 查看数据集基本信息 +print(f"数据集大小: {len(dataset)} 条记录") +print(f"数据集特征: {dataset.features}") +dataset +``` + +```Python +# 定义函数提取最终答案 +# GSM8K数据集中,最终答案位于####符号之后 +def extract_hash_answer(text): + """ + 从GSM8K答案中提取最终数值答案 + + Args: + text (str): 包含推理过程和最终答案的完整文本 + + Returns: + str or None: 提取的最终答案,如果没有####标记则返回None + """ + if "####" not in text: + return None + # 分割文本,取####后面的部分并去除空格 + return text.split("####")[1].strip() + +# 测试提取函数 +final_answer = extract_hash_answer(dataset[0]["answer"]) +print(f"提取的最终答案: {final_answer}") + +# 验证提取结果 +final_answer +``` + +## 步骤5: 设计输出格式和系统提示词 + +```Python +# 定义输出格式的标记符号 +# 这些标记帮助我们识别和评估模型输出的不同部分 +reasoning_start = "" # 推理过程开始标记 +reasoning_end = "" # 推理过程结束标记 +solution_start = "" # 最终答案开始标记 +solution_end = "" # 最终答案结束标记 + +# 创建系统提示词 +# 这个提示词指导模型按照我们期望的格式输出答案 +system_prompt = f"""You are given a problem. +Think about the problem and provide your working out. +Place it between {reasoning_start} and {reasoning_end}. +Then, provide your solution between {solution_start}{solution_end}""" + +print("系统提示词内容:") +print(system_prompt) +print("\n这个提示词告诉模型:") +print("1. 需要思考问题") +print("2. 将推理过程放在指定标记之间") +print("3. 将最终答案放在SOLUTION标记之间") + +system_prompt +``` + +**加载数据集【这里使用一个数学推理的数据集】** + +## 步骤6: 转换数据集格式 + +```Python +# 转换数据集格式 +# 将原始数据转换为对话格式,便于模型训练 +dataset = dataset.map(lambda x: { + # 构建对话prompt,包含系统提示和用户问题 + "prompt" : [ + {"role": "system", "content": system_prompt}, # 系统消息:指导输出格式 + {"role": "user", "content": x["question"]}, # 用户消息:具体的数学问题 + ], + # 提取标准答案,用于后续的奖励计算 + "answer": extract_hash_answer(x["answer"]), +}) + +print("转换后的数据格式示例:") +print("1. prompt包含系统提示和用户问题") +print("2. answer是提取的数值答案") +print(f"3. 数据集大小保持不变: {len(dataset)} 条") + +# 查看转换后的第一个样本 +dataset[0] +``` + +## 步骤7: 设计奖励函数系统 + +**奖励函数部分** + +GRPO的核心是通过奖励函数来指导模型学习。我们将设计4个奖励函数来评估模型输出的不同方面: + +### 7.1 首先定义正则表达式来匹配期望的格式 + +```Python +# 导入正则表达式库 +import re + +# 定义正则表达式来匹配期望的输出格式 +# 这个正则表达式确保模型输出包含所有必需的标记并按正确顺序排列 +match_format = re.compile( + rf"^[\s]{{0,}}" # 开头可以有任意数量的空白字符 + rf"{reasoning_start}.+?{reasoning_end}.*?" # 推理过程部分(非贪婪匹配) + rf"{solution_start}(.+?){solution_end}" # 解决方案部分(捕获组获取答案) + rf"[\s]{{0,}}$", # 结尾可以有任意数量的空白字符 + flags = re.MULTILINE | re.DOTALL # 多行模式,.匹配换行符 +) + +print("正则表达式说明:") +print("1. 匹配从的推理过程") +print("2. 匹配从的最终答案") +print("3. 捕获SOLUTION标记内的内容作为答案") +print("4. 允许前后有空白字符") +``` + +### 7.2 奖励函数1: 精确格式匹配 + +```Python +def match_format_exactly(completions, **kwargs): + """ + 奖励函数1: 检查输出是否严格遵循指定格式 + + Args: + completions: 模型生成的完成文本列表 + **kwargs: 其他参数(未使用) + + Returns: + list: 每个完成文本的奖励分数列表 + """ + scores = [] + for completion in completions: + score = 0 + response = completion[0]["content"] + + # 如果输出完全匹配期望格式,给予高分奖励 + if match_format.search(response) is not None: + score += 3.0 + + scores.append(score) + + return scores + +print("奖励函数1说明:") +print("- 检查输出是否包含完整的推理过程和解决方案格式") +print("- 格式正确: +3.0分") +print("- 格式不正确: 0分") +``` + +### 7.3 奖励函数2: 近似格式匹配 + +```Python +def match_format_approximately(completions, **kwargs): + """ + 奖励函数2: 检查格式标记的出现次数 + + 这个函数更宽松,检查各个格式标记是否恰好出现1次。 + 如果某个标记出现1次,获得奖励;如果出现0次或多次,会被惩罚。 + + Args: + completions: 模型生成的完成文本列表 + **kwargs: 其他参数(未使用) + + Returns: + list: 每个完成文本的奖励分数列表 + """ + scores = [] + for completion in completions: + score = 0 + response = completion[0]["content"] + + # 检查每个标记的出现次数,理想情况下每个标记应该恰好出现1次 + score += 0.5 if response.count(reasoning_start) == 1 else -0.5 + score += 0.5 if response.count(reasoning_end) == 1 else -0.5 + score += 0.5 if response.count(solution_start) == 1 else -0.5 + score += 0.5 if response.count(solution_end) == 1 else -0.5 + + scores.append(score) + return scores + +print("奖励函数2说明:") +print("- 检查每个格式标记的出现次数") +print("- 每个标记出现1次: +0.5分") +print("- 每个标记出现0次或多次: -0.5分") +print("- 总分范围: -2.0 到 +2.0") +``` + +### 7.4 奖励函数3: 答案正确性检查 + +```Python +def check_answer(prompts, completions, answer, **kwargs): + """ + 奖励函数3: 检查答案的正确性 + + 这个函数实现了多层次的答案评估机制,从严格匹配到近似匹配。 + + Args: + prompts: 输入提示列表 + completions: 模型生成的完成文本列表 + answer: 标准答案列表 + **kwargs: 其他参数 + + Returns: + list: 每个完成文本的奖励分数列表 + """ + question = prompts[0][-1]["content"] + responses = [completion[0]["content"] for completion in completions] + + # 从模型输出中提取答案 + extracted_responses = [ + guess.group(1) + if (guess := match_format.search(r)) is not None else None \ + for r in responses + ] + + scores = [] + for guess, true_answer in zip(extracted_responses, answer): + score = 0 + + # 如果无法提取答案,得分为0 + if guess is None: + scores.append(0) + continue + + # 完全匹配:最高奖励 + if guess == true_answer: + score += 3.0 + # 去除空格后匹配:高奖励 + elif guess.strip() == true_answer.strip(): + score += 1.5 + else: + # 数值接近性检查:对于数值答案,允许一定误差 + try: + ratio = float(guess) / float(true_answer) + if ratio >= 0.9 and ratio <= 1.1: # 10%误差内 + score += 0.5 + elif ratio >= 0.8 and ratio <= 1.2: # 20%误差内 + score += 0.25 + else: + score -= 1.0 # 错误答案惩罚 + except: + score -= 0.5 # 无法转换为数值的惩罚 + + scores.append(score) + return scores + +print("奖励函数3说明:") +print("- 完全匹配: +3.0分") +print("- 去空格匹配: +1.5分") +print("- 10%误差内: +0.5分") +print("- 20%误差内: +0.25分") +print("- 错误答案: -1.0分") +print("- 无法解析: -0.5分") +``` + +### 7.5 奖励函数4: 数值提取检查 + +```Python +# 定义用于提取数字的正则表达式 +# 这个正则表达式专门用于从SOLUTION标记中提取数值 +match_numbers = re.compile( + rf"{solution_start}.*?([\d\.]{{1,}})", # 匹配SOLUTION标记内的数字(包括小数) + flags = re.MULTILINE | re.DOTALL # 多行模式 +) + +# 测试数字提取功能 +test_solution = " 0.34 " +extracted_numbers = match_numbers.findall(test_solution) + +print(f"测试文本: {test_solution}") +print(f"提取的数字: {extracted_numbers}") +print("✓ 数字提取正则表达式工作正常" if extracted_numbers else "✗ 数字提取失败") + +extracted_numbers +``` + +```Python +def check_numbers(prompts, completions, answer, **kwargs): + """ + 奖励函数4: 检查数值提取能力 + + 这个函数专门检查模型是否能在SOLUTION标记内输出有效的数值, + 并与标准答案进行精确数值比较。 + + Args: + prompts: 输入提示列表 + completions: 模型生成的完成文本列表 + answer: 标准答案列表 + **kwargs: 其他参数 + + Returns: + list: 每个完成文本的奖励分数列表 + """ + question = prompts[0][-1]["content"] + responses = [completion[0]["content"] for completion in completions] + + # 使用数字提取正则表达式从响应中提取数值 + extracted_responses = [ + guess.group(1) + if (guess := match_numbers.search(r)) is not None else None \ + for r in responses + ] + + scores = [] + + # 打印调试信息(训练时会显示) + print('*'*20, f"Question:\n{question}", + f"\nAnswer:\n{answer[0]}", + f"\nResponse:\n{responses[0]}", + f"\nExtracted:\n{extracted_responses[0]}") + + for guess, true_answer in zip(extracted_responses, answer): + # 如果无法提取数字,得分为0 + if guess is None: + scores.append(0) + continue + + # 尝试将提取的答案和标准答案转换为数值进行比较 + try: + true_answer_num = float(true_answer.strip()) + guess_num = float(guess.strip()) + # 数值完全匹配时给予奖励,否则为0 + scores.append(1.5 if guess_num == true_answer_num else 0.0) + except: + # 转换失败时得分为0 + scores.append(0) + continue + + return scores + +print("奖励函数4说明:") +print("- 专门检查SOLUTION标记内的数值提取") +print("- 数值完全匹配: +1.5分") +print("- 无法提取数值或不匹配: 0分") +print("- 用于确保模型输出包含有效数字") +``` + +## 步骤8: 配置GRPO训练参数 + +**GRPO部分** + +```Python +# 设置提示词的最大长度 +max_prompt_length = 256 + +# 导入GRPO相关的配置和训练器 +from trl import GRPOConfig, GRPOTrainer + +# 创建GRPO训练配置 +training_args = GRPOConfig( + # 优化器参数 + learning_rate = 5e-6, # 学习率:GRPO通常使用较小的学习率 + adam_beta1 = 0.9, # Adam优化器的beta1参数 + adam_beta2 = 0.99, # Adam优化器的beta2参数 + weight_decay = 0.1, # 权重衰减,防止过拟合 + optim = "adamw_torch_fused", # 使用融合的AdamW优化器,更高效 + + # 学习率调度 + warmup_ratio = 0.1, # 学习率预热比例 + lr_scheduler_type = "cosine", # 余弦学习率调度 + + # 训练批次设置 + per_device_train_batch_size = 1, # 每个设备的批次大小 + gradient_accumulation_steps = 1, # 梯度累积步数(可以增加到4获得更平滑的训练) + num_generations = 4, # 每个提示生成的候选数量(显存不足时可减少) + + # 序列长度控制 + max_prompt_length = max_prompt_length, # 提示的最大长度 + max_completion_length = max_seq_length - max_prompt_length, # 完成文本的最大长度 + + # 训练控制 + max_steps = 50, # 最大训练步数(演示用,实际训练建议更多) + save_steps = 50, # 保存模型的步数间隔 + max_grad_norm = 0.1, # 梯度裁剪阈值 + + # 日志和监控 + logging_steps = 1, # 日志记录间隔 + report_to = "swanlab", # 这里改成swanlab + output_dir = "outputs", # 输出目录 +) + +print("GRPO训练配置已设置完成!") +print(f"- 最大训练步数: {training_args.max_steps}") +print(f"- 每步生成候选数: {training_args.num_generations}") +print(f"- 学习率: {training_args.learning_rate}") +print(f"- 使用SwanLab进行可视化监控") +``` + +## 步骤9: 执行GRPO训练 + +```Python +# 创建GRPO训练器 +# 训练器整合了模型、奖励函数、训练参数和数据集 +trainer = GRPOTrainer( + model = model, # 要训练的模型 + processing_class = tokenizer, # 分词器(用于文本处理) + + # 奖励函数列表:这些函数将评估模型输出质量 + reward_funcs = [ + match_format_exactly, # 奖励函数1:严格格式匹配 + match_format_approximately, # 奖励函数2:近似格式匹配 + check_answer, # 奖励函数3:答案正确性 + check_numbers, # 奖励函数4:数值提取 + ], + + args = training_args, # 训练配置参数 + train_dataset = dataset, # 训练数据集 +) + +print("GRPO训练器创建完成!") +print("包含的奖励函数:") +print("1. match_format_exactly - 检查完整格式") +print("2. match_format_approximately - 检查标记使用") +print("3. check_answer - 检查答案正确性") +print("4. check_numbers - 检查数值提取") +print("\n开始训练...") + +# 开始GRPO训练 +# 注意:训练过程中会显示大量调试信息,包括问题、答案和模型输出 +trainer.train() +``` + +## 步骤10: 测试训练后的模型 + +**训练完毕后调用模型** + +```Python +# 构建测试消息 +# 使用训练时相同的系统提示词,但提出一个新问题 +messages = [ + {"role": "system", "content": system_prompt}, # 使用相同的格式指导 + {"role": "user", "content": "What is the sqrt of 101?"}, # 新的数学问题 +] + +# 将消息转换为模型输入格式 +text = tokenizer.apply_chat_template( + messages, + add_generation_prompt = True, # 添加生成提示,告诉模型开始回答 + tokenize = False, # 先不分词,保持文本格式 +) + +print("测试问题: What is the sqrt of 101?") +print("期望输出格式:") +print("- 包含 ... 的推理过程") +print("- 包含 ... 的最终答案") +print("\n模型输出:") + +# 导入文本流输出器,用于实时显示生成过程 +from transformers import TextStreamer + +# 生成回答 +_ = model.generate( + **tokenizer(text, return_tensors = "pt").to("cuda"), # 将输入转换为张量并移到GPU + max_new_tokens = 64, # 限制输出长度(可以根据需要增加) + + # Gemma-4推荐的生成参数 + temperature = 1.0, # 控制输出的随机性 + top_p = 0.95, # 核采样参数 + top_k = 64, # top-k采样参数 + + # 实时输出流 + streamer = TextStreamer(tokenizer, skip_prompt = True), # 跳过输入提示,只显示生成内容 +) +``` + +## 步骤11: 保存训练后的模型 + +**保存模型** + +```Python +# 保存LoRA适配器(推荐方式) +# 这种方式只保存训练过程中新增的LoRA权重,文件很小 +print("正在保存LoRA适配器...") + +model.save_pretrained("gemma-4") # 保存模型(包含LoRA权重) +tokenizer.save_pretrained("gemma-4") # 保存分词器 + +print("✓ LoRA适配器和分词器已保存到 'gemma-4' 目录") +print("保存内容:") +print("- adapter_config.json: LoRA配置文件") +print("- adapter_model.safetensors: LoRA权重文件") +print("- tokenizer相关文件") +print("\n使用方法:") +print("1. 先加载原始Gemma 4 E4B模型") +print("2. 再加载这个LoRA适配器") +print("3. 即可获得微调后的模型") +``` + +```Python +# 可选:保存完整的微调模型 +# 将LoRA权重合并到原模型中,生成一个完整的模型文件 +if False: # 设置为True以执行保存 + print("正在保存完整的微调模型...") + model.save_pretrained_merged("gemma-4-finetune", tokenizer) + print("✓ 完整模型已保存到 'gemma-4-finetune' 目录") + print("注意:完整模型文件很大(几GB),但使用时不需要原始模型") +``` + +```Python +# 可选:保存为GGUF格式 +# GGUF格式支持量化,文件更小,推理速度更快 +if False: # 设置为True以执行保存 + print("正在保存GGUF格式模型...") + model.save_pretrained_gguf( + "gemma-4-finetune", + quantization_type = "Q8_0", # 量化类型:目前支持Q8_0, BF16, F16 + ) + print("✓ GGUF格式模型已保存") + print("特点:") + print("- 文件更小(通过量化压缩)") + print("- 推理速度更快") + print("- 适合部署到边缘设备") + print("- 可以用llama.cpp等工具加载") +``` + +## Swanlab + +![06-02](./images/06-02.png) + +> ++[SwanLab](https://github.com/swanhubx/swanlab)++ 是一个开源的模型训练记录工具,面向 AI 研究者,提供了训练可视化、自动日志记录、超参数记录、实验对比、多人协同等功能。在 `SwanLab` 上,研究者能基于直观的可视化图表发现训练问题,对比多个实验找到研究灵感,并通过在线链接的分享与基于组织的多人协同训练,打破团队沟通的壁垒。 + +### 为什么要记录训练? + +相较于软件开发,模型训练更像一个实验科学。一个品质优秀的模型背后,往往是成千上万次实验。研究者需要不断尝试、记录、对比,积累经验,才能找到最佳的模型结构、超参数与数据配比。在这之中,如何高效进行记录与对比,对于研究效率的提升至关重要。 + +### 在哪里用? + +建议先在 ++[SwanLab 官网](https://swanlab.cn/)++ 注册账号,然后在GRPO训练初始化阶段选择 + +```Python +from trl import GRPOConfig, GRPOTrainer +training_args = GRPOConfig( + # 优化器参数 + learning_rate = 5e-6, # 学习率:GRPO通常使用较小的学习率 + adam_beta1 = 0.9, # Adam优化器的beta1参数 + adam_beta2 = 0.99, # Adam优化器的beta2参数 + weight_decay = 0.1, # 权重衰减,防止过拟合 + optim = "adamw_torch_fused", # 使用融合的AdamW优化器,更高效 + + # 学习率调度 + warmup_ratio = 0.1, # 学习率预热比例 + lr_scheduler_type = "cosine", # 余弦学习率调度 + + # 训练批次设置 + per_device_train_batch_size = 1, # 每个设备的批次大小 + gradient_accumulation_steps = 1, # 梯度累积步数(可以增加到4获得更平滑的训练) + num_generations = 4, # 每个提示生成的候选数量(显存不足时可减少) + + # 序列长度控制 + max_prompt_length = max_prompt_length, # 提示的最大长度 + max_completion_length = max_seq_length - max_prompt_length, # 完成文本的最大长度 + + # 训练控制 + max_steps = 50, # 最大训练步数(演示用,实际训练建议更多) + save_steps = 50, # 保存模型的步数间隔 + max_grad_norm = 0.1, # 梯度裁剪阈值 + + # 日志和监控 + logging_steps = 1, # 日志记录间隔 + report_to = "swanlab", # 这里改成swanlab + output_dir = "outputs", # 输出目录 +) +``` + +### 本试验的试验记录 + +#### GRPO阶段 + +![06-03](./images/06-03.png) +400个step之后loss会有明显变化 + +## 教程总结 + +🎉 恭喜!你已经成功完成了Gemma 4 E4B的GRPO微调教程。 + +### 本教程涵盖的核心概念: + +1. **GRPO微调**: 使用奖励函数指导模型学习特定输出格式 +2. **LoRA技术**: 高效的参数微调方法,节省显存和时间 +3. **奖励函数设计**: 多层次评估体系,从格式到内容的全面评价 +4. **结构化输出**: 训练模型按照特定格式输出推理过程和答案 +5. **SwanLab监控**: 实时跟踪训练进度和指标变化 + +### 学到的技能: + +- ✅ 设置GRPO训练环境 +- ✅ 设计多维度奖励函数 +- ✅ 配置LoRA参数进行高效微调 +- ✅ 处理数学推理数据集 +- ✅ 监控和分析训练过程 +- ✅ 保存和部署微调模型 + +### 进一步探索: + +1. **调整奖励函数**: 设计更复杂的评估机制 +2. **扩展数据集**: 使用更大或不同类型的数据集 +3. **优化参数**: 尝试不同的LoRA配置和训练参数 +4. **模型评估**: 在测试集上系统评估模型性能 +5. **应用部署**: 将模型集成到实际应用中 + +### 注意事项: + +- 本教程使用了较少的训练步数作为演示,实际应用中建议使用更多步数 +- 可以根据显存情况调整批次大小和生成数量 +- SwanLab提供了丰富的可视化功能,建议深入探索 + +感谢你的学习!如果有任何问题,欢迎查看SwanLab的实验记录或重新运行代码。 + +# 总结 + +Congratulations!看到了这,你已经初步实现了一个简单的RL实战,掌握了使用 Unsloth 对 Gemma4 这类大模型进行 GRPO 微调的具体操作步骤,更能体会到 Unsloth 在大幅提升训练速度、显著降低显存占用方面的强大优势,从而使在有限资源下进行复杂强化学习实验成为可能!如果支持我们的工作希望得到你的star!!这是我们持续更新的最大动力!!! + +# 相关链接 + +- 完整可运行的代码:[GitHub](https://github.com/datawhalechina/self-llm/blob/master/models/Gemma4/6-gemma4-E4B-itGRPO微调及通过swanlab可视化.ipynb) +- 综述:https://arxiv.org/abs/2001.06921 +- deepseek-r1:https://arxiv.org/abs/2501.12948 +- 数学原理:https://blog.csdn.net/weixin\_38991876/article/details/146474767 +- Unsloth:https://docs.unsloth.ai/ diff --git a/models/Gemma4/7-gemma4-E4B-it AMD环境准备.md b/models/Gemma4/7-gemma4-E4B-it AMD环境准备.md new file mode 100644 index 0000000..a7c9a97 --- /dev/null +++ b/models/Gemma4/7-gemma4-E4B-it AMD环境准备.md @@ -0,0 +1,113 @@ +# gemma-4-E4B-it AMD环境准备 + +> 与 **AMD Ryzen AI / Lemonade** 搭配部署时,多模态权重说明见 [google/gemma-4-E4B-it](https://huggingface.co/google/gemma-4-E4B-it);GGUF 见 [ggml-org/gemma-4-E4B-it-GGUF](https://huggingface.co/ggml-org/gemma-4-E4B-it-GGUF)。概述:[Gemma 4 博文](https://huggingface.co/blog/gemma4)。 + +## 环境准备 + +本文基础环境如下: + +``` +---------------- +Windows11 +CPU AI 395 +内存 128G +---------------- +``` + +> 非常感谢 AMD University Program 对本开源项目的大力支持,本项目的环境都在此主机下完成 + +## 芯片介绍 + +AMD Strix Halo处理器可以说是一款划时代的产品,尤其是旗舰型号锐龙 AI Max+ 395,拥有史上最强集显,可以轻松媲美桌面级RTX 4060独立显卡。全新"Zen5"架构 CPU、RDNA3.5 架构 GPU、XDNA2架构 NPU,其中NPU AI引擎拥有高达50 TOPS的算力。锐龙 AI Max系列可以搭配最多128GB LPDDR5X-8000统一内存,带宽高达256GB/s,分配最多96GB作为专属显存,从而能在本地直接运行例如GPT-OSS-120B这种千亿参数的完整大模型,尤其是对于MoE专家模型可谓得天独厚。 + +## NPU 安装与配置 + +### 先决条件 + +Ryzen AI 软件支持带有神经网络处理单元(NPU)的 AMD 处理器。请参考发布说明以获取完整的支持配置列表。 + +在安装 Ryzen AI 软件之前,系统必须安装以下依赖项: + +| 依赖项 | 版本要求 | +|--------|---------| +| Windows 11 | build >= 22621.3527 | +| Visual Studio | 2022 | +| cmake | version >= 3.26 | +| Python 发行版(推荐 Miniforge) | 最新版本 | + +⚠ **重要提示**: + +* Visual Studio 2022 Community:确保安装了"使用 C++ 的桌面开发"工作负载 +* Miniforge:确保在系统 PATH 环境变量中设置以下路径之一: + - `path\to\miniforge3\condabin` + - `path\to\miniforge3\Scripts\` + - `path\to\miniforge3\` + + (系统 PATH 变量应在"环境变量"窗口的"系统变量"部分设置),安装程序请都是用管理员权限打开使用!!!! + +### 安装 NPU 驱动程序 + +1. **下载 NPU 驱动程序** + - 下载并安装 NPU 驱动程序版本:32.0.203.280 或更新版本 + - 下载链接: + * [NPU Driver (Version 32.0.203.280)](https://ryzenai.docs.amd.com/en/latest/inst.html#install-npu-drivers) + * [NPU Driver (Version 32.0.203.304)](https://ryzenai.docs.amd.com/en/latest/inst.html#install-npu-drivers) + +2. **安装步骤** + - 解压下载的 ZIP 文件 + - 以管理员模式打开终端 + - 执行 `.\npu_sw_installer.exe` 文件 + +3. **验证安装** + - 打开任务管理器 -> 性能 -> NPU0 + - 确保 NPU MCDM 驱动程序已正确安装: + * 版本:32.0.203.280,日期:5/16/2025 + * 或版本:32.0.203.304,日期:10/07/2025 + +### 安装 Ryzen AI 软件 + +1. **下载安装程序** + - 下载 Ryzen AI 软件安装程序:`ryzenai-lt-1.6.1.exe` + +2. **运行安装向导** + - 启动 EXE 安装程序并按照安装向导的说明操作: + * 接受许可协议条款 + * 提供 Ryzen AI 安装的目标文件夹(默认:`C:\Program Files\RyzenAI\1.6.1`) + * 指定 conda 环境的名称(默认:`ryzen-ai-1.6.1`) + +3. **完成安装** + - Ryzen AI 软件包现在已安装在安装程序创建的 conda 环境中 + +> **注意**:NuGet 包可在 [ryzen-ai-1.6.1-nuget.zip](https://ryzenai.docs.amd.com/en/latest/inst.html#install-npu-drivers) 下载 + +### 测试安装 + +Ryzen AI 软件安装文件夹包含用于验证软件是否正确安装的测试。此安装测试位于 `quicktest` 子文件夹中。 + +1. **打开 Conda 命令提示符** + - 在 Windows 开始菜单中搜索"Miniforge Prompt" + +2. **激活 Conda 环境** + ```bash + conda activate + ``` + 其中 `` 是安装程序创建的 conda 环境名称(默认为 `ryzen-ai-1.6.1`) + +3. **运行测试** + ```bash + cd %RYZEN_AI_INSTALLATION_PATH%/quicktest + python quicktest.py + ``` + +4. **验证结果** + - `quicktest.py` 脚本会设置环境并运行一个简单的 CNN 模型 + - 成功运行时,您将看到类似以下的输出,这表明模型正在 NPU 上运行,并且 Ryzen AI 软件的安装成功: + ``` + [Vitis AI EP] No. of Operators : NPU 398 VITIS_EP_CPU 2 + [Vitis AI EP] No. of Subgraphs : NPU 1 Actually running on NPU 1 + Test Passed + ``` + +> **注意**:Ryzen AI 软件安装文件夹的完整路径存储在 `RYZEN_AI_INSTALLATION_PATH` 环境变量中。 + +--- diff --git a/models/Gemma4/8-gemma4-E4B-it 模型服务部署.md b/models/Gemma4/8-gemma4-E4B-it 模型服务部署.md new file mode 100644 index 0000000..d810e9a --- /dev/null +++ b/models/Gemma4/8-gemma4-E4B-it 模型服务部署.md @@ -0,0 +1,171 @@ +# 8-gemma4-E4B-it 模型服务部署 + +> **GGUF 仓库**(llama.cpp / Lemonade 等):[ggml-org/gemma-4-E4B-it-GGUF](https://huggingface.co/ggml-org/gemma-4-E4B-it-GGUF)。简介见 [Gemma 4 博文](https://huggingface.co/blog/gemma4)。 + +## 基础环境准备 + +本文基础环境如下: + +``` +---------------- +Windows11 +CPU AI 395 +内存 128G +---------------- +``` + +> 请确定AMD芯片的版本,目前支持AI 395和 AI 370 + +下载 lemonade-server 进行安装 +![](./images/11-01.png) +> NPU 需要配备 AMD Ryzen AI 300 系列的 Windows 11 电脑及驱动安装。请先下载并安装 NPU 驱动程序,再继续操作,请参考 7-gemma4-E4B-it AMD环境准备.md + + +首先 `pip` 换源加速下载并安装依赖包 + +```shell +# 升级pip +python -m pip install --upgrade pip +# 更换 pypi 源加速库的安装 +pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple + +pip install -U huggingface_hub +pip install lemonade-sdk[dev] +``` + + +## 模型下载 + +在 Windows Powershell 下输入以下配置镜像站 + > $env:HF_ENDPOINT = "https://hf-mirror.com" + +使用 `huggingface_hub` 中的 `snapshot_download` 函数下载模型,第一个参数为模型名称,参数 `cache_dir` 为模型的下载路径。 + +新建 `model_download.py` 文件并在其中输入以下内容,粘贴代码后请及时保存文件,如下图所示。并运行 `python model_download.py ggml-org/gemma-4-E4B-it-GGUF "C:\Users\aup\.cache\huggingface\hub\gemma-4-E4B-it-GGUF"` 执行下载。 + +```python +#!/usr/bin/env python +""" +使用 Python API 下载 Hugging Face 模型 +""" +import os +from huggingface_hub import snapshot_download + +def download_model(repo_id: str, local_dir: str = None, resume_download: bool = True): + """ + 下载 Hugging Face 模型 + + Args: + repo_id: 模型仓库ID,例如 "ggml-org/gemma-4-E4B-it-GGUF" + local_dir: 本地保存目录,如果为None则使用默认缓存目录 + resume_download: 是否支持断点续传 + """ + print(f"开始下载模型: {repo_id}") + print(f"保存位置: {local_dir if local_dir else '默认缓存目录'}") + + try: + # 如果指定了本地目录,使用它;否则使用默认缓存 + if local_dir: + cache_dir = os.path.dirname(local_dir) if os.path.dirname(local_dir) else None + local_dir_use = local_dir + else: + cache_dir = None + local_dir_use = None + + # 下载模型 + downloaded_path = snapshot_download( + repo_id=repo_id, + local_dir=local_dir_use, + cache_dir=cache_dir, + resume_download=resume_download, + local_files_only=False + ) + + print(f"\n✓ 模型下载完成!") + print(f"保存路径: {downloaded_path}") + return downloaded_path + + except Exception as e: + print(f"\n✗ 下载失败: {str(e)}") + raise + +if __name__ == "__main__": + import sys + + # 默认下载的模型 + repo_id = "ggml-org/gemma-4-E4B-it-GGUF" + + # 如果提供了命令行参数,使用它作为模型ID + if len(sys.argv) > 1: + repo_id = sys.argv[1] + + # 可选:指定本地保存目录 + local_dir = None + if len(sys.argv) > 2: + local_dir = sys.argv[2] + + download_model(repo_id, local_dir) + + +``` +## 支持的硬件加速器 +![](./images/11-05.png) + +## 支持的推理引擎 +![](./images/11-06.png) + +## 启动服务 +> 在菜单栏里输入 Lemonade Server 启动,点击菜单里中对应的图标 + +![](./images/11-02.png) + +然后选中对应的模型即可启动,可以对外输出兼容OpenAI的服务接口 + + +![](./images/11-03.png) + +## 服务测试 + +新建 `test.py` 文件并在其中输入以下内容,粘贴代码后请及时保存文件。以下代码有很详细的注释,大家如有不理解的地方,欢迎提出 issue 。 +```python +# Client library provided by OpenAI to automate request +# and response processing with the server +from openai import OpenAI + +# The base_url points to an LLM server, which can either be +# local (localhost address) or cloud-based (web address) +base_url = f"http://localhost:8000/api/v1" + +# The `client` instance here provides APIs to request +# LLM invocations from the server +client = OpenAI( + base_url=base_url, + api_key="lemonade", # required, but unused in Lemonade +) + +# The `messages` list provides the history of messages from +# the system, assistant, and user roles +messages = [ + {"role":"system", "content":"You are a helpful assistant."}, + {"role":"user", "content":"Hi, how are you?"}, +] + +# This is the API call that sends the `messages` history to +# the server's specific LLM `model` +# It returns a `completion`, which is OpenAI's way of referring +# to the LLM's reponse to the messages +completion = client.chat.completions.create( + model="gemma-4-E4B-it-GGUF", + messages=messages, +) + +# This code gets the LLM's response from the `completion` +# and prints it to the screen +response = completion.choices[0].message.content +print(response) + +``` +返回结果如下 +![](./images/11-04.png) + + diff --git a/models/Gemma4/api.py b/models/Gemma4/api.py new file mode 100644 index 0000000..9967715 --- /dev/null +++ b/models/Gemma4/api.py @@ -0,0 +1,185 @@ +# api.py — 与教程 01 一致,已适配 Pydantic v2 / FastAPI,并支持本地模型路径 +from __future__ import annotations + +import logging +import os +import time +from contextlib import asynccontextmanager +from typing import List, Literal, Optional + +import torch +import uvicorn +from fastapi import Body, FastAPI, HTTPException +from pydantic import BaseModel, Field, model_validator +from transformers import AutoModelForMultimodalLM, AutoProcessor + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +DEVICE = "cuda" +DEVICE_ID = os.environ.get("CUDA_DEVICE_ID", "0") +CUDA_DEVICE = f"{DEVICE}:{DEVICE_ID}" if DEVICE_ID else DEVICE + +MODEL_PATH = os.environ.get("GEMMA_MODEL_PATH", "/dataset/gemma-4-E4B-it") + +model = None +processor = None +DEFAULT_SYSTEM_PROMPT = "You are a helpful assistant." + + +def torch_gc(): + if torch.cuda.is_available(): + with torch.cuda.device(CUDA_DEVICE): + torch.cuda.empty_cache() + torch.cuda.ipc_collect() + + +class ContentItem(BaseModel): + type: Literal["text", "image"] + text: Optional[str] = Field(None, description="文本内容(当 type 为 text 时必填)") + image: Optional[str] = Field(None, description="图片 URL 或 base64(当 type 为 image 时必填)") + + @model_validator(mode="after") + def validate_content(self): + if self.type == "text": + if not self.text or not str(self.text).strip(): + raise ValueError("文本类型必须提供 text 字段") + elif self.type == "image": + img = self.image or "" + if not str(img).startswith(("http://", "https://", "data:image")): + raise ValueError("图片必须是有效的 URL 或 base64 编码字符串") + return self + + +class Message(BaseModel): + role: Literal["system", "user", "assistant"] + content: List[ContentItem] + + +class ProcessRequest(BaseModel): + messages: List[Message] = Field(..., min_length=1, description="对话历史记录") + max_new_tokens: int = Field(1000, ge=10, le=4096, description="生成的最大 token 数") + + +class ProcessResponse(BaseModel): + response: str + status: int + time: int + processing_time: float + tokens_generated: int + + +def load_models(): + global model, processor + if not os.path.isdir(MODEL_PATH): + raise FileNotFoundError(f"模型目录不存在: {MODEL_PATH}") + try: + logger.info("正在加载模型: %s", MODEL_PATH) + model = AutoModelForMultimodalLM.from_pretrained( + MODEL_PATH, + dtype="auto", + device_map="auto", + trust_remote_code=True, + ).eval() + logger.info("正在加载处理器...") + processor = AutoProcessor.from_pretrained(MODEL_PATH, trust_remote_code=True) + logger.info("模型加载完成 device=%s", getattr(model, "device", "?")) + except Exception as e: + logger.error("模型加载失败: %s", e) + raise + + +@asynccontextmanager +async def lifespan(app: FastAPI): + try: + load_models() + yield + except Exception as e: + logger.error("服务初始化失败: %s", e) + raise + finally: + torch_gc() + + +app = FastAPI(lifespan=lifespan) + + +def _normalize_content_items(items): + out = [] + for it in items: + if it.get("type") == "text": + out.append({"type": "text", "text": it.get("text") or ""}) + elif it.get("type") == "image": + img = it.get("image") or it.get("url") + if not img: + continue + if str(img).startswith(("http://", "https://")): + out.append({"type": "image", "url": img}) + else: + out.append({"type": "image", "image": img}) + return out + + +@app.post("/chat/completions", response_model=ProcessResponse) +async def generate_response(payload: ProcessRequest = Body(...)): + start_time = time.time() + try: + processed_messages = [] + system_prompt = DEFAULT_SYSTEM_PROMPT + + for msg in payload.messages: + if msg.role == "system": + system_prompt = " ".join( + [item.text or "" for item in msg.content if item.type == "text"] + ) + else: + d = msg.model_dump() + d["content"] = _normalize_content_items(d["content"]) + processed_messages.append(d) + + messages = [ + {"role": "system", "content": [{"type": "text", "text": system_prompt}]}, + *processed_messages, + ] + + inputs = processor.apply_chat_template( + messages, + add_generation_prompt=True, + tokenize=True, + return_tensors="pt", + return_dict=True, + ).to(model.device) + + input_len = inputs["input_ids"].shape[-1] + max_token_num = min(4096, int(payload.max_new_tokens)) + with torch.inference_mode(): + generation = model.generate( + **inputs, + max_new_tokens=max_token_num, + do_sample=False, + ) + response_ids = generation[0][input_len:] + raw = processor.decode(response_ids, skip_special_tokens=False) + try: + parsed = processor.parse_response(raw) + decoded = parsed.get("content", raw) if isinstance(parsed, dict) else raw + except Exception: + decoded = processor.decode(response_ids, skip_special_tokens=True) + + ntok = int(response_ids.numel()) if hasattr(response_ids, "numel") else len(response_ids) + return ProcessResponse( + response=str(decoded), + status=200, + time=int(time.time()), + processing_time=time.time() - start_time, + tokens_generated=ntok, + ) + except HTTPException: + raise + except Exception as e: + logger.error("处理请求时出错: %s", e) + raise HTTPException(status_code=500, detail=str(e)) from e + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=6006) diff --git a/models/Gemma4/images/.keep b/models/Gemma4/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/models/Gemma4/images/01-1.png b/models/Gemma4/images/01-1.png new file mode 100644 index 0000000..8aeec28 Binary files /dev/null and b/models/Gemma4/images/01-1.png differ diff --git a/models/Gemma4/images/01-2.png b/models/Gemma4/images/01-2.png new file mode 100644 index 0000000..4900ed4 Binary files /dev/null and b/models/Gemma4/images/01-2.png differ diff --git a/models/Gemma4/images/02-01.png b/models/Gemma4/images/02-01.png new file mode 100644 index 0000000..44617a5 Binary files /dev/null and b/models/Gemma4/images/02-01.png differ diff --git a/models/Gemma4/images/02-02.png b/models/Gemma4/images/02-02.png new file mode 100644 index 0000000..fb6338f Binary files /dev/null and b/models/Gemma4/images/02-02.png differ diff --git a/models/Gemma4/images/02-03.png b/models/Gemma4/images/02-03.png new file mode 100644 index 0000000..b5bc023 Binary files /dev/null and b/models/Gemma4/images/02-03.png differ diff --git a/models/Gemma4/images/03-1.png b/models/Gemma4/images/03-1.png new file mode 100644 index 0000000..36d9219 Binary files /dev/null and b/models/Gemma4/images/03-1.png differ diff --git a/models/Gemma4/images/03-2.png b/models/Gemma4/images/03-2.png new file mode 100644 index 0000000..ed97a69 Binary files /dev/null and b/models/Gemma4/images/03-2.png differ diff --git a/models/Gemma4/images/04-01.png b/models/Gemma4/images/04-01.png new file mode 100644 index 0000000..050b946 Binary files /dev/null and b/models/Gemma4/images/04-01.png differ diff --git a/models/Gemma4/images/04-02.png b/models/Gemma4/images/04-02.png new file mode 100644 index 0000000..d0d80c7 Binary files /dev/null and b/models/Gemma4/images/04-02.png differ diff --git a/models/Gemma4/images/04-03.png b/models/Gemma4/images/04-03.png new file mode 100644 index 0000000..293b1cc Binary files /dev/null and b/models/Gemma4/images/04-03.png differ diff --git a/models/Gemma4/images/04-05.png b/models/Gemma4/images/04-05.png new file mode 100644 index 0000000..bbada31 Binary files /dev/null and b/models/Gemma4/images/04-05.png differ diff --git a/models/Gemma4/images/06-01.png b/models/Gemma4/images/06-01.png new file mode 100644 index 0000000..ff0f766 Binary files /dev/null and b/models/Gemma4/images/06-01.png differ diff --git a/models/Gemma4/images/06-02.png b/models/Gemma4/images/06-02.png new file mode 100644 index 0000000..27d999a Binary files /dev/null and b/models/Gemma4/images/06-02.png differ diff --git a/models/Gemma4/images/06-03.png b/models/Gemma4/images/06-03.png new file mode 100644 index 0000000..1cc2622 Binary files /dev/null and b/models/Gemma4/images/06-03.png differ diff --git a/models/Gemma4/images/11-01.png b/models/Gemma4/images/11-01.png new file mode 100644 index 0000000..c68cb4a Binary files /dev/null and b/models/Gemma4/images/11-01.png differ diff --git a/models/Gemma4/images/11-02.png b/models/Gemma4/images/11-02.png new file mode 100644 index 0000000..306e2dd Binary files /dev/null and b/models/Gemma4/images/11-02.png differ diff --git a/models/Gemma4/images/11-03.png b/models/Gemma4/images/11-03.png new file mode 100644 index 0000000..9d759fa Binary files /dev/null and b/models/Gemma4/images/11-03.png differ diff --git a/models/Gemma4/images/11-04.png b/models/Gemma4/images/11-04.png new file mode 100644 index 0000000..b6f25e4 Binary files /dev/null and b/models/Gemma4/images/11-04.png differ diff --git a/models/Gemma4/images/11-05.png b/models/Gemma4/images/11-05.png new file mode 100644 index 0000000..69f4157 Binary files /dev/null and b/models/Gemma4/images/11-05.png differ diff --git a/models/Gemma4/images/11-06.png b/models/Gemma4/images/11-06.png new file mode 100644 index 0000000..856541f Binary files /dev/null and b/models/Gemma4/images/11-06.png differ diff --git a/models/Gemma4/verify_gemma4_tutorials.py b/models/Gemma4/verify_gemma4_tutorials.py new file mode 100644 index 0000000..81fac76 --- /dev/null +++ b/models/Gemma4/verify_gemma4_tutorials.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +""" +Gemma4 教程冒烟测试(不默认加载整模权重,避免 OOM;可按需开启网络拉取 Processor)。 + +用法: + /path/to/self-llm/bin/python verify_gemma4_tutorials.py + GEMMA_PULL_PROCESSOR=1 ... # 从 Hub 拉取 Gemma4Processor(需已安装 torchvision,与 torch 同 CUDA 版本) + +说明:本脚本不做整模加载/推理;真机部署请参考各 .md 并自行下载权重。 +""" + +import json +import os +import sys +import time +import traceback +from pathlib import Path +from collections import UserDict +from typing import Any +from unittest.mock import MagicMock + +REPO_ROOT = Path(__file__).resolve().parents[2] +DATASET = REPO_ROOT / "dataset" / "huanhuan.json" +MODEL_ID = "google/gemma-4-E4B-it" + + +def _ok(name: str, detail: str = "") -> None: + print(f"[PASS] {name}" + (f" — {detail}" if detail else "")) + + +def _fail(name: str, err: BaseException) -> None: + print(f"[FAIL] {name}: {err}") + traceback.print_exc() + + +def test_imports() -> bool: + import numpy + import torch + import transformers + from transformers import AutoModelForMultimodalLM, AutoProcessor + + assert hasattr(torch, "cuda") + _ok( + "import 栈", + f"numpy={numpy.__version__}, torch={torch.__version__}, " + f"cuda={torch.cuda.is_available()}, transformers={transformers.__version__}", + ) + _ = AutoModelForMultimodalLM + _ = AutoProcessor + return True + + +def test_pull_processor() -> bool: + if os.environ.get("GEMMA_PULL_PROCESSOR", "").strip() not in ("1", "true", "yes"): + print("[SKIP] Processor Hub 拉取(设置 GEMMA_PULL_PROCESSOR=1 可开启)") + return True + from transformers import AutoProcessor + + t0 = time.time() + proc = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True) + dt = time.time() - t0 + _ok("AutoProcessor.from_pretrained", f"{type(proc).__name__}, {dt:.1f}s") + return True + + +def test_fastapi_smoke_with_mock() -> bool: + """教程 01 路由逻辑:用 Mock 模型验证请求/响应(避免真推理)。""" + from contextlib import asynccontextmanager + + from fastapi import Body, FastAPI, HTTPException + from fastapi.testclient import TestClient + from pydantic import BaseModel, Field, model_validator + from typing import List, Literal, Optional + + class ContentItem(BaseModel): + type: Literal["text", "image"] + text: Optional[str] = Field(None, description="文本") + image: Optional[str] = Field(None, description="图片 URL 或 base64") + + @model_validator(mode="after") + def _v(self): + if self.type == "text" and not (self.text and str(self.text).strip()): + raise ValueError("文本类型必须提供 text 字段") + if self.type == "image": + img = self.image or "" + if not img.startswith(("http://", "https://", "data:image")): + raise ValueError("图片必须是有效的 URL 或 base64(data:image)") + return self + + class Message(BaseModel): + role: Literal["system", "user", "assistant"] + content: List[ContentItem] + + class ProcessRequest(BaseModel): + messages: List[Message] = Field(..., min_length=1) + max_new_tokens: int = Field(1000, ge=10, le=4096) + + class ProcessResponse(BaseModel): + response: str + status: int + time: int + processing_time: float + tokens_generated: int + + import torch + + class _Batch(UserDict): + def to(self, _device): + return self + + mock_model = MagicMock() + mock_model.device = "cpu" + mock_model.generate = MagicMock(return_value=torch.tensor([[1, 2, 99, 100]])) + + mock_processor = MagicMock() + mock_processor.apply_chat_template = MagicMock( + return_value=_Batch( + input_ids=torch.tensor([[1, 2]]), + attention_mask=torch.tensor([[1, 1]]), + ) + ) + mock_processor.decode = MagicMock(return_value="") + mock_processor.parse_response = MagicMock(side_effect=Exception("no parse")) + + model_ref = {"m": mock_model, "p": mock_processor} + + @asynccontextmanager + async def lifespan(app: FastAPI): + yield + + app = FastAPI(lifespan=lifespan) + DEFAULT_SYSTEM_PROMPT = "You are a helpful assistant." + + def _normalize_content_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]: + out = [] + for it in items: + if it.get("type") == "text": + out.append({"type": "text", "text": it.get("text") or ""}) + elif it.get("type") == "image": + img = it.get("image") or it.get("url") + if not img: + continue + if str(img).startswith(("http://", "https://")): + out.append({"type": "image", "url": img}) + else: + out.append({"type": "image", "image": img}) + return out + + @app.post("/chat/completions", response_model=ProcessResponse) + async def generate_response(chat: ProcessRequest = Body(...)): + start_time = time.time() + try: + model, processor = model_ref["m"], model_ref["p"] + processed_messages = [] + system_prompt = DEFAULT_SYSTEM_PROMPT + for msg in chat.messages: + if msg.role == "system": + system_prompt = " ".join([item.text or "" for item in msg.content if item.type == "text"]) + else: + d = msg.model_dump() + d["content"] = _normalize_content_items(d["content"]) + processed_messages.append(d) + messages = [ + {"role": "system", "content": [{"type": "text", "text": system_prompt}]}, + *processed_messages, + ] + inputs = processor.apply_chat_template( + messages, + add_generation_prompt=True, + tokenize=True, + return_tensors="pt", + return_dict=True, + ).to(model.device) + input_len = inputs["input_ids"].shape[-1] + max_token_num = min(4096, int(chat.max_new_tokens)) + with torch.inference_mode(): + generation = model.generate(**inputs, max_new_tokens=max_token_num, do_sample=False) + response_ids = generation[0][input_len:] + raw = processor.decode(response_ids, skip_special_tokens=False) + try: + parsed = processor.parse_response(raw) + decoded = parsed.get("content", raw) if isinstance(parsed, dict) else raw + except Exception: + decoded = processor.decode(response_ids, skip_special_tokens=True) + return ProcessResponse( + response=str(decoded), + status=200, + time=int(time.time()), + processing_time=time.time() - start_time, + tokens_generated=int(len(response_ids)), + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) from e + + payload = { + "messages": [ + { + "role": "user", + "content": [ + {"type": "text", "text": "你好,只做连通性测试"}, + ], + } + ], + "max_new_tokens": 64, + } + with TestClient(app) as client: + r = client.post("/chat/completions", json=payload) + assert r.status_code == 200, r.text + body = r.json() + assert "response" in body + _ok("FastAPI /chat/completions (Mock)", f"tokens_generated={body.get('tokens_generated')}") + return True + + +def test_lora_dataset() -> bool: + if not DATASET.is_file(): + print(f"[SKIP] 数据集不存在: {DATASET}") + return True + raw = json.loads(DATASET.read_text(encoding="utf-8")) + assert isinstance(raw, list) and len(raw) > 0 + first = raw[0] + for k in ("instruction", "input", "output"): + assert k in first + _ok("05 LoRA 数据集", f"{DATASET.name} 条数={len(raw)}") + return True + + +def test_evalscope_import() -> bool: + from evalscope.config import TaskConfig + from evalscope.run import run_task + + _ = TaskConfig + _ = run_task + _ok("evalscope 导入", "TaskConfig / run_task 可用(完整评测需 Ollama 等服务)") + return True + + +def main() -> int: + tests = [ + ("环境导入", test_imports), + ("05 嬛嬛数据集", test_lora_dataset), + ("01 FastAPI Mock", test_fastapi_smoke_with_mock), + ("04 evalscope", test_evalscope_import), + ("Hub Processor", test_pull_processor), + ] + failed = 0 + for name, fn in tests: + try: + fn() + except Exception as e: + failed += 1 + _fail(name, e) + print("---") + print(f"完成: {len(tests) - failed}/{len(tests)} 通过") + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main())