深度学习--RNN文本分类

本文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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,646评论 4 366
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,979评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,391评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,356评论 0 215
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,740评论 3 293
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,836评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,022评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,764评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,487评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,728评论 2 252
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,200评论 1 263
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,548评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,217评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,134评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,921评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,919评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,766评论 2 274

推荐阅读更多精彩内容