02_eval_and_infer.ipynb 说明文档

这份 notebook 的任务不是继续训练,而是做另一件同样重要的事:

这一步非常关键,因为很多同学第一次做 LoRA 实验时,会以为:

其实不是。

真正完整的闭环应该是:

  1. 模型能训练
  2. 训练结果能保存
  3. 保存结果能重新加载
  4. 重新加载后能正常生成

02_eval_and_infer.ipynb 就是在验证第 3、4 步。


一、这个 notebook 整体在做什么

它做了 4 件事:

  1. 加载训练前的 base model
  2. 加载训练后保存的 LoRA adapter
  3. 把两者重新组合起来
  4. 输入一个 prompt,看看模型能不能生成结果

所以,这份 notebook 其实是在回答:


二、Cell 0:开场说明

# 02_eval_and_infer

这个 notebook 用于加载训练后的输出目录,做简单推理测试。

这一格没有代码,只是在告诉你:


三、Cell 1:重新加载 base model、adapter 和 tokenizer

代码:

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

base_model_name = "/root/course_lora/models/tiny-gpt2"
adapter_dir = "outputs/notebook_demo"

tokenizer = AutoTokenizer.from_pretrained(adapter_dir)
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token

base_model = AutoModelForCausalLM.from_pretrained(base_model_name)
model = PeftModel.from_pretrained(base_model, adapter_dir)
model.eval()
if torch.cuda.is_available():
    model = model.cuda()
print("Model loaded.")

这一格在做什么

这是整份 notebook 最重要的一格。

它在做一件 LoRA 实验里非常关键的事:

这一步会帮助你真正理解:

逐行解释

from transformers import AutoTokenizer, AutoModelForCausalLM

导入两个 Hugging Face 常用类:

这里和 01_lora_demo.ipynb 一样,说明当前实验仍然在 GPT 风格的因果语言模型框架里工作。

from peft import PeftModel

导入 PeftModel

这是这一格的新重点。

如果前一个 notebook 主要在讲:

那么这里要看的就是:

import torch

导入 PyTorch。

后面会用它判断 GPU 是否可用,并把模型搬到 GPU 上。

base_model_name = "/root/course_lora/models/tiny-gpt2"

定义 base model 的本地路径。

这个目录里放的是:

它是你训练之前的底座模型。

adapter_dir = "outputs/notebook_demo"

定义 adapter 的保存目录。

这是前一个 notebook 训练结束后保存的结果目录。
里面重点存的是:

tokenizer = AutoTokenizer.from_pretrained(adapter_dir)

从输出目录里加载 tokenizer。

为什么不是从 base_model_name 里加载?

因为课程代码在训练结束后把 tokenizer 也保存到了输出目录里。
这样后续推理时:

这是一种很常见的工程习惯。

if tokenizer.pad_token is None:

再次检查 tokenizer 是否有 pad token。

为什么推理阶段还要检查?

因为 notebook 想保持稳妥,不假设一切默认都已经配置好。

tokenizer.pad_token = tokenizer.eos_token

如果没有 pad token,就继续沿用:

这和前一个 notebook 保持一致。

base_model = AutoModelForCausalLM.from_pretrained(base_model_name)

从原始模型目录加载 base model。

注意这里得到的还只是:

model = PeftModel.from_pretrained(base_model, adapter_dir)

这一行是整格最核心的一行。

它的意思是:

也就是说,这一步之后的 model 才是:

这一行其实正好回答了很多同学最容易糊涂的问题:

答案是:

model.eval()

把模型切换到评估模式。

eval() 的作用可以简单理解成:

这样像 dropout 这类训练时才会随机生效的机制,在推理时就会关闭。

这是一个标准动作:

if torch.cuda.is_available():

如果当前机器有可用 GPU,就继续执行下面一行。

model = model.cuda()

把模型放到 GPU 上。

这里的 .cuda() 可以先简单理解成:

如果不把模型搬过去,而后面输入又在 GPU 上,就会报设备不一致错误。

print("Model loaded.")

打印加载完成提示。

这一格跑通之后,说明:


四、Cell 2:构造 prompt,执行一次最小推理

代码:

prompt = "Instruction: 将下面这句话改写得更清楚。\nInput: 学生要先学会检查 GPU,再开始训练。\nResponse:"
inputs = tokenizer(prompt, return_tensors="pt")
if torch.cuda.is_available():
    inputs = {k: v.cuda() for k, v in inputs.items()}

with torch.no_grad():
    outputs = model.generate(**inputs, max_new_tokens=50)

print(tokenizer.decode(outputs[0], skip_special_tokens=True))

这一格在做什么

这格是在做一次最小推理实验。

它的逻辑很简单:

  1. 写一个 prompt
  2. 用 tokenizer 把 prompt 转成模型输入
  3. 如果有 GPU,就把输入也搬到 GPU
  4. 调用 generate()
  5. 把生成结果解码成人类能读的文本

逐行解释

prompt = "..."

定义一个字符串 prompt。

这里的 prompt 格式和训练时故意保持一致:

Instruction: ...
Input: ...
Response:

最后只写到 Response:,没有把答案写出来。
这样模型就会尝试继续生成它认为合适的回复。

这一点非常重要,因为它体现了一个课程关键点:

inputs = tokenizer(prompt, return_tensors="pt")

把 prompt token 化。

这里的 return_tensors="pt" 很关键:

如果不加这项,tokenizer 可能只返回普通 Python 列表。
而模型推理通常需要的是张量。

所以这行代码做的事情是:

通常里面会包括:

if torch.cuda.is_available():

如果有 GPU,就继续把输入也搬到 GPU。

inputs = {k: v.cuda() for k, v in inputs.items()}

这一行是一个字典推导式。

意思是:

为什么要这样做?

因为 inputs 不是单个张量,而是一个字典,例如:

{
  "input_ids": tensor(...),
  "attention_mask": tensor(...)
}

所以必须把字典里的每个张量都搬到 GPU。

这一步也非常重要,因为:

with torch.no_grad():

这是 PyTorch 推理时很常见的一句。

意思是:

为什么?

因为现在是推理,不是训练。

不计算梯度的好处是:

outputs = model.generate(**inputs, max_new_tokens=50)

开始真正生成文本。

这是 Hugging Face 模型推理里最常见的接口之一。

generate(...)

表示:

**inputs

这里的 ** 是 Python 里的“字典展开”。

它的意思是:

例如如果 inputs 是:

{"input_ids": ..., "attention_mask": ...}

那么:

model.generate(**inputs)

就相当于:

model.generate(input_ids=..., attention_mask=...)
max_new_tokens=50

表示:

为什么不是越大越好?

因为这只是最小演示:

print(tokenizer.decode(outputs[0], skip_special_tokens=True))

这行是在把模型输出重新变成人能读懂的文本。

outputs[0]

因为这里通常只输入了一条 prompt,所以只取第一条输出。

tokenizer.decode(...)

decode 的作用是:

skip_special_tokens=True

表示:

例如不要把某些控制 token 直接原样打印出来。

这一格的意义

如果这格能顺利跑完,说明:

  1. 训练输出可被加载
  2. 模型和输入设备一致
  3. generate() 能正常工作
  4. 结果可以被解码成自然语言

换句话说,这一步证明:


五、Cell 3:结果解释

这里的结果主要用于验证“训练输出可被加载、推理流程正常”,不追求真正高质量的生成效果。

这句话非常重要。

很多同学第一次看到生成结果时,会立刻问:

但这份 notebook 的目标不是生成高质量答案,而是验证:

也就是说,这里真正想确认的是:

而不是:


六、这份 notebook 最重要的 3 个概念

1. LoRA 训练结果不是完整模型,而是 adapter

这就是为什么推理时要先:

再:

2. 训练时的数据格式,要和推理时的 prompt 格式尽量一致

你训练时用的是:

Instruction: ...
Input: ...
Response: ...

那推理时最好也按这个格式来。

3. 推理和训练是两种不同模式

推理阶段至少有两个标准动作:

这是大模型工程里非常基础的习惯。


七、这份 notebook 真正回答了什么问题

如果说:

那么:

这两份 notebook 加在一起,才构成了真正完整的最小闭环:

  1. 环境检查
  2. 最小 LoRA 训练
  3. 重新加载并推理

八、跑完这份 notebook 后,你应该能回答什么

如果你真的理解了它,至少应该能回答:

  1. 为什么 LoRA 推理时还需要 base model?
  2. PeftModel.from_pretrained(...) 在做什么?
  3. 为什么推理前要 model.eval()
  4. 为什么 inputs 也要搬到 GPU?
  5. 为什么 generate() 输出后还要 decode()

如果这些问题你都能用自己的话说清楚,那说明你已经不只是“跑通了实验”,而是已经开始真正理解 LoRA 推理链路。