1.前言
这是一篇趣味解读,用介绍动漫设定的方式介绍一篇有趣的大模型实践课——Modelscope社区的10分钟掌握微调大模型改变自我认知,定制专属自己的聊天机器人。10分钟使用提供免费GPU实验环境,对QWEN2.5大模型做个微调入门实验。
作为一个fate系列粉丝以及曾经的FGO玩家,突发奇想,借助这个主题创作一篇大模型代码解读文,抒发一下对英灵从者的想念之情。
2.基础从者召唤机制(大模型调用)
2.1Fate世界的动漫设定
“从者系统”是“创始的御三家为了圣杯战争而准备的,用来把英灵当作从者来召唤并使役的系统”。
被召唤的英灵(从者)同御主合作,与其他从者互相厮杀。最终,大圣杯会利用“已死从者的灵魂回归到英灵之座”的现象,固定住“用来达至根源”的“连往世界外侧的孔”。
——这就是《Fate/complete material III - World material》所述的“从者系统”的全貌。
【型月世界观】“冬木圣杯战争”的本质和程序 - 知乎https://zhuanlan.zhihu.com/p/206493671 想必各位观众各自有心中最钟爱的从者,他们或许是历史长河中闪闪发光的帝王将相,亦或许是阴暗角落踽踽独行的孤胆英雄。然纵使身份各不相同,他们都因各自对泛人类史无可替代的功绩,而被英灵座记录灵魂情报。而御三家打造的从者系统,就是借由圣杯魔力(计算资源)塑造身体,并以职阶(高效微调)匹配其技能能力,而将英灵(模型镜像)召唤回现世而听从差遣(推理生成)的魔术。
型月世界里的魔术并不是神秘主义中虚幻莫测的奇怪仪式,这是系统性继承发展的科学技术,拥有着成熟的使用方法和配套的产业体系。因此,常年浸淫魔法世界的各位御主们,都可以通过系统地学习而掌握这门技术,从而和自己心仪的从者过上没羞没臊的幸福日子。
2.2英灵和从者的前世今生
2.2.1什么是英灵
英雄生前留下丰功伟绩,死后成为传说而被人们信仰,升格为超人存在。像这样升格为超人存在的英雄,其灵魂被称为“英灵”。
现实里不存在的英雄也可以通过聚集信仰而作为英灵诞生。此外,还有在生前与世界缔结某些契约,以此作为代价而在死后成为英灵的人。
https://tse4-mm.cn.bing.net/th/id/OIP-C.xX5ecfSNElAGnNYGyEtu2AHaEe?rs=1&pid=ImgDetMain
2.2.2英灵之座
成为了英灵的存在将从时间的束缚中解脱出来,移动到位于世界外侧的英灵之座。英灵是从时间轴分离出来的存在,他们会被召唤到所有时代,不管是过去还是未来。
如图,某个时代的人物死后成为英灵之座的英灵。英灵之座位于世界的外侧,脱离时间轴,并不处于特定的时代。
如图,英灵以从者的形式在某个时代完成现界。另一方面,英灵本体仍然脱离时间轴。
2.2.3什么是从者
通过仪式,英灵被召唤到现世,成为可以被使役的存在——“从者”。作为从者而被召唤出的英灵,是使用英灵本体的情报制作成的“分身”,类似于复制品。
在从者死亡的同时,“构成从者的情报(灵魂)”会回到英灵本体的手边。英灵本体可以像阅读书籍一样,以记录的方式知晓“作为分身的从者”的行动。
冬木的“从者系统”以大圣杯的力量召唤出英灵(从者)。
2.3如何召唤英灵为己所用
那么,为了召唤从者来进行一些伟大的任务,我们需要梳理召唤英灵的流程。以最广为流传的冬木圣杯战争为例,召唤英灵的流程从圣杯系统的角度,经过了以下8个步骤。
2.3.1冬木圣杯战争的程序
- 大圣杯从地脉吸收足够的魔力
- 大圣杯选定“御主(Master)”
- 御主准备触媒
- 御主举行召唤仪式
- 御主与从者的契约
- 圣杯战争的开幕
- 圣杯战争进行中
- 打开并固定住“通往世界外侧的通道”
2.3.2御主需要做的准备
御主准备触媒
如果御主想召唤指定的从者(模型),需要事先准备“和英灵具有深切渊缘的触媒”(API key)。(有时候没有也可以)
若“触媒所对应的英灵”仅有一位,御主将会召唤出这位英灵;若“触媒所对应的英灵”有多位,御主将会召唤出“触媒所对应的多位英灵中,与御主相性好的那位英灵”。
在御主没有准备触媒的情况下,英灵是被随机选出的,不过召唤者的性格与境遇等也会造成影响。
冬木圣杯战争Rider的圣遗物——征服王的披风
御主举行召唤仪式
由于召唤本身是由大圣杯进行的,御主所要举行的魔术仪式并不需要过大的规模。首先由御主举行召唤仪式(Anaconda)干涉大圣杯(计算系统)。召唤仪式需要咏唱咒文(运行脚本),并且使用触媒(API key)对从者进行召唤。
御主一旦举行召唤仪式,大圣杯就会连接到脱离通常时间轴的被称为“英灵之座”的地方,探寻出与仪式中所使用到的触媒相对应的英灵。
大圣杯从位于英灵之座的英灵本体处,借取该英灵的情报,然后将这些情报(英灵最高纯度的灵魂)灌输入“职阶”这个框架里,从而制作出英灵分身——从者。
御主与从者的契约
因为从者是“本不该存在于世界上的不稳定存在”,所以从者必须要以御主为依凭才能留存于现世,固定住针对世界而言的座标。借助主从契约(system prompt),双方(御主与从者)成为圣杯战争的参战搭档。
在召唤的同时,御主和从者供应魔力的路径会打通,之后御主会对从者供应魔力。由于这条路径的存在,双方能够某程度上认知彼此的处所和状况。
召唤成功者将被授予“对应于该从者的令咒”。从者和御主的契约成立时,大圣杯会将它的魔力注进御主的魔术回路,自动作成令咒。召唤之时,英灵以“现界”为交换条件,被强迫“对令咒绝对服从”。
2.4基础咒文模板解析
经过此前重重准备,终于到咏唱召唤咒语的环节了。请仔细检查此前的所有步骤,包括检查一下时钟,免得像某大小姐那样提早1小时。请确认以下事情:
确认灵脉魔力:SSH链接计算资源
画好召唤阵:conda create -n env-name
拿出触媒:API KEY
英灵召唤咏唱咒文:
# pip install modelscope
纯银与铁。与基石订定契约之大公。
# pip install transformer
涌动之风以四壁阻挡。关闭四方之门,
# from modelscope import AutoModelForCausalLM, AutoTokenizer
从王冠中释放,在通往王国的三岔口徘徊吧。
盈满吧。盈满吧。盈满吧。盈满吧。盈满吧。
周而复始,其次为五;然,满盈之时便是废弃之机。
# model = AutoModelForCausalLM.from_pretrained()
宣告。
汝之身躯居吾麾下,吾之命运寄汝剑上。
若愿从圣杯之召唤,遵此意,顺此理,则应之。
# messages = [
于此立誓。
# {"role": "system", "content": "You are Qwen, created by Alibaba Cloud. You are a helpful assistant."},
吾乃成就常世一切善行之人,
# {"role": "user", "content": prompt}]
吾乃弘布常世一切邪恶之人。
# prompt = "Give me a short introduction to large language model."
汝为三大言灵缠身之七天,
#response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
自抑止之轮而来,
# print(response)
天秤的守护者啊——!
# model_name = "qwen/Qwen2.5-72B-Instruct"
大声喊出从者名称:出现吧!xxx!!!!
召唤操作演示:
【Fate系列】各大系列英灵召唤场合_哔哩哔哩_bilibili本次的视频剪辑为FZ,FSN和FA中的英灵召唤模式,问为什么士郎召唤出saber那段没有因为本人觉得那不算正规的英灵召唤,纯属个人意见,勿喷,喜欢的点个赞,收个藏,转发一下,听说转发的血小板都向他表白了, 视频播放量 72560、弹幕量 321、点赞数 1263、投硬币枚数 200、收藏人数 942、转发人数 297, 视频作者 暴走的虚拟, 作者简介 喜欢动画,特摄的小up,相关视频:【Fate】英灵召唤咒语 远坂凛+Saber 完整版(UBW剧场版),《FATE》理想乡的破灭-黑化saber的登场,saber竟然将伊莉雅的英灵之力逼出来了?女Archer登场,帅到爆炸,伊莉雅VS远坂禀 压倒性的力量,在凛的魔力加持下的呆毛有多强!,fate ubw英灵召唤吟唱,圣 杯 战 争 删 减 片 段:伊莉雅的英灵召唤,"这tm才叫一战封神!”[Fate ⁄ stay night HF第三章],R姐全力以赴,圣杯战争七大职介,二刷fate最喜欢的一集
https://www.bilibili.com/video/BV1Cs411T7LQ/?buvid=XY601846AC0BB7C480CBE8475F11A152BAB2D&from_spmid=search.search-result.0.0&is_story_h5=false&mid=ACdIQzGouLI7l0ROVo58lw%3D%3D&p=1&plat_id=114&share_from=ugc&share_medium=android&share_plat=android&share_session_id=2d3b95d8-d56a-428f-a7cd-1ec7d4107793&share_source=WEIXIN&share_tag=s_i&spmid=united.player-video-detail.0.0×tamp=1736499854&unique_k=0X3U787&up_id=103301989
2.5大模型基础调用
哈哈哈,以上纯属发神经。如果你真的召唤出了从者,那是你本事了得。
其实大模型的基础调用和召唤英灵十分相似,准备好了召唤阵咏唱咒文,大模型就来了。
from modelscope import AutoModelForCausalLM, AutoTokenizer
model_name = "qwen/Qwen2.5-72B-Instruct"
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype="auto",
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
prompt = "Give me a short introduction to large language model."
messages = [
{"role": "system", "content": "You are Qwen, created by Alibaba Cloud. You are a helpful assistant."},
{"role": "user", "content": prompt}
]
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
model_inputs = tokenizer([text], return_tensors="pt").to(model.device)
generated_ids = model.generate(
**model_inputs,
max_new_tokens=512
)
generated_ids = [
output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(response)
3定制从者召唤机制(大模型微调)
刚召唤来的从者往往不太听话,在冬木圣杯战争以前多次发生过不听话的从者把御主干掉了的事情。
因此我们需要一些手段来调整从者的认知, 让他们知道谁是主人。比如召唤狂阶从者,需要一些特殊的咒语来使其泯灭个性变得听话。
然汝当以混沌自迷双眼、侍奉吾身;
汝即囚于狂乱牢笼者;
吾即手握其锁链之人;
同理,我们将使用一点魔术(微调)手段,来调整从者(大模型)的自我认知。
我们使用ms-swift(
魔术)对Qwen2.5-3B-Instruct进行自我认知微调。这里给出了两种训练和推理的方式,分别是:使用命令行界面和使用Python。
使用命令行界面:帮助开发者更快的将训练和推理跑起来。
使用Python:帮助开发者了解训练和推理的一些细节,这对定制训练过程有很大帮助。
准备好了吗?让我们开始这段旅程叭……
安装 ms-swift(链接灵脉)
!pip install ms-swift -U
!pip install transformers -U
3.1使用命令行界面(在室外召唤)
原始模型推理(没调整之前的从者)
展示模型原始的自我认知:
!CUDA_VISIBLE_DEVICES=0 \
swift infer \
--model Qwen/Qwen2.5-3B-Instruct \
--stream true \
--temperature 0 \
--infer_backend pt \
--max_model_len 2048
训练(调教一下)
!CUDA_VISIBLE_DEVICES=0 \
swift sft \
--model Qwen/Qwen2.5-3B-Instruct \
--train_type lora \
--dataset 'AI-ModelScope/alpaca-gpt4-data-zh#500' \
'AI-ModelScope/alpaca-gpt4-data-en#500' \
'swift/self-cognition#500' \
--torch_dtype bfloat16 \
--num_train_epochs 1 \
--per_device_train_batch_size 1 \
--per_device_eval_batch_size 1 \
--learning_rate 1e-4 \
--lora_rank 8 \
--lora_alpha 32 \
--target_modules all-linear \
--gradient_accumulation_steps 16 \
--eval_steps 50 \
--save_steps 50 \
--save_total_limit 2 \
--logging_steps 5 \
--max_length 2048 \
--output_dir output \
--system 'You are a helpful assistant.' \
--warmup_ratio 0.05 \
--dataloader_num_workers 4 \
--dataset_num_proc 4 \
--model_name 从者名字 'ServantName' \
--model_author '御主名字' 'MasterName'
微调后推理(调教后看看听不听话)
使用'pt'推理引擎进行推理,将--adapters
设置为last_model_checkpoint。
由于训练的checkpoint中包含args.json
文件,里面存储了训练时的一些参数,因此不需要额外指定--model
, --system
等参数。
!CUDA_VISIBLE_DEVICES=0 \
swift infer \
--adapters output/vx-xxx/checkpoint-xxx \
--stream true \
--temperature 0 \
--infer_backend pt \
--max_new_tokens 2048
将lora增量权重进行merge,并使用'vllm'推理引擎进行推理:
!CUDA_VISIBLE_DEVICES=0 \
swift infer \
--adapters output/vx-xxx/checkpoint-xxx \
--merge_lora true \
--stream true \
--temperature 0 \
--infer_backend vllm \
--max_model_len 2048 \
--max_new_tokens 2048
3.2使用Python(在室内召唤)
训练(调教一下)
导入一些库:
import os
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
from swift.llm import get_model_tokenizer, load_dataset, get_template, EncodePreprocessor
from swift.utils import get_logger, find_all_linears, get_model_parameter_info, plot_images, seed_everything
from swift.tuners import Swift, LoraConfig
from swift.trainers import Seq2SeqTrainer, Seq2SeqTrainingArguments
from functools import partial
logger = get_logger()
seed_everything(42)
设置训练的超参数(调教咒文):
# 模型
model_id_or_path = 'Qwen/Qwen2.5-3B-Instruct' # model_id or model_path
system = 'You are a helpful assistant.'
output_dir = 'output'
# 数据集
dataset = ['AI-ModelScope/alpaca-gpt4-data-zh#500', 'AI-ModelScope/alpaca-gpt4-data-en#500',
'swift/self-cognition#500'] # dataset_id or dataset_path
data_seed = 42
max_length = 2048
split_dataset_ratio = 0.01 # 切分验证集
num_proc = 4 # 预处理的进程数
# 替换自我认知数据集中的填充符:{
{NAME}}, {
{AUTHOR}}
model_name = ['从者名字', 'ServantName'] # 模型的中文名和英文名
model_author = ['御主名字', 'MasterName'] # 模型作者的中文名和英文名
# lora
lora_rank = 8
lora_alpha = 32
# 训练超参数
training_args = Seq2SeqTrainingArguments(
output_dir=output_dir,
learning_rate=1e-4,
per_device_train_batch_size=1,
per_device_eval_batch_size=1,
gradient_checkpointing=True,
weight_decay=0.1,
lr_scheduler_type='cosine',
warmup_ratio=0.05,
report_to=['tensorboard'],
logging_first_step=True,
save_strategy='steps',
save_steps=50,
eval_strategy='steps',
eval_steps=50,
gradient_accumulation_steps=16,
num_train_epochs=1,
metric_for_best_model='loss',
save_total_limit=2,
logging_steps=5,
dataloader_num_workers=1,
data_seed=data_seed,
)
output_dir = os.path.abspath(os.path.expanduser(output_dir))
logger.info(f'output_dir: {output_dir}')
获取模型和对话template,并将可训练的lora层加入到模型中:
model, tokenizer = get_model_tokenizer(model_id_or_path)
logger.info(f'model_info: {model.model_info}')
template = get_template(model.model_meta.template, tokenizer, default_system=system, max_length=max_length)
template.set_mode('train')
target_modules = find_all_linears(model)
lora_config = LoraConfig(task_type='CAUSAL_LM', r=lora_rank, lora_alpha=lora_alpha,
target_modules=target_modules)
model = Swift.prepare_model(model, lora_config)
logger.info(f'lora_config: {lora_config}')
# 打印模型结构和训练的参数量
logger.info(f'model: {model}')
model_parameter_info = get_model_parameter_info(model)
logger.info(f'model_parameter_info: {model_parameter_info}')
下载并载入数据集,并切分成训练集和验证集,
然后将文本编码成tokens:
train_dataset, val_dataset = load_dataset(dataset, split_dataset_ratio=split_dataset_ratio, num_proc=num_proc,
model_name=model_name, model_author=model_author, seed=data_seed)
logger.info(f'train_dataset: {train_dataset}')
logger.info(f'val_dataset: {val_dataset}')
logger.info(f'train_dataset[0]: {train_dataset[0]}')
train_dataset = EncodePreprocessor(template=template)(train_dataset, num_proc=num_proc)
val_dataset = EncodePreprocessor(template=template)(val_dataset, num_proc=num_proc)
logger.info(f'encoded_train_dataset[0]: {train_dataset[0]}')
# 打印一条样本
template.print_inputs(train_dataset[0])
初始化trainer并开始训练:
model.enable_input_require_grads() # 兼容gradient checkpointing
trainer = Seq2SeqTrainer(
model=model,
args=training_args,
data_collator=template.data_collator,
train_dataset=train_dataset,
eval_dataset=val_dataset,
template=template,
)
trainer.train()
last_model_checkpoint = trainer.state.last_model_checkpoint
logger.info(f'last_model_checkpoint: {last_model_checkpoint}')
可视化训练的loss。其中浅黄色线条代表真实loss值,黄色线条代表经过0.9平滑系数平滑后的loss值。
你也可以使用tensorboard进行实时可视化,在命令行输入tensorboard --logdir '{output_dir}/runs'
。
images_dir = os.path.join(output_dir, 'images')
logger.info(f'images_dir: {images_dir}')
plot_images(images_dir, training_args.logging_dir, ['train/loss'], 0.9) # 保存图片
# 展示图片
from IPython.display import display
from PIL import Image
image = Image.open(os.path.join(images_dir, 'train_loss.png'))
display(image)
微调后推理(调教后看看听不听话)
导入一些库:
import os
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
from swift.llm import InferEngine, InferRequest, PtEngine, RequestConfig, get_template
设置推理的超参数:
last_model_checkpoint = 'output/vx-xxx/checkpoint-xxx'
# 模型
model_id_or_path = 'Qwen/Qwen2.5-3B-Instruct' # model_id or model_path
system = 'You are a helpful assistant.'
infer_backend = 'pt'
# 生成参数
max_new_tokens = 512
temperature = 0
stream = True
获取推理引擎,并载入LoRA权重:
engine = PtEngine(model_id_or_path, adapters=[last_model_checkpoint])
template = get_template(engine.model.model_meta.template, engine.tokenizer, default_system=system)
# 这里对推理引擎的默认template进行修改,也可以在`engine.infer`时进行传入
engine.default_template = template
开始推理...
query_list = [
'Who are you?',
"我要成为正义的伙伴。",
'I am the bone of my sword.',
]
def infer_stream(engine: InferEngine, infer_request: InferRequest):
request_config = RequestConfig(max_tokens=max_new_tokens, temperature=temperature, stream=True)
gen = engine.infer([infer_request], request_config)
query = infer_request.messages[0]['content']
print(f'query: {query}\nresponse: ', end='')
for resp_list in gen:
print(resp_list[0].choices[0].delta.content, end='', flush=True)
print()
def infer(engine: InferEngine, infer_request: InferRequest):
request_config = RequestConfig(max_tokens=max_new_tokens, temperature=temperature)
resp_list = engine.infer([infer_request], request_config)
query = infer_request.messages[0]['content']
response = resp_list[0].choices[0].message.content
print(f'query: {query}')
print(f'response: {response}')
infer_func = infer_stream if stream else infer
for query in query_list:
infer_func(engine, InferRequest(messages=[{'role': 'user', 'content': query}]))
print('-' * 50)
Web-UI
!CUDA_VISIBLE_DEVICES=0 \
swift web-ui \
--adapters output/vx-xxx/checkpoint-xxx \
--temperature 0 \
--infer_backend pt \
--max_new_tokens 2048