CNN文本分类

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yangfengling1023/article/details/82780788

文本分类是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,结果如下所示:

 

 

文中有不对的地方,欢迎指出,共同交流

 

 

猜你喜欢

转载自blog.csdn.net/yangfengling1023/article/details/82780788