06. 简易中文情绪分类器

1. 背景介绍

1-1. 课本上的引例

  如果机器可以根据一只股票的关键词以及网络上包含这些关键词的句子所蕴含的情绪而自动判断这只股票的走势,我们就可以让机器快速进行股票的买卖。
  据说,这种自动交易软件会导致我们的大盘走势忽起忽落。下面我们看一个典型事件。2013年4月23日13点07分,一名黑客用美国副总统的推特账号发文称“白宫遭袭,奥巴马受伤”。监控舆情的人工智能算法开始狂卖股票,导致道琼斯指数在60秒内狂跌150点,相当于损失了1360亿美元。13点10分,副总统辟谣,算法开始反向操作;13点13分,道琼斯指数又回来了。
  事实上,这种文本自动识别的应用还有非常多的场景。运用这类技术,不仅可以识别语言中的情绪,而且可以对文本进行分类。比如,新闻分类、社交网络分析、广告精准投放等都是文本分类的应用领域。

(说不准还可以用到后续的智能聊天机器人)

1-2. 分类问题的特点

相比于预测问题而言,分类问题有如下特点:

  • 神经元输出单元数为所分类别个数;
  • 每个输出单元的输出值为一个(0,1)区间中的数,而且它们加起来等于1;
  • 最后一层计算为softmax(软分类)函数;
  • 最终的输出类别为使得输出值最大的类别,或者根据输出值概率大小随机选取输出类别;
  • 采用了交叉熵这个特殊的损失函数形式。

原理参见课本P63~P67


2. 词袋模型分类器的表示

中文句子的向量表示:

所有词今日开心开学每个都有一个
今日开学,我很开心11111000000
每个人都有一个爱的人00000121111

向量归一化:

所有词今日开心开学每个都有一个
今日开学,我很开心1/51/51/51/51/5000000
每个人都有一个爱的人000001/72/71/71/71/71/7

3. 搭建简单文本分类器

3-1. 数据获取

  前几天按照课本实例,抓取了一些京东的评价数据,包括一件卫衣和一件羽绒服的带“好、坏”标签的评论。抓取方法见此文:→“爬取京东评论以构建词向量——实现中文情绪分类器基础”

3-2. 数据处理

导入函数包:

# PyTorch用的包
import torch
import torch.nn as nn
import torch.optim
from torch.autograd import Variable

# 自然语言处理相关的包
import re #正则表达式的包
import jieba #结巴分词包
from collections import Counter

# 绘图、计算用的包
import matplotlib.pyplot as plt
import numpy as np

给出数据文件路径,准备处理:

good_file = 'good.txt'
bad_file = 'bad.txt'

定义过滤标点符号的函数filter_punc

# 过滤文本中的符号
def filter_punc(sentence):
    sentence = re.sub("[\s\.\!\/_,$%^*()+\"\'“”《》?“]+|[+—!,。?、~@#¥%·&…*():]+","", sentence)
    return(sentence)

定义数据预处理函数——扫描所有的文本,分词并建立词典,分出正向还是负向的评论,构建整体字典:

# 扫描所有的文本,分词并建立词典,分出正向还是负向的评论,is_filter可以过滤是否筛选掉标点符号
def Prepare_data(good_file, bad_file, is_filter = True):
    all_words = [] #存储所有的单词
    pos_sentences = [] #存储正向的评论
    neg_sentences = [] #存储负向的评论
    with open(good_file, 'r', encoding='utf-8') as fr:
        for idx,line in enumerate(fr):
            if is_filter:
                # 过滤标点符号
                line = filter_punc(line)
            # 分词
            words = jieba.lcut(line)
            if len(words) > 0:
                all_words += words
                pos_sentences.append(words)
    print('{0} 包含{1}行,{2}个词.'.format(good_file, idx+1, len(all_words)))
    
    count = len(all_words)
    with open(bad_file, 'r', encoding='utf-8') as fr:
        for idx, line in enumerate(fr):
            if is_filter:
                line = filter_punc(line)
                words = jieba.lcut(line)
            if len(words) > 0:
                all_words += words
                neg_sentences.append(words)
    print('{0} 包含 {1} 行,{2} 个词.'.format(bad_file, idx+1, len(all_words)-count))
    
    # 建立词典,diction的每一项为{w:[id, 单词出现次数]}
    diction = {}
    cnt = Counter(all_words)
    for word, freq in cnt.items():
        diction[word] = [len(diction), freq]
    print('字典大小:{}'.format(len(diction)))
    return(pos_sentences, neg_sentences, diction)

# 调用Prepare_data,完成数据处理工作
pos_sentences, neg_sentences, diction = Prepare_data(good_file, bad_file, True)
st = sorted([(v[1], w) for w, v in diction.items()])

查看分出的评价:

# 好的评价词表
pos_sentences
[['不',
  '掉色',
  '版型',
  '好百搭会',
  '回购',
  '尺码',
  '准摸',
  '起来',
  '很',
  '舒服',
  '版型',
  '很',
  '好',
  '质量',
  '很',
  '好',
  '衣服',
  '很',
  '好看',
  '很',
  '合身',
  '暖和'],
 ['与',
  '图片',
  '一致',
  '没有',
  '色差',
  '店家',
  '的',
  '宝贝',
  '虽然',
  '价格',
  '实惠',
  '但是',
  '质量',
  '个人感觉',
  '真心',
  '不错',
  '做工',
  '精细',
  '款式',
  '我',
  '喜欢',
  '面料',
  '柔软',
  '亲肤度',
  '高',
  '款式',
  '简洁',
  '大方',
  '做工',
  '精细',
  '颜色',
  '比较',
  '的',
  '衬',
  '皮肤',
  '级',
  '满意'],
······
]
# 差的评价词表
neg_sentences
[['这个', '价位', '还', '可以', '尺寸', '偏小'],
 ['保暖', '还', '行', '就是', '穿', '几天', '就', '起球'],
 ['还行', '吧', '就是', '没有', '照片', '里面', '的', '好看'],
 ['穿', '起来', '不', '舒服', '样子', '还', '可以'],
 ['衣服', '还', '可以', '穿', '起来', '很', '暖和'],
 ['衣服', '还是', '挺', '容易', '起球', '的'],
 ['上身', '效果', '不错', '只是', '衣服', '容易', '起球'],
 ['还', '行', '就是', '容易', '起球', '总的来说', '不错'],
 ['边边', '我', '不', '喜欢', '都', '还', '可以'],
 ['一般', '吧', '摸', '了', '一下', '挺厚', '的', '毕竟', '价格', '在', '这'],
 ['就是', '布料', '不是', '棉', '的', '其他', '都', '还行', '吧', '个人', '觉得', '一般'],
······
 ]

查看总字典:

# 总字典
diction
{'不': [0, 268],
 '掉色': [1, 148],
 '版型': [2, 223],
 '好百搭会': [3, 74],
 '回购': [4, 74],
 '尺码': [5, 167],
 '准摸': [6, 74],
 '起来': [7, 159],
 '很': [8, 2195],
 '舒服': [9, 453],
 '好': [10, 986],
 ······
 '磨损': [799, 1],
 '一分': [800, 1]}

3-3. 文本数据向量化

定义输入一个句子和相应的词典,得到这个句子的向量化表示的函数sentence2vec

# 输入一个句子和相应的词典,得到这个句子的向量化表示
# 向量的尺寸为词典中词汇的个数,i位置上面的数值为第i个单词在sentence中出现的频率
def sentence2vec(sentence, dictionary):
    vector = np.zeros(len(dictionary))
    for l in sentence:
        vector[l] += 1
    return(1.0 * vector / len(sentence))

遍历所有句子,将每一个词映射成编码:

# 遍历所有句子,将每一个词映射成编码
dataset = [] #数据集
labels = [] #标签
sentences = [] #原始句子,调试用
# 处理正向评论
for sentence in pos_sentences :
    new_sentence = []
    for l in sentence:
        if l in diction:
            new_sentence.append(word2index(l, diction))
    dataset.append(sentence2vec(new_sentence, diction))
    labels.append(0) #正标签为0
    sentences.append(sentence)
    
#处理负向评论
for sentence in neg_sentences:
    new_sentence = []
    for l in sentence:
        if l in diction:
            new_sentence.append(word2index(l, diction))
    dataset.append(sentence2vec(new_sentence, diction))
    labels.append(1) #负标签为1
    sentences.append(sentence)
    
# 打乱所有的数据顺序,形成数据集
# indices 为所有数据下标的全排列
indices = np.random.permutation(len(dataset))

#根据打乱的下标,重新生成数据集dataset、标签集labels,以及对应的原始句子sentences
dataset = [dataset[i] for i in indices]
labels = [labels[i] for i in indices]
sentences = [sentences[i] for i in indices]

3-4. 划分数据集

将整体数据划为单位“1”,那么训练集占80%,测试集占剩下的20%,校验集占测试集的50%,如下图所示:

bIEZXF.png

# 将整个数据集划分为训练集、校验集和测试集,其中校验集和测试集的长度都是整个数据集的十分之一
# 数据集dataset、标签集labels
import math

test_size = len(dataset) #数据总量
train_data = dataset[:math.floor(0.8 * test_size)] #测试集数据
train_label = labels[:math.floor(0.8 * test_size)] #测试集标签

valid_data = dataset[math.floor(0.8 * test_size):] #校验集数据
valid_label = labels[math.floor(0.8 * test_size):] #校验集标签

test_data = dataset[math.floor(0.9 * test_size):] #测试集数据
test_label = labels[math.floor(0.9 * test_size):] #测试集标签

3-5. 建立神经网络

据已有数据及所学知识,本次建立的网络结构为:

  • 一个线性输入层(801个神经单元)
  • 一个ReLu隐含层(10个神经单元)
  • 一个线性输出层(2个神经单元)

调用“搭积木”函数Sequential来建立神经网络运算模型:

# 一个简单的前馈神经网络,共3层
# 第一层为线性层,加一个非线性ReLU,输出层为线性层,中间有10个隐含层神经元

# 输入维度为词典的大小:每一段评论的词袋模型
model = nn.Sequential(
    nn.Linear(len(diction), 10),
    nn.ReLU(),
    nn.Linear(10, 2),
    nn.LogSoftmax(dim=1),
)

自定义的计算一组数据分类准确度的函数:

# 自定义的计算一组数据分类准确度的函数
# prediction 为模型给出的预测结果,labels 为数据中的标签,比较二者以确定整个神经网络当前的表现
def rightness(predictions, labels):
    # 计算预测错误率的函数、其中 predictions 是模型给出的一组预测结果,batch size行num classes列的
    # 矩阵,labels是数据中的正确答案
    # 对于任意一行(一个样本)的输出值的第1个维度求最大,得到每一行最大元素的下标
    pred = torch.max(predictions.data, 1)[1]
    # 将下标与labels中包含的类别进行比较,并累计得到比较正确的数量
    rights = pred.eq(labels.data.view_as(pred)).sum()
    return rights, len(labels) #返回正确的数量和这一次一共比较了多少元素交叉熵

使用交叉熵损失函数:

# 损失函数为交叉熵
cost = torch.nn.NLLLoss()
# 优化算法为 SGD ,可以自动调节学习率
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
records = []

losses = []

循环40个epoch进行训练:

# 循环 40 个 epoch
for epoch in range(40):
    for i, data in enumerate(zip(train_data, train_label)):
        x, y = data

        # 将输入的数据进行适当的变形,主要是多出一个 batch_size 的维度,即第一个为1的维度
        x = Variable(torch.FloatTensor(x).view(1, -1))
        # x 的尺寸为 1
        # 标签也要加一层外衣以变成 1*1 的张量
        y = Variable(torch.LongTensor(np.array([y])))
        # y 的尺寸为 1,1

        # 清空梯度
        optimizer.zero_grad()
        # 模型预测
        predict = model(x)
        # 计算损失函数
        loss = cost(predict, y)
        # 将损失函数数值加入列表中
        losses.append(loss.data.numpy())
        # 开始进行梯度反传
        loss.backward()
        # 开始对参数进一步优化
        optimizer.step()

        # 每隔3000步,跑一下校验集的数据,输出临时结果
        if i % 3000 == 0:
            val_losses = []
            rights = []
            # 在所有校验集上实验
            for j, val in enumerate(zip(valid_data, valid_label)):
                x, y = val
                x = Variable(torch.FloatTensor(x).view(1, -1))
                y = Variable(torch.LongTensor(np.array([y])))
                predict = model(x)
                # 调用 rightness 函数计算准确度
                right = rightness(predict, y)
                rights.append(right)
                loss = cost(predict, y)
                val_losses.append(loss.data.numpy())

            # 将校验集上的平均准确度计算出来
            right_ratio = 1.0 * np.sum([i[0] for i in rights]) / np.sum([i[1] for i in rights])
            print('第{}轮,训练损失:{:.2f},校验损失:{:.2f},校验准确率:{:.2f}'.format(epoch, np.mean(losses), np.mean(val_losses), right_ratio))
            records.append([np.mean(losses),np.mean(val_losses), right_ratio])

循环过程:

第0轮,训练损失:0.78,校验损失:0.73,校验准确率:0.26
第1轮,训练损失:0.57,校验损失:0.57,校验准确率:0.74
第2轮,训练损失:0.56,校验损失:0.57,校验准确率:0.74
第3轮,训练损失:0.55,校验损失:0.56,校验准确率:0.74
第4轮,训练损失:0.55,校验损失:0.54,校验准确率:0.74
第5轮,训练损失:0.54,校验损失:0.52,校验准确率:0.74
第6轮,训练损失:0.53,校验损失:0.49,校验准确率:0.74
第7轮,训练损失:0.52,校验损失:0.45,校验准确率:0.74
第8轮,训练损失:0.51,校验损失:0.41,校验准确率:0.79
第9轮,训练损失:0.49,校验损失:0.36,校验准确率:0.79
第10轮,训练损失:0.47,校验损失:0.32,校验准确率:0.82
第11轮,训练损失:0.46,校验损失:0.28,校验准确率:0.88
第12轮,训练损失:0.44,校验损失:0.25,校验准确率:0.91
第13轮,训练损失:0.42,校验损失:0.22,校验准确率:0.93
第14轮,训练损失:0.41,校验损失:0.20,校验准确率:0.94
第15轮,训练损失:0.39,校验损失:0.18,校验准确率:0.95
第16轮,训练损失:0.38,校验损失:0.17,校验准确率:0.96
第17轮,训练损失:0.36,校验损失:0.15,校验准确率:0.96
第18轮,训练损失:0.35,校验损失:0.14,校验准确率:0.96
第19轮,训练损失:0.34,校验损失:0.13,校验准确率:0.96
第20轮,训练损失:0.33,校验损失:0.12,校验准确率:0.97
第21轮,训练损失:0.32,校验损失:0.11,校验准确率:0.97
第22轮,训练损失:0.31,校验损失:0.11,校验准确率:0.97
第23轮,训练损失:0.30,校验损失:0.10,校验准确率:0.97
第24轮,训练损失:0.29,校验损失:0.09,校验准确率:0.98
第25轮,训练损失:0.28,校验损失:0.09,校验准确率:0.98
第26轮,训练损失:0.27,校验损失:0.08,校验准确率:0.98
第27轮,训练损失:0.27,校验损失:0.08,校验准确率:0.99
第28轮,训练损失:0.26,校验损失:0.08,校验准确率:0.99
第29轮,训练损失:0.25,校验损失:0.07,校验准确率:0.99
第30轮,训练损失:0.25,校验损失:0.07,校验准确率:0.99
第31轮,训练损失:0.24,校验损失:0.07,校验准确率:0.99
第32轮,训练损失:0.23,校验损失:0.06,校验准确率:0.99
第33轮,训练损失:0.23,校验损失:0.06,校验准确率:0.99
第34轮,训练损失:0.22,校验损失:0.06,校验准确率:0.99
第35轮,训练损失:0.22,校验损失:0.06,校验准确率:0.99
第36轮,训练损失:0.21,校验损失:0.06,校验准确率:0.99
第37轮,训练损失:0.21,校验损失:0.05,校验准确率:0.99
第38轮,训练损失:0.20,校验损失:0.05,校验准确率:0.99
第39轮,训练损失:0.20,校验损失:0.05,校验准确率:0.99

4. 运行结果

4-1. 绘制损失函数和准确度的曲线

import matplotlib.pyplot as plt

record = np.array(records) #数据类型转化

Steps = np.linspace(0,39,40)
# 绘制损失函数和准确度的曲线
plt.figure(figsize = (20, 10)) #设定绘图窗口大小
xplot, = plt.plot(Steps, record[:, 0], '-') #绘制训练损失曲线
yplot, = plt.plot(Steps, record[:, 1], '-') #绘制校验损失曲线
wplot, = plt.plot(Steps, record[:, 2], '-') #绘制校验准确率曲线
plt.xlabel('Steps') #更改横坐标轴标注
plt.ylabel('Loss & Rightness') #更改纵坐标轴标注
plt.title('EzXxY PC')

plt.legend([xplot,yplot,wplot], ['Train Loss', 'Valid Loss','Valid Accuracy'])#绘制图例
plt.show()

bInfSI.png

4-2. 查看模型在测试集上的结果

# 在测试集上分批运行,并计算总的正确率
vals =[] #记录准确率所用列表

# 对测试数据集进行循环
for data, target in zip(test_data, test_label):
    data, target = Variable(torch.FloatTensor(data).view(1,-1)), Variable(torch.LongTensor(np.array([target])))
    output = model(data) #将特征数据输入网络,得到分类的输出
    val = rightness(output, target) #获得正确样本数以及总样本数
    vals.append(val) #记录结果

# 计算准确率
rights = (sum([tup[0] for tup in vals]), sum([tup[1] for tup in vals]))
right_rate = 1.0 * rights[0] / rights[1]

right_rate
tensor(0.9802)

  从上述的分类结果输出可以看出,本模型对某件卫衣和某件羽绒服评价分类的准确度已经达到了98%。这个简单的网络能够对数据集的支持效果表现优异,但是对于其他的文本就不一定了。我们可以增加样本数据量来获得更好的文本情绪评价模型。

5. 输入自定义评价来判断情绪

自定义函数,通过输入评论得到对应的情绪分类结果:

# 自定义函数,通过输入评论得到对应的情绪分类结果
def judgement(comment):
    # 分词
    comment = jieba.lcut(comment)
    # 建立输入词向量
    xx = np.zeros(len(diction))
    j = 0
    for l in comment:
        if l in diction:
            xx[j] += 1
        j += 1
    xx = 1.0 * xx / len(comment)
    xx = Variable(torch.FloatTensor(xx).view(1,-1))
    # 得到预测输出
    output = model(xx)
    pred = torch.max(output.data, 1)[1]
    # 比较得到情绪结果
    if pred == 0:
        print('好的评价')
    else:
        print('差的评价')

测试模型

# 输入评价
comment = '没有色差,摸起来很舒服,孩子喜欢'
# 得到预测结果
judgement(comment)
好的评价
# 输入评价
comment = '衣服挺容易起球的。'
# 得到预测结果
judgement(comment)
差的评价
# 输入评价
comment = '版型好,百搭,尺码准,质量不错'
# 得到预测结果
judgement(comment)
好的评价
# 输入评价
comment = '穿久就开线了,感觉不值'
# 得到预测结果
judgement(comment)
差的评价

  当然我们可以更改数据集、神经网络各层神经元的个数和训练次数将带标签的中文对话大数据集分为三类,并以输入对话的情绪来判断我们下一句说话的语气和内容。

打赏
文章目录