您的位置:首页 > 其它

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

2017-03-21 15:55 435 查看
上节已经介绍了数据预处理部分代码,本节则详细介绍一下模型构建和训练部分。旨在以一个新手的角度出发,详细介绍一下tf中不同类型cnn的使用方法以及其中tensor的shape变化,来感受一下其运行过程的细节。代码可以在我的github上下载

模型初始化

首先我们介绍一下模型构建部分。这部分代码全部写在model.py文件中。为了方便调用,我们写了一个MPCNN_Layer类来实现整个论文模型的构建。首先是init函数,主要用于初始化一些权重参数等,代码入下:

#初始化一些变量主要包括了各层之间的权重,偏置。
def __init__(self, num_classes, embedding_size, filter_sizes, num_filters, n_hidden,
input_x1, input_x2, input_y, dropout_keep_prob):
'''

:param num_classes:  6,代表6种类别。即输出y的维度
:param embedding_size: 词向量维度
:param filter_sizes: 卷积窗口大小。此处为列表【1,2,100】100表示对整个句子直接卷积。
:param num_filters: 卷积核数量,这里为列表【num_filters_A,num_filters_B】分别为20,20.论文中A为300
:param n_hidden:全连接层的神经元个数
:param input_x1:输入句子矩阵。shape为【batch_size,sentence_length, embed_size,1】
:param input_x2:同inpt_x1
:param input_y:输出6维的array。one-hot编码
:param dropout_keep_prob:dropout比率
'''
self.embedding_size = embedding_size
self.filter_sizes = filter_sizes
self .num_filters = num_filters
self.poolings = [tf.reduce_max, tf.reduce_min, tf.reduce_mean]

self.input_x1 = input_x1
self.input_x2 = input_x2
self.input_y = input_y
self.dropout_keep_prob = dropout_keep_prob
#Block_A的参数。因为有三种窗口尺寸,所以初始化三个参数
self.W1 = [init_weight([filter_sizes[0], embedding_size, 1, num_filters[0]]),
init_weight([filter_sizes[1], embedding_size, 1, num_filters[0]]),
init_weight([filter_sizes[2], embedding_size, 1, num_filters[0]])]
self.b1 = [tf.Variable(tf.constant(0.1, shape=[num_filters[0]])),
tf.Variable(tf.constant(0.1, shape=[num_filters[0]])),
tf.Variable(tf.constant(0.1, shape=[num_filters[0]]))]
# Block_B的参数。这里只需要两种窗口尺寸(舍弃100的窗口),所以初始化两个参数
self.W2 = [init_weight([filter_sizes[0], embedding_size, 1, num_filters[1]]),
init_weight([filter_sizes[1], embedding_size, 1, num_filters[1]])]
self.b2 = [tf.Variable(tf.constant(0.1, shape=[num_filters[1], embedding_size])),
tf.Variable(tf.constant(0.1, shape=[num_filters[1], embedding_size]))]
#卷积层经过句子相似计算之后的输出flatten之后的尺寸。用于生成隐藏层的参数。具体会在后面介绍
self.h = num_filters[0]*len(self.poolings)*2 + \
num_filters[1]*(len(self.poolings)-1)*(len(filter_sizes)-1)*3 + \
len(self.poolings)*len(filter_sizes)*len(filter_sizes)*3
#全连接层参数
self.Wh = tf.Variable(tf.random_normal([self.h, n_hidden], stddev=0.01), name='Wh')
self.bh = tf.Variable(tf.constant(0.1, shape=[n_hidden]))
#输出层参数
self.Wo = tf.Variable(tf.random_normal([n_hidden, num_classes], stddev=0.01), name='Wo')


Block_A和Block_B的构建

接下来进行Block_A的构建,这个比较简单,直接调用conv2d函数即可。如下图所示,代码入下:



def bulit_block_A(self, x):
#bulid block A and cal the similarity according to algorithm 1
out = []
with tf.name_scope("bulid_block_A"):
#遍历每个pooling方式,max、min、mean
for pooling in self.poolings:
pools = []
#每个pooling都对应几种不同的窗口大小。【1,2,100】
for i, ws in enumerate(self.filter_sizes):
#print x.get_shape(), self.W1[i].get_shape()
with tf.name_scope("conv-pool-%s" %ws):
#x->[batch_size, sentence_length, embed_size, 1], W1[i]->[ws, embed_size, 1, num_filters]
conv = tf.nn.conv2d(x, self.W1[i], strides=[1, 1, 1, 1], padding="VALID")
#print conv.get_shape()
conv = tf.nn.relu(conv + self.b1[i])  # [batch_size, sentence_length-ws+1, 1, num_filters_A]
pool = pooling(conv, axis=1) ## [batch_size, 1, num_filters_A]
pools.append(pool)
out.append(pools)
return out


所以Block_A的输出out是一个嵌套列表。第一层长度为3,代表三种pooling方式,第二层长度为3,代表三种卷积窗口大小,然后其中每个元素都是[batch_size, 1, num_filters_A]的三维Tensor。

接下来看Block_B部分的构建。由于此处用到了per_dim的卷积核,所以比较麻烦。图和代码如下所示。



def per_dim_conv_layer(self, x, w, b, pooling):
'''
实现per_dim卷积,请结合模型介绍篇的图片进行理解
:param input: [batch_size, sentence_length, embed_size, 1]
:param w: [ws, embedding_size, 1, num_filters]
:param b: [num_filters, embedding_size]
:param pooling:
:return:
'''
# 为了实现per_dim的卷积。所以我们要将输入和权重偏置参数在embed_size维度上进行unstack
#这样我们就获得了每个维度上的输入、权重、偏置。可以结合模型介绍篇里面的图片进行理解
input_unstack = tf.unstack(x, axis=2)
w_unstack = tf.unstack(w, axis=1)
b_unstack = tf.unstack(b, axis=1)
convs = []
#对每个embed_size维度进行卷积操作
for i in range(x.get_shape()[2]):
#conv1d要求三维的输入,三维的权重(没有宽度,只有长度。所以称为1d卷积)。具体可以参见官方API。
conv = tf.nn.relu(tf.nn.conv1d(input_unstack[i], w_unstack[i], stride=1, padding="VALID") + b_unstack[i])
# [batch_size, sentence_length-ws+1, num_filters_A]
convs.append(conv)
#将embed_size个卷积输出在第三个维度上进行进行stack。所以又获得了一个4位的tensor
conv = tf.stack(convs, axis=2)  # [batch_size, sentence_length-ws+1, embed_size, num_filters_A]
#池化。即对第二个维度的sentence_length-ws+1个值取最大、最小、平均值等操作、
pool = pooling(conv, axis=1)  # [batch_size, embed_size, num_filters_A]

return pool

def bulid_block_B(self, x):
out = []
with tf.name_scope("bulid_block_B"):
for pooling in self.poolings[:-1]:
pools = []
for i, ws in enumerate(self.filter_sizes[:-1]):
with tf.name_scope("per_conv-pool-%s" % ws):
pool = self.per_dim_conv_layer(x, self.W2[i], self.b2[i], pooling)
pools.append(pool)
out.append(pools)
return out


其中bulid_block_B函数作用与bulid_block_A相同不做介绍。而 per_dim_conv_layer函数则实现了per_dim卷积方法。注释已经很清楚了。所以Block_B的输出out也是一个嵌套列表。第一层长度为2,代表两种pooling方式,第二层长度为2,代表两种卷积窗口大小,然后其中每个元素都是[batch_size, 50, num_filters_B]的三维Tensor。

相似性测量层的构建

经过上面两个部分的构建,我们就应实现了句子表示部分的代码。接下来就是实现相似测量层的两个算法。如下图所示:





def similarity_sentence_layer(self):
#对输入的两个句子进行构建block_A。
#sent1,2都是3*3*[batch_size,1, num_filters_A]的嵌套列表
sent1 = self.bulit_block_A(self.input_x1)
sent2 = self.bulit_block_A(self.input_x2)
fea_h = []
#实现算法1
with tf.name_scope("cal_dis_with_alg1"):
for i in range(3):
#将max,men,mean三个进行连接
regM1 = tf.concat(sent1[i], 1)
regM2 = tf.concat(sent2[i], 1)
#按照每个维度进行计算max,men,mean三个值的相似度。可以参考图中绿色框
for k in range(self.num_filters[0]):
#comU2计算两个tensor的距离,参见上篇博文,得到一个(batch_size,2)的tensor。2表示余弦距离和L2距离
fea_h.append(comU2(regM1[:, :, k], regM2[:, :, k]))
#得到fea_h是一个长度3*20=60的list。其中每个元素都是(batch_size,2)的tensor
fea_a = []
#实现算法2的2-9行
with tf.name_scope("cal_dis_with_alg2_2-9"):
for i in range(3):
for j in range(len(self.filter_sizes)):
for k in range(len(self.filter_sizes)):
# comU1计算两个tensor的距离,参见上篇博文,上图中的红色框。得到一个(batch_size,3)的tensor。3表示余弦距离和L2距离,L1距离
fea_a.append(comU1(sent1[i][j][:, 0, :], sent2[i][k][:, 0, :]))
#得到fea_a是一个长度为3*3*3=27的list。其中每个元素是(batch_size,3)的tensor

# 对输入的两个句子进行构建block_B。
# sent1,2都是2*2*[batch_size,50, num_filters_B]的嵌套列表
sent1 = self.bulid_block_B(self.input_x1)
sent2 = self.bulid_block_B(self.input_x2)

fea_b = []
# 实现算法2的剩余行
with tf.name_scope("cal_dis_with_alg2_last"):
for i in range(len(self.poolings)-1):
for j in range(len(self.filter_sizes)-1):
for k in range(self.num_filters[1]):
fea_b.append(comU1(sent1[i][j][:, :, k], sent2[i][j][:, :, k]))
##得到fea_b是一个长度为2*2*20=80的list。其中每个元素是(batch_size,3)的tensor
return tf.concat(fea_h + fea_a + fea_b, 1)


经过相似计算函数我们就获得了两个句子的相似性向量fea_h,fea_a,fea_b,通过tf.concat()函数就可以将其进行连接得到一个flatten的相似性输出向量fea。这里需要注意的问题是fea维度的计算,我们之前已经知道了fea_h,fea_a,fea_b的维度,分别是60*[batch_size, 2]、27*[batch_size, 3]、80*[batch_size, 3],那么首先将其连接,然后再第一个维度上进行扩展,最终就可以获得一个shape是[batch_size, 441]的Tensor。

全连接层构建

有了fea,接下来的工作就简单了,我们只需要将其输入全连接层即可完成模型的构建工作。代码入下:

def similarity_measure_layer(self):
#调用similarity_sentence_layer函数获得句子的相似性向量
fea = self.similarity_sentence_layer()
# fea_h.extend(fea_a)
# fea_h.extend(fea_b)
#print len(fea_h), fea_h
#fea = tf.concat(fea_h+fea_a+fea_b, 1)
#print fea.get_shape()
with tf.name_scope("full_connect_layer"):
h = tf.nn.tanh(tf.matmul(fea, self.Wh) + self.bh)
h = tf.nn.dropout(h, self.dropout_keep_prob)
o = tf.matmul(h, self.Wo)
return o


模型训练

至此我们就完成了模型的搭建工作。接下来就可以进行训练了。这部分代码写在train1.py文件中,主要完成了词向量、训练、测试数据的读取,模型的定义和训练,Summary的记录等工作。是一个比较标准的tf执行流程。这里也推荐大家使用app.flags来定义模型的超参数,这样一来之后修改参数会很方便,修改一个地方就可以了,免得改来改去还总是报错。app定义如下:

tf.app.flags.DEFINE_integer('embedding_dim', 50, 'The dimension of the word embedding')
tf.app.flags.DEFINE_integer('num_filters_A', 20, 'The number of filters in block A')
tf.app.flags.DEFINE_integer('num_filters_B', 20, 'The number of filters in block B')
tf.app.flags.DEFINE_integer('n_hidden', 150, 'number of hidden units in the fully connected layer')
tf.app.flags.DEFINE_integer('sentence_length', 100, 'max size of sentence')
tf.app.flags.DEFINE_integer('num_classes', 6, 'num of the labels')

tf.app.flags.DEFINE_integer('num_epochs', 10, 'Number of epochs to be trained')
tf.app.flags.DEFINE_integer('batch_size', 32, 'size of mini batch')

tf.app.flags.DEFINE_integer("display_step", 100, "Evaluate model on dev set after this many steps (default: 100)")
tf.app.flags.DEFINE_integer("evaluate_every", 100, "Evaluate model on dev set after this many steps (default: 100)")
tf.app.flags.DEFINE_integer("checkpoint_every", 100, "Save model after this many steps (default: 100)")
tf.app.flags.DEFINE_integer("num_checkpoints", 5, "Number of checkpoints to store (default: 5)")

tf.app.flags.DEFINE_float('lr', 1e-3, 'learning rate')
tf.app.flags.DEFINE_float('l2_reg_lambda', 1e-4, 'regularization parameter')

tf.app.flags.DEFINE_boolean("allow_soft_placement", True, "Allow device soft device placement")
tf.app.flags.DEFINE_boolean("log_device_placement", False, "Log placement of ops on devices")

filter_size = [1,2,100]
conf = tf.app.flags.FLAGS
conf._parse_flags()


接下来就是进行数据读入和模型构建工作。都很简单不做过多介绍,代码入下:

#glove是载入的次向量。glove.d是单词索引字典<word, index>,glove.g是词向量矩阵<词个数,300>
glove = emb.GloVe(N=50)
#-------------------------------------Loading data----------------------------------------------#
print ("Loading data...")
Xtrain, ytrain = load_set(glove, path='./sts/semeval-sts/all')
#[22592, 句长]
Xtest, ytest = load_set(glove, path='./sts/semeval-sts/2016')
#[1186, 句长]
#-------------------------------------Loading finished----------------------------------------------#

#-------------------------------------training the network----------------------------------------------#
with tf.Session() as sess:
#定义输入输出等placeholder
input_1 = tf.placeholder(tf.int32, [None, conf.sentence_length], name="input_x1")
input_2 = tf.placeholder(tf.int32, [None, conf.sentence_length], name="input_x2")
input_3 = tf.placeholder(tf.int32, [None, conf.num_classes], name="input_y")
dropout_keep_prob = tf.placeholder(tf.float32, name="dropout_keep_prob")

#通过embedding_lookup进行单词索引-》词向量的转换。glove.g是词向量矩阵
s0_embed = tf.nn.embedding_lookup(glove.g, input_1)
s1_embed = tf.nn.embedding_lookup(glove.g, input_2)

input_x1 = tf.reshape(s0_embed, [-1, conf.sentence_length, conf.embedding_dim, 1])
input_x2 = tf.reshape(s1_embed, [-1, conf.sentence_length, conf.embedding_dim, 1])
input_y = tf.reshape(input_3, [-1, conf.num_classes])

#构建MPCNN模型
setence_model = MPCNN_Layer(conf.num_classes, conf.embedding_dim, filter_size,
[conf.num_filters_A, conf.num_filters_B], conf.n_hidden,
input_x1, input_x2, input_y, dropout_keep_prob)
#调用similarity_measure_layer函数获得模型输出
out = setence_model.similarity_measure_layer()

#使用softmax交叉熵函数计算cost
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=out, labels=setence_model.input_y))
train_step = tf.train.AdamOptimizer(conf.lr).minimize(cost)

#获得预测和精确度
predict_op = tf.argmax(out, 1)

acc = tf.reduce_mean(tf.cast(tf.equal(tf.argmax(input_y, 1), tf.argmax(out, 1)), tf.float32))

timestamp = str(int(time.time()))
out_dir = os.path.abspath(os.path.join(os.path.curdir, "runs", timestamp))
print("Writing to {}\n".format(out_dir))

loss_summary = tf.summary.scalar("loss", cost)
acc_summary = tf.summary.scalar("accuracy", acc)

train_summary_op = tf.summary.merge([loss_summary, acc_summary])
train_summary_dir = os.path.join(out_dir, "summaries", "train")
train_summary_writer = tf.summary.FileWriter(train_summary_dir, sess.graph)

dev_summary_op = tf.summary.merge([loss_summary, acc_summary])
dev_summary_dir = os.path.join(out_dir, "summaries", "dev")
dev_summary_writer = tf.summary.FileWriter(dev_summary_dir, sess.graph)

checkpoint_dir = os.path.abspath(os.path.join(out_dir, "checkpoints"))
checkpoint_prefix = os.path.join(checkpoint_dir, "model")
if not os.path.exists(checkpoint_dir):
os.makedirs(checkpoint_dir)
saver = tf.train.Saver(tf.global_variables(), max_to_keep=conf.num_checkpoints)

init = tf.global_variables_initializer().run()

for j in range(10):
for i in range(0, 20000, conf.batch_size):
x1 = Xtrain[0][i:i + conf.batch_size]
x2 = Xtrain[1][i:i + conf.batch_size]
y = ytrain[i:i + conf.batch_size]
_, summaries, accc, loss = sess.run([train_step, train_summary_op, acc, cost],
feed_dict={input_1: x1, input_2: x2, input_3: y, dropout_keep_prob: 0.5})
time_str = datetime.datetime.now().isoformat()
print("{}: loss {:g}, acc {:g}".format(time_str, loss, accc))
train_summary_writer.add_summary(summaries)
print("\nEvaluation:")
accc = sess.run(acc, feed_dict={input_1: Xtest[0], input_2: Xtest[1], input_3: ytest, dropout_keep_prob: 1.0})
print "test accuracy:", accc


一些效果图

执行上面的代码我们就可以去tensorboard查看我们程序运行的效果了。先贴几张模型架构图:



下面这张是Block_B的内部架构,其实蓝色方框内还有embed_size个per_dim的卷积核,由于太多了所以不进行展示:



再来一张Block_A的内部架构,里面一共9个卷积-pooling层:



等等吧,还有很多可以去细看的图,这里就不一一列举了。接下来说一下程序运行不理想的地方==那就是准确度了。这里由于很多参数并未按照论文中所说的那样设置,也有可能是被的一些原因导致准确度上不去,如下图所示。总是震荡,而无法收敛。请教了一些朋友,改了改学习率啊、dropout啊、batch_size啊等等,都收效甚微。但是本着写程序练手的态度就不纠结与这些细节了,也是因为每一次训练成本都比较高。自己懒得去一步步调参了==不过还是贴出来两张训练的结果吧。





最后的总结

好了,这样我们就算勉强完成了对该论文的方针实现啦(捂脸)。在总结一下几个需要注意的点,也是我在编程序的时候遇到的坑:

在程序编写过程中一定要对各个变量的shape聊熟于心,刚入门的要多看官方文档,把一些重要的函数用法和作用了解的比较透彻,这样才能在使用的时候知道用哪个函数,怎么用。

词向量千万不要在读取训练集的时候就转化,否则太消耗内存了。要是用tf提供的embedding_lookup函数在训练的过程中在进行转化

输入要转化为np数组才行,不然feed的时候会报错。

变量、每一个比较重要的部分都是用tf.name_scope()函数命名,不然最后你会发现TensorBoard上面的结构图夸张复杂到无法加载。然后就是在TensorBoard架构图页面右侧的模块可以右键添加到左侧,这样才能形成一个完整的流图,不然你看到的可能是一个个分离的模块。

至于模型调参的问题,我还要继续慢慢研究,感觉还有很长的路要走

看教程的时候要自己动手写一遍,甚至调试一边,这样会收获更多

对了,说到调试,跑程序的时候遇到了几个问题,很棘手,比如cost是NAN,当时想着用tfdbg调试呢,但是环境老是报错,最终无奈放弃了。改用调整学习率等参数解决了。但是我觉得tfdbg看起来很好用,决定最近抽时间看一看。配置好。

还有很多遇到的问题,但是一时想不起来了,以后再说吧。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: