上一篇博客主要介绍了在文本在输入到模型前做的一系列必不可少的数据预处理操作。本篇博客主要介绍一下作为baseline的文本分类任务的模型在tf2.x框架下是如何构建的。
提到文本分类,现在基本都是想到深度学习,然后准确率比较高的方法就是预训练语言模型加微调,然后比较经典一点还有text_cnn,lstm等;本篇博客主要将text_cnn的构建情况,至于bert这类的预训练模型后续会专门再整体总结一下。
本篇博文主要将text_cnn,作为比较经典的模型,具备速度快,在短文本领域的准确率高等特点,是非常适合作为baseline的模型。下面先讲讲text_cnn原理,比较常见的一张论文图片也就是下图:
当原始句子进入模型后,首先经过预处理变成embedding的格式,在上图中每个词就是5维的向量,一个句子就变成了sequence_length*5的向量形式,然后这个向量会通过几种卷积核提取浅层语义特征,例如上图所示的三种不同形状的矩阵,它们的列维度和embedding的维度保持一致,但是行长度不一样,分别为2,3,4;这三种长度是比较常用的提取语义特征的卷积核向量,同时卷积核也可以设置不同的数量,像上图这样的就是每个卷积核的数量有两种,可以提取出两个特征向量。
接下来就是卷积计算和池化操作,卷积核与输入矩阵相乘得到特征向量,会得到(sequence_length-filter_size+1)*1的向量,然后每个向量进行最大池化操作,即取向量中数值最大的作为池化结果,最终会得到filter_size*filter_num个结果,将所有的结果拼接就得到最终的向量。
最后就是全连接层,也就是将上一步卷积池化等一系列计算后得到的最终向量首先通过全连接层降维到目标结果维度,例如上图中的任务是情感二分类,最终就要得到一个二维的向量,最后通过激活函数做数值缩放,将数值映射到0-1之间的范围。也就是可以成为概率的数值。
下面就是基于tf2.x的版本代码实现了,相比与1.x版本,2.x版本的代码相对清爽了很多,主要是继承tf.keras.Model这个父类进行子类模型构建,代码如下:
class TextCnn(tf.keras.Model):
'''
构建textcnn网络结构
'''
def __init__(self, config, vocab_size, word_vectors):
self.config = config
self.vocab_size = vocab_size
self.word_vectors = word_vectors
#输入层
word_ids = tf.keras.layers.Input(shape=(None, ), dtype=tf.int64, name='input_word_ids')
#embedding层
class GatherLayer(tf.keras.layers.Layer):
def __init__(self, config, vocab_size, word_vectors):
super(GatherLayer, self).__init__()
self.config = config
self.vocab_size = vocab_size
self.word_vectors = word_vectors
def build(self, input_shape):
with tf.name_scope('embedding'):
if not self.config['use_word2vec']:
self.embedding_w = tf.Variable(tf.keras.initializers.glorot_normal()(
shape=[self.vocab_size, self.config['embedding_size']],
dtype=tf.float32), trainable=True, name='embedding_w')
else:
self.embedding_w = tf.Variable(tf.cast(self.word_vectors, tf.float32), trainable=True,
name='embedding_w')
self.build = True
def call(self, indices):
return tf.gather(self.embedding_w, indices, name='embedded_words')
def get_config(self):
config = super(GatherLayer, self).get_config()
return config
# 利用词嵌入矩阵将输入数据转成词向量,shape=[batch_size, seq_len, embedding_size]
embedded_words = GatherLayer(config, vocab_size, word_vectors)(word_ids)
#卷积输入是四维向量,需要增加维度[batch_size, height, width, channels]
expanded_words = tf.expand_dims(embedded_words, axis=-1, name='expanded_words')
# 进行卷积和池化操作
pooled_outputs = []
# 创建几种不同尺寸的卷积核提取文本信息,通常使用长度为3,4,5等长度
for filter in self.config['conv_filters']:
# 初始化卷积核权重和偏置
# [filter_height, filter_width, in_channels, out_channels],先矩阵相乘然后求和
# filter_size = [filter, self.config['embedding_size'], 1, self.config['num_filters']]
#kenel_size [height, width]
kernel_size = [filter, self.config['embedding_size']]
#output_size
output_size = self.config['num_filters']
# conv_w = tf.keras.initializers.truncated_normal(filter_size, stddev=0.1)
# conv_b = tf.constant(0.1, shape=self.config['num_filters'])
h = tf.keras.layers.Conv2D(
filters=output_size,
kernel_size=kernel_size,
strides=(1,1),
kernel_initializer=tf.keras.initializers.truncated_normal(),
bias_initializer=tf.keras.initializers.constant(0.1),
activation='relu',
padding='VALID',
data_format="channels_last",
input_shape=(self.config['seq_len'], self.config['embedding_size'], 1)
)(expanded_words)
# relu函数的非线性映射,得到卷积的结果[batch_size, seq_len-filter+1, 1, num_filters]
# h = tf.keras.layers.Activation.relu(tf.keras.layers.Layer.bias_add(conv, conv_b))
# 池化层,最大池化
pooled = tf.keras.layers.MaxPooling2D(
# ksize,shape[batch_size, height, width, channels]
pool_size=[self.config['seq_len'] - filter+ 1, 1],
strides=(1,1),
padding='VALID',
data_format="channels_last"
)(h)
# 每种filter得到num_filters个池化结果
pooled_outputs.append(pooled)
# 最终输出的cnn长度
total_cnn_output = self.config['num_filters'] * len(self.config['conv_filters'])
# 将不同的卷积核根据channel concat起来
h_pool = tf.concat(pooled_outputs, 3)
# 摊平成二维输入到全连接层[batch_size, total_cnn_output]
h_pool_flat = tf.reshape(h_pool, [-1, total_cnn_output])
# dropout层
h_drop_out = tf.keras.layers.Dropout(self.config['dropout_rate'])(h_pool_flat)
# 全连接层的输出
with tf.name_scope('output'):
output_w = tf.keras.initializers.glorot_normal()(shape=[total_cnn_output, self.config['num_classes']])
output_b = tf.constant(0.1, shape=[self.config['num_classes']])
# self.l2_loss += tf.nn.l2_loss(output_w)
# self.l2_loss += tf.nn.l2_loss(output_b)
# self.logits = tf.matmul(h_drop_out, output_w) + output_b
self.logits = tf.keras.layers.Dense(self.config["num_classes"])(h_drop_out)
#输出为[batch_size, num_classes]
outputs = dict(logits=self.logits)
super(TextCnn, self).__init__(inputs=[word_ids], outputs=outputs)
完整的工程代码可以到https://github.com/dextroushands/nlp_tasks进行使用,包括模型训练保存预测一系列步骤。
参考文献:
《A Sensitivity Analysis of (and Practitioners’ Guide to) Convolutional Neural Networks for Sentence Classification》