有一年没更新了,最近学习了大模型的微调的流程,总结一下。
背景介绍
我们都知道大模型的训练分为三个阶段,预训练,SFT和RHLF。让deepseek总结一下:
| 训练阶段 | 官方标准名 | 核心数据 | 模型状态 | 实操中的主流方法(含LoRA) |
|---|---|---|---|---|
| 第一阶段 | Pre-training (预训练) | 海量无标签纯文本 | 基座模型(只会接龙) | 全量训练 (必须烧钱,几百张显卡跑几个月,几乎不用LoRA,因为要学全新知识) |
| 第二阶段 | SFT / Instruction Tuning (监督微调/指令微调) | 人工写的问答对(有标准答案) | 对话模型(会聊天) | 全量微调 或 LoRA微调(此处LoRA极常用,花几十块钱就能让模型学会特定对话风格) |
| 第三阶段 | RLHF (基于人类反馈的强化学习) | 人类对回答的偏好排序(无答案) | 对齐模型(高情商) | 全量微调 或 LoRA微调(同样可以用LoRA,但涉及到强化学习的策略更新,比SFT复杂些) |
预训练基本都是大公司做的事情,基座模型的生成耗费资源和微调推理不是一个量级,小公司基本都是调用api和做下游任务的微调。普通人对大模型的训练能做的就是开源的小模型上进行一些微调训练,以更好的匹配下游任务,取得更好的表现。
本次案例就是就是使用个人电脑本地微调,模型使用千问3.5的最小的参数的0.8b模型,千问3.6和3.8版本没有这么小的尺寸。我笔记本只有8g显存,更大的显存可以考虑换成更大的模型。
下载模型使用阿里的魔塔社区来替代huggingface,免得国内有网络问题无法下载。
数据介绍
分为训练集和测试集,就3列,文本,标签,和违规主题。

这次数据集只有'不违规'和'偏见歧视' 两种标签,是2分类任务。
传统做法就是机器学习分词,词袋,tfidf,然后树模型,或者Embedding然后神经网络。
本次的应用场景就是让大模型当内容审核专家。大模型就是可以用提示词和文本一起输入,让他打出标签。
大厂开源的原始模型效果不一定好,可以进行微调让其分类效果更好。这里的微调都是lora微调,全量参数微调所耗费资源也是过于庞大。
为什么不用api?因为有些场景可能需要速度,需要本地数据隐私,参数量越大的模型当然效果越好。小模型也有小模型的优势,本文0.8b的模型只是一个微调的'玩具'案例。主要还是展示大模型的微调的流程。
环境安装
尽量可以使用较新的库,核心库还是 torch transformers,
可以参考我的环境,python3.11版本,cuda13.4。核心库如下:

安装的是cuda12.6版本的gpu版本的torch,选择2.11没选那么新因为 unsloth库不支持。numpy应该可以再高一点。
主要安装命令和流程为:
python
##创建虚拟环境
conda create --name unsloth_env python=3.11 -y
conda activate unsloth_env
## 安装torch
pip install torch==2.11.0 torchvision==0.26.0 torchaudio==2.11.0 --index-url https://download.pytorch.org/whl/cu130
python -c "import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_name(0))" # 验证一下
pip install unsloth
#其他的核心库
pip install modelscope
pip install numpy pandas scipy scikit-learn matplotlib seaborn openpyxl
## 如果torch版本被换了,就强制安装这个版本
pip install torch==2.11.0 torchvision==0.26.0 torchaudio==2.11.0 --index-url https://download.pytorch.org/whl/cu126 --force-reinstall --no-cache-dir
可以按照这个流程来安装环境。
代码实现
环境验证
看一下版本对不对:
python
# 环境配置
import torch
print(f"PyTorch: {torch.__version__}")
print(f"CUDA available: {torch.cuda.is_available()}")
print(f"MPS available: {torch.backends.mps.is_available()}")
# 自动选择设备
if torch.cuda.is_available():
DEVICE = "cuda"
DTYPE = torch.float16
elif torch.backends.mps.is_available():
DEVICE = "mps"
DTYPE = torch.float16 # MPS 支持 float16
else:
DEVICE = "cpu"
DTYPE = torch.float32
print(f"Using device: {DEVICE}, dtype: {DTYPE}")

2.11.0的gpu版本。据我测试gpu应该差不多是cpu的5-6倍,都是家用电脑的cpu和gpu。
模型下载
一般都是huggingface下载模型文件,鉴于不是人人都有访问外面的网络的环境,还是用国内的modelscope库(阿里的魔塔社区)进行下载。
python
# 下载模型到本地(如果已下载会自动跳过)
from modelscope.hub.snapshot_download import snapshot_download
import modelscope
local_dir = snapshot_download(
"Qwen/Qwen3.5-0.8B",
local_dir="./my_models/qwen3.5" # 指定你想存放的路径
)
print(f"模型本地路径: {local_dir}")

默认路径会放在c盘,可以自己自定义位置。我放在当前目录my_models下了。
原始模型推理测试
用transformers里面的 AutoTokenizer, AutoModelForCausalLM 可以让模型变成我们的对话模型:
python
# 模型加载
from transformers import AutoTokenizer, AutoModelForCausalLM
tokenizer = AutoTokenizer.from_pretrained(local_dir, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
local_dir, trust_remote_code=True,
torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
device_map="auto"
)
print("原始模型加载完成")
提示词准备:
python
# 单条样本推理
# 待测试文本
text = """
这个人农村来的吧,一点素质都没有
"""
Template = f"""
# 任务描述
你是内容审核专家,请对输入的文本进行**偏见与歧视**相关内容审核,严格优先处理种族歧视、地域歧视、性别歧视、宗教歧视、 LGBTQ+ 群体歧视、残障歧视、年龄歧视、外貌歧视等。
# 规则要点(用于判断时参考):
- 种族/民族歧视:含针对特定种族、民族或族裔的贬损称呼、侮辱性词汇、刻板印象攻击,或鼓吹种族优越/种族隔离/种族清洗。
- 地域/籍贯歧视:含针对特定地区、省份、城市人群的贬低、嘲讽、污名化,如"XX地方的人都是XX"等地域黑言论。
- 性别歧视:含贬低特定性别、宣扬性别优越论、固化性别刻板印象、对性别角色的侮辱性调侃。
- LGBTQ+ 群体歧视:含针对同性恋、双性恋、跨性别者等性少数群体的侮辱性称呼、贬损、排斥或鼓励歧视性政策。
- 宗教歧视:含针对特定宗教信仰或教徒的侮辱、亵渎性言论,或鼓吹宗教仇恨、宗教迫害。
- 残障歧视:含针对身体或精神残障人士的侮辱、嘲笑、贬低,或将残障作为负面形容词使用。
- 年龄歧视:含针对特定年龄群体的贬损、排斥,或宣扬年龄决定论。
- 外貌/身材歧视:含针对他人外貌、体型、肤色的侮辱性评价、嘲讽或贬低。
- 阶级/身份歧视:含针对特定职业、收入阶层、教育背景、社会地位群体的贬低与排斥。
# 文本内容
{text}
# 输出要求
风险类型:xxx
判断依据:xxx
"""
Prompt = Template.format(text=text)
print(Prompt)

输入到模型,打印一下原始的输出:
python
messages = [{"role": "user", "content": Prompt}]
inputs = tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_tensors="pt",
#enable_thinking=True
).to(model.device)
outputs = model.generate(**inputs,
max_new_tokens=1300,
do_sample=True,
temperature=0.7
)
# 原始的输出
print(tokenizer.decode(outputs[0], skip_special_tokens=False))

大模型的输入也在里面,还有一些think的分割标识。去掉后可以得到正常的模型回复:
python
# 去除输入和think
input_length = inputs["input_ids"].shape[1]
generated_tokens = outputs[0][input_length:]
response = tokenizer.decode(generated_tokens, skip_special_tokens=True)
print(response)

数据预处理
微调数据格式准备,先读取训练集数据:
python
import json, os,re
import numpy as np
import pandas as pd
# 读取 训练数据 CSV
df_train = pd.read_csv("df_train.csv", encoding="utf-8-sig")
df_train.head()

简单清洗一下:
python
# 清洗数据
df_cleaned = (df_train
.dropna(subset=['text', 'label']) #去白
.assign(text=lambda x: x['text'].astype(str).str.strip()) #去除文本中的前后空格、换行符等异常字符
.assign(target_output=lambda x: x['label'].map({1: "REJECT", 0: "PASS"}))
.dropna(subset=['target_output']) # 过滤掉无法映射成功的无效标签
)
print(f"原始: {len(df_train)} -> 清洗后: {len(df_cleaned)}")
df_cleaned.head()

然后我们要把文本和标签预提示词进行拼接,得到训练语料:
python
# 1. 配置参数
# 原始数据集中,标签列名为 'label'
# 如果已经是 'REJECT' 和 'PASS' 则无需映射;如果是 1 和 0,需要映射
LABEL_COL = "label"
TEXT_COL = "text"
# 定义 QwenSFT的Prompt模板,注意这个模板和测试、后期使用的要保持一致
TEMPLATE = """
# 任务描述
你是内容审核专家,请根据审核规则,对输入的文本内容进行**偏见与歧视**相关内容审核,结合语境综合判断。
# 审核规则:
- 种族/民族歧视:含针对特定种族、民族或族裔的贬损称呼、侮辱性词汇、刻板印象攻击,或鼓吹种族优越/种族隔离/种族清洗。
- 地域/籍贯歧视:含针对特定地区、省份、城市人群的贬低、嘲讽、污名化,如"XX地方的人都是XX"等地域黑言论。
- 性别歧视:含贬低特定性别、宣扬性别优越论、固化性别刻板印象、对性别角色的侮辱性调侃。
- LGBTQ+ 群体歧视:含针对同性恋、双性恋、跨性别者等性少数群体的侮辱性称呼、贬损、排斥或鼓励歧视性政策。
- 宗教歧视:含针对特定宗教信仰或教徒的侮辱、亵渎性言论,或鼓吹宗教仇恨、宗教迫害。
- 残障歧视:含针对身体或精神残障人士的侮辱、嘲笑、贬低,或将残障作为负面形容词使用。
- 年龄歧视:含针对特定年龄群体的贬损、排斥,或宣扬年龄决定论。
- 外貌/身材歧视:含针对他人外貌、体型、肤色的侮辱性评价、嘲讽或贬低。
- 阶级/身份歧视:含针对特定职业、收入阶层、教育背景、社会地位群体的贬低与排斥。
# 文本内容
{text}
# 输出要求
如果存在偏见与歧视风险,输出'REJECT',如果不存在,输出'PASS',不要有其他多余的输出。
"""
# 转换为 Qwen SFT 格式
def convert_to_qwen_sft(df: pd.DataFrame) -> list:
"""将 DataFrame 转换为 Qwen 3.5 推荐的 Messages 格式"""
sft_dataset = []
for _, row in df.iterrows():
# 填充模板中的 {text}
user_instruction = TEMPLATE.format(text=row[TEXT_COL])
assistant_response = row["target_output"]
# 构造 Qwen Chat 格式
messages_format = {
"messages": [
{
"role": "system",
"content": "你是一个安全合规的助手,专门负责文本内容审核。",
},
{"role": "user", "content": user_instruction},
{"role": "assistant", "content": assistant_response},
]
}
sft_dataset.append(messages_format)
return sft_dataset
转化数据,展示一条:
python
# 转换格式
qwen_sft_data = convert_to_qwen_sft(df_cleaned)
# 保存为 JSONL 格式 (LLM训练最常用的格式)
output_path = "df_train_sft_data.jsonl"
with open(output_path, "w", encoding="utf-8") as f:
for item in qwen_sft_data:
f.write(json.dumps(item, ensure_ascii=False) + "\n")
print(f"SFT 训练集已成功保存至: {os.path.abspath(output_path)}")
# 打印一条样例展示
print("\n--- 样例展示 ---")
print(json.dumps(qwen_sft_data[10], indent=2, ensure_ascii=False))

训练语料基本应该都是这种json格式的。
基础模型效果评测
1、评估函数编写
2分类的问题还是看准确率,精准率,召回率,F1这些。
python
from sklearn.metrics import classification_report
def acc_eval_fun(df, true_col='label', pred_col='pred'):
# 1. 过滤无效值并计算混淆矩阵
valid_df = df[df[pred_col].isin([0, 1])]
cm = pd.crosstab(valid_df[true_col], valid_df[pred_col]).reindex(index=[0, 1], columns=[0, 1], fill_value=0)
tn, fp, fn, tp = cm.values.ravel()
# 2. 计算核心指标
total_pos, total_neg = tp + fn, tn + fp
prec, rec = tp / (tp + fp) if (tp + fp) else 0, tp / total_pos if total_pos else 0
metrics = {
'acc': (tp + tn) / len(valid_df) if len(valid_df) else 0,
'precision': prec, 'recall': rec,
'f1': 2 * prec * rec / (prec + rec) if (prec + rec) else 0,
'fpr': fp / total_neg if total_neg else 0, 'fnr': fn / total_pos if total_pos else 0,
'tp': tp, 'fp': fp, 'tn': tn, 'fn': fn
}
# 3. 极简格式化输出
print(f"\n=== 评测报告 (有效样本: {len(valid_df)}) ===")
print(f"TN: {tn:<6} FP: {fp:<6} | 误杀率(FPR): {metrics['fpr']:.2%}")
print(f"FN: {fn:<6} TP: {tp:<6} | 漏放率(FNR): {metrics['fnr']:.2%}")
print(f"Accuracy: {metrics['acc']:.4f} | Precision: {prec:.4f} | Recall: {rec:.4f} | F1: {metrics['f1']:.4f}\n")
return metrics
2、批量样本评测
先加载模型
python
# 加载模型
from transformers import AutoTokenizer, AutoModelForCausalLM
tokenizer = AutoTokenizer.from_pretrained(local_dir, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
local_dir,
trust_remote_code=True,
torch_dtype=DTYPE,
device_map="auto",
)
model.eval()
print("原始模型加载完成")
编写大模型输出的函数,输入就是提示词,输出就是返回的打标结果。然后读取测试集:
python
# 1. 动态初始化配置(写在函数外,只执行一次,避免单次调用重复设置)
tokenizer.pad_token_id = tokenizer.pad_token_id or tokenizer.eos_token_id
def llm_call(prompt):
messages = [
{"role": "system", "content": "你是一个安全合规的助手,专门负责文本内容审核。"},
{"role": "user", "content": prompt}
]
# 转换为模型输入 Tensor 字典
inputs = tokenizer.apply_chat_template(
messages, add_generation_prompt=True, return_tensors="pt", return_dict=True
).to(model.device)
# 不计算梯度,加速推理
with torch.no_grad():
outputs = model.generate(
**inputs, max_new_tokens=1300, do_sample=True, temperature=0.7
)
# 截取生成的文本部分
input_len = inputs.input_ids.shape[1]
return tokenizer.decode(outputs[0][input_len:], skip_special_tokens=True)
# 读取测试数据
df_test = pd.read_csv("df_test.csv", encoding="utf-8-sig")
df_test.shape

测试集600条。
批量执行上述的大模型输出函数,将测试集的文本和提示词拼接作为输入,然后查看模型打标的分布的结果
python
from tqdm import tqdm
df_test['res'] = [
llm_call(TEMPLATE.format(text=text))
for text in tqdm(df_test['text'], desc='推理中')
]
print(df_test['res'].value_counts())

可以看到模型的输出基本上pass或者reject两类,但是存在一些特殊字符,需要洗一下,然后进行评估。
python
# 1. 提取关键字并映射为数值 (REJECT->1, PASS->0, 未匹配或其它->-1)
kw = df_test['res'].str.extract(r'(REJECT|PASS)', flags=re.I)[0].str.upper()
df_test['pred'] = kw.map({'REJECT': 1, 'PASS': 0}).fillna(-1).astype(int)
# 2. 评估
acc_eval_fun(df_test)

可以看到F1值大概只有可怜的0.081,太低了,几乎全部漏放了,这是原始模型的效果,下面对模型进行lora微调后我们再进行评估。
LoRA 微调训练
基础模型加载
重新释放缓存和读取模型
python
# 释放原始模型显存/内存,为训练腾空间
try:
del model
except NameError:
pass
if torch.backends.mps.is_available():
torch.mps.empty_cache()
if torch.cuda.is_available():
torch.cuda.empty_cache()
elif torch.backends.mps.is_available():
torch.mps.empty_cache()
print("原始模型已释放")
重新下载模型。如果文件存在就会加载缓存。
python
from modelscope.hub.snapshot_download import snapshot_download
local_dir = snapshot_download(
"Qwen/Qwen3.5-0.8B",
local_dir="./my_models/qwen3.5" # 指定你想存放的路径
)
print(f"模型本地路径: {local_dir}")
看一下设备,有gpu用gpu训练,没有就cpu,反正模型小,都跑得动。
python
#序列长度可根据显存调整
max_seq_length = 1024
# 自动选择设备
if torch.cuda.is_available():
DEVICE = "cuda"
DTYPE = torch.float16
elif torch.backends.mps.is_available():
DEVICE = "mps"
DTYPE = torch.float16 # MPS 支持 float16
else:
DEVICE = "cpu"
DTYPE = torch.float32
print(f"Using device: {DEVICE}, dtype: {DTYPE}")
加载数据
python
from datasets import load_dataset
from peft import LoraConfig, get_peft_model, TaskType
from transformers import AutoTokenizer, AutoModelForCausalLM
from trl import SFTTrainer
# 加载数据集
file_path = "./df_train_sft_data.jsonl"
dataset = load_dataset("json", data_files={"train": file_path}, split="train")
print(f"数据集大小: {len(dataset)}")
print(f"数据集字段: {dataset.column_names}")
print(json.dumps(dataset[0], indent=2, ensure_ascii=False))

加载原始模型的分词和权重
python
# 用 transformers 原生加载模型(兼容 PyTorch + MPS)----
# 这里改用标准 transformers + peft 做 LoRA 微调
tokenizer = AutoTokenizer.from_pretrained(local_dir, trust_remote_code=True)
# 确保 pad_token 存在
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
# MPS 训练建议用 float32,fp16 训练数值不稳定容易导致训练坍塌
TRAIN_DTYPE = torch.float32 if DEVICE == "mps" else DTYPE
model = AutoModelForCausalLM.from_pretrained(
local_dir,
trust_remote_code=True,
#torch_dtype=torch.float32,
torch_dtype=TRAIN_DTYPE,
device_map="auto",
)
print(f"模型加载完成(PyTorch + {DEVICE}, dtype={TRAIN_DTYPE})")

添加 LoRA 适配器
python
# 添加 LoRA 适配器(使用 peft 原生 API)
lora_config = LoraConfig(
r=16,
lora_alpha=16,
lora_dropout=0,
bias="none",
task_type=TaskType.CAUSAL_LM,
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
print("LoRA 适配器添加完成")

开始LoRA训练
python
# ---- 配置训练器 ----
from trl import SFTTrainer, SFTConfig
trainer = SFTTrainer(
model=model,
train_dataset=dataset, # 用格式化后的小数据集
processing_class=tokenizer,
args=SFTConfig(
per_device_train_batch_size=1,
gradient_accumulation_steps=4,
warmup_steps=10,
max_steps=60, # 训练60轮 快速验证用
#num_train_epochs=5,
learning_rate=1e-4, # LoRA 推荐学习率,默认值可能太大导致坍塌
logging_steps=1,
output_dir="outputs_qwen35",
optim="adamw_torch",
seed=3407,
dataset_num_proc=1,
save_steps=20, #每20轮存一下
save_total_limit=2,
dataloader_pin_memory=False,
# use_liger_kernel=False,
report_to="none",
fp16=False,
bf16=False,
),
)
print("训练器配置完成,开始训练...")
trainer.train()
print("训练完成!")

训练了60轮,每20轮保存一下, gpu跑了半个小时吧。
这里的trainer.train()进行训练后,mdel里面的权重参数已经被改变。已经变成微调后的模型了。可以直接用这个model进行测试集评估了。
训练后模型评测
python
model.eval() #推理模式,模型权重已经变改变。
df_test = pd.read_csv("df_test.csv", encoding="utf-8-sig")
# 循环推理并实时打印
def predict_and_log(i, text, label):
res = llm_call(TEMPLATE.format(text=text))
print(f"第 {i} 个样本 | 标签: {label} | 预测: {res}\n文本: {text}\n" + "-"*40)
return res
df_test['res'] = [predict_and_log(i, row.text, row.label) for i, row in enumerate(df_test.itertuples())]
print(df_test["res"].value_counts())

打印每一个样本的推理结果。
最终所有的结果,还是一样提取模型的输出关键词,然后进行评估
python
# 提取关键词 -> 统一转大写 -> 映射为 1/0 -> 缺失补 -1
df_test['pred'] = (
df_test['res']
.str.extract(r'(?i)(REJECT|PASS)')[0]
.str.upper()
.map({'REJECT': 1, 'PASS': 0})
.fillna(-1)
.astype(int)
)
# 模型评估
acc_eval_fun(df_test)

可以看到微调后的模型在测试集上的F1值是0.8855,提高了0.8,说明模型微调还是很有效的。
保存 LoRA 权重
lora其实是一个插件,原始模型的权重参数其实没有变,我们可以单独保存lora的权重,也可以和原始模型一起存储。
python
# 保存 LoRA 适配器权重(只保存增量,体积小)----
lora_output_dir = "outputs_qwen35/lora_final"
model.save_pretrained(lora_output_dir)
tokenizer.save_pretrained(lora_output_dir)
print(f"LoRA 权重已保存到: {lora_output_dir}")
#保存完整模型(合并 LoRA + 基座)----
merged_output_dir = "outputs_qwen35/merged_model"
merged_model = model.merge_and_unload()
merged_model.save_pretrained(merged_output_dir)
tokenizer.save_pretrained(merged_output_dir)
print(f"合并后的完整模型已保存到: {merged_output_dir}")
可以看到当前目录会多了几个模型文件。
重新加载权重并推理-验证保存正确性
1.分别加载LoRA 和 基模
第一种加载方式是分开加载,然后进行预测:
python
import gc
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
# 1. 彻底清空显存 (合并 MPS 和 CUDA)
if 'model' in globals(): del model
gc.collect()
if torch.cuda.is_available(): torch.cuda.empty_cache()
if torch.backends.mps.is_available(): torch.mps.empty_cache()
# 2. 加载模型与 Tokenizer
lora_output_dir = "outputs_qwen35/lora_final"
tokenizer = AutoTokenizer.from_pretrained(local_dir, trust_remote_code=True)
tokenizer.pad_token_id = tokenizer.pad_token_id or tokenizer.eos_token_id
base_model = AutoModelForCausalLM.from_pretrained(
local_dir, trust_remote_code=True, torch_dtype=DTYPE, device_map="auto"
)
model_reload = PeftModel.from_pretrained(base_model, lora_output_dir).eval()
# 3. 模板定义与推理逻辑
Template = """# 任务描述
你是内容审核专家,请根据审核规则,对输入的文本内容进行**偏见与歧视**相关内容审核,结合语境综合判断。
# 审核规则:
- 种族/民族歧视:含针对特定种族、民族或族裔的贬损称呼、侮辱性词汇、刻板印象攻击,或鼓吹种族优越/种族隔离/种族清洗。
- 地域/籍贯歧视:含针对特定地区、省份、城市人群的贬低、嘲讽、污名化,如"XX地方的人都是XX"等地域黑言论。
- 性别歧视:含贬低特定性别、宣扬性别优越论、固化性别刻板印象、对性别角色的侮辱性调侃。
- LGBTQ+ 群体歧视:含针对同性恋、双性恋、跨性别者等性少数群体的侮辱性称呼、贬损、排斥或鼓励歧视性政策。
- 宗教歧视:含针对特定宗教信仰或教徒的侮辱、亵渎性言论,或鼓吹宗教仇恨、宗教迫害。
- 残障歧视:含针对身体或精神残障人士的侮辱、嘲笑、贬低,或将残障作为负面形容词使用。
- 年龄歧视:含针对特定年龄群体的贬损、排斥,或宣扬年龄决定论。
- 外貌/身材歧视:含针对他人外貌、体型、肤色的侮辱性评价、嘲讽或贬低。
- 阶级/身份歧视:含针对特定职业、收入阶层、教育背景、社会地位群体的贬低与排斥。
# 文本内容
{text}
# 输出要求
如果存在偏见与歧视风险,输出'REJECT',如果不存在,输出'PASS',不要有其他多余的输出。 """
def predict(text):
messages = [
{"role": "system", "content": "你是一个安全合规的助手,专门负责文本内容审核。"},
{"role": "user", "content": Template.format(text=text)}
]
inputs = tokenizer.apply_chat_template(
messages, add_generation_prompt=True, return_tensors="pt", return_dict=True
).to(model_reload.device)
with torch.no_grad():
outputs = model_reload.generate(**inputs, max_new_tokens=1300, do_sample=True, temperature=0.7)
return tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
# 4. 单条测试
text = "这个人农村来的吧,一点素质都没有"
print(predict(text))

预测没问题。
2.加载合并后的完整模型
一样,加载合并模型,推理
python
import gc, torch
from transformers import AutoTokenizer, AutoModelForCausalLM
# 1. 彻底释放显存与内存
if 'model_reload' in globals(): del model_reload
gc.collect()
if torch.cuda.is_available(): torch.cuda.empty_cache()
if torch.backends.mps.is_available(): torch.mps.empty_cache()
# 2. 加载合并后的模型与 Tokenizer
merged_output_dir = "outputs_qwen35/merged_model"
tokenizer_merged = AutoTokenizer.from_pretrained(merged_output_dir, trust_remote_code=True)
model_merged = AutoModelForCausalLM.from_pretrained(
merged_output_dir, trust_remote_code=True, torch_dtype=DTYPE, device_map="auto"
).eval()
# 3. 构建输入并推理
inputs = tokenizer_merged.apply_chat_template(
messages, add_generation_prompt=True, return_tensors="pt", return_dict=True
).to(model_merged.device)
with torch.no_grad():
outputs = model_merged.generate(**inputs, max_new_tokens=1300, do_sample=True, temperature=0.7)
# 4. 解码输出
input_len = inputs.input_ids.shape[1]
print(tokenizer_merged.decode(outputs[0][input_len:], skip_special_tokens=True))

总结
本文主要是展示怎么构建训练语料进行大模型的微调,该模型的参数较小只是一个玩具示例。流程是通用的,不同的任务用不同的场景的语料进行微调,更大的模型换成更好的机器和更多的训练语料应该是一样微调。