处理数据集
我们用的是SQuAD的train-v1.1.json和dev-v1.1.json.
训练数据集大家去官网上即可下载.
机器阅读理解数据集比其它的NLP任务如文本分类,情感分析,序列标注等的数据集要难处理一些.
我们首先打印squad数据集内部的数据格式:
我们可以看到整个训练数据集json文件是一个大的字典,该字典的key是[“version”,“data”],所有的数据都存放在data这个关键词下的value里面.以列表的形式存储,长度为442.
每一个值是一个字典,代表一篇文章(那篇论文也说了,总共就537篇文章,其中442篇用来做训练),key为"title"和"paragraphs".我们同样只关心paragraphs.
注意这里所说的文章不是简单的一篇上下文,而是围绕着title有多个上下文,每一个上下文段落是独立的.可以看到这442篇文章对应着442个主题,而每一个主题下有一篇大文章,这篇大文章有多个上下文段落.比如第一个大文章下有55个段落.
我们取这442篇大文章中的55个上下文段落中的第一个段落来看:
可以看到每一个段落用一个字典表示,“context"对应的是该段落的文本,“qas"代表的是针对该段落提出的问答对.
从qas可以看出针对当前这个context的内容,有多个问答对,但是注意在SQuAD数据集中每一个问题有且只有一个答案,所以"answers”:[]这个有点多余.
上图给出了完整的结构:
即:从取出来"data"这个key对应的value,是一个大列表.
每一个值代表一篇文章,每一篇文章用字典表示,关键字是"title"和"paragraphs”,其中"paragraphs"是一个列表,每一个元素对应一个段落,
每一个段落用字典表示,关键词是"context"和"qas",其中"context"就是这篇段落的文本,是一个字符串.“qas"是针对这个段落文本提出的多个问答对,用一个列表表示,其中每一个元素是一个问答对,用字典表示,关键词是"answers"和"question”,“id”.其中"answers"是长度为1的列表,因为每一个问题只有一个答案,而答案用字典来表示,关键词是"text"和"answer_start","text"表示这段答案文本,"answer_start"表示答案在它所在的上下文段落的起始位置.
下面我们从数据集中读取数据.这里需要介绍下spacy工具包,它是nlp领域很好用的包,处理分词的时候可以将单词与标点符号分隔开
我们接下来看看如何寻找单词在段落中的跨度
context="how to find ``token's span".replace("``",'" ').replace("''",'" ')
context_token_list=[token.text for token in word_tokenizer(context)]
print(context,len(context))
print(context_token_list)
record_spans=[]
current=0
for token in context_token_list:
current=context.find(token,current)
print(current)
if current<0:
print("Not find any token in context")
break
record_spans.append((current,current+len(token)))
current+=len(token)
print("spans is ",record_spans)
从图中可以看到,我们通过上述代码可以找到每一个单词在context中的前后位置,注意我们是要将context中的单词和标点符号分隔开然后寻找的,但是不用担心找错位置的问题。
这是因为``在原文本中算两个字符,而我们利用(" )取代它,(" )也是两个字符,所以不会有找错位置的问题
而且SQuAD数据集给我们的是答案的起始位置,我们自己找出来终止位置作为训练标签,因此更不会有找错位置的问题。
下面我们来看如何根据答案的起始位置和终止位置找到答案对应的单词在原文中的位置:
显然答案answer’s在原文中有两个跨度answer和’s的跨度,即(16, 22), (22, 24),我们的目的根据答案的跨度(16,24)找到这两个跨度的下标,因为它们代表了答案的单词即answer’s。
通过这些id我们就可以还原答案:
现在我们从新开始:
重头开始一步一步的构建训练数据样本
def get_token_spans(context,context_token_list):
current=0
spans=[]
for token in context_token_list:
current=context.find(token,current)
if current<0:
print("Not find {} in context!".format(token))
raise Exception()
spans.append((current,current+len(token)))
current+=len(token)
return spans
import collections
word_counter=collections.Counter()#统计单词出现的次数
char_counter=collections.Counter()
examples=[]
eval_examples={}
total_examples=0
for each_article in squad_data:
#each_article.keys()==["title","paragraphs"]
multi_context_paragraphs=each_article["paragraphs"]
for each_context_paragraph in multi_context_paragraphs:
#each_context_paragraph.keys()==["context","qas"]
context=each_context_paragraph["context"]
qas=each_context_paragraph["qas"]
#type(context)==string 由于spacy不能分开''和``,因此我们把``和''这两个字符替换成" ,
#注意替换后的" 仍然是两个字符。
context=context.replace("''",'" ').replace("``",'" ')
context_token_list=tokenize_sentence(context)
context_char_list=[list(token) for token in context_token_list]
context_token_spans=get_token_spans(context,context_token_list)
#现在得到了context中各个token的跨度
#现在我们要统计context中单词出现的次数,注意注意注意,context中单词出现的次数并不是指单词在context中
#出现的次数,而是这个单词作为训练的单词在训练集中出现的次数,而每一个context对应多个questions,因此
#有多少个question,context中单词就会出现多少次,对于字符出现的次数同理计算
for context_word in context_token_list:
word_counter[context_word]+=len(qas)
for char in context_word:
char_counter[char]+=len(qas)
for each_qa in qas:
question=each_qa["question"]
answers=each_qa["answers"]
assert len(answers)==1#每一个问题只有一个答案。
answer=answers[0]
question=question.replace("``",'" ').replace("''",'" ')
question_token_list=tokenize_sentence(question)
question_char_list=[list(token) for token in question_token_list]
for question_token in question_token_list:
word_counter[question_token]+=1
for char in question_token:
char_counter[char]+=1
#answer.keys()==["text","answer_start"]
answer_text=answer["text"]
answer_start=answer["answer_start"]
answer_end=answer_start+len(answer_text)#答案在context中的起始位置和终止位置
#接下来要从context_token_spans中找出答案起始位置和终止位置对应单词的下标
answer_span_in_context_ids=[]
for id_,token_span in enumerate(context_token_spans):
if not(answer_end<=token_span[0] or answer_start>=token_span[1]):
answer_span_in_context_ids.append(id_)
y1,y2=answer_span_in_context_ids[0],answer_span_in_context_ids[-1]
#我们仅仅要起始位置和终止位置的单词,中间的单词不考虑
total_examples+=1#注意我们是一个context,一个question,以及一个y1,y2构成一个example
one_example={"context_token_list":context_token_list,
"context_char_list":context_char_list,
"question_token_list":question_token_list,
"question_char_list":question_char_list,
"y1":y1,"y2":y2,"question_id":total_examples}
#因此一个context对应的多个question会构成多个example,它们之间是独立的。
examples.append(one_example)
eval_examples[str(total_examples)]={"answer_text":answer_text,
"context_token_spans":context_token_spans,
"context":context,
"original_context":each_context_paragraph["context"],
}
我们看看一共有多少个训练样本:
一共有87599个训练样本,每一个训练样本包含有:
context中的单词(以列表的形式存储,而且通过spacy已经将单词和标点符号分隔开),context中单词的字符(可以认为是一个二维列表,每一个值是一个列表,代表一个单词,而每一个列表就是该单词的所有字符,之所以这样做是因为后面字符嵌入要用到),还有question的单词和相应的字符,因为question我们也要做词嵌入和字符嵌入,此外还有当前问题的答案在context中的起始位置和终止位置。
我们再来看看word_counter和char_counter,
构建词嵌入矩阵
min_count=10
filter_word=[k for k,v in word_counter.items() if v>=min_count]
print("total word nums: %d and filtered word nums: %d " %(len(word_counter),len(filter_word)))
import numpy as np
word2id={"<PAD>":0,"<UNK>":1}
for word in filter_word:
word2id[word]=len(word2id)
vocab_size=len(word2id)
embedding_size=100
embedding_matrix=np.random.uniform(-1.0,1.0,(vocab_size,embedding_size))
print(embedding_matrix.shape)
我们得到了(46742, 100)的词嵌入矩阵。
构建字符嵌入矩阵
char2id={"<PAD>":0,"<UNK>":1}
char_filtered_nums=50
char_filtered=[char for char,freq in char_counter.items() if freq>=char_filtered_nums]
for char in char_filtered:
char2id[char]=len(char2id)
char_embedding_size=30
char_embedding_matrix=np.random.uniform(-1.0,1.0,(len(char2id),char_embedding_size))
print(char_embedding_matrix.shape)
现在我们得到了(286, 30)的字符嵌入矩阵。
为了方便运行,我仅仅取其中的几个样例作为训练数据集:
my_train_examples=[]
my_eval_examples=[]
for i in range(20):
one_example=examples[i]
total_example_id=one_example["question_id"]
one_eval_example=eval_examples[str(total_example_id)]
my_train_examples.append(one_example)
my_eval_examples.append(one_eval_example)
with open("SQuAD/my_train_examples.json","w") as f:
json.dump(my_train_examples,f)
with open("SQuAD/my_eval_examples.json","w") as f:
json.dump(my_eval_examples,f)
然后我们利用这两个小的文件构建我们的训练样本,因为设备资源有限,只能这样。。。。
所以我们按照下面的流程重新加载数据
with open("/home/sun/Desktop/my_train_examples.json") as f:
train_examples=json.load(f)
with open("/home/sun/Desktop/my_eval_examples.json") as f:
eval_examples=json.load(f)
import collections
word_counter=collections.Counter()
char_counter=collections.Counter()
for each_example in train_examples:
context_token_list=each_example["context_token_list"]
context_char_list=each_example["context_char_list"]
question_token_list=each_example["question_token_list"]
question_char_list=each_example["question_char_list"]
for word in context_token_list:
word_counter[word]+=1
for word in question_token_list:
word_counter[word]+=1
for char_list in context_char_list:
for char in char_list:
char_counter[char]+=1
for char_list in question_char_list:
for char in char_list:
char_counter[char]+=1
word2id={"<PAD>":0,"<UNK>":1}
char2id={"<PAD>":0,"<UNK>":1}
for word,freq in word_counter.items():
word2id[word]=len(word2id)
for char,freq in char_counter.items():
char2id[char]=len(char2id)
print(len(word2id))
print(len(char2id))
import numpy as np
word_embed_size=100
char_embed_size=30
word_embedding_matrix=np.random.uniform(-1.0,1.0,(len(word2id),word_embed_size))
char_embedding_matrix=np.random.uniform(-1.0,1.0,(len(char2id),char_embed_size))
现在vocab_size仅有几百个单词,char_size也很小。
我们先定义一些可以用得到的函数,
def sentence_to_id(sentence_list,token2id,max_length):
result=[]
UNK_id=token2id["<UNK>"]
for token in sentence_list:
result.append(token2id.get(token,UNK_id))
if len(result)>=max_length:
return result
else:
pad_length=max_length-len(result)
result+=[0]*pad_length
return result
def sentence_token_char_to_id(sentence_char_list,char2id,max_seq_length,
max_char_length):
result=np.zeros([max_seq_length,max_char_length],dtype=np.int32)
for i,char_list in enumerate(sentence_char_list):
for j,char in enumerate(char_list):
result[i,j]=char2id.get(char,char2id["<UNK>"])
return result
上面两个函数是将句子转成id形式以及句子中的字符转成id,这样才能够通过id索引找到单词或字符对应的向量。
接下来我们利用下面的函数构造特征,并且以TFRcords文件格式存储。以tfrecords文件格式存储是因为整个SQuAD数据集要训练起来还是不小的,避免一次全部加载进内存。
import tqdm
def create_String_Feature(value):
return tf.train.BytesList(value=[value])
def create_Float_Feature(value):
return tf.train.FloatList(value=[value])
def create_Int_Feature(value):
return tf.train.Int64List(value=[value])
def build_features(examples,word2id,char2id,config,is_test=False):
max_context_seq_length=config.test_max_context_seq_length if is_test else config.max_context_seq_length
max_question_seq_length=config.test_max_question_seq_length if is_test else config.max_question_seq_length
max_word_char_length=config.max_word_char_length
writer=tf.python_io.TFRecordWriter(config.train_feature_dir)
for each_example in tqdm.tqdm(examples):
context_token_list=each_example["context_token_list"]
context_char_list=each_example["context_char_list"]
question_token_list=each_example["question_token_list"]
question_char_list=each_example["question_char_list"]
if len(context_token_list)>max_context_seq_length or len(question_token_list)>max_question_seq_length:
continue
context_token_ids=sentence_to_id(sentence_list=context_token_list,
token2id=word2id,
max_length=max_context_seq_length)
question_token_ids=sentence_to_id(sentence_list=question_token_list,
token2id=word2id,
max_length=max_question_seq_length)
context_token_ids=np.array(context_token_ids,dtype=np.int32)
question_token_ids=np.array(question_token_ids,dtype=np.int32)
context_token_char_ids=sentence_token_char_to_id(sentence_char_list=context_char_list,
char2id=char2id,
max_seq_length=max_context_seq_length,
max_char_length=max_word_char_length)
question_token_char_ids=sentence_token_char_to_id(sentence_char_list=question_char_list,
char2id=char2id,
max_seq_length=max_question_seq_length,
max_char_length=max_word_char_length)
start=each_example["y1"]
end=each_example["y2"]
y1=np.zeros([max_context_seq_length],dtype=np.float32)#标签
y2=np.zeros([max_context_seq_length],dtype=np.float32)
y1[start]=1.0
y2[end]=1.0#one_hot形式的标签,用来计算交叉商
assert context_token_char_ids.shape==(max_context_seq_length,max_word_char_length)
assert context_token_ids.shape==(max_context_seq_length,)
assert question_token_char_ids.shape==(max_question_seq_length,max_word_char_length)
assert question_token_ids.shape==(max_question_seq_length,)
assert y1.shape==(max_context_seq_length,)==y2.shape
one_example_feature={"context_token_ids":tf.train.Feature(bytes_list=create_String_Feature(value=context_token_ids.tostring())),
"question_token_ids":tf.train.Feature(bytes_list=create_String_Feature(value=question_token_ids.tostring())),
"context_token_char_ids":tf.train.Feature(bytes_list=create_String_Feature(value=context_token_char_ids.tostring())),
"question_token_char_ids":tf.train.Feature(bytes_list=create_String_Feature(value=question_token_char_ids.tostring())),
"y1":tf.train.Feature(bytes_list=create_String_Feature(y1.tostring())),
"y2":tf.train.Feature(bytes_list=create_String_Feature(y2.tostring())),
"question_id":tf.train.Feature(int64_list=create_Int_Feature(each_example["question_id"]))}
one_record=tf.train.Example(features=tf.train.Features(feature=one_example_feature))
writer.write(one_record.SerializeToString())
writer.close()
下面调用这个函数
import tensorflow as tf
class Config:
def __init__(self):
self.max_context_seq_length=400
self.max_question_seq_length=50
self.test_max_context_seq_length=1000
self.test_max_question_seq_length=200
self.max_word_char_length=16
self.word_embedding_dim=100
self.char_embedding_dim=30
self.train_feature_dir="SQuAD/train_examples_features.tfrecords"
self.test_feature_dir="SQuAD/test_examples_features.tfrecords"
config=Config()
build_features(examples=train_examples,char2id=char2id,word2id=word2id,config=config)
看起来是成功写入了,函数也没有问题。
接下来就是读取我们写入的tfrecords文件
读取tfrecords文件的API是
tf.data.TFRecordDataset(record_file)
也就是从我们保存的record_file文件中读取出来tfrecords文件,然后定义一个文件解析器解析tfrecords文件为原来的数据格式。
即:
tf.data.TFRecordDataset(record_file).map(tfrecord_parser)
利用上面的函数将record_file文件中的tfrecords格式的数据读取出来并且应用我们自定义的tfrecord_parser解析器将tfrecords文件格式还原为原来的数据格式。,注意tfrecord_parser是一个函数
下面我们看看怎么定义文件解析器
def get_tfrecord_parser(config,is_test=False):
def parser_fun(example):
max_context_seq_length=config.test_max_context_seq_length if is_test else config.max_context_seq_length
max_question_seq_length=config.test_max_question_seq_length if is_test else config.max_question_seq_length
max_word_char_length=config.max_word_char_length
features=tf.parse_single_example(example,
features={"context_token_ids":tf.FixedLenFeature(shape=[],dtype=tf.string),
"context_token_char_ids":tf.FixedLenFeature(shape=[],dtype=tf.string),
"question_token_ids":tf.FixedLenFeature(shape=[],dtype=tf.string),
"question_token_char_ids":tf.FixedLenFeature(shape=[],dtype=tf.string),
"y1":tf.FixedLenFeature(shape=[],dtype=tf.string),
"y2":tf.FixedLenFeature(shape=[],dtype=tf.string),
"question_id":tf.FixedLenFeature(shape=[],dtype=tf.int64)})
#这里的features就是一个解析字典,它的key要和我们最开始的特征字典的key对应
#tf.FixedLenFeature代表定长特征,shape=[]表示不指定形状,dtype要和我们最开始定义的BytesList对应
#所以dtype=tf.string,question_id由于最开始定义的是Int64List,因此dtype=tf.int64
#tf.parse_single_example将example解析为features,下面就是还原features
context_token_ids=tf.reshape(tf.decode_raw(features["context_token_ids"],out_type=tf.int32),
shape=(max_context_seq_length,))
context_token_char_ids=tf.reshape(tf.decode_raw(features["context_token_char_ids"],out_type=tf.int32),
shape=(max_context_seq_length,max_word_char_length))
question_token_ids=tf.reshape(tf.decode_raw(features["question_token_ids"],out_type=tf.int32),
shape=(max_question_seq_length,))
question_token_char_ids=tf.reshape(tf.decode_raw(features["question_token_char_ids"],out_type=tf.int32),
shape=(max_question_seq_length,max_word_char_length))
y1=tf.reshape(tf.decode_raw(features["y1"],out_type=tf.float32),shape=(max_context_seq_length,))
y2=tf.reshape(tf.decode_raw(features["y2"],out_type=tf.float32),shape=(max_context_seq_length,))
question_id=features["question_id"]
#每一个question_id对应一个example
return context_token_ids,context_token_char_ids,question_token_ids,question_token_char_ids,y1,y2,question_id
return parser_fun
以上的代码就定义了tfrecords文件解析器
下面我们调用它测试下
我们可以看到数据的shape和dtype是对的.
我们再来看数据如何批处理
batch_size设置为5,如上图所示。
上面的数据就是我们要输入给模型的。
针对模型的构造,请看另一个博客模型构建篇