你的第一个神经网络文本分类任务——让机器也懂感情
本文是学习完 集智学园《PyTorch入门课程:火炬上的深度学习》系列课 之后的梳理。
该系列课程包含大量的实操任务,如文本分类,手写数字识别,翻译器,作曲机,AI游戏等。而在实操中还融入了大量机器学习领域的基础和经典知识,如反向传播原理,神经元原理剖析,NLP领域的 RNN,LSTM,word2vec
,以及图像领域的 迁移学习,卷积神经网络
等等,非常适合想要入门机器学习的同学,是当前网络上绝无仅有的好课程。
个人学完后受益匪浅,在此真诚地向张江老师表示感谢
同系列文章:
正文
本文的任务,是希望基于电商的商品评论文本数据实现一个神经网络模型,用于判断一段评论是好评还是差评。这个任务本质上是一个二分类问题。同理,二分类问题还可以解决诸如筛选无意义评论,判断一张图片属于猫还狗等任务。区别就在于输入的数据是文本,图片,还是其他数据类型。
开始之前先允许我做一个简单的划重点,以便大家对全文内容有一个初步的认识。我们要完成这个任务的核心分为两点:
首先,是数据处理环节。在 你的第一个神经网络——共享单车预测器 中,我也花了大量的篇幅来讲数据处理,其中用到了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地址中下载
dataset
和 label
就包含了我们需要的所有信息,包括文本数据和对应标签。接下去,我们就可以进入训练模型的步骤了
接下去,就可以开始训练模型了。下面的部分代码量比较多,再次强调,训练过程代码的详细说明,与 你的第一个神经网络——共享单车预测器 这篇文章大同小异,所以本文和它重复的部分代码不会再做详细解释。所以下面的代码都是纸老虎而已。
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(), )
-
-
输入文本向量,长度为字典大小
-
-
经过一层非线性变换relu
-
-
经过一层线性变换
-
-
经过归一化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%