最近在整理之前的一些实践工作,一方面是为了笔记记录,另一方面也是自己做一些温故知新,或许对于理解一些现在大模型工作也有助益。
1. 基于bert模型实现中文语句的embedding编码
首先是基于bert模型实现中文语句的embedding编码,主要是借助了bert-as-service项目,目前该项目已经更新为了clip-as-service【1】,提供低延迟、高可扩展性的服务,主要用于图像和文本的嵌入。不过本文的内容主要还是围绕之前的工作的总结。
利用bert-as-service项目,实现利用 BERT 模型来获取中文句子的嵌入(embedding)编码。通过anaconda配置bert的运行环境。从bert和bert-as-service的readme里面,可以看到,bert依赖的python版本需要大于3.5(因此选择3.7版本), 同时tensorflow需要大于1.10版本(选择1.13.1这个版本)。
(1) 首先使用anaconda,创建一个新的环境(在Environment中create即可),命名为tf20。执行:conda install ipykernel。
在 tf20 环境写入 notebook 的 kernel 中:python -m ipykernel install --name 环境名 --display-name "展示名",即:
python -m ipykernel install --user --name tf20 --display-name "tf20"
(2) 安装tensorflow,执行: conda install tensorflow==1.13.1
(3) 安装成功之后,开始安装bert-as-service, 它包括两部分:server和client,是典型的CS的架构。执行:
pip install bert-serving-server
pip install bert-serving-client
(4)配置预训练模型,下载bert的pretraining 模型,有很多版本,因为是用在中文相关的任务,因此下载这个版本chinese_L-12_H-768_A-12即可。即BERT-Base, Chinese。模型文件信息:Chinese Simplified and Traditional, 12-layer, 768-hidden, 12-heads, 110M parameters。
(5) 启动BERT服务,在anaconda prompt中执行:
bert-serving-start -model_dir /tmp/chinese_L-12_H-768_A-12/ -num_worker=2
作为示例,只启动两个work,也就是一次能够应对两个并发请求。看到 work ready and listening的提示,说明你的服务就启动成功了。
(6) 使用client来获取中文语句的embedding
from bert_serving.client import BertClient
bc = BertClient()
embs = bc.encode(['我非常荣幸来到这', '很高兴来到这里', '很高兴来这里游玩', '请好好游玩'])
可以获得以下的编码:
另外,还可以实现计算句对之间的余弦相似度。
from math import *
def cosine_similarity(x,y):
numerator = sum(a*b for a,b in zip(x,y))
denominator = square_rooted(x)*square_rooted(y)
return round(numerator/float(denominator),3)
def square_rooted(x):
return round(sqrt(sum([a*a for a in x])),3)
cosine_similarity(embs[0], embs[1])
cosine_similarity(embs[0], embs[2])
cosine_similarity(embs[1], embs[2])
cosine_similarity(embs[2], embs[3])
cosine_similarity(embs[1], embs[3])
cosine_similarity(embs[0], embs[3])
虽然使用通用的bert 预训练模型获得了语句编码向量,能够进行相似度衡量。但是也能发现明显的问题,即使某些语句的语义是完全相反的,但是相似度却很高。需要针对具体的业务要求,进行fine tune来更新参数,适配你的具体应用场景。
一些说明:bert-as-service 仅仅是对BERT的特征提取服务,因此可以使用任意的经过fine tune之后的模型文件;最后得到的embedding vector,是使用倒数第二层针对所有token的输出做平均池化得到的;如果你想使用多层的输出来计算最后的embedding结果,而不是仅仅依赖倒数第二层的输出,那么可以使用类似下面的命令启动server:
bert-serving-start -pooling_layer -4 -3 -2 -1 -model_dir chinese_L-12_H-768_A-12/
另外为什么bert-as-service不使用[CLS]作为sentencen的embedding输出,而采用所有tokens的embedding average呢,是因为假如使用的bert model文件只是pre-trained的话,没有经过fine tune,那么使用[CLS]没有什么意义。假如使用的是fine tune之后的model文件,那么可以使用[CLS]。启动的方式如下:
bert-serving-start -pooling_strategy CLS_TOKEN -model_dir /Users/yuanquan/bert
2. 基于bert模型实现简易对话机器人
原理:利用bert来进行文本语义匹配的工作。假如用户问一个问题,需要为用户从知识库中抽取出最匹配的相应问答对。
这里以社保咨询为例:
from bert_serving.client import BertClient
import numpy as np
bc = BertClient()
questions = [
'医疗保险怎么缴纳',
'医疗保险如何交费',
'养老保险补缴流程',
'养老保险断缴以后怎么办',
'养老保险缴费地址',
'养老能不能迁移',
'医保定点医院',
'定点医院报销',
'定点医院有哪些',
'社保福利',
'杭州社保怎么缴纳',
'工伤标准是啥',
'医保断交了怎么办'
]
embs = bc.encode(questions)
topk = 3
while True:
query = input('your question: ')
query_vec = bc.encode([query])[0]
score = np.sum(query_vec * embs, axis=1) / np.linalg.norm(embs, axis=1)
topk_idx = np.argsort(score)[::-1][:topk]
for idx in topk_idx:
print('> %s\t%s' % (score[idx], questions[idx]))
进一步,可以使用faiss来做正式可用的对话机器人,Faiss是用于相似性搜索和密集聚类向量的库。
3. 微调模型(fine-tuning)
使用预训练模型的信息如下:Chinese Simplified and Traditional, 12层,768维隐藏层,12个注意力头,包含1.1亿参数。该模型可以从Google在GitHub上的开源代码中找到下载链接。下载并解压压缩文件后,会发现其中有五个文件,其中以 bert_model.ckpt
开头的文件负责加载模型变量,vocab.txt
文件则是在训练过程中使用的中文词汇表,而 bert_config.json
则提供了在训练期间可以调整的BERT模型参数。
模型的训练与预测都需要清晰的输入格式,而在BERT的代码中,processor
类负责处理输入数据。以分类任务为例,要修改 processor
类以适应自己的数据集并进行微调。在 run_classifier.py
文件中,Google 已经为一些公共数据集编写了相应的处理器,例如 XnliProcessor
、MnliProcessor
、MrpcProcessor
和 ColaProcessor
。这些现成的处理器为我们如何针对自己的数据集编写处理器提供了很好的参考。
对于需要经历训练、交叉验证以及测试全流程的模型来说,自定义的 processor
类需要继承自 DataProcessor
类,并且要重写 get_labels
方法来获取标签列表,以及 get_train_examples
、get_dev_examples
和 get_test_examples
方法来分别获取训练、验证和测试数据。这些方法会在主函数中依据 FLAGS.do_train
、FLAGS.do_eval
和 FLAGS.do_predict
参数被调用。
上述三个方法的功能相似,主要区别在于它们各自读取的数据文件路径不同。以 get_train_examples
方法为例,该方法应返回一个由 InputExample
对象构成的列表。InputExample
是一个简单的类,它具有一个构造函数,要求传入的参数包括用于唯一标识每个样例的 guid
,可以按 train-%d'%(i)
的方式定义。text_a
是一个字符串,代表输入文本的一部分,而 text_b
则是另一个可选的字符串。经过后续处理(这部分工作已在BERT代码中实现)之后,text_a
和 text_b
将会被转化为 [CLS] text_a [SEP] text_b [SEP]
的格式输入到模型中。最后,标签 label
应该是一个字符串,并且要确保其值包含在 get_labels
方法返回的标签列表中。
这里介绍两种数据的fine tune:(1)利用分类信息数据(如新闻类别);(2)判断句子相似度(两句话是否为同一语义)
(1)以新闻类别为例
参照ColaProcessor进行添加数据处理的类,程序如下:
class NicoProcessor(DataProcessor):
"""Processor for the Demo data set."""
def __init__(self):
self.labels = set()
def get_train_examples(self, data_dir):
"""定义训练集的数据,文件名需要根据自己的实际情况修改"""
return self._create_examples(
self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")
def get_dev_examples(self, data_dir):
"""定义验证集的数据,文件名需要根据自己的实际情况修改."""
return self._create_examples(
self._read_tsv(os.path.join(data_dir, "dev.tsv")), "dev")
def get_test_examples(self, data_dir):
"""定义测试集的数据,文件名需要根据自己的实际情况修改."""
return self._create_examples(
self._read_tsv(os.path.join(data_dir, "test.tsv")), "test")
def get_labels(self):
"""这里是分类的标签,根据实际情况修改,我这里是3类"""
return ['民生', '文化', '娱乐', '体育', '财经', '房产', '汽车', '教育', '科技', '军事','旅游', '国际', '证券', '农业', '电竞']
def _create_examples(self, lines, set_type):
"""Creates examples for the training and dev sets.
这个函数是用来把数据处理, 把每一个例子分成3个部分,填入到InputExample的3个参数
text_a 是第一个句子 的文本数据
text_b是第二个句子的文本,但是由于此任务是单句分类, 所以 这里传入为None
guid 是一个二元组 第一个表示此数据是什么数据集类型(train dev test) 第二个表示数据标号。
label 表示句子类别
"""
examples = []
for (i, line) in enumerate(lines):
# Only the test set has a header
if set_type == "test" and i == 0:
continue
guid = "%s-%s" % (set_type, i)
if set_type == "test":
text_a = tokenization.convert_to_unicode(line[1])
label = "民生"
else:
text_a = tokenization.convert_to_unicode(line[1])
label = tokenization.convert_to_unicode(line[0])
examples.append(InputExample(guid=guid, text_a=text_a, text_b=None, label=label))
return examples
def main(_):
tf.logging.set_verbosity(tf.logging.INFO)
processors = {
"cola": ColaProcessor,
"mnli": MnliProcessor,
"mrpc": MrpcProcessor,
"xnli": XnliProcessor,
"nico": NicoProcessor,
}
然后配置环境,运行run_classifier.py代码
export BERT_Chinese_DIR=/user/yuanquan/bert/chinese_L-12_H-768_A-12
export Nico_DIR=/user/yuanquan/data
python run_classifier.py \
--task_name=nico \ #task_name 表示我调用的是什么处理类,这里需要修改成我们新的定义的demo
--do_train=true \
--do_eval=true \
--data_dir=$Nico_DIR \
--vocab_file=$BERT_Chinese_DIR/vocab.txt \
--bert_config_file=$BERT_Chinese_DIR/bert_config.json \
--init_checkpoint=$BERT_Chinese_DIR/bert_model.ckpt \
--max_seq_length=128 \
--train_batch_size=32 \
--learning_rate=2e-5 \
--num_train_epochs=3.0 \
--output_dir=/tmp/Nico_output
train完之后,就可以得到fine tune的model文件,接下去进行测试
python run_classifier.py \
--task_name=nico \
--do_predict=true \
--data_dir=$Nico_DIR \
--vocab_file=$BERT_Chinese_DIR/vocab.txt \
--bert_config_file=$BERT_Chinese_DIR/bert_config.json \
--init_checkpoint=/tmp/Nico_output \
--max_seq_length=128 \
--output_dir=/tmp/Nico_output
(2)以自定义的数据为例
假如我们输入的训练数据格式如下,第一列为相似度标签:
0,医保缴纳流程,养老保险缴费流程
1,医保怎么缴纳,医疗保险怎么缴费
那么可以写一个如下的get_train_examples的函数。当然对于csv的处理,可以使用诸如csv.reader的形式进行读入。
def get_train_examples(self, data_dir):
file_path = os.path.join(data_dir, 'train.csv')
with open(file_path, 'r') as f:
reader = f.readlines()
examples = []
for index, line in enumerate(reader):
guid = 'train-%d'%index
split_line = line.strip().split(',')
text_a = tokenization.convert_to_unicode(split_line[1])
text_b = tokenization.convert_to_unicode(split_line[2])
label = split_line[0]
examples.append(InputExample(guid=guid, text_a=text_a, text_b=text_b, label=label))
return examples
同时对应判断句子相似度这个二分类任务,get_labels函数可以写成如下的形式:
def get_labels(self):
reutrn ['0','1']
在对get_dev_examples和get_test_examples函数做类似get_train_examples的操作后,便完成了对processor的修改。其中get_test_examples可以传入一个随意的label数值,因为在模型的预测(prediction)中label将不会参与计算。
修改完成processor后,需要在在原本main函数的processor字典里,加入修改后的processor类,即可在运行参数里指定调用该processor。
processors = {
"cola": ColaProcessor,
"mnli": MnliProcessor,
"mrpc": MrpcProcessor,
"xnli": XnliProcessor,
"selfsim": SelfProcessor #添加自己的processor
}
之后就可以直接运行run_classsifier.py进行模型的训练。在运行时需要制定一些参数,一个较为完整的运行参数如下所示:
export BERT_BASE_DIR=/user/yuanquan/bert/chinese_L-12_H-768_A-12 #全局变量 下载的预训练bert地址
export MY_DATASET=/user/yuanquan/xnli #全局变量 数据集所在地址
python run_classifier.py \
--task_name=selfsim \ #自己添加processor在processors字典里的key名
--do_train=true \
--do_eval=true \
--dopredict=true \
--data_dir=$MY_DATASET \
--vocab_file=$BERT_BASE_DIR/vocab.txt \
--bert_config_file=$BERT_BASE_DIR/bert_config.json \
--init_checkpoint=$BERT_BASE_DIR/bert_model.ckpt \
--max_seq_length=128 \ #模型参数
--train_batch_size=32 \
--learning_rate=5e-5 \
--num_train_epochs=2.0 \
--output_dir=/tmp/selfsim_output/ #模型输出路径
4. text-to-image 图像编码及文本找图或者图生文本
开头我们提到bert-as-service已经更新成了clip-as-service【1】,而clip模型(Contrastive Language-Image Pre-training)是由oai在2021年发布的一种多模态预训练神经网络模型,用于匹配图像和文本。 该模型通过对比学习的方式进行预训练,将图像和文本映射到统一的向量空间中,使得模型能够直接在向量空间中计算图像和文本之间的相似性,无需额外的中间表示。CLIP模型的核心原理包括使用大量图像和文本的配对数据进行预训练,以学习图像和文本之间的对齐关系。它具有多模态学习的能力,能够同时理解图像和文本两种不同模态的信息,并在它们之间建立联系。
所以我们完全可以使用clip-as-service建立一套通过文本查询图像的搜素引擎。接下来展示一下如何实现:
使用 Totally Looks Like 数据集(也可以使用你自己的图像数据集)和 DocArray 包。DocArray 已作为上游依赖包含在 clip-client 中,所以无需单独安装。DocArray 是用于多模态数据的表示、传输、存储和检索。
下载数据(也可以手动下载后解压加载):
from docarray import DocumentArray
da = DocumentArray.pull('ttl-original', show_progress=True, local_cache=True)
da.plot_image_sprites()
编码图像,使用 python -m clip_server
启动服务器。假设服务器地址为 0.0.0.0:51000 并且使用 gRPC 协议。
from clip_client import Client
c = Client(server='grpc://0.0.0.0:51000')
da = c.encode(da, show_progress=True)
如果执行过慢的话,也可以使用已经编码好的版本:
from docarray import DocumentArray
da = DocumentArray.pull('ttl-embedding', show_progress=True, local_cache=True)
接下来就可以通过句子进行搜索,创建一个简单的提示,允许用户输入句子:”
while True:
vec = c.encode([input('sentence> ')])
r = da.find(query=vec, limit=9)
r[0].plot_image_sprites()
一些文字找图片示例:
也可以将上述程序的输入和输出进行交换,以实现图像到文本的搜索。具体来说,给定一个查询图像,找到最能描述该图像的句子。
首先下载文本描述数据:
from docarray import Document, DocumentArray
d = Document(uri='https://www.gutenberg.org/files/1342/1342-0.txt').load_uri_to_text()
da = DocumentArray(
Document(text=s.strip()) for s in d.text.replace('\r\n', '').split('.') if s.strip()
)
然后对文本进行clip模型编码:
from clip_client import Client
c = Client('grpc://0.0.0.0:51000')
r = c.encode(da, show_progress=True)
执行文本编码与图片编码的最相似计算,输出得分最高的文本内容
from docarray import DocumentArray
img_da = DocumentArray.load_binary('ttl-image')
for d in img_da.sample(10):
print(da.find(d.embedding, limit=1)[0].text)
从示例来看,受限于文本内容过少,得到的结果有些不靠谱,数据越丰富应该可以获得更可靠的结果输出。在这里主要就是了解下这种场景的原理,document array支持ANN vector search。
5. 参考材料
【1】clip-as-service
【2】bert fine-tune 实践