深度学习--RNN文本分类
本文Github
1. RNN文本分类网络结构
RNN文本分类网络结构 图片来源。我们以word作为基本元素,将每个句子分词成若干词。故,X1,X2等表示的是句子中的单词,我们可以将一个句子从前往后当成一个时间序列。RNN网络的特点是在时间上参数共享,也就是说在一个时间序列中,每一步使用的参数都是相同的。2. tensorflow中的RNN
RNN在tensorflow中有静态RNN,动态RNN之分。两者差异挺大,我们在使用tensorflow进行RNN实践时,主要注意以下几点:
- 静态RNN一般需要将所有句子padding成等长处理,这点与TextCNN一样的,但动态rnn稍显灵活一点,动态RNN中,只要一个batch中的所有句子等长就可以;
- 静态RNN的输入与输出是list或二维张量;动态RNN中输入输出的是三维张量,相对与TextCNN,少了一维;
- 静态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网络吧!