CNN在句子相似性建模的应用--tensorflow实现篇1
2017-03-20 21:38
411 查看
经过上周不懈的努力,终于把“Multi-Perspective Sentence Similarity Modeling with Convolution Neural Networks”这篇论文用tensorflow大致实现出来了,代码后续回放到我的github上面。踩了很多坑,其实现在也还有一些小的问题没有搞明白和解决,但是也算自己实现了第一个完整的Tensorflow程序,至于剩下的一些小问题,接下来慢慢边学习边解决吧。因为代码比较长,我们分为两篇来介绍,本篇主要介绍读入数据和一些功能函数。好了,接下来我们就开始介绍代码吧。
其中all文件夹中包含了2012-2015之间的所有数据,我们是用all作为训练集,2016作为测试集。共有20000多条训练数据和1000多条测试数据,论文中说有10000多条训练数据,我也不知道为什么不同。但是这并不作为本文的关注点(仅以使用tensorflow实现论文提到的模型为主,至于准确率等并未考虑在内)。接下来介绍数据的读取代码,这部分代码位于data_helper.py文件中:
上面代码已经注释的很清楚了,这里对几个关键的地方进行介绍。
首先是要对句子进行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的标签列表。
具体调用时我们的操作是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,数据集介绍与读取
首先介绍一下数据集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中每个函数的功能。
相关文章推荐
- CNN在句子相似性建模的应用--tensorflow实现篇2
- CNN在句子相似性建模的应用续--基于attention的多角度CNN模型
- CNN在句子相似性建模的应用--模型介绍篇
- Language Modeling with Gated Convolutional Networks(句子建模之门控CNN)--Tensorflow实现篇
- 卷积神经网络(CNN)在句子建模上的应用
- CNN模型和RNN模型在分类问题中的应用(Tensorflow实现)
- 卷积神经网络(CNN)在句子建模上的应用
- CNN与句子分类之动态池化方法DCNN--TensorFlow实现篇
- 卷积神经网络(CNN)在句子建模上的应用
- tensorflow实现CNN
- Implementing a CNN for Text Classification in TensorFlow(用tensorflow实现CNN文本分类) 阅读笔记
- TensorFlow学习笔记--CNN精要及实现
- 利用 TensorFlow 高级 API Keras 实现 MLP,CNN,LSTM
- 用Tensorflow实现CNN手写数字识别
- tensorflow实现CNN
- TensorFlow教程06:MNIST的CNN实现——源码和运行结果
- TensorFlow实现CNN
- tensorflow实现基于CNN残差网络的一个简化版YOLO
- CNN对句子分类(tensorflow)
- TensorFlow教程06:MNIST的CNN实现——源码和运行结果