在本文中,我将对分类任务应用两种不同的方法。我将首先应用一个经典的机器学习分类算法-梯度增强分类器。
在代码的后面,我将使用LSTM技术来训练RNN模型。因为我们正在处理tweets,所以这是一个NLP任务,我将与大家分享一些技巧,以便大家更加熟悉大多数NLP项目中的一些常见步骤。
我将使用Kaggle挑战赛的数据,名为“自然语言处理-灾难推文”。你可以在“data”部分的链接下面找到“train.csv文件
https://www.kaggle.com/c/nlp-getting-started/overview
数据集有5列。列“target”是标签列,这意味着我将训练一个模型,该模型可以使用其他列(如“text”、“location”和“keyword”)预测列“target”的值。现在我们先来了解一下每一列的含义:
对于这个任务,我将使用Sklearn和Keras等库来训练分类器模型。Sklearn用于使用梯度增强分类器训练模型,Keras用于训练LSTM模型。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import re
import nltk
nltk.download('stopwords')
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
from nltk.stem import SnowballStemmer
from sklearn import model_selection, metrics, preprocessing, ensemble, model_selection, metrics
from sklearn.feature_extraction.text import CountVectorizer
import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.layers import Conv1D, Bidirectional, LSTM, Dense, Dropout, Input
from tensorflow.keras.optimizers import Adam
对于这个任务,我们只使用'train.csv“并将其分解为训练和测试数据集。我将把数据加载到Pandas Dataframe并查看前几行。
# 读取训练数据集
file_path = "./train.csv"
raw_data = pd.read_csv(file_path)
print("Data points count: ", raw_data['id'].count())
raw_data.head()
首先,我想更加熟悉数据集,以便理解这些特征(列)。“目标”列是我们的模型要学习预测的列。因为它只有0和1这两个唯一的值,所以这是一个二分类任务。
我想知道token为0和1的tweet的分布,所以让我们基于列“target”绘制数据。
如你所见,标签0表示非灾难tweets的数据点较多,标签1表示与灾难相关tweets的数据点较少。通常,对于有一些倾斜标签的数据,建议使用F1分数而不是准确率来进行模型评估,我们将在本文末尾讨论这个问题。
接下来,我想知道我们的数据集中每一列缺失的数据点是怎样的。下面的热图显示“keyword”这一列缺少的数据点很少,我将填补这些缺失的数据点,并将这一列作为一个特征使用。
列“location”数据非常缺失,数据质量非常差。所以我决定不使用这个列。列“text”,这是tweet的实际文本,它没有丢失数据。
我也注意到有一些tweet包含的单词不到3个,我认为两个单词的句子可能无法很好地传递内容。为了弄清楚句子的字数分布,我可视化每个句子的字数直方图。
正如我们所看到的,大多数tweet都在11到19个单词之间,所以我决定删除少于2个单词的tweet。我相信用三个字的句子就足以说明这条微博了。删除超过25-30个单词的tweet可能是个好主意,因为它们可能会减慢训练时间。
在处理tweet的NLP任务中,清除数据的常见步骤是删除特殊字符、删除停用词、删除url、删除数字和进行词干分析。但我们先来熟悉一些NLP数据预处理的概念:
单词向量化是一种将单词映射到实数的技术,或者更好地说是实数向量。我使用了Sklearn和Keras库的向量化。
token化是将一个短语(可以是句子、段落或文本)分解成更小的部分,如一系列单词、一系列字符或一系列子单词,它们被称为token。token化的一个用途是从文本生成token,然后将token转换为数字(向量化)。
神经网络模型要求输入具有相同的形状和大小,这意味着一个接一个地输入到模型中的所有tweet必须具有完全相同的长度,所以我们要用上填充(padding)。
数据集中的每条tweet都有不同的字数,我们将为每条tweet设置一个最大字数,如果一条tweet较长,那么我们可以删除一些字数,如果tweet的字数少于max,我们可以用固定值(如“0”)填充tweet的开头或结尾。
词干分析的任务是将多余的字符从一个词减少到词干形式。例如,将“working”和“worked”这两个词词干化为“work”。
我使用了Snowball词干分析器,这是一种词干算法(也称为Porter2词干算法)。它是波特词干分析器的一个更好的版本,因为一些问题在这个词干分析器中得到了解决。
词嵌入是对文本的一种学习表示,其中具有相同含义的单词具有相似的表示。每个单词被映射到一个向量,向量值以类似于神经网络的方式学习。
现在让我们看看整个数据清理代码:
def clean_text(each_text):
# 从文本中删除URL
each_text_no_url = re.sub(r"http\S+", "", each_text)
# 从文本中删除数字
text_no_num = re.sub(r'\d+', '', each_text_no_url)
# token化每个文本
word_tokens = word_tokenize(text_no_num)
# 删除特殊字符
clean_text = []
for word in word_tokens:
clean_text.append("".join([e for e in word if e.isalnum()]))
# 删除停用词并小写化
text_with_no_stop_word = [w.lower() for w in clean_text if not w in stop_words]
# 词干化
stemmed_text = [stemmer.stem(w) for w in text_with_no_stop_word]
return " ".join(" ".join(stemmed_text).split())
raw_data['clean_text'] = raw_data['text'].apply(lambda x: clean_text(x) )
raw_data['keyword'] = raw_data['keyword'].fillna("none")
raw_data['clean_keyword'] = raw_data['keyword'].apply(lambda x: clean_text(x) )
为了能够同时使用“text”和“keyword”列,有多种方法可以应用,但我应用的一种简单方法是将这两种特征结合到一个新特征中,称为“keyword_text”
# #将“clean_keyword”列和“clean_text”列合并为一个列
raw_data['keyword_text'] = raw_data['clean_keyword'] + " " + raw_data["clean_text"]
我使用了Sklearn的“train_test_split”函数来执行训练和测试集的划分。
feature = "keyword_text"
label = "target"
# 分割训练测试
X_train, X_test,y_train, y_test = model_selection.train_test_split(raw_data[feature],raw_data[label],test_size=0.3,random_state=0,shuffle=True)
正如我已经提到的向量化,我们必须将文本转换成数字,因为机器学习模型只能处理数字,所以我们在这里使用“Countervectorize”。我们对训练数据进行拟合和变换,只对测试数据进行变换。确保测试数据没有拟合。
# 向量化文本
vectorizer = CountVectorizer()
X_train_GBC = vectorizer.fit_transform(X_train_GBC)
x_test_GBC = vectorizer.transform(x_test_GBC)
梯度Boosting分类器是一种机器学习算法,它将决策树等弱学习模型结合起来,形成一个强预测模型。
model = ensemble.GradientBoostingClassifier(learning_rate=0.1,
n_estimators=2000,
max_depth=9,
min_samples_split=6,
min_samples_leaf=2,
max_features=8,
subsample=0.9)
model.fit(X_train_GBC, y_train)
评价模型性能的一个很好的指标是F-score。在计算F分数之前,让我们先熟悉精确度和召回率。
精度:在我们正确标记为阳性的数据点中,有多少点我们正确标记为阳性。
召回率:在我们正确标记为阳性的数据点中,有多少是阳性的。
F1分数:是召回率和精确度的调和平均值。
# 评估模型
predicted_prob = model.predict_proba(x_test_GBC)[:,1]
predicted = model.predict(x_test_GBC)
accuracy = metrics.accuracy_score(predicted, y_test)
print("Test accuracy: ", accuracy)
print(metrics.classification_report(y_test, predicted, target_names=["0", "1"]))
print("Test F-scoare: ", metrics.f1_score(y_test, predicted))
Test accuracy: 0.7986784140969163
precision recall f1-score support
0 0.79 0.88 0.83 1309
1 0.81 0.69 0.74 961
accuracy 0.80 2270
macro avg 0.80 0.78 0.79 2270
weighted avg 0.80 0.80 0.80 2270
Test F-scoare: 0.7439775910364146
混淆矩阵是一个表,它显示了分类模型相对于两个类的性能。从图中可以看出,我们的模型在检测目标值“0”时比检测目标值“1”时有更好的性能。
LSTM(Long-Short-Term Memory network)是一种递归神经网络(RNN,Recurrent Neural network),具有学习长期依赖性和记忆信息的能力。
我已经在上面谈到了词嵌入,现在是时候将其用于我们的LSTM方法了。我使用了斯坦福大学的GloVe嵌入技术。读取GloVe嵌入文件之后,我们使用Keras创建一个嵌入层。
# 读取词嵌入
embeddings_index = {}
with open(path_to_glove_file) as f:
for line in f:
word, coefs = line.split(maxsplit=1)
coefs = np.fromstring(coefs, "f", sep=" ")
embeddings_index[word] = coefs
print("Found %s word vectors." % len(embeddings_index))
# 在Keras中定义嵌入层
embedding_matrix = np.zeros((vocab_size, embedding_dim))
for word, i in word_index.items():
embedding_vector = embeddings_index.get(word)
if embedding_vector is not None:
embedding_matrix[i] = embedding_vector
embedding_layer = tf.keras.layers.Embedding(vocab_size,embedding_dim,weights[embedding_matrix],input_length=sequence_len,trainable=False)
对于LSTM模型,我从一个嵌入层开始,为每个输入序列生成一个嵌入向量。然后我使用卷积模型来减少特征的数量,然后是一个双向LSTM层。最后一层是Dense层。因为它是一个二分类,所以我们使用sigmoid作为激活函数。
# 定义模型体系结构
sequence_input = Input(shape=(sequence_len, ), dtype='int32')
embedding_sequences = embedding_layer(sequence_input)
x = Conv1D(128, 5, activation='relu')(embedding_sequences)
x = Bidirectional(LSTM(128, dropout=0.5, recurrent_dropout=0.2))(x)
x = Dense(512, activation='relu')(x)
x = Dropout(0.5)(x)
x = Dense(512, activation='relu')(x)
outputs = Dense(1, activation='sigmoid')(x)
model = Model(sequence_input, outputs)
model.summary()
对于模型优化,我使用了以二元交叉熵作为损失函数的Adam优化。
# 优化模型
model.compile(optimizer=Adam(learning_rate=learning_rate), loss='binary_crossentropy', metrics=['accuracy'])
模型训练完成后,我想看看训练精度和损失的学习曲线。该图显示,模型精度的不断提高和损失的不断减少
现在我已经训练了模型,所以现在是时候评估它的模型性能了。我将得到模型的准确率和测试数据的F1分数。因为预测值是介于0和1之间的浮点值,所以我使用0.5作为阈值来分隔“0”和“1”。
# 评估模型
predicted = model.predict(X_test, verbose=1, batch_size=10000)
y_predicted = [1 if each > 0.5 else 0 for each in predicted]
score, test_accuracy = model.evaluate(X_test, y_test, batch_size=10000)
print("Test Accuracy: ", test_accuracy)
print(metrics.classification_report(list(y_test), y_predicted))
Test Accuracy: 0.7726872
precision recall f1-score support
0 0.78 0.84 0.81 1309
1 0.76 0.68 0.72 961
accuracy 0.77 2270
macro avg 0.77 0.76 0.76 2270
weighted avg 0.77 0.77 0.77 2270
正如我们在混淆矩阵中看到的那样,RNN方法的性能与梯度增强分类器方法非常相似。该模型在检测“0”方面比检测“1”做得更好。
如你所见,两种方法的输出非常接近。梯度增强分类器的训练速度比LSTM模型快得多。
有许多方法可以提高模型的性能,如修改输入数据,应用不同的训练方法,或使用超参数搜索算法,如GridSearch或RandomizedSearch来寻找超参数的最佳值。
参考文献:
https://keras.io/examples/nlp/pretrained_word_embeddings/