2019-02 Classification in NLP

每年在分类上的paper不断,我主要罗列一些我觉得还行的分类模型吧。

1. Self-Attention based Bidirection LSTM for Text Classification

2. Transformer for Text Classification

 以上两种模型的思想原理都在前面讲过:https://www.jianshu.com/p/f169eaec9b8b

3. Adversarial training methods for supervised Text Classification

以上paper地址:Adversarial training methods for supervised Text Classification
加了对抗的loss后使得分类的效果似乎提升的很大,所以这里就将这模型罗列在这里。下图就是该模型的框图:

The model with perturbed embeddings.png

上图模型能够有效的地方有两个关键点:

  1. 在输入的时候加上一个扰动 r ,使得模型更具备健壮性。
  2. 这里,词向量如果是trainable的,所以为了防止模型学习到较大膜的词向量而抵消扰动的作用,需要将词向量进行归一化。
Normalization Equation.png

该模型的loss有两个部分loss组成:
  ,对于这个公式和以下公式的理解为:当在 Word Embedding 上添加的 Perturbation 使得很大时,由于计算 的参数是不参与梯度计算的,也就是说 模型 (LSTM 和最后的 Dense Layer) 的 Weight 和 Bias 的改变并不会影响,模型只能通过改变 Word Embedding 来努力降低。

Cost Equation.png

当然,r 的计算并不是那么简单,但可以通过如下的公式等效的表示:

The computation of r .png
   def _add_perturbation(self, embedded, loss):
        """Adds gradient to embedding"""
        grad, = tf.gradients(loss,
                             embedded,
                             aggregation_method=tf.AggregationMethod.EXPERIMENTAL_ACCUMULATE_N)
        grad = tf.stop_gradient(grad)  ## 用来阻挡计算 grad里的参数更新,也就是说一旦加入了perturbation就只会更新embedded这个参数
        perturb = scale_l2(grad, self.epsilon)
        return embedded + perturb

总的来说,该模型作用是:

  1. 对抗训练可以使得句子的语义不会因为微小的变动而大相径庭,也就是说有着相似语法的但是有不同语义的句子向量空间会分离的较大。
  2. 不能把单纯的加微小扰动的操作看作是加上noise,因为加噪的正则化效果是远差于加对抗loss产生的正则化效果的。
  3. 词向量在训练后会变得更加精准。

4. RMDL: Random Multimodel Deep Learning for Classification

以上paper地址:https://arxiv.org/abs/1805.01890
这个模型效果是远超于以上模型的,我就拿来仔细看了半天,结果是篇含金量不高的文章,就是一个Emsemble的基本思想,在DNN,RNN,CNN中进行类别的多数投票。毕竟Emsemble大法好啊,确实可以秒杀很多单一模型的效果。其框图如下:

RMDL.png

不过怎么说,其中的Keras代码还是值得借鉴的!

5. Deep Pyramid Convolutional Neural Networks for Text Categorization (DPCNN)

Three Models.png

上图中可以看出,DPCNN既保村了一般TextCNN的conv层的处理类型方式,又采取了ResNet的残差连接方式。这是深层的word-level的创新之作。其每个部分的结构描述如下:

  1. region embedding:
      首先,这里不是完全和TextCNN的conv层一样,是不保留词序 (即使用词袋模型) ,即首先对N-gram中的N个词的embedding取均值得到一个size=D的向量,然后设置一组size=D的一维卷积核对该N-gram进行卷积。
      其次,再是类似bert的预训练方式,通过预测下一句的embedding的方式进行无监督的训练。
  2. 3 conv;250:对上述的embedding每3个做conv,但是feature map不变就是250。但是得做等长的卷积。
  3. 1/2 pooling: 就是对上述的conv结果每2个进行max-pooling,得到以前embedding的一半长度。对第2,3步的过程描述图如下:
  4. Shortcut connections with pre-activation: 就是借鉴resnet的方式进行残差的连接。


    image.png

Note: paper中阐述的值得注意的地方有很多,比如

  1. 对于conv来说,一般我们习惯先将x作weighting之后,再作activation。但是paper中确说先对x作activation,再作weighting效果会更好。
  2. 对于开始的region embedding的选取方法也是要进行实验甄选的,可以bow input或者bag-of-n-gram input或者就是单个词向量。
  3. 该模型设计的很巧妙,其实在region embedding之后就不是用conv层在抽feature了。而是通过使用conv层的并行化优势,充当了fc层进行分类层的设计,这样作用就是捕捉长距离特征之间的关系,通过加weight,可以使得分类器更加精准。

代码如下:

    def inference(self):
        # 词向量映射
        isTraining = True
        with tf.name_scope("embedding"):
            embedding = tf.get_variable("embedding", [self.vocab_size, self.embedding_dim])
            embedding_inputs = tf.nn.embedding_lookup(embedding, self.input_x)
            embedding_inputs = tf.expand_dims(embedding_inputs, axis=-1)  # [None,seq,embedding,1]
            # region_embedding  # [batch,seq-3+1,1,250]
            region_embedding = tf.layers.conv2d(embedding_inputs,
                                                self.num_filters,
                                                [self.kernel_size, self.embedding_dim])

            pre_activation = tf.nn.relu(region_embedding, name='preactivation')

        with tf.name_scope("conv3_0"):
            conv3 = tf.layers.conv2d(pre_activation,
                                     self.num_filters,
                                     self.kernel_size,
                                     padding="same",
                                     activation=tf.nn.relu)
            conv3 = tf.layers.batch_normalization(conv3, training=isTraining)  ##

        with tf.name_scope("conv3_1"):
            conv3 = tf.layers.conv2d(conv3,
                                     self.num_filters,
                                     self.kernel_size,
                                     padding="same",
                                     activation=tf.nn.relu)
            conv3 = tf.layers.batch_normalization(conv3, training=isTraining)

        # resdul
        conv3 = conv3 + region_embedding  # [batch,seq-3+1,1,250]
        with tf.name_scope("pool_1"):
            pool = tf.pad(conv3, paddings=[[0, 0], [0, 1], [0, 0], [0, 0]])  ## 在那一维添加
            pool = tf.nn.max_pool(pool, [1, 3, 1, 1], strides=[1, 2, 1, 1], padding='VALID')

        with tf.name_scope("conv3_2"):
            conv3 = tf.layers.conv2d(pool, self.num_filters, self.kernel_size,
                                     padding="same", activation=tf.nn.relu)
            conv3 = tf.layers.batch_normalization(conv3, training=isTraining)

        with tf.name_scope("conv3_3"):
            conv3 = tf.layers.conv2d(conv3, self.num_filters, self.kernel_size,
                                     padding="same", activation=tf.nn.relu)
            conv3 = tf.layers.batch_normalization(conv3, training=isTraining)

        # resdul
        conv3 = conv3 + pool
        pool_size = int((self.seq_length - 3 + 1) / 2)
        conv3 = tf.layers.max_pooling1d(tf.squeeze(conv3, [2]), pool_size, 1)
        conv3 = tf.squeeze(conv3, [1])  # [batch,250]
        conv3 = tf.nn.dropout(conv3, self.keep_prob)

        with tf.name_scope("score"):
            # classify
            self.logits = tf.layers.dense(conv3, self.num_classes, name='fc2')
            self.y_pred_cls = tf.argmax(tf.nn.softmax(self.logits), 1, name="pred")

        with tf.name_scope("loss"):
            # 损失函数,交叉熵
            cross_entropy = tf.nn.softmax_cross_entropy_with_logits(
                logits=self.logits, labels=self.input_y)

            # l2_loss = tf.losses.get_regularization_loss()
            self.loss = tf.reduce_mean(cross_entropy, name="loss")
            # self.loss += l2_loss

            # optim
            self.optim = tf.train.AdamOptimizer(
                learning_rate=self.learning_rate).minimize(self.loss)
        with tf.name_scope("accuracy"):
            # 准确率
            correct_pred = tf.equal(tf.argmax(self.input_y, 1), self.y_pred_cls)
            self.acc = tf.reduce_mean(tf.cast(correct_pred, tf.float32), name="acc")

6. 对分类效果影响大的trick

  1. 关于分词器:确保分词器与词向量表中的token是match的,不然会有很多的OOV问题,即使分的再好又有什么用呢?下面就说下怎么去处理吧。
      1.在已知预训练词向量的分词器时,就直接使用该分词器。
      2.在未知预训练词向量的分词器时,要么用多个分词器去下游任务尝试,看看哪个表现更好;要么就用自己的分词器去预训练一份好的词向量。
      Note: 需要注意的是大小写问题、OOV的定义问题等都会影响下游任务的效果。

  2. 关于中文字向量
     如果只想用char-level的向量,就得预训练字向量,并且要把窗口开大一些,不要直接使用word-level的窗口大小。

  3. 关于数据集噪声
     数据集噪声分为两种:x内部的噪声 (文本是比较随意的言语、不严谨等) ;y的噪声 (有些样本可能是被标错或者很难定义是哪一类,具有二义性时)。
     第1种噪声的处理是:先用char-level的向量和word-level的向量进行效果的对比,如果char-level更好就直接使用;否则就需要提高word-level的向量效果,那就是使用FastText训练一份词向量,其中需要将char ngram的窗口设置为1-2左右,这样为了让模型捕获错别字 (例如“似乎”写成了“是乎”,可以让这些错别字一下子语义回到了一起,一定程度上对抗了分词器产生的噪声)。
     第2种噪声的处理是:做标签平滑的效果很差。所以首先忽略噪声训练好一个模型,再将该模型去预测其中的训练集和验证集,将那些高置信的错误样本进行badcase的总结,最后要么发现一定的规律写一个脚本批量纠错标注,要么就删除错误的标注样本。(其实这里就是Active Learning的思想)

  4. 关于baseline的选择
     个人觉得其实可以TextCNN起手,这个作为baseline其实一点也不会差;其次就是用Transformer再去尝试,总之RNN最好别用,因为在实际项目中毕竟时间性能也是考量的一个大标准。

  5. Dropout层加在哪里
     一句话:word embedding层后、pooling层后、FC层后。可以先保持概率统一,有时间再去微调不同层的Dropout。

  6. 关于二分类
     二分类问题一定要用sigmoid作为输出层的激活函数吗?其实未必,实测中用包含两类的softmax函数会带来零点几个点的提升,虽然有点玄学。

  7. 关于多分类
     可以先用N个二分类问题进行拆解原多分类问题,在tf中有现成的API哦:tf.nn.sigmoid_cross_entropy_with_logits

  8. 关于类别不均衡问题
     如果是类别比例是1:9这种比例以下的话,就不用管它,因为模型这点的健壮性还是有的。但是如果类别比例相差太大,也就是一个batch中都是同一个样本,那么就需要均衡了!

9. 加深特征提取层抑或加深分类层?
 这个问题目前来看,分两种情况:一种是特征是否隐藏很深,如果很深则特征提取层需要加深;否则就加深分类层(一般是全连接层)看看。而对于上述的DPCNN则是在已有抽取到的feature上面精准的设计Conv层来替换分类层,可以解决全连接层因为训练过慢的问题。

7. 未完待续~

参考文献:

  1. 夕小瑶的卖萌屋
  2. https://zhuanlan.zhihu.com/p/35457093

你可能感兴趣的:(2019-02 Classification in NLP)