📜  在Python中使用BERT Tokenizer和TensorFlow 2.0进行文本分类

📅  最后修改于: 2020-08-26 05:09:23             🧑  作者: Mango

在本文中,我们将研究BERT,它代表“ 变形金刚”的双向编码器表示形式及其在文本分类中的应用。BERT是一种文本表示技术,例如Word Embeddings。

像单词嵌入一样,BERT也是一种文本表示技术,它融合了各种最新的深度学习算法,例如双向编码器LSTM和Transformers。BERT由Google的研究人员于2018年开发,并被证明是用于各种自然语言处理任务(例如文本分类,文本摘要,文本生成等)的最新技术。就在最近,Google宣布 BERT是被用作其搜索算法的核心部分,以更好地理解查询。

在本文中,我们将不介绍BERT如何实现的数学细节,因为已经有很多在线资源可供使用。相反,我们将看到如何使用BERT令牌生成器执行文本分类。在本文中,您将看到如何使用BERT Tokenizer创建文本分类模型。在下一篇文章中,我将解释BERT令牌生成器以及BERT嵌入层如何用于创建效率更高的NLP模型。

注意:本文中的所有脚本均已使用Google Colab环境(需要科学上网)进行了测试,并且Python运行时设置为GPU。

 

数据集

可以从该Kaggle链接下载本文中使用的数据集。

如果下载数据集并提取压缩文件,则将看到一个CSV文件。该文件包含50,000条记录和两列:回顾和观点。评论栏包含评论文本,情感栏包含评论文本。情感列可以具有两个值,即“正”和“负”,这使我们的问题成为二进制分类问题。

 

安装和导入所需的库

在可以使用BERT文本表示形式之前,需要为TensorFlow 2.0安装BERT。在终端上执行以下pip命令以安装用于TensorFlow 2.0的BERT。 

!pip install bert-for-tf2
!pip install sentencepiece

接下来,您需要确保正在运行TensorFlow 2.0。默认情况下,Google Colab不会在TensorFlow 2.0上运行您的脚本。因此,要确保您正在通过TensorFlow 2.0运行脚本,请执行以下脚本: 

try:
    %tensorflow_version 2.x
except Exception:
    pass
import tensorflow as tf

import tensorflow_hub as hub

from tensorflow.keras import layers
import bert

在上面的脚本中,除了TensorFlow 2.0外,我们还导入了tensorflow_hub,基本上可以在其中找到在TensorFlow中开发的所有预构建和预训练的模型。我们将从TF hub导入并使用内置的BERT模型。最后,如果在输出中看到以下输出,那就很好了:

TensorFlow 2.x selected.

导入和预处理数据集

以下脚本使用read_csv()Pandas数据框的方法导入数据集。该脚本还会打印数据集的形状。

movie_reviews = pd.read_csv("/content/drive/My Drive/Colab Datasets/IMDB Dataset.csv")

movie_reviews.isnull().values.any()

movie_reviews.shape

输出量

(50000, 2)

输出显示我们的数据集具有50,000行和2列。

接下来,我们将预处理数据以删除所有标点符号和特殊字符。为此,我们将定义一个函数,该函数将原始文本审阅作为输入并返回相应的已清理文本审阅。

def preprocess_text(sen):
    # Removing html tags
    sentence = remove_tags(sen)

    # Remove punctuations and numbers
    sentence = re.sub('[^a-zA-Z]', ' ', sentence)

    # Single character removal
    sentence = re.sub(r"\s+[a-zA-Z]\s+", ' ', sentence)

    # Removing multiple spaces
    sentence = re.sub(r'\s+', ' ', sentence)

    return sentence

 

TAG_RE = re.compile(r'<[^>]+>')

def remove_tags(text):
    return TAG_RE.sub('', text)

以下脚本清除所有文本评论:

reviews = []
sentences = list(movie_reviews['review'])
for sen in sentences:
    reviews.append(preprocess_text(sen))

我们的数据集包含两列,可以通过以下脚本进行验证:

print(movie_reviews.columns.values)

输出:

['review' 'sentiment']

review列包含文本,而该sentiment列包含情绪。情感列包含文本形式的值。以下脚本在sentiment列中显示唯一值:

movie_reviews.sentiment.unique()

输出:

array(['positive', 'negative'], dtype=object)

您可以看到,情感列包含两个唯一值,即positivenegative。深度学习算法适用于数字。由于输出中只有两个唯一值,我们可以将它们分别转换为1和0。以下脚本用替换positive情感,用替换1消极的情感0

y = movie_reviews['sentiment']

y = np.array(list(map(lambda x: 1 if x=="positive" else 0, y)))

现在,reviews变量包含文本评论,而y变量包含相应的标签。让我们随机打印评论。

print(reviews[10])

输出:

Phil the Alien is one of those quirky films where the humour is based around the oddness of everything rather than actual punchlines At first it was very odd and pretty funny but as the movie progressed didn find the jokes or oddness funny anymore Its low budget film thats never problem in itself there were some pretty interesting characters but eventually just lost interest imagine this film would appeal to stoner who is currently partaking For something similar but better try Brother from another planet 

显然,它看起来像是负面评论。让我们通过打印相应的标签值来确认它:

print(y[10])

输出:

0

输出0确认它是否定的。现在,我们已经对数据进行了预处理,现在可以根据文本数据创建BERT表示形式了。

创建一个BERT令牌生成器

为了使用BERT文本嵌入作为训练文本分类模型的输入,我们需要标记化文本评论。标记化是指将句子分为单个单词。为了标记文本,我们将使用BERT标记器。看下面的脚本:

BertTokenizer = bert.bert_tokenization.FullTokenizer
bert_layer = hub.KerasLayer("https://tfhub.dev/tensorflow/bert_en_uncased_L-12_H-768_A-12/1",
                            trainable=False)
vocabulary_file = bert_layer.resolved_object.vocab_file.asset_path.numpy()
to_lower_case = bert_layer.resolved_object.do_lower_case.numpy()
tokenizer = BertTokenizer(vocabulary_file, to_lower_case)

在上面的脚本中,我们首先FullTokenizerbert.bert_tokenization模块创建类的对象。接下来,我们通过从中导入BERT模型来创建BERT嵌入层hub.KerasLayer。该trainable参数设置为False,这意味着我们将不会被训练BERT嵌入。在下一行中,我们以numpy数组的形式创建BERT词汇表文件。然后,将文本设置为小写,最后将vocabulary_fileto_lower_case变量传递给BertTokenizer对象。

值得一提的是,在本文中,我们将仅使用BERT Tokenizer。在下一篇文章中,我们将使用BERT嵌入和令牌生成器。

现在让我们看看我们的BERT令牌生成器是否正在正常工作。为此,我们将标记一个随机句子,如下所示:

tokenizer.tokenize("don't be so judgmental")

输出:

['don', "'", 't', 'be', 'so', 'judgment', '##al']

您可以看到该文本已成功标记。您还可以使用convert_tokens_to_ids()tokenizer对象的来获取令牌的ID 。看下面的脚本:

tokenizer.convert_tokens_to_ids(tokenizer.tokenize("dont be so judgmental"))

您可以看到该文本已成功标记。您还可以使用convert_tokens_to_ids()tokenizer对象的来获取令牌的ID 。看下面的脚本:

tokenizer.convert_tokens_to_ids(tokenizer.tokenize("dont be so judgmental"))

输出:

[2123, 2102, 2022, 2061, 8689, 2389]

现在将定义一个函数,该函数接受单个文本审阅并返回审阅中标记化单词的ID。执行以下脚本:

def tokenize_reviews(text_reviews):
    return tokenizer.convert_tokens_to_ids(tokenizer.tokenize(text_reviews))

并执行以下脚本以实际标记输入数据集中的所有评论:

tokenized_reviews = [tokenize_reviews(review) for review in reviews]

准备训练数据

我们数据集中的评论有不同的长度。有些评论很小,而另一些则很长。要训​​练模型,输入句子的长度应相等。要创建长度相等的句子,一种方法是将较短的句子填充0s。但是,这可能导致稀疏矩阵包含大量0。另一种方法是在每批中填充句子。由于我们将分批训练模型,因此我们可以根据最长句子的长度在训练批中局部填充句子。为此,我们首先需要找到每个句子的长度。

以下脚本创建一个列表列表,其中每个子列表都包含标记化的评论,评论的标签和评论的长度:

reviews_with_len = [[review, y[i], len(review)]
                 for i, review in enumerate(tokenized_reviews)]

在我们的数据集中,上半部分评论为正面,而后半部分则包含负面评论。因此,为了在培训批次中同时拥有正面和负面评论,我们需要将评论改组。以下脚本随机地随机整理数据:

random.shuffle(reviews_with_len)

整理数据后,我们将按照评论的长度对数据进行排序。为此,我们将使用sort()列表的功能,并告诉我们要针对子列表中的第三项(即评论的长度)对列表进行排序。

reviews_with_len.sort(key=lambda x: x[2])

一旦评论按长度排序,我们就可以从所有评论中删除length属性。执行以下脚本来执行此操作:

sorted_reviews_labels = [(review_lab[0], review_lab[1]) for review_lab in reviews_with_len]

对评论进行排序后,我们将转换数据集,以便将其用于训练TensorFlow 2.0模型。运行以下代码以将排序后的数据集转换为兼容TensorFlow 2.0的输入数据集形状。

processed_dataset = tf.data.Dataset.from_generator(lambda: sorted_reviews_labels, output_types=(tf.int32, tf.int32))

最后,我们现在可以填充每个批次的数据集。我们将使用的批量大小为32,这意味着在处理32条评论之后,神经网络的权重将被更新。要在本地对批次进行评论,请执行以下操作:

BATCH_SIZE = 32
batched_dataset = processed_dataset.padded_batch(BATCH_SIZE, padded_shapes=((None, ), ()))

让我们打印第一批并查看填充如何应用到它:

next(iter(batched_dataset))

输出: 

(,
 )

上面的输出显示了前五个和最后五个已填充评论。从最近的五个评论中,您可以看到最大句子中的单词总数为21。因此,在前五个评论中,在句子的末尾添加了0,因此它们的总长度也为21。下一批的大小将取决于该批中最大句子的大小。

将填充应用于数据集后,下一步就是将数据集分为测试集和训练集。我们可以借助以下代码来做到这一点:

TOTAL_BATCHES = math.ceil(len(sorted_reviews_labels) / BATCH_SIZE)
TEST_BATCHES = TOTAL_BATCHES // 10
batched_dataset.shuffle(TOTAL_BATCHES)
test_data = batched_dataset.take(TEST_BATCHES)
train_data = batched_dataset.skip(TEST_BATCHES)

在上面的代码中,我们首先将总记录除以32,以找到批处理的总数。接下来,将10%的数据留给测试。为此,我们使用对象take()方法batched_dataset()将10%的数据存储在test_data变量中。train_data使用该skip()方法将剩余数据存储在对象中以进行训练。

数据集已经准备好了,现在我们可以创建文本分类模型了。

创建模型

现在,我们都准备创建模型。为此,我们将创建一个TEXT_MODEL继承自该类的名为的tf.keras.Model类。在类内部,我们将定义模型层。我们的模型将由三个卷积神经网络层组成。您可以改用LSTM层,也可以增加或减少层数。我已经从SuperDataScience的Google colab笔记本中复制了层的数量和类型,并且这种架构对于IMDB电影评论数据集似乎也非常适用。

现在创建模型类: 

class TEXT_MODEL(tf.keras.Model):
    
    def __init__(self,
                 vocabulary_size,
                 embedding_dimensions=128,
                 cnn_filters=50,
                 dnn_units=512,
                 model_output_classes=2,
                 dropout_rate=0.1,
                 training=False,
                 name="text_model"):
        super(TEXT_MODEL, self).__init__(name=name)
        
        self.embedding = layers.Embedding(vocabulary_size,
                                          embedding_dimensions)
        self.cnn_layer1 = layers.Conv1D(filters=cnn_filters,
                                        kernel_size=2,
                                        padding="valid",
                                        activation="relu")
        self.cnn_layer2 = layers.Conv1D(filters=cnn_filters,
                                        kernel_size=3,
                                        padding="valid",
                                        activation="relu")
        self.cnn_layer3 = layers.Conv1D(filters=cnn_filters,
                                        kernel_size=4,
                                        padding="valid",
                                        activation="relu")
        self.pool = layers.GlobalMaxPool1D()
        
        self.dense_1 = layers.Dense(units=dnn_units, activation="relu")
        self.dropout = layers.Dropout(rate=dropout_rate)
        if model_output_classes == 2:
            self.last_dense = layers.Dense(units=1,
                                           activation="sigmoid")
        else:
            self.last_dense = layers.Dense(units=model_output_classes,
                                           activation="softmax")
    
    def call(self, inputs, training):
        l = self.embedding(inputs)
        l_1 = self.cnn_layer1(l) 
        l_1 = self.pool(l_1) 
        l_2 = self.cnn_layer2(l) 
        l_2 = self.pool(l_2)
        l_3 = self.cnn_layer3(l)
        l_3 = self.pool(l_3) 
        
        concatenated = tf.concat([l_1, l_2, l_3], axis=-1) # (batch_size, 3 * cnn_filters)
        concatenated = self.dense_1(concatenated)
        concatenated = self.dropout(concatenated, training)
        model_output = self.last_dense(concatenated)
        
        return model_output

上面的脚本非常简单。在类的构造函数中,我们使用默认值初始化一些属性。这些值将稍后由TEXT_MODEL创建类的对象时传递的值替换。

接下来,已分别使用2,3和4的内核或过滤器值初始化了三个卷积神经网络层。同样,您可以根据需要更改过滤器尺寸。

接下来,在call()函数内部,将全局最大池应用于每个卷积神经网络层的输出。最后,将三个卷积神经网络层连接在一起,并将它们的输出馈送到第一个紧密连接的神经网络。第二个紧密连接的神经网络用于预测输出情感,因为它仅包含2个类别。如果输出中有更多类,则可以相应地更新output_classes变量。

现在让我们为模型的超级参数定义值。

VOCAB_LENGTH = len(tokenizer.vocab)
EMB_DIM = 200
CNN_FILTERS = 100
DNN_UNITS = 256
OUTPUT_CLASSES = 2

DROPOUT_RATE = 0.2

NB_EPOCHS = 5

接下来,我们需要创建TEXT_MODEL该类的对象,并将在最后一步中定义的超参数值传递给该类的构造函数TEXT_MODEL

text_model = TEXT_MODEL(vocabulary_size=VOCAB_LENGTH,
                        embedding_dimensions=EMB_DIM,
                        cnn_filters=CNN_FILTERS,
                        dnn_units=DNN_UNITS,
                        model_output_classes=OUTPUT_CLASSES,
                        dropout_rate=DROPOUT_RATE)

在实际训练模型之前,我们需要对其进行编译。以下脚本编译模型:

if OUTPUT_CLASSES == 2:
    text_model.compile(loss="binary_crossentropy",
                       optimizer="adam",
                       metrics=["accuracy"])
else:
    text_model.compile(loss="sparse_categorical_crossentropy",
                       optimizer="adam",
                       metrics=["sparse_categorical_accuracy"])

最后,要训练我们的模型,我们可以使用fit模型类的方法。

text_model.fit(train_data, epochs=NB_EPOCHS)

这是5个纪元后的结果:

Epoch 1/5
1407/1407 [==============================] - 381s 271ms/step - loss: 0.3037 - accuracy: 0.8661
Epoch 2/5
1407/1407 [==============================] - 381s 271ms/step - loss: 0.1341 - accuracy: 0.9521
Epoch 3/5
1407/1407 [==============================] - 383s 272ms/step - loss: 0.0732 - accuracy: 0.9742
Epoch 4/5
1407/1407 [==============================] - 381s 271ms/step - loss: 0.0376 - accuracy: 0.9865
Epoch 5/5
1407/1407 [==============================] - 383s 272ms/step - loss: 0.0193 - accuracy: 0.9931

您可以看到我们在训练集上的准确性为99.31%。

现在让我们在测试集上评估模型的性能:

results = text_model.evaluate(test_dataset)
print(results)

输出:

156/Unknown - 4s 28ms/step - loss: 0.4428 - accuracy: 0.8926[0.442786190037926, 0.8926282]

从输出中,我们可以看到测试集的准确性为89.26%。

结论

在本文中,您了解了如何使用BERT Tokenizer创建可用于执行文本分类的单词嵌入。我们对IMDB电影评论进行了情感分析,并在测试集上达到了89.26%的准确性。在本文中,我们没有使用BERT嵌入,仅使用BERT Tokenizer对单词进行了标记。在下一篇文章中,您将看到如何将BERT令牌生成器和BERT嵌入一起用于执行文本分类。