目标
概念
一般情况下预训练模型都是大型模型, 具备复杂的网络结构, 众多的参数量, 以及足够大的数据集进行训练而产生的模型, 在NLP领域, 预训练模型往往是语言模型, 因为语言模型是无监督的, 可以获得大量的语料, 同时语言模型优势许多经典NLP任务的基础, 如:
常见预训练模型包括
根据给定的预训练模型, 改变它的部分参数或者为其新增部分输出结构后, 通过小部分训练集上训练, 来使整个模型更好的使用特定任务
实现微调过程的代码文件, 这些脚本文件中, 包含对预训练模型的调用, 对微调参数的选定以及对微调结构的更改, 同时因为微调是一个训练过程, 同样需要一些超参数的设定, 以及损失函数和优化器的选取等, 因此微调脚本往往也包含了整个迁移学习的过程
说明
一般情况下, 微调脚本应该由不同的任务类型开发者自己编写, 但是由于目前研究的NLP任务类型(分类, 提取, 生成)以及对应的微调输出结构是有限的, 有些微调方式已经在很多数据集上验证是有效的, 因此微调脚本也可以使用已经完成的规范化脚本
直接用预训练模型的方式, 已经在fasttext的词向量迁移中学习, 接下来的迁移学习实践将主要讲解微调方式进行迁移学习
GLUE由纽约大学, 华盛顿大学和Google推出涵盖不同NLP任务, 成为衡量NLP研究发展的衡量标准
''' Script for downloading all GLUE data.
Note: for legal reasons, we are unable to host MRPC.
You can either use the version hosted by the SentEval team, which is already tokenized,
or you can download the original data from (https://download.microsoft.com/download/D/4/6/D46FF87A-F6B9-4252-AA8B-3604ED519838/MSRParaphraseCorpus.msi) and extract the data from it manually.
For Windows users, you can run the .msi file. For Mac and Linux users, consider an external library such as 'cabextract' (see below for an example).
You should then rename and place specific files in a folder (see below for an example).
mkdir MRPC
cabextract MSRParaphraseCorpus.msi -d MRPC
cat MRPC/_2DEC3DBE877E4DB192D17C0256E90F1D | tr -d $'\r' > MRPC/msr_paraphrase_train.txt
cat MRPC/_D7B391F9EAFF4B1B8BCE8F21B20B1B61 | tr -d $'\r' > MRPC/msr_paraphrase_test.txt
rm MRPC/_*
rm MSRParaphraseCorpus.msi
1/30/19: It looks like SentEval is no longer hosting their extracted and tokenized MRPC data, so you'll need to download the data from the original source for now.
2/11/19: It looks like SentEval actually *is* hosting the extracted data. Hooray!
'''
import os
import sys
import shutil
import argparse
import tempfile
import urllib.request
import zipfile
TASKS = ["CoLA", "SST", "MRPC", "QQP", "STS", "MNLI", "QNLI", "RTE", "WNLI", "diagnostic"]
TASK2PATH = {"CoLA":'https://dl.fbaipublicfiles.com/glue/data/CoLA.zip',
"SST":'https://dl.fbaipublicfiles.com/glue/data/SST-2.zip',
"QQP":'https://dl.fbaipublicfiles.com/glue/data/QQP-clean.zip',
"STS":'https://dl.fbaipublicfiles.com/glue/data/STS-B.zip',
"MNLI":'https://dl.fbaipublicfiles.com/glue/data/MNLI.zip',
"QNLI":'https://dl.fbaipublicfiles.com/glue/data/QNLIv2.zip',
"RTE":'https://dl.fbaipublicfiles.com/glue/data/RTE.zip',
"WNLI":'https://dl.fbaipublicfiles.com/glue/data/WNLI.zip',
"diagnostic":'https://dl.fbaipublicfiles.com/glue/data/AX.tsv'}
MRPC_TRAIN = 'https://dl.fbaipublicfiles.com/senteval/senteval_data/msr_paraphrase_train.txt'
MRPC_TEST = 'https://dl.fbaipublicfiles.com/senteval/senteval_data/msr_paraphrase_test.txt'
def download_and_extract(task, data_dir):
print("Downloading and extracting %s..." % task)
if task == "MNLI":
print("\tNote (12/10/20): This script no longer downloads SNLI. You will need to manually download and format the data to use SNLI.")
data_file = "%s.zip" % task
urllib.request.urlretrieve(TASK2PATH[task], data_file)
with zipfile.ZipFile(data_file) as zip_ref:
zip_ref.extractall(data_dir)
os.remove(data_file)
print("\tCompleted!")
def format_mrpc(data_dir, path_to_data):
print("Processing MRPC...")
mrpc_dir = os.path.join(data_dir, "MRPC")
if not os.path.isdir(mrpc_dir):
os.mkdir(mrpc_dir)
if path_to_data:
mrpc_train_file = os.path.join(path_to_data, "msr_paraphrase_train.txt")
mrpc_test_file = os.path.join(path_to_data, "msr_paraphrase_test.txt")
else:
try:
mrpc_train_file = os.path.join(mrpc_dir, "msr_paraphrase_train.txt")
mrpc_test_file = os.path.join(mrpc_dir, "msr_paraphrase_test.txt")
URLLIB.urlretrieve(MRPC_TRAIN, mrpc_train_file)
URLLIB.urlretrieve(MRPC_TEST, mrpc_test_file)
except urllib.error.HTTPError:
print("Error downloading MRPC")
return
assert os.path.isfile(mrpc_train_file), "Train data not found at %s" % mrpc_train_file
assert os.path.isfile(mrpc_test_file), "Test data not found at %s" % mrpc_test_file
with io.open(mrpc_test_file, encoding='utf-8') as data_fh, \
io.open(os.path.join(mrpc_dir, "test.tsv"), 'w', encoding='utf-8') as test_fh:
header = data_fh.readline()
test_fh.write("index\t#1 ID\t#2 ID\t#1 String\t#2 String\n")
for idx, row in enumerate(data_fh):
label, id1, id2, s1, s2 = row.strip().split('\t')
test_fh.write("%d\t%s\t%s\t%s\t%s\n" % (idx, id1, id2, s1, s2))
try:
URLLIB.urlretrieve(TASK2PATH["MRPC"], os.path.join(mrpc_dir, "dev_ids.tsv"))
except KeyError or urllib.error.HTTPError:
print("\tError downloading standard development IDs for MRPC. You will need to manually split your data.")
return
dev_ids = []
with io.open(os.path.join(mrpc_dir, "dev_ids.tsv"), encoding='utf-8') as ids_fh:
for row in ids_fh:
dev_ids.append(row.strip().split('\t'))
with io.open(mrpc_train_file, encoding='utf-8') as data_fh, \
io.open(os.path.join(mrpc_dir, "train.tsv"), 'w', encoding='utf-8') as train_fh, \
io.open(os.path.join(mrpc_dir, "dev.tsv"), 'w', encoding='utf-8') as dev_fh:
header = data_fh.readline()
train_fh.write(header)
dev_fh.write(header)
for row in data_fh:
label, id1, id2, s1, s2 = row.strip().split('\t')
if [id1, id2] in dev_ids:
dev_fh.write("%s\t%s\t%s\t%s\t%s\n" % (label, id1, id2, s1, s2))
else:
train_fh.write("%s\t%s\t%s\t%s\t%s\n" % (label, id1, id2, s1, s2))
print("\tCompleted!")
def download_diagnostic(data_dir):
print("Downloading and extracting diagnostic...")
if not os.path.isdir(os.path.join(data_dir, "diagnostic")):
os.mkdir(os.path.join(data_dir, "diagnostic"))
data_file = os.path.join(data_dir, "diagnostic", "diagnostic.tsv")
urllib.request.urlretrieve(TASK2PATH["diagnostic"], data_file)
print("\tCompleted!")
return
def get_tasks(task_names):
task_names = task_names.split(',')
if "all" in task_names:
tasks = TASKS
else:
tasks = []
for task_name in task_names:
assert task_name in TASKS, "Task %s not found!" % task_name
tasks.append(task_name)
return tasks
def main(arguments):
parser = argparse.ArgumentParser()
parser.add_argument('--data_dir', help='directory to save data to', type=str, default='glue_data')
parser.add_argument('--tasks', help='tasks to download data for as a comma separated string',
type=str, default='all')
parser.add_argument('--path_to_mrpc', help='path to directory containing extracted MRPC data, msr_paraphrase_train.txt and msr_paraphrase_text.txt',
type=str, default='')
args = parser.parse_args(arguments)
if not os.path.isdir(args.data_dir):
os.mkdir(args.data_dir)
tasks = get_tasks(args.tasks)
for task in tasks:
if task == 'MRPC':
format_mrpc(args.data_dir, args.path_to_mrpc)
elif task == 'diagnostic':
download_diagnostic(args.data_dir)
else:
download_and_extract(task, args.data_dir)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))
其中训练集(train.tsv)和测试集(test.tsv)都是带标签的数据, test.tsv是不带标签的
任务类型
说明
matched: 代表与训练集一同采集的
mismatched: 代表与训练集分开采集的
任务类型
目标
流行的预训练模型
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
bert-base-uncased | 12 | 768 | 12 | 110M | 在小写英文文本上进行训练 |
bert-large-uncased | 24 | 1024 | 16 | 340M | 在小写英文文本上进行训练 |
bert-base-cased | 24 | 1024 | 16 | 340M | 在不区分大小写的英文文本上训练得到 |
bert-large-cased | 24 | 1024 | 16 | 340M | 在不区分大小写的英文文本上训练得到 |
bert-base-multilingual-uncased | 12 | 768 | 12 | 110M | 在小写的102种语言文本上进行训练得到 |
bert-large-multilingual-uncased | 24 | 1024 | 16 | 340M | 在不区分大小写的102种语言文本上进行训练而得到 |
bert-base-chinese | 12 | 768 | 12 | 110M | 在简体和繁体中文文本上进行训练而得到 |
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
openai-gpt | 12 | 768 | 12 | 110M | OpenAI在英文语料上进行训练得到 |
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
gpt2 | 12 | 768 | 12 | 117M | OpenAI GPT-2英文语料上进行训练得到 |
gpt2-xl | 48 | 1600 | 25 | 1558M | 在大型的OpenAI GPT-2英文语料上进行训练得到 |
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
transfo-xl-wt103 | 18 | 1024 | 16 | 257M | 在wikitext-103英文语料进行训练得到 |
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
xlnet-base-cased | 12 | 768 | 12 | 110M | 在英文文本上进行训练得到 |
xlnet-large-cased | 24 | 1024 | 16 | 240M | 在英文文本上进行训练得到 |
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
xlm-mlm-en-2048 | 12 | 2048 | 12 | 110M | 在英文文本上进行训练得到 |
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
roberta-base | 12 | 768 | 12 | 125M | 在英文文本上进行训练得到 |
roberta-large | 24 | 1024 | 16 | 355M | 在英文文本上进行训练得到 |
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
distilbert-base-uncased | 6 | 768 | 12 | 66M | 在英文文本上进行训练得到 |
distilbert-base-multilingual-cased | 6 | 768 | 12 | 66M | 基于bert-base-multilingual-uncased蒸馏(压缩)模型 |
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
albert-base-v1 | 12 | 768 | 12 | 110M | 在英文文本上进行训练得到 |
albert-base-v2 | 12 | 768 | 12 | 110M | 在英文文本上进行训练得到,比v1花费更多时间和数据量 |
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
t5-small | 6 | 512 | 8 | 60M | 在c4语料上进行训练而得到 |
t5-base | 12 | 768 | 12 | 220M | 在c4语料上进行训练而得到 |
t5-large | 24 | 1024 | 16 | 770M | 在c4语料上进行训练而得到 |
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
xlm-roberta-base | 12 | 768 | 12 | 125M | 在2.5TB的100种语言文本上进行训练得到 |
xlm-roberta-large | 24 | 1027 | 16 | 355M | 在2.5TB的100种语言文本上进行训练得到 |
所有的预训练模型及其变体都是以transformers
为基础, 只是在模型结构如神经元连接方式, 编码器隐层数, 多头注意力头数发生改变, 这些改变是依据标注顺聚集上的表现而定的, 对于使用者需要在自己处理的目标数据上, 尽量遍历所有可用的模型达到最优效果即可
目标
工具
结构化字段对齐
[{"role":"user","content":...},{"role":"assistant","content":...}]
,避免混合使用speaker/listener
等不同标签编码与符号规范
与\n
混用)unicodedata.normalize
标准化字符类别平衡策略
多任务数据配比
低质量数据过滤
领域适配性筛选
语义保留型增强
对抗样本注入
文本长度分层
难度渐进训练
时间敏感性划分
领域覆盖度验证
数据版本控制
低资源场景优化
典型错误案例:某电商客服系统直接使用通用语料微调,未过滤"请给五星好评"类诱导性文本,导致生成结果频繁出现违规话术。解决方案:构建领域敏感词库进行二次过滤,并加入合规性负样本。
通过以上多维度的数据质量控制,可使模型微调成功率提升40%以上(实际项目验证数据)。建议每轮迭代后使用LIME等可解释性工具分析数据影响。
bert-base-chinese
pip install tqdm boto3 requests regex sentencepiece scremoses
tokenizer
import torch
# 预训练模型来源, 几乎固定的写法
source = "huggingface/pytorch-transformers"
# 选定加载模型哪一个部分, 这里是映射器
part = "tokenizer"
# 加载的预训练模型的名字
model_name = "bert-base-chinese"
# 只要是上面提到的常用模型都可以通过`torch.hub`来加载使用
tokenizer = torch.hub.load(source, part, model_name)
# 不带头的模型
part = 'model'
model = torch.hub.load(source, part, model_name)
# 加载带有语言模型头的预训练模型
part = 'modelWithLMHead'
lm_model = torch.hub.load(source, part, model_name)
# 加载带有分类模型头的预训练模型
part = 'modelForSequenceClassification'
classification_model = torch.hub.load(source, part, model_name)
# 加载带有问答模型头的预训练模型
part = 'modelForQuestionAnswering'
qa_model = torch.hub.load(source, part, model_name)
使用不带头的模型进行输出
import torch
input_text = "人生该如何起头"
# 使用tokenizer进行数值映射
indexed_tokens = tokenizer.encode(input_text)
# 打印映射后的结构
print("indexed_tokens: ", indexed_tokens)
# 将映射结构转化为张量输送给不带头的预训练模型
tokens_tensor = torch.tensor([indexed_tokens])
# 使用不带头的预训练模型获得效果, 直接利用模型进行输出, 不求导,不更新参数
with torch.no_grad():
# 编码层的输出, 和隐藏层的输出
encoded_layers, _ = model(tokens_tensor)
print("不带头的模型输出结果: ", encoded_layers)
print("不带头的模型输出结果尺寸:", encoded_layers.shape)
"""
output:
# tokenizer映射后, 101和102是起止符
# 中间的每个数据对应“人生该如何起头的每个字”
indexed_tokens: [101, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 202]
不带头的模型输出结果: tensor([[[ 0.0000, 0.0000, 0.0000, ..., 0.0000, 0.0000, 0.0000],]])
# 输出尺寸为 1*9*768, 就是每个字已经使用768维的向量进行了表示
# 我们可以基于此编码结构进行接下来的自定义操作, 如: 编写自己的微调网络进行最终的输出
不带头的模型输出结果的尺寸: torch.Size([1, 9, 768])
"""
使用带有语言模型头的模型进行输出
with torch.no_grad():
lm_output = lm_model(tokens_tensor)
print("带语言模型头的模型输出结果:", lm_output)
print("带语言模型头的模型输出结果尺寸:", lm_output.shape)
"""output:
带语言模型头的模型输出结果: tensor([[[ 0.0000, 0.0000, 0.0000, ..., 0.0000, 0.0000, 0.0000],]])
# 输出尺寸为 1*9*21128, 每个字使用了21128维的向量进行表示
# 和不带头的模型一样, 我们可以基于此编码结果进行接下来的自定义操作, 如编写自己的微调网络进行最终输出
带语言模型头的模型输出结果尺寸: torch.Size([1, 9, 21128])
"""
使用带有分类模型头的模型进行输出
with torch.no_grad():
classification_output = classification_model(tokens_tensor)
print("带分类模型头的模型输出结果: ", classification_output)
print("带分类模型头的模型输出尺寸: ", classification_output[0].shape)
"""output:
带分类模型头的模型输出结果: (tensor([[-0.0649, -0.1593]]),)
# 输出尺寸为1*2, 可以直接用于文本二分类问题
带分类模型头的模型输出尺寸: torch.Size([1, 2])
"""
使用带有问答模型头的模型进行输出
# 使用带有问答模型头的模型进行输出时, 需要输出入的形式为句子对
# 第一个句子是对客观事物的陈述, 第二个句子是针对第一个具体提出的问题
# 问答模型最终将得到两个张量, 每个张量中最大值对应索引的分别代表答案的在文本中的起始位置和终止位置
import torch
input_text1 = "我家小狗是黑色的"
input_text2 = "我家小狗是什么颜色的?"
# 映射两个句子
indexed_tokens = tokenizer.encode(input_text1, input_text2)
# 101 我家小狗是黑色的 102 我家小狗是什么颜色的 102
# 输出结果: [101, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 202, 102, 1997, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 102]
# 用0, 1来区分第一条和第二条句子
segments_ids = [0] * 11 + [1] * 14
# 转化张量形式
segments_tensors = torch.tensor([segments_ids])
tokens_tensor = torch.tensor([indexed_tokens])
# 使用带有问答模型头的预训练模型获得结果
with torch.no_grad():
# token_type_ids 指定位置那些是陈述, 哪些是问题
start_logits, end_logits = qa_model(tokens_tensor, token_type_ids=segments_tensors)
print("问答模型输出结果: ", start_logits, end_logits)
print("问答模型输出结果尺寸: ", start_logits.shape, end_logits.shape)
"""output:
# 输出为两个形状1*25的张量, 他们是两条句子合并长度的概率分布
# 第一个张量中最大值所在的索引代表答案出现的起始索引, 第二个张量中最大值所在的索引代表答案出现的终止索引
带问答模型头的模型输出结果: (tensor([[ 0.2574, -0.0293, 0.0000, ..., 0.0000, 0.0000, 0.0000, 0.2426]]),
tensor([[ 0.0000, 0.0000, 0.0000, ..., 0.0000, 0.0000, 0.0000, 0.0000]]))
)
带问答模型头的模型输出结果尺寸: torch.Size([1, 25]) torch.Size([1, 25])
"""
目标
指定任务类型的微调脚本
# 克隆huggingface的transformers文件
git clone https://github.com/huggingface/transformers.git
# 进行transformers安装
cd transformers && pip install .
# 进入微调脚本所需要的路径并查看
cd examples && ls
# 启动 run_glue.py 就是针对 GLUE 数据集合微调脚本
# 定义DATA_DIR: 微调数据所在路径, 这里我们使用 glue_data 中的数据作为微调数据
export DATA_DIR="../../glue_data"
# 定义SAVE_DIR: 模型的保存路径, 我们将模型保存在当前目录的bert_finetuning_test文件中
export SAVE_DIR="./bert_finetuning_test"
# 使用python运行微调脚本
# --model_type: 选择需要微调的模型类型, 可以使用BERT, XLNET, XLM, roBERTa, DistilBERT, ALBERT, XLM-RoBERTa, XLM-MLM, XLM-MLM-U, XLM-MLM-U-S, XLM-MLM-17-1280, XLM-MLM-17-1280-S, XLM-MLM-100-1280, XLM-MLM-100-1280-S, RoBERTa-
# --model_name_or_path: 选择具体的模型或者变体, 这里是在英文语料上微调, 因此选择bert-base-uncased
# --task_name: 代表对应的任务类型, 比如MPRPC代表对句子的二分类任务
# --do_train: 使用微调脚本进行训练
# --do_eval: 使用微调脚本进行验证
# --data_dir: 微调数据所在路径, 将自动寻找该路径下的`train.tsv`, `dev.tsv`作为训练集和验证集
# --max_seq_length: 输入句子的最大长度, 超过则截断, 不足则补齐
# --learning_rate: 学习率
# --num_train_epochs: 训练轮数
# --output_dir $SAVE_DIR: 模型保存路径
# --overwrite_output_dir: 如果输出目录存在, 则覆盖该目录
python run_glue.py \
--model_type BERT \
--model_name_or_path bert-base-uncased \
--task_name MRPC \
--do_train \
--do_eval \
--data_dir $DATA_DIR \
--max_seq_length 128 \
--learning_rate 2e-5 \
--num_train_epochs 3 \
--output_dir $SAVE_DIR \
--overwrite_output_dir
sh run_glue.sh
https://huggingface.co/join
创建一个账户transformers-cli login
# 上传模型
transformers-cli upload ./bert_finetuning_test/
# 查看上传结果
transformers-cli ls
import torch
source = "huggingface/pytorch-transformers"
part = 'tokenizer'
# 加载的预训练模型的名字, 使用自己模型的名字“username/model_name”
model_name = 'zjs/bert_finetuning_test'
tokenizer = torch.hub.load(source, part, model_name)
index = tokenizer.encode("我是谁", add_special_tokens=True)
在使用Transformers库上传和使用自己的模型时,可以遵循以下步骤。这些步骤包括模型的保存、上传到Hugging Face Model Hub以及从Model Hub加载模型。以下是详细的步骤指南:
首先,确保你已经安装了transformers
和datasets
库。如果还没有安装,可以使用以下命令进行安装:
pip install transformers datasets
假设你已经训练了一个模型,并且想要将其保存为Hugging Face格式。你可以使用以下代码来保存模型和配置文件:
from transformers import AutoModel, AutoTokenizer
# 假设你的模型和tokenizer已经准备好
model = AutoModel.from_pretrained("your_model_name")
tokenizer = AutoTokenizer.from_pretrained("your_tokenizer_name")
# 保存模型和tokenizer
model.save_pretrained("path/to/your/model")
tokenizer.save_pretrained("path/to/your/model")
如果你还没有Hugging Face账号,需要先注册一个。注册后,你会获得一个访问令牌(access token),用于上传模型。
使用Hugging Face CLI登录你的账户。首先,确保你已经安装了Hugging Face CLI:
pip install huggingface_hub
然后,使用以下命令登录:
huggingface-cli login
输入你的访问令牌(access token)完成登录。
使用transformers
库中的push_to_hub
方法将模型和tokenizer上传到Hugging Face Model Hub。你需要提供一个仓库名称(repository name),该名称将在Hugging Face上显示。
from transformers import AutoModel, AutoTokenizer
# 加载模型和tokenizer
model = AutoModel.from_pretrained("path/to/your/model")
tokenizer = AutoTokenizer.from_pretrained("path/to/your/model")
# 上传模型和tokenizer
model.push_to_hub("your-username/your-model-name")
tokenizer.push_to_hub("your-username/your-model-name")
一旦模型上传成功,你可以通过Hugging Face Model Hub加载模型。使用以下代码加载模型和tokenizer:
from transformers import AutoModel, AutoTokenizer
# 加载模型和tokenizer
model = AutoModel.from_pretrained("your-username/your-model-name")
tokenizer = AutoTokenizer.from_pretrained("your-username/your-model-name")
现在你可以在你的项目中使用这个模型了。例如,你可以使用它进行推理:
# 示例文本
text = "Hello, how are you?"
# 编码文本
inputs = tokenizer(text, return_tensors="pt")
# 获取模型输出
outputs = model(**inputs)
# 打印输出
print(outputs)
根据实际经验, 自定义为微调网络参数总数应当大于0.5倍的训练数据量, 小于10倍的训练数据量, 这样有助于模型在合理的时间范围内收敛, 如果是分类任务样本数量应当保持在1:1
import torch
import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):
"""自定义微调网络"""
def __init__(self, char_size=32, embedding_size=768):
"""
:param char_size: 输入句子中的字符数量, 输入句子规范后的长度128
:param embedding_size: 字嵌入的维度, 因为使用的bert中文模型嵌入维度是768, 因此这里也使用768
"""
super(Net, self).__init__()
self.char_size = char_size
self.embedding_size = embedding_size
# 因为是一个2分类, 所以输出为2
self.fc1 = nn.Linear(char_size * embedding_size, 2)
def forward(self, x):
# 对输入张量形状进行变换, 以满足接下来层(nn.Linear)的输入要求
x = x.view(-1, self.char_size * self.embedding_size)
# 使用一个全连接层
return self.fc1(x)
if __name__ == "__main__":
# 随机初始化参数
x = torch.randn(1, 32, 768)
net = Net()
nr = net(x)
print(nr)
"""output:
tensor([[-0.0000, 0.0000]], grad_fn=)
"""
import torch
import pandas as pd
from collections import Counter
from functools import reduce
from sklearn.utils import shuffle
from keras.api.preprocessing import sequence
source = "huggingface/pytorch-transformers"
model_name = "bert-base-chinese"
# 加载模型
model = torch.hub.load(source, "model", model_name)
# 加载字符映射
tokenizer = torch.hub.load(source, "tokenizer", model_name)
# 设定超参, 句子长度
cutlen = 32
def get_bert_encode(text):
"""
:param text: 要进行编码的中文
"""
# 首先进行字符映射对中文进行编码, 因为BERT编码后会添加101, 102的标志, 对于任务无意义, 去掉
indexed_tokens = tokenizer.encode(text[:cutlen])[1:-1]
# 使用sequence对句子进行长度规范, 长度超出了进行阶段, 长度不足进行补齐
indexed_tokens = sequence.pad_sequences([indexed_tokens], cutlen)
# 对结果进行封装
tokens_tensor = torch.LongTensor(indexed_tokens)
with torch.no_grad():
encoded_output, _ = model(tokens_tensor)
# 进行一次降维度后返回
return encoded_output[0]
def data_loader(train_data_path, valid_data_path, batch_size=32):
"""从持久化文件中加载数据
:param train_data_path: 训练数据路径
:param valid_data_path: 验证数据路径
:param batch_size: 批次大小
"""
# 使用pd进行csv的读取, 并去除第一列的列名
train_data = pd.read_csv(train_data_path, header=None, sep="\t").drop([0])
valid_data = pd.read_csv(valid_data_path, header=None, sep="\t").drop([0])
# 打印训练和验证集的正负样本数量
print("训练正负样本数量: ", Counter(train_data[0].values))
print("验证正负样本数量: ", Counter(valid_data[0].values))
# 验证数据集中的数据总数至少可以满足一个批次
if len(valid_data) < batch_size:
raise Exception("Batch size or split not match!")
def _loader_generator(data):
"""获得训练数据的批次生成器
"""
t_data = shuffle(data.values.tolist())
for batch in range(0, len(data), batch_size):
batch_encoded = []
batch_labels = []
# 首先将数据使用shuffle打乱, 将一个batch_size大小的数据转换成列表形式, 并进行逐条遍历
for item in t_data[batch: batch + batch_size]:
# 使用bert中文模型进行编码
batch_encoded.append(get_bert_encode(item[0]))
batch_labels.append([int(item[1])])
# 使用reduce高阶函数将列表中的数据转换成模型需要的张量形式
# encoded的形状是(batch_size, 2*max_len, embedding_size)
encoded = reduce(lambda x, y: torch.cat((x, y), dim=0), batch_encoded)
labels = torch.tensor(reduce(lambda x, y: x+y, batch_labels))
yield encoded, labels
# 对训练集和验证集分别使用_loader_generator函数获得批次生成器
return _loader_generator(train_data), _loader_generator(valid_data), len(train_data), len(valid_data)
import torch
import torch.optim as optim
from torch.optim import optimizer
import torch.nn as nn
from torch_test.data_loader import data_loader
from torch_test.net import Net
net = Net(32, 768)
def train(train_data_labels):
"""训练函数, 在这个过程中将更新模型参数, 并收集准确率和损失率
:param train_data_labels: 训练数据和标签的生成器对象
:return:
"""
# 定义训练过程的初始损失和准确率累加数
train_running_loss = 0.0
train_running_acc = 0.0
# 遍历循环训练数据和标签生成器, 每个批次更新一次模型参数
for train_tensor, train_labels in train_data_labels:
# 初始化该批次的优化器
optimizer.zero_grad()
# 使用微调网络获得输出
train_outputs = net(train_tensor)
# 得到该批次下的平均损失
train_loss = criterion(train_outputs, train_labels)
# 将该批次的平均损失驾到 train_running_loss中
train_running_loss+= train_loss.item()
# 损失反向传播
train_loss.backward()
# 优化器跟新模型参数
optimizer.step()
# 将该批次中正确的标签数量进行累加, 以便后续计算准确率
train_running_acc += (train_outputs.argmax(1) == train_labels).sum().item()
return train_running_loss, train_running_acc
def valid(valid_data_labels):
"""验证函数, 在这个过程中将验证模型在新数据集上的标签, 手机损失和准确率
:param valid_data_labels: 验证数据和标签的生成器对象
:return:
"""
# 定义训练过程的初始损失和准确率累加数
valid_running_loss = 0.0
valid_running_acc = 0.0
# 循环便利验证数据和标签生成器
for valid_tensor, valid_labels in valid_data_labels:
# 不自动更新梯度
with torch.no_grad():
# 使用微调网络获得输出
valid_outputs = net(valid_tensor)
# 得到该批次下的平均损失
valid_loss = criterion(valid_outputs, valid_labels)
# 将该批次的平均损失驾到 valid_running_loss中
valid_running_loss+= valid_loss.item()
# 将该批次中正确的标签数量进行累加, 以便后续计算准确率
valid_running_acc += (valid_outputs.argmax(1) == valid_labels).sum().item()
return valid_running_loss, valid_running_acc
if __name__ == '__main__':
train_data_path = ".csv"
valid_data_path = ".csv"
# 定义交叉熵损失函数
criterion = nn.CrossEntropyLoss()
# 定义SGD优化方法, 随机梯度下降, 优化器优化的参数(net.parameters()), lr学习了0.001, momentum动量学习0.9
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
# 定义训练轮数
epochs = 4
# 定义批次样本数量
batch_size = 16
# 进行指定轮次的训练
for epoch in range(epochs):
# 打印轮次
print("Epoch: ", epoch + 1)
# 通过数据加载器获得训练数据和验证数据生成器, 以及对应的样本数量
train_data_labels, valid_data_labels, train_data_len, valid_data_len = data_loader(train_data_path, valid_data_path, batch_size)
# 调用训练函数进行训练
train_running_loss, train_running_acc = train(train_data_labels)
# 调用验证函数进行验证
valid_running_loss, valid_running_acc = valid(valid_data_labels)
# 计算每一轮的平均损失, train_running_loss和valid_running_loss是每个批次的平均损失之和
# 因此将她们乘以batch_size就得到了该轮的总损失, 除以样本数即该轮次的平均损失
train_average_loss = train_running_loss * batch_size / train_data_len
valid_average_loss = valid_running_loss * batch_size / valid_data_len
# train_running_acc和valid_running_acc是每个批次的正确标签累加和, 因此只需要除以对应的样本总数就是该轮的准确率
train_average_acc = train_running_acc / train_data_len
valid_average_acc = valid_running_acc / valid_data_len
# 打印该轮次下的训练损失和准确率以及验证损失和准确率
print("Train Loss:", train_average_loss, "|", "Train Acc:", train_average_acc)
print("Valid Loss:", valid_average_loss, "|", "Valid Acc:", valid_average_acc)
print("Finished Training")
# 保存路径
MODEL_PATH = "./BERT_net.path"
# 保存模型参数
torch.save(net.state_dict(), MODEL_PATH)
print("Finished Saving")
if __name__ == "__main__":
MODEL_PATH = "./BERT_net.path"
net.load_state_dict(torch.load(MODEL_PATH))
# text = "酒店设备一般, 套房里卧室的不能上网, 要到客厅去"
text = "房间应该超过30平米, 是HK同级酒店中少有的大, 重装之后, 设备也不错"
print("输入文本为: ", text)
with torch.no_grad():
output = net(get_bert_encode(text))
# 从output中取出最大值对应的索引
print("预测标签为: ", torch.argmax(output).item())