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

来源:互联网 发布:恒创的域名服务器 编辑:程序博客网 时间:2024/06/11 03:25

经过上周不懈的努力,终于把“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)

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

  1. 首先是要对句子进行PADDING,因为每个句子长度不同,而我们的模型构建时输入的placeholder尺寸要指定为[None, sentence_length],如果不指定的话会报错说使用sequence来给tensor赋值之类的,具体错误名称我忘了,反正大概就是tensorflow因为不知道shape无法将feed进来的变量复制给placeholder之类的。
  2. 将句子中的每个单词转化为其在word2vec(下面介绍)中的索引,注意这里千万不要直接将其转化为词向量,不然你可以试想一下两万个句子队,每个句子加入包含10个单词,每个单词转化为300维的float32变量,这将对你的内存造成何等的的伤害==别问我是怎么知道的。所以我们仅需将其转化为索引,这样句子所占内存很小,而词向量占用内存也很小,尽在程序运行的时候通过lookup进行查找即可!!!
  3. 每个句子都要使用np.array()进行转化,不然也会报跟第一个一样的问题。恩,反正都很麻烦不好处理。
  4. 将标签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=utf8import tensorflow as tfdef compute_l1_distance(x, y):    with tf.name_scope('l1_distance'):        d = tf.reduce_sum(tf.abs(tf.subtract(x, y)), axis=1)        return ddef 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 ddef 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 ddef 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中每个函数的功能。

1 0