tf2+cnn+中文文本分类优化系列(2)

1 前言

接着上次的tf2+cnn+中文文本分类优化系列(1),本次进行优化:使用多个卷积核进行特征抽取。之前是使用filter_size=2进行2-gram特征的识别,本次使用filter_size=[3,4,5]三个不同的卷积核抽取三个不同的gram特征,这样就能通过卷积获取更多的词特征。其实,本次主要看cnn在做中文文本分类中single kernel与multi kernel的对比。

2 前期处理

数据集仍是复旦大学开源的文本数据集,label种类为20。所用的包如下:

import tensorflow as tf
from tensorflow.keras import layers
from tensorflow.keras.callbacks import ReduceLROnPlateau
import numpy as np
import collections
import matplotlib.pyplot as plt
import codecs
import re
import os

一些参数设定如下:

class TextConfig():
    embedding_size=100     #词的维度
    vocab_size=6000        #词表的大小

    seq_length=300         #文本长度
    num_classes=20         #类别数量

    num_filters=128        #卷积核数量
    filter_sizes=[3,4,5]   #多个卷积核大小


    keep_prob=0.5          #dropout
    lr= 1e-3               #学习率
    
    is_training=True
    num_epochs=10          #epochs
    batch_size=64         #batch_size


    train_dir=r'E:\data\train.txt'  #train data
    test_dir=r'E:\data\test.txt'    #test data
    vocab_dir=r'E:\data\vocab.txt'  #vocabulary

接着构建词表,并将文本token化:

def read_file(file_dir):
    """
    读入数据文件,将每条数据的文本和label存入各自列表中
    """
    re_han = re.compile(u"([\u4E00-\u9FD5a-zA-Z]+)")  #去掉标点符号和数字类型的字符
    
    with codecs.open(file_dir,'r',encoding='utf-8') as f:
        for line in f:
            label,text=line.split('\t')
            content=[]
            for w in text:
                if re_han.match(w):
                    content.append(w)
            yield content,label
def build_vocab(file_dirs,vocab_dir,vocab_size=6000):
    """
    利用训练集和测试集的数据生成字级的词表
    """
    all_data = []
    for filename in file_dirs:
        for content,_ in read_file(filename):
            all_data.extend(content)
    counter=collections.Counter(all_data)
    count_pairs=counter.most_common(vocab_size-1)
    words,_=list(zip(*count_pairs))
    words=['']+list(words)

    with codecs.open(vocab_dir,'w',encoding='utf-8') as f:
        f.write('\n'.join(words)+'\n')
def convert_examples_to_tokens(input_dir,vocab_dir,seq_length):
    """
    将文本按字级别进行token化,按后截断的方式进行padding;
    """
    
    words=codecs.open(vocab_dir,'r',encoding='utf-8').read().strip().split('\n')
    word_to_id=dict(zip(words,range(len(words))))
    
    categories = ['Art', 'Literature', 'Education', 'Philosophy', 'History', 'Space', 'Energy', 'Electronics',
                  'Communication', 'Computer','Mine','Transport','Enviornment','Agriculture','Economy',
                  'Law','Medical','Military','Politics','Sports']
    cat_to_id=dict(zip(categories,range(len(categories))))
    
    input_ids,label_ids=[],[]
    for content,label in read_file(input_dir):
        input_ids.append([word_to_id[x] if x in word_to_id else 0 for x in content ])
        label_ids.append(cat_to_id[label])
    
    input_ids =tf.keras.preprocessing.sequence.pad_sequences(input_ids, value=0,padding='post', maxlen=seq_length)
    label_ids=np.array(label_ids)
    return (input_ids,label_ids)

该部分跟之前的基本是一样的,只是参数中filter_sizes变成一个列表

3 模型构建

不同单个卷积核,在使用多个卷积核时,要进行遍历,然后将每次卷积+池化的结构进行拼接。

def cnn_model(cfg):
    """
    定义一个实现多个卷积核的layer,然后进行Flatten,dropout,softmax层;
    """
    
    def convolution():
        inn = layers.Input(shape=(cfg.seq_length, cfg.embedding_size, 1))
        cnns = []
        for size in cfg.filter_sizes:
            conv = layers.Conv2D(filters=cfg.num_filters, kernel_size=(size, cfg.embedding_size),
                                 strides=1, padding='valid', activation='relu',
                                 kernel_regularizer=tf.keras.regularizers.l2(0.001))(inn)
            pool = layers.MaxPool2D(pool_size=(cfg.seq_length - size + 1, 1), padding='valid')(conv)
            pool = layers.BatchNormalization()(pool)
            cnns.append(pool)
        outt = layers.concatenate(cnns)
        model = tf.keras.Model(inputs=inn, outputs=outt)
        return model

    model = tf.keras.Sequential([
        layers.Embedding(input_dim=cfg.vocab_size, output_dim=cfg.embedding_size,
                        input_length=cfg.seq_length),
        layers.Reshape((cfg.seq_length, cfg.embedding_size, 1)),
        convolution(),
        layers.Flatten(),
        layers.Dropout(cfg.keep_prob),
        layers.Dense(cfg.num_classes, activation='softmax')

    ])
    model.compile(optimizer=tf.keras.optimizers.Adam(cfg.lr),
                 loss='sparse_categorical_crossentropy',
                 metrics=['accuracy'])
    
    print(model.summary())
    return model

其中 function convolution就是定义的一个多个卷积核形成的layer,在卷积中使用了2维卷积,所以在embedding后增加了一个维度:layers.Reshape((cfg.seq_length, cfg.embedding_size, 1))

4 训练

在训练中,本次我们调用了ReduceLROnPlateau定义一个学习率衰减策略:在val_loss超过2个epoch没减少,则学习率按lr=lr*0.6进行下调。

if __name__ == "__main__":

    cfg=TextConfig()
    model=cnn_model(cfg)
    
    if not os.path.exists(cfg.vocab_dir):
        build_vocab(cfg.train_dir, cfg.vocab_dir, cfg.vocab_size)

    #载入训练数据,并进行样本打乱;
    train_x,train_y=convert_examples_to_tokens(cfg.train_dir,cfg.vocab_dir,cfg.seq_length)
    indices=np.random.permutation(np.arange(len(train_x)))
    train_x=train_x[indices]
    train_y=train_y[indices]
    
    #定义学习率衰减策略
    reduce_lr = ReduceLROnPlateau(monitor='val_loss',factor=0.6, patience=2, min_lr=0.0001)
    history=model.fit(train_x,train_y,epochs=cfg.num_epochs,batch_size=cfg.batch_size,
                      verbose=1,validation_split=0.1, callbacks=[reduce_lr])

5 结果对比

  • 在测试集上损失和准确率
模型 test_loss test_accuracy
single kernel(之前) 0.573 0.833
multi kernel(本次) 0.505 0.888

可以看出,本次多个卷积核有5.5%效果的提升,还是很明显的,也证实了多个卷积核能抽取更多相关的N-gram特征,进而影响识别效果。

  • 各个label的f1-score 指标对比


    从上图可以看出,在多个卷积核识别下,“Literature”label从原先的0提升到0.21,其他指标也得到一定的提升。

  • 训练过程对比

    image.png

从训练的损失和验证集的准确率来看,multi kernel都是优于single kernel的。

6 结语

通过本次的实验可以得出,在使用cnn做text classification任务,多个卷积核已是标配。至于用多少个卷积核,一般是取3个,具体也看数据场景,但更多的话会增大模型复杂度而对效果提升不大。在本次实验下,仍有优化空间,下次从词的角度进行优化。

更多文章可关注笔者公众号:自然语言处理算法与实践

你可能感兴趣的:(tf2+cnn+中文文本分类优化系列(2))