Python如何使用Tensorflow 2和Keras实现文本分类?

2021年11月11日17:19:50 发表评论 984 次浏览

在 Python 中使用 Tensorflow 和 Keras 为不同的文本分类问题(例如情感分析或 20 个新闻组分类)构建深度学习模型(使用嵌入和循环层)。

Python使用Tensorflow 2和Keras实现文本分类 - 文本分类是监督机器学习中重要且常见的任务之一。它是关于为文档、文章、书籍、评论、推文或任何涉及文本的内容分配一个类别(一个类)。它是自然语言处理的核心任务。

许多应用程序似乎使用文本分类作为主要任务,示例包括垃圾邮件过滤、情感分析、语音标记、语言检测等等。

Python如何实现文本分类?在本教程中,我们将使用 Python 中的 Tensorflow 使用 RNN 构建文本分类器模型,我们将使用IMDB 评论数据集,其中包含5 万条真实世界的电影评论及其情绪(正面或负面)。在本教程的最后,我将向你展示如何集成你自己的数据集,以便你可以在其上训练模型。

尽管我们使用的是情感分析数据集,但本教程旨在对任何任务执行文本分类,如果你希望立即执行情感分析,请查看本教程。

如果你希望使用最先进的转换器模型,例如 BERT,请查看本教程,我们为自定义数据集微调 BERT。

Python文本分类示例 - 首先,你需要安装以下库:

pip3 install tqdm numpy tensorflow==2.0.0 sklearn

现在打开一个新的 Python 笔记本或文件并继续,让我们导入我们需要的模块:

from tqdm import tqdm
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.layers import Dense, Dropout, LSTM, Embedding, Bidirectional
from tensorflow.keras.models import Sequential
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.callbacks import TensorBoard
from sklearn.model_selection import train_test_split
import numpy as np

from glob import glob
import random
import os

数据准备

现在我们我们的数据加载到的Python之前,你需要下载数据集在这里,你会看到两个文件存在,reviews.txt其中包含每行一个电影评论,并labels.txt持有其相应的标签。

以下函数加载和预处理数据集:

def load_imdb_data(num_words, sequence_length, test_size=0.25, oov_token=None):
    # read reviews
    reviews = []
    with open("data/reviews.txt") as f:
        for review in f:
            review = review.strip()
            reviews.append(review)

    labels = []
    with open("data/labels.txt") as f:
        for label in f:
            label = label.strip()
            labels.append(label)
    # tokenize the dataset corpus, delete uncommon words such as names, etc.
    tokenizer = Tokenizer(num_words=num_words, oov_token=oov_token)
    tokenizer.fit_on_texts(reviews)
    X = tokenizer.texts_to_sequences(reviews)
    X, y = np.array(X), np.array(labels)
    # pad sequences with 0's
    X = pad_sequences(X, maxlen=sequence_length)
    # convert labels to one-hot encoded
    y = to_categorical(y)
    # split data to training and testing sets
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=1)
    data = {}
    data["X_train"] = X_train
    data["X_test"]= X_test
    data["y_train"] = y_train
    data["y_test"] = y_test
    data["tokenizer"] = tokenizer
    data["int2label"] =  {0: "negative", 1: "positive"}
    data["label2int"] = {"negative": 0, "positive": 1}
    return data

Python如何实现文本分类?这里有很多内容,这个函数执行以下操作:

  • 它从前面提到的文件加载数据集。
  • 之后,它使用 Keras 的实用程序Tokenizer类,它帮助我们自动删除所有标点符号,标记语料库,删除名称等稀有单词,将文本句子转换为数字序列(每个单词对应一个数字)。
  • 我们已经知道神经网络需要固定长度的输入,并且由于评论没有相同长度的单词,我们需要一种方法来使序列的长度固定为固定大小。pad_sequences()函数派上用场,我们告诉它我们只想在每个评论(参数)中说300 个单词maxlen,它将删除超过该数字的单词,并将用0填充下面的评论300 .
  • 我们使用Keras' to_categorical()函数来独热编码标签,这是一个二元分类,所以它会转换标签0到[1,0]矢量,和1至[0,1] 。但总的来说,它将分类标签转换为固定长度的向量。
  • 之后,我们使用 sklearn 的train_test_split()函数将我们的数据集拆分为训练集和测试集,并使用数据字典添加我们在训练过程中需要的所有东西,即数据集、标记器和标签编码字典。

构建模型

Python使用Tensorflow 2和Keras实现文本分类:现在我们知道如何加载数据集,让我们构建我们的模型。

我们将使用嵌入层作为模型的第一层。嵌入被证明在将分类变量(在这种情况下是单词)映射到连续数字向量方面很有用,它被广泛用于自然语言处理任务。

更准确地说,我们将使用预训练的GloVe词向量,这些词向量是预训练的向量,可将每个词映射到特定大小的向量。这个尺寸参数通常被称为嵌入尺寸,虽然使用手套50,100,200,或300嵌入尺寸的载体。在本教程中,我们将尝试所有这些,看看哪个效果最好。此外,具有相同含义的两个词往往具有非常接近的向量。

第二层将是循环层,你可以选择任何你想要的循环单元,包括LSTMGRU,甚至只是SimpleRNN,我们将再次看到哪个优于其他单元。

最后一层应该是一个有N 个神经元的密集层,N应该是你的数据集的相同数量的类别。在正面/负面情绪分析的情况下,它应该是2。

该模型的总体架构如下图所示(摘自垃圾邮件分类器教程):

Python如何使用Tensorflow 2和Keras实现文本分类?

Python文本分类示例:现在你需要下载预训练的GloVe在此处下载),完成后,将它们全部提取到数据文件夹中(你会发现不同嵌入大小的不同向量),以下函数加载这些向量:

def get_embedding_vectors(word_index, embedding_size=100):
    embedding_matrix = np.zeros((len(word_index) + 1, embedding_size))
    with open(f"data/glove.6B.{embedding_size}d.txt", encoding="utf8") as f:
        for line in tqdm(f, "Reading GloVe"):
            values = line.split()
            # get the word as the first word in the line
            word = values[0]
            if word in word_index:
                idx = word_index[word]
                # get the vectors as the remaining values in the line
                embedding_matrix[idx] = np.array(values[1:], dtype="float32")
    return embedding_matrix

现在我们需要一个从头开始创建模型的函数,给定超参数

def create_model(word_index, units=128, n_layers=1, cell=LSTM, bidirectional=False,
                embedding_size=100, sequence_length=100, dropout=0.3, 
                loss="categorical_crossentropy", optimizer="adam", 
                output_length=2):
    """Constructs a RNN model given its parameters"""
    embedding_matrix = get_embedding_vectors(word_index, embedding_size)
    model = Sequential()
    # add the embedding layer
    model.add(Embedding(len(word_index) + 1,
              embedding_size,
              weights=[embedding_matrix],
              trainable=False,
              input_length=sequence_length))
    for i in range(n_layers):
        if i == n_layers - 1:
            # last layer
            if bidirectional:
                model.add(Bidirectional(cell(units, return_sequences=False)))
            else:
                model.add(cell(units, return_sequences=False))
        else:
            # first layer or hidden layers
            if bidirectional:
                model.add(Bidirectional(cell(units, return_sequences=True)))
            else:
                model.add(cell(units, return_sequences=True))
        model.add(Dropout(dropout))
    model.add(Dense(output_length, activation="softmax"))
    # compile the model
    model.compile(optimizer=optimizer, loss=loss, metrics=["accuracy"])
    return model

Python如何实现文本分类?我知道,这个函数中有很多参数。好吧,为了测试各种参数,这个功能会灵活地对所有提供的参数。让我们解释一下:

  • word_index:这是一个将每个单词映射到其对应索引号的字典,这是由前面提到的Tokenizer对象生成的。
  • units:这是每个循环层中神经元的数量,默认为128,但可以使用任何你想要的数字,请注意,单位越多,要调整的权重越多,因此在训练中会越慢过程。
  • n_layers:这是我们想要使用的循环层数,1是一个很好的开始。
  • cell: 你要使用的recurrent cell,LSTM是不错的选择。
  • bidirectional:这是一个布尔变量,指示我们是否使用双向循环层。 
  • embedding_size:我们前面提到的嵌入向量的大小,我们将尝试各种大小。
  • sequence_length:输入神经网络的每个文本样本上的标记词数,我们也将使用此参数进行试验。
  • dropout:它是在层上训练给定节点的概率,它对于减少过拟合很有用。40%非常适合这个,但尝试调整它,看看它是否表现更好。
  • loss:这是用于训练的损失函数,默认情况下,我们使用分类交叉熵函数。
  • optimizer: 要使用的优化器函数,我们在这里使用ADAM
  • output_length:这是最后一层使用的神经元数量,因为我们只使用正面和负面情绪分类,它必须是 2。

当你仔细观察时,你会注意到我使用了带参数的Embedding类weights,它指定了我们刚刚下载的预训练权重,我们还设置trainable为False,因此这些向量在此期间根本不会改变训练过程。

如果你的数据集使用的语言与英语不同,请确保为你使用的语言找到嵌入向量,如果没有,则根本不应设置 weights 参数,并且需要设置trainable为True,以便进行训练从头开始矢量的参数,查看此页面以获取你语言的词向量。

训练模型

Python使用Tensorflow 2和Keras实现文本分类 - 现在开始训练,我们需要定义所有前面提到的超参数和更多:

# max number of words in each sentence
SEQUENCE_LENGTH = 300
# N-Dimensional GloVe embedding vectors
EMBEDDING_SIZE = 300
# number of words to use, discarding the rest
N_WORDS = 10000
# out of vocabulary token
OOV_TOKEN = None
# 30% testing set, 70% training set
TEST_SIZE = 0.3
# number of CELL layers
N_LAYERS = 1
# the RNN cell to use, LSTM in this case
RNN_CELL = LSTM
# whether it's a bidirectional RNN
IS_BIDIRECTIONAL = False
# number of units (RNN_CELL ,nodes) in each layer
UNITS = 128
# dropout rate
DROPOUT = 0.4
### Training parameters
LOSS = "categorical_crossentropy"
OPTIMIZER = "adam"
BATCH_SIZE = 64
EPOCHS = 6

def get_model_name(dataset_name):
    # construct the unique model name
    model_name = f"{dataset_name}-{RNN_CELL.__name__}-seq-{SEQUENCE_LENGTH}-em-{EMBEDDING_SIZE}-w-{N_WORDS}-layers-{N_LAYERS}-units-{UNITS}-opt-{OPTIMIZER}-BS-{BATCH_SIZE}-d-{DROPOUT}"
    if IS_BIDIRECTIONAL:
        # add 'bid' str if bidirectional
        model_name = "bid-" + model_name
    if OOV_TOKEN:
        # add 'oov' str if OOV token is specified
        model_name += "-oov"
    return model_name

到目前为止,我已经设置了最佳参数,我发现get_model_name()函数正在根据参数生成唯一的模型名称,这在比较TensorBoard上的各种参数时非常有用。

让我们把所有东西放在一起,开始训练我们的模型:

# create these folders if they does not exist
if not os.path.isdir("results"):
    os.mkdir("results")
if not os.path.isdir("logs"):
    os.mkdir("logs")
if not os.path.isdir("data"):
    os.mkdir("data")
# dataset name, IMDB movie reviews dataset
dataset_name = "imdb"
# get the unique model name based on hyper parameters on parameters.py
model_name = get_model_name(dataset_name)
# load the data
data = load_imdb_data(N_WORDS, SEQUENCE_LENGTH, TEST_SIZE, oov_token=OOV_TOKEN)
# construct the model
model = create_model(data["tokenizer"].word_index, units=UNITS, n_layers=N_LAYERS, 
                    cell=RNN_CELL, bidirectional=IS_BIDIRECTIONAL, embedding_size=EMBEDDING_SIZE, 
                    sequence_length=SEQUENCE_LENGTH, dropout=DROPOUT, 
                    loss=LOSS, optimizer=OPTIMIZER, output_length=data["y_train"][0].shape[0])
model.summary()
# using tensorboard on 'logs' folder
tensorboard = TensorBoard(log_dir=os.path.join("logs", model_name))
# start training
history = model.fit(data["X_train"], data["y_train"],
                    batch_size=BATCH_SIZE,
                    epochs=EPOCHS,
                    validation_data=(data["X_test"], data["y_test"]),
                    callbacks=[tensorboard],
                    verbose=1)
# save the resulting model into 'results' folder
model.save(os.path.join("results", model_name) + ".h5")

这将需要几分钟的时间来训练,这是训练完成后我的执行输出:

Reading GloVe: 400000it [00:17, 23047.55it/s]
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, 300, 300)          37267200  
_________________________________________________________________
lstm (LSTM)                  (None, 128)               219648    
_________________________________________________________________
dropout (Dropout)            (None, 128)               0         
_________________________________________________________________
dense (Dense)                (None, 2)                 258       
=================================================================
Total params: 37,487,106
Trainable params: 219,906
Non-trainable params: 37,267,200
_________________________________________________________________
Train on 35000 samples, validate on 15000 samples
Epoch 1/6
35000/35000 [==============================] - 186s 5ms/sample - loss: 0.4359 - accuracy: 0.7919 - val_loss: 0.2912 - val_accuracy: 0.8788
Epoch 2/6
35000/35000 [==============================] - 179s 5ms/sample - loss: 0.2857 - accuracy: 0.8820 - val_loss: 0.2608 - val_accuracy: 0.8919
Epoch 3/6
35000/35000 [==============================] - 175s 5ms/sample - loss: 0.2501 - accuracy: 0.8985 - val_loss: 0.2472 - val_accuracy: 0.8977
Epoch 4/6
35000/35000 [==============================] - 174s 5ms/sample - loss: 0.2184 - accuracy: 0.9129 - val_loss: 0.2525 - val_accuracy: 0.8997
Epoch 5/6
35000/35000 [==============================] - 185s 5ms/sample - loss: 0.1918 - accuracy: 0.9246 - val_loss: 0.2576 - val_accuracy: 0.9035
Epoch 6/6
35000/35000 [==============================] - 188s 5ms/sample - loss: 0.1598 - accuracy: 0.9391 - val_loss: 0.2494 - val_accuracy: 0.9004

复制太棒了,经过6 个epoch 的训练,它达到了大约90% 的准确率。

测试模型

使用模型非常简单,下面的函数使用model.predict()方法来生成输出:

def get_predictions(text):
    sequence = data["tokenizer"].texts_to_sequences([text])
    # pad the sequences
    sequence = pad_sequences(sequence, maxlen=SEQUENCE_LENGTH)
    # get the prediction
    prediction = model.predict(sequence)[0]
    return prediction, data["int2label"][np.argmax(prediction)]

如你所见,为了正确生成预测,我们需要使用之前使用的标记器将文本转换为序列,然后填充序列使其成为固定长度的序列,然后我们使用模型生成输出。 predict()方法,让我们试试这个模型:

text = "The movie is awesome!"
output_vector, prediction = get_predictions(text)
print("Output vector:", output_vector)
print("Prediction:", prediction)

输出:

Output vector: [0.3001343  0.69986564]
Prediction: positive

让我们使用另一个文本:

text = "The movie is bad."
output_vector, prediction = get_predictions(text)
print("Output vector:", output_vector)
print("Prediction:", prediction)

复制输出:

Output vector: [0.92491007 0.07508987]
Prediction: negative

可以肯定的是,这是一种负面情绪,置信度约为92%。让我们更具挑战性:

text = "Not very good, but pretty good try."
output_vector, prediction = get_predictions(text)
print("Output vector:", output_vector)
print("Prediction:", prediction)

输出:

Output vector: [0.38528103 0.61471903]
Prediction: positive

这是相当61%肯定这是一个很好的情绪,你可以看到,它给有趣的结果,花一些时间欺骗的典范!

超参数调优

Python文本分类示例:在我想出90% 的准确率之前,我已经尝试了各种超参数,以下是一些有趣的参数:

Python如何使用Tensorflow 2和Keras实现文本分类?

这是 4 个模型,每个模型都有不同的嵌入大小,如你所见,具有 300 长度向量的模型(每个单词有 300 长度向量)达到了最低的验证损失值。

当我使用序列长度作为可变参数时,这是另一个:

Python如何使用Tensorflow 2和Keras实现文本分类?

序列长度为300的模型(绿色的)往往表现更好。

使用 tensorboard,你可以看到在到达 epochs 4-5-6 后,验证损失将尝试再次增加,这显然是过度拟合。这就是我将 epochs 设置为6的原因。尝试调整其他参数,例如辍学率,看看是否可以进一步降低。

集成自定义数据集

Python如何实现文本分类?由于这是一个文本分类教程,如果你可以使用自己的数据集而无需更改本教程的大部分代码,这将非常有用。事实上,你需要改变的只是加载数据函数,之前我们使用了load_imdb_data()函数,它返回一个数据字典,它具有:

  • X_train:一个 NumPy 数组,其形状(训练样本数、序列长度)包含每个数据样本的所有序列。
  • X_test: 同上,但用于测试样品。
  • y_train:这些是训练集的标签,它是一个形状的 NumPy 数组(测试样本数,总类别数),在情感分析的情况下,这应该类似于 ( 15000 , 2 )
  • y_test: 同上,但用于测试样品。
  • tokenizer:这是来自tensorflow.keras.preprocessing.text模块的 Tokenizer 实例,用于标记语料库的对象。
  • label2int:将标签转换为其对应编码整数的 Python 字典,在情感分析示例中,我们使用 1 表示正面,0 表示负面。
  • int2label: 反之亦然。

Python使用Tensorflow 2和Keras实现文本分类 - 这是一个加载20 个新闻组数据集(包含20 个主题的大约18000 个新闻组帖子)的示例函数,它使用 sklearn 的内置函数fetch_20newsgroups():

from sklearn.datasets import fetch_20newsgroups

def load_20_newsgroup_data(num_words, sequence_length, test_size=0.25, oov_token=None):
    # load the 20 news groups dataset
    # shuffling the data & removing each document's header, signature blocks and quotation blocks
    dataset = fetch_20newsgroups(subset="all", shuffle=True, remove=("headers", "footers", "quotes"))
    documents = dataset.data
    labels = dataset.target
    tokenizer = Tokenizer(num_words=num_words, oov_token=oov_token)
    tokenizer.fit_on_texts(documents)
    X = tokenizer.texts_to_sequences(documents)
    X, y = np.array(X), np.array(labels)
    # pad sequences with 0's
    X = pad_sequences(X, maxlen=sequence_length)
    # convert labels to one-hot encoded
    y = to_categorical(y)
    # split data to training and testing sets
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=1)
    data = {}
    data["X_train"] = X_train
    data["X_test"]= X_test
    data["y_train"] = y_train
    data["y_test"] = y_test
    data["tokenizer"] = tokenizer
    data["int2label"] = { i: label for i, label in enumerate(dataset.target_names) }
    data["label2int"] = { label: i for i, label in enumerate(dataset.target_names) }
    return data

好的,祝你在实现自己的文本分类器时好运,如果你在集成分类器时遇到任何问题,请在下方发表你的评论,我会尽快与你取得联系。

正如我之前提到的,尝试使用提供的所有超参数进行试验,我尝试编写尽可能灵活的代码,以便你可以只更改参数而无需执行任何其他操作。如果你的参数超过我的参数,请在下面的评论中与我们分享!

木子山

发表评论

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