TensorFlow 实现简单的TextCNN(回归)

直接上代码,因为此前都是使用keras,所以在过程中有一些和keras的对比(主要是使用习惯的对比,不是性能)。
import tensorflow as tf
import pandas as pd
import jieba
import numpy as np

当使用tensorflow的时候,要引用的包就很少了,就一个tensorflow就够用了,不像keras,需要从layers到activation都要手动引入。

def batch_iter(x, y, batch_size=64):
    """生成批次数据"""
    data_len = len(x)
    num_batch = int((data_len - 1) / batch_size) + 1
    
    indices = np.random.permutation(np.arange(data_len))
    x_shuffle = x[indices]
    y_shuffle = y[indices]

    for i in range(num_batch):
        start_id = i * batch_size
        end_id = min((i + 1) * batch_size, data_len)
        yield x_shuffle[start_id:end_id], y_shuffle[start_id:end_id]

定义生成batch的函数,不同于keras,tf的batch需要自己生成,然后在每一个batch和每一个epoch都要手动feed_dict。

def feed_data(x_batch, y_batch):
    feed_dict = {
        input_x: x_batch,
        input_y: y_batch,
    }
    return feed_dict
定义的feed_data是用于每次输入数据到模型中,可有可无。
embedding_dim = 64  # 词向量维度
seq_length = 100  # 序列长度
num_filters = 32  # 卷积核数目
filter_sizes = [3,4,5] # 卷积核尺寸
vocab_size = 80000  # 词汇表达小
dropout_keep_prob = 0.5  # dropout保留比例
learning_rate = 1e-3  # 学习率
batch_size = 64  # 每批训练大小
num_epochs = 10  # 总迭代轮次
print_per_batch = 100  # 每多少轮输出一次结果
save_per_batch = 10  # 每多少轮存入tensorboard

定义模型中常用的参数,其中需要注意的,是filter_sizes,根据textcnn的原作,这里的filter是用了3个,然后再将各自的结果merge起来,所以这里需要3个不同尺寸的filter,用来捕捉不同长度的词语(相当于ngram吧)组合的特征。

with tf.name_scope('input_layer'):
    input_x = tf.placeholder(dtype=tf.int32,shape=[None,seq_length],name='input_x')
    input_y = tf.placeholder(dtype=tf.float32,shape=[None,1],name='input_y')
with tf.device('/cpu:0'):
    embedding = tf.get_variable('embedding5',[vocab_size,embedding_dim])
    embeddign_inputs = tf.nn.embedding_lookup(embedding,input_x)
    embedded_expaned = tf.expand_dims(embeddign_inputs,-1)

首先定义了一个name_scope,用于隔离不同空间之下的变量。

然后声明一个用cpu来处理的操作,而就这一句代码让我理解了整个embeedding的工作原理。因为keras并没有这个embedding_lookup的操作。所以我一直不明白vocab_size,和我的sequence_length的关系。现在有了这个lookup就容易理解很多了。

for i, filter_size in enumerate(filter_sizes):
    with tf.name_scope("conv-maxpool-%s" % filter_size):
        pooled_outputs = []
        for i, filter_size in enumerate(filter_sizes):
            filter_shape = [filter_size, embedding_dim, 1, num_filters]
            W = tf.Variable(tf.truncated_normal(filter_shape, stddev=0.1), name="W")
            b = tf.Variable(tf.constant(0.1, shape=[num_filters]), name="b")
            conv1 = tf.nn.conv2d(input=embedded_expaned,
                                 filter=W,
                                 strides=[1,1,1,1],
                                 padding='VALID',
                                 name='conv')
            h =  tf.nn.relu(tf.nn.bias_add(conv1,b),name='relu')
            max_pool = tf.nn.max_pool(
                h,
                ksize=[1,seq_length-filter_size+1,1,1],
                strides= [1,1,1,1],
                padding='VALID',
                name='max_pool'
            )
            pooled_outputs.append(max_pool)
num_filters_total = num_filters * len(filter_sizes)
h_pool = tf.concat(pooled_outputs,3)
h_pool_flat  =tf.reshape(h_pool,shape=[-1,num_filters_total])

可以说这个TextCNN的精髓所在,但其实在这种情况下还是很容易理解的(这种情况对应的是使用的预训练好的词向量)。

首先是对于不同size的filter,设定不同的name_scope。然后是定义filter_shape,这个是用来生成下面的权重。这里的权重,其实就是最后将要生成的embedding层,而如果是预训练好的词向量,这里的W就是全1的矩阵。接下来就进行conv的操作,这里和图像处理的无异。max_pool的处理也和图像的没差别。将几个size的filter处理好后,就将结果存起来。

然后将所有结果flat起来,用于连接下面的全连接层。

with tf.name_scope('dropout'):
    h_drop = tf.nn.dropout(h_pool_flat,0.5)
with tf.name_scope('output'):
    W = tf.get_variable(
        "W4",
        shape=[num_filters_total,1],
        initializer= tf.contrib.layers.xavier_initializer(),
    )
    b = tf.Variable(tf.constant(0.1,shape=[1]),name="b")
    l2_loss +=tf.nn.l2_loss(W)
    l2_loss +=tf.nn.l2_loss(b)
    scores = tf.nn.xw_plus_b(h_drop,W,b)
with tf.name_scope('loss'):
    losses = mean_squared_error(scores,target=input_y,is_mean=True)
with tf.name_scope("optimize"):
    optim = tf.train.GradientDescentOptimizer(learning_rate=learning_rate).minimize(losses)

全连接层接的之前先连接dropout,loss函数加上l2正则化的W和b,因为我这里做的是回归任务,所以目标函数是一个自定义的mse。接下来就优化目标函数。

for epoch in range(15):
    print('Epoch:', epoch + 1)
    batch_train = batch_iter(data[:train.shape[0]], train['score'].values, 32)
    for x_batch, y_batch in batch_train:
        y_batch = np.reshape(y_batch,(-1,1))
        feed_dict = feed_data(x_batch, y_batch)
        session.run(optim, feed_dict=feed_dict)  # 运行优化

运行的代码如上,这里略去了数据预处理过程。

写这篇文章主要是为了记录自己的思路,有缘人看到有啥问题的话欢迎留言。

以后会尽量封装好,包括可以选择是否用预训练的词向量,以及训练的可视化等等。

你可能感兴趣的:(实战)