情感分析
如果MNIST是计算机视觉的“hello world”,那么IMDB评论数据集就是自然语言处理的“hello world”:它提取了来自著名的互联网电影数据库的50000条英文电影评论(其中25000条用于训练,25000条用于测试),每条评论的简单二元目标值表明了该评论是负面(0)还是正面(1)。就像MNIST一样,IMDB评论数据集也很受欢迎,这有充分的理由:它足够简单,可以在合理的时间内利用笔记本电脑来处理,但又具有挑战性。Keras提供了一个简单的函数来加载它:
from tensorflow import keras
(X_train, y_train), (X_test, y_test) = keras.datasets.imdb.load_data()
X_train[0][:10]
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/imdb.npz
17465344/17464789 [==============================] - 1s 0us/step
17473536/17464789 [==============================] - 1s 0us/step
[1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65]
数据集已经进行了预处理:X_train由一个评论列表组成,每条评论都由一个NumPy整数数组表示,其中每个整数代表一个单词。删除了所有标点符号,然后将单词转换为小写字母,用空格分隔,最后按频率索引(因此,小的整数对应于常用单词),整数0、1和2是特殊的:它们分别表示填充令牌、序列开始(SSS)令牌和位置单词。如果想可视化评论可以按以下方式对其进行解码:
word_index = keras.datasets.imdb.get_word_index()
id_to_word = {id_ + 3: word for word, id_ in word_index.items()}
for id_, token in enumerate(("<pad>", "<sos>", "<unk>")): # 将整数0、1和2可视化为pad、sos和unk
id_to_word[id_] = token
' '.join([id_to_word[id_] for id_ in X_train[0][:10]])
'<sos> this film was just brilliant casting location scenery story'
在实际的项目中,不得不自己处理文本。但是可以使用与之前使用的相同的Tokenizer类来执行此操作,但是这次要设置char_level=False(这是默认值)。在对单词进行编码时,它滤掉很多字符,包括大多数标点符号、换行符和制表符(但是可以通过filter参数来更改)。最重要的是,它使用空格来表示单词边界。对于英文和许多其他在单词之间使用空格的脚本(书面语言)这是可以的,但并非所有脚本都用此方式。中文在单词之间不使用空格,越南语甚至在单词中也使用空格,例如德语之类的语言经常将多个单词附加在一起,而没有空格。即使在英文中,空格也不总是分词的最佳方法:例如“San Francisco”
还有更好的选择,Taku Kudo在20118年发表的论文引入了一种无监督学习技术,用一种独立于语言的方式在子单词级别对文本进行分词和组词,像对待其他字符一样对待空格。使用这种方法,即使模型遇到一个从未见过的单词,也仍然可以合理地猜出其含义。例如,它可能在训练期间从未见过“smartest”一词,但可能学会了“smart”一词,并且还学习到后缀“est”的意思是“the most”,因此它可以推断出“smartest”的意思。Google的SentencePiece项目提供了一个开源实现,Takku Kudo和John Rihardson在论文中对此进行了描述
Rico Sennrich等人在较早的论文中提出了另一种选择,探索了创建子单词编码的其他方式(例如使用字节对编码)。TensorFlow团队于2019年6月发布了TF.Text库,该库实现了各种分词策略,包括WordPiece(字节对编码的一种变体)
如果想将模型部署到移动设备或Web浏览器上,而又不想每次都编写不同的预处理函数,那么可以使用TensorFlow操作来做预处理,因此其包含在模型本身中。首先使用TensorFlow数据集以文本(字节字符串)的形式加载原始的IMDB评论:
import tensorflow as tf
import tensorflow_datasets as tfds
datasets, info = tfds.load('imdb_reviews', as_supervised=True, with_info=True)
train_size = info.splits['train'].num_examples
# 编写预处理函数
def preprocess(X_batch, y_batch):
X_batch = tf.strings.substr(X_batch, 0, 300)
X_batch = tf.strings.regex_replace(X_batch, b'<br\\s*/?>', b' ')
X_batch = tf.strings.regex_replace(X_batch, b"[^a-zA-z']", b' ')
X_batch = tf.strings.split(X_batch)
return X_batch.to_tensor(default_value=b'<pad>'), y_batch
[1mDownloading and preparing dataset Unknown size (download: Unknown size, generated: Unknown size, total: Unknown size) to C:\Users\reion\tensorflow_datasets\imdb_reviews\plain_text\1.0.0...[0m
Dl Completed...: 0 url [00:00, ? url/s]
Dl Size...: 0 MiB [00:00, ? MiB/s]
Generating splits...: 0%| | 0/3 [00:00<?, ? splits/s]
Generating train examples...: 0 examples [00:00, ? examples/s]
Shuffling imdb_reviews-train.tfrecord...: 0%| | 0/25000 [00:00<?, ? examples/s]
Generating test examples...: 0 examples [00:00, ? examples/s]
Shuffling imdb_reviews-test.tfrecord...: 0%| | 0/25000 [00:00<?, ? examples/s]
Generating unsupervised examples...: 0 examples [00:00, ? examples/s]
Shuffling imdb_reviews-unsupervised.tfrecord...: 0%| | 0/50000 [00:00<?, ? examples/s]
[1mDataset imdb_reviews downloaded and prepared to C:\Users\reion\tensorflow_datasets\imdb_reviews\plain_text\1.0.0. Subsequent calls will reuse this data.[0m
它从阶段评论开始,每条评论仅保留前300个字符:这会加快训练速度,并且不会对性能产生太大的影响,因此通常可以在第一句话或第二句话中判断出评论是正面还是负面的。它使用正则表达式用空格来替换<br >标记,还用空格替换字母和引号以外的所有字符。例如,文本"Well, I can't<br >"将变成"Well I can't"。最后preprocess()将评论按空格分割,并返回一个不规则的张量,并将该不规则的张量转换为密集张良,还使用填充标记"
接下来,需要构建词汇表。这需要依次遍历整个数据集,应用preprocess()函数,并使用Counter来对每个单词出现的次数进行计数:
from collections import Counter
vocabulary = Counter()
for X_batch, y_batch in datasets['train'].batch(32).map(preprocess):
for review in X_batch:
vocabulary.update(list(review.numpy()))
# 最常见的三个单词
vocabulary.most_common()[:3]
[(b'<pad>', 224533), (b'the', 61156), (b'a', 38567)]
但是,为了良好的性能,可能不需要模型知道字典中的所有单词,因此截断词汇表,只保留10000个最常见的单词:
vocab_size = 10000
truncated_vocabulary = [word for word, count in vocabulary.most_common()[:vocab_size]]
现在需要添加一个预处理步骤,以便把每个单词替换为其ID(即其在词汇表中的索引),使用out-of-vocabulary(oov)存储桶来创建一个查找表
words = tf.constant(truncated_vocabulary)
word_ids = tf.range(len(truncated_vocabulary), dtype=tf.int64)
vocab_init = tf.lookup.KeyValueTensorInitializer(words, word_ids)
num_oov_buckets = 1000
table = tf.lookup.StaticVocabularyTable(vocab_init, num_oov_buckets)
# 然后可以使用此表来查找几个单词的ID:
table.lookup(tf.constant([b'This movie was faaaaaantastic'.split()]))
<tf.Tensor: shape=(1, 4), dtype=int64, numpy=array([[ 24, 12, 13, 10053]], dtype=int64)>
在表中可以找到单词’this‘、’movie‘和’was‘,因此它们的ID低于10000,而单词“faaaaaantastic”没找到,因此被映射的ID大于或等于10000的一个ovv桶中
TF Transform提供了一些有用的函数来处理此类词汇表。例如,查看tft.compute_and_apply_vocabulary()函数:它会遍历数据集来查早所有不同的的单词并构建词汇表,并将生成对使用此词汇表的单词进行编码所需的TF操作
现在,准备创建最终的训练集,对评论进行批处理,然后使用preprocess()函数将它们转换为单词的短序列,然后使用简单的encode_words()函数来对这些单词进行编码,该函数会使用刚才构建的单词表,最后预取一下批次:
def encode_words(X_batch, y_batch):
return table.lookup(X_batch), y_batch
train_set = datasets['train'].batch(32).map(preprocess)
train_set = train_set.map(encode_words).prefetch(1)
# 最后创建模型并对其进行训练
embed_size = 128
model = keras.models.Sequential([
keras.layers.Embedding(vocab_size + num_oov_buckets, embed_size, input_shape=[None]),
keras.layers.GRU(128, return_sequences=True),
keras.layers.GRU(128),
keras.layers.Dense(1, activation='sigmoid')
])
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
history = model.fit(train_set, epochs=5)
Epoch 1/5
782/782 [==============================] - 9s 10ms/step - loss: 0.6192 - accuracy: 0.6206
Epoch 2/5
782/782 [==============================] - 8s 10ms/step - loss: 0.3875 - accuracy: 0.8297
Epoch 3/5
782/782 [==============================] - 8s 10ms/step - loss: 0.2304 - accuracy: 0.9146
Epoch 4/5
782/782 [==============================] - 8s 10ms/step - loss: 0.1442 - accuracy: 0.9513
Epoch 5/5
782/782 [==============================] - 8s 10ms/step - loss: 0.1087 - accuracy: 0.9624
第一层是嵌入层,它将单词ID转换为嵌入。嵌入矩阵需要每个ID(vocab_size+num_oov_buckets)一行,每个嵌入维度一列(这里使用128维,这是可以调整的超参数)。模型的输入使形状为[批处理大小,时间步长]的2D张量,而嵌入层的输出为[批处理大小,时间步长,嵌入大小]的3D张量
该模型其余部分先当简单直接:它由两个GRU层组成,第二个GRU层仅返回最后一个时间步长的输出。输出层只是使用sigmoid激活函数来输出估计概率的单个神经元。该概率反映了评论表达了与电影有关的正面情绪。然后可以简单地编译模型,并将其拟合到之前准备的数据集中,进行几个轮次的训练