你的第一个神经网络文本分类任务——让机器也懂感情

2,713

引言

本文是学习完集智学园《PyTorch入门课程:火炬上的深度学习》系列课之后的梳理。

该系列课程包含大量的实操任务,如文本分类,手写数字识别,翻译器,作曲机,AI游戏等。而在实操中还融入了大量机器学习领域的基础和经典知识,如反向传播原理,神经元原理剖析,NLP领域的RNN,LSTM,word2vec,以及图像领域的迁移学习,卷积神经网络等等,非常适合想要入门机器学习的同学,是当前网络上绝无仅有的好课程。

个人学完后受益匪浅,在此真诚地向张江老师表示感谢

同系列文章:

  1. 用线性回归拟合案例,透彻理解深度学习的反向传播
  2. 你的第一个神经网络——共享单车预测器

正文

本文的任务,是希望基于电商的商品评论文本数据实现一个神经网络模型,用于判断一段评论是好评还是差评。这个任务本质上是一个二分类问题。同理,二分类问题还可以解决诸如筛选无意义评论,判断一张图片属于猫还狗等任务。区别就在于输入的数据是文本,图片,还是其他数据类型。

开始之前先允许我做一个简单的划重点,以便大家对全文内容有一个初步的认识。我们要完成这个任务的核心分为两点:

首先,是数据处理环节。在你的第一个神经网络——共享单车预测器中,我也花了大量的篇幅来讲数据处理,其中用到了one-hot,归一化等基本的数据处理操作。而在本任务中,数据处理更是重中之重。主要涉及到,如何让机器识别文本,并用其进行计算的问题。在这里,我们主要使用了最简单的词袋模型(BOW)。其中对中文的处理还需要使用到正则匹配以及jieba分词工具

其次,就是基本的神经网络训练步骤,其实这个步骤和你的第一个神经网络——共享单车预测器一文的步骤基本大同小异,无非就是建立模型,输入数据,得到预测值,反向传播计算loss,再进行梯度运算调整权重,并不断重复这整个过程,直到loss不再下降,所以对训练步骤的说明就不再详细展开了,还不了解的同学可以回过头看上一篇文章。不同之处仅仅在于,分类任务的损失函数和回归任务不同,这一点待会儿会具体说明。

下面就开始正式操作环节,让我们先来看数据处理。

1. 数据预处理

先来看一下我们用于建模的数据格式。分成了两个文本文件,good.txt和bad.txt,分别存放好评和差评,每一行代表一条评论。数据来源于京东(2013年),可在集智学园github上获取数据文件。

将文件按行读取并依次存储,我们可以在读取的时候就过滤掉标点,并对句子进行分词。这样可以得到两个数组,一个好评文本数组,一个差评文本数组。

其中过滤标点可以直接使用正则表达式,分词则可以直接使用结巴分词库,这个工具可以准确地把一个句子分割为几个有意义的词语。一句话特定词语出现的次数多,比如“很好”,“赞”等,通常我们就可以认为这句话是一句正向评论。所以中文词语才是我们要分析的最小单元。

import re #正则表达式的包
import jieba #结巴分词包
# 正向文本
with open('good.txt', 'r') as fr:
    for idx, line in enumerate(fr):
        if True:
            #过滤标点符号
            line = re.sub("[\s+\.\!\/_,$%^*(+\"\'“”《》?“]+|[+——!,。?、~@#¥%……&*():]+", "", line)  
        #分词
        words = jieba.lcut(line)
        if len(words) > 0:
            all_words += words
            pos_sentences.append(words)

# 负向文本处理方法相同,得到数组neg_sentences
BOW(词袋模型)

要让计算机能够处理文本,首先就要想办法将文本向量化。BOW方法就是一个非常容易理解的文本向量化方法。我举一个简单的例子大家就能一目了然。它的思路就是把文本所包含的所有词汇数量作为向量的维度,把词语在当前句子中出现的频数作为对应位置的值。

句子1:“我喜欢跳舞,小明也喜欢。”
句子2:“我也喜欢唱歌。”

上面是两句话,我们现在想要把这两句话用向量表示。从这两句话中我们可以提取出一个词典,包含了这两句话中的所有词汇。

词典 = {1:“我”,2:“喜欢”,3:“跳舞”,4:“小明”,5:“也”,6:“唱歌”}

文本所包含的所有词汇数量作为向量的维度,把词语在当前句子中出现的频数作为对应位置的值。那么,我们就立刻有了句子的向量表示。

句子 1:[1, 2, 1, 1, 1, 0]
句子 2:[1, 1, 0, 0, 1, 1]

回到我们的任务中来。依照上面的思路,我们就需要建立一个包含所有词汇的大字典,并统计每个词语的词频,从而就能得到每个句子的向量表示。这里使用collections工具可以让词频统计更加简单 。

from collections import Counter #搜集器,可以让统计词频更简单

diction = {} # 要建立的大字典
cnt = Counter(all_words)
for word, freg in cnt.items():
    diction[word] = [len(diction), freg] # 在字典中存储每个词语的编号,以及词频

建立好大字典后,开始逐行处理评论文本

dataset = [] # 所有句子的向量表示集合,即我们训练,测试要使用到的数据
# 处理正向评论
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)

# 其中
# word2index是根据单词获得编码函数
# sentence2vec是把目标句子转化为向量函数
# 这里不详细展示,大家可以尝试自己编写
# 源代码可以从文章开头提到的集智学园github地址中下载

datasetlabel就包含了我们需要的所有信息,包括文本数据和对应标签。接下去,我们就可以进入训练模型的步骤了

接下去,就可以开始训练模型了。下面的部分代码量比较多,再次强调,训练过程代码的详细说明,与你的第一个神经网络——共享单车预测器这篇文章大同小异,所以本文和它重复的部分代码不会再做详细解释。所以下面的代码都是纸老虎而已。

2. 开始训练

2.1 构建输入和目标函数,构建模型

即处理初始数据,基于dataset和label把数据分为训练集,校验集和测试集

#对整个数据集进行划分,分为:训练集、校准集和测试集,其中校准和测试集合的长度都是整个数据集的10分之一
test_size = len(dataset) // 10
train_data = dataset[2 * test_size :]
train_label = labels[2 * test_size :]

valid_data = dataset[: test_size]
valid_label = labels[: test_size]

test_data = dataset[test_size : 2 * test_size]
test_label = labels[test_size : 2 * test_size]

使用pytorch可以快速建立一个简单的神经网络模型

# 输入维度为词典的大小:每一段评论的词袋模型
model = nn.Sequential(
    nn.Linear(len(diction), 10),
    nn.ReLU(),
    nn.Linear(10, 2),
    nn.LogSoftmax(),
)
  1. 输入文本向量,长度为字典大小
  2. 经过一层非线性变换relu
  3. 经过一层线性变换
  4. 经过归一化logSoftmax

这里的为什么输出是二维,我们的label不是1或者0吗?

其实为了这里为了方便计算,我们将标签做了one-hot编码,one-hot编码的作用在上篇文章也提到过,是因为这里的0和1并没有“1比0大”这样的概念,就像星期一星期二一样,他们都是类型变量,为了避免类型变量0和1的数值大小影响了神经网络的训练。

2.1 训练 + 校验过程

先直接上代码

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

#循环10个Epoch
losses = []
for epoch in range(10):
    for i, data in enumerate(zip(train_data, train_label)):
        x, y = data
        x = torch.tensor(x, requires_grad = True, dtype = torch.float).view(1, -1)
        y = torch.tensor([y], dtype = torch.long)
        optimizer.zero_grad()
        predict = model(x)
        loss = cost(predict, y)
        # 将损失函数数值加入到列表中
        losses.append(loss.data.numpy())
        # 开始进行梯度反传
        loss.backward()
        # 开始对参数进行一步优化
        optimizer.step()

       # 每隔3000步,跑一下校验数据集的数据,输出临时结果
        if i % 3000 == 0:
            rights = []
            val_losses = []
            for j, val in enumerate(zip(valid_data, valid_label)):
                x, y = val
                x = torch.tensor(x, requires_grad = True, dtype = torch.float).view(1, -1)
                y = torch.tensor([y], dtype = torch.long)
                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])

这里想要着重强调以下几点:

首先,上面的代码和上篇文章略有不同的是,这里的校验过程和训练过程写在了一起,但思路还是一样的,使用校验集数据在训练好的模型上跑,观察校验集val_loss的变化情况。这样的结果会更加客观

其次,针对分类问题,还可以计算结果的准确度rightness。对真实标签和预测出的值进行比较,计算预测的准确度。其中真实标签和预测值都是二维矩阵

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)

最后,也是最重要的,就是损失函数torch.nn.NLLLoss(),即交叉熵。

二分类的交叉熵公式如下:

这也是当前任务使用的方法,其中:

  • y —— 表示样本的label,正样本是1,负样本是0
  • p —— 表示样本预测为正的概率。

神经网络对于分类问题的预测值通常是一个概率,在当前任务中,比如预测吐出了[0.8, 0.2],这意味着,神经网络预测当前样本为1的概率更大(第一位的数值更大)。交叉熵是用于计算分类问题的预测损失,即如果真实样本是1,那就对比[0.8, 0.2]和[1,0]之间的“差”,这个“差”值越小,说明预测和真实就越接近。

当loss不再下降时,模型基本完成训练,下图是绘制了训练集loss,校验集loss和准确度的变化情况。

我们可以认为,校验集loss和训练集loss重合的部分,模型的效果是最好的,再继续训练下去,虽然训练集的loss还在持续下降,但是校验集loss却不降反升,这个时候模型已经过拟合了。

3. 测试模型效果。

我们取测试集数据,查看模型的预测效果。

rights = []
for i, data in enumerate(zip(test_data, test_label)):
    x, y = data
    x = torch.tensor(x, requires_grad = True, dtype = torch.float).view(1, -1)
    y = torch.tensor([y], dtype = torch.long)
    predict = model(x)
    right = rightness(predict, y)
    rights.append(right)

right_ratio = 1.0 * np.sum([i[0] for i in rights]) / np.sum([i[1] for i in rights])
print(' 测试准确率: {:.2f}'.format(right_ratio))

最终输出准确率:0.91。

到这里,整个任务就完成了,我们得到了一个可以分辨好评还是差评的文本分类器,并且这个分类器的准确率可达91%

结束

感谢您看到这里。文本分类器的坑填完了。

在今后的一段时间里,我还会尝试图像识别,文本翻译,AI游戏等真实案例。所有学习案例都来自张江老师的PyTorch与深度学习课程。

望与大家共勉。