文本分类是NLP领域的一个重要的子任务,文本分类的目标是自动的将文本打上已经定义好的标签,常见的文本分类任务有:垃圾邮件过滤、情感分析、新闻分类等等。
代码是来自
https://github.com/gaussic/text-classification-cnn-rnn
大家可以自行下载阅读,下面仅仅是自己对代码的一个解读,仅此而已,若有不合适的地方,希望大家多多指出,共同交流
1、任务
对给定的一个文本进行盗窃类型的判断
2、类型种类
入户盗窃、扒窃、一般盗窃
代码的具体阅读如下所示:
1、文件的位置
base_dir = 'data/cnews'
train_dir = os.path.join(base_dir, 'cnews.train.txt')
test_dir = os.path.join(base_dir, 'cnews.test.txt')
val_dir = os.path.join(base_dir, 'cnews.val.txt')
vocab_dir = os.path.join(base_dir, 'cnews.vocab.txt')
print ('train_dir',train_dir)
print ('test_dir',test_dir)
print ('val_dir',val_dir)
print ('vocab_dir',vocab_dir)
打印出的结果如下所示:
os.path.join()函数的作用是将各个路径进行拼接操作
2、主函数
if __name__ == '__main__':
if len(sys.argv) != 2 or sys.argv[1] not in ['train', 'test']:
raise ValueError("""usage: python run_cnn.py [train / test]""")
categories = ['入户盗窃', '扒窃', '一般盗窃']
if not os.path.exists(vocab_dir): # 如果不存在词汇表,重建
build_vocab(train_dir, vocab_dir, config.vocab_size)
categories, cat_to_id = read_category(categories) ##将文本的标签进行id转换
words, word_to_id = read_vocab(vocab_dir) ##字、字对应的id号
#该处是使用提前训练好的字向量,但此处注释掉了,此次不进行讲解
#embedding = embed(word_to_id)
print('Configuring CNN model...')
config = TCNNConfig(embedding)
config.vocab_size = len(words)
print('vocab_size',config.vocab_size) ##2426
程序运行时的命令行:
python run_cnn.py train
(1)build_vocab()函数:
该函数的主要功能是建立字典
def build_vocab(train_dir, vocab_dir, vocab_size=5000):
"""根据训练集构建词汇表,存储"""
data_train, _ = read_file(train_dir) ##data_train存储的是一个一个的文本
print(data_train)
all_data = []
for content in data_train:
all_data.extend(content)
counter = Counter(all_data)
count_pairs = counter.most_common(vocab_size - 1)
words, _ = list(zip(*count_pairs))
# 添加一个 <PAD> 来将所有文本pad为同一长度
words = ['<PAD>'] + list(words)
open_file(vocab_dir, mode='w').write('\n'.join(words) + '\n')
该函数主要是构建词汇表,使用字符级的表示,这一函数会将词汇表存储下来,避免每一次重复处理
def read_file(filename):
"""读取文件数据"""
contents, labels = [], []
with open_file(filename) as f:
for line in f:
try:
label, content = line.strip().split('\t')
if content:
contents.append(list(native_content(content)))
labels.append(native_content(label))
except:
pass
return contents, labels
(2)open_file()函数:
def open_file(filename, mode='r'):
"""
常用文件操作,可在python2和python3间切换.
mode: 'r' or 'w' for read or write
"""
if is_py3:
return open(filename, mode, encoding='utf-8', errors='ignore')
else:
return open(filename, mode)
例子:
文本中存储的内容如下所示:
入户盗窃 被告人周×于2014年1月26日,趁无人之机,用事先偷取的钥匙进入被害人李×租住地,窃取“科龙牌”KFR-26W/VGFDBP-3型壁挂式空调1台、“美的牌”F40-15A4型热水机1台及椅子6把、茶几1张。
data_train, _ = read_file()之后:
data_train = ['二、被告人周×于2014年1月26日,趁无人之机,用事先偷取的钥匙进入被害人李×租住地,窃取“科龙牌”KFR-26W/VGFDBP-3型壁挂式空调1台、“美的牌”F40-15A4型热水机1台及椅子6把、茶几1张。']
_存储的是labels:['入户盗窃']
执行完build_vocab()函数之后新生成的字典文件的内容如下所示:
<PAD>
1
、
人
2
4
6
……
几
张
(3)read_category()函数
def read_category(categories):
"""读取分类目录,固定"""
categories = ['入户盗窃', '扒窃', '一般盗窃']
categories = [native_content(x) for x in categories]
cat_to_id = dict(zip(categories, range(len(categories)))) #将类别分别进行id的转换
return categories, cat_to_id
该函数的目的主要是将类别进行id的转换,即使用数字对分类的类别进行表示。将分类目录固定,转换为{类别:id}表示
(4)read_vocab()函数
def read_vocab(vocab_dir):
"""读取词汇表"""
with open_file(vocab_dir) as fp:
# 如果是py2 则每个值都转化为unicode
words = [native_content(_.strip()) for _ in fp.readlines()]
word_to_id = dict(zip(words, range(len(words))))
return words, word_to_id
该函数是从已经建立好的字典文件中读取每一个字,同时进行id的转换
3、模型参数的设置
模型参数设置的调用:
config = TCNNConfig()
模型参数设置的类:
class TCNNConfig(object): ##神经网络相关参数的设定
"""CNN配置参数"""
def __init__(self,embedding):
self.embedding = embedding
embedding_dim = 100 # 词向量维度
seq_length = 600 # 序列长度 即文本设置的长度
num_classes = 3 # 类别数 3类
num_filters = 256 # 卷积核数目
kernel_size = 5 # 卷积核尺寸
vocab_size = 5000 # 词汇表达小
hidden_dim = 128 # 全连接层神经元
dropout_keep_prob = 0.5 # dropout保留比例
learning_rate = 1e-3 # 学习率
batch_size = 64 # 每批训练大小
num_epochs = 10 # 总迭代轮次
print_per_batch = 100 # 每多少轮输出一次结果
save_per_batch = 10 # 每多少轮存入tensorboard
##提前训练好的词向量可以放在这个里面,进行后续得网络的输入
# embedding = tf.get_variable('embedding', [vocab_size,embedding_dim])
4、网络层的理解
(1)模型的调用:
model = TextCNN(config)
(2)模型的定义
class TextCNN(object):
"""文本分类,CNN模型"""
def __init__(self, config):
self.config = config
# 三个待输入的数据,首先定义我们传递给我们网络的输入数据
##seq_length :文本的长度
##num_classes 标签的数量
self.input_x = tf.placeholder(tf.int32, [None, self.config.seq_length], name='input_x')
self.input_y = tf.placeholder(tf.float32, [None, self.config.num_classes], name='input_y') self.keep_prob = tf.placeholder(tf.float32, name='keep_prob')
self.cnn()
def cnn(self):
"""CNN模型"""
# 词向量映射
with tf.device('/cpu:0'):
#下面是输入的向量时随机进行初始化的
embedding = tf.get_variable('embedding', [self.config.vocab_size, self.config.embedding_dim]) ##词向量的初始值为随机值
# print('embedding',embedding) ##embedding <tf.Variable 'embedding:0' shape=(2426, 100) dtype=float32_ref>
embedding_inputs = tf.nn.embedding_lookup(embedding, self.input_x)
#
# ##下面是使用训练好的字向量进行训练
# embed = tf.Variable(initial_value=self.config.embedding, dtype=tf.float32)
# print('self.config.embedding', embed)
# embedding_inputs = tf.nn.embedding_lookup(embed, self.input_x)
# print('self.input_x',self.input_x) ##Tensor("input_x:0", shape=(?, 600), dtype=int32)
with tf.name_scope("cnn"):
# CNN layer
conv = tf.layers.conv1d(embedding_inputs, self.config.num_filters, self.config.kernel_size, name='conv') ##256,5
# print('embedding_inputs',embedding_inputs) ##Tensor("embedding_lookup:0", shape=(?, 600, 100), dtype=float32, device=/device:CPU:0)
# print('conv',conv) ##Tensor("cnn/conv/BiasAdd:0", shape=(?, 596, 256), dtype=float32) 596 = 600-5+1
# global max pooling layer
gmp = tf.reduce_max(conv, reduction_indices=[1], name='gmp') ##取最大值 是在第二维上进行最大值的取出
# print('gmp',gmp)##Tensor("cnn/gmp:0", shape=(?, 256), dtype=float32)
with tf.name_scope("score"):
# 全连接层,后面接dropout以及relu激活
fc = tf.layers.dense(gmp, self.config.hidden_dim, name='fc1')
fc = tf.contrib.layers.dropout(fc, self.keep_prob)
fc = tf.nn.relu(fc)
# 分类器
self.logits = tf.layers.dense(fc, self.config.num_classes, name='fc2')
# print('self.logits',self.logits) #self.logits Tensor("score/fc2/BiasAdd:0", shape=(?, 3), dtype=float32)
self.y_pred_cls = tf.argmax(tf.nn.softmax(self.logits), 1) # 预测类别
with tf.name_scope("optimize"):
# 损失函数,交叉熵
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=self.logits, labels=self.input_y) ##返回的是一个向量
# print('cross_entropy',cross_entropy) #cross_entropy Tensor("optimize/Reshape_2:0", shape=(?,), dtype=float32)
self.loss = tf.reduce_mean(cross_entropy) ##对向量求均值,计算loss 损失函数
'''
,logits是作为softmax的输入。经过softmax的加工,就变成“归一化”的概率(设为q),然后和labels代表的概率分布(设为q),于是,整个函数的功能就是前面的计算labels(概率分布p)和logits(概率分布q)之间的交叉熵
'''
# 优化器
self.optim = tf.train.AdamOptimizer(learning_rate=self.config.learning_rate).minimize(self.loss)
with tf.name_scope("accuracy"):
# 准确率
correct_pred = tf.equal(tf.argmax(self.input_y, 1), self.y_pred_cls) ##bool型
self.acc = tf.reduce_mean(tf.cast(correct_pred, tf.float32)) ##数据类型的转换,同时进行计算准确率
代码的理解:
tf.placeholder创建一个占位符变量,第一个维度表示的是批量大小,使用None表示,第二个参数是输入张量的形状
tf.nn.softmax_cross_entropy_with_logits是一个损失的函数,计算每个类的交叉熵损失,给定我们的分数和正确的输入标签,对损失进行计算平均值
损失是我们训练模型好坏的衡量标准,目的是降低损失,减少网络的误差,分类问题的标准损失函数是交叉熵损失
CNN网络:
代码中的卷积神经网络时最基本的网络,有input layer、convolutional layer、max-pooling layer以及最后的输出的softmax layer
(1)input layer层
使用随机初始化的embedding作为输入层输入的信息
embedding = tf.get_variable('embedding', [self.config.vocab_size, self.config.embedding_dim]) ##词向量的初始值为随机值
embedding_inputs = tf.nn.embedding_lookup(embedding, self.input_x)
此时的输入的shape为:shape=(?, 600, 100)
(2)convolutional layer层
conv = tf.layers.conv1d(embedding_inputs, self.config.num_filters, self.config.kernel_size, name='conv')
参数的大小:
embedding_inputs:shape=(?, 600, 100)
self.config.num_filters:256 卷积核数目 卷积出的最后一个维度 256个卷积核
self.config.kernel_size:5 卷积核尺寸 一维卷积窗口大大小
输出的维度大小:
shape=(?, 596, 256)
其中596 = 600-5+1
256个卷积核的初始化的大小是随机的,所以对句子进行256个卷积结果是不同的,即使初始化卷积核的大小都是一样的,但是根据标签进行调参时,也会将卷积核矩阵中的内容进行训练修改,此时的卷积核矩阵中的数据相当于传统神经网络的权重参数W
对句子中的每一个字的向量的每一维度都进行参数的学习
tf.layers.conv1d()功能:
生成卷积核,对输入层进行卷积,产生输出的tensor
(3)max-pooling layer层
gmp = tf.reduce_max(conv, reduction_indices=[1], name='gmp') ##取最大值 是在第二维上进行最大值的取出
print('gmp',gmp)##Tensor("cnn/gmp:0", shape=(?, 256), dtype=float32)
(4)softmax layer层
# 全连接层,后面接dropout以及relu激活
fc = tf.layers.dense(gmp, self.config.hidden_dim, name='fc1')
fc = tf.contrib.layers.dropout(fc, self.keep_prob)
fc = tf.nn.relu(fc)
# 分类器
self.logits = tf.layers.dense(fc, self.config.num_classes, name='fc2')
print('self.logits',self.logits) #self.logits Tensor("score/fc2/BiasAdd:0", shape=(?, 3), dtype=float32)
self.y_pred_cls = tf.argmax(tf.nn.softmax(self.logits), 1) # 预测类别
在自然语言处理中,我们假设一个序列是600个单词,每个单词的词向量是300维,那么一个序列输入到网络中就是(600,300),当我使用Conv1D进行卷积的时候,实际上就完成了直接在序列上的卷积,卷积的时候实际是以(3,300)进行卷积,又因为每一行都是一个词向量,因此使用Conv1D(kernel_size=3)也就相当于使用神经网络进行了n_gram=3的特征提取了。这也是为什么使用卷积神经网络处理文本会非常快速有效的内涵。
5、模型的训练
if sys.argv[1] == 'train':
train()
else:
test()
具体的train()函数代码如下所示:
def train():
print("Configuring TensorBoard and Saver...")
# 配置 Tensorboard,重新训练时,请将tensorboard文件夹删除,不然图会覆盖
tensorboard_dir = 'tensorboard/textcnn'
if not os.path.exists(tensorboard_dir):
os.makedirs(tensorboard_dir)
tf.summary.scalar("loss", model.loss)
tf.summary.scalar("accuracy", model.acc)
merged_summary = tf.summary.merge_all()
writer = tf.summary.FileWriter(tensorboard_dir)
# 配置 Saver
saver = tf.train.Saver()
if not os.path.exists(save_dir):
os.makedirs(save_dir)
print("Loading training and validation data...")
# 载入训练集与验证集
start_time = time.time()
x_train, y_train = process_file(train_dir, word_to_id, cat_to_id, config.seq_length)
x_val, y_val = process_file(val_dir, word_to_id, cat_to_id, config.seq_length)
time_dif = get_time_dif(start_time)
print("Time usage:", time_dif)
# 创建session
session = tf.Session()
session.run(tf.global_variables_initializer())
writer.add_graph(session.graph)
print('Training and evaluating...')
start_time = time.time()
total_batch = 0 # 总批次
best_acc_val = 0.0 # 最佳验证集准确率
last_improved = 0 # 记录上一次提升批次
require_improvement = 1000 # 如果超过1000轮未提升,提前结束训练
flag = False
for epoch in range(config.num_epochs):
print('Epoch:', epoch + 1)
batch_train = batch_iter(x_train, y_train, config.batch_size)
for x_batch, y_batch in batch_train:
feed_dict = feed_data(x_batch, y_batch, config.dropout_keep_prob)
if total_batch % config.save_per_batch == 0:
# 每多少轮次将训练结果写入tensorboard scalar
s = session.run(merged_summary, feed_dict=feed_dict)
writer.add_summary(s, total_batch)
if total_batch % config.print_per_batch == 0:
# 每多少轮次输出在训练集和验证集上的性能
feed_dict[model.keep_prob] = 1.0
loss_train, acc_train = session.run([model.loss, model.acc], feed_dict=feed_dict)
loss_val, acc_val = evaluate(session, x_val, y_val) # todo
if acc_val > best_acc_val:
# 保存最好结果
best_acc_val = acc_val
last_improved = total_batch
saver.save(sess=session, save_path=save_path)
improved_str = '*'
else:
improved_str = ''
time_dif = get_time_dif(start_time)
msg = 'Iter: {0:>6}, Train Loss: {1:>6.2}, Train Acc: {2:>7.2%},' \
+ ' Val Loss: {3:>6.2}, Val Acc: {4:>7.2%}, Time: {5} {6}'
print(msg.format(total_batch, loss_train, acc_train, loss_val, acc_val, time_dif, improved_str))
session.run(model.optim, feed_dict=feed_dict) # 运行优化
total_batch += 1
if total_batch - last_improved > require_improvement:
# 验证集正确率长期不提升,提前结束训练
print("No optimization for a long time, auto-stopping...")
flag = True
break # 跳出循环
if flag: # 同上
break
对train()函数进行分析:
(1)process_file()函数
def process_file(filename, word_to_id, cat_to_id, max_length=600):
"""将文件转换为id表示"""
contents, labels = read_file(filename)
'''
一个句子放在一个列表中,且字与字之间是分开放的,将所有的句子放在一个列表contents中
'''
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]])
# 使用keras提供的pad_sequences来将文本pad为固定长度
x_pad = kr.preprocessing.sequence.pad_sequences(data_id, max_length)
y_pad = kr.utils.to_categorical(label_id, num_classes=len(cat_to_id)) # 将标签转换为one-hot表示
return x_pad, y_pad
该函数主要实现了将训练语料、开发集语料、测试集语料中的每一句话中的每一个字进行id的转换,同时做padding,将所有的句子长度进行统一长度
将数据集从文字转换为固定长度的id序列表示
对于CNN,输入与输出都是固定的,当每一个句子长短不一是,需要做定长处理,超过的截断,不足的补0,注意补充的0对后面的结果没有影响,因为后面的max-pooling只会输出最大值,补0的项会被过滤掉
(2)batch_iter()函数
def batch_iter(x, y, batch_size=64):
"""生成批次数据"""
data_len = len(x)
##可以将语料分成多少个batch_size
num_batch = int((data_len - 1) / batch_size) + 1
##做shuffle 即进行对语料的前后顺序进行打乱 在整个语料上进行打乱操作
indices = np.random.permutation(np.arange(data_len))
x_shuffle = x[indices]
y_shuffle = y[indices]
for i in range(num_batch):
start_id = i * batch_size
##只有在最后一个batch上,才有可能取data_len,前面的都是取前者
end_id = min((i + 1) * batch_size, data_len)
yield x_shuffle[start_id:end_id], y_shuffle[start_id:end_id]
该函数主要是为神经网络的训练准备经过shuffle的批次的数据
网络的大致结构如下所示:
CNN网络的理解:
1、输入层
输入层输入的是句子中的字对应的向量,假设句子有n个字,向量的维度为k,则输入的是一个nxk的矩阵(在CNN中可以看作一副高度为n、宽度为k的图像),在本文中输入的矩阵大小为:600x100,一个句子的最大长度设置为600,每一个字的字向量长度设置为100维
对于未登录词,映射到PAD上
输入的矩阵可以是静态的,也可以是动态的。静态指的是字向量是固定不变的,而动态则是在模型训练过程中,字向量也被当做是可以进行优化的参数,通常把反向误差传播导致字向量中值发生变化的这一过程称为Finetune。若字向量是随机初始化的,不仅训练得到了CNN分类模型,还得到了字向量这个副产品,如果已经有训练的字向量,那么其实是一个迁移学习的过程
网址:https://blog.csdn.net/zbc1090549839/article/details/53055386
Embedding layer:通过一个隐藏层,将one-hot编码的词投影到一个低维空间中,本质上是特征提取器,在指定维度中编码语义特征,这样,语义相近的词,它们的欧氏距离或余弦距离也比较近
运行python run_cnn.py train,显示如下所示:
运行python run_cnn.py test,结果如下所示:
文中有不对的地方,欢迎指出,共同交流