深度学习--RNN文本分类

作者: Nlp_小菜 | 来源:发表于2019-01-30 15:02 被阅读0次

    本文Github

    1. RNN文本分类网络结构

    RNN文本分类网络结构 图片来源。我们以word作为基本元素,将每个句子分词成若干词。故,X1,X2等表示的是句子中的单词,我们可以将一个句子从前往后当成一个时间序列。RNN网络的特点是在时间上参数共享,也就是说在一个时间序列中,每一步使用的参数都是相同的。

    2. tensorflow中的RNN

    RNN在tensorflow中有静态RNN,动态RNN之分。两者差异挺大,我们在使用tensorflow进行RNN实践时,主要注意以下几点:

    1. 静态RNN一般需要将所有句子padding成等长处理,这点与TextCNN一样的,但动态rnn稍显灵活一点,动态RNN中,只要一个batch中的所有句子等长就可以;
    2. 静态RNN的输入与输出是list或二维张量;动态RNN中输入输出的是三维张量,相对与TextCNN,少了一维;
    3. 静态RNN生成过程所需的时间更长,网络所占内存会更大,但模型中会带有每个序列的中间信息,利于调试;动态RNN生成过程所需时间相对少,所占内存相对更小,但模型中只有最后的状态。

    本文介绍使用动态RNN进行文本分类。

    2.1 数据预处理

    首先去除文本中的标点符号,对文本分词,最后将每句的分词结果依次存入contents列表,标签也依次存入labels列表。

    def read_file(filename):
       re_han = re.compile(u"([\u4E00-\u9FD5a-zA-Z0-9+#&\._%]+)")
       contents, labels = [], []
       with codecs.open(filename, 'r', encoding='utf-8') as f:
           for line in f:
               try:
                   line = line.rstrip()
                   assert len(line.split('\t')) == 2
                   label, content = line.split('\t')
                   labels.append(label)
                   blocks = re_han.split(content)
                   word = []
                   for blk in blocks:
                       if re_han.match(blk):
                           word.extend(jieba.lcut(blk))
                   contents.append(word)
               except:
                   pass
       return labels, contents
    

    接下来,建立词典,将词典中词语的词向量单独存入文件。这些词应该具有一定的重要性,我们通过词频排序,选择前N个词。但在这之前,应该去停用词!去了停用词之后,取文本(这个文本指的是所有文本,包括训练、测试、验证集)中前N个词,表示这N个词是比较重要的。我提取了文本的前9999个比较重要的词,并按顺序保存了下来。embeddings= np.zeros([10000, 100]) 表示我建立了一个10000个词,维度是100的词向量集合。然后将9999个词在大词向量中的数值,按1-9999的顺序,放入了新建的词向量中。第0项,让它保持是100个0的状态。

    def built_vocab_vector(filenames,voc_size = 10000):
        '''
        去停用词,得到前9999个词,获取对应的词 以及 词向量
        :param filenames:
        :param voc_size:
        :return:
        '''
        stopword = open('./data/stopwords.txt', 'r', encoding='utf-8')
        stop = [key.strip(' \n') for key in stopword]
    
        all_data = []
        j = 1
        embeddings = np.zeros([10000, 100])
    
        for filename in filenames:
            labels, content = read_file(filename)
            for eachline in content:
                line =[]
                for i in range(len(eachline)):
                    if str(eachline[i]) not in stop:#去停用词
                        line.append(eachline[i])
                all_data.extend(line)
    
        counter = Counter(all_data)
        count_paris = counter.most_common(voc_size-1)
        word, _ = list(zip(*count_paris))
    
        f = codecs.open('./data/vector_word.txt', 'r', encoding='utf-8')
        vocab_word = open('./data/vocab_word.txt', 'w', encoding='utf-8')
        for ealine in f:
            item = ealine.split(' ')
            key = item[0]
            vec = np.array(item[1:], dtype='float32')
            if key in word:
                embeddings[j] = np.array(vec)
                vocab_word.write(key.strip('\r') + '\n')
                j += 1
        np.savez_compressed('./data/vector_word.npz', embeddings=embeddings)
    

    然后建立词典,目的是为了让中文单词能够转换成数字序列。

    def get_wordid(filename):
        key = open(filename, 'r', encoding='utf-8')
        wordid = {}
        wordid['<PAD>'] = 0
        j = 1
        for w in key:
            w = w.strip('\n')
            w = w.strip('\r')
            wordid[w] = j
            j += 1
        return wordid
    

    下面,开始将句子中的词,以及标签中的词,都变成数字的序列。其中将标签中的值,变成one-hot形式。read_category()是建立标签的词典,作用与上面建立的词典作用一致。

    def read_category():
        categories = ['体育', '财经', '房产', '家居', '教育', '科技', '时尚', '时政', '游戏', '娱乐']
        cat_to_id = dict(zip(categories, range(len(categories))))
        return categories, cat_to_id
    

    接下来,需要进行padding处理,区别与CNN中的处理,这里是统计一个batch中最长句子,然后按batch进行padding,这是比较标注的做法。但由于单个子句非常长,按原长处理电脑运行非常吃力,故指定了最大长度为250(吐槽下文本)。因此这一步实际上是对所有句子进行padding。并将中文词按照词典转换为数字,y_pad = kr.utils.to_categorical(label_id)是将标签转换为one-hot形式。

    def process(filename, word_to_id, cat_to_id, max_length=250):
        labels, contents = read_file(filename)
        data_id, label_id = [], []
    
        for i in range(len(contents)):
            data_id.append([word_to_id[x] for x in contents[i] if x in word_to_id])
            label_id.append(cat_to_id[labels[i]])
    
        x_pad = kr.preprocessing.sequence.pad_sequences(data_id, max_length, padding='post', truncating='post')
        y_pad = kr.utils.to_categorical(label_id)
    
        return x_pad, y_pad
    

    然后,是生成每一次输入RNN模型的batch了。这里用了np.random.permutation函数将indices打乱。

    def batch_iter(x, y, batch_size = 64):
        data_len = len(x)
        x = np.array(x)
        num_batch = int((data_len - 1)/batch_size) + 1
        indices = np.random.permutation(np.arange(data_len))
        '''
        np.arange(4) = [0,1,2,3]
        np.random.permutation([1, 4, 9, 12, 15]) = [15,  1,  9,  4, 12]
        '''
        x_shuff = x[indices]
        y_shuff = y[indices]
        for i in range(num_batch):
            start_id = i * batch_size
            end_id = min((i+1) * batch_size, data_len)
            yield x_shuff[start_id:end_id], y_shuff[start_id:end_id]
    

    最后,根据动态RNN模型的特点,需要计算各句子的真实长度,存入列表。为啥要计算真实长度?因为有用啊!!!因为给动态RNN输入真实的句子长度,它就知道超过句子真实长度的部分是无用信息了,超过真实长度部分的值为0。

    def sequence(x_batch):
        seq_len = []
        for line in x_batch:
            length = np.sum(np.sign(line))
            seq_len.append(length)
    
        return seq_len
    

    2.2 RNN网络

    数据预处理好了,接下里就可以用tensorflow写RNN网络结构了。RNN网络首先要定义Cell,有三种,分别是:RNNCell,LSTMCell,GRUCell。
    接下来,考虑使用单层,多层,是单向还是双向;最后是使用动态还是静态。本文使用的是动态双层LSTM网络,因此,输入的是三维张量。RNN的返回值有两个,一个是结果,一个是Cell状态,结果也是三维张量。在使用多层RNN需要注意的地方:在使用单层RNN时,embedding_dim和hidden_dim在数值上可以不一致,但涉及到多层的时候,需要将两者的数值相等,否则会报错。具体可以看

    class RnnModel(object):
    
       def __init__(self):
           self.input_x = tf.placeholder(tf.int32, shape=[None, pm.seq_length], name='input_x')
           self.input_y = tf.placeholder(tf.float32, shape=[None, pm.num_classes], name='input_y')
           self.seq_length = tf.placeholder(tf.int32, shape=[None], name='sequen_length')
           self.keep_prob = tf.placeholder(tf.float32, name='keep_prob')
           self.global_step = tf.Variable(0, trainable=False, name='global_step')
           self.rnn()
    
       def rnn(self):
    
           with tf.device('/cpu:0'), tf.name_scope('embedding'):
               embedding = tf.get_variable('embedding', shape=[pm.vocab_size, pm.embedding_dim],
                                           initializer=tf.constant_initializer(pm.pre_trianing))
               self.embedding_input = tf.nn.embedding_lookup(embedding, self.input_x)
    
           with tf.name_scope('cell'):
               cell = tf.nn.rnn_cell.LSTMCell(pm.hidden_dim)
               cell = tf.nn.rnn_cell.DropoutWrapper(cell, output_keep_prob=self.keep_prob)
    
               cells = [cell for _ in range(pm.num_layers)]
               Cell = tf.nn.rnn_cell.MultiRNNCell(cells, state_is_tuple=True)
    
           with tf.name_scope('rnn'):
               #hidden一层 输入是[batch_size, seq_length, embendding_dim]
               #hidden二层 输入是[batch_size, seq_length, 2*hidden_dim]
               #2*hidden_dim = embendding_dim + hidden_dim
               output, _ = tf.nn.dynamic_rnn(cell=Cell, inputs=self.embedding_input, sequence_length=self.seq_length, dtype=tf.float32)
               output = tf.reduce_sum(output, axis=1)
               #output:[batch_size, seq_length, hidden_dim]
    
           with tf.name_scope('dropout'):
               self.out_drop = tf.nn.dropout(output, keep_prob=self.keep_prob)
    
           with tf.name_scope('output'):
               w = tf.Variable(tf.truncated_normal([pm.hidden_dim, pm.num_classes], stddev=0.1), name='w')
               b = tf.Variable(tf.constant(0.1, shape=[pm.num_classes]), name='b')
               self.logits = tf.matmul(self.out_drop, w) + b
               self.predict = tf.argmax(tf.nn.softmax(self.logits), 1, name='predict')
    
           with tf.name_scope('loss'):
               losses = tf.nn.softmax_cross_entropy_with_logits_v2(logits=self.logits, labels=self.input_y)
               self.loss = tf.reduce_mean(losses)
    
           with tf.name_scope('optimizer'):
               optimizer = tf.train.AdamOptimizer(pm.learning_rate)
               gradients, variables = zip(*optimizer.compute_gradients(self.loss))#计算变量梯度,得到梯度值,变量
               gradients, _ = tf.clip_by_global_norm(gradients, pm.clip)
               #对g进行l2正则化计算,比较其与clip的值,如果l2后的值更大,让梯度*(clip/l2_g),得到新梯度
               self.optimizer = optimizer.apply_gradients(zip(gradients, variables), global_step=self.global_step)
               #global_step 自动+1
    
           with tf.name_scope('accuracy'):
               correct_prediction = tf.equal(self.predict, tf.argmax(self.input_y, 1))
               self.accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32), name='accuracy')
    
    

    2.3 训练模型

    模型构建好了,可以开始训练了。当global_step为100的倍数时,输出当前训练结果,本次训练迭代三次,每迭代完一次,保存模型。

    def train():
    
        tensorboard_dir = './tensorboard/Text_Rnn'
        save_dir = './checkpoints/Text_Rnn'
        if not os.path.exists(tensorboard_dir):
            os.makedirs(tensorboard_dir)
        if not os.path.exists(save_dir):
            os.makedirs(save_dir)
        save_path = os.path.join(save_dir, 'best_validation')
    
        tf.summary.scalar('loss', model.loss)
        tf.summary.scalar('accuracy', model.accuracy)
        merged_summary = tf.summary.merge_all()
        writer = tf.summary.FileWriter(tensorboard_dir)
        saver = tf.train.Saver()
        session = tf.Session()
        session.run(tf.global_variables_initializer())
        writer.add_graph(session.graph)
    
        x_train, y_train = process(pm.train_filename, wordid, cat_to_id, max_length=250)
        x_test, y_test = process(pm.test_filename, wordid, cat_to_id, max_length=250)
        for epoch in range(pm.num_epochs):
            print('Epoch:', epoch+1)
            num_batchs = int((len(x_train) - 1) / pm.batch_size) + 1
            batch_train = batch_iter(x_train, y_train, batch_size=pm.batch_size)
            for x_batch, y_batch in batch_train:
                seq_len = sequence(x_batch)
                feed_dict = model.feed_data(x_batch, y_batch, seq_len, pm.keep_prob)
                _, global_step, _summary, train_loss, train_accuracy = session.run([model.optimizer, model.global_step, merged_summary,
                                                                                    model.loss, model.accuracy],feed_dict=feed_dict)
                if global_step % 100 == 0:
                    test_loss, test_accuracy = model.evaluate(session, x_test, y_test)
                    print('global_step:', global_step, 'train_loss:', train_loss, 'train_accuracy:', train_accuracy,
                          'test_loss:', test_loss, 'test_accuracy:', test_accuracy)
    
                if global_step % num_batchs == 0:
                    print('Saving Model...')
                    saver.save(session, save_path, global_step=global_step)
    
            pm.learning_rate *= pm.lr_decay
    
    训练结果如下: 训练结果

    从每次运行的结果上看,成绩较为理想。运用最后保存的模型对验证集进行预测,并计算准确率,以及输出前10条结果,进行查看。

    def val():
        pre_label = []
        label = []
        session = tf.Session()
        session.run(tf.global_variables_initializer())
        save_path = tf.train.latest_checkpoint('./checkpoints/Text_Rnn')
        saver = tf.train.Saver()
        saver.restore(sess=session, save_path=save_path)
    
        val_x, val_y = process(pm.val_filename, wordid, cat_to_id, max_length=250)
        batch_val = batch_iter(val_x, val_y, batch_size=64)
        for x_batch, y_batch in batch_val:
            seq_len = sequence(x_batch)
            pre_lab = session.run(model.predict, feed_dict={model.input_x: x_batch,
                                                            model.seq_length: seq_len,
                                                            model.keep_prob: 1.0})
            pre_label.extend(pre_lab)
            label.extend(y_batch)
        return pre_label, label
    
    预测结果

    在5000条验证集上预测准确率达到了96.7%,从前10条结果上也可以看出,结果相当理想。

    3 总结

    本文使用的数据来自https://github.com/cjymz886/text-cnn。文本分为10类,数据来自新闻文本,故文本比较长。在做本次实验之前,由于比较懒,直接用的上一次TextCnn文本预处理的程序,也就是指定一个max_length=n,然后将所有句子padding成max_length。收敛速度被TextCnn甩老远。后来进行了部分改进,将长度变短。收敛速度依旧不如TextCnn。看来,在做长文本的文本分类时,还是用CNN网络吧!

    参考

    https://zhuanlan.zhihu.com/p/28054589

    相关文章

      网友评论

        本文标题:深度学习--RNN文本分类

        本文链接:https://www.haomeiwen.com/subject/tviqjqtx.html