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

作者:liuchonge

经过上周不懈的努力,终于把“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=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中每个函数的功能。

发表评论

4个评论

  • luhaojian2011

    请问一下博主的 'glove.6B/glove.6B.%dd.txt' 这个文件在哪里找呢?谢谢

    2017-04-24 15:24:28回复

  • liuchonge

    回复luhaojian2011: 斯坦福官网有下载链接,https://nlp.stanford.edu/projects/glove/http://nlp.stanford.edu/data/glove.6B.zip。

    2017-04-24 15:32:37回复

  • weixin_36741382

    楼主,您好,可以分享您的glove.6B文件吗,找不到合适的,谢谢~~~

    2017-04-24 14:42:57回复

  • liuchonge

    回复weixin_36741382: 斯坦福官网有下载链接,https://nlp.stanford.edu/projects/glove/http://nlp.stanford.edu/data/glove.6B.zip。

    2017-04-24 15:32:41回复

我要留言×

技术领域:

我要留言×

留言成功,我们将在审核后加至投票列表中!

提示x

人工智能开发框架知识库已成功保存至我的图谱现在你可以用它来管理自己的知识内容了

删除图谱提示×

你保存在该图谱下的知识内容也会被删除,建议你先将内容移到其他图谱中。你确定要删除知识图谱及其内容吗?

删除节点提示×

无法删除该知识节点,因该节点下仍保存有相关知识内容!

删除节点提示×

你确定要删除该知识节点吗?