tensorflow1: nn与cnn实现情感分类

0.数据集以及运行环境

数据集的地址:情绪分析的数据集,能稍微看懂英文就应该知道如何下载了

运行环境:Windows10,IDE:pycharm或者是Linux

0.数据预处理

"0","1467810369","Mon Apr 06 22:19:45 PDT 2009","NO_QUERY","_TheSpecialOne_","@switchfoot http://twitpic.com/2y1zl - Awww, that's a bummer.  You shoulda got David Carr of Third Day to do it. ;D"

上图是原始数据的情况,总共有6个字段,主要是第一个字段(情感评价的结果)以及最后一个字段(tweet内容)是有用的。针对csv文件,我们使用pandas进行读取然后进行处理。处理的代码如下:

# 提取文件中的有用的字段
def userfull_filed(org_file, outuput_file):
    data = pd.read_csv(os.path.join(data_dir, org_file), header=None, encoding='latin-1')
    clf = data.values[:, 0]
    content = data.values[:, -1]
    new_clf = []
    for temp in clf:
        # 这个处理就是将情感评论结果进行所谓的one_hot编码
        if temp == 0:
            new_clf.append([1, 0]) # 消极评论
        # elif temp == 2:
        #     new_clf.append([0, 1, 0]) # 中性评论
        else:
            new_clf.append([0, 1]) # 积极评论

    df = pd.DataFrame(np.c_[new_clf, content], columns=['emotion0', 'emotion1', 'content'])
    df.to_csv(os.path.join(data_dir, outuput_file), index=False)

这样处理的原因是,将情感评论值变成一个one-hot编码形式,这样我们进行nn或者是cnn处理的最后一层的输出单元为2(但有个巨大的bug那就是实际上训练集没有中性评论)

接下来利用nltk(这个简直是英语文本的自然语言的神奇呀)来生成单词集合,代码如下:

def sentence_english_manage(line):
    # 英文句子的预处理
    pattern = re.compile(r"[!#$%&'()*+,-./:;<=>?@[\]^_`{|}~0123456789]")
    line = re.sub(pattern, '', line)
    # line = [word for word in line.split() if word not in stopwords]
    return line

def create_lexicon(train_file):
    lemmatizer = WordNetLemmatizer()
    df = pd.read_csv(os.path.join(data_dir, train_file))
    count_word = {} # 统计单词的数量
    all_word = []
    for content in df.values[:, 2]:
        words = word_tokenize(sentence_english_manage(content.lower())) # word_tokenize就是一个分词处理的过程
        for word in words:
            word = lemmatizer.lemmatize(word) # 提取该单词的原型
            all_word.append(word) # 存储所有的单词

    count_word = Counter(all_word)
    # count_word = OrderetodDict(sorted(count_word.items(), key=lambda t: t[1]))
    lex = []
    for word in count_word.keys():
        if count_word[word] < 100000 and count_word[word] > 100: # 过滤掉一些单词
            lex.append(word)

    with open('lexcion.pkl', 'wb') as file_write:
        pickle.dump(lex, file_write)

    return lex, count_word

最后生成一个只含有有用信息的文本内容

1.利用NN进行文本情感分类

1.1神经网络结构的搭建

在这里我设计了两层的隐藏层,单元数分别为1500/1500。神经网络的整个结构代码如下:

with open('lexcion.pkl', 'rb') as file_read:
    lex = pickle.load(file_read)

n_input_layer = len(lex) # 输入层的长度
n_layer_1 = 1500 # 有两个隐藏层
n_layer_2 = 1500
n_output_layer = 2 # 输出层的大小

X = tf.placeholder(shape=(None, len(lex)), dtype=tf.float32, name="X")
Y = tf.placeholder(shape=(None, 2), dtype=tf.float32, name="Y")
batch_size = 500
dropout_keep_prob = tf.placeholder(tf.float32)

def neural_network(data):
    layer_1_w_b = {
        'w_': tf.Variable(tf.random_normal([n_input_layer, n_layer_1])),
        'b_': tf.Variable(tf.random_normal([n_layer_1]))
    }
    layer_2_w_b = {
        'w_': tf.Variable(tf.random_normal([n_layer_1, n_layer_2])),
        'b_': tf.Variable(tf.random_normal([n_layer_2]))
    }
    layer_output_w_b = {
        'w_': tf.Variable(tf.random_normal([n_layer_2, n_output_layer])),
        'b_': tf.Variable(tf.random_normal([n_output_layer]))
    }
    # wx+b
    # 这里有点需要注意那就是最后输出层不需要加激活函数
    # 同时加入了dropout参数
    full_conn_dropout_1 = tf.nn.dropout(data, dropout_keep_prob)
    layer_1 = tf.add(tf.matmul(full_conn_dropout_1, layer_1_w_b['w_']), layer_1_w_b['b_'])
    layer_1 = tf.nn.sigmoid(layer_1)
    full_conn_dropout_2 = tf.nn.dropout(layer_1, dropout_keep_prob)
    layer_2 = tf.add(tf.matmul(full_conn_dropout_2, layer_2_w_b['w_']), layer_2_w_b['b_'])
    layer_2 = tf.nn.sigmoid(layer_2)
    layer_output = tf.add(tf.matmul(layer_2, layer_output_w_b['w_']), layer_output_w_b['b_'])
    # layer_output = tf.nn.softmax(layer_output)

    return layer_output

比起我看的代码,这里我加入了dropout的参数,使得训练不会过拟合。但实际操作中,我发现加不加这个数据对于整个实验的影响并没有那麽大。

1.2获取训练与测试数据

原本的教程其实有一套自己的提取数据的过程,但我看了一眼感觉有些麻烦,我就自己写了一个数据提取的方法,代码如下:

def get_random_n_lines(i, data, batch_size):
    # 从训练集中找训批量训练的数据
    # 这里的逻辑需要理解,同时我们要理解要从积极与消极的两个集合中分层取样
    if ((i * batch_size) % len(data) + batch_size) > len(data):
        rand_index = np.arange(start=((i*batch_size) % len(data)),
                               stop=len(data))
    else:
        rand_index = np.arange(start=((i*batch_size) % len(data)),
                               stop=((i*batch_size) % len(data) + batch_size))

    return data[rand_index, :]

def get_test_data(test_file):
    # 获取测试集的数据用于测试
    lemmatizer = WordNetLemmatizer()
    df = pd.read_csv(os.path.join('data', test_file))
    # groups = df.groupby('emotion1')
    # group_neg_pos = groups.get_group(0).values # 获取非中性评论的信息
    group_neg_pos = df.values

    test_x = group_neg_pos[:, 2]
    test_y = group_neg_pos[:, 0:2]

    new_test_x = []
    for tweet in test_x:
        words = word_tokenize(tweet.lower())
        words = [lemmatizer.lemmatize(word) for word in words]
        features = np.zeros(len(lex))
        for word in words:
            if word in lex:
                features[lex.index(word)] = 1

        new_test_x.append(features)

    return new_test_x, test_y

上面是提取训练集所需的代码,设计了一个提取一个batch_size大小的数据,因为我是分别从积极评论与消极评论中分别提取一定量的数据,所以要调用两次该代码。

下面这个则是提取测试集的代码,借用了词袋模型的思想,对一条tweet在字典长度的维度上进行一个编码过程。其实训练集也是这样处理的,但我写了两次有点傻。

1.3训练过程

借用别人博客中的代码,我进行了细微的修改,主要调整了代码的部分位置,添加了tensorboard的内容以及模型保存的内容,代码如下:

def train_neural_network():
    # 配置tensorboard
    tensorboard_dir = "tensorboard/nn"
    if not os.path.exists(tensorboard_dir):
        os.makedirs(tensorboard_dir)

    # 损失函数
    predict = neural_network(X)
    cost_func = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=predict, labels=Y))
    tf.summary.scalar("loss", cost_func)
    optimizer = tf.train.AdamOptimizer().minimize(cost_func)

    # 准确率
    correct = tf.equal(tf.argmax(predict, 1), tf.argmax(Y, 1))
    accuracy = tf.reduce_mean(tf.cast(correct, 'float'))
    tf.summary.scalar("accuracy", accuracy)

    merged_summary = tf.summary.merge_all()
    writer = tf.summary.FileWriter(tensorboard_dir)

    df = pd.read_csv(os.path.join('data', 'new_train_data.csv'))
    # data = df.values

    group_by_emotion0 = df.groupby('emotion0')
    group_neg = group_by_emotion0.get_group(0).values
    group_pos = group_by_emotion0.get_group(1).values

    test_x, test_y = get_test_data('new_test_data.csv')

    with tf.Session() as sess:
        sess.run(tf.initialize_all_variables())

        writer.add_graph(sess.graph)

        lemmatizer = WordNetLemmatizer() # 判断词干所用的
        saver = tf.train.Saver()

        i = 0
        # pre_acc = 0 # 存储前一次的准确率以和后一次的进行比较

        while i < 5000:
            rand_neg_data = get_random_n_lines(i, group_neg, batch_size)
            rand_pos_data = get_random_n_lines(i, group_pos, batch_size)
            rand_data = np.vstack((rand_neg_data, rand_pos_data)) # 矩阵合并
            np.random.shuffle(rand_data) # 打乱顺序

            batch_y = rand_data[:, 0:2] # 获取得分情况
            batch_x = rand_data[:, 2] # 获取内容信息

            new_batch_x = []
            for tweet in batch_x:
                words = word_tokenize(tweet.lower())
                words = [lemmatizer.lemmatize(word) for word in words]

                features = np.zeros(len(lex))
                for word in words:
                    if word in lex:
                        features[lex.index(word)] = 1  # 一个句子中某个词可能出现两次,可以用+=1,其实区别不大
                new_batch_x.append(features)

            # batch_y = group_neg[:, 0: 3] + group_pos[:, 0: 3]

            loss, _, train_acc = sess.run([cost_func, optimizer, accuracy],
                                          feed_dict={X: new_batch_x, Y: batch_y, dropout_keep_prob: 0.6})

            if i % 100 == 0:
                print("第{}次迭代,损失函数为{}, 训练的准确率为{}".format(i, loss, train_acc))
                s = sess.run(merged_summary, feed_dict={X: new_batch_x, Y: batch_y, dropout_keep_prob: 0.6})
                writer.add_summary(s, i)

            if i % 100 == 0:
                # print(sess.run(accuracy, feed_dict={X: new_batch_x, Y: batch_y}))
                test_acc = accuracy.eval({X: test_x[:200], Y: test_y[:200], dropout_keep_prob: 1.0})
                print('测试集的准确率:', test_acc)
            i += 1

        if not os.path.isdir('./checkpoint'):
            os.mkdir('./checkpoint')
        saver.save(sess, './checkpoint/model.ckpt')  # 保存session

其实这个过程没什么可说的,基本是一个套路。

1.4运行结果

我是在实验室的服务器上跑的上述代码,跑了几个小时,主要感觉还是模型的存储的时间比较长。因为整个模型的参数还是相当大的。最后的结果在tensorboard上显示结果为:



整体来说效果还可以。

2.利用CNN进行情感分类

这个项目实际上是一个CNN在NLP上的一个应用。一般而言,CNN是用来处理像图像、视频等矩阵类的东西,而他是如何应用到自然语言的呢?我们可以引入词嵌入矩阵这个概念,类似于Word2Vec这种东西,我们可以将一个词语或是字符拓展为一个长度为K的特征向量空间,这样借助词袋模型,我们就可以将一个句子或是文档拓展成一个矩阵。这样我们就可以引入卷积以及池化等方法来分析这些数据。

这里分享一个大神的博客,是专门针对CNN在NLP上的应用implementing-a-cnn-for-text-classification-in-tensorflow,通过这篇论文你可以充分了解如何利用tensorflow来实现分文分类以及cnn的一些解释。情绪分析与这个是基本一致的方法,我们也可以从这篇博客中详细了解整个流程。

接下来我们开始讲解CNN的情感分类的过程。从数据的预处理、CNN网络的构建、训练这三个方面进行讲解:

1.1神经网络的搭建:

这是一个重点的内容,我先引用一个图片,然后使用代码慢慢解释,图片如下:


图片有点大,顺便附上代码:

with open('lexcion.pkl', 'rb') as file_read:
    lex = pickle.load(file_read)

input_size = len(lex) # 输入的长度
num_classes = 2 # 分类的数量
batch_size = 64
seq_length = 100 # 一个tweet的固定长度

X = tf.placeholder(tf.int32, [None, seq_length])
Y = tf.placeholder(tf.float32, [None, num_classes])

dropout_keep_prob = tf.placeholder(tf.float32)
def neural_network():
    '''
    整个流程的解释:
    输入为一个X,shape=[None, 8057],8057为字典的长度
    首先利用embedding_lookup的方法,将X转换为[None, 8057, 128]的向量,但有个疑惑就是emdeding_lookup的实际用法,在ceshi.py中有介绍
    接着expande_dims使结果变成[None, 8057, 128, 1]的向量,但这样做的原因不是很清楚,原因就是通道数的设置

    然后进行卷积与池化:
    卷积核的大小有3种,每种卷积后的feature_map的数量为128
    卷积核的shape=[3/4/5, 128, 1, 128],其中前两个为卷积核的长宽,最后一个为卷积核的数量,第三个就是通道数
    卷积的结果为[None, 8057-3+1, 1, 128],矩阵的宽度已经变为1了,这里要注意下

    池化层的大小需要注意:shape=[1, 8055, 1, 1]这样的化池化后的结果为[None, 1, 1, 128]
    以上就是一个典型的文本CNN的过程
    :return:
    '''

    ''' 进行修改采用短编码 '''

    ''' tf.name_scope() 与 tf.variable_scope()的作用基本一致'''
    with tf.name_scope("embedding"):
        embedding_size = 64
        '''
        这里出现了一个问题没有注明上限与下限
        '''
        # embeding = tf.get_variable("embedding", [input_size, embedding_size]) # 词嵌入矩阵
        embedding = tf.Variable(tf.random_uniform([input_size, embedding_size], -1.0, 1.0)) # 词嵌入矩阵
        # with tf.Session() as sess:
        #     # sess.run(tf.initialize_all_variables())
        #     temp = sess.run(embedding)
        embedded_chars = tf.nn.embedding_lookup(embedding, X)
        embedded_chars_expanded = tf.expand_dims(embedded_chars, -1) # 设置通道数

    # 卷积与池化层
    num_filters = 256 # 卷积核的数量
    filter_sizes = [3, 4, 5] # 卷积核的大小
    pooled_outputs = []

    for i, filter_size in enumerate(filter_sizes):
        with tf.name_scope("conv_maxpool_{}".format(filter_size)):
            filter_shape = [filter_size, embedding_size, 1, num_filters] # 要注意下卷积核大小的设置
            W = tf.Variable(tf.truncated_normal(filter_shape, stddev=0.1))
            b = tf.Variable(tf.constant(0.1, shape=[num_filters]))

            conv = tf.nn.conv2d(embedded_chars_expanded, W, strides=[1, 1, 1, 1], padding="VALID")
            h = tf.nn.relu(tf.nn.bias_add(conv, b)) # 煞笔忘了加这个偏置的加法

            pooled = tf.nn.max_pool(h, ksize=[1, seq_length - filter_size + 1, 1, 1],
                                    strides=[1, 1, 1, 1], padding='VALID')
            pooled_outputs.append(pooled)


    num_filters_total = num_filters * len(filter_sizes)
    '''
    # tensor t3 with shape [2, 3]
    # tensor t4 with shape [2, 3]
    tf.shape(tf.concat([t3, t4], 0))  # [4, 3]
    tf.shape(tf.concat([t3, t4], 1))  # [2, 6]
    '''
    h_pool = tf.concat(pooled_outputs, 3) # 原本是一个[None, 1, 1, 128]变成了[None, 1, 1, 384]
    h_pool_flat = tf.reshape(h_pool, [-1, num_filters_total]) # 拉平处理 [None, 384]

    # dropout
    with tf.name_scope("dropout"):
        h_drop = tf.nn.dropout(h_pool_flat, dropout_keep_prob)

    # output
    with tf.name_scope("output"):
        # 这里就是最后的一个全连接层处理
        # from tensorflow.contrib.layers import xavier_initializer
        W = tf.get_variable("w", shape=[num_filters_total, num_classes],
                            initializer=tf.contrib.layers.xavier_initializer()) # 这个初始化要记住
        b = tf.Variable(tf.constant(0.1, shape=[num_classes]))

        output = tf.nn.xw_plus_b(h_drop, W, b)
        # output = tf.nn.relu(output)

    return output

在代码的最上面实际上比较详细的介绍了整个流程,首先这个卷积过程和一般的卷积过程不一样。卷积核是一个矩形的,其宽度与词嵌入矩阵的维度是一样的,他是在长度的方向进行卷积过程,实际含义就是寻找3个词(或者是5个或者是4个)词之间的特征属性,这样卷积的结果如上图所示成了一个1维的向量结果,然后我们进行一个max_pool操作,其他大小与卷积后生成的向量一样,最后我们得到了一个个1*1大小的向量。我们利用concat方法将这些向量合并,最后接入一个全连接层,然后softmax之后我们就可以得到分类的结果。整体流程在代码中有些,而且所有的tensor大小也注明了。

这里我需要说的是,一开始我使用的与NN一样的tweet向量化的方法,即将tweet映射到一个字典长度的维度上,但实验之后发现效果并不好,于是我再查看资料是发现了另一种文本向量化的方法,就是规定每条tweet长度固定均为100,这是在统计后得出的一个基本结果(我统计过训练集中tweet的长度一般在100以下,而且之后我还进行了数据的一个预处理,就是删除标点符号与数字),这样我们就可以将tweet映射到一个100维的向量上,这样矩阵就比较稠密点,而且训练的batch_size可以调的比较大,我们通常采用“多了截断/少了补充”的策略。

1.2就是最后的训练

在训练阶段,我顺便学习了tensorboard以及模型保存的方法,代码如下,基本上是一种常见的训练格式:

def train_neural_netword():
    # 配置tensorboard
    tensorboard_dir = "tensorboard/cnn"
    if not os.path.exists(tensorboard_dir):
        os.makedirs(tensorboard_dir)

    output = neural_network()

    # 构建准确率的计算过程
    predictions = tf.argmax(output, 1)
    correct_predictions = tf.equal(predictions, tf.argmax(Y, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_predictions, "float")) # 强制转换
    tf.summary.scalar("accuracy", accuracy)

    # 构建损失函数的计算过程
    optimizer = tf.train.AdamOptimizer(0.001)
    loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=output, labels=Y))
    tf.summary.scalar("loss", loss) # 将损失函数加入
    grads_and_vars = optimizer.compute_gradients(loss)
    train_op = optimizer.apply_gradients(grads_and_vars)

    # 将参数保存如tensorboard中
    merged_summary = tf.summary.merge_all()
    writer = tf.summary.FileWriter(tensorboard_dir)

    # 构建模型的保存模型
    saver = tf.train.Saver(tf.global_variables())

    # 数据集的获取
    df = pd.read_csv(os.path.join('data', 'new_train_data.csv'))

    group_by_emotion0 = df.groupby('emotion0')
    group_neg = group_by_emotion0.get_group(0).values
    group_pos = group_by_emotion0.get_group(1).values

    test_x, test_y = get_test_data('new_test_data.csv')

    with tf.Session() as sess:
        sess.run(tf.initialize_all_variables())
        # sess.run(tf.global_variables_initializer())
        # 将图像加入tensorboard中
        writer.add_graph(sess.graph)

        lemmatizer = WordNetLemmatizer()
        i = 0
        pre_acc = 0
        while i < 10000:
            rand_neg_data = get_random_n_lines(i, group_neg, batch_size)
            rand_pos_data = get_random_n_lines(i, group_pos, batch_size)
            rand_data = np.vstack((rand_neg_data, rand_pos_data))
            np.random.shuffle(rand_data)

            batch_x = rand_data[:, 3]
            batch_y = rand_data[:, 0: 3]

            new_batch_x = []
            for tweet in batch_x:
                # 这段循环的意义就是将单词提取词干,并将字符转换成下标
                words = word_tokenize(tweet.lower())
                words = [lemmatizer.lemmatize(word) for word in words]

                features = np.zeros(len(lex))
                for word in words:
                    if word in lex:
                        features[lex.index(word)] = 1  # 一个句子中某个词可能出现两次,可以用+=1,其实区别不大
                new_batch_x.append(features)

            _, loss_ = sess.run([train_op, loss], feed_dict={X: new_batch_x, Y: batch_y, dropout_keep_prob: 0.5})

            if i % 20 == 0:
                # 每二十次保存一次tensorboard
                s = sess.run(merged_summary, feed_dict={X: new_batch_x, Y: batch_y, dropout_keep_prob: 0.5})
                writer.add_summary(s, i)

            if i % 10 == 0:
                # 每10次打印出一个损失函数与准确率(这是指评测的准确率)
                print(loss_)
                accur = sess.run(accuracy, feed_dict={X: test_x, Y: test_y, dropout_keep_prob: 1.0})

                if accur > pre_acc:
                    # 当前的准确率高于之前的准确率,更新模型
                    pre_acc = accur
                    print("准确率:", pre_acc)
                    tf.summary.scalar("accur", accur)
                    saver.save(sess, "cnn_model/model.ckpt")
            i += 1

2.3运行结果

在tensorboard上可视化的结果如下:



说实话最后的训练效果并不好,我也不清楚是为什么,希望知道的同学告诉我一声吧。

3.后记

感谢CSDN用户“MachineLP”,我的整个流程是在他的基础上进行修改。实在是受益匪浅。

以上的代码会贴到我的github上(点击打开链接)

阅读更多
个人分类: tensorflow练习
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭