迁移学习理论
目标
- 迁移学习中的有关概念
- 掌握迁移学习的两种方式
概念
- 预训练模型
- 微调
- 微调脚本
预训练模型(Pretrained model)
一般情况下预训练模型都是大型模型, 具备复杂的网络结构, 众多的参数量, 以及足够大的数据集进行训练而产生的模型, 在NLP领域, 预训练模型往往是语言模型, 因为语言模型是无监督的, 可以获得大量的语料, 同时语言模型优势许多经典NLP任务的基础, 如:
- 机器翻译
- 文本生成
- 阅读理解
常见预训练模型包括
- BEAR
- GPT
- roBERTa
- Transformer-XL
微调(Fine-tuning)
根据给定的预训练模型, 改变它的部分参数或者为其新增部分输出结构后, 通过小部分训练集上训练, 来使整个模型更好的使用特定任务
微调脚本(Fine-tuning script)
实现微调过程的代码文件, 这些脚本文件中, 包含对预训练模型的调用, 对微调参数的选定以及对微调结构的更改, 同时因为微调是一个训练过程, 同样需要一些超参数的设定, 以及损失函数和优化器的选取等, 因此微调脚本往往也包含了整个迁移学习的过程
说明
一般情况下, 微调脚本应该由不同的任务类型开发者自己编写, 但是由于目前研究的NLP任务类型(分类, 提取, 生成)以及对应的微调输出结构是有限的, 有些微调方式已经在很多数据集上验证是有效的, 因此微调脚本也可以使用已经完成的规范化脚本
两种迁移方式
- 直接使用预训练模型, 进行相同任务的处理, 不需要调整参数或者模型结构, 这些模型开箱即用, 但是一般这种情况只适合简单的任务, 如 fasttext工具包中预训练的词向量模型, 很多预训练模型开发者为了达到开箱即用的效果, 将模型结构分哥哥部分保存为不同的预训练模型, 提供对应的加载方法来完成特定的目标
- 通过主流的迁移学习方式是发挥预训练模型特征抽象的能力, 然后通过微调的方式通过训练更新小部分参数以此来适应不同的任务, 这种迁移方式需要提供小部分的标注数据来进行监督学习
迁移方式的说明
直接用预训练模型的方式, 已经在fasttext的词向量迁移中学习, 接下来的迁移学习实践将主要讲解微调方式进行迁移学习
NLP中的标准数据集
GLUE数据集合
- CoLA数据集
- SST-2数据集
- MRPC数据集
- STS-B数据集
- QQP数据集
- MNLI数据集
- SNLI数据集
- QNLI数据集
- RTE数据集
- WNLI数据集
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.msi1/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 zipfileTASKS = ["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" % taskurllib.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")returnassert os.path.isfile(mrpc_train_file), "Train data not found at %s" % mrpc_train_fileassert os.path.isfile(mrpc_test_file), "Test data not found at %s" % mrpc_test_filewith 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.")returndev_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!")returndef get_tasks(task_names):task_names = task_names.split(',')if "all" in task_names:tasks = TASKSelse:tasks = []for task_name in task_names:assert task_name in TASKS, "Task %s not found!" % task_nametasks.append(task_name)return tasksdef 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:]))
QQP
- dev.tsv: 验证集
- test.tsv: 测试集
- train.tsv: 训练集
- original/
其中训练集(train.tsv)和测试集(test.tsv)都是带标签的数据, test.tsv是不带标签的
任务类型
- 句子对二分类任务
- 评估指标为 ACC/F1
MNLI数据集
- dev_matched.tsv: 验证集
- dev_mismatched.tsv: 验证集
- original/
- test_matched.tsv: 测试集
- test_mismatched.tsv: 测试集
- train.tsv: 训练集合
说明
matched: 代表与训练集一同采集的
mismatched: 代表与训练集分开采集的
任务类型
- 句子分类任务
- 评估指标为 ACC
QNCL/RTE/WNLI 数据集样式
- dev.tsv: 验证集
- test.tsv: 测试集
- train.tsv: 训练集
NLP中常用的预训练模型
目标
- 了解当下NLP流行的预训练模型
- 掌握如何加载和使用预训练模型
流行的预训练模型
- BERT
- GPT
- GPT-2
- Transformer-XL
- XLNet
- XLM
- RoBerta
- DistiBERT
- ALBERT
- T5
- XLM-RoBERTa
BEAR变体
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
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 | 在简体和繁体中文文本上进行训练而得到 |
GPT
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
openai-gpt | 12 | 768 | 12 | 110M | OpenAI在英文语料上进行训练得到 |
GPT2及其变体
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
gpt2 | 12 | 768 | 12 | 117M | OpenAI GPT-2英文语料上进行训练得到 |
gpt2-xl | 48 | 1600 | 25 | 1558M | 在大型的OpenAI GPT-2英文语料上进行训练得到 |
Transformer-XL
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
transfo-xl-wt103 | 18 | 1024 | 16 | 257M | 在wikitext-103英文语料进行训练得到 |
XLNet及其变体
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
xlnet-base-cased | 12 | 768 | 12 | 110M | 在英文文本上进行训练得到 |
xlnet-large-cased | 24 | 1024 | 16 | 240M | 在英文文本上进行训练得到 |
XLM
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
xlm-mlm-en-2048 | 12 | 2048 | 12 | 110M | 在英文文本上进行训练得到 |
RoBERTa及其变体
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
roberta-base | 12 | 768 | 12 | 125M | 在英文文本上进行训练得到 |
roberta-large | 24 | 1024 | 16 | 355M | 在英文文本上进行训练得到 |
DistilBERT及其变体
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
distilbert-base-uncased | 6 | 768 | 12 | 66M | 在英文文本上进行训练得到 |
distilbert-base-multilingual-cased | 6 | 768 | 12 | 66M | 基于bert-base-multilingual-uncased蒸馏(压缩)模型 |
ALBERT及其变体
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
albert-base-v1 | 12 | 768 | 12 | 110M | 在英文文本上进行训练得到 |
albert-base-v2 | 12 | 768 | 12 | 110M | 在英文文本上进行训练得到,比v1花费更多时间和数据量 |
T5及其变体
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
t5-small | 6 | 512 | 8 | 60M | 在c4语料上进行训练而得到 |
t5-base | 12 | 768 | 12 | 220M | 在c4语料上进行训练而得到 |
t5-large | 24 | 1024 | 16 | 770M | 在c4语料上进行训练而得到 |
XLM-RoBERTa及其变体
名称 | 隐藏层 | 张量维度 | 注意力头 | 参数 | 说明 |
---|---|---|---|---|---|
xlm-roberta-base | 12 | 768 | 12 | 125M | 在2.5TB的100种语言文本上进行训练得到 |
xlm-roberta-large | 24 | 1027 | 16 | 355M | 在2.5TB的100种语言文本上进行训练得到 |
说明
所有的预训练模型及其变体都是以transformers
为基础, 只是在模型结构如神经元连接方式, 编码器隐层数, 多头注意力头数发生改变, 这些改变是依据标注顺聚集上的表现而定的, 对于使用者需要在自己处理的目标数据上, 尽量遍历所有可用的模型达到最优效果即可
加载和使用预训练模型
目标
- 了解加载和使用预训练模型的工具
- 掌握加载和使用预训练模型的过程
工具
- 使用torch.hub进行模型的加载和使用
- 预训练模型使用huggingface
数据集准备事项
数据格式统一化
-
结构化字段对齐
- 示例:对话场景需统一字段为
[{"role":"user","content":...},{"role":"assistant","content":...}]
,避免混合使用speaker/listener
等不同标签 - 解决:通过正则表达式或模板引擎强制格式标准化
- 示例:对话场景需统一字段为
-
编码与符号规范
- 实际痛点:混合全角/半角符号(如用户输入"123" vs “123”)、特殊占位符(
<br>
与\n
混用) - 方案:统一转换为UTF-8编码,使用
unicodedata.normalize
标准化字符
- 实际痛点:混合全角/半角符号(如用户输入"123" vs “123”)、特殊占位符(
样本比例控制
-
类别平衡策略
- 分类任务:当存在长尾分布时(如欺诈检测正样本仅占1%),采用动态过采样(SMOTE)与困难样本挖掘结合
- 生成任务:对话数据中需控制正/负样本比例(如用户有效提问 vs 无意义输入建议7:3)
-
多任务数据配比
- 实际案例:客服系统需同时处理FAQ问答(60%)、工单分类(30%)、闲聊(10%),需按业务优先级分配比例
- 风险:任务间样本量差异过大(如9:1)易导致模型偏向主导任务
数据清洗重点
-
低质量数据过滤
- 检测指标:文本重复率(使用MinHash去重)、语句通顺度(基于语言模型困惑度过滤)
- 典型场景:爬取的网页数据中含广告文本("点击这里>>>"类噪声)
-
领域适配性筛选
- 医疗领域:剔除含娱乐八卦的通用语料,保留专业文献/问诊记录
- 技巧:使用TF-IDF计算文本与目标领域的余弦相似度阈值过滤
数据增强技巧
-
语义保留型增强
- 同义词替换:使用专业词库(如UMLS医疗词表)避免通用词替换导致的语义偏移
- 句式改写:基于T5模型生成语法结构变化但语义一致的句子
-
对抗样本注入
- 实际应用:在客服场景中人工添加10%的模糊查询(如"那个东西怎么用?")
- 效果验证:通过BadCase分析持续迭代增强策略
样本复杂度分布
-
文本长度分层
- 短文本(<50字):适用于意图识别
- 长文本(>500字):用于文档摘要任务
- 实践建议:按3:5:2比例混合短/中/长样本,提升模型鲁棒性
-
难度渐进训练
- 分阶段策略:
阶段1:清晰明确的指令样本(“请翻译这句话:…”)
阶段2:含干扰信息的复杂指令(“虽然可能不太对,但能否帮我翻译…”)
- 分阶段策略:
验证集构建原则
-
时间敏感性划分
- 金融领域:严格按时间划分(如用2023年数据训练,2024年Q1数据验证)
- 防止数据泄漏:确保验证集包含近期涌现的新术语/事件
-
领域覆盖度验证
- 医疗子领域验证集应包含:问诊对话(40%)、检查报告(30%)、学术论文(30%)
- 使用KL散度检测训练/验证集分布差异
工程化实践要点
-
数据版本控制
- 示例:使用DVC工具记录每次数据变更(如v1.2-新增对抗样本200条)
- 元数据记录:标注数据来源(人工标注/爬取/第三方购买)、清洗规则版本
-
低资源场景优化
- 当标注数据<1000条时:
- 优先使用Prompt-based微调
- 采用LoRA等参数高效微调方法
- 使用RAG增强生成效果
- 当标注数据<1000条时:
典型错误案例:某电商客服系统直接使用通用语料微调,未过滤"请给五星好评"类诱导性文本,导致生成结果频繁出现违规话术。解决方案:构建领域敏感词库进行二次过滤,并加入合规性负样本。
通过以上多维度的数据质量控制,可使模型微调成功率提升40%以上(实际项目验证数据)。建议每轮迭代后使用LIME等可解释性工具分析数据影响。
加载和使用预训练模型的步骤
1. 确定需要加载的预训练模型并安装依赖包
- 确定可以使用哪些模型
- 以中文文本任务为例,
bert-base-chinese
- 安装依赖的包
pip install tqdm boto3 requests regex sentencepiece scremoses
2. 加载预训练模型的映射器tokenizer
import torch# 预训练模型来源, 几乎固定的写法
source = "huggingface/pytorch-transformers"
# 选定加载模型哪一个部分, 这里是映射器
part = "tokenizer"
# 加载的预训练模型的名字
model_name = "bert-base-chinese"
# 只要是上面提到的常用模型都可以通过`torch.hub`来加载使用
tokenizer = torch.hub.load(source, part, model_name)
3. 加载带/不带头(多头注意力)的预训练模型
- 加载预训练模型时我们可以选择带头和不带头的模型
- 这里的‘头’是指模型的任务输出层, 选择加载不带头的模型, 相当于使用模型对输入文本进行特征表示
- 选择加载带头的模型时, 有三种类型的‘头’可以选择
- modelWithLMHead(模型语言头)
- modelForSequenceClassification(分类模型头)
- modelForQuestionAnswering(问答模型头)
- 不同的头, 可以使预训练模型输出指定的张量维度, 比如使用’分类模型头’, 则输出尺寸为(1, 2)的张量, 用于进行分类任务判定结果
# 不带头的模型
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)
4. 使用模型获得输出
使用不带头的模型进行输出
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 torchinput_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研究机构向我们提供了针对GLUE数据集合任务类型的微调脚本, 这些微调脚本的核心都是微调模型的最后一个全连接层(对于大多数的fine-tuning对于原始模型的架构大都是不动的. 只是针对最后(1~2)或者少数的3层进行改变)
- 通过间的参数配置来指定GLUE中存在任务类型(如CoLA对应的文本二分类, MRPC对应句子对文本二分类, STS-B对应句子文本多分类), 以及指定需要微调的预训练模型
指定任务类型的微调脚本使用步骤(Torch)
1. 下载微调脚本文件
# 克隆huggingface的transformers文件
git clone https://github.com/huggingface/transformers.git# 进行transformers安装
cd transformers && pip install . # 进入微调脚本所需要的路径并查看
cd examples && ls # 启动 run_glue.py 就是针对 GLUE 数据集合微调脚本
2. 配置微调脚本参数
# 定义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
3. 运行并检验效果
sh run_glue.sh
4. 上传并使用模型
- 在
https://huggingface.co/join
创建一个账户 - 在服务器使用transformers-cli登陆
transformers-cli login
- 使用transformers-cli上传模型并查看
# 上传模型 transformers-cli upload ./bert_finetuning_test/ # 查看上传结果 transformers-cli ls
- 使用pytorch.hub加载模型进行使用
import torchsource = "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)
指定任务类型的微调脚本使用步骤(Transformer)
在使用Transformers库上传和使用自己的模型时,可以遵循以下步骤。这些步骤包括模型的保存、上传到Hugging Face Model Hub以及从Model Hub加载模型。以下是详细的步骤指南:
1. 安装必要的库
首先,确保你已经安装了transformers
和datasets
库。如果还没有安装,可以使用以下命令进行安装:
pip install transformers datasets
2. 保存模型
假设你已经训练了一个模型,并且想要将其保存为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")
3. 注册Hugging Face账号
如果你还没有Hugging Face账号,需要先注册一个。注册后,你会获得一个访问令牌(access token),用于上传模型。
4. 登录Hugging Face
使用Hugging Face CLI登录你的账户。首先,确保你已经安装了Hugging Face CLI:
pip install huggingface_hub
然后,使用以下命令登录:
huggingface-cli login
输入你的访问令牌(access token)完成登录。
5. 上传模型
使用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")
6. 从Hugging Face Model Hub加载模型
一旦模型上传成功,你可以通过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")
7. 使用模型
现在你可以在你的项目中使用这个模型了。例如,你可以使用它进行推理:
# 示例文本
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 Fclass 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_sizeself.embedding_size = embedding_size# 因为是一个2分类, 所以输出为2self.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=<AddmmBackward>)"""
构建训练与验证数据集批次生成器
import torch
import pandas as pd
from collections import Counter
from functools import reduce
from sklearn.utils import shuffle
from keras.api.preprocessing import sequencesource = "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 = 32def 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 nnfrom torch_test.data_loader import data_loader
from torch_test.net import Netnet = Net(32, 768)def train(train_data_labels):"""训练函数, 在这个过程中将更新模型参数, 并收集准确率和损失率:param train_data_labels: 训练数据和标签的生成器对象:return:"""# 定义训练过程的初始损失和准确率累加数train_running_loss = 0.0train_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_accdef valid(valid_data_labels):"""验证函数, 在这个过程中将验证模型在新数据集上的标签, 手机损失和准确率:param valid_data_labels: 验证数据和标签的生成器对象:return:"""# 定义训练过程的初始损失和准确率累加数valid_running_loss = 0.0valid_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_accif __name__ == '__main__':train_data_path = ".csv"valid_data_path = ".csv"# 定义交叉熵损失函数criterion = nn.CrossEntropyLoss()# 定义SGD优化方法, 随机梯度下降, 优化器优化的参数(net.parameters()), lr学习了0.001, momentum动量学习0.9optimizer = 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_lenvalid_average_loss = valid_running_loss * batch_size / valid_data_len# train_running_acc和valid_running_acc是每个批次的正确标签累加和, 因此只需要除以对应的样本总数就是该轮的准确率train_average_acc = train_running_acc / train_data_lenvalid_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())