1.数据集
本文采用的是STS数据集,如下图所示,包括所有的2012-2016年的数据,而all文件夹包含2012-2015的所有数据。
每一个文件的具体数据如下所示,每一行为一个三元组:<相似性得分,句子1,句子2>.
在实现时将all文件夹中的所有数据当作训练集,将2016年的文件当作测试集。
1.1数据读取
采用以下代码进行单个文件的数据读取:
"""读取一个数据集文件"""
def load_one_sts(filename):
s0s = []
s1s = []
labels = []
num_samples = 0
with open(filename, 'r', encoding='utf-8') as f:
for line in f:
#rstrip:是从字符串最右边删除了参数指定字符后的字符串,不带参数进去则是去除最右边的空格
#strip:同时去除左右两边指定的字符,不带参数进去则是去除空格
data = line.rstrip()
# line = data.split('\t')
# print(line)
label, s0, s1 = data.split('\t')
#如果没有对应的相似性得分,则直接跳过
if label == '':
continue
else:
score = round(float(label)) #如果距离两边一样远,会保留到偶数的一边。比如round(0.5)和round(-0.5)都会保留到0,而round(1.5)会保留到2
# scores.append(score)
"""经验证可知score的取值范围为0-5,故标签使用one-hot encoding,数目为6"""
y = [0] * 6 #此时的y是一个list
y[score] = 1 #将score值所对应的位置置为1
labels.append(np.array(y)) #此时label转换完成,内置元素应为array
# labels = np.asarray(labels)
num_samples = len(labels)
s0s.append(s0)
s1s.append(s1)
注意:如上面所示代码data = line.rstrip(),在本地文件中,有的两个句子是没有对应的相似度得分的,此时对应字段为空,如果使用data = line.strip(),程序会将这一行左面的空格去掉,在后面进行循环读取每个文件的时候会报错:You don't get unenough unpacks(expected 3, get 2)这样的信息。
此时已经将相似度得分,句子1,句子2分别存储,接下来就是将每一个句子映射成id索引的组合形式,这就需要读入GloVe模型,以下代码为读入GloVe模型的辅助函数(这些函数在另一个文件embedding.py中):
import word2vec
import os
import shutil
from sys import platform
import numpy as np
import pandas as pd
# 计算行数,就是单词数
def getFileLineNums(filename):
f = open(filename, 'r', encoding='utf-8')
count = 0
for line in f:
count += 1
return count
# Linux或者Windows下打开词向量文件,在开始增加一行
def prepend_line(infile, outfile, line):
with open(infile, 'r', encoding='utf-8') as old:
with open(outfile, 'w', encoding='utf-8') as new:
new.write(str(line) + "\n")
shutil.copyfileobj(old, new)
def prepend_slow(infile, outfile, line):
with open(infile, 'r', encoding='utf-8') as fin:
with open(outfile, 'w', encoding='utf-8') as fout:
fout.write(line + "\n")
for line in fin:
fout.write(line)
"""生成符合word2vec工具读取格式的模型文件"""
def normalize_data(filename):
num_lines = getFileLineNums(filename)
model_file = 'glove_model_50d.txt'
model_first_line = "{} {}".format(num_lines, 50)
# Prepends the line.
if platform == "linux" or platform == "linux2":
prepend_line(filename, model_file, model_first_line)
else:
prepend_slow(filename, model_file, model_first_line)
print('模型向量文件数据已规范化!后续请使用文件', model_file)
"""读取Glove模型,生成id和词向量"""
def load_glove_model(glove_model_path):
normalize_data(glove_model_path)
wv = word2vec.load('glove_model_50d.txt')
print('GloVe模型载入完毕!')
vocab = wv.vocab
word2id = pd.Series(range(1, len(vocab)+1), index=vocab)
#将未知词对应的id设置为0,对应word_embedding中的第0行
word2id['<unk>'] = 0
# print(word2id[399990:])
print('word2id转换完成,未知词使用<unk>标识符!')
word_embedding = wv.vectors
#采取均值作为未知词的词向量表示
word_mean = np.mean(word_embedding, axis=0)
word_embedding = np.vstack([word_mean, word_embedding])
# print(word_embedding[:2])
print('id词向量嵌入完成!')
return word2id, word_embedding
由于官方提供的GloVe文件格式并不符合word2vec工具读取的要求,故使用其中的normalize_data()将其标准化,未知词采用‘<unk>’标记,继而调用load_glove_model()得到word2id和word_embedding。得到词对应的嵌入向量之后,对第一步读取到的数据进行映射。
"""通过单词获取id"""
def get_id(word):
if word in word2id:
return word2id[word]
else:
return word2id['<unk>']
"""数据清洗并将句子表示成索引组合"""
def seq2id(texts):
texts = clean_text(texts)
texts = texts.split(' ')
texts_id = map(get_id, texts)
return texts_id
"""填充句子, padding_length:句子填充的长度"""
def padding_sentence(s0, s1, padding_length):
sentence_num = len(s1)
# sentence_length = 100
# print('句子填充长度为100')
s0s = []
s1s = []
for s in s0:
left = padding_length -len(s)
pad = [0] * left
s= list(s)
s.extend(pad)
s0s.append(np.array(s))
for s in s1:
left = padding_length -len(s)
pad = [0] * left
s= list(s)
s.extend(pad)
s1s.append(np.array(s))
# print('%d个句子填充完毕!'%sentence_num)
return s0s, s1s
上面所示为句子映射的辅助函数,其中seqid()用来得到句子各个单词对应的id。实际情况中每条句子的长度都不一样,导致输入网络的tensor长度也不一致,故此处调用padding_sentence()填充各个句子(此处使用定长100, 有些地方使用最长句子的长度来进行填充)。
其中数据清洗函数如下所示,对于其中的标点(!、......等)、缩写(You're替换成You 're)等进行处理:
"""数据清洗函数"""
def clean_text(line):
# print('过滤前--------------->', line)
#替换掉无意义的单个字符
line = re.sub(r'[^A-Za-z0-9(),!?.\'\`]', ' ', line)
"""使用空格将单词后缀单独分离开来"""
line = re.sub(r'\'s', ' \'s ', line)
line = re.sub(r'\'ve', ' \'ve ', line)
line = re.sub(r'n\'t', ' n\'t ', line)
line = re.sub(r'\'re', ' \'re ', line)
line = re.sub(r'\'d', ' \'d ',line)
line = re.sub(r'\'ll', ' \'ll ',line)
"""使用空格将标点符号、括号等字符单独分离开来"""
line = re.sub(r',', ' , ', line)
line = re.sub(r'!', ' ! ', line)
line = re.sub(r'\?', ' \? ', line)
line = re.sub(r'\(', ' ( ', line)
line = re.sub(r'\)', ' ) ', line)
line = re.sub(r'\s{2,}', ' ', line)
# line = re.sub(r'\n', '', line)
# line = re.sub(r'')
# line = re.sub(r',', ' , ', line)
# print('过滤后--------------->',line)
return line.strip().lower()
做完上述动作,继续在load_one_sts()函数中进行编辑:
def load_one_sts(filename):
"""以下是紧接着第一部分继续编写的代码,二者合起来才是一个完整的函数"""
s0s_id = []
for s0 in s0s:
s0_id = list(seq2id(s0))
# print(type(s0_id))
# print('s0_id:', s0_id)
s0s_id.append(np.asarray(s0_id))
s1s_id = []
for s1 in s1s:
s1_id = list(seq2id(s1))
s1s_id.append(np.asarray(s1_id))
#句子填充,填充长度为100
s0_padding, s1_padding= padding_sentence(s0s_id, s1s_id, 100)
# print(len(s0_padding[0]))
# print(s0_padding[0])
return s0_padding, s1_padding, labels
如上所示,单个文件的读取编写完毕,接下来需要遍历某个路径下的所有文件,将所得到的数据放入s0,s1,labels中。
"""将不同文件的数据进行拼接"""
def concat(data):
s0s = []
s1s = []
labels = []
for s0, s1, label in data:
s0s += s0
s1s += s1
labels += label
s0s = np.asarray(s0s)
s1s = np.asarray(s1s)
labels = np.asarray(labels)
return s0s, s1s, labels
"""读取整个数据集"""
def load_datasets(path):
files = []
#列出路径path下所有的文件
for dirpath,dirnames,filenames in os.walk(path):
for filename in filenames:
# print(os.path.join(dirpath,filename))
files.append(dirpath + '/' + filename)
s0, s1, labels = concat([load_one_sts(file) for file in files])
return ([s0, s1], labels)
这样,我们就完成了某个路径下的所有数据文件的读取工作。
为了放心,对其进行测试,读取测试集和训练集:
print('读取训练集-------》')
path = './sts/semeval-sts/all'
x_train, y_train = load_datasets(path)
print('训练集样本数:', len(y_train))
print('读取测试集-------》')
path = './sts/semeval-sts/2016'
x_test, y_test = load_datasets(path)
print('测试集样本数:', len(y_test))
程序执行结果如下所示:
OK,至此我们就完成了数据集的读取和创建。下一步就是创建神经网络模型MPCNN,并应用其中的相似度计算公式来得到相似度得分。