您的位置:首页 > 其它

CNN在句子相似性建模的应用--tensorflow实现篇1

2017-03-20 21:38 411 查看
经过上周不懈的努力,终于把“Multi-Perspective Sentence Similarity Modeling with Convolution Neural Networks”这篇论文用tensorflow大致实现出来了,代码后续回放到我的github上面。踩了很多坑,其实现在也还有一些小的问题没有搞明白和解决,但是也算自己实现了第一个完整的Tensorflow程序,至于剩下的一些小问题,接下来慢慢边学习边解决吧。因为代码比较长,我们分为两篇来介绍,本篇主要介绍读入数据和一些功能函数。好了,接下来我们就开始介绍代码吧。

1,数据集介绍与读取

首先介绍一下数据集STS,这是一个比赛的数据集,包含有2012-2016所有年份的数据,文件中的每一行都由三元组(sentence1, sentence2, similarity)组成,也就是两个句子的相似度。每一年的数据都会有好几个文件,分别用于不同领域(比如问答系统等)。文件结构如下图所示:



其中all文件夹中包含了2012-2015之间的所有数据,我们是用all作为训练集,2016作为测试集。共有20000多条训练数据和1000多条测试数据,论文中说有10000多条训练数据,我也不知道为什么不同。但是这并不作为本文的关注点(仅以使用tensorflow实现论文提到的模型为主,至于准确率等并未考虑在内)。接下来介绍数据的读取代码,这部分代码位于data_helper.py文件中:

def load_sts(dsfile, glove):
""" 读取一个文件 """
#分别存放第一、二个句子以及他们的标签
s0 = []
s1 = []
labels = []
with codecs.open(dsfile, encoding='utf8') as f:
for line in f:
line = line.rstrip()
label, s0x, s1x = line.split('\t')
#如果是测试文件只有两个句子,而不包含其相似度分值,则不读取
if label == '':
continue
else:
#将相似性分数转化为一个六维数组(因为分数取值范围是0-6)将其转化为one-hot编码方便作为神经网络的输出
score_int = int(round(float(label)))
y = [0] * 6
y[score_int] = 1
labels.append(np.array(y))
#将两个句子进行分词,并根据word2vec转化为单词索引列表,对于不在word2vec中的单词使用UNKNOW来表示
for i, ss in enumerate([s0x, s1x]):
words = word_tokenize(ss)
index = []
for word in words:
word = word.lower()
if word in glove.w:
index.append(glove.w[word])
else:
index.append(glove.w['UKNOW'])
#对每个句子进行PADDING,这里将其补位长度为100的句子
left = 100 - len(words)
pad = [0]*left
index.extend(pad)
#注意这里一定要将其转化为np数组在保存,不然后面feed_dic的时候会报错,我就被这个错误困扰了一天才找出来
if i == 0:
s0.append(np.array(index))
else:
s1.append(np.array(index))
#s0.append(word_tokenize(s0x))
#s1.append(word_tokenize(s1x))
print len(s0)
return (s0, s1, labels)

def concat_datasets(datasets):
""" 本函数用于将不同文件的数据进行连接"""
s0 = []
s1 = []
labels = []
for s0x, s1x, labelsx in datasets:
s0 += s0x
s1 += s1x
labels += labelsx
#这里也要返回np.narray()
return (np.array(s0), np.array(s1), np.array(labels))

def load_set(glove, path):
'''读取所有文件'''
files = []
for file in os.listdir(path):
if os.path.isfile(path + '/' + file):
files.append(path + '/' + file)
s0, s1, labels = concat_datasets([load_sts(d, glove) for d in files])
#s0, s1, labels = np.array(s0), np.array(s1), np.array(labels)
#print('(%s) Loaded dataset: %d' % (path, len(s0)))
#e0, e1, s0, s1, labels = load_embedded(glove, s0, s1, labels)
return ([s0, s1], labels)


上面代码已经注释的很清楚了,这里对几个关键的地方进行介绍。

首先是要对句子进行PADDING,因为每个句子长度不同,而我们的模型构建时输入的placeholder尺寸要指定为[None, sentence_length],如果不指定的话会报错说使用sequence来给tensor赋值之类的,具体错误名称我忘了,反正大概就是tensorflow因为不知道shape无法将feed进来的变量复制给placeholder之类的。

将句子中的每个单词转化为其在word2vec(下面介绍)中的索引,注意这里千万不要直接将其转化为词向量,不然你可以试想一下两万个句子队,每个句子加入包含10个单词,每个单词转化为300维的float32变量,这将对你的内存造成何等的的伤害==别问我是怎么知道的。所以我们仅需将其转化为索引,这样句子所占内存很小,而词向量占用内存也很小,尽在程序运行的时候通过lookup进行查找即可!!!

每个句子都要使用np.array()进行转化,不然也会报跟第一个一样的问题。恩,反正都很麻烦不好处理。

将标签y直接转化为长度为6的数组即可。这样就可以直接将其作为模型的输出label。

经过上面的步骤,我们就将文件中的数据读取到了程序里面。直接调用load_set()函数即可,其返回结果是[s0, s1], labels。s0,s1和labels都是长度为20000+(即训练集大小)的嵌套列表,其中每个元素都是长度为100(句子长度)的单词索引列表、长度为6的标签列表。

2,词向量读入

接下来的任务就是读取word2vec与训练好的词向量,词向量文件如上图的glove.6B文件夹,里面有训练好的50,100,200,300维的词向量。读取词向量的函数写在embedding.py文件中,这是在网上看到了别人的代码截取了一部分,可以不用仔细看。直接使用
glove = emb.GloVe(N=50)
调用即可。只需要知glove.w是单词-索引的字典,glove.g是词向量就行了。本部分不做过多介绍

class Embedder(object):
def map_tokens(self, tokens, ndim=2):
gtokens = [self.g[self.w[t]] for t in tokens if t in self.w]
if not gtokens:
return np.zeros((1, self.N)) if ndim == 2 else np.zeros(self.N)
gtokens = np.array(gtokens)
if ndim == 2:
return gtokens
else:
return gtokens.mean(axis=0)

def map_set(self, ss, ndim=2):
""" apply map_tokens on a whole set of sentences """
return [self.map_tokens(s, ndim=ndim) for s in ss]

def map_jset(self, sj):
""" for a set of sentence emb indices, get per-token embeddings """
return self.g[sj]

def pad_set(self, ss, spad, N=None):
ss2 = []
if N is None:
N = self.N
for s in ss:
if spad > s.shape[0]:
if s.ndim == 2:
s = np.vstack((s, np.zeros((spad - s.shape[0], N))))
else:  # pad non-embeddings (e.g. toklabels) too
s = np.hstack((s, np.zeros(spad - s.shape[0])))
elif spad < s.shape[0]:
s = s[:spad]
ss2.append(s)
return np.array(ss2)

class GloVe(Embedder):
""" A GloVe dictionary and the associated N-dimensional vector space """
def __init__(self, N=50, glovepath='glove.6B/glove.6B.%dd.txt'):
self.N = N
self.w = dict()
self.g = []
self.glovepath = glovepath % (N,)

# [0] must be a zero vector
self.g.append(np.zeros(self.N))

with open(self.glovepath, 'r') as f:
for line in f:
l = line.split()
word = l[0]
self.w[word] = len(self.g)
self.g.append(np.array(l[1:]).astype(float))
self.w['UKNOW'] = len(self.g)
self.g.append(np.zeros(self.N))
self.g = np.array(self.g, dtype='float32')


3,tf中tensor的余弦距离计算

为什么要专门介绍这一部分呢?因为作为一个小白这个问题也困扰了很长时间。这部分是为了实现论文算法1和算法2。我们都知道卷积神经网络的输出是shape为[len, dim, 1, num_filters]的四维Tensor,而算法1、2都是要计算两个向量之间的余弦距离。那么如何实现呢?我们先来看一下代码:

#coding=utf8
import tensorflow as tf

def compute_l1_distance(x, y):
with tf.name_scope('l1_distance'):
d = tf.reduce_sum(tf.abs(tf.subtract(x, y)), axis=1)
return d

def compute_euclidean_distance(x, y):
with tf.name_scope('euclidean_distance'):
d = tf.sqrt(tf.reduce_sum(tf.square(tf.subtract(x, y)), axis=1))
return d

def compute_cosine_distance(x, y):
with tf.name_scope('cosine_distance'):
#cosine=x*y/(|x||y|)
#先求x,y的模 #|x|=sqrt(x1^2+x2^2+...+xn^2)
x_norm = tf.sqrt(tf.reduce_sum(tf.square(x), axis=1)) #reduce_sum函数在指定维数上进行求和操作
y_norm = tf.sqrt(tf.reduce_sum(tf.square(y), axis=1))
#求x和y的内积
x_y = tf.reduce_sum(tf.multiply(x, y), axis=1)
#内积除以模的乘积
d = tf.divide(x_y, tf.multiply(x_norm, y_norm))
return d

def comU1(x, y):
result = [compute_cosine_distance(x, y), compute_euclidean_distance(x, y), compute_l1_distance(x, y)]
#stack函数是将list转化为Tensor
return tf.stack(result, axis=1)

def comU2(x, y):
result = [compute_cosine_distance(x, y), compute_euclidean_distance(x, y)]
return tf.stack(result, axis=1)


具体调用时我们的操作是comU2(regM1[:, :, k], regM2[:, :, k])。其中regM1是一个三维的Tensor,具体的我们会在下节模型构建是进行介绍。譬如说regM的shape为【batch_size, 3, num_filters】则regM2[:, :, k]就是取出前面两维,然后comU2计算时会使用axis指定维度为1,即计算维度1上面三个值组成向量的相似度。这一部分需要好好理解一下,可以自己写一个test试一下,具体感受其功能和实际作用。

本部分就介绍到这里,下一节中我们会主要进行模型的构建和训练,并着重介绍在程序运行时每个阶段tensor的shape变化,更深层次的理解tf中每个函数的功能。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: