Skip to content

Latest commit

 

History

History
319 lines (247 loc) · 12.5 KB

File metadata and controls

319 lines (247 loc) · 12.5 KB

10.7 文本情感分类:使用循环神经网络

文本分类是自然语言处理的一个常见任务,它把一段不定长的文本序列变换为文本的类别。本节关注它的一个子问题:使用文本情感分类来分析文本作者的情绪。这个问题也叫情感分析,并有着广泛的应用。例如,我们可以分析用户对产品的评论并统计用户的满意度,或者分析用户对市场行情的情绪并用以预测接下来的行情。

同搜索近义词和类比词一样,文本分类也属于词嵌入的下游应用。在本节中,我们将应用预训练的词向量和含多个隐藏层的双向循环神经网络,来判断一段不定长的文本序列中包含的是正面还是负面的情绪。

在实验开始前,导入所需的包或模块。

import collections
from collections import defaultdict
import os
import random
import tarfile
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import preprocessing
from tensorflow.keras import layers
from tensorflow.keras.preprocessing.text import text_to_word_sequence, one_hot, Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
import torchtext.vocab as Vocab
import numpy as np
import sys
import time
import os
sys.path.append("..")
import d2lzh_tensorflow2 as d2l
print(tf.test.gpu_device_name())
DATA_ROOT = "../../data"

os.environ["CUDA_VISIBLE_DEVICES"] = "0"
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

DATA_ROOT = "/S1/CSCL/tangss/Datasets"

10.7.1 文本情感分类数据

我们使用斯坦福的IMDb数据集(Stanford's Large Movie Review Dataset)作为文本情感分类的数据集 [1]。这个数据集分为训练和测试用的两个数据集,分别包含25,000条从IMDb下载的关于电影的评论。在每个数据集中,标签为“正面”和“负面”的评论数量相等。

10.7.1.1 读取数据

首先下载这个数据集到DATA_ROOT路径下,然后解压。

#数据放入data目录下,代码解压速度较慢,如果不想用代码解压,也可直接手动解压,跳过这一步
import os
path = os.getcwd()#返回当前进程的工作目录
a_path = os.path.abspath(os.path.join(path, "../../data/aclImdb_v1.tar.gz"))
with tarfile.open(a_path, 'r') as f:
    f.extractall(DATA_ROOT)

接下来,读取训练数据集和测试数据集。每个样本是一条评论及其对应的标签:1表示“正面”,0表示“负面”。

from tqdm import tqdm
# 本函数已保存在d2lzh_tensorflow2包中方便以后使用
def read_imdb(folder='train', data_root="../../data/aclImdb/"):
    data = []
    for label in ['pos', 'neg']:
        folder_name = os.path.join(data_root, folder, label)
        for file in (os.listdir(folder_name)):
            with open(os.path.join(folder_name, file), 'rb') as f:
                review = f.read().decode('utf-8').replace('\n', '').lower()
                data.append([review, 1 if label == 'pos' else 0])
    random.shuffle(data)
    return data

train_data, test_data = read_imdb('train'), read_imdb('test')

10.7.1.2 预处理数据

我们需要对每条评论做分词,从而得到分好词的评论。这里定义的get_tokenized_imdb函数使用最简单的方法:基于空格进行分词。

# 本函数已保存在d2lzh_pytorch包中方便以后使用
def get_tokenized_imdb(data):
    """
    data: list of [string, label]
    """
    def tokenizer(text):
        return [tok.lower() for tok in text.split(' ')]
    return [tokenizer(review) for review, _ in data]

现在,我们可以根据分好词的训练数据集来创建词典了。我们在这里过滤掉了出现次数少于5的词。

# 本函数已保存在d2lzh_tensorflow2包中方便以后使用
def get_vocab_imdb(data):
    tokenized_data = get_tokenized_imdb(data)
    #counter已经创建了一个词典,统计了每个词出现的频率
    counter = collections.Counter([tk for st in tokenized_data for tk in st])
    return Vocab.Vocab(counter, min_freq=5)
    #text值保存counter中出现频率大于等于五的词
#     text = {w: freq for w, freq in counter.most_common() if freq >= 5}
#     vocab ={index:word for word,index in enumerate(text.keys())}
#     return vocab
    #return Vocab.Vocab(counter, min_freq=5)


vocab = get_vocab_imdb(train_data)
'# words in vocab:', len(vocab)

输出:

('# words in vocab:', 46152)

因为每条评论长度不一致所以不能直接组合成小批量,我们定义preprocess_imdb函数对每条评论进行分词,并通过词典转换成词索引,然后通过截断或者补0来将每条评论长度固定成500。

def preprocess_imdb(data, vocab):  # 本函数已保存在d2lzh_tensorflow2包中方便以后使用
    max_l = 500

    # 将每条评论通过截断或者补0,使得长度变成500
    def pad(x):
        return x[:max_l] if len(x) > max_l else x + [0] * (max_l - len(x))
    
    #tokenized_data为一个二维的列表,里面有我们分好的词
    tokenized_data = get_tokenized_imdb(data)
    
     #将每个词转换为词索引并进行截断或补0
    features = tf.Variable([pad([vocab[word] for word in words] ) for words in tokenized_data])
    labels = tf.Variable([score for _, score in data])
    return features, labels

10.7.1.3 创建数据迭代器

现在,我们创建数据迭代器。每次迭代将返回一个小批量的数据。

batch_size = 64
train_set = (tf.data.Dataset.from_tensor_slices(
    ((preprocess_imdb(train_data, vocab))))
    .repeat()
    .shuffle(2048)
    .batch(batch_size)
    .prefetch(AUTO))
test_set = (tf.data.Dataset.from_tensor_slices(
    ((preprocess_imdb(test_data, vocab))))
    .shuffle(2048)
    .batch(batch_size)
    .prefetch(AUTO))

打印第一个小批量数据的形状以及训练集中小批量的个数。

for X, y in train_set:
    print('X', X.shape, 'y', y.shape)
    print(X,y)
    break
'#batches:', data.shape[0]//batch_size

输出:

X (64, 500) y (64,)
('#batches:', 390)

10.7.2 使用循环神经网络的模型

在这个模型中,每个词先通过嵌入层得到特征向量。然后,我们使用双向循环神经网络对特征序列进一步编码得到序列信息。最后,我们将编码的序列信息通过全连接层变换为输出。具体来说,我们可以将双向长短期记忆在最初时间步和最终时间步的隐藏状态连结,作为特征序列的表征传递给输出层分类。在下面实现的BiRNN类中,Embedding实例即嵌入层,LSTM实例即为序列编码的隐藏层,Linear实例即生成分类结果的输出层。

#因为tensorflow并没有像pytorch,mxnet关于glove接口的api,所以必须要重写一个

def load_embedding_from_disks(glove_filename, with_indexes=True):
    """
    Read a GloVe txt file. If `with_indexes=True`, we return a tuple of two dictionnaries
    `(word_to_index_dict, index_to_embedding_array)`, otherwise we return only a direct 
    `word_to_embedding_dict` dictionnary mapping from a string to a numpy array.
    """
    if with_indexes:
        word_to_index_dict = dict()
        index_to_embedding_array = []
        index_to_word_dict = dict()
        word_to_embedding = dict()
    else:
        word_to_embedding_dict = dict()

    
    with open(glove_filename, 'r',encoding='utf-8') as glove_file:
        for (i, line) in enumerate(glove_file):
            
            split = line.split(' ')
            
            word = split[0]
            
            representation = split[1:]
            representation = np.array(
                [float(val) for val in representation]
            )
            
            if with_indexes:
                word_to_index_dict[word] = i
                index_to_word_dict[i] = word
                word_to_embedding[word] = representation
                index_to_embedding_array.append(representation)
            else:
                word_to_embedding_dict[word] = representation

    _WORD_NOT_FOUND = [0.0]* len(representation)  # Empty representation for unknown words.
    if with_indexes:
        _LAST_INDEX = i + 1
        word_to_index_dict = defaultdict(lambda: _LAST_INDEX, word_to_index_dict)
        index_to_embedding_array = np.array(index_to_embedding_array + [_WORD_NOT_FOUND])
        return word_to_index_dict, index_to_embedding_array,index_to_word_dict,word_to_embedding
    else:
        word_to_embedding_dict = defaultdict(lambda: _WORD_NOT_FOUND)
        return word_to_embedding_dict

word_to_index, index_to_embedding, index_to_word,word_to_embedding = load_embedding_from_disks("C:/Users/HP/dive into d2l/code/chapter10_natural-language-processing/embeddings/ GloVe.6B/glove.6B.50d.txt", with_indexes=True)

# 本函数已保存在d2lzh_tensorflow包中方便以后使用
def get_weights(vocab, word_to_embedding,embedding_dim,word_to_index,index_to_embedding):
    """从预训练好的vocab中提取出words对应的词向量"""
    embedding_matrix = np.zeros((len(vocab), embedding_dim))
#     embedding_matrix = np.zeros((len(vocab), embedding_dim))
    for index, word in enumerate(vocab.itos):
        if word in word_to_embedding.keys():
            embedding_matrix[index] = index_to_embedding[index]
    return embedding_matrix
embedding_matrix = get_weights(vocab,word_to_embedding,50,word_to_index,index_to_embedding)


embed_size, num_hiddens, max_len = 50, 100, 500
num_epochs = 5

#因为tensorflowlSTM无法自动堆叠,故我们不进行堆叠
model = tf.keras.Sequential([
    layers.Embedding(len(vocab), embed_size,weights=[embedding_matrix],input_length=500),
    layers.Bidirectional(layers.LSTM(num_hiddens)),
    tf.keras.layers.Dense(2,activation='softmax')
])

model.layers[0].trainable = False
model.summary()

10.7.2.1 训练并评价模型

这时候就可以开始训练模型了。

model.compile(tf.keras.optimizers.Adam(0.01),
            loss='sparse_categorical_crossentropy',
            metrics=['sparse_categorical_accuracy'])

model.fit(
    train_set,
    steps_per_epoch=data.shape[0]//batch_size,
    validation_data= test_set,
    epochs=5
    )

输出:

Train for 390 steps, validate for 391 steps
Epoch 1/5
390/390 [==============================] - 33s 86ms/step - loss: 0.6919 - sparse_categorical_accuracy: 0.5321 - val_loss: 0.6814 - val_sparse_categorical_accuracy: 0.5680
Epoch 2/5
390/390 [==============================] - 31s 80ms/step - loss: 0.6584 - sparse_categorical_accuracy: 0.6091 - val_loss: 0.6442 - val_sparse_categorical_accuracy: 0.6415
Epoch 3/5
390/390 [==============================] - 32s 82ms/step - loss: 0.6078 - sparse_categorical_accuracy: 0.6710 - val_loss: 0.6638 - val_sparse_categorical_accuracy: 0.6090
Epoch 4/5
390/390 [==============================] - 32s 82ms/step - loss: 0.5892 - sparse_categorical_accuracy: 0.6888 - val_loss: 0.6524 - val_sparse_categorical_accuracy: 0.6535
Epoch 5/5
390/390 [==============================] - 33s 84ms/step - loss: 0.5105 - sparse_categorical_accuracy: 0.7536 - val_loss: 0.5836 - val_sparse_categorical_accuracy: 0.7169

最后,定义预测函数。

# 本函数已保存在d2lzh_tensorflow2包中方便以后使用
def predict_sentiment(net, vocab, sentence):
    """sentence是词语的列表"""
    sentence = tf.Variable([vocab.stoi[word] for word in sentence])
    print(sentence)
    print(tf.reshape(sentence,[1,-1]))
    label = np.argmax(net(tf.reshape(sentence,[1,-1])), axis=1)
    return 'positive' if np.array(label) == 1 else 'negative'

下面使用训练好的模型对两个简单句子的情感进行分类。

predict_sentiment(net, vocab, ['this', 'movie', 'is', 'so', 'great']) # positive
predict_sentiment(net, vocab, ['this', 'movie', 'is', 'so', 'bad']) # negative

小结

  • 文本分类把一段不定长的文本序列变换为文本的类别。它属于词嵌入的下游应用。
  • 可以应用预训练的词向量和循环神经网络对文本的情感进行分类。

参考文献

[1] Maas, A. L., Daly, R. E., Pham, P. T., Huang, D., Ng, A. Y., & Potts, C. (2011, June). Learning word vectors for sentiment analysis. In Proceedings of the 49th annual meeting of the association for computational linguistics: Human language technologies-volume 1 (pp. 142-150). Association for Computational Linguistics.


注:本节除代码外与原书基本相同,原书传送门