RNN实现情感分类(Emotion Classifier)

  • A+
所属分类:深度学习
摘要这篇文章使用RNN,更确切的说是GRU实现了简单的情感分类。这一部分主要是介绍整个在处理自然语言的时候常用的步骤和一个完整的流程。

前言

这一篇会介绍使用RNN,更详细一点是GRU来实现句子的情感分类。其实RNN的基本用法都是相同的,和RNN完成姓名分类,在做法上其实是很接近的。

不过在这一篇文章,我会把整个处理自然语言的流程过一篇,如前面的数据预处理,数据填充这一通用的部分。也是方便之后做相关内容的时候可以加快做的速度。(这一部分也是我自己的总结,可能会有写的不好的地方)

后面每一个大的章节我会讲一部分。

导入库

这个没什么好讲的,把需要使用的导入。

  1. import unicodedata
  2. import string
  3. import re
  4. import time
  5. import random
  6. import numpy as np
  7. import pandas as pd
  8. import matplotlib.pyplot as plt
  9. %matplotlib inline
  10. from sklearn.model_selection import train_test_split
  11. import pickle
  12. import torch
  13. import torch.nn as nn
  14. from torch import optim
  15. import torch.nn.functional as F
  16. import torch.utils.data as Data
  17. from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence
  18. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

 导入数据

通常情况下,我们会将处理好的数据进行保存,来方便之后的使用.(不需要每次使用之前都进行预处理,所以先定义两个函数,用来导出和导入数据。)

  1. def convert_to_pickle(item, directory):
  2.     """导出数据
  3.     """
  4.     pickle.dump(item, open(directory,"wb"))
  5. def load_from_pickle(directory):
  6.     """导入数据
  7.     """
  8.     return pickle.load(open(directory,"rb"))

这次使用的数据是保存为csv的格式,我们将其读入:

  1. data = pd.read_csv('./data/train.csv',lineterminator='\n',encoding='utf-8')
  2. data.head()
RNN实现情感分类(Emotion Classifier)

数据预处理

数据预处理是比较关键的一部分,也是可以通用的一部分。数据预处理大致分为下面几个部分.

  1. 将所有字母转为Ascii。
  2. 将大写都转换为小写; 同时, 只保留常用的标点符号
  3. 新建完成, word2idx, idx2word, word2count(每个单词出现的次数), n_word(总的单词个数)
  4. 去掉一些低频的词汇(有时也会去掉停用词);
  5. 将句子转换为Tensor, 每个word使用index来进行代替;
  6. 对句子进行填充, 使每句句子的长度相同, 这样可以使用batch进行训练(需要确定最终句子的长度)
  7. 将label转换为one-hot的格式, 方便最后的训练(Pytorch中只需要转换为标号即可)

下面我们逐个来讲一下是如何完成的。

转为Ascii&大小写转换与去掉标点&完成word2index等创建

这一部分主要是完成上面提到的数据预处理的前三个部分。函数如下所示。

  1. # 第一步数据预处理
  2. def unicodeToAscii(s):
  3.     """转换为Ascii
  4.     """
  5.     return ''.join(
  6.         c for c in unicodedata.normalize('NFD', s)
  7.         if unicodedata.category(c) != 'Mn'
  8.     )
  9. # 第二步数据预处理
  10. def normalizeString(s):
  11.     """转换为小写, 同时去掉奇怪的符号
  12.     """
  13.     s = unicodeToAscii(s.lower().strip())
  14.     s = re.sub(r"([.!?])", r" \1", s)
  15.     s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
  16.     return s
  17. # 第三步数据预处理
  18. class Lang():
  19.     def __init__(self):
  20.         self.word2index = {}
  21.         self.word2count = {}
  22.         # 初始的时候SOS表示句子开头(0还在padding的时候会进行填充), EOS表示句子结尾(或是表示没有加入index中的新的单词, 即不常用的单词)
  23.         self.index2word = {0:"SOS",1:"EOS"}
  24.         self.n_words = 2
  25.     def addSentence(self, sentence):
  26.         """把句子中的每个单词加入字典中
  27.         """
  28.         for word in sentence.split(' '):
  29.             self.addWord(word)
  30.     def addWord(self, word):
  31.         if word not in self.word2index:
  32.             self.word2index[word] = self.n_words # 新单词的标号
  33.             self.word2count[word] = 1 # 新单词的个数
  34.             self.index2word[self.n_words] = word
  35.             self.n_words = self.n_words + 1
  36.         else:
  37.             self.word2count[word] = self.word2count[word] + 1

下面我们我们对上面导入的数据进行上面三步的预处理,最后可以得到一个类,包含word2index, index2word, word2count, n_word等属性。

  1. # 把数据集中的每句话中的单词进行转化
  2. lang = Lang()
  3. for sentence_data in data["review"].values.tolist():
  4.     # 数据清洗
  5.     sentence_data = normalizeString(sentence_data)
  6.     # 增加word2index
  7.     lang.addSentence(sentence_data)
  8. # 显示一些统计数据
  9. print("Count word:{}".format(lang.n_words))
  10. """
  11. Count word:17684
  12. """

 去掉低频词汇

下面我们要去掉一些低频的词汇。那么是如何进行去除呢。我们首先要看一下他的统计上的特性。

我们首先看一下单词出现次数的中位数,平均数和最大值。(可以看到有很多单词只出现了一次)

  1. # 打印一下单词个数的分布
  2. data_count = np.array(list(lang.word2count.values()))
  3. # 有大量单词只出现了很少的次数
  4. np.median(data_count), np.mean(data_count), np.max(data_count)
  5. """
  6. (1.0, 5.799343965614749, 3033)
  7. """

接着我们查看一下出现次数<n的单词,占总的单词出现次数的比例:

  1. # 计算<n的单词, 出现次数占总的出现次数的比例
  2. less_count = 0
  3. total_count = 0
  4. for _,count in lang.word2count.items():
  5.     if count < 2:
  6.         less_count = less_count + count
  7.     total_count = total_count + count
  8. print("小于N的单词出现次数 : ",less_count,
  9.       "\n总的单词出现次数 : ",total_count,
  10.       "\n小于N的单词占比 : ",less_count/total_count*100)
  11. """
  12. 小于N的单词出现次数 :  10547 
  13. 总的单词出现次数 :  102544 
  14. 小于N的单词占比 :  10.285340926821657
  15. """

有的时候,我们还会看一些<n的单词占总的单词的比例(注意这里和上面的区别,这里是不考虑单词出现的次数,只考虑单词的个数,即有多少个不同的单词)

  1. # 计算<n的单词, 出现个数占总的出现个数的比例
  2. less_count = 0
  3. total_count = 0
  4. for _,count in lang.word2count.items():
  5.     if count < 2:
  6.         less_count = less_count + 1
  7.     total_count = total_count + 1
  8. print("小于N的单词出现个数 : ",less_count,
  9.       "\n总的单词出现个数 : ",total_count,
  10.       "\n小于N的单词占比 : ",less_count/total_count*100)

最后,我们去掉只出现了1次的单词。重新创建类(lang_process)

  1. # 我们设置单词至少出现2次
  2. lang_process = Lang()
  3. for word,count in lang.word2count.items():
  4.     if count >= 2:
  5.         lang_process.word2index[word] = lang_process.n_words # 新单词的标号
  6.         lang_process.word2count[word] = count # 新单词的个数
  7.         lang_process.index2word[lang_process.n_words] = word
  8.         lang_process.n_words = lang_process.n_words + 1
  9. # 显示一些统计数据
  10. print("Count word:{}".format(lang_process.n_words))
  11. """
  12. Count word:7137
  13. """

我们简单查看一下每个单词出现的次数:

  1. # 简单查看一下lang_process留下的单词
  2. lang_process.word2count
  3. """
  4. {'jo': 319,
  5.  'bhi': 670,
  6.  'ap': 276,
  7.  'se': 1235,
  8.  'tou': 99,
  9.  'behtar': 43,
  10. """

到这里,比较建议把lang_process存储一下,之后就不用重新生成这个对应关系了。

  1. convert_to_pickle(lang_process, './data/lang_process.pkl')

将text转换为Tensor

需要注意的是,前面在生成word2index的时候, 都进行了单词的处理(大小写等)。所以现在将text转为tensor的时候,也需要注意这一点。(不然会有很多单词匹配不到对应的index, 可能是由于大小写等的原因)

于是下面将text转换为Tensor。

  1. # 把data中的句子按顺序转为tensor
  2. # 这里转换的时候,句子中的单词也是需要标准化的
  3. def convertWord2index(word):
  4.     if lang_process.word2index.get(word)==None:
  5.         # 一些出现次数很少的词汇使用1来表示
  6.         return 1
  7.     else:
  8.         return lang_process.word2index.get(word)
  9. input_tensor = [[convertWord2index(s) for s in normalizeString(es).split(' ')]  for es in data["review"].values.tolist()]

最后,我们简单看一下转换完之后,word和index是否是对应的。

  1. # 查看最后两句话的文字
  2. data["review"].values.tolist()[-2:]
  3. """
  4. ['Ma na suna ha lemon sa haddiyan kamzor hoti hn regular Lana sa?',
  5.  'Ball poar jadooi giraft se inhe rafter aur swing ko qaboo karne ka hairat angez fun aata hai']
  6. """
  7. # 查看最后两句话的index
  8. input_tensor[-2:]
  9. """
  10. [[192, 288, 1741, 239, 2065, 1552, 1, 4867, 169, 487, 5937, 181, 1552, 28],
  11.  [5012,
  12.   1,
  13.   1,
  14.   3569,
  15.   5,
  16.   2386,
  17.   7113,
  18.   43,
  19.   1,
  20.   145,
  21.   1,
  22.   406,
  23.   49,
  24.   6395,
  25.   4930,
  26.   753,
  27.   1923,
  28.   84]]
  29. """
  30. # 这个index可以和word对应上(可以看到是忽略大小写的-这个很重要)
  31. lang_process.index2word[192]
  32. """
  33. 'ma'
  34. """

Word Padding

在这一步,我们将Tensor转换为定长的Tensor, 多余的去掉, 不足的补0。这一步是方便使用GPU计算。可以使用batch来进行加速计算。

那么首先,我们要确定一下最后统一之后句子的长度。我们首先看一下句子长度的统计信息。

  1. # 查看句子的平均长度, 长度的中位数, 最长的长度
  2. sentence_length = [len(t) for t in input_tensor]
  3. print(np.mean(sentence_length))
  4. print(np.median(sentence_length))
  5. print(np.max(sentence_length))
  6. """
  7. 16.20480404551201
  8. 12.0
  9. 313
  10. """

可以看到最长是313个字符,我们画一下柱状图,可以更加直观看到句子长度的分布。

  1. fig = plt.figure()
  2. ax = fig.add_subplot(1,1,1)
  3. bins = np.arange(0,300,10) # 产生区间刻度
  4. ax.hist(sentence_length,bins=bins)
  5. fig.show()
RNN实现情感分类(Emotion Classifier)

我们可以看到,主要集在在100以内。那么我们再细化一下,画一下细化后的分布图。

  1. fig = plt.figure()
  2. ax = fig.add_subplot(1,1,1)
  3. bins = np.arange(0,100,5) # 产生区间刻度
  4. ax.hist(sentence_length,bins=bins)
  5. fig.show()
RNN实现情感分类(Emotion Classifier)

根据上面的图像, 我们可以知道, 句子长度取80是一个比较好的值。即超过80个单词的句子去掉后面的部分, 少于80个单词的句子后面补充0。

  1. def pad_sequences(x, max_len):
  2.     """定义自动填充的函数
  3.     """
  4.     padded = np.zeros((max_len), dtype=np.int64)
  5.     if len(x) > max_len:
  6.         padded[:] = x[:max_len]
  7.     else:
  8.         padded[:len(x)] = x
  9.     return padded

接下来进行padding,并简单查看一下padding之后的结果。

  1. input_tensor = [pad_sequences(x, 80) for x in input_tensor]
  2. # 查看一下完成填充之后的数据
  3. input_tensor[-2:]
  4. """
  5. [array([ 192,  288, 1741,  239, 2065, 1552,    1, 4867,  169,  487, 5937,
  6.          181, 1552,   28,    0,    0,    0,    0,    0,    0,    0,    0,
  7.            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
  8.            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
  9.            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
  10.            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
  11.            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
  12.            0,    0,    0], dtype=int64),
  13.  array([5012,    1,    1, 3569,    5, 2386, 7113,   43,    1,  145,    1,
  14.          406,   49, 6395, 4930,  753, 1923,   84,    0,    0,    0,    0,
  15.            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
  16.            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
  17.            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
  18.            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
  19.            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
  20.            0,    0,    0], dtype=int64)]
  21. """

将Label转为index(one-hot的格式)

接下来就是数据预处理的最后一部分,我们需要处理一下label的数据。在PyTorch中,只需要转换为标号即可。

我们首先查看一下有多少个分类。

  1. # 查看一下所有的分类
  2. data['label'].unique()
  3. """
  4. array(['Negative', 'Positive'], dtype=object)
  5. """

接着我们对label进行转换

  1. index2emotion = {0: 'Negative', 1: 'Positive'}
  2. emotion2index = {'Negative' : 0, 'Positive' : 1}
  3. target_tensor = [emotion2index.get(s) for s in data['label'].values.tolist()]
  4. # 简单查看一下样本
  5. target_tensor[:10]
  6. """
  7. [0, 1, 0, 0, 1, 0, 0, 1, 1, 0]
  8. """

到这里我们就基本完成了数据的预处理的部分。到这里我自己通常会将target_tensor和input_tensor保存一下,之后就不用在进行重复的数据预处理了

数据的保存

  1. convert_to_pickle(input_tensor, './data/input_tensor.pkl')
  2. convert_to_pickle(target_tensor, './data/target_tensor.pkl')

到这里,数据集的预处理就已经全部处理好了,下面就是创建Data Loader, 最后可以用来放入整个模型中去。

Data Loader

这一部分主要有下面的两个步骤来完成。

  • 数据集的划分, 训练集和测试集
  • 数据集的加载, 使用DataLoader来加载数据集

数据集的划分

首先,我们完成数据集的划分,我们划出10%作为测试来使用。

  1. # 数据集的划分(这里全部是训练集)
  2. END = int(len(input_tensor)*0.9)
  3. # 将数据转为tensor的数据格式
  4. input_tensor_train = torch.from_numpy(np.array(input_tensor[:END]))
  5. target_tensor_train = torch.from_numpy(np.array(target_tensor[:END])).long()
  6. input_tensor_test = torch.from_numpy(np.array(input_tensor[END:]))
  7. target_tensor_test = torch.from_numpy(np.array(target_tensor[END:])).long()
  8. # Show length
  9. len(input_tensor_train), len(target_tensor_train), len(input_tensor_test), len(target_tensor_test)
  10. """
  11. (5695, 5695, 633, 633)
  12. """

 创建DataLoader

接下来,我们创建dataloader,方便之后Pytorch进行训练。

  1. # 加载dataloader
  2. train_dataset = Data.TensorDataset(input_tensor_train, target_tensor_train) # 训练样本
  3. test_dataset = Data.TensorDataset(input_tensor_test, target_tensor_test) # 测试样本
  4. MINIBATCH_SIZE = 64
  5. train_loader = Data.DataLoader(
  6.     dataset=train_dataset,
  7.     batch_size=MINIBATCH_SIZE,
  8.     shuffle=True,
  9.     num_workers=2           # set multi-work num read data
  10. )
  11. test_loader = Data.DataLoader(
  12.     dataset=test_dataset,
  13.     batch_size=MINIBATCH_SIZE,
  14.     shuffle=True,
  15.     num_workers=2           # set multi-work num read data
  16. )

到这里,就创建好了dataload, 后面就可以开始构建网络,可以开始训练了。

创建Model

后面的内容就是和之前这篇RNN完成姓名分类是一样的了。

模型的建立

  1. # GRU的model
  2. class EmotionGRU(nn.Module):
  3.     def __init__(self, vocab_size, embedding_dim, hidden_units, batch_sz, output_size, layers=2):
  4.         super(EmotionGRU, self).__init__()
  5.         self.vocab_size = vocab_size # 总的单词的个数
  6.         self.embedding_dim = embedding_dim
  7.         self.hidden_units = hidden_units
  8.         self.batch_sz = batch_sz
  9.         self.output_size = output_size
  10.         self.num_layers = layers
  11.         # layers
  12.         self.embedding = nn.Embedding(self.vocab_size, self.embedding_dim) # 可以将标号转为向量
  13.         self.dropout = nn.Dropout(0.5)
  14.         self.gru = nn.GRU(self.embedding_dim, self.hidden_units, num_layers=self.num_layers, batch_first = True, bidirectional=True, dropout=0.5)
  15.         self.fc = nn.Linear(self.hidden_units*2, self.output_size)
  16.     def init_hidden(self):
  17.         # 使用了双向RNN, 所以num_layer*2
  18.         return torch.zeros((self.num_layers*2, self.batch_sz, self.hidden_units)).to(device)
  19.     def forward(self,x):
  20.         self.batch_sz = x.size(0)
  21.         # print(x.shape)
  22.         x = self.embedding(x)
  23.         # print(x.shape)
  24.         self.hidden = self.init_hidden()
  25.         output, self.hidden = self.gru(x, self.hidden)
  26.         # print(output.shape)
  27.         # 因为是 batch*seq*output, 所以要取最后一个seq
  28.         output = output[:,-1,:]
  29.         output = self.dropout(output)
  30.         output = self.fc(output)
  31.         # print(output.shape)
  32.         return output

 模型的测试

在正式开始训练之前,我们首先测试一下模型是否正常运行,最后输出的结果的size是否是和我们预期是一样的。

  1. # 测试模型
  2. vocab_inp_size = len(lang_process.word2index)
  3. embedding_dim = 256
  4. hidden_units = 512
  5. target_size = 2 # 一共有2种emotion
  6. layers = 3
  7. # 测试模型
  8. model = EmotionGRU(vocab_inp_size, embedding_dim, hidden_units, MINIBATCH_SIZE, target_size, layers).to(device)
  9. # 测试数据
  10. it = iter(train_loader)
  11. x, y = next(it)
  12. output = model(x.to(device))
  13. # 64*2, 表示一共有64个样本, 每个样本是2个emotion的概率
  14. output.size()
  15. """
  16. torch.Size([64, 2])
  17. """

训练模型

  • 定义辅助函数(这里是计算准确率的函数)
  • 定义损失函数和优化器
  • 开始训练

定义辅助函数

  1. def accuracy(target, logit):
  2.     ''' Obtain accuracy for training round '''
  3.     target = torch.max(target, 1)[1] # convert from one-hot encoding to class indices
  4.     corrects = (logit == target).sum()
  5.     accuracy = 100.0 * corrects / len(logit)
  6.     return accuracy

定义损失函数, 优化器和开始优化

  1. # 模型超参数
  2. vocab_inp_size = len(lang_process.word2index)
  3. embedding_dim = 256
  4. hidden_units = 512
  5. target_size = 2 # 一共有6种emotion
  6. num_layers = [1,2,3]
  7. for layers in num_layers:
  8.     # 测试模型
  9.     modelGRU = EmotionGRU(vocab_inp_size, embedding_dim, hidden_units, MINIBATCH_SIZE, target_size, layers).to(device)
  10.     modelLSTM = EmotionLSTM(vocab_inp_size, embedding_dim, hidden_units, MINIBATCH_SIZE, target_size, layers).to(device)
  11.     models = {'GRU':modelGRU, 'LSTM',modelLSTM}
  12.     for key, model in models.items():
  13.         # 定义损失函数和优化器
  14.         criterion = nn.CrossEntropyLoss()
  15.         optimizer = optim.Adam(model.parameters(),lr=0.001)
  16.         # 开始训练
  17.         num_epochs = 25
  18.         for epoch in range(num_epochs):
  19.             start = time.time()
  20.             train_total_loss = 0 # 记录一整个epoch中的平均loss
  21.             train_total_accuracy = 0 # 记录一整个epoch中的平均accuracy
  22.             ### Training
  23.             for batch, (inp, targ) in enumerate(train_loader):
  24.                 predictions = model(inp.to(device))
  25.                 # 计算误差     
  26.                 loss = criterion(predictions, targ.to(device))
  27.                 # 反向传播, 修改weight
  28.                 optimizer.zero_grad()
  29.                 loss.backward()
  30.                 optimizer.step()
  31.                 # 记录Loss下降和准确率的提升
  32.                 batch_loss = (loss / int(targ.size(0))) # 记录一个bacth的loss      
  33.                 batch_accuracy = accuracy(predictions, targ.to(device))
  34.                 train_total_loss = train_total_loss + batch_loss
  35.                 train_total_accuracy = train_total_accuracy + batch_accuracy
  36.                 if batch % 25 == 0:
  37.                     record_train_accuracy = train_total_accuracy.cpu().detach().numpy()/(batch+1)
  38.                     print('Epoch {} Batch {} Accuracy {:.4f}. Loss {:.4f}'.format(epoch + 1,
  39.                                                                  batch,
  40.                                                                  train_total_accuracy.cpu().detach().numpy()/(batch+1),
  41.                                                                  train_total_loss.cpu().detach().numpy()/(batch+1)))
  42.             # 每一个epoch来计算在test上的准确率
  43.             print('------------')
  44.             model.eval()
  45.             test_total_accuracy = 0
  46.             for batch, (input_data, target_data) in enumerate(test_loader):
  47.                 predictions = model(input_data.to(device))
  48.                 batch_accuracy = accuracy(predictions, target_data.to(device))
  49.                 test_total_accuracy = test_total_accuracy + batch_accuracy
  50.             print('Test : Lay {}, Model {}, Epoch {} Accuracy {:.4f}'.format(layers, key, epoch + 1, test_total_accuracy.cpu().detach().numpy()/(batch+1)))
  51.             record_test_accuracy = test_total_accuracy.cpu().detach().numpy()/(batch+1)
  52.             if epoch == num_epochs - 1:
  53.                 # 把最后一轮的结果写入文件
  54.                 with open('byr.txt','a') as file:
  55.                     file.write('{},{},{:.4f},{:.4f}'.format(key,layers,record_train_accuracy,record_test_accuracy))
  56.             print('============')

到这里就开始训练,并每个epoch会打印在测试集上的效果。

模型的保存

最后一步,我们将上面的模型进行保存即可。

  1. # 保存模型
  2. torch.save(model, 'EmotionRNN.pkl')

结语

到这里,就完成了整个RNN用于感情分类的步骤。这里最主要的步骤,还是 记录一下整个处理自然语言时的步骤。只是我自己的一个整理,可能并不是很完整。

RNN实现情感分类(Emotion Classifier)

完整的代码见下面的链接 :  Emotion RNN(RNN用于语句情感分类)

  • 微信公众号
  • 关注微信公众号
  • weinxin
  • QQ群
  • 我们的QQ群号
  • weinxin
王 茂南

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: