GPT2训练自己的对话问答机器人
Posted 我是小z呀
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了GPT2训练自己的对话问答机器人相关的知识,希望对你有一定的参考价值。
GPT2训练自己的对话问答机器人
1.环境搭建
这里我搭建了虚拟的3.6环境
conda create -n gpt python=3.6
conda activate gpt
conda install pytorch==1.7.0 torchvision==0.8.0 torchaudio==0.7.0 cudatoolkit=11.0 -c pytorch
pip install transformers==4.4.2 -i https://pypi.python.org/simple
pip install tensorboard
pip uninstall sklearn
pip install scikit-learn -i https://pypi.tuna.tsinghua.edu.cn/simple/
pip install pandas
pip install matplotlib
pip install jieba
2.理论研究
基于GPT2的中文闲聊机器人,模型实现基于HuggingFace的transformers ,精读GPT2-Chinese的论文和代码,获益匪浅。
3.模型训练与测试
data/train.txt:默认的原始训练集文件,存放闲聊语料;data/train.pkl:对原始训练语料进行tokenize之后的文件,存储一个list对象,list的每条数据表示一个多轮对话,表示一条训练数据
model:存放对话生成的模型;epoch40:经过40轮训练之后得到的模型,config.json:模型参数的配置文件;pytorch_model.bin:模型文件
vocab/vocab.txt:字典文件。默认的字典大小为13317,若需要使用自定义字典,需要将confog.json文件中的vocab_size字段设为相应的大小。
sample:存放人机闲聊生成的历史聊天记录
3.1语料tokenize
运行preprocess.py:数据预处理代码。
from tokenizers import BertWordPieceTokenizer
from transformers import BertTokenizer
from transformers import BertTokenizerFast
import argparse
import pandas as pd
import pickle
import jieba.analyse
from tqdm import tqdm
from transformers import GPT2TokenizerFast, GPT2LMHeadModel
import logging
import numpy as np
def create_logger(log_path):
"""
将日志输出到日志文件和控制台
"""
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
formatter = logging.Formatter(
'%(asctime)s - %(levelname)s - %(message)s')
# 创建一个handler,用于写入日志文件
file_handler = logging.FileHandler(
filename=log_path)
file_handler.setFormatter(formatter)
file_handler.setLevel(logging.INFO)
logger.addHandler(file_handler)
# 创建一个handler,用于将日志输出到控制台
console = logging.StreamHandler()
console.setLevel(logging.DEBUG)
console.setFormatter(formatter)
logger.addHandler(console)
return logger
def preprocess():
"""
对原始语料进行tokenize,将每段对话处理成如下形式:"[CLS]utterance1[SEP]utterance2[SEP]utterance3[SEP]"
"""
# 设置参数
parser = argparse.ArgumentParser()
parser.add_argument('--vocab_path', default='vocab/vocab.txt', type=str, required=False,
help='词表路径')
parser.add_argument('--log_path', default='data/preprocess.log', type=str, required=False, help='训练日志存放位置')
parser.add_argument('--train_path', default='data/train.txt', type=str, required=False, help='训练日志存放位置')
parser.add_argument('--save_path', default='data/train.pkl', type=str, required=False, help='tokenize的训练数据集')
args = parser.parse_args()
# 初始化日志对象
logger = create_logger(args.log_path)
# 初始化tokenizer
tokenizer = BertTokenizerFast(vocab_file=args.vocab_path, sep_token="[SEP]", pad_token="[PAD]", cls_token="[CLS]")
sep_id = tokenizer.sep_token_id
cls_id = tokenizer.cls_token_id
logger.info("preprocessing data,data path:, save path:".format(args.train_path, args.save_path))
# 读取训练数据集
with open(args.train_path, 'rb') as f:
data = f.read().decode("utf-8")
# 需要区分linux和windows环境下的换行符
if "\\r\\n" in data:
train_data = data.split("\\r\\n\\r\\n")
else:
train_data = data.split("\\n\\n")
logger.info("there are dialogue in dataset".format(len(train_data)))
# 开始进行tokenize
# 保存所有的对话数据,每条数据的格式为:"[CLS]utterance1[SEP]utterance2[SEP]utterance3[SEP]"
dialogue_len = [] # 记录所有对话tokenize之后的长度,用于统计中位数与均值
dialogue_list = []
with open(args.save_path, "w", encoding="utf-8") as f:
for index, dialogue in enumerate(tqdm(train_data)):
if "\\r\\n" in data:
utterances = dialogue.split("\\r\\n")
else:
utterances = dialogue.split("\\n")
input_ids = [cls_id] # 每个dialogue以[CLS]开头
for utterance in utterances:
input_ids += tokenizer.encode(utterance, add_special_tokens=False)
input_ids.append(sep_id) # 每个utterance之后添加[SEP],表示utterance结束
dialogue_len.append(len(input_ids))
dialogue_list.append(input_ids)
len_mean = np.mean(dialogue_len)
len_median = np.median(dialogue_len)
len_max = np.max(dialogue_len)
with open(args.save_path, "wb") as f:
pickle.dump(dialogue_list, f)
logger.info("finish preprocessing data,the result is stored in ".format(args.save_path))
logger.info("mean of dialogue len:,median of dialogue len:,max len:".format(len_mean, len_median, len_max))
if __name__ == '__main__':
preprocess()
3.2用GPT2训练数据
运行train.py,使用预处理后的数据,对模型进行自回归训练,模型保存在根目录下的model文件夹中。
import argparse
import math
import time
import torch
import torch.nn.functional as F
import torch.optim as optim
import logging
from datetime import datetime
import os
from torch.utils.data import Dataset, DataLoader
from os.path import join, exists
from torch.nn import CrossEntropyLoss
from tqdm import tqdm
from torch.nn import DataParallel
import transformers
import pickle
import sys
from pytorchtools import EarlyStopping
from sklearn.model_selection import train_test_split
from data_parallel import BalancedDataParallel
from transformers import GPT2TokenizerFast, GPT2LMHeadModel, GPT2Config
from transformers import BertTokenizerFast
import pandas as pd
import torch.nn.utils.rnn as rnn_utils
import numpy as np
from dataset import MyDataset
def set_args():
parser = argparse.ArgumentParser()
parser.add_argument('--device', default='3', type=str, required=False, help='设置使用哪些显卡')
parser.add_argument('--no_cuda', action='store_true', help='不使用GPU进行训练')
parser.add_argument('--vocab_path', default='vocab/vocab.txt', type=str, required=False,
help='词表路径')
parser.add_argument('--model_config', default='config/config.json', type=str, required=False,
help='设置模型参数')
parser.add_argument('--train_path', default='data/train.pkl', type=str, required=False, help='训练集路径')
parser.add_argument('--max_len', default=150, type=int, required=False, help='训练时,输入数据的最大长度')
parser.add_argument('--log_path', default='data/train.log', type=str, required=False, help='训练日志存放位置')
parser.add_argument('--log', default=True, help="是否记录日志")
parser.add_argument('--ignore_index', default=-100, type=int, required=False, help='对于ignore_index的label token不计算梯度')
# parser.add_argument('--input_len', default=200, type=int, required=False, help='输入的长度')
parser.add_argument('--epochs', default=15, type=int, required=False, help='训练的最大轮次')
parser.add_argument('--batch_size', default=4, type=int, required=False, help='训练的batch size')
parser.add_argument('--gpu0_bsz', default=10, type=int, required=False, help='0号卡的batch size')
parser.add_argument('--lr', default=2.6e-5, type=float, required=False, help='学习率')
parser.add_argument('--eps', default=1.0e-09, type=float, required=False, help='衰减率')
parser.add_argument('--log_step', default=1, type=int, required=False, help='多少步汇报一次loss')
parser.add_argument('--gradient_accumulation_steps', default=4, type=int, required=False, help='梯度积累')
parser.add_argument('--max_grad_norm', default=2.0, type=float, required=False)
parser.add_argument('--save_model_path', default='model', type=str, required=False,
help='模型输出路径')
parser.add_argument('--pretrained_model', default='', type=str, required=False,
help='预训练的模型的路径')
# parser.add_argument('--seed', type=int, default=None, help='设置种子用于生成随机数,以使得训练的结果是确定的')
parser.add_argument('--num_workers', type=int, default=0, help="dataloader加载数据时使用的线程数量")
parser.add_argument('--patience', type=int, default=0, help="用于early stopping,设为0时,不进行early stopping.early stop得到的模型的生成效果不一定会更好。")
parser.add_argument('--warmup_steps', type=int, default=4000, help='warm up步数')
# parser.add_argument('--label_smoothing', default=True, action='store_true', help='是否进行标签平滑')
parser.add_argument('--val_num', type=int, default=8000, help='验证集大小')
args = parser.parse_args()
return args
def create_logger(args):
"""
将日志输出到日志文件和控制台
"""
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
formatter = logging.Formatter(
'%(asctime)s - %(levelname)s - %(message)s')
# 创建一个handler,用于写入日志文件
file_handler = logging.FileHandler(
filename=args.log_path)
file_handler.setFormatter(formatter)
file_handler.setLevel(logging.INFO)
logger.addHandler(file_handler)
# 创建一个handler,用于将日志输出到控制台
console = logging.StreamHandler()
console.setLevel(logging.DEBUG)
console.setFormatter(formatter)
logger.addHandler(console)
return logger
def collate_fn(batch):
input_ids = rnn_utils.pad_sequence(batch, batch_first=True, padding_value=0)
labels = rnn_utils.pad_sequence(batch, batch_first=True, padding_value=-100)
return input_ids, labels
# def padding_batch(data_list, pad_id):
# """
# 使用pad_id将data_list的每条数据,填充至data_list中最长的长度
# :param data_list:
# :param pad_id:
# :return:
# """
# # 统计data_list中的最大长度
# max_len = 0
# for data in data_list:
# max_len = max_len if max_len > len(data) else len(data)
#
# # 对数据进行padding
# new_data_list = []
# for data in data_list:
# new_data = data + [pad_id] * (max_len - len(data))
# new_data_list.append(new_data)
# return new_data_list
def load_dataset(logger, args):
"""
加载训练集和验证集
"""
logger.info("loading training dataset and validating dataset")
train_path = args.train_path
with open(train_path, "rb") as f:
input_list = pickle.load(f)
# 划分训练集与验证集
val_num = args.val_num
input_list_train = input_list[val_num:]
input_list_val = input_list[:val_num]
# test
# input_list_train = input_list_train[:24]
# input_list_val = input_list_val[:24]
train_dataset = MyDataset(input_list_train, args.max_len)
val_dataset = MyDataset(input_list_val, args.max_len)
return train_dataset, val_dataset
def train_epoch(model, train_dataloader, optimizer, scheduler, logger,
epoch, args):
model.train()
device = args.device
# pad_id = args.pad_id
# sep_id = args.sep_id
ignore_index = args.ignore_index
epoch_start_time = datetime.now()
total_loss = 0 # 记录下整个epoch的loss的总和
# epoch_correct_num:每个epoch中,output预测正确的word的数量
# epoch_total_num: 每个epoch中,output预测的word的总数量
epoch_correct_num, epoch_total_num = 0, 0
for batch_idx, (input_ids, labels) in enumerate(train_dataloader):
# 捕获cuda out of memory exception
try:
input_ids = input_ids.to(device)
labels = labels.to(device)
outputs = model.forward(input_ids, labels=labels)
logits = outputs.logits
loss = outputs.loss
loss = loss.mean()
# 统计该batch的预测token的正确数与总数
batch_correct_num, batch_total_num = calculate_acc(logits, labels, ignore_index=ignore_index)
# 统计该epoch的预测token的正确数与总数
epoch_correct_num += batch_correct_num
epoch_total_num += batch_total_num
# 计算该batch的accuracy
batch_acc = batch_correct_num / batch_total_num
total_loss += loss.item()
if args.gradient_accumulation_steps > 1:
loss = loss / args.gradient_accumulation_steps
loss.backward()
# 梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), args.max_grad_norm)
# 进行一定step的梯度累计之后,更新参数
if (batch_idx + 1) % args.gradient_accumulation_steps == 0:
# 更新参数
optimizer.step()
# 更新学习率
scheduler.step()
# 清空梯度信息
optimizer.zero_grad()
if (batch_idx + 1) % args.log_step == 0:
logger.info(
"batch of epoch , loss , batch_acc , lr ".format(
batch_idx + 1, epoch + 1, loss.item() * args.gradient_accumulation_steps, batch_acc, scheduler.get_lr()))
del input_ids, outputs
except RuntimeError as exception:
if "out of memory" in str(exception):
logger.info("WARNING: ran out of memory")
if hasattr(torch.cuda, 'empty_cache'):
torch.cuda.empty_cache()
else:
logger.info(str(exception))
raise exception
# 记录当前epoch的平均loss与accuracy
epoch_mean_loss = total_loss / len(train_dataloader)
epoch_mean_acc = epoch_correct_num / epoch_total_num
logger.info(
"epoch : loss , predict_acc ".format(epoch + 1, epoch_mean_loss, epoch_mean_acc))
# save model
logger.info('saving model for epoch '.format(epoch + 1))
model_path = join(args.save_model_path, 'epoch'.format(epoch + 1))
if not os.path.exists(model_path):
os.mkdir(model_path)
model_to_save = model.module if hasattr(model, 'module') else model
model_to_save.save_pretrained(model_path)
logger.info('epoch finished'.format(epoch + 1))
epoch_finish_time = datetime.now()
logger.infoNLP中的对话机器人——预训练基准模型
引言
本文是七月在线《NLP中的对话机器人》的视频笔记,主要介绍FAQ问答型聊天机器人的实现。
场景二
上篇文章中我们解决了给定一个问题和一些回答,从中找到最佳回答的任务。
在场景二中,我们来实现: 给定新问题,从问答库中找到能回答该问题的最佳答案。
那怎么实现呢? 一种方案是直接匹配问题和答案,计算一个匹配分数,选出匹配分数最高的。
但是基于这些数据集上,这种方法的效果不佳。
另一种做法是类似相似问题检索,从问答库中找到给定问题的最相似的问题,用它的答案作为给定问题的答案。
那如何计算问题之间的相似度呢?
我们这里尝试两个简单的基准模型,分别是基于ELMo和BERT的语句表示,得到问题的句向量,然后通过余弦相似度来计算问题间的相似得分。
我们这里使用现成的ELMo和BERT模型。
ELMo基准模型
数据集
https://pan.baidu.com/s/1z1Rnnk-ubRSvzDu4UvLlIw
实现
可以使用哈工大的预训练的ELMo模型, 仓库为: https://github.com/HIT-SCIR/ELMoForManyLangs
选择简体中文版,可能仓库中的链接打不开,可以从百度云: https://pan.baidu.com/s/1RNKnj6hgL-2orQ7f38CauA?errno=0&errmsg=Aut 下载。
下载并解压之后,我们需要修改config.json
当中的config_path
参数,改成:configs/cnn_50_100_512_4096_sample.json
,即去掉前面的../
。
还要安装ELMoForManyLangs的包:
git clone https://github.com/HIT-SCIR/ELMoForManyLangs
cd ELMoForManyLangs
python setup.py install
在安装分词工具pkuseg
。
上面步骤都做好之后,我们进行测试,看能否正常工作:
from elmoformanylangs import Embedder
import pkuseg
# 得到我们的elmo encoder
e = Embedder('zhs.model')
sents = ["今天天气真好啊",
"潮水退了就知道谁没穿裤子"]
seg = pkuseg.pkuseg()
sents = [seg.cut(sent) for sent in sents]
print(sents)
embeddings = e.sents2elmo(sents)
print(embeddings)
测试失败了吗,报错:
TypeError: Highway.forward: return type `<class 'torch.Tensor'>` is not a `<class 'NoneType'>`.
解决:
pip uninstall -y overrides
pip install overrides==3.1.0
再次运行,结果:
[['今天', '天气', '真', '好', '啊'], ['潮水', '退', '了', '就', '知道', '谁', '没', '穿', '裤子']]
[array([[ 0.07444969, 0.12170488, 0.18306534, ..., -0.27513036,
0.03082917, -0.6021218 ],
[-0.17950936, 0.3002583 , 0.41858768, ..., -0.7238658 ,
1.5381049 , -1.1729732 ],
[ 0.2640982 , 0.53036195, 0.6604749 , ..., 0.12223349,
0.5540275 , -0.50095123],
[ 0.5308723 , 0.05570847, 0.09236425, ..., -0.9979615 ,
0.13710646, -0.33056557],
[ 0.22956477, 0.43202046, -0.42210087, ..., -0.9834735 ,
0.0821436 , -0.15420763]], dtype=float32), array([[ 0.04015709, 0.42856467, 0.6768381 , ..., -0.10523877,
0.48780942, 0.00312402],
[ 0.5922051 , 0.21770744, 1.1809255 , ..., -0.12190163,
0.61034995, 0.1050753 ],
[ 0.614235 , -0.04580465, -0.03073702, ..., 0.34837866,
0.5443483 , 0.02076633],
...,
[ 0.76723504, 1.413464 , 1.071545 , ..., -1.464156 ,
0.5184709 , -0.5683145 ],
[ 0.13504069, 0.44970334, 0.8248094 , ..., -0.17855829,
-0.39308357, 0.24178888],
[ 0.44061318, 0.6780157 , -0.07952423, ..., -1.2203757 ,
0.14167261, 0.3741262 ]], dtype=float32)]
正常运行,下面可以开始编码了。
首先引入相关包:
import os
import pickle
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import pandas as pd
import pkuseg
from elmoformanylangs import Embedder # 通过上面的python setup.py install安装
目录结构如下:
具体实现非常简单,只需要把分词后的句子列表传给ELMo模型,就可以得到每个单词的词向量。
为了得到句向量,这里我们采用对句子中每个单词向量求均值的方法。
e = Embedder('zhs.model') # EMLo模型
seg = pkuseg.pkuseg() # 分词工具
# 如果第一次执行(较耗时)
if not os.path.exists("embeddings.pkl"):
train_df = pd.read_csv("financezhidao_filter.csv").sample(frac=0.1) # 只取10%的数据量
# 取最佳答案中的tile和reply字段
candidates = train_df[train_df["is_best"] == 1][["title", "reply"]]
candidate_title = candidates["title"].tolist()
candidate_reply = candidates["reply"].tolist()
# 对title分词
titles = [seg.cut(title) for title in candidates["title"]]
# 计算title中单词词向量
embeddings = e.sents2elmo(titles)
# list of numpy arrays, each array with shape [seq_len * 1024]
# code.interact(local=locals())
# 对seq_len中的单词取平均,这样不管句子长度多少,都可以得到维度一致的句向量
# 除了取平均还可以:求和/取最大值等
candidate_embeddings = [np.mean(embedding, 0) for embedding in embeddings] # a list of 1024 dimensional vectors
# 保存嵌入
with open("embeddings.pkl", "wb") as f:
pickle.dump([candidate_title, candidate_reply, candidate_embeddings], f)
print("Save embeddings to embeddings.pkl")
else:
with open("embeddings.pkl", "rb") as f:
candidate_title, candidate_reply, candidate_embeddings = pickle.load(f)
print("Load embeddings success")
我们可以先为问答库中的问题计算好句向量,然后保存下来,下次直接加载就可以了。
当有了新问题进来,我们只需要按照相同的方法计算新问题的句向量,然后与问答库中的问题计算余弦相似度。
while True:
title = input("你的问题是?\\n > ")
if len(title.strip()) == 0:
continue
title = [seg.cut(title.strip())]
title_embedding = [np.mean(e.sents2elmo(title)[0], 0)] # 得到了新问题的ELMo embedding
scores = cosine_similarity(title_embedding, candidate_embeddings)[0]
# 得到得分最高的5个问题对应的索引
top5_indices = scores.argsort()[-5:][::-1]
print("[得分] 参考问题 对应答案")
for index in top5_indices:
print(f"[scores[index]] candidate_title[index] 对应答案:candidate_reply[index]\\n")
运行效果:
你的问题是?
> 如何发财
2023-02-26 15:07:57,114 INFO: 1 batches, avg len: 4.0
[得分] 参考问题 对应答案
[0.6191050410270691] 如何去理财,教我如何理财 对应答案:欢迎关注招行理财,招行有储蓄、大额存单、基金、理财产品、外汇、黄金、白银等投资可供您选择。风险和收益基本成正比,要求保本就选择储蓄,追求低风险可以考虑货币基金和低风险的理财产品,追求高收益可以了解投资型基金、外汇,黄金及白银,若您当地有招行,可以联系网点客户经理交流理财事宜。
[0.6009083390235901] 100万年轻人现在如何理财 对应答案:要是这100万是你所有的资产、建议你分散投资,可以选择三种不同的风险投资。1、低风险占40%,收益在5%左右一年利息两万左右,可以把钱做银行理财或买国债。2、中风险占50%,收益在11%左右一年利息五六万,可以选择一些靠谱的P2P网贷。3、高风险占10%,收益20%多甚至更高,可以做一些股票、期货类的。要么就找个有升值空间的三、四线城市或海边投资套房子,剩下的理财。
[0.5920484066009521] 聪明女人应该怎样理财 对应答案:所在城市若有招商银行,可了解下招行发售的理财产品,首次购买理财产品,需先办理风险评估,评估后,可购买对应您的风险承受能力等级的理财产品。您可以进入招行主页,点击“理财产品”-“个人理财产品”页面查看,也可通过“搜索”分类您需要的理财产品。温馨提示:购买之前请详细阅读产品说明书。
[0.58339524269104] 年轻人应该如何理财? 对应答案:目前招行个人投资理财方式较多:定期、国债、受托理财、基金、黄金等做组合投资,不同产品的投资起点不一,对应的风险级别也不相同。建议您可以到我行网点咨询理财经理的相关建议。
[0.5725947022438049] 手头几万闲钱如何理财最靠谱 对应答案:欢迎关注招行理财,招行有储蓄、大额存单、基金、理财产品、外汇、黄金、白银等投资可供您选择。风险和收益基本成正比,要求保本就选择储蓄,追求低风险可以考虑货币基金和低风险的理财产品,追求高收益可以了解投资型基金、外汇,黄金及白银,若您当地有招行,可以联系网点客户经理交流理财事宜。
你的问题是?
> 我要如何借钱
2023-02-26 15:08:40,206 INFO: 1 batches, avg len: 6.0
[得分] 参考问题 对应答案
[0.8377593159675598] 我是说要怎样挂失 对应答案:如果是招商银行一卡通或信用卡,都可以致电相应的客服热线,进入人工挂失。也可以通过网上银行挂失。
[0.8101509213447571] 我可以借钱吗 对应答案:建议您通过正规平台咨询办理。若在招行申请个人贷款,目前规定贷款人的年龄需年满18周岁,且年龄+贷款年限不得超过70岁。未成年人如需申请贷款,可尝试由父母(或一方)作为共同贷款申请人;具体能否办理,建议您联系当地网点的个贷部门详询。
[0.8072128295898438] 我要还款怎么搞 对应答案:若是招行信用卡,我行信用卡还款有多种方式,如通过自助存款机存现、网点柜面还款,网络还款、便利店还款等,您可以登录以下网址查看我行的还款方式介绍,以选择适合您的方式(网址:http://cc.cm####na.com/Cu######ce/Re#####nt.aspx)。
[0.7984545230865479] 我想贷你能? 对应答案:所在城市若有招商银行,可通过招行尝试申请贷款,由于各贷款项目所需条件及申请材料有所不同,请您在8:30-18:00致电9####选择3个人客户服务-3-3-8进入人工服务提供贷款用途及城市详细了解所需资料。贷款申请是否通过,请以经办行个贷部门的综合审核结果为准。
[0.7939909100532532] 我要贷款谁能帮我 对应答案:若您所在城市有招行,可通过招行尝试申请贷款,请您在8:30-18:00致电9####选择“2人工服务-“1”个人银行业务-“4”个人贷款业务进入人工服务提供贷款用途及城市详细了解所需资料。
你的问题是?
>
可以看到,我们没有进行任何训练,就可以得到一个效果还行的模型,我们可以利用分数排名来计算mean receiprocal rank。
然后基于此,去实现一个比基准模型更好的模型。
下面,我们来看如何实现BERT基准模型。
BERT基准模型
与ELMo模型类似,我们可以用BERT来对句子进行编码。BERT句向量表示有多种方式,比如使用[CLS]
的向量表示句向量,但在这里效果不好,推荐和ELMo一样,才有句子中单词的平均值。
数据集
下载地址: https://pan.baidu.com/s/18Lwq16VBo6wBD_qLb3i33g
这次我们使用法律知道数据集,如上图所示。
实现
同样也是使用哈工大提供的预训练模型: https://github.com/ymcui/Chinese-BERT-wwm 。
我们利用HuggingFace包,这里可以不用费劲地去寻找该模型的下载地址,而可以直接通过huggingface下载。
下面会看到怎么做。
首先加载相应的包:
import logging
import os
import pickle
import pandas as pd
import torch
from sklearn.metrics.pairwise import cosine_similarity
from torch.utils.data import DataLoader, SequentialSampler, TensorDataset
from tqdm import tqdm
from transformers import (
BertConfig,
BertModel,
BertTokenizer,
)
from transformers import glue_convert_examples_to_features
from transformers.data.processors.utils import InputExample, DataProcessor
然后定义需要用到的参数:
# 参数
# 从HuggingFace官网下载
model_path = "hfl/chinese-bert-wwm-ext"
max_seq_length = 128
learning_rate = 2e-5
batch_size = 32
data_dir = "lawzhidao_filter.csv"
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
定义数据处理类:
class FAQProcessor(DataProcessor):
"""处理我们的数据"""
def get_example_from_tensor_dict(self, tensor_dict):
"""
构建输入样本,我们这里针对BERT的输入只是单个句子,所以只有text_a参数
:param tensor_dict:
:return:
"""
return InputExample(
tensor_dict["idx"].numpy(), # 样本ID
tensor_dict["sentence"].numpy().decode("utf-8"), # text_a
None, # text_b
str(tensor_dict["label"].numpy()) # 标签
)
def get_candidates(self, file_dir):
train_df = pd.read_csv(file_dir)
# 只需要最佳答案中的title和reply
candidates = train_df[train_df["is_best"] == 1][["title", "reply"]]
# 转换为列表
self.candidate_title = candidates["title"].tolist()
# 保存在属性candidate_reply中
self.candidate_reply = candidates["reply"].tolist()
return self._create_examples(self.candidate_title, "train")
def _create_examples(self, lines, set_type):
"""创建训练样本"""
examples = []
for i, line in enumerate(lines):
guid = f"set_type-i"
examples.append(InputExample(guid=guid, text_a=line, text_b=None, label=1))
return examples
将数据转换为BERT能理解的格式:
def convert_examples_to_dataset(examples, tokenizer):
# 将数据转换为特征
features = glue_convert_examples_to_features(
examples,
tokenizer,
max_length=max_seq_length,
label_list=[1],
output_mode="classification"
)
# 转换为tensor
all_input_ids = torch.tensor([f.input_ids for f in features], dtype=torch.long)
all_attention_mask = torch.tensor([f.attention_mask for f in features], dtype=torch.long)
all_token_type_ids = torch.tensor([f.token_type_ids for f in features], dtype=torch.long)
dataset = TensorDataset(all_input_ids, all_attention_mask, all_token_type_ids)
return dataset
定理模型推理函数:
def infer(model, dataset):
"""
让model在数据dataset上进行推理
:param model:
:param dataset:
:return:
"""
sentence_outputs = []
sampler = SequentialSampler(dataset)
dataloader = DataLoader(dataset, sampler=sampler, batch_size=batch_size)
logger.info(f" Num examples = len(dataset), Batch size = batch_size")
for batch in tqdm(dataloader, desc="Inferring"):
model.eval()
batch = tuple(t.to(device) for t in batch)
with torch.no_grad():
inputs =
"input_ids": batch[0],
"attention_mask": batch[1],
"token_type_ids": (batch[2])
outputs = model(**inputs)
# 我们计算句子中单词向量的均值
sequence_output, _ = outputs[:2]
sequence_output = sequence_output.mean(axis=1)
sentence_outputs.append(sequence_output)
return sentence_outputs
我们也是直接加载预训练好的模型来使用:
def main():
# 配置日志打印格式
logging.basicConfig(
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
datefmt="%m/%d/%Y %H:%M:%S",
level=logging.INFO,
)
config = BertConfig.from_pretrained(model_path)
tokenizer = BertTokenizer.from_pretrained(model_path)
model = BertModel.from_pretrained(model_path, config=config)
model.to(device)
if not os.path.exists("embeddings.pkl"):
processor = FAQProcessor()
candidate_title, candidate_reply = processor.candidate_title, processor.candidate_reply
examples = (processor.get_candidates(data_dir))
dataset = convert_examples_to_dataset(examples, tokenizer)
outputs = infer(model, dataset)
candidate_embeddings = torch.cat([o.cpu().data for o in outputs]).numpy()
with open("embeddings.pkl", "wb") as fout:
pickle.dump([candidate_title, candidate_reply, candidate_embeddings], fout)
print("Save candidate_embeddings to embeddings.pkl")
else:
with open("embeddings.pkl", "rb") as fin:
candidate_title, candidate_reply, candidate_embeddings = pickle.load(fin)
print("Embeddings loaded.")
while True:
title = input("你的问题是?\\n >")
if len(title.strip()) == 0:
continue
# 单个InputExample
examples = [InputExample(guid=0, text_a=titleNLP中的对话机器人——预训练基准模型