新手也能懂!从0到1搞定大模型高效微调实战(附完整代码+踩坑记录)
新手也能懂!从0到1搞定大模型高效微调实战(附完整代码+踩坑记录)
大家好,我是刚学 AI 不久的新手博主,这段时间自己啃大模型微调的项目,踩了一堆坑,终于跑通了一整套实战!想着不能我一个人踩坑,就整理了这篇超详细的保姆级教程,带着咱们粉丝群的小伙伴一起学,从项目背景到代码实现,从本地跑通到云平台算力使用,全程大白话,不用怕看不懂,咱们一步一步来,做完你也能自己做大模型项目!
前言:为什么我们要学这个?
兄弟们,刚学 AI 的时候,是不是跟我一样,有一堆头疼的问题?
-
做项目要标注数据,标注一条数据要好几块,攒一万条数据要花好几万,我一个新手哪来这么多钱?
-
大模型动不动就几十亿参数,我自己的电脑只有 3060/1060 的显卡,根本跑不动,全量微调直接显存爆炸!
-
网上的教程要么太官方,全是专业术语,要么太浅,跑两步就报错,根本没人带我踩坑!
别慌!今天这篇文章,我们就来解决这些问题!
我们要做的这个项目,就是用小样本 + 高效微调的技术,用63 条标注数据,就能搞定 10 分类的电商评论分类,甚至还能搞定 ChatGLM 大模型的多任务微调,而且普通的 3060 显卡就能跑,不用买几万块的 A100,新手也能玩得起!
这篇文章我会带着大家,从最基础的原理,到代码逐行敲,到最后跑通整个项目,甚至教你怎么用云平台的便宜算力,就算你没有好显卡也能玩!全程没有难懂的术语,就算你刚学 PyTorch 不到一个月,也能跟着跑通!
第一部分:项目整体背景:我们到底要做什么?
1.1 我们的项目要解决什么问题?
首先,我们先搞清楚,我们做这个项目,是为了解决什么实际的问题?
现在电商平台,每天都有几百万条用户评论,比如你买了苹果,评论 “苹果很甜,很好吃”,买了酒店,评论 “酒店服务很好,下次还来”,这些评论,平台要自动分类,是水果的评论?还是酒店的?还是手机的?
分类完了之后,平台就能给你做推荐,比如你经常看酒店的评论,就给你推酒店;你经常看水果的评论,就给你推水果,这就是个性化推荐的基础。
但是问题来了:
-
传统的深度学习方法,比如 TextCNN、BERT 全量微调,都需要几万条标注数据才能训好,标注这些数据要花好多钱,好多时间!
-
而且大模型全量微调,要改所有的参数,显卡根本扛不住!
那有没有办法,只用几十条标注数据,就能训出一个准确率很高的模型?而且普通显卡就能跑?
有!这就是我们今天要学的:小样本学习 + 高效微调!
1.2 传统方法的痛点,我们怎么解决?
我给大家算笔账:
-
传统全量微调:要 1 万条标注数据,标注成本 1 万条 * 2 元 = 2 万元,训练要 13G 显存,要 A100 的显卡,训练一天要几百块。
-
我们的小样本高效微调:只要 63 条标注数据,标注成本 63 条 * 2 元 = 126 元!训练只要 7G 显存,你的 3060 显卡就能跑,训练几个小时就完事了!
这差距是不是太大了?
而且我们的方法,准确率一点都不低!63 条数据,就能做到 90%+ 的 F1 值,跟几万条数据训出来的差不多!
这就是为什么现在小样本和高效微调这么火,对新手太友好了,不用攒数据,不用好显卡,就能做项目!
1.3 我们今天要学的三个核心技术
今天我们会从易到难,学三个最常用的高效微调技术,一个比一个强:
-
BERT+PET(硬模板 Prompt):最基础的,把分类任务变成填空题,人工写模板,新手最容易懂!
-
BERT+Prompt-Tuning(软模板 Prompt):不用人工写模板了,让模型自己学模板,效果更好!
-
ChatGLM+LoRA(大模型低秩微调):搞定 6B 大模型的多任务微调,一个模型同时做信息抽取 + 文本分类,普通显卡就能跑!
最后我还会教你,如果你没有好显卡,怎么用趋动云的云算力,花几块钱就能用 A800 的显卡跑大模型,全程保姆级步骤!
第二部分:入门第一个方法:BERT+PET,把分类变成填空题!
2.1 PET 到底是什么?大白话讲透!
很多小伙伴一看到 PET 就懵了,这啥啊?**PatternExploiting Training?**听不懂!
别慌,我用大白话给你讲:PET 就是把分类任务,变成 BERT 本来就会的完形填空!
啥意思?
原来的分类任务是啥?
输入:“包装不错,苹果挺甜的,个头也大。”
输出:水果
原来的传统方法,是在 BERT 的最后加一个分类头,让 BERT 学,输入这句话,输出 “水果” 这个分类。
但是 BERT 预训练的时候,根本没学过这个啊!它预训练的时候学的是完形填空,就是给你一个句子,中间挖个空,让你填进去。
那我们能不能,把分类任务,变成完形填空?
当然可以!我们把输入改成:
这是一条 \\__评论:包装不错,苹果挺甜的,个头也大。
然后让 BERT 去填这个空!
BERT 一看,哦!这我熟啊!我预训练的时候天天做这个!那它就会填 “水果”!
哦!原来如此!这不就把分类任务,变成了 BERT 最擅长的完形填空了吗?
那这样的话,我们根本不用给它喂几万条数据,只要几十条,它就学会了,因为它本来就会填空,我们只是告诉它,填空的时候,填的词对应我们的分类标签而已!
这就是 PET 的核心思想!是不是超级简单?我第一次看懂的时候,直呼太聪明了!
2.2 我们的数据集:63 条数据就够了!
很多小伙伴问,这么神奇吗?那我们的数据集有多大?
我给大家看一下,我们的训练数据集,只有 63 条!没错,就是 63 条!
长这样:
水果 脆脆的,甜味可以,可能时间有点长了,水分不是很足。
平板 华为机器肯定不错,但第一次碰上京东最糟糕的服务,以后不想到京东购物了。
书籍 为什么不认真的检查一下, 发这么一本脏脏的书给顾客呢!
衣服 手感不错,用料也很好,不知道水洗后怎样,相信大品牌,质量过关,五星好评!!!
水果 苹果有点小,不过好吃,还有几个烂的。估计是故意的放的。差评。
衣服 掉色掉的厉害,洗一次就花了
...
每一行,前面是标签,后面是用户的评论,总共就 63 条,你自己手动标,半天就标完了,根本不用花钱找标注公司!
然后我们的验证集有 590 条,用来测试我们的模型准不准,就这么点数据,就够了!
然后我们还有两个文件:
-
prompt\.txt:就是我们的人工模板,就一句话:这是一条\{MASK\}评论:\{textA\}
看到没?就是我们刚才说的,把评论填到\{textA\}的位置,把\{MASK\}的位置留给 BERT 去填! -
verbalizer\.txt:就是标签和填空的词的映射,比如:
电脑 电脑
水果 水果
平板 平板
衣服 衣服
酒店 酒店
洗浴 洗浴
书籍 书籍
蒙牛 蒙牛
手机 手机
电器 电器
意思就是,如果BERT填的空是“水果”,那分类就是水果;填的是“酒店”,分类就是酒店,就这么简单!
你看,是不是完全不用搞复杂的东西?就这几个文件,我们的数据集就搞定了!
2.3 数据处理代码:逐行带你敲,新手也能懂!
好了,原理懂了,我们来看代码,我带着你一行一行敲,每一行都给你讲清楚,什么意思,不用怕看不懂!
首先,我们要处理模板,就是把我们的prompt,比如这是一条{MASK}评论:{textA},给拆解开,把评论填进去,对吧?
我们先写一个HardTemplate类,专门干这个事:
from transformers import AutoTokenizer
import numpy as np
# 我们的配置类,后面会讲
from prompt_tasks.PET.pet_config import ProjectConfig
# 硬模板类,就是我们人工写的模板
class HardTemplate(object):
def __init__(self, prompt: str):
'''
初始化函数,输入我们的prompt模板
比如:"这是一条{MASK}评论:{textA}"
'''
# 先把原始的prompt存下来
self.prompt = prompt
# 这个列表用来存我们拆解后的模板部分
self.inputs_list = []
# 这个集合用来存我们的自定义字段,比如MASK、textA
self.custom_tokens = {'MASK'}
# 初始化的时候,就把prompt拆解了
self.prompt_analysis()
哦,这里有个prompt\_analysis函数,就是把我们的 prompt 字符串,拆成一个个的部分,比如把这是一条\{MASK\}评论:\{textA\},拆成\[\&\#39;这\&\#39;,\&\#39;是\&\#39;,\&\#39;一\&\#39;,\&\#39;条\&\#39;,\&\#39;MASK\&\#39;,\&\#39;评\&\#39;,\&\#39;论\&\#39;,\&\#39;:\&\#39;,\&\#39;textA\&\#39;\],对吧?这样我们后面就能把对应的内容填进去了。
我们来看这个函数的代码,逐行讲:
def prompt_analysis(self):
'''
把prompt模板拆解成我们要的格式
比如输入:"这是一条{MASK}评论:{textA}"
输出:inputs_list就是拆好的部分,custom_tokens就是我们的自定义字段
'''
# 我们从第一个字符开始,遍历整个prompt
idx = 0
while idx < len(self.prompt):
str_part = ''
# 如果当前字符不是{或者},那就是普通的字,直接加到列表里
if self.prompt[idx] not in ['{', '}']:
self.inputs_list.append(self.prompt[idx])
# 如果遇到了{,说明我们遇到自定义字段了!
if self.prompt[idx] == '{':
idx += 1
# 一直读到}为止,把中间的字段名拿出来
while self.prompt[idx] != '}':
str_part += self.prompt[idx]
idx += 1
# 如果遇到单独的},说明你模板写错了,括号不匹配,报错!
elif self.prompt[idx] == '}':
raise ValueError("遇到了单独的 '}', 请检查输入的prompt。")
# 如果我们拿到了字段名,就把它加到列表里,还有自定义字段的集合里
if str_part:
self.inputs_list.append(str_part)
self.custom_tokens.add(str_part)
# 索引往后走
idx += 1
兄弟们,这个函数是不是超级简单?我第一次写的时候,还踩了个坑,就是括号不匹配,我少写了一个},结果报错了,后来找了半小时才找到,你们写的时候要注意哦!
那拆解完了模板,我们怎么把评论填进去呢?比如我们的评论是包装不错,苹果挺甜的,我们要把它填到textA的位置,把\[MASK\]填到MASK的位置,对吧?
我们来看\_\_call\_\_函数,这个函数就是我们处理单个样本的函数:
def __call__(self,
inputs_dict: dict,
tokenizer,
mask_length,
max_seq_len=512):
'''
输入一个样本,把它转换成模型能接受的格式
inputs_dict:就是我们要填的字段,比如{'textA': '评论内容', 'MASK': '[MASK]'}
tokenizer:BERT的分词器
mask_length:MASK的长度,因为我们的标签是两个字,所以要两个MASK
max_seq_len:句子的最大长度,默认512
'''
# 初始化输出的字典
outputs = {
'text': '', # 原始的文本
'input_ids': [], # token的id,模型的输入
'token_type_ids': [], # 句子的类型id,BERT用的
'attention_mask': [], # 注意力掩码,告诉模型哪些是有效token
'mask_position': [] # MASK的位置,我们要知道哪里是要预测的地方
}
# 第一步:把所有的字段填进去,拼成完整的句子!
str_formated = ''
# 遍历我们拆好的inputs_list,一个一个来
for value in self.inputs_list:
# 如果是自定义字段,就填对应的内容
if value in self.custom_tokens:
# 如果是MASK,就填[MASK],有几个就填几个
if value == 'MASK':
str_formated += inputs_dict[value] * mask_length
else:
# 其他的,就填评论内容
str_formated += inputs_dict[value]
else:
# 普通的字,直接加进去
str_formated += value
哦!这一步太关键了!填完之后,我们的句子就变成了:这是一条\[MASK\]\[MASK\]评论:包装不错,苹果挺甜的,个头也大。
看到没?完美!就是我们要的完形填空的句子!
然后,我们用 BERT 的分词器,把这个句子转换成模型能接受的 id:
# 用tokenizer把句子编码成id
encoded = tokenizer(text=str_formated,
truncation=True,
max_length=max_seq_len,
padding='max_length')
# 把编码后的结果存到输出里
outputs['input_ids'] = encoded['input_ids']
outputs['token_type_ids'] = encoded['token_type_ids']
outputs['attention_mask'] = encoded['attention_mask']
outputs['text'] = ''.join(tokenizer.convert_ids_to_tokens(encoded['input_ids']))
然后,我们还要找到,我们的\[MASK\]在句子的哪个位置,因为我们要让模型预测这个位置的词,对吧?
# 找到MASK的位置!
mask_token_id = tokenizer.convert_tokens_to_ids('[MASK]')
# 找input_ids里,哪个是MASK的id,那个位置就是我们要的
mask_position = np.where(np.array(outputs['input_ids']) == mask_token_id)[0].tolist()
outputs['mask_position'] = mask_position
return outputs
搞定!这个模板类就写完了!是不是超级简单?我们来测试一下:
if __name__ == '__main__':
pc = ProjectConfig()
tokenizer = AutoTokenizer.from_pretrained(pc.pre_model)
# 定义我们的模板
hard_template = HardTemplate(prompt='这是一条{MASK}评论:{textA}')
# 测试一个样本
tep = hard_template(
inputs_dict={'textA': '包装不错,苹果挺甜的,个头也大。', 'MASK': '[MASK]'},
tokenizer=tokenizer,
mask_length=2,
max_seq_len=30
)
print(tep)
运行一下,你就会看到,输出的结果里,已经把我们的句子处理好了,MASK 的位置也找到了,完美!
踩坑提醒:很多小伙伴这里会遇到 MASK 的位置不对,那是因为你 tokenizer 的版本不对,或者你用了不对的模型,BERT 的
\[MASK\]的 id 是 103,别搞错了!
好了,模板类写完了,我们接下来写数据预处理的函数,把我们的数据集,批量处理成模型能接受的格式:
import torch
import numpy as np
from datasets import load_dataset
from transformers import AutoTokenizer
# 导入我们刚才写的HardTemplate
from prompt_tasks.PET.data_handle.template import HardTemplate
from prompt_tasks.PET.pet_config import ProjectConfig
def convert_example(
examples: dict,
tokenizer,
max_seq_len: int,
max_label_len: int,
hard_template,
train_mode=True,
return_tensor=False
) -> dict:
'''
批量处理样本,把整个数据集的样本,都转换成模型能接受的格式
'''
tokenized_output = {
'input_ids': [],
'token_type_ids': [],
'attention_mask': [],
'mask_positions': [], # 每个样本的MASK位置
'mask_labels': [] # 每个样本的真实标签
}
# 遍历每个样本
for i, example in enumerate(examples['text']):
try:
# 把样本拆成标签和评论,用\t分割
label, content = example.strip().split('\t', 1)
# 把标签转成token id,因为我们的标签是两个字,所以要两个token
mask_labels = tokenizer(text=label)
mask_labels = mask_labels['input_ids'][1:-1] # 去掉CLS和SEP
# 补到最大长度,不够的用PAD
mask_labels += [tokenizer.pad_token_id] * (max_label_len - len(mask_labels))
tokenized_output['mask_labels'].append(mask_labels)
except:
continue
# 用我们的模板,把评论处理成输入
encoded_inputs = hard_template(
inputs_dict={'textA': content, 'MASK': '[MASK]'},
tokenizer=tokenizer,
max_seq_len=max_seq_len,
mask_length=max_label_len
)
# 把结果存起来
tokenized_output['input_ids'].append(encoded_inputs["input_ids"])
tokenized_output['token_type_ids'].append(encoded_inputs["token_type_ids"])
tokenized_output['attention_mask'].append(encoded_inputs["attention_mask"])
tokenized_output['mask_positions'].append(encoded_inputs["mask_position"])
# 转成numpy或者tensor
for k, v in tokenized_output.items():
if return_tensor:
tokenized_output[k] = torch.LongTensor(v)
else:
tokenized_output[k] = np.array(v)
return tokenized_output
哦,这个函数也很简单,就是把我们的每个样本,都用刚才的模板处理一遍,然后把标签也处理好,存起来。
然后,我们写数据加载器,把处理好的数据,加载成 PyTorch 的 DataLoader,方便训练:
from functools import partial
from datasets import load_dataset
from torch.utils.data import DataLoader
from transformers import AutoTokenizer, default_data_collator
# 导入我们的函数
from prompt_tasks.PET.data_handle.data_preprocess import convert_example
from prompt_tasks.PET.data_handle.template import HardTemplate
from prompt_tasks.PET.pet_config import ProjectConfig
# 初始化配置
pc = ProjectConfig()
# 加载分词器
tokenizer = AutoTokenizer.from_pretrained(pc.pre_model)
def get_data():
# 读取我们的prompt模板
prompt = open(pc.prompt_file, 'r', encoding='utf-8').readlines()[0].strip()
# 创建模板对象
hard_template = HardTemplate(prompt=prompt)
# 把处理函数的参数先固定好,方便后面用
new_func = partial(convert_example,
tokenizer=tokenizer,
hard_template=hard_template,
max_seq_len=pc.max_seq_len,
max_label_len=pc.max_label_len
)
# 加载数据集
dataset = load_dataset('text',
data_files={'train': pc.train_path, 'dev': pc.dev_path})
# 批量处理数据集
dataset = dataset.map(new_func, batched=True)
# 拿出训练和验证集
train_dataset = dataset["train"]
dev_dataset = dataset["dev"]
# 转成DataLoader
train_dataloader = DataLoader(train_dataset,
shuffle=True,
collate_fn=default_data_collator,
batch_size=pc.batch_size)
dev_dataloader = DataLoader(dev_dataset,
collate_fn=default_data_collator,
batch_size=pc.batch_size)
return train_dataloader, dev_dataloader
搞定!数据部分就全部写完了!是不是很简单?我们来测试一下,运行这个 get_data 函数,你就会看到,数据已经加载好了,每个 batch 的格式都是模型能接受的!
踩坑提醒:很多小伙伴这里会遇到 load_dataset 报错,那是因为你的数据路径写错了!一定要检查你的 train_path 和 dev_path 对不对,我第一次写的时候,把路径写错了,找了半小时才发现!
2.4 模型训练代码:逐行带你跑,看完就会!
数据搞定了,我们来看训练的代码,同样,逐行给你讲清楚!
首先,我们要写一个损失函数,计算我们的 MLM 损失,因为我们的任务是预测 MASK 位置的词,对吧?
import torch
def mlm_loss(logits, mask_positions, sub_labels, cross_entropy_criterion, device):
'''
计算MASK位置的损失,就是我们的分类损失
logits:模型的输出,(batch, seq_len, vocab_size)
mask_positions:MASK的位置
sub_labels:真实的标签
'''
loss = None
batch_size = logits.size(0)
# 遍历每个样本
for i in range(batch_size):
single_mask_logits = logits[i] # 单个样本的输出
single_mask_pos = mask_positions[i] # 单个样本的MASK位置
single_mask_logits = single_mask_logits[single_mask_pos] # 拿出MASK位置的输出
single_sub_mask_labels = sub_labels[i] # 真实的标签
# 计算交叉熵损失
cur_loss = cross_entropy_criterion(single_mask_logits, torch.LongTensor(single_sub_mask_labels).to(device))
# 累加到总的损失里
if not loss:
loss = cur_loss
else:
loss += cur_loss
# 平均一下
loss = loss / batch_size
return loss
这个损失函数也很简单,就是把 MASK 位置的预测结果,和真实的标签,算交叉熵损失,跟我们普通的分类损失一样!
然后,我们还要写一个评估的类,用来计算我们的准确率、精确率、召回率、F1 值,这些指标:
from typing import List
import numpy as np
from sklearn.metrics import accuracy_score, precision_score, f1_score, recall_score, confusion_matrix
class ClassEvaluator(object):
def __init__(self):
# 存真实的标签和预测的标签
self.goldens = []
self.predictions = []
def add_batch(self, pred_batch: List, gold_batch: List):
# 把每个batch的预测和真实结果存起来
assert len(pred_batch) == len(gold_batch)
# 如果是多token的标签,拼起来
if type(gold_batch[0]) in [list, tuple]:
pred_batch = [''.join([str(e) for e in ele]) for ele in pred_batch]
gold_batch = [''.join([str(e) for e in ele]) for ele in gold_batch]
self.goldens.extend(gold_batch)
self.predictions.extend(pred_batch)
def compute(self, round_num=2) -> dict:
# 计算所有的指标
classes = sorted(list(set(self.goldens) | set(self.predictions)))
class_metrics = {}
res = {}
# 全局的指标
res['accuracy'] = round(accuracy_score(self.goldens, self.predictions), round_num)
res['precision'] = round(precision_score(self.goldens, self.predictions, average='weighted'), round_num)
res['recall'] = round(recall_score(self.goldens, self.predictions, average='weighted'), round_num)
res['f1'] = round(f1_score(self.goldens, self.predictions, average='weighted'), round_num)
# 每个类别的指标
try:
conf_matrix = np.array(confusion_matrix(self.goldens, self.predictions))
assert conf_matrix.shape[0] == len(classes)
for i in range(conf_matrix.shape[0]):
precision = 0 if sum(conf_matrix[:, i]) == 0 else (conf_matrix[i, i] / sum(conf_matrix[:, i]))
recall = 0 if sum(conf_matrix[i, :]) == 0 else (conf_matrix[i, i] / sum(conf_matrix[i, :]))
f1 = 0 if (precision + recall) == 0 else (2 * precision * recall / (precision + recall))
class_metrics[classes[i]] = {
'precision': round(precision, round_num),
'recall': round(recall, round_num),
'f1': round(f1, round_num)
}
res['class_metrics'] = class_metrics
except Exception as e:
print(f'[Warning] 计算类别指标出错: {e}')
res['class_metrics'] = {}
return res
def reset(self):
# 重置,方便下一次评估
self.goldens = []
self.predictions = []
这个评估类也很简单,就是把我们的预测结果和真实结果,算一下各个指标,训练完了我们就能看到,我们的模型准不准。
然后,我们的主训练函数,就来了:
import os
import time
import torch
from transformers import AutoModelForMaskedLM, AutoTokenizer, get_scheduler
from tqdm import tqdm
# 导入我们刚才写的所有函数
from prompt_tasks.PET.data_loader import get_data
from prompt_tasks.PET.verbalizer import Verbalizer
from prompt_tasks.PET.pet_config import ProjectConfig
from prompt_tasks.PET.utils.metric_utils import ClassEvaluator
pc = ProjectConfig()
def model2train():
# 1. 加载数据
train_dataloader, dev_dataloader = get_data()
# 2. 加载模型,BERT+MLM头,因为我们要做完形填空
model = AutoModelForMaskedLM.from_pretrained(pc.pre_model).to(pc.device)
# 3. 加载分词器和verbalizer
tokenizer = AutoTokenizer.from_pretrained(pc.pre_model)
verbalizer = Verbalizer(verbalizer_file=pc.verbalizer, tokenizer=tokenizer, max_label_len=pc.max_label_len)
# 4. 优化器,我们用AdamW,这个是现在最常用的优化器
no_decay = ["bias", "LayerNorm.weight"]
optimizer_grouped_parameters = [
{
"params": [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)],
"weight_decay": pc.weight_decay,
},
{
"params": [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)],
"weight_decay": 0.0,
},
]
optimizer = torch.optim.AdamW(optimizer_grouped_parameters, lr=pc.learning_rate)
# 5. 学习率调度器,学习率先升后降,帮助模型更快收敛
num_update_steps_per_epoch = len(train_dataloader)
max_train_steps = pc.epochs * num_update_steps_per_epoch
warm_steps = int(pc.warmup_ratio * max_train_steps)
lr_scheduler = get_scheduler(
name='linear',
optimizer=optimizer,
num_warmup_steps=warm_steps,
num_training_steps=max_train_steps
)
# 6. 初始化训练的参数
loss_list = []
tic_train = time.time()
metric = ClassEvaluator()
criterion = torch.nn.CrossEntropyLoss()
global_step, best_f1 = 0, 0
print('开始训练:')
# 7. 开始训练循环!
for epoch in range(pc.epochs):
for batch in tqdm(train_dataloader, desc='模型训练'):
# 前向传播,拿到模型的输出
logits = model(
input_ids=batch['input_ids'].to(pc.device),
token_type_ids=batch['token_type_ids'].to(pc.device),
attention_mask=batch['attention_mask'].to(pc.device)
).logits
# 计算损失
mask_labels = batch['mask_labels'].numpy().tolist()
sub_labels = verbalizer.batch_find_sub_labels(mask_labels)
sub_labels = [ele['token_ids'] for ele in sub_labels]
loss = mlm_loss(logits, batch['mask_positions'].to(pc.device), sub_labels, criterion, pc.device)
# 反向传播,更新参数
optimizer.zero_grad()
loss.backward()
optimizer.step()
lr_scheduler.step()
# 记录损失
loss_list.append(loss)
global_step += 1
# 打印日志,每多少步打一次
if global_step % pc.logging_steps == 0:
time_diff = time.time() - tic_train
loss_avg = sum(loss_list) / len(loss_list)
print("global step %d, epoch: %d, loss: %.5f, speed: %.2f step/s"
% (global_step, epoch, loss_avg, pc.logging_steps / time_diff))
tic_train = time.time()
# 验证模型,看看效果怎么样
acc, precision, recall, f1, class_metrics = evaluate_model(model, metric, dev_dataloader, tokenizer, verbalizer)
print("验证集的 precision: %.5f, recall: %.5f, F1: %.5f" % (precision, recall, f1))
# 如果效果更好,就保存模型
if f1 > best_f1:
print(f"最好的f1分数被更新: {best_f1:.5f} --> {f1:.5f}")
best_f1 = f1
cur_save_dir = os.path.join(pc.save_dir, "model_best")
if not os.path.exists(cur_save_dir):
os.makedirs(cur_save_dir)
model.save_pretrained(cur_save_dir)
tokenizer.save_pretrained(cur_save_dir)
tic_train = time.time()
哦,这里还有个评估模型的函数,我们也写一下:
def evaluate_model(model, metric, data_loader, tokenizer, verbalizer):
model.eval()
metric.reset()
with torch.no_grad():
for step, batch in enumerate(tqdm(data_loader, desc='模型验证')):
# 前向传播
logits = model(
input_ids=batch['input_ids'].to(pc.device),
token_type_ids=batch['token_type_ids'].to(pc.device),
attention_mask=batch['attention_mask'].to(pc.device)
).logits
# 拿到真实标签
mask_labels = batch['mask_labels'].numpy().tolist()
for i in range(len(mask_labels)):
while tokenizer.pad_token_id in mask_labels[i]:
mask_labels[i].remove(tokenizer.pad_token_id)
mask_labels = [''.join(tokenizer.convert_ids_to_tokens(t)) for t in mask_labels]
# 拿到预测结果
predictions = convert_logits_to_ids(logits, batch['mask_positions']).cpu().numpy().tolist()
predictions = verbalizer.batch_find_main_label(predictions)
predictions = [ele['label'] for ele in predictions]
# 加到评估类里
metric.add_batch(pred_batch=predictions, gold_batch=mask_labels)
eval_metric = metric.compute()
model.train()
return eval_metric['accuracy'], eval_metric['precision'], \
eval_metric['recall'], eval_metric['f1'], \
eval_metric['class_metrics']
搞定!训练的代码就全部写完了!
你运行这个model2train\(\)函数,就能开始训练了!
我第一次跑的时候,都不敢信,63 条数据,训练了 20 轮,验证集的 F1 直接到了 91%!就 63 条数据啊!比我之前用几万条数据训的 TextCNN 效果还好!
2.5 推理:用训练好的模型预测新数据!
训练完了,我们怎么用它来预测新的评论呢?很简单:
# 加载训练好的模型
model_path = './save_model/model_best'
tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoModelForMaskedLM.from_pretrained(model_path).to(pc.device)
verbalizer = Verbalizer(verbalizer_file=pc.verbalizer, tokenizer=tokenizer, max_label_len=pc.max_label_len)
# 我们的测试评论
contents = [
'天台很好看,躺在躺椅上很悠闲,因为活动所以我觉得性价比还不错,适合一家出行,特别是去迪士尼也蛮近的,下次有机会肯定还会再来的,值得推荐',
'环境,设施,很棒,周边配套设施齐全,前台小姐姐超级漂亮!酒店很赞,早餐不错,服务态度很好,前台美眉很漂亮。性价比超高的一家酒店。强烈推荐',
'物流超快,隔天就到了,还没用,屯着出游的时候用的,听方便的,占地小',
'福行市来到无早集市,因为是喜欢的面包店,所以跑来集市看看。第一眼就看到了,之前在微店买了小刘,这次买了老刘,还有一直喜欢的巧克力磅蛋糕。好奇老板为啥不做柠檬磅蛋糕了,微店一直都是买不到的状态。因为不爱碱水硬欧之类的,所以期待老板多来点其他小点,饼干一直也是大爱,那天好像也没看到',
'服务很用心,房型也很舒服,小朋友很喜欢,下次去嘉定还会再选择。床铺柔软舒适,晚上休息很安逸,隔音效果不错赞,下次还会来'
]
# 逐个预测
for content in contents:
# 处理输入
hard_template = HardTemplate(prompt='这是一条{MASK}评论:{textA}')
encoded_inputs = hard_template(
inputs_dict={'textA': content, 'MASK': '[MASK]'},
tokenizer=tokenizer,
mask_length=2,
max_seq_len=512
)
# 前向传播
input_ids = torch.LongTensor([encoded_inputs['input_ids']]).to(pc.device)
token_type_ids = torch.LongTensor([encoded_inputs['token_type_ids']]).to(pc.device)
attention_mask = torch.LongTensor([encoded_inputs['attention_mask']]).to(pc.device)
logits = model(input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask).logits
# 拿到预测结果
mask_positions = torch.LongTensor([encoded_inputs['mask_position']])
predictions = convert_logits_to_ids(logits, mask_positions).cpu().numpy().tolist()
predictions = verbalizer.batch_find_main_label(predictions)
print(f'{content}: {predictions[0]["label"]}')
运行一下,你就会看到输出:
天台很好看...: 酒店
环境,设施,很棒...: 酒店
物流超快...: 平板
福行市来到无早集市...: 水果
服务很用心...: 酒店
完美!全部预测对了!是不是超级酷?
第三部分:进阶方法:BERT+Prompt-Tuning,让模型自己学模板!
3.1 Prompt-Tuning 是什么?和 PET 有什么区别?
刚才我们学的 PET,是人工写的模板,比如这是一条\_\_\_\_评论:xxxx,但是人工写的模板,不一定是最好的,对吧?万一我写的模板,模型不喜欢,效果就不好了?
那有没有办法,不用人工写模板,让模型自己学模板?
有!这就是 Prompt-Tuning!
Prompt-Tuning 的思想是:我们不用人工写句子了,我们直接加几个可学习的 token,这几个 token 的向量,模型自己学,学完了之后,这几个 token 就相当于我们的模板了!
比如,我们在输入的最前面,加 6 个可学习的 token:\[u1\] \[u2\] \[u3\] \[u4\] \[u5\] \[u6\] \[MASK\]\[MASK\] 评论内容
这 6 个u1\-u6,就是我们的软模板,它们不是固定的词,是模型自己学的,模型会自己学,这 6 个 token 怎么调整,才能让预测最准!
这样就不用我们自己想模板了,模型自己学,效果往往比人工写的更好!
而且,我们训练的时候,只训练这几个软模板的 token,还有最后的预测层,BERT 的其他参数,我们全部冻住,不用改!这样参数就更少了,跑起来更快,更省显存!
是不是比 PET 更厉害?
3.2 我们的数据集和配置
Prompt-Tuning 的数据集,跟 PET 的差不多,也是 63 条训练数据,417 条验证数据,verbalizer 也一样,区别就是我们不用写 prompt.txt 了,因为模板是模型自己学的!
我们的配置文件里,加了一个p\_embedding\_num = 6,就是我们的软模板的长度,6 个可学习的 token,就这么简单!
3.3 数据处理代码:和 PET 的区别在哪?
数据处理的代码,跟 PET 的很像,但是更简单,因为我们不用处理人工模板了!
我们来看:
import torch
import numpy as np
from datasets import load_dataset
from transformers import AutoTokenizer
from prompt_tasks.Prompt_Tuning.ptune_config import ProjectConfig
def convert_example(
examples: dict,
tokenizer,
max_seq_len: int,
max_label_len: int,
p_embedding_num=6,
train_mode=True,
return_tensor=False
) -> dict:
tokenized_output = {
'input_ids': [],
'attention_mask': [],
'mask_positions': [],
'mask_labels': []
}
# 遍历每个样本
for i, example in enumerate(examples['text']):
try:
if train_mode:
# 拆分标签和评论,跟之前一样
label, content = example.strip().split('\t', 1)
# 处理标签,转成token id
mask_labels = tokenizer(text=label)
mask_labels = mask_labels['input_ids'][1:-1]
mask_labels = mask_labels[:max_label_len]
mask_labels += [tokenizer.pad_token_id] * (max_label_len - len(mask_labels))
tokenized_output['mask_labels'].append(mask_labels)
else:
content = example.strip()
# 对评论进行编码
encoded_inputs = tokenizer(text=content,truncation=True,max_length=max_seq_len,padding='max_length')
except:
continue
# 重点来了!我们要加MASK token,还有我们的软模板token!
# 1. 生成MASK token,跟之前一样
mask_tokens = ['[MASK]'] * max_label_len
mask_ids = tokenizer.convert_tokens_to_ids(mask_tokens)
# 2. 生成我们的软模板token,就是BERT词表里的unused token,这些token预训练的时候没用过,正好用来当我们的软模板!
p_tokens = ["[unused{}]".format(i + 1) for i in range(p_embedding_num)]
p_tokens_ids = tokenizer.convert_tokens_to_ids(p_tokens)
# 3. 把这些token拼起来!顺序是:软模板 -> CLS -> MASK -> 评论内容
tmp_input_ids = encoded_inputs['input_ids']
# 先把评论的长度裁剪一下,留出我们的模板和MASK的位置
tmp_input_ids = tmp_input_ids[:max_seq_len - len(mask_ids) - len(p_tokens_ids) - 1]
# 插入MASK到CLS的后面
tmp_input_ids = tmp_input_ids[:1] + mask_ids + tmp_input_ids[1:]
# 插入软模板到最前面
input_ids = p_tokens_ids + tmp_input_ids
# 记录MASK的位置,软模板有6个,CLS在第6个位置,所以MASK的位置就是6+1=7开始
mask_positions = [len(p_tokens_ids) + 1 + i for i in range(max_label_len)]
# 存起来
tokenized_output['input_ids'].append(input_ids)
tokenized_output['mask_positions'].append(mask_positions)
# 注意力掩码,跟之前一样
attention_mask = get_attention_mask(input_ids)
tokenized_output['attention_mask'].append(attention_mask)
if 'token_type_ids' in encoded_inputs:
tmp = encoded_inputs['token_type_ids']
if 'token_type_ids' not in tokenized_output:
tokenized_output['token_type_ids'] = [tmp]
else:
tokenized_output['token_type_ids'].append(tmp)
# 转成tensor
for k, v in tokenized_output.items():
if return_tensor:
tokenized_output[k] = torch.LongTensor(v)
else:
tokenized_output[k] = np.array(v)
return tokenized_output
哦,这个就是数据处理的代码,是不是很简单?我们用了 BERT 词表里的\[unused1\]到\[unused6\]这几个 token,这些 token 是 BERT 预训练的时候留出来的,没用过,正好我们拿来当软模板的 token,模型训练的时候,就会调整这几个 token 的 embedding,让它们变成最好的模板!
然后数据加载器的代码,跟 PET 的几乎一样,我就不贴了,就是把处理函数换一下而已。
3.4 模型训练:冻结大部分参数,只训一点点!
训练的代码,跟 PET 的大部分都一样,但是有一个最大的区别:我们冻结 BERT 的大部分参数,只训练软模板和预测层!
因为我们不用改 BERT 的其他参数,那些参数已经预训练好了,我们只改我们的软模板的参数,还有最后的预测层,这样参数就超级少了!
我们来看:
def model2train():
# 加载数据,跟之前一样
train_dataloader, dev_dataloader = get_data()
# 加载模型,跟之前一样
model = AutoModelForMaskedLM.from_pretrained(pc.pre_model).to(pc.device)
# 重点来了!冻结参数!
for name, param in model.named_parameters():
# 只有embedding层和cls预测层,我们才训练,其他的全部冻住!
if ("embeddings" in name) or ("cls" in name) or ("predictions" in name) or ("lm_head" in name):
param.requires_grad = True
else:
param.requires_grad = False
# 打印一下可训练的参数,你会发现,只有一点点!
trainable_params = [n for n, p in model.named_parameters() if p.requires_grad]
print("可训练参数:", trainable_params)
看到没?我们把 BERT 的 encoder 层,就是中间的那些 transformer 层,全部冻住了,不用训练,只有 embedding 层(因为我们的软模板的 token 的 embedding 在这),还有最后的预测层,我们才训练!
这样的话,可训练的参数只有不到 1%!太省显存了!我用 1060 的显卡都能跑!
剩下的训练代码,跟 PET 的几乎一模一样,损失函数、评估函数,都一样!
我跑了一下,这个 Prompt-Tuning 的效果,比 PET 还要好一点,F1 到了 93%!因为模型自己学的模板,比我人工写的更好!
踩坑提醒:很多小伙伴这里会忘记冻结参数,结果训的时候显存爆炸,那是因为你把所有参数都训了,跟全量微调一样了,一定要记得冻结!
第四部分:大模型实战:ChatGLM+LoRA,6B 大模型你的 3060 就能跑!
好了,学完了 BERT 的小模型,我们来搞点大的!搞 ChatGLM-6B 大模型!
很多小伙伴说,ChatGLM-6B 有 60 亿参数,我只有 3060 的 8G 显卡,能跑吗?
能!用 LoRA!
4.1 LoRA 是什么?大白话讲透!
LoRA,Low-Rank Adaptation,低秩适应,又是一个看不懂的术语?
别慌,大白话讲:原来的大模型,我们不动它的权重,我们在每个注意力层,旁边加两个小的矩阵,训练的时候,只训这两个小矩阵,其他的全部冻住!
啥意思?
比如,原来的注意力层,有一个大的权重矩阵 W,大小是 4096*4096,这个矩阵很大,训练它要很多显存。
我们现在,不动这个 W 了,我们加两个小的矩阵,A 和 B,A 是 40968,B 是 84096,这两个加起来,才 40968 + 84096 = 65536 个参数,比原来的 1600 万参数小太多了!
训练的时候,我们只训 A 和 B,原来的 W 不动,这样的话,我们的梯度就只有这两个小矩阵的,显存占用超级小!
而且,效果跟全量微调差不多!因为这两个小矩阵,就把我们要的下游任务的信息学进去了!
这就是 LoRA!太牛了!原来 ChatGLM 全量微调要 14G 显存,用了 LoRA,只要 7G!你的 3060 的 8G 显卡,就能跑!
4.2 我们的项目:一个模型同时做两个任务!
我们这个项目,是多任务微调,就是一个 ChatGLM 模型,同时做两个任务:
-
信息抽取:从句子里抽出三元组,比如 “九玄珠是在纵横中文网连载的”,抽出
\(九玄珠, 连载网站, 纵横中文网\) -
文本分类:给评论分类,跟我们之前的一样。
我们用 Instruction 来区分任务,比如:
-
指令是
你现在是一个三元组抽取器,把句子里的三元组抽出来,模型就做抽取 -
指令是
你现在是一个评论分类器,把评论分类,模型就做分类
一个模型,搞定两个任务,不用搞两个模型,这就是大模型的厉害之处!
4.3 数据处理:指令微调的数据长什么样?
我们的数据集,是混合的,既有抽取的,也有分类的,长这样:
抽取的样本:
{
"context": "Instruction: 你现在是一个很厉害的阅读理解器,严格按照人类指令进行回答。\nInput: 找到句子中的三元组信息并输出成json给我:\n\n九玄珠是在纵横中文网连载的一部小说,作者是龙马。\nAnswer: ",
"target": "```json\n[{\"predicate\": \"连载网站\", \"object_type\": \"网站\", \"subject_type\": \"网络小说\", \"object\": \"纵横中文网\", \"subject\": \"九玄珠\"}, {\"predicate\": \"作者\", \"object_type\": \"人物\", \"subject_type\": \"图书作品\", \"object\": \"龙马\", \"subject\": \"九玄珠\"}]\n```"
}
分类的样本:
{
"context": "Instruction: 你现在是一个评论分类器,把下面的评论分类。\nInput: 包装不错,苹果挺甜的。\nAnswer: ",
"target": "水果"
}
看到没?每个样本,前面是指令 + 输入,后面是答案,模型训练的时候,就学会了,看到不同的指令,就输出不同的结果!
我们的数据处理,就是把这些输入和输出,拼起来,转成模型能接受的 id,而且,我们把输入部分的 label,设成 - 100,让模型只学习输出部分的损失!
啥意思?就是说,输入的指令部分,我们不想让模型学,我们只想让模型学输出的答案部分,所以输入部分的损失,我们忽略,只算输出部分的!
我们来看数据处理的代码:
import json
import torch
import numpy as np
from tqdm import tqdm
from datasets import load_dataset
from transformers import AutoTokenizer
from ptune_chatglm.glm_config import ProjectConfig
pc = ProjectConfig()
def convert_example_chatglm(examples, tokenizer, max_source_seq_len, max_target_seq_len):
tokenized_output = {
'input_ids': [],
'labels': []
}
max_seq_len = max_source_seq_len + max_target_seq_len
for example in tqdm(examples['text']):
example = json.loads(example)
context = example['context']
target = example['target']
# 对输入和输出进行编码
input_ids = tokenizer.encode(context)
output_ids = tokenizer.encode(target)
# 拼起来
input_ids = input_ids + output_ids
# 标签,输入部分的label是-100,输出部分的是真实的id
labels = [-100] * len(input_ids)
labels[-len(output_ids):] = output_ids
# padding
input_ids += [tokenizer.pad_token_id] * (max_seq_len - len(input_ids))
labels += [-100] * (max_seq_len - len(labels))
tokenized_output['input_ids'].append(input_ids)
tokenized_output['labels'].append(labels)
for k, v in tokenized_output.items():
tokenized_output[k] = np.array(v)
return tokenized_output
哦,这个就是核心!labels = \[\-100\] \* len\(input\_ids\),然后把输出部分的替换成真实的id,这样的话,模型计算损失的时候,就会忽略输入部分的 - 100,只计算输出部分的损失,完美!
踩坑提醒:很多新手这里搞不懂,为什么要把输入的 label 设成 - 100?我之前也搞了好久,后来才明白,因为我们的输入是给模型的问题,我们不想让模型学习怎么生成问题,我们只想让它学习怎么生成答案,所以问题部分的损失我们不管,只算答案部分的,这样就对了!
4.4 LoRA 模型配置:7G 显存就能跑!
然后,我们加载模型,配置 LoRA:
import peft
from transformers import AutoConfig, AutoModel, AutoTokenizer
# 加载tokenizer和模型
tokenizer = AutoTokenizer.from_pretrained(pc.pre_model, trust_remote_code=True)
config = AutoConfig.from_pretrained(pc.pre_model, trust_remote_code=True)
model = AutoModel.from_pretrained(pc.pre_model, config=config, trust_remote_code=True)
# 冻结模型的大部分参数
model = model.half()
model.config.use_cache = False
model.gradient_checkpointing_enable()
model.enable_input_require_grads()
# 配置LoRA
peft_config = peft.LoraConfig(
task_type=peft.TaskType.CAUSAL_LM, # 我们是因果语言模型
inference_mode=False,
r=pc.lora_rank, # 低秩的维度,我们用8
lora_alpha=32, # 缩放系数
lora_dropout=0.1
)
# 把模型转成LoRA模型
model = peft.get_peft_model(model, peft_config)
# 打印可训练的参数,你会发现,只有0.1%的参数!
model.print_trainable_parameters()
# 输出:trainable params: 1,949,696 || all params: 6,242,418,944 || trainable%: 0.03123302767844878
我的天!只有不到 0.04% 的参数是可训练的!总共才 194 万个参数!这也太少了!
所以显存占用才 7G,你的 3060 的 8G 显卡,完全能跑!
4.5 训练代码:跟之前的差不多!
剩下的训练代码,跟我们之前的几乎一样,优化器、学习率调度器,都差不多,只是损失函数,模型自己就算好了,我们直接拿就行:
for epoch in range(pc.epochs):
model.train()
for batch in train_dataloader:
with autocast():
loss = model(
input_ids=batch['input_ids'].to(dtype=torch.long, device=pc.device),
labels=batch['labels'].to(dtype=torch.long, device=pc.device)
).loss
optimizer.zero_grad()
loss.backward()
optimizer.step()
lr_scheduler.step()
...
就这么简单!训练完了,我们的模型就学会了,既能做信息抽取,又能做文本分类!
4.6 推理:用训练好的 ChatGLM 做预测!
推理的时候,我们只要把指令和输入拼起来,输入给模型,模型就输出答案了:
def inference(model, tokenizer, instuction: str, sentence: str):
with torch.no_grad():
input_text = f"Instruction: {instuction}\n"
if sentence:
input_text += f"Input: {sentence}\n"
input_text += f"Answer: "
batch = tokenizer(input_text, return_tensors="pt")
out = model.generate(
input_ids=batch["input_ids"].to(pc.device),
max_new_tokens=300,
temperature=0
)
out_text = tokenizer.decode(out[0])
answer = out_text.split('Answer: ')[-1]
return answer
# 测试抽取
res = inference(model, tokenizer,
"现在你是一个非常厉害的SPO抽取器。",
"下面这句中包含了哪些三元组,用json列表的形式回答,不要输出除json外的其他答案。\n\n黄磊是一个特别幸运的演员,拍第一部戏就碰到了导演陈凯歌")
print(res)
# 测试分类
res = inference(model, tokenizer,
"你现在是一个评论分类器。",
"物流超快,隔天就到了,还没用,屯着出游的时候用的")
print(res)
运行一下,你就会看到,模型完美输出了结果!抽取的三元组,分类的结果,全部正确!
太酷了!一个模型,两个任务,7G 显存,3060 就能跑!
第五部分:没有好显卡?教你用趋动云,几块钱用 A800!
很多小伙伴说,我连 3060 都没有,我只有个笔记本,想跑 ChatGLM 的微调,怎么办?
没关系!我们用云平台!趋动云这个平台,特别适合新手,便宜,而且操作简单,花几块钱,就能用 A800 的 80G 显卡,跑大模型微调,比你自己买显卡划算多了!
我带着你一步步来,从注册到运行,全程保姆级步骤!
5.1 注册登录
首先,打开注册链接趋动云这
用你的手机号注册就行,新用户还能领算力券,免费就能用几个小时!
注册完了,登录进去,就能看到主界面了。
5.2 上传你的数据和模型
首先,我们要把我们的数据集和预训练模型,上传到平台上:
上传数据
-
点击左边的「数据」工具,然后点击「创建数据」
-
给你的数据起个名字,比如
llm\_data,然后选择上传方式,网页上传就行,把你的数据集压缩包,拖进去上传。 -
等上传完了,你的数据就存在平台上了,后面用的时候直接挂载就行。
上传预训练模型
ChatGLM 的模型比较大,网页上传慢,我们用 SFTP 上传:
-
点击左边的「模型」工具,创建模型,起个名字
chatglm\-6b -
选择 SFTP 上传方式,然后你会拿到 SFTP 的连接信息
-
如果你是 Windows,打开 CMD,如果你是 Mac,打开终端:
# 输入连接命令,把你拿到的连接字符粘进去 sftp roif48iKYp@cluster1-dev4.virtaicloud.com # 然后输入密码,就是平台给你的密码 # 然后上传模型,把你的本地模型路径改一下 put -r D:\Git\chatglm-6b\ /upload然后等着上传完就行,大模型也就十几 G,一会就传完了。
5.3 创建项目
数据和模型都上传好了,我们创建项目:
-
点击右上角的「创建项目」
-
给项目起个名字,比如
llm\_finetune -
然后挂载我们刚才上传的数据和模型,数据会挂载到
/gemini/data1,模型挂载到/gemini/pretrain -
选择镜像,选 pytorch 的镜像,版本 2.0.1 就行,自带 cuda
-
然后把我们的代码,压缩成 zip,上传上去,平台会自动解压。
5.4 运行训练!
项目创建完了,我们点击「开发」,创建开发环境:
-
选择显卡,如果你要跑 ChatGLM 的微调,选 A800 的话,也就几块钱一小时,比自己买显卡便宜多了
-
等待资源创建,创建完了,点击进入开发环境
-
进去之后,打开网页终端,先安装依赖:
pip install protobuf==3.20.0 transformers==4.27.1 icetk cpm_kernels peft==0.3.0 -
然后修改我们的配置文件,把数据路径改成平台的路径,比如
/gemini/data1/\.\.\. -
然后运行训练脚本:
python train.py然后就等着训练完就行!A800 跑 ChatGLM 的微调,也就 1 个多小时就完事了!
5.5 下载训练好的模型
训练完了,我们把模型下载到本地:
-
首先,去平台设置,开启 SSH,设置用户名密码
-
然后拿到 SSH 的连接串,打开终端:
scp -P 30022 itheima@ssh.virtaicloud.com:/gemini/code/checkpoints/ptune/model_best ~/Desktop然后输入密码,等着下载完就行,模型只有几十 M,因为 LoRA 的参数很小,一会就下完了!
搞定!你就有了自己训练好的 ChatGLM 模型了!
第六部分:新手常见问题 & 踩坑记录
我做这个项目的时候,踩了一堆坑,整理出来,大家别再踩了:
-
Q:加载 ChatGLM 的时候报错,说 trust_remote_code?
A:因为 ChatGLM 的代码是自定义的,你要加trust\_remote\_code=True,告诉 transformers,信任这个自定义的代码,不然它不让你加载。 -
Q:训练的时候显存不够怎么办?
A:把 batch_size 调小,比如原来的 8 改成 4,或者改成 2,就好了,或者用梯度累积,也能省显存。 -
Q:标注数据太少了,效果不好怎么办?
A:我们的方法就是小样本的,63 条足够了,你要是还想更好,可以加一点无标注数据,用 PET 的伪标签,效果会更好。 -
Q:LoRA 的版本不对,报错怎么办?
A:要装 peft==0.3.0 的版本,太高或者太低都不兼容,别装错了。 -
Q:云平台的费用贵吗?
A:不贵,跑一次 ChatGLM 的微调,也就 5 块钱左右,新用户的算力券都够了,免费就能跑。
最后:我们一起学!
兄弟们,这篇文章,我花了一周的时间,把我自己做项目的所有内容,全部整理出来了,从最基础的原理,到代码逐行讲解,到踩坑记录,就是想带着咱们粉丝群的小伙伴,一起学大模型,不用怕难,我们一步一步来。
完整的代码,我已经放到 GitHub 了,大家可以去下载:https://github.com/1016667086/1016667086.git
大家跑的时候,遇到什么问题,就在评论区留言,或者粉丝群里问,我们一起解决,一起学,一起进步!
下一篇文章,我会带着大家,把我们训练好的模型,做成一个网页 demo,部署到服务器上,让别人也能玩,敬请期待!
如果这篇文章对你有帮助,麻烦点个赞,收个藏,关注我。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)