信息自由请求的文本分类:第一部分
使用机器学习来预测市政当局的决策。
根据《信息自由和隐私保护法》(FIPPA),加拿大安大略省有义务对信息请求做出回应。更具体地说,《市政信息自由和隐私保护法》允许任何年龄、任何地点的人发出这样的请求。与个人相关的信息可能包括印刷品、胶片、电子邮件、图纸、照片等。只要发布的内容不侵犯另一个人的隐私权。在本帖中,我们将尝试预测一个市政当局做出的决定,即安大略省基奇纳-滑铁卢(第一部分)和后来的多伦多(第二部分)。
所使用的数据在每个城市的开放数据门户下提供。任何识别信息都已被删除。
使用有限的 Kitchener-Waterloo 数据集将允许我们基线化一些度量标准和原型化我们的技术。这些数据通过 API 提供,涵盖 1992 年至 2016 年。
一旦所有数据都被汇总并转换成熊猫数据框架,我们需要更仔细地看看我们有什么。可能的“决定”如下:
全部公开 238
部分免除 197
撤回 116
无记录存在 54
部分公开的信息 50
部分不存在 32
未公开 28
无记录存在 21
全部公开 16
转发出去 15
放弃 13
全部公开 13
无响应记录存在 11
更正被拒绝 3
不存在 3
已披露 2
已转移 1
未披露信息 1
已提交不同意声明 1
不存在额外记录 1
批准更正 1
请求撤销 1
在可能的决定中,显然有一些重叠,共有 24 项。虽然可以(并且已经)在不太长的时间内手动合并这些类,但是如果有数百个这样的类,只是在空格的存在(或不存在)上有所不同呢?还是一个 s?还是以资本化的方式?SeatGeek 开发的 python 包 fuzzywuzzy 非常适合这种类型的模糊字符串匹配。
该软件包提供了四种方法来探测两个字符串的相似性。本质上,“fuzz_ratio”比较整个字符串,“fuzz_partial_ratio”在字符串中搜索部分匹配,“fuzz_token_sort_ratio”比较无序的字符串,“fuzz_token_set_ratio”将标记排序扩展到更长的字符串。每一个都返回一个相似率(越高越相似),之后可以在可接受的截止点设置一个阈值。让我们测试一下每一个的性能。
from fuzzywuzzy import fuzzall_df_clean = all_df.copy()for row in all_df['Decision'].unique():
for row2 in all_df['Decision'].unique():
matching_result = fuzz.ratio(row, row2) # could be ratio, partial_ratio, token_set_ratio, token_sort_ratio
if matching_result > 80:
#print(row, row2)
#print(matching_results)
#Combine if match found
all_df_clean['Decision'][all_df_clean['Decision'] == row2] = row
print(all_df_clean['Decision'].value_counts())
print(len(all_df_clean['Decision'].unique()))
全部披露 240
部分豁免 197
撤回 116
无记录存在 75
部分披露的信息 50
部分不存在 32
无信息披露 30
无披露 28
转发出去 15
放弃 13
无响应记录存在 11
不存在 3
改正被拒绝 3
改正作出 2
移交滑铁卢地区公共卫生 2
已转移 1
更正批准 1
请求撤回 1
名称:决定,dtype:int 64
20
还不错。我们合并了四个类别(即“不存在记录”和“不存在记录”),但“未披露信息”和“未披露任何内容”的意思显然是一样的。80%的截止值是通过反复试验确定的。让我们试试部分比率:
全部披露 240
部分豁免 197
请求撤回 117
无响应记录存在 87
无信息披露 80
不存在 35
无披露 28
转发出去 15
放弃 13
更正拒绝 3
移交 3
更正作出 2
异议陈述归档 1
更正批准 1
名称:决定、dtt
另外六个类合并了,但是算法并不真正知道如何处理子串‘discovered’。它将“部分披露的信息”和“未披露的信息”结合在一起,这两个词的意思不同。
全部披露 240
部分豁免 197
撤回 116
无信息披露 80
无记录存在 75
部分不存在 32
无披露 28
转发出去 15
放弃 13
无响应记录存在 11
拒绝改正 3
不存在 3
改正作出 2
转移到滑铁卢公共卫生区 2【T55
类似于基本比率情况。
未披露信息 348
部分免除 197
要求撤回 117
无响应记录存在 87
不存在 35
转发出去 15
放弃 13
更正拒绝 3
移交 3
更正作出 2
异议声明存档 1
更正批准 1
名称:决定,dtype: int64
12
迄今为止最积极的匹配案例,但我们再次被“披露”绊倒,因为现在我们不再有“全部披露”的案例,这是不可接受的。部分比率的情况似乎是一种折衷——也许我们可以进一步手动合并几个类别。我们将会看到当多伦多的数据被加入第二部分时会发生什么。
all_df_pr['Decision'].loc[all_df_pr['Decision'] == "Abandoned"] = "Request withdrawn"
all_df_pr['Decision'].loc[all_df_pr['Decision'] == "Non-existent"] = "No responsive records exist"
all_df_pr['Decision'].loc[all_df_pr['Decision'] == "No information disclosed "] = "Nothing disclosed"
all_df_pr['Decision'].loc[all_df_pr['Decision'] == "Transferred"] = "Forwarded out"
all_df_pr['Decision'].loc[all_df_pr['Decision'] == "Correction granted"] = "Correction made"
print(all_df_pr['Decision'].value_counts())
全部公开 240
部分免除 197
请求撤回 130
无响应记录存在 122
无公开 108
转发 18
更正已做出 3
更正被拒绝 3
异议声明已归档 1
名称:决定,dtype: int64
现在一切都变得清晰了。让我们砍掉人数下降幅度大的班级:
all_df_over20 = all_df_pr.groupby('Decision').filter(lambda x: len(x) > 20)
本文的剩余部分将大量借用 Susan Li 的关于多类文本分类的优秀文章。我们的预期是,基于我们的数据集较小,以及文本本身相当不透明的事实,我们的结果不会很好。我们数据的快速概述:
这些阶层并不像我们想象的那样不平衡。“category_id”列是可能的决策案例的整数表示。现在,为每个请求计算一个 tf-idf 向量,去掉所有停用词。
from sklearn.feature_extraction.text import TfidfVectorizertfidf = TfidfVectorizer(sublinear_tf=True, min_df=5, norm='l2',
encoding='latin-1', ngram_range=(1,2), stop_words='english')features = tfidf.fit_transform(all_df_over20.Request_summary).toarray()
labels = all_df_over20.category_id
features.shape
打印每个类的常见二元模型和二元模型。
from sklearn.feature_selection import chi2
import numpy as npN=2
for Decision, category_id in sorted(category_to_id.items()):
features_chi2 = chi2(features, labels == category_id)
indices = np.argsort(features_chi2[0])
feature_names = np.array(tfidf.get_feature_names())[indices]
unigrams = [v for v in feature_names if len(v.split(' ')) ==1]
bigrams = [v for v in feature_names if len(v.split(' ')) ==2]
print("# '{}':".format(Decision))
print(" . Most correlated unigrams:\n. {}".format('\n.'.join(unigrams[-N:])))
print(" . Most correlated bigrams:\n. {}".format('\n.'.join(bigrams[-N:])))
#‘全部公开’:
。最相关的单字:
。协助
。个人
。最相关的二元模型:
。信息已删除
。移除竞争
#‘不存在响应记录’:
。最相关的单字:
。地点
。阶段
。最相关的二元模型:
。评估地址
。现场评估
#【未披露内容】:
。最相关的单字:
。总线
.2014
。最相关的二元模型:
。完成安大略省
。工程文件
#‘部分免除’:
。最相关的单字:
。比赛
.94
。最相关的二元模型:
。信息竞赛
。狂犬病控制
#‘请求撤回’:
。最相关的单字:
。省级
。评测
。最相关的二元模型:
。处理厂
。省级违法行为
这就是我说的请求本身在通过视觉进行分类方面相当不透明的意思。至少在阶级之间有很好的区分。
尝试四种不同的型号:
import warnings
warnings.filterwarnings(action='once')
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import LinearSVCfrom sklearn.model_selection import cross_val_scoremodels = [
RandomForestClassifier(n_estimators=200, max_depth=3,
random_state=0), #, class_weight='balanced'),
LinearSVC(), #class_weight='balanced'),
MultinomialNB(),
LogisticRegression(random_state=0)#, class_weight='balanced'),
]
CV=5
cv_df = pd.DataFrame(index=range(CV * len(models)))
entries=[]
for model in models:
model_name = model.__class__.__name__
accuracies = cross_val_score(model, features, labels,
scoring='accuracy', cv=CV)
for fold_idx, accuracy in enumerate(accuracies):
entries.append((model_name, fold_idx, accuracy))
cv_df = pd.DataFrame(entries, columns=['model_name', 'fold_idx', 'accuracy'])import seaborn as snssns.boxplot(x='model_name', y='accuracy', data=cv_df)
sns.stripplot(x='model_name', y='accuracy', data=cv_df,
size=8, jitter=True, edgecolor="gray", linewidth=2)
plt.show()
cv_df.groupby('model_name').accuracy.mean()
model _ Name
linear SVC 0.362882
logistic regression 0.398066
MultinomialNB 0.398152
RandomForestClassifier 0.349038
Name:accuracy,dtype: float64
不出所料,性能非常差。我们来看看混淆矩阵。
precision recall f1-score support
All disclosed 0.36 0.69 0.47 67
No responsive records exist 0.50 0.49 0.49 41
Partly exempted 0.58 0.48 0.53 73
Request withdrawn 0.38 0.11 0.17 47
Nothing disclosed 0.58 0.39 0.47 36
avg / total 0.48 0.45 0.44 264
这个模型至少在尝试。在后续文章中,我们将尝试通过显著增加可用数据和在每个类上尝试一系列二元分类器来改进这一点。
GitHub 上提供的代码:https://github.com/scjones5/foi-kw
信息自由请求的文本分类:第二部分
在第一部分中,我们吸收了可用的 Kitchener-Waterloo 信息自由数据,并设法使用模糊字符串匹配来聚合预测类。从那里,我们能够尝试使用四种不同的分类算法进行预测:随机森林、线性支持向量分类、多项式朴素贝叶斯和逻辑回归。结果并不令人印象深刻。
在这里,我们将通过包含多伦多市发出的请求来增加我们的数据集。我们将进行更多的探索性数据分析,看看是否有任何方法可以帮助我们的模型,然后我们将尝试更智能地使用机器学习,在第三部分中,深度学习,以实现最佳结果。
在第一部分,我们的数据集有 832 个条目;用来自大城市的数据补充这一点,使我们的总数达到 11521 个条目,这开始类似于更适合机器学习问题的东西。千瓦数据仍然涵盖了更长的时间段,从 1993 年开始,直到 2016 年结束:
FOI requests over time in KW
多伦多 2011 年至 2018 年的数据没有显示出同样的增长趋势,
Requests by year in Toronto
但是,如果我们能够想出某种模式,将请求本身的案文与所作出的决定联系起来,就一定会节省一些时间。
请求本身的内容呢?为了快速理解人们正在使用的短语,我们可以检查两个数据集的单词云:
KW requests
Toronto requests
在每一个案例中,我都删除了一些被用来保护个人隐私的二元模型,但它们本身并没有传达任何有用的信息。这些包括“地址已删除”、“名称已删除”和“位置已删除”。留给我们的是更有趣的二元结构,如“安大略工程”、“建筑许可证”和“环境网站”。像这样将数据集分开,让我们注意到 KW 的请求更倾向于环境问题,而多伦多的请求看起来更一般。重要的是在得出太多的结论之前要小心,因为这是未知的,也可能是不太可能的,这是由个人/企业提交的文本。更有可能的是,这些数据是在某个时候从网上或打印出来的。
处理任何自由格式文本的一个重要部分是预处理。在这里,每个请求通常可以很好地包含在大约 50 个单词的句子中,事情可以很容易地标记化。将每个请求提供给下面的函数:
import spacy
from spacy.lang.en import English
from nltk.corpus import stopwords
from sklearn.feature_extraction.stop_words import ENGLISH_STOP_WORDS
from wordcloud import WordCloud, STOPWORDS
import string
parser = English()
STOPLIST = set(stopwords.words('english') + list(ENGLISH_STOP_WORDS)+ list(STOPWORDS))
SYMBOLS = " ".join(string.punctuation).split(" ") + ["-", "...", "”", "”"]def tokenizeText(sample):
tokens = parser(sample)
lemmas = []
for tok in tokens:
#lemmatizes, converts to lowercase, omits pronouns
lemmas.append(tok.lemma_.lower().strip() if tok.lemma_ != "-PRON-" else tok.lower_)
tokens = lemmas
#removes stop word tokens and symbols
tokens = [tok for tok in tokens if tok not in STOPLIST]
tokens = [tok for tok in tokens if tok not in SYMBOLS]
return tokens
我们可以先用 SpaCy 解析文本,然后找到每个单词的引理,转换成小写。使用 nltk,我们最终可以删除任何停用词或符号。nltk 还为词汇化提供了“Morphy”方法,这是一个很大程度上重复的进一步步骤,但对我来说,仍然抓住了一些 SpaCy 遗漏的复数:
import nltk
nltk.download('wordnet')
from nltk.corpus import wordnet as wn
def get_lemma(word):
#print("un-lemmad word: ", word)
lemma = wn.morphy(word)
#print("lemma-d word: ", lemma)
if lemma is None:
return word
else:
return lemma
调用这两个函数并删除任何长度小于 5 的标记:
def prepare_text_for_lda(text):
# tokenizes text using spacy
tokens = tokenizeText(text)
# removes tokens less that five characters
tokens = [token for token in tokens if len(token) > 4]
# here we are lemmatizing again too; noticed that previous lemmatization doesn't handle plurals
tokens = [get_lemma(token) for token in tokens]
return tokens
将所有这些放在一起并整合结果:
import random
text_data = []
for request in (all_df['Summary_of_Request']):
tokens = prepare_text_for_lda(request)
text_data.append(tokens)
例如,下面是数据帧中的第一个请求及其相关的令牌:
Notes written by members of the Maintenance Review Committee for Clerk III, Healthy Environments.
['note', 'write', 'member', 'maintenance', 'review', 'committee', 'clerk', 'healthy', 'environment']
EDA 中对文本数据最感兴趣的最后一点是做一些主题建模。GenSim 包可以主要通过潜在的狄利克雷分配来识别语义相似性(因此命名为上述函数),并从中挑选出所需数量的“主题”,显示与每个主题最相似的单词。基于一个预定义的字典(这里是前面的 text_data 列表),方法 doc2bow 使用单词包格式返回每个单词的索引和频率。然后将其传递给 LDA 模型,直到最终能够看到与每个主题相关联的单词及其相对重要性:
(0, '0.078*"record" + 0.063*"permit" + 0.057*"build" + 0.055*"inspection"')
------------------------------------------------------------------------------------------
(1, '0.124*"report" + 0.092*"incident" + 0.076*"occur" + 0.032*"address"')
------------------------------------------------------------------------------------------
(2, '0.167*"specify" + 0.084*"address" + 0.027*"toronto" + 0.020*"include"')
------------------------------------------------------------------------------------------
(3, '0.044*"water" + 0.042*"record" + 0.029*"toronto" + 0.021*"sewer"')
------------------------------------------------------------------------------------------
(4, '0.071*"record" + 0.034*"property" + 0.032*"complaint" + 0.021*"investigation"')
------------------------------------------------------------------------------------------
我们可以看到“记录”一词与多个不同的主题相关联,但总体而言,所使用的术语是相当通用的。
在第一部分中,我们主要讨论了使用 fuzzywuzzy 包来组合相似的表面决定。随着多伦多数据的加入,“partial_ratio”方法仍然是最适用的,并允许我们将决策范围缩小到 6 个类别,如下图所示,同时列出了每组的观察数量。
Disclosed in Part: Partially Exempt 6406
All Disclosed 2847
No Records Exist 1393
Nothing Disclosed 371
Request withdrawn 256
Transferred Out in Full 168
当我们进行预测时,我们应该记住,最常见的决策“部分披露:部分豁免”应该对我们的模型进行最严格的测试,因为这是一个介于“全部披露”和“没有披露”极端情况之间的情况。如果我们能准确预测这种情况,那么其他的应该会更容易发生。幸运的是,它拥有最多的数据。
下图更新后包括了多伦多的数据:
与第一部分相比,我们现在有更多的不平衡类问题。这可能需要更彻底地解决,特别是如果我们希望,如前所述,尝试二元“一个对其余的”分类器。这样不公平地存在正反两方面的情况是不行的。
打包的不平衡学习正是考虑到这些类型的用例而构建的。这有助于对较小的类进行过采样,或者对较大的类进行欠采样,以平衡观测值的分布。该软件包提供了几种不同的数据过采样方法。RandomOverSampler 例程通过替换对代表性不足的类进行采样,直到达到最密集类的频率。但是,这种方法容易导致大量的数据重复,可能导致模型过拟合。相反,合成少数过采样技术(SMOTE)进行插值,以生成新的样本。我们试试 SMOTE:
from imblearn.over_sampling import RandomOverSampler, SMOTEX_resampled, y_resampled = SMOTE().fit_resample(features, labels)from collections import Counterprint(sorted(Counter(y_resampled).items()))
[(0, 6406), (1, 6406), (2, 6406), (3, 6406), (4, 6406), (5, 6406)]
现在每个类都有相同数量的观察值。
作为我们预测能力的基线,我们可以简单地重复第一部分中使用的方法,这里使用额外的数据和重新采样的类。这样做已经大大提高了我们的准确性:
线性 SVC 0.858418
物流回归 0.806723
多项式 B 0.752838
随机森林分类器 0.500750
在本系列的剩余部分,第三部分,我们将通过最终引入 OneVsRest 二元分类器来深化我们的模型,并将其性能与完全成型的 GRU 递归神经网络进行比较。
信息自由请求的文本分类:第三部分
在本系列的第一部分中,我们针对 Kitchener-Waterloo 信息自由请求运行了一个不成熟的模型,试图预测一个响应。在第二部分中,我们通过包含多伦多市的请求扩展了我们的数据集,并通过探索性数据分析增加了一些领域知识。我们还处理了六个不同决策类别中观察值数量之间的不平等。
在第三部分中,我们将重复使用 LinearSVC、多项朴素贝叶斯、逻辑回归和随机森林中的每一个,但现在每次只针对一个类,而不是所有其他类。这是一种经常用于多标签问题的技术,在我们的例子中,每个请求都有多个决策,例如,由不同的参与方提交。需要的关键假设是,每个标签或类别(决策)是互斥的,并且独立于所有其他类别。虽然可以提出一个论点,即在这里不完全正确(“部分披露”可能最终变成“全部披露”,或者可能存在对相同请求做出不同决定的情况),但我们将继续理解已经做出的假设。
基本代码的结构如下:
from sklearn.metrics import accuracy_scoreNB_pipeline = Pipeline([
('tfidf', TfidfVectorizer(tokenizer=tokenizeText, stop_words='english')),
('clf', OneVsRestClassifier(MultinomialNB())),
])for decision in categories:
print(decision)
NB_pipeline.fit(X_train, y_train_dums[decision])
prediction = NB_pipeline.predict(X_test)
print('Test accuracy is {}'.format(accuracy_score(y_test_dums[decision], prediction)))Disclosed in Part: Partially Exempt Test accuracy is 0.8520182907600126
All Disclosed Test accuracy is 0.8698360138757489
No Responsive Records Exist Test accuracy is 0.8816619362976978 Nothing Disclosed (exemption) Test accuracy is 0.934799747713655 Request withdrawn Test accuracy is 0.9653106275622831
Transferred Out in Full Test accuracy is 0.9943235572374646
其中目标变量先前已经被编码为独热码向量。LinearSVC 的结果:
部分披露:部分豁免测试精度为 0.9100441501103753
全部披露测试精度为 0.9057079785556607
无响应记录存在测试精度为 0.9505676442762535
无披露(豁免)测试精度为 0.9874645222327342
要求撤回测试精度为 0.995
逻辑回归:
部分披露:部分豁免测试精度为 0.8661305581835383
全部披露测试精度为 0.8785083569851782
无响应记录存在测试精度为 0.8984547461368654
未披露(豁免)测试精度为 0.96661778618732261
请求撤回
随机森林:
部分披露:部分豁免测试精度为 0.826631977294229
全部披露测试精度为 0.8324660990223904
无响应记录存在测试精度为 0.8423210343740145
无披露(豁免)测试精度为 0.8342005676442763
要求撤回测试精度
LinearSVC 方法似乎仍然表现最好,最接近“部分公开”类。一般来说,趋势是增加具有较少(上采样前)数据的类的准确性。很可能,向上采样的数据有些人为,根据定义,必须非常类似于已经存在的数据。
还尝试以同样的方式使用 XGBoost,对“部分公开”的准确率为 83.4%,尽管计算成本高得多。
用递归神经网络进行深度学习
作为对这个数据集的最后努力,我们将采用递归神经网络(RNNs)形式的深度学习。rnn 保留序列中之前步骤的一些记忆,或者对于双向实现,在当前步骤之后的和之前。因此,它们在文档摘要和机器翻译等任务中非常有用,可以处理不同大小的输入和输出。
在我们的项目中,我们有许多输入令牌作为请求字符串的一部分,并且本质上,有一个决策类形式的输出。因此,该模型可以被认为是多对一分类器。
使用 Google 协作 GPU 功能,实现了以下模型:
input= Input(shape=(max_len, ), dtype = 'int32')
embedding_layer = Embedding(len(word_index) + 1, embedding_dim, embeddings_initializer=Constant(embedding_matrix), input_length=max_len, trainable=False)
embedded_sequences = embedding_layer(input)
x = Bidirectional(GRU(units=32, return_sequences=True))(embedded_sequences)
x = GlobalMaxPooling1D()(x)
x = Dense(50, activation = 'relu')(x)
x = Dropout(0.2)(x)
output = Dense(num_classes, activation='softmax')(x)
model = Model(inputs=input, outputs=output)
model.compile(loss="categorical_crossentropy", optimizer='adam', metrics=['accuracy'])metrics=['accuracy'])
print(model.summary())
我们使用了长度为 40 个单位的手套词嵌入,这是大多数请求遵循的最大长度:
embeddings_index = {}# Download this file first
f = open("/content/drive/My Drive/foi-kw/glove.6B.300d.txt", encoding="utf8")for line in f:
values = line.split()
word = ''.join(values[:-embedding_dim])
coefs = np.asarray(values[-embedding_dim:], dtype="float32")
embeddings_index[word] = coefs
f.close()tokenizer = Tokenizer(num_words = None)
tokenizer.fit_on_texts(X_train)sequences_train = tokenizer.texts_to_sequences(X_train)
X_train = pad_sequences(sequences_train, maxlen=max_len)sequences_val = tokenizer.texts_to_sequences(X_test)
X_test = pad_sequences(sequences_val, maxlen=max_len)word_index = tokenizer.word_index#create embedding layer
embedding_matrix = np.zeros((len(word_index) + 1, 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_vectorimport matplotlib.pyplot as pltcheckpoint = ModelCheckpoint("/content/drive/My Drive/foi-kw/colab/models/model.h5", monitor='val_loss', verbose=1, save_best_only=True, mode='min')
early = EarlyStopping(monitor='val_loss', mode='min', patience=3)
callback = [checkpoint, early]history = model.fit(X_train, y_train, batch_size=batch_size, epochs=epochs, validation_data=(X_test, y_test), callbacks=callback, class_weight=class_weights)
loss, accuracy = model.evaluate(X_train, y_train, verbose=0)
print('Accuracy: %f' % (accuracy*100))
print(history.history.keys())
# Plot history for accuracy
plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])
plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()
# Plot history for loss
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()
model.reset_states()
早期停止被用来努力避免过度拟合,就像“类权重”处理类不平衡一样。关于关键词及其含义的进一步描述,我会向读者推荐曼迪·顾的优秀文章。
如上编码的精度和损耗图得出:
Training and test accuracy over 12 epochs.
Training and test loss over 12 epochs.
奇怪的是,测试损失保持在一个相当恒定的值。在我们的模型中,我们在训练期间使用了压差正则化,这意味着网络没有满负荷运行,而在测试期间没有使用压差。另一种解释可能是,一个时期的测试损失是在该时期结束时计算的,而训练损失是每批训练数据的平均值。
结论
这一系列文章覆盖了很多领域,代表了作者第一次真正尝试为文本数据构建机器学习模型。多伦多数据集的增加使分析受益匪浅,因此我们可以有信心能够发现一些共同趋势。尽管如此,请求的实际文本可能没有什么变化,决定仍然是人类监督者的判断,这两个因素都可能混淆我们的结果。最后,即使通过这三篇文章,仍然有很多可以尝试的地方(不同类型的神经网络,GridSearchCV for hyperparameter tuning,AutoML)来改进我们的结果。
文本分类——RNN 的还是 CNN 的?
是一类人工神经网络,其中节点之间的连接沿着序列形成有向图。它基本上是一系列神经网络块,像链条一样相互链接。每一个都在向下一个传递信息。如果你想深入内部机制,我强烈推荐科拉的博客。这种架构允许 RNN 展示时间行为并捕获顺序数据,这使它在处理文本数据时成为一种更“自然”的方法,因为文本天生就是顺序的。
CNN 是一类深度前馈人工神经网络,其中节点之间的连接不形成循环。CNN 通常用在计算机视觉中,然而当应用于各种 NLP 任务时,它们也显示出有希望的结果。再次深入细节,Colah 的博客是一个很好的起点。
RNN 人被训练识别跨时间的模式,而 CNN 学习识别跨空间的模式。
哪种 DNN 类型在处理文本数据时表现得更好取决于理解全局/远程语义的频率。对于文本长度很重要的任务,使用 RNN 变体是有意义的。这些类型的任务包括:问答、翻译等。
事实证明,应用于某些 NLP 问题的 CNN 表现相当好。让我们简单看看当我们在文本数据上使用 CNN 时会发生什么。
当检测到特殊模式时,将触发每个卷积的结果。通过改变内核的大小并连接它们的输出,您允许自己检测多个大小的模式(2、3 或 5 个相邻的单词)。模式可以是表达式(单词 ngrams?)像“我讨厌”、“非常好”,因此 CNN 可以在句子中识别它们,而不管它们的位置。基于上述解释,最适合 CNN 的似乎是分类任务,如情感分析、垃圾邮件检测或主题分类。卷积和汇集操作丢失了关于单词的本地顺序的信息,因此像在词性标注或实体提取中那样的序列标注有点难以适应纯 CNN 架构(尽管并非不可能,但您可以向输入添加位置特征)。汇集也减少了输出维度,但(希望)保留了最突出的信息。你可以把每个过滤器想象成检测一个特定的特征,比如检测句子是否包含否定,比如“不惊人”。如果这个短语出现在句子中的某个地方,那么对该区域应用过滤器的结果将产生一个大值,而在其他区域产生一个小值。通过执行 max 操作,您保留了关于该特征是否出现在句子中的信息,但是您丢失了关于它出现的确切位置的信息。
当当前步骤与前面的步骤有某种关系时,rnn 被设计成利用顺序数据。这使得它们非常适合具有时间成分(音频、时序数据)和自然语言处理的应用。对于顺序信息显然很重要的应用程序,RNN 的表现非常好,因为如果不使用顺序信息,意思可能会被误解或者语法可能不正确。应用包括图像字幕,语言建模和机器翻译。
CNN 擅长提取局部的和位置不变的特征,而当分类是由长范围的语义依赖而不是一些局部的关键短语来确定时,RNN 更好。对于文本中的特征检测更重要的任务,例如,搜索愤怒的术语、悲伤、辱骂、命名实体等。CNN 做得很好,而对于顺序建模更重要的任务,RNN 做得更好。基于上述特征,选择 CNN 用于分类任务(如情感分类)是有意义的,因为情感通常由一些关键短语确定,而选择 RNNs 用于序列建模任务(如语言建模或机器翻译或图像字幕)是有意义的,因为它需要对上下文依赖性进行灵活的建模。rnn 通常擅长预测序列中的下一步,而 CNN 可以学习对句子或段落进行分类。
CNN 的一个重要理由是他们速度快。非常快。基于计算时间,CNN 似乎比 RNN 快得多(~ 5 倍)。卷积是计算机图形的核心部分,在 GPU 的硬件层面上实现。像文本分类或情感分析这样的应用实际上并不需要使用存储在数据序列中的信息。比如一个假设的餐厅点评:我对这家餐厅非常失望。服务慢得令人难以置信,食物也很一般。我不会回来了。虽然数据中有顺序信息,但如果你试图预测情绪是好是坏,CNN 模型可能就足够了,甚至在计算方面更好。做出预测所需的重要信息存在于短语“非常失望”、“慢得令人难以置信”和“平庸”中如果你只使用 2-gram,一个 RNN 可能能够额外捕捉到是服务慢得令人难以置信,相比之下,其他东西可能对慢有好处(也许是音乐?).但是,通常情况下,这是不必要的,与简单的模型相比,更复杂的 RNN 可能会溢出。
参考
当我们听到卷积神经网络(CNN)时,我们通常会想到计算机视觉。CNN 对此负有责任…
www.wildml.com](http://www.wildml.com/2015/11/understanding-convolutional-neural-networks-for-nlp/)**
极小数据集下的文本分类
充分利用微型数据集的指南
Stop overfitting!
俗话说,在这个深度学习的时代“数据是新的石油”。然而,除非你为谷歌、脸书或其他科技巨头工作,否则获取足够的数据可能是一项艰巨的任务。这对于那些在你我可能拥有的利基领域或个人项目中运营的小公司来说尤其如此。
在这篇博客中,我们将模拟一个场景,其中我们只能访问一个非常小的数据集,并详细探讨这个概念。特别是,我们将构建一个可以检测 clickbait 标题的文本分类器,并试验不同的技术和模型来处理小数据集。
博客概要:
- 什么是 Clickbait?
- 获取数据集
- 为什么小型数据集是 ML 中的一个难点?
- 拆分数据
- 简单的探索性数据分析
- 词袋、TF-IDF 和词嵌入
- 特征工程
- 探索模型和超参数调整
- 降维
- 摘要
1.什么是 clickbait?
通常,您可能会遇到这样的标题:
“我们试图用一个小数据集构建一个分类器。你不会相信接下来会发生什么!”
“我们喜欢这 11 种技术来构建文本分类器。# 7 会让你震惊。”
“智能数据科学家使用这些技术来处理小型数据集。点击了解它们是什么"
这些吸引人的标题在互联网上随处可见。但是,是什么让一个标题“吸引人”呢?维基百科将其定义为:
Clickbait 是一种虚假广告,它使用超链接文本或缩略图链接,旨在吸引注意力并诱使用户点击该链接,阅读、查看或收听链接的在线内容,其显著特征是欺骗性、典型的耸人听闻或误导性。
一般来说,一个帖子是否是点击诱饵的问题似乎是相当主观的。(查看:“为什么 BuzzFeed 不做 click bait”[1])。这意味着在查找数据集时,最好是查找由多人手动审阅的数据集。
2.获取数据集
经过一番搜索,我发现:停止点击诱饵:检测和防止网络新闻媒体中的点击诱饵Chakraborty 等人(2016)【2】及其附随的 Github repo
该数据集包含 15,000 多篇文章标题,分别被标记为点击诱饵和非点击诱饵。非点击诱饵标题来自维基新闻,由维基新闻社区策划,而点击诱饵标题来自“BuzzFeed”、“Upworthy”等。
为了确保没有任何假阳性,标记为 clickbait 的标题由六名志愿者验证,每个标题由至少三名志愿者进一步标记。文件的第 2 部分包含更多细节。
基准性能:作者在随机采样的 15k 数据集(平衡)上使用了 10 倍 CV。他们取得的最好结果是 RBF-SVM 达到了 93%的准确率,0.95 的精确度,0.9 的召回率,0.93 的 F1,0.97 的 ROC-AUC
所以这是我们的挑战:
我们将用的 50 个数据点作为我们的训练集,用的 10000 个数据点作为我们的测试集。这意味着训练组只是测试组的 0.5%。我们不会在训练中使用测试集的任何部分,它只是作为一个省略验证集。
评估指标:
随着我们进行不同的实验,跟踪性能指标对于理解我们的分类器表现如何至关重要。 F1 分数将是我们的主要绩效指标,但我们也会跟踪精确度、召回、 ROC-AUC 和精确度。
3.为什么小型数据集是 ML 中的一个难点?
在我们开始之前,理解为什么小数据集难以处理是很重要的:
- 过拟合 : 当数据集较小时,分类器有更多的自由度来构造决策边界。为了证明这一点,我在同一个数据集(只有 8 个点的 Iris 数据集的修改版本)上训练了随机森林分类器 6 次
Varying Decision Boundaries for a small dataset
注意决策边界是如何剧烈变化的。这是因为分类器很难用少量数据进行归纳。从数学上来说,这意味着我们的预测会有很高的方差。
潜在解决方案:
正规化:我们将不得不使用大量的 L1,L2 和其他形式的正规化。
二。*更简单的模型:*像逻辑回归和支持向量机这样的低复杂度线性模型往往表现更好,因为它们的自由度更小。
2。离群值:
离群值对小数据集有着巨大的影响,因为它们会显著扭曲决策边界。在下面的图中,我添加了一些噪声,并改变了其中一个数据点的标签,使其成为异常值——注意这对决策边界的影响。
Effect of Outliers on the Decision Boundary
潜在解决方案:
异常检测和去除:我们可以使用像 DBSCAN 这样的聚类算法或者像隔离森林这样的集成方法
3。高维度:
随着更多特征的添加,分类器有更大的机会找到超平面来分割数据。然而,如果我们在不增加训练样本数量的情况下增加维数,特征空间会变得更加稀疏,分类器很容易过拟合。这是维度诅咒的直接结果——在这个博客中有最好的解释
潜在解决方案:
I. 分解技术 : PCA/SVD 降低特征空间的维数
二。特征选择:去除预测中无用的特征。
我们将在这篇博客中深入探讨这些解决方案。
4.拆分数据
让我们从将数据分成训练集和测试集开始。如前所述,我们将使用 50 个数据点进行训练,10000 个数据点进行测试。
(为了保持整洁,我删除了一些琐碎的代码:您可以查看GitHub repo中的完整代码)
data = pd.DataFrame(clickbait_data)#Now lets split the datafrom sklearn.model_selection import train_test_splittrain, test = train_test_split(data, shuffle = True, stratify = data.label, train_size = 50/data.shape[0], random_state = 50)test, _ = train_test_split(test, shuffle = True,
stratify = test.label, train_size = 10000/test.shape[0], random_state = 50)train.shape, test.shape**Output:** ((50, 2), (10000, 2))
这里重要的一步是确保我们的训练集和测试集来自相同的分布,这样训练集的任何改进都会反映在测试集中。
Kagglers 使用的一个常用技术是在不同的数据集之间使用“对抗性验证”。(我见过它有很多名字,但我认为这是最常见的一个)
这个想法非常简单,我们混合两个数据集,并训练一个分类器来尝试区分它们。如果分类器不能做到这一点,我们可以得出结论,分布是相似的。可以在这里阅读更多:https://www . kdnugges . com/2016/10/adversarial-validation-explained . html
ROC AUC 是首选指标——值约为 0.5 或更低意味着分类器与随机模型一样好,并且分布相同。
Code for Adversarial Validation
在进行对抗性验证之前,让我们使用词袋对标题进行编码
bow = CountVectorizer()
x_train = bow.fit_transform(train.title.values)
x_test = bow.transform(test.title.values)x_test = shuffle(x_test)adversarial_validation(x_train, x_test[:50])**Output:** Logisitic Regression AUC : 0.384
Random Forest AUC : 0.388
低 AUC 值表明分布是相似的。
为了看看如果发行版不同会发生什么,我在 breitbart.com 上运行了一个网络爬虫,收集了一些文章标题。
bow = CountVectorizer()
x_train = bow.fit_transform(breitbart.title.values)
x_test = bow.transform(test.title.values)x_train = shuffle(x_train)
x_test = shuffle(x_test)
adverserial_validation(x_train[:50], x_test[:50])**Output:** Logisitic Regression AUC : 0.720
Random Forest AUC : 0.794
AUC 值高得多,表明分布是不同的。
现在让我们继续,在 train 数据集上做一些基本的 EDA。
5.简单的探索性数据分析
让我们从检查数据集是否平衡开始:
print('Train Positive Class % : {:.1f}'.format((sum(train.label == 'clickbait')/train.shape[0])*100))
print('Test Positive Class % : {:.1f}'.format((sum(test.label == 'clickbait')/test.shape[0])*100))print('Train Size: {}'.format(train.shape[0]))
print('Test Size: {}'.format(test.shape[0]))**Output:** Train Positive Class % : 50.0
Test Positive Class % : 50.0
Train Size: 50
Test Size: 10000
接下来,让我们检查字数的影响。
看起来 Clickbait 标题中包含的单词更多。平均单词长度呢?
与非点击诱饵标题相比,点击诱饵标题使用较短的单词。由于 clickbait 标题通常有更简单的单词,我们可以检查标题中有百分之多少的单词是停用词
奇怪的是,clickbait 标题似乎没有 NLTK 停用词列表中的停用词。这可能是一个巧合,因为火车测试分裂或我们需要扩大我们的停用词列表。在特征工程中肯定要探索的东西。此外,停止单词删除作为预处理步骤在这里不是一个好主意。
单词云可以帮助我们识别每个类别中更突出的单词。让我们来看看:
Wordcloud for Clickbait Titles
Wordcloud for Non-Clickbait Titles
点击诱饵和非点击诱饵标题之间的单词分布非常不同。例如:非点击诱饵标题有州/国家,如“尼日利亚”、“中国”、“加利福尼亚”等,以及更多与新闻相关的词,如“暴乱”、“政府”和“破产”。非点击诱饵标题似乎有更多的通用词,如“最爱”、“关系”、“事情”等
使用单词包、TF-IDF 或 GloVe/W2V 这样的单词嵌入作为特性应该会有所帮助。与此同时,我们也可以通过简单的文本特性,如长度、单词比率等,获得大量的性能提升。
让我们试试 TSNE 对标题的单词包编码:
TSNE on BoW Title Encodings
这两个类似乎都用 BoW 编码聚集在一起。在下一节中,我们将探索不同的嵌入技术。
实用功能:
在我们开始探索嵌入之前,让我们编写几个助手函数来运行逻辑回归和计算评估指标。
因为我们想要优化 F1 分数的模型,所以对于所有的模型,我们将首先预测正类的概率。然后,我们将使用这些概率来获得精度-召回曲线,从这里我们可以选择一个具有最高 F1 分数的阈值。为了预测标签,我们可以简单地使用这个阈值。
Utility Functions to calculate F1 and run Log Reg
6.词袋、TF-IDF 和词嵌入
在这一节中,我们将使用 BoW、TF-IDF 和 Word 嵌入对标题进行编码,并在不添加任何其他手工制作的功能的情况下使用这些功能。
从 BoW 和 TF-IDF 开始:
y_train = np.where(train.label.values == 'clickbait', 1, 0)
y_test = np.where(test.label.values == 'clickbait', 1, 0)from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizerbow = CountVectorizer()
x_train = bow.fit_transform(train.title.values)
x_test = bow.transform(test.title.values)run_log_reg(x_train, x_test, y_train, y_test)from sklearn.feature_extraction.text import TfidfVectorizertfidf = TfidfVectorizer()
x_train = tfidf.fit_transform(train.title.values)
x_test = tfidf.transform(test.title.values)run_log_reg(x_train, x_test, y_train, y_test)**Output:
For BoW:** F1: 0.782 | Pr: 0.867 | Re: 0.714 | AUC: 0.837 | Accuracy: 0.801**For TF-IDF:**
F1: 0.829 | Pr: 0.872 | Re: 0.790 | AUC: 0.896 | Accuracy: 0.837
TFIDF 的表现略好于 BoW。一个有趣的事实是,我们只用 50 个数据点就得到 0.837 的 F1 分数。这就是为什么 Log Reg + TFIDF 是 NLP 分类任务的一个很好的基线。
接下来,让我们试试 100 维手套向量。我们将使用 PyMagnitude 库: (PyMagnitude 是一个非常棒的库,它包含了很多优秀的特性,比如智能的声外表示。强烈推荐!)
由于标题可以有不同的长度,我们将找到每个单词的手套表示,并将它们平均在一起,给出每个标题的单一 100-D 向量表示。
# We'll use Average Glove here
from tqdm import tqdm_notebook
from nltk import word_tokenize
from pymagnitude import *glove = Magnitude("./vectors/glove.6B.100d.magnitude")def avg_glove(df):
vectors = []
for title in tqdm_notebook(df.title.values):
vectors.append(np.average(glove.query(word_tokenize(title)), axis = 0))
return np.array(vectors)x_train = avg_glove(train)
x_test = avg_glove(test)run_log_reg(x_train, x_test, y_train, y_test)**Output:**
F1: 0.929 | Pr: 0.909 | Re: 0.950 | AUC: 0.979 | Accuracy: 0.928
哇哦。这是 F1 分数的巨大增长,只是标题编码有一点点变化。改进的性能是合理的,因为 W2V 是包含大量上下文信息的预训练嵌入。这将有助于分类器的性能,尤其是当我们的数据集非常有限时。
如果我们不只是取每个单词的平均值,而是取一个加权平均值,特别是 IDF 加权平均值,会怎么样?
from sklearn.feature_extraction.text import TfidfVectorizertfidf = TfidfVectorizer()
tfidf.fit(train.title.values)# Now lets create a dict so that for every word in the corpus we have a corresponding IDF value
idf_dict = dict(zip(tfidf.get_feature_names(), tfidf.idf_))# Same as Avg Glove except instead of doing a regular average, we'll use the IDF values as weights.def tfidf_glove(df):
vectors = []
for title in tqdm_notebook(df.title.values):
glove_vectors = glove.query(word_tokenize(title))
weights = [idf_dict.get(word, 1) for word in word_tokenize(title)]
vectors.append(np.average(glove_vectors, axis = 0, weights = weights))
return np.array(vectors)x_train = tfidf_glove(train)
x_test = tfidf_glove(test)run_log_reg(x_train, x_test, y_train, y_test)**Output:** F1: 0.957 | Pr: 0.943 | Re: 0.971 | AUC: 0.989 | Accuracy: 0.956
我们的 F1 提高了大约 0.02 分。性能的提高是有意义的——经常出现的单词权重变小,而不经常出现(也许更重要)的单词在标题的向量表示中更有发言权。
既然 GloVe 工作得如此之好,让我们试试最后一种嵌入技术——脸书的下一代模型。该模型将整个句子转换成向量表示。然而,一个潜在的问题是,向量表示是 4096 维的,这可能导致我们的模型容易过度拟合。不管怎样,让我们试一试:
from InferSent.models import InferSent
import torchMODEL_PATH = './encoder/infersent1.pkl'
params_model = {'bsize': 64, 'word_emb_dim': 300, 'enc_lstm_dim': 2048,'pool_type': 'max', 'dpout_model': 0.0, 'version': 1}infersent = InferSent(params_model)
infersent.load_state_dict(torch.load(MODEL_PATH))infersent.set_w2v_path('GloVe/glove.840B.300d.txt')infersent.build_vocab(train.title.values, tokenize= False)x_train = infersent.encode(train.title.values, tokenize= False)
x_test = infersent.encode(test.title.values, tokenize= False)run_log_reg(x_train, x_test, y_train, y_test, alpha = 1e-4)**Output:** F1: 0.927 | Pr: 0.912 | Re: 0.946 | AUC: 0.966 | Accuracy: 0.926
正如预期的那样,性能下降—很可能是由于 4096 维特征的过度拟合。
在我们结束本节之前,让我们再次尝试 TSNE,这次是在 IDF 加权的手套向量上
这一次,我们在 2D 投影中看到了两个类别之间的一些分离。在某种程度上,这解释了我们用简单的 Log Reg 实现的高精度。
为了进一步提高性能,我们可以添加一些手工制作的功能。让我们在下一节中尝试一下。
7.特征工程
创建新功能可能很棘手。在这方面领先的最好方法是深入该领域,寻找研究论文、博客、文章等。相关领域中的 Kaggle 内核也是查找有趣特性信息的好方法。
对于 clickbait 检测,我们用于数据集的论文(Chakraborthy 等人)提到了他们使用的一些特征。我还找到了 Potthast 等人 (2016) [3],他们在其中记录了超过 200 个特性。
我们可以实现一些简单的方法以及上一节中的手套嵌入,并检查是否有任何性能改进。以下是这些功能的简要总结:
- 以数字开头:一个布尔特性,检查标题是否以数字开头。例如:“11 种不可思议的 XYZ 方式”
- Clickbait 短语 : Downworthy 是一个搞笑的 chrome 扩展,它用“更真实的标题”取代了 Clickbait 标题。Github repo 列出了流行的 clickbait 短语,如“你需要知道的一切”、“这就是发生的事情”等。我们可以使用这个列表来检查我们的数据集中的标题是否包含任何这些短语。Chakraborthy 等人还提供了一系列进一步的短语。
- Clickbait 正则表达式 : Downworthy 也有一些正则表达式,我们可以将它们与标题进行匹配。
- 数字点点点
- 可读性分数:计算 Flesch-Kincaid 等级和 Dale-Chall 可读性分数。这些分数提供了阅读标题的难易程度。textstat python 包提供了实现这些的简单方法。一般来说,我们认为新闻标题更难阅读。
- **简单文本特征:**最长单词的长度,以字符为单位的平均单词长度,以字符为单位的标题长度。
- 标点符号的数量
- 字比:这里我们计算 6 种不同的比:(一)。简单词汇(根据戴尔·查尔简单词汇列表的定义)(ii)停止词(iii)缩写词(例如:不是、不应该等)(iv)夸张词汇(根据 Chakraborthy 等人的定义,例如:惊人的、不可思议的等)(v)点击诱饵主题(Chakraborthy 等人定义了一些在点击诱饵标题中更常见的名词/主题,如“家伙”、“狗”等)(vi)非点击诱饵主题(与上述非点击诱饵标题相同,例如:印度、伊朗、政府等)
- 标签数量
- 情感评分:我们可以使用 NLTK 的 Vader 情感分析仪,得到每个标题的负面、中性、正面和复合评分。
- 嵌入:tfi df/手套/加重手套
实现这些之后,我们可以选择使用 sklearn 的PolynomialFeatures()
用多项式(如 X)或交互特征(如 XY)来扩展特征空间
注:特征缩放技术的选择对分类器的性能影响很大,我尝试了RobustScaler
StandardScaler
Normalizer
MinMaxScaler
,发现* MinMaxScaler
效果最好。***
**from featurization import *train_features, test_features, feature_names = featurize(train, test, 'tfidf_glove')run_log_reg(train_features, test_features, y_train, y_test, alpha = 5e-2)**Output:** F1: 0.964 | Pr: 0.956 | Re: 0.972 | AUC: 0.993 | Accuracy: 0.964**
不错!在简单的逻辑回归分析中,我们的 F1 值从 0.957 上升到 0.964。当我们尝试不同的模型并稍后进行超参数调整时,我们可能能够挤出更多的性能改进。
现在,让我们简短地探讨一下模型的可解释性,以检查我们的模型是如何做出这些预测的。我们将使用 SHAP 和 ELI5 库来理解这些特性的重要性。
特征权重
先说特征重要性。这对于 ELI5 库来说非常简单。
**from sklearn.linear_model import SGDClassifier
import eli5# Train a Log Reg Classifier
log_reg = SGDClassifier(loss = 'log', n_jobs = -1, alpha = 5e-2)
log_reg.fit(train_features, y_train)#Pass the model instance along with the feature names to ELI5
eli5.show_weights(log_reg, feature_names = feature_names, top = 100)**
****
Feature Weights
除了手套的尺寸,我们可以看到许多手工制作的特点有很大的重量。特征越环保,将样本归类为“点击诱饵”就越重要。
例如,starts_with_number
特征对于分类一个标题是 clickbait 是非常重要的。这很有意义,因为在数据集中,像“你应该去 XYZ 的 12 个理由”这样的标题经常是点击诱饵。
我们来看看dale_chall_readability_score
这个特征,它的权重是-0.280。如果 Dale Chall 可读性分数很高,说明标题很难读。在这里,我们的模型已经知道,如果一个标题更难阅读,它可能是一个新闻标题,而不是点击诱饵。相当酷!
此外,还有一些功能的权重非常接近于 0。移除这些特性可能有助于减少过度拟合,我们将在特性选择部分对此进行探讨。
SHAP 力剧情
现在让我们来看看 SHAP 力图
**import shaplog_reg = SGDClassifier(loss = 'log', n_jobs = -1, alpha = 5e-2)
log_reg.fit(train_features, y_train)explainer = shap.LinearExplainer(log_reg, train_features, feature_dependence = 'independent')
shap_values = explainer.shap_values(test_features)shap.initjs()
ind = 0
shap.force_plot(explainer.expected_value, shap_values[ind,:], test_features.toarray()[ind,:],
feature_names = feature_names)**
SHAP Force Plot
一个原力剧情就像是一场角色间的‘拔河’游戏。每个特征将模型的输出推到基值的左侧或右侧。基础值是模型在整个测试数据集上的平均输出。请记住,这不是一个概率值。
粉红色的特征有助于模型检测正面类别,即“点击诱饵”标题,而蓝色的特征检测负面类别。每个特征的宽度与其在预测中的权重成正比。
在上面的例子中,starts_with_number
特征是 1,并且非常重要,因此将模型的输出推到右边。另一方面,clickbait_subs_ratio
和easy_words_ratio
(这些特性中的高值通常表示 clickbait,但在这种情况下,值较低)都将模型推向左侧。
我们可以验证,在这个特定的例子中,模型最终预测“点击诱饵”
**print('Title: {}'.format(test.title.values[0]))
print('Label: {}'.format(test.label.values[0]))
print('Prediction: {}'.format(log_reg.predict(test_features.tocsr()[0,:])[0]))**Output:
Title**: 15 Highly Important Questions About Adulthood, Answered By Michael Ian Black
**Label**: clickbait
**Prediction**: 1**
正如所料,模型正确地将标题标记为 clickbait。
让我们看另一个例子:
SHAP Force Plot for Non-Clickbait Titles
**print('Title: {}'.format(test.title.values[400]))
print('Label: {}'.format(test.label.values[400]))
print('Prediction: {}'.format(log_reg.predict(test_features.tocsr()[400,:])[0]))**Output:
Title**: Europe to Buy 30,000 Tons of Surplus Butter
**Label**: not-clickbait
**Prediction**: 0**
在这种情况下,模型被推到左边,因为像sentiment_pos
(clickbait 标题通常具有正面情绪)这样的特征具有较低的值。
力图是一种很好的方式来观察模型是如何逐个样本地进行预测的。在下一节中,我们将尝试不同的模型,包括集成和超参数调整。
8.探索模型和超参数调整
在本节中,我们将使用我们在上一节中创建的功能,以及 IDF 加权嵌入,并在不同的模型上进行尝试。
如前所述,在处理小数据集时,像逻辑回归、支持向量机和朴素贝叶斯这样的低复杂度模型将会概括得最好。我们将这些模型与非参数模型(如 KNN)和非线性模型(如 Random Forest、XGBoost 等)一起尝试。
我们还将尝试使用性能最佳的分类器和模型堆叠进行引导聚合或打包。我们开始吧!
对于我们的情况,超参数调整GridSearchCV
是一个很好的选择,因为我们有一个小的数据集(允许它快速运行),这是一个穷举搜索。我们需要做一些修改,以使它(a)使用我们预定义的测试集,而不是交叉验证(b)使用我们的 F1 评估指标,该指标使用 PR 曲线来选择阈值。
GridSearchCV with PrefededfinedSplit
逻辑回归
**from sklearn.linear_model import SGDClassifierlr = SGDClassifier(loss = 'log')
lr_params = {'alpha' : [10**(-x) for x in range(7)],
'penalty' : ['l1', 'l2', 'elasticnet'],
'l1_ratio' : [0.15, 0.25, 0.5, 0.75]}best_params, best_f1 = run_grid_search(lr, lr_params, X, y)print('Best Parameters : {}'.format(best_params))lr = SGDClassifier(loss = 'log',
alpha = best_params['alpha'],
penalty = best_params['penalty'],
l1_ratio = best_params['l1_ratio'])
lr.fit(train_features, y_train)
y_test_prob = lr.predict_proba(test_features)[:,1]
print_model_metrics(y_test, y_test_prob)**Output:** Best Parameters : {'alpha': 0.1, 'l1_ratio': 0.15, 'penalty': 'elasticnet'}
F1: 0.967 | Pr: 0.955 | Re: 0.979 | AUC: 0.994 | Accuracy: 0.967**
请注意,调整后的参数同时使用了高 alpha 值(表示大量的正则化)和 elasticnet。这些参数选择是因为小数据集容易过拟合。
我们可以对 SVM、朴素贝叶斯、KNN、随机森林和 XGBoost 进行同样的调整。下表总结了这些测试的结果(您可以参考 GitHub repo 获得完整的代码)
Summary of Hyperparameter Tuning
简单的 MLP
在 fast.ai 课程中,杰瑞米·霍华德提到深度学习已经在许多情况下相当成功地应用于表格数据。让我们看看它在我们的用例中表现如何:
2-Layer MLP in Keras
y_pred_prob = simple _ nn . predict(test _ features . to dense())
print _ model _ metrics(y _ test,y _ pred _ prob)
****Output**:
F1: 0.961 | Pr: 0.952 | Re: 0.970 | AUC: 0.992 | Accuracy: 0.960**
鉴于数据集较小,两层 MLP 模型的效果令人惊讶。
装袋分级机
既然 SVM 做得如此之好,我们可以通过使用 SVM 作为基本估计量来尝试一个 bagging 分类器。这将改善基础模型的方差并减少过度拟合。
**from sklearn.ensemble import BaggingClassifier
from sklearn.svm import SVC
from sklearn.model_selection import RandomizedSearchCVsvm = SVC(C = 10, kernel = 'poly', degree = 2, probability = True, verbose = 0)svm_bag = BaggingClassifier(svm, n_estimators = 200, max_features = 0.9, max_samples = 1.0, bootstrap_features = False, bootstrap = True, n_jobs = 1, verbose = 0)svm_bag.fit(train_features, y_train)
y_test_prob = svm_bag.predict_proba(test_features)[:,1]
print_model_metrics(y_test, y_test_prob)**Output:** F1: 0.969 | Pr: 0.959 | Re: 0.980 | AUC: 0.995 | Accuracy: 0.969**
性能提升几乎微不足道。
最后,我们可以尝试的最后一件事是堆叠分类器(也称为投票分类器)
堆积分级机
这是不同模型预测的加权平均值。因为我们也使用 Keras 模型,所以我们不能使用 Sklearn 的VotingClassifier
,相反,我们将运行一个简单的循环,获得每个模型的预测,并运行一个加权平均。我们将为每个模型使用调整后的超参数。
Simple Stacking Classifier
****Output:** Training LR
Training SVM
Training NB
Training KNN
Training RF
Training XGB
F1: 0.969 | Pr: 0.968 | Re: 0.971 | AUC: 0.995 | Accuracy: 0.969**
现在,我们需要一种方法来为每个模型选择最佳权重。最好的选择是使用像 Hyperopt 这样的优化库,它可以搜索最大化 F1 分数的最佳权重组合。
Running Hyperopt for the stacking classifier
Hyperopt 找到一组给出 F1 ~ 0.971 的权重。让我们检查优化的砝码:
**{'KNN': 0.7866810233035141,
'LR': 0.8036572275670447,
'NB': 0.9102009774357307,
'RF': 0.1559824350958057,
'SVM': 0.9355079606348642,
'XGB': 0.33469066125332436,
'simple_nn': 0.000545264707939086}**
像逻辑回归、朴素贝叶斯和 SVM 这样的低复杂度模型具有高权重,而像随机森林、XGBoost 和 2 层 MLP 这样的非线性模型具有低得多的权重。这符合我们的预期,即低复杂性和简单模型将最好地概括较小的数据集。
最后,使用优化的权重运行堆叠分类器得到:
**F1: 0.971 | Pr: 0.962 | Re: 0.980 | AUC: 0.995 | Accuracy: 0.971**
在下一节中,我们将解决小数据集的另一个问题——高维特征空间。
9.降维
正如我们在简介中所讨论的,随着我们增加小数据集的维度,特征空间变得稀疏,导致分类器很容易过拟合。
解决方法就是降低维度。实现这一点的两种主要方法是特征选择和分解。
特征选择
这些技术根据特征在预测中的相关性来选择特征。
**SelectKBest**
我们从SelectKBest
开始,顾名思义,它只是根据所选的统计数据(默认为 ANOVA F-Scores)选择 k-best 特征
**from sklearn.feature_selection import SelectKBestselector = SelectKBest(k = 80)
train_features_selected = selector.fit_transform(train_features, y_train)
test_features_selected = selector.transform(test_features)
run_log_reg(train_features_selected, test_features_selected, y_train, y_test)**Output:** F1: 0.958 | Pr: 0.946 | Re: 0.971 | AUC: 0.989 | Accuracy: 0.957**
SelectKBest 的一个小问题是,我们需要手动指定想要保留的特性的数量。一种简单的方法是运行一个循环来检查每个 k 值的 F1 分数。下面是要素数量与 F1 分数的关系图:
F1 Scores for different values of K
大约 45 个特征给出了最佳 F1 值。让我们用 K = 45 重新运行 SelectKBest:
**selector = SelectKBest(k = 45)
train_features_selected = selector.fit_transform(train_features, y_train)
test_features_selected = selector.transform(test_features)
run_log_reg(train_features_selected, test_features_selected, y_train, y_test, alpha = 1e-2)**Output:** F1: 0.972 | Pr: 0.967 | Re: 0.978 | AUC: 0.995 | Accuracy: 0.972**
另一个选择是使用 SelectPercentile,它使用我们想要保留的特性的百分比。
**SelectPercentile**
按照与上面相同的步骤,我们得到最佳 F1 分数的百分位数= 37。现在使用 SelectPercentile:
**selector = SelectPercentile(percentile = 37)
train_features_selected = selector.fit_transform(train_features, y_train)
test_features_selected = selector.transform(test_features)
run_log_reg(train_features_selected, test_features_selected, y_train, y_test, alpha = 1e-2)**Output:** F1: 0.972 | Pr: 0.966 | Re: 0.979 | AUC: 0.995 | Accuracy: 0.972**
简单的特征选择将 F1 分数从 0.966(先前调整的 Log Reg 模型)增加到 0.972。如前所述,这是因为低维特征空间减少了模型过拟合的机会。
对于这两种技术,我们也可以使用selector.get_support()
来检索所选特征的名称。
**np.array(feature_names)[selector.get_support()]**Output:** array(['starts_with_number', 'easy_words_ratio', 'stop_words_ratio',
'clickbait_subs_ratio', 'dale_chall_readability_score', 'glove_3',
'glove_4', 'glove_6', 'glove_10', 'glove_14', 'glove_15',
'glove_17', 'glove_19', 'glove_24', 'glove_27', 'glove_31',
'glove_32', 'glove_33', 'glove_35', 'glove_39', 'glove_41',
'glove_44', 'glove_45', 'glove_46', 'glove_49', 'glove_50',
'glove_51', 'glove_56', 'glove_57', 'glove_61', 'glove_65',
'glove_68', 'glove_72', 'glove_74', 'glove_75', 'glove_77',
'glove_80', 'glove_85', 'glove_87', 'glove_90', 'glove_92',
'glove_96', 'glove_97', 'glove_98', 'glove_99'], dtype='<U28')**
**RFECV (Recursive Features Elimination)**
RFE 是一种后向特征选择技术,它使用估计器来计算每个阶段的特征重要性。名称中的单词 recursive 意味着该技术递归地删除了对分类不重要的特征。
我们将使用 CV 变量,它在每个循环中使用交叉验证来确定在每个循环中要删除多少个特征。RFECV 需要一个具有feature_importances_
属性的估计器,因此我们将使用具有 log loss 的 SGDClassifier。
我们还需要指定所需的交叉验证技术的类型。我们将使用在超参数优化中使用的相同的PredefinedSplit
。
**from sklearn.feature_selection import RFECVlog_reg = SGDClassifier(loss = ‘log’, alpha = 1e-3)selector = RFECV(log_reg, scoring = ‘f1’, n_jobs = -1, cv = ps, verbose = 1)
selector.fit(X, y)# Now lets select the best features and check the performance
train_features_selected = selector.transform(train_features)
test_features_selected = selector.transform(test_features)run_log_reg(train_features_selected, test_features_selected, y_train, y_test, alpha = 1e-1)**Output:** F1: 0.978 | Pr: 0.970 | Re: 0.986 | AUC: 0.997 | Accuracy: 0.978**
让我们检查一下所选的功能:
**print('Number of features selected:{}'.format(selector.n_features_))
np.array(feature_names)[selector.support_]**Output:** Number of features selected : 60
array(['starts_with_number', 'clickbait_phrases', 'num_dots',
'mean_word_length', 'length_in_chars', 'easy_words_ratio',
'stop_words_ratio', 'contractions_ratio', 'hyperbolic_ratio',
'clickbait_subs_ratio', 'nonclickbait_subs_ratio',
'num_punctuations', 'glove_1', 'glove_2', 'glove_4','glove_6'
'glove_10', 'glove_13', 'glove_14', 'glove_15', 'glove_16',
'glove_17', 'glove_21', 'glove_25', 'glove_27', 'glove_32',
'glove_33', 'glove_35', 'glove_39', 'glove_41', 'glove_43',
'glove_45', 'glove_46', 'glove_47', 'glove_50', 'glove_51',
'glove_52', 'glove_53', 'glove_54', 'glove_56', 'glove_57',
'glove_58', 'glove_61', 'glove_65', 'glove_72', 'glove_74',
'glove_77', 'glove_80', 'glove_84', 'glove_85', 'glove_86',
'glove_87', 'glove_90', 'glove_93', 'glove_94', 'glove_95',
'glove_96', 'glove_97', 'glove_98', 'glove_99'], dtype='<U28')**
这一次,我们选择了一些额外的特性,使性能略有提高。由于传递了估计量和 CV 集,该算法有更好的方法来判断保留哪些特征。
这里的另一个优点是,我们不必提及要保留多少特性,RFECV
会自动为我们找到。然而,我们可以提到我们希望拥有的特性的最小数量,默认情况下是 1。
**SFS**
(顺序向前选择)
最后,让我们试试SFS
——它和RFE
做同样的事情,但是依次添加特性。SFS
从 0 个特征开始,以贪婪的方式在每个循环中逐个添加特征。一个小的区别是SFS
只使用 CV 集上的特征集性能作为选择最佳特征的度量,不像RFE
使用模型权重(feature_importances_
)。
**# Note: mlxtend provides the SFS Implementation
from mlxtend.feature_selection import SequentialFeatureSelectorlog_reg = SGDClassifier(loss = ‘log’, alpha = 1e-2)selector = SequentialFeatureSelector(log_reg, k_features = ‘best’, floating = True, cv = ps, scoring = ‘f1’, verbose = 1, n_jobs = -1) # k_features = ‘best’ returns the best subset of features
selector.fit(X.tocsr(), y)train_features_selected = selector.transform(train_features.tocsr())
test_features_selected = selector.transform(test_features.tocsr())run_log_reg(train_features_selected, test_features_selected, y_train, y_test, alpha = 1e-2)**Output:** F1: 0.978 | Pr: 0.976 | Re: 0.981 | AUC: 0.997 | Accuracy: 0.978**
我们还可以检查选定的功能:
**print('Features selected {}'.format(len(selector.k_feature_idx_)))
np.array(feature_names)[list(selector.k_feature_idx_)]**Output:** Features selected : 53array(['starts_with_number', 'clickbait_phrases','mean_word_length',
'length_in_chars','stop_words_ratio','nonclickbait_subs_ratio',
'flesch_kincaid_grade', 'dale_chall_readability_score',
'num_punctuations', 'glove_0', 'glove_1', 'glove_2','glove_4'
'glove_8', 'glove_10', 'glove_13', 'glove_14', 'glove_15',
'glove_16', 'glove_17', 'glove_18', 'glove_25', 'glove_30',
'glove_32', 'glove_33', 'glove_38', 'glove_39', 'glove_40',
'glove_41', 'glove_42', 'glove_45', 'glove_46', 'glove_47',
'glove_48', 'glove_51', 'glove_56', 'glove_57', 'glove_61',
'glove_65', 'glove_67', 'glove_69', 'glove_72', 'glove_73',
'glove_76', 'glove_77', 'glove_80', 'glove_81', 'glove_84',
'glove_85', 'glove_87', 'glove_93', 'glove_95', 'glove_96'],
dtype='<U28')**
向前和向后选择经常给出相同的结果。现在让我们来看看分解技术。
分解
与挑选最佳特征的特征选择不同,分解技术分解特征矩阵以降低维数。由于这些技术改变了特征空间本身,一个缺点是我们失去了模型/特征的可解释性。我们不再知道分解的特征空间的每个维度代表什么。
让我们尝试在我们的特征矩阵上截断 VD。我们要做的第一件事是找出解释的方差是如何随着元件数量而变化的。
**from sklearn.decomposition import TruncatedSVDsvd = TruncatedSVD(train_features.shape[1] - 1)
svd.fit(train_features)
plt.plot(np.cumsum(svd.explained_variance_ratio_))**
Plot to find out n_components for TruncatedSVD
看起来仅仅 50 个组件就足以解释训练集特征中 100%的差异。这意味着我们有许多从属特征(即一些特征只是其他特征的线性组合)。
这与我们在特征选择部分看到的一致——尽管我们有 119 个特征,但大多数技术选择了 40-70 个特征(剩余的特征可能不重要,因为它们只是其他特征的线性组合)。
现在我们可以将特征矩阵减少到 50 个组件。
**svd = TruncatedSVD(50)
train_featurse_decomposed = svd.fit_transform(train_features)
test_featurse_decomposed = svd.transform(test_features)
run_log_reg(train_featurse_decomposed, test_featurse_decomposed, y_train, y_test, alpha = 1e-1)**Output:** F1: 0.965 | Pr: 0.955 | Re: 0.975 | AUC: 0.993 | Accuracy: 0.964**
性能不如特征选择技术——为什么?
分解技术(如 TruncatedSVD)的主要工作是用较少的分量解释数据集中的方差。这样做时,它从不考虑每个特征在预测目标时的重要性(“点击诱饵”或“非点击诱饵”)。然而,在特征选择技术中,每次移除或添加特征时都使用特征重要性或模型权重。RFE
和SFS
特别选择功能以优化模型性能。(您可能已经注意到,在特性选择技术中,我们在每个 *fit()*
调用中都传递了‘y’。)
具有特征选择的堆叠分类器
最后,我们可以将上述任何技术与性能最佳的模型——堆叠分类器结合使用。我们必须将每个模型重新调整到缩减的特征矩阵,并再次运行 hyperopt 以找到堆叠分类器的最佳权重。
现在,在使用RFECV
选定功能并重新调整后:
**F1: 0.980 | Pr: 0.976 | Re: 0.984 | AUC: 0.997 | Accuracy: 0.980**
10.总结:
以下是我们迄今为止运行的所有模型和实验的总结:
Summary of all experiments
让我们来看看堆叠分类器的混淆矩阵:
Stacking Classifier Confusion Matrix
以下是十大高可信度的错误分类书目:
**Title : A Peaking Tiger Woods
Label : not-clickbait
Predicted Probability : 0.7458264596039637
----------
Title : Stress Tests Prove a Sobering Idea
Label : not-clickbait
Predicted Probability : 0.7542456646954389
----------
Title : Woods Returns as He Left: A Winner
Label : not-clickbait
Predicted Probability : 0.7566487248241188
----------
Title : In Baseball, Slow Starts May Not Have Happy Endings
Label : not-clickbait
Predicted Probability : 0.7624898001334597
----------
Title : Ainge Has Heart Attack After Celtics Say Garnett May Miss Playoffs
Label : not-clickbait
Predicted Probability : 0.7784241132465458
----------
Title : Private Jets Lose That Feel-Good Factor
Label : not-clickbait
Predicted Probability : 0.7811035856329488
----------
Title : A Little Rugby With Your Cross-Dressing?
Label : not-clickbait
Predicted Probability : 0.7856236669189782
----------
Title : Smartphone From Dell? Just Maybe
Label : not-clickbait
Predicted Probability : 0.7868008600434597
----------
Title : Cellphone Abilities That Go Untapped
Label : not-clickbait
Predicted Probability : 0.8057172770139488
----------
Title : Darwinism Must Die So That Evolution May Live
Label : not-clickbait
Predicted Probability : 0.8305944075171504
----------**
所有高可信度的错误分类标题都是“非点击诱饵”,这反映在混淆矩阵中。
乍一看,这些标题似乎与常规的新闻标题大相径庭。以下是从测试集中随机选择的“非点击诱饵”标题的示例:
**test[test.label.values == 'not-clickbait'].sample(10).title.values**Output:** array(['Insurgents Are Said to Capture Somali Town',
'Abducted teen in Florida found',
'As Iraq Stabilizes, China Eyes Its Oil Fields',
'Paramilitary group calls for end to rioting in Northern Ireland',
'Finding Your Way Through a Maze of Smartphones',
'Thousands demand climate change action',
'Paternity Makes Punch Line of Paraguay President',
'Comcast and NFL Network Continue to Haggle',
'Constant Fear and Mob Rule in South Africa Slum',
'Sebastian Vettel wins 2010 Japanese Grand Prix'], dtype=object)**
你怎么想呢?
我们可以尝试一些技术,如半监督伪标签,回译等,以尽量减少这些假阳性,但在博客长度的利益,我会留到另一个时间。
总之,通过了解过拟合在小型数据集中的工作方式以及特征选择、堆叠、调整等技术,我们能够在仅有 50 个样本的情况下将性能从 F1 = 0.801 提高到 F1 = 0.98。还不错!
如果你有任何问题,请随时与我联系。我希望你喜欢!
****GitHub Repo:https://GitHub . com/anirudhshenoy/text-classification-small-datasets
参考资料:
- https://www . BuzzFeed . com/Ben Smith/why-BuzzFeed-donts-do-click bait
- Abhijnan Chakraborty、Bhargavi Paranjape、Sourya Kakarla 和 Niloy Ganguly。“阻止点击诱饵:检测和防止在线新闻媒体中的点击诱饵”。2016 年美国旧金山 2016 年 8 月 IEEE/ACM 社交网络分析和挖掘进展国际会议(ASONAM)论文集。(https://github.com/bhargaviparanjape/clickbait)
- 米(meter 的缩写))Potthast,S . kpsel,B.Stein,M.Hagen,Clickbait Detection (2016)发表于 ECIR 2016 年https://webis . de/downloads/publications/papers/Stein _ 2016 B . pdf
- https://www . kdnugges . com/2016/10/adversarial-validation-explained . html
- 看淡:https://github.com/snipe/downworthy
- dale Chall Easy word list:hTTP://www . readability formulas . com/articles/dale-Chall-readability-word-list . PHP
- terrier Stop word list:https://github . com/terrier-org/terrier-desktop/blob/master/share/Stop word-list . txt
- https://www . vision dummy . com/2014/04/curse-dimensionality-affect-class ification/# The _ curse _ of _ dimensionality _ and _ over fitting
文本数据扩充使您的模型更强大
使用马尔可夫链生成文本数据,以提高模型性能
Photo by Julienne Erika Alviar on Unsplash
文本分类算法对训练中存在的多样性极其敏感。一个健壮的 NLP 流水线必须考虑到低质量数据存在的可能性,并试图以最好的方式解决这个问题。
处理图像时,加强分类算法和引入多样性的标准方法是操作数据扩充。现在有很多漂亮和聪明的技术来操作自动图像增强。在自然语言处理任务中,文本数据扩充的方法并不常见,结果也不明确。
在这篇文章中,我将展示一个简单直观的技术来执行文本数据生成。使用马尔可夫链规则,我们将能够生成新的文本样本来填充我们的模型并测试其性能。
数据集
我从卡格尔那里得到了我们实验的数据。优步骑行评论数据集是 2014-2017 年期间发布的骑行评论集,从网上搜集而来。在里面,我们可以找到原始的文字评论、用户给出的乘坐评级(1-5)和乘坐感受(如果评级高于 3:感受为 1,否则为 0)。如你所见,这是一个不平衡的分类问题,骑行评论的分布偏向于正面评价。
Label Distributions: the reviews with ‘3 stars Ride Rating’ are excluded from the analysis
首先,我们的目标是预测适合和尝试不同架构的评论的情绪。最有趣的一点发生在第二阶段,我们想给我们的模型施加压力;即我们让他们预测一些用马尔可夫链随机产生的虚假数据。我们想测试我们的模型是否足够稳定,以实现来自列车的足够的性能预测数据(在添加一些噪声之后)。如果一切正常,我们的模型应该不会有问题,在这个假数据上产生良好的结果,并有望提高测试性能,相反,我们需要重新审视训练过程。
文本数据扩充
在开始训练程序之前,我们必须生成我们的假数据。都开始研究火车上复习长度的分布。
Review Length Distribution
我们必须存储这些信息,因为我们的新评论将有类似的长度分布。生成过程由两个阶段组成。第一种,我们“构建链”,即,我们接收文本集合(在我们的情况下是训练语料库)作为输入,并自动为每个单词记录语料库中存在的每个可能的后续单词。在第二阶段,我们可以简单地基于之前的链创建新的评论…我们从起始语料库的整个词汇中随机选择一个词(我们评论的开始),并随机选择下面的新词进入其链。在这个决定的最后,我们准备从新选择的单词重新开始这个过程。一般来说,我们在模拟一个马尔可夫链过程,在这个过程中,为了建立一个新的评论,一个词的选择仅仅基于前一个词。
我在一个独特的函数(生成器)中集合了这两个阶段。该函数接收文本评论作为输入,带有相关标签,以及要生成的新实例的期望前缀数量(针对每个类)。原始长度分布是有用的,因为我们可以从中抽取评论的合理长度。
def **build_chain**(texts):
index = 1
chain = {}
for text in texts:
text = text.split()
for word in text[index:]:
key = text[index-1]
if key in chain:
chain[key].append(word)
else:
chain[key] = [word]
index += 1
index = 1
return chaindef **create_sentence**(chain, lenght):
start = random.choice(list(chain.keys()))
text = [start] while len(text) < lenght:
try:
after = random.choice(chain[start])
start = after
text.append(after)
except: #end of the sentence
#text.append('.')
start = random.choice(list(chain.keys()))
return ' '.join(text)def **Generator**(x_train, y_train, rep, concat=False, seed=33):
np.random.seed(seed)
new_corpus, new_labels = [], []
for i,lab in enumerate(np.unique(y_train)): selected = x_train[y_train == lab]
chain = build_chain(selected) sentences = []
for i in range(rep):
lenght = int(np.random.choice(lenghts, 1, p=freq))
sentences.append(create_sentence(chain, lenght)) new_corpus.extend(sentences)
new_labels.extend([lab]*rep)
if concat:
return list(x_train)+new_corpus, list(y_train)+new_labels
return new_corpus, new_labels
我们需要带标签的文本作为输入,因为我们将生成过程分成不同的子过程:来自特定类的评论被选择来为同一类生成新的评论;因此,我们需要区分构建链和采样过程,以便为我们的预测模型生成真实的样本。
Example of randomly generated reviews. Don’t care about their literally meaning
模特们
我们在训练和测试中分割初始数据集。我们用火车作为语料库来支持我们的生成器,并创建新的评论。我们生成 200 个(每个类 100 个)评论以形成新的独立测试集,并生成 600 个(每个类 300 个)评论以加强我们的训练集。我们的模型库由一个层感知器神经网络、一个逻辑回归和一个随机森林组成。培训过程分为两个阶段。首先,我们用原始训练拟合所有模型,并分别在测试和伪测试数据上检查性能。我们期望所有的模型都优于假测试数据,因为它们是从训练中生成的。其次,我们用强化训练重复我们的模型的拟合,并在我们的测试集上检查性能。
Performace report on the true test set
Performace report on the fake test set
在第一阶段,测试数据的最佳模型是神经网络(AUC、precision、recall 和 f1 被报告为性能指标),但令人惊讶的是,逻辑回归和随机森林在假测试中失败了!这表明我们的模型不太合适。我们再次尝试拟合模型,但这次我们使用强化训练集。在这一点上,所有模型在原始测试中的性能都有所提高,现在它们也开始在假数据上进行很好的推广。
摘要
在这篇文章中,我组装了一个简单的程序来生成假文本数据。当我们安装一个 NLP 分类器并想测试它的强度时,这种技术对我们很有用。如果我们的模型不能很好地分类来自训练的虚假数据,那么重新访问训练过程、调整超参数或直接在训练中添加这些数据是合适的。
保持联系: Linkedin
文本编码研究综述
执行任何文本挖掘操作(如主题检测或情感分析)的关键是将单词转换成数字,将单词序列转换成数字序列。一旦我们有了数字,我们就回到了众所周知的数据分析游戏中,机器学习算法可以帮助我们进行分类和聚类。
在这里,我们将重点放在将单词转换成数字和将文本转换成数字向量的分析部分:文本编码。
对于文本编码,有几种可用的技术,每一种都有自己的优缺点,并且每一种都最适合特定的任务。最简单的编码技术不保留词序,而其他编码技术可以。一些编码技术快速而直观,但是产生的文档向量的大小随着字典的大小而快速增长。其他编码技术优化了向量维度,但损失了可解释性。让我们来看看最常用的编码技术。
1。一键或频繁文档矢量化(未订购)
一种常用的文本编码技术是文档矢量化。这里,从文档集合中所有可用的单词构建一个字典,每个单词成为向量空间中的一列。每个文本都变成了 0 和 1 的向量。1 表示单词存在,0 表示单词不存在。文档的这种数字表示被称为一键文档矢量化。
这种一键矢量化的变体使用文档中每个单词的频率,而不仅仅是它的存在/不存在。这种变化被称为基于频率的矢量化。
虽然这种编码易于解释和生成,但它有两个主要缺点。它不保留文本中的词序,最终向量空间的维数随着词词典快速增长。
例如,在考虑否定或语法结构时,文本中单词的顺序很重要。另一方面,一些更原始的 NLP 技术和机器学习算法可能无论如何都不会利用词序。
此外,向量空间的快速增长可能只会成为大型字典的问题。并且即使在这种情况下,例如通过从文档文本中清除和/或提取关键词,也可以将字数限制到最大值。
2。一键编码(有序)
一些机器学习算法可以在序列中建立项目的内部表示,如句子中的有序单词。例如,递归神经网络(RNNs)和 LSTM 层可以利用序列顺序来获得更好的分类结果。
在这种情况下,我们需要从一键文档矢量化转移到一键编码,其中保留了单词顺序。这里,文档文本再次由单词存在/不存在的向量表示,但是单词被顺序地输入到模型中。
当使用独热编码技术时,每个文档由一个张量表示。每个文档张量由可能非常长的 0/1 向量序列组成,导致文档语料库的非常大且非常稀疏的表示。
3。基于索引的编码
另一种保留单词在句子中出现的顺序的编码是基于索引的编码。基于索引的编码背后的思想是将每个单词映射到一个索引,即一个数字。
第一步是创建一个将单词映射到索引的字典。基于这个字典,每个文档通过一系列索引(数字)来表示,每个数字编码一个单词。基于索引的编码的主要缺点是它在文本之间引入了一个实际上并不存在的数字距离。
注意,基于索引的编码允许不同长度的文档向量。事实上,索引序列的长度是可变的,而文档向量的长度是固定的。
4。单词嵌入
我们想探索的最后一种编码技术是单词嵌入。单词嵌入是一系列自然语言处理技术,旨在将语义映射到几何空间。1 这是通过将数字向量与字典中的每个单词相关联来实现的,因此任意两个向量之间的距离将捕获两个关联单词之间的部分语义关系。由这些向量形成的几何空间称为嵌入空间。最著名的单词嵌入技术是 Word2Vec 和 GloVe。
实际上,我们将每个单词投影到一个连续的向量空间,由一个专用的神经网络层产生。神经网络层学习关联每个单词的矢量表示,这有利于其整体任务,例如周围单词的预测。2
辅助预处理技术
许多机器学习算法需要固定长度的输入向量。通常,最大序列长度被定义为文档中允许的最大字数。较短的文档用零填充。较长的文档会被截断。零填充和截断是文本分析的两个有用的辅助准备步骤。
补零意味着根据需要添加尽可能多的零,以达到允许的最大字数。
截断是指在达到最大字数后切断所有单词。
总结
我们探讨了四种常用的文本编码技术:
- 文档矢量化
- 一键编码
- 基于索引的编码
- 单词嵌入
文档矢量化是唯一不保留输入文本中单词顺序的技术。但是,它很容易解释,也很容易生成。
一键编码是在保留序列中的单词顺序和保持结果的易解释性之间的折衷。要付出的代价是一个非常稀疏,非常大的输入张量。
基于索引的编码试图通过将每个单词映射到一个整数索引并将索引序列分组到一个集合类型列中来解决输入数据大小减小和序列顺序保持的问题。
最后,单词嵌入将基于索引的编码或一键编码投影到具有较小维度的新空间中的数值向量中。新空间由深度学习神经网络中嵌入层的数值输出来定义。这种方法的额外优点包括具有相似角色的单词的紧密映射。当然,缺点是复杂程度更高。
我们希望我们已经对当前可用的文本编码技术提供了足够全面和完整的描述,以便您选择最适合您的文本分析问题的技术。
参考文献
1 Chollet,Francois " 在 Keras 模型中使用预先训练的单词嵌入",Keras 博客,2016 年
2 Brownlee,Jason " 如何使用单词嵌入层与 Keras 进行深度学习,《机器学习之谜》,2017
首次发表于 数据科学中心。
深度学习的文本匹配
在我们的日常生活中,我们总是想知道它们是否是相似的东西。典型的例子之一是 Face ID。苹果推出了一个面部识别系统,用于解锁你的 iPhone X。你必须拍几张照片作为黄金图像。当你想解锁你的 iPhone,iPhone 计算当前照片是否匹配预定义的照片。
Photo by Edward Ma on Unsplash
在以前的博客中,我分享了使用单词存在测量和 WMD 来计算两个句子之间的差异。与以前的方法不同,我们应用神经网络来解决同样的问题。
看完这篇文章,你会明白:
- 计算句子相似度的原因
- 曼哈顿 LSTM
- 曼哈顿 LSTM 变体
计算句子相似度的原因
“black clothes hanged in rack” by The Creative Exchange on Unsplash
除了图像领域,我们能在自然语言处理领域应用相似性检查吗?作为 Stack Overflow 这样的论坛所有者,你不希望有很多重复的问题,因为这会损害用户体验。当从搜索引擎中搜索某个东西时,你会发现搜索结果包含一些相似的东西,但不仅仅是你输入的内容。
根据我的项目经验,我利用这种方法来比较客户名称。由于某些原因,输入是模糊的,模型必须为应用程序找到最相似的客户名称。
曼哈顿 LSTM
“New York street during daytime” by Aaron Sebastian on Unsplash
Muelle 等人在 2016 年提出了用于学习句子相似性的曼哈顿 LSTM 架构。曼哈顿 LSTM 的总体目标是比较两个句子,以确定它们是否相同。这种神经网络结构包括两个相同的神经网络。两个输入通过相同的神经网络(共享权重)。
首先,将两个句子转换为向量表示(即嵌入),然后将其传递给神经网络。两个向量表示将进入两个子神经网络(共享权重)。与其他语言模型 RNN 架构不同,它不预测下一个单词,而是计算两个句子之间的相似度。
在实验过程中,Muelle 等人使用:
- 矢量: word2vec
- 词向量维数:300
- 损失函数:均方误差(MSE)
- 优化器:Adadelta
- LSTM 单位数量:50
曼哈顿 LSTM 变体
这个概念是,你可以建立任何简单或复杂的神经网络,只要它接受两个输入。根据我的经验,你可以尝试任何更复杂的曼哈顿 LSTM 神经网络。我还包括了额外的单词特性和其他 RNN 架构,如 GRU 或注意力机制。
拿走
- 准备大量带标签的数据很重要
- 总计算时间可能很长。对于我的情况,我必须在预测时比较所有客户名称(> 5M)。因此,我不得不使用其他方法来减少记录数量,使其能够满足在线预测的要求。
关于我
我是湾区的数据科学家。专注于数据科学、人工智能,尤其是 NLP 和平台相关领域的最新发展。你可以通过媒体博客、 LinkedIn 或 Github 联系我。
参考
Keras 实施:https://github.com/likejazz/Siamese-LSTM
Thyagarajan muelle j …“用于学习句子相似性的暹罗循环结构”。2016.hTTP://www . MIT . edu/~ jonasm/info/MuellerThyagarajan _ aaai 16 . pdf
放大图片作者:Koch G …“用于一次性图像识别的连体神经网络”。2015.http://www.cs.cmu.edu/~rsalakhu/papers/oneshot1.pdf
民主辩论中的文本挖掘
用 R 分析文本的简短介绍
到目前为止,已经有 6 场正式的民主辩论,未来还会有更多。辩论时间很长,坦率地说非常无聊。辩论的目的是向美国介绍候选人,了解他们的政策立场。然而,美国人的注意力持续时间很短,只有 3 个小时,所以花在看电视上的时间很多。总的来说,一个人会花 18 个小时观看辩论。这根本不现实,必须有更好的方法来客观地总结辩论。
文本挖掘简介
文本挖掘是从文本中提取信息的实践。诸如 n-grams、单词包和主题建模等技术是文本挖掘中的一些基本技术。我相信文本挖掘将是一个比机器学习更重要的领域,因为社交媒体与商业的整合越来越多。我们生活在一个收集和存储数据极其容易和廉价的时代。企业现在正在探索处理和理解数据的方法,这导致了机器学习、深度学习和人工智能。然而,非结构化数据正在增长,尤其是文本数据。消费者评论、推文和电子邮件是企业希望利用的非结构化数据的例子。所有这些听起来极其复杂;非结构化数据的机器学习?理解语言的神经网络?怎么可能呢?信不信由你,从文本中提取真知灼见可以像计算字数一样简单。文本挖掘最难的部分是收集数据。
幸运的是,每次辩论后,新闻机构都会发布辩论记录。我下载了文字记录,并把它整理成一个简单的 csv 文件,可以在这里找到。R 和 Python 都有很多文本挖掘包。我将使用 TidyText,它有一本很棒的教科书来学习文本的基础知识,这篇文章基本上是技术的总结。
N-grams 和单词袋
N-grams 和词袋是大多数文本挖掘技术的基础。他们背后的想法是计算每个单词。鉴于句子“我喜欢狗。”单词袋特征将如下所示。
现在让我们看看有了单词袋功能的两个句子会是什么样子。
请注意,对于“我喜欢猫”,狗的功能计数为 0。单词袋功能有助于将文本数据映射成传统的数字格式。
N-grams 就像是单词袋,只不过多了一个步骤。N-grams 不是像单词袋那样计算一个单词,而是指定有多少个单词。n 是字数。所以,2-grams 或" bigrams "是两个词。让我们用前面的句子来看看一个二元模型的特征是什么样的。
看表的时候和单词袋表看起来没什么区别。然而, bigrams 添加了更多的上下文,我们可以看到狗和猫像一样与一词配对。这意味着我们可以看到,这两个句子对这两种动物都是积极的。你甚至可以进一步使用 n 元语法,如三元语法、四元语法、五元语法等。N-grams 添加上下文,但是,可以开始变得非常具体。这可能是件好事,也可能是件坏事。
R 中的 N-grams 和词袋特征工程
可以使用正则表达式函数生成 N-gram 和单词包,但是正则表达式看起来很乱,而且有一些包可以完成大部分的跑腿工作。Tidytext 包含一个名为**的函数 unnest_tokens。**记号在文本挖掘中被提到很多。标记是文本挖掘的输入,因此单词包和 n 元语法都将被视为标记。
首先,我们将加载包并进行一些格式化。数据集中有三列;辩论、性格和文本。我从记录中过滤掉了版主和其他不知名的人,这样数据只包含总统候选人。
library(tidyverse)
library(tidytext)df <- read.csv("DemDebates.csv", colClasses = c("factor","factor","character"))df <- df %>%
filter(!character %in% c("(Unknown)", "Announcer","Bash","Bridgewater","Burnett","Cooper","Davis","Diaz-Balart","Guthrie","Holt","Jose Diaz-Balart","Lacey","Lemon","Lester Holt", "Maddow","Muir","Protesters","Protestor","Ramos","Savannah Guthrie","Stephanopoulos","Tapper","Todd","Unknown"))
现在我们将使用 unnest_tokens 函数提取单词包。第一个输入命名令牌,第二个输入是包含文本的列,第三个输入指定令牌的类型,最后一个输入是 n 元语法的 n。因此,对于单词包,我们将指定 n = 1。
df %>% unnest_tokens(word, "text", token = "ngrams", n =1)
现在我们有了单词,我们需要计算它们。这是一个简单的计数函数,我们将按字符计数。我还将筛选出每个候选人的前 5 个单词,并按候选人和词频排序。所有这些都将在一个命令中完成,但是,您可以将单词包分配给一个变量。
df %>%
unnest_tokens(word, "text", token = "ngrams", n =1) %>%
count(character, word) %>%
group_by(character) %>%
top_n(n, n = 5) %>%
arrange(character, -n)
如你所见,每个候选人的前 5 个单词并不十分有用。列表中充满了几乎没有意义的单词。这些词被称为停用词,通常会被过滤掉。tidytext 包包含一个频繁停用词的列表,我们可以用它来过滤单词包列表。这将通过来自 dplyr 的 anti_join 来完成,尽管您也可以使用过滤器功能。Tidytext 将它的许多功能与 tidyverse 集成在一起,使它非常直观。
df %>%
unnest_tokens(word, "text", token = "ngrams", n =1) %>%
anti_join(stop_words) %>%
count(character, word) %>%
group_by(character) %>%
top_n(n, n = 5) %>%
arrange(character, -n)
现在一些有趣的事情正在发生。这些词看起来有些相关,可以给每个候选人一些见解。让我们和候选人一起画单词。我们将使用 ggplot 并制作一个简单的条形图,显示每个候选人的前 5 个单词及其频率。我还将使用 facet_wrap ,为一列中的每个值创建一个单独的图。这将为每个候选人创建单独的图表。绘制单词或分类变量的一个常见问题是排列它们,但是 tidytext 提供了帮助这个过程的函数。 reorder_within 排列单词,并在使用 facet_wrap 时帮助保持排序。此外scale _ x _ recordered需要与它一起使用来格式化 x 轴。
df %>%
unnest_tokens(word, "text", token = "ngrams", n =1) %>%
anti_join(stop_words) %>%
count(character, word) %>%
group_by(character) %>%
top_n(n, n = 5) %>%
arrange(character, -n) %>%
ggplot(aes(x = reorder_within(word, n, character),#Reorders word by freq
y = n,
fill = character)) +
geom_col() +
scale_x_reordered() + #Reorders the words
facet_wrap(~character, scales ="free") + #Creates individual graphs
coord_flip() +
theme(legend.position = "None")
这个图表非常好,揭示了当前候选人的很多情况。民主党人在谈论人民、总统、特朗普、美国、政府、医疗保健、气候等。所有这些都是有道理的,但是,所有的候选人都使用相同的单词,并且没有解释每个候选人的细节。让我们看看每个候选人的二元模型。
虽然您应该将令牌重命名为 bigram,但是我们可以将 n = 1 改为 n = 2。看到用 tidytext 看 n 元图有多简单了吧?
df %>%
unnest_tokens(word, "text", token = "ngrams", n =2) %>%
anti_join(stop_words) %>%
count(character, word) %>%
group_by(character) %>%
top_n(n, n = 5) %>%
arrange(character, -n) %>%
ggplot(aes(x = reorder_within(word, n, character),#Reorders the words by freq
y = n,
fill = character)) +
geom_col() +
scale_x_reordered() + #Reorders the words
facet_wrap(~character, scales ="free") + #Creates individual graphs
coord_flip() +
theme(legend.position = "None")
好吧,也许没那么简单。anti_join 命令正在寻找单字或单词,而不是双字。我们可以通过使用分离和联合功能进行过滤。我们只是创建两列,包含二元模型的单词 1 和单词 2,如果其中一个单词包含停用词,就过滤掉二元模型。
df %>%
unnest_tokens(word, "text", token = "ngrams", n =2) %>%
separate(word, c("word1","word2"), sep = " ") %>%
filter(!word1 %in% stop_words$word | !word2 %in% stop_words$word) %>%
unite("bigram", c(word1, word2), sep = " ") %>%
count(character,bigram) %>%
group_by(character) %>%
top_n(n, n = 5) %>%
arrange(character, -n) %>%
ggplot(aes(x = reorder_within(bigram, n, character),#Reorders the words by freq
y = n,
fill = character)) +
geom_col() +
scale_x_reordered() + #Reorders the words
facet_wrap(~character, scales ="free") + #Creates individual graphs
coord_flip() +
theme(legend.position = "None")
正如你所看到的,二元模型对每个候选人来说信息更丰富。贝托谈论埃尔帕索,桑德斯谈论全民医疗保险。我们还可以评估这些辩论,看看它们之间是否有什么主题。
df %>%
unnest_tokens(bigram, "text", token = "ngrams", n =2) %>%
separate(bigram, c("word1","word2"), sep = " ") %>%
filter(!word1 %in% stop_words$word | !word2 %in% stop_words$word) %>%
unite("bigram", c("word1", "word2"), sep = " ") %>%
count(debate, bigram) %>%
group_by(debate) %>%
top_n(n, n =5) %>%
ggplot(aes(x = reorder_within(bigram, n, debate),
y = n,
fill = debate)) +
geom_col() +
scale_x_reordered() +
facet_wrap(~debate, scales = "free") +
coord_flip() +
theme(legend.position = "None")
起初,图表显示没有差异,事实上,二元模型和排名非常相似。**这是简单计算 n-grams 或词袋的缺点之一。**没有考虑到流行词。你可以为每个文档创建一个单独的流行词列表,然后把它们过滤掉,但这要花很多时间。这里是 TF-IDF 大放异彩的地方。
术语频率和反向文档频率(TF-IDF)
术语频率和逆文档频率是我在组内计算 n-grams 或词袋的首选方法。TF-IDF 通过将一个组的单词与整个文档集进行比较,为每个组找到唯一的单词。数学也很简单。
https://skymind.ai/wiki/bagofwords-tf-idf
Tidytext 包含一个 Tf-idf 函数,因此您不需要手动创建公式。Tf-idf 的伟大之处在于,从技术上讲,您不需要删除停用词,因为停用词将在所有文档中共享。让我们看看它的实际效果。
df %>%
unnest_tokens(word, "text", token = "ngrams", n =1) %>%
count(debate, word) %>%
bind_tf_idf(word, debate, n) %>%
group_by(debate) %>%
top_n(tf_idf, n = 5) %>%
ggplot(aes(x = reorder_within(word, tf_idf, debate), y = tf_idf, fill = debate)) +
geom_col() +
scale_x_reordered() +
facet_wrap(~debate, scales = "free") +
coord_flip() +
theme(legend.position = "none")
Bind_tf_idf 跟随计数函数。 Bind_tf_idf 然后将一个术语、文档和 n
所以,图表显示了许多不同的单词,有些单词没有任何意义。Tf-idf 确实有所帮助,但是有些词根本没有增加任何价值,比如在辩论 4 和 2a 中。为什么有个“h”?可能是因为在可能有不需要的空间的地方清理了数据。但是我们仍然可以看到非常清晰的文字。**虽然 Tf-idf 确实去掉了很多停用词,但还是去掉比较好。**让我们来看看 tf-idf 为我们展示了每一位候选人。
df %>%
unnest_tokens(word, "text", token = "ngrams", n =1) %>%
count(character, word) %>%
bind_tf_idf(word, character, n) %>%
group_by(character) %>%
top_n(tf_idf, n = 5) %>%
ggplot(aes(x = reorder_within(word, tf_idf, character), y = tf_idf, fill = character)) +
geom_col() +
scale_x_reordered() +
facet_wrap(~character, scales = "free") +
coord_flip() +
theme(legend.position = "none")
这很有趣。前 5 个 tf-idf 单词确实揭示了每个候选人的很多信息。杨谈 1000 指的是他的自由红利。贝托谈到了德克萨斯和帕索,这很有道理,因为那是他的家乡。Eric Swallwell 用 Torch 来形容他是最容易辨认的,这个词指的是臭名昭著的“传递火炬”演讲。即使看着 x 轴火炬也有最高值,这表明它能很好地识别候选人。Tf-idf 也可以应用于任何 n 元文法。您可能已经注意到,第一个图表显示了每个候选人的前 5 个三元模型。
df %>%
unnest_tokens(trigram, "text", token = "ngrams", n = 3) %>%
count(character, trigram) %>%
bind_tf_idf(trigram, character,n) %>%
group_by(character) %>%
top_n(tf_idf, n = 5) %>%
mutate(rank = rank(tf_idf, ties.method = "random")) %>%
arrange(character, rank) %>%
filter(rank <=5) %>%
ggplot(aes(x = reorder_within(trigram, tf_idf, character),
y = tf_idf,
color = character,
fill = character)) +
geom_col() +
scale_x_reordered() +
facet_wrap(~character, scales = "free") +
coord_flip() +
theme(legend.position = "None")
结论
词袋和 n 元语法是文本挖掘和其他 NLP 主题的基础。这是将文本转换成数字特征的一种非常简单而优雅的方式。此外,tf-idf 是另一个可以增加单词袋和 n 元语法有效性的工具。这些基本概念可以带你深入文本挖掘的世界。考虑使用词袋和 n-grams 创建一个模型,尝试 tf-idf 如何改变模型的准确性,使用词典将情感值应用于词袋,或者使用二元模型进行否定。与数据挖掘中的其他领域相比,文本挖掘是一个相对未开发的领域。然而,随着智能手机、在线业务和社交媒体的兴起,将需要文本挖掘来处理每天发布、共享和转发的数百万条文本。
数据科学家的文本预处理
文本预处理的简便指南
Image by Devanath from Pixabay
文本预处理
文本预处理是文本分析和自然语言处理的重要任务和关键步骤。它将文本转换为可预测和可分析的形式,以便机器学习算法可以更好地执行。这是一个方便的文本预处理指南,是我之前关于文本挖掘的博客的延续。在这篇博客中,我使用了来自 Kaggle 的 twitter 数据集。
有不同的方法来预处理文本。这里有一些你应该知道的常用方法,我会试着强调每种方法的重要性。
Image by the author
密码
**#Importing necessary libraries**import numpy as np
import pandas as pd
import re
import nltk
import spacy
import string**# Reading the dataset**df = pd.read_csv("sample.csv")
df.head()
输出
下部外壳
这是最常见和最简单的文本预处理技术。适用于大多数文本挖掘和 NLP 问题。主要目标是将文本转换为小写,以便“apple”、“APPLE”和“Apple”得到相同的处理。
密码
**# Lower Casing --> creating new column called text_lower**df['text_lower'] = df['text'].str.lower()
df['text_lower'].head()
输出
0 @applesupport causing the reply to be disregar...
1 @105835 your business means a lot to us. pleas...
2 @76328 i really hope you all change but i'm su...
3 @105836 livechat is online at the moment - htt...
4 @virgintrains see attached error message. i've...
Name: text_lower, dtype: object
删除标点符号
密码
**#removing punctuation, creating a new column called 'text_punct]'**
df['text_punct'] = df['text'].str.replace('[^\w\s]','')
df['text_punct'].head()
输出
0 applesupport causing the reply to be disregard...
1 105835 your business means a lot to us please ...
2 76328 I really hope you all change but im sure...
3 105836 LiveChat is online at the moment https...
4 virginTrains see attached error message Ive tr...
Name: text_punct, dtype: object
停用词删除
停用词是一种语言中的一组常用词。英语中停用词的例子有“a”、“we”、“the”、“is”、“are”等。使用停用词背后的想法是,通过从文本中删除低信息量的词,我们可以专注于重要的词。我们可以自己创建一个自定义的停用词列表(基于用例),也可以使用预定义的库。
密码
**#Importing stopwords from nltk library**
from nltk.corpus import stopwords
STOPWORDS = set(stopwords.words('english'))**# Function to remove the stopwords**
def stopwords(text):
return " ".join([word for word in str(text).split() if word not in STOPWORDS])**# Applying the stopwords to 'text_punct' and store into 'text_stop'**
df["text_stop"] = df["text_punct"].apply(stopwords)
df["text_stop"].head()
输出
0 appleSupport causing reply disregarded tapped ...
1 105835 your business means lot us please DM na...
2 76328 I really hope change Im sure wont becaus...
3 105836 LiveChat online moment httpstcoSY94VtU8...
4 virgintrains see attached error message Ive tr...
Name: text_stop, dtype: object
常用词去除
我们还可以从文本数据中删除常见的单词。首先,让我们检查一下文本数据中最常出现的 10 个单词。
密码
**# Checking the first 10 most frequent words**
from collections import Counter
cnt = Counter()
for text in df["text_stop"].values:
for word in text.split():
cnt[word] += 1
cnt.most_common(10)
输出
[('I', 34),
('us', 25),
('DM', 19),
('help', 17),
('httpstcoGDrqU22YpT', 12),
('AppleSupport', 11),
('Thanks', 11),
('phone', 9),
('Ive', 8),
('Hi', 8)]
现在,我们可以删除给定语料库中的常用词。如果我们使用 tf-idf,这可以自动处理
密码
**# Removing the frequent words**
freq = set([w for (w, wc) in cnt.most_common(10)])**# function to remove the frequent words**
def freqwords(text):
return " ".join([word for word in str(text).split() if word not
in freq])**# Passing the function freqwords**
df["text_common"] = df["text_stop"].apply(freqwords)
df["text_common"].head()
输出
0 causing reply disregarded tapped notification ...
1 105835 Your business means lot please name zip...
2 76328 really hope change Im sure wont because ...
3 105836 LiveChat online moment httpstcoSY94VtU8...
4 virgintrains see attached error message tried ...
Name: text_common, dtype: object
去除生僻字
这是非常直观的,因为对于不同的 NLP 任务,一些本质上非常独特的词,如名称、品牌、产品名称,以及一些干扰字符,如 html 省略,也需要被删除。我们还使用单词的长度作为标准来删除非常短或非常长的单词
密码
**# Removal of 10 rare words and store into new column called** 'text_rare'
freq = pd.Series(' '.join(df['text_common']).split()).value_counts()[-10:] # 10 rare words
freq = list(freq.index)
df['text_rare'] = df['text_common'].apply(lambda x: " ".join(x for x in x.split() if x not in freq))
df['text_rare'].head()
输出
0 causing reply disregarded tapped notification ...
1 105835 Your business means lot please name zip...
2 76328 really hope change Im sure wont because ...
3 105836 liveChat online moment httpstcoSY94VtU8...
4 virgintrains see attached error message tried ...
Name: text_rare, dtype: object
拼写纠正
社交媒体数据总是杂乱的数据,而且有拼写错误。因此,拼写纠正是一个有用的预处理步骤,因为这将帮助我们避免多个单词。例如,“text”和“txt”将被视为不同的单词,即使它们在相同的意义上使用。这可以通过 textblob 库来完成
代号
**# Spell check using text blob for the first 5 records**
from textblob import TextBlob
df['text_rare'][:5].apply(lambda x: str(TextBlob(x).correct()))
输出
表情符号移除
表情符号是我们生活的一部分。社交媒体文字有很多表情符号。我们需要在文本分析中删除相同的内容
密码
代码参考: Github
**# Function to remove emoji.**
def emoji(string):
emoji_pattern = re.compile("["
u"\U0001F600-\U0001F64F" # emoticons
u"\U0001F300-\U0001F5FF" # symbols & pictographs
u"\U0001F680-\U0001F6FF" # transport & map symbols
u"\U0001F1E0-\U0001F1FF" # flags (iOS)
u"\U00002702-\U000027B0"
u"\U000024C2-\U0001F251"
"]+", flags=re.UNICODE)
return emoji_pattern.sub(r'', string)emoji("Hi, I am Emoji 😜")
**#passing the emoji function to 'text_rare'**
df['text_rare'] = df['text_rare'].apply(remove_emoji)
输出
'Hi, I am Emoji '
表情移除
在前面的步骤中,我们已经删除了表情符号。现在,我要移除表情符号。表情符号和表情符号有什么区别?:-)是一个表情符号😜→表情符号。
使用 emot 库。请参考更多关于表情
密码
from emot.emo_unicode import UNICODE_EMO, EMOTICONS**# Function for removing emoticons**
def remove_emoticons(text):
emoticon_pattern = re.compile(u'(' + u'|'.join(k for k in EMOTICONS) + u')')
return emoticon_pattern.sub(r'', text)remove_emoticons("Hello :-)")
**# applying remove_emoticons to 'text_rare'**
df['text_rare'] = df['text_rare'].apply(remove_emoticons)
输出
'Hello '
将表情符号和表情符号转换为文字
在情感分析中,表情符号和表情符号表达了一种情感。因此,删除它们可能不是一个好的解决方案。
密码
from emot.emo_unicode import UNICODE_EMO, EMOTICONS**# Converting emojis to words**
def convert_emojis(text):
for emot in UNICODE_EMO:
text = text.replace(emot, "_".join(UNICODE_EMO[emot].replace(",","").replace(":","").split()))
return text**# Converting emoticons to words **
def convert_emoticons(text):
for emot in EMOTICONS:
text = re.sub(u'('+emot+')', "_".join(EMOTICONS[emot].replace(",","").split()), text)
return text**# Example**
text = "Hello :-) :-)"
convert_emoticons(text)text1 = "Hilarious 😂"
convert_emojis(text1)**# Passing both functions to 'text_rare'**
df['text_rare'] = df['text_rare'].apply(convert_emoticons)
df['text_rare'] = df['text_rare'].apply(convert_emojis)
输出
'Hello happy smiley face happy smiley face:-)'
'Hilarious face_with_tears_of_joy'
移除 URL
删除文本中的 URL。我们可以使用漂亮的汤库
密码
**# Function for url's**
def remove_urls(text):
url_pattern = re.compile(r'https?://\S+|www\.\S+')
return url_pattern.sub(r'', text)**# Examples**
text = "This is my website, [https://www.abc.com](https://www.abc.com)"
remove_urls(text)**#Passing the function to 'text_rare'**
df['text_rare'] = df['text_rare'].apply(remove_urls)
输出
'This is my website, '
移除 HTML 标签
另一种常见的预处理技术是删除 HTML 标签。通常出现在抓取数据中的 HTML 标签。
密码
from bs4 import BeautifulSoup**#Function for removing html**
def html(text):
return BeautifulSoup(text, "lxml").text
**# Examples**
text = """<div>
<h1> This</h1>
<p> is</p>
<a href="[https://www.abc.com/](https://www.abc.com/)"> ABCD</a>
</div>
"""
print(html(text))
**# Passing the function to 'text_rare'**
df['text_rare'] = df['text_rare'].apply(html)
输出
This
is
ABCD
标记化
标记化是指将文本分成一系列单词或句子。
密码
**#Creating function for tokenization**
def tokenization(text):
text = re.split('\W+', text)
return text
**# Passing the function to 'text_rare' and store into'text_token'**
df['text_token'] = df['text_rare'].apply(lambda x: tokenization(x.lower()))
df[['text_token']].head()
输出
词干化和词汇化
词汇化是将一个词转换成它的基本形式的过程。词干化和词元化的区别在于,词元化考虑上下文并将单词转换为其有意义的基本形式,而词干化只是删除最后几个字符,通常会导致不正确的意思和拼写错误。这里,仅执行了术语化。我们需要为 NLTK 中的 lemmatizer 提供单词的 POS 标签。根据位置的不同,lemmatizer 可能会返回不同的结果。
密码
from nltk.corpus import wordnet
from nltk.stem import WordNetLemmatizerlemmatizer = WordNetLemmatizer()
wordnet_map = {"N":wordnet.NOUN, "V":wordnet.VERB, "J":wordnet.ADJ, "R":wordnet.ADV} # Pos tag, used Noun, Verb, Adjective and Adverb**# Function for lemmatization using POS tag**
def lemmatize_words(text):
pos_tagged_text = nltk.pos_tag(text.split())
return " ".join([lemmatizer.lemmatize(word, wordnet_map.get(pos[0], wordnet.NOUN)) for word, pos in pos_tagged_text])**# Passing the function to 'text_rare' and store in 'text_lemma'**
df["text_lemma"] = df["text_rare"].apply(lemmatize_words)
输出
以上方法是常见的文本预处理步骤。
感谢阅读。请继续学习,并关注更多内容!
参考:
- 【https://www.nltk.org
- https://www.edureka.co
- https://www . geeks forgeeks . org/part-speech-tagging-stop-words-using-nltk-python/
自然语言处理中的文本预处理
文本预处理在模型性能中的意义。
数据预处理是建立机器学习模型的必要步骤,取决于数据预处理的好坏;结果是看到的。
在自然语言处理中,文本预处理是建立模型的第一步。
各种文本预处理步骤是:
- 标记化
- 下部外壳
- 停止单词删除
- 堵塞物
- 词汇化
这些不同的文本预处理步骤被广泛用于降维。
在向量空间模型中,每个单词/术语是一个轴/维度。文本/文档被表示为多维空间中的向量。
唯一字的数量就是维度的数量。
安装:我将用来实现文本预处理任务的 python 库是 nltk
pip install nltk==3.4.5
标记化:将句子拆分成单词。
**Output:** ['Books', 'are', 'on', 'the', 'table']
**小写:**将单词转换成小写(NLP - > nlp)。
像 Book 和 book 这样的单词意思相同,但是当不转换成小写时,这两个单词在向量空间模型中表示为两个不同的单词(导致更多的维度)。
**Output:** books are on the table.
**停用词去除:**停用词是非常常用的词(a、an、the 等。)在文档中。这些词实际上并不表示任何重要性,因为它们无助于区分两个文档。
**Output**: ['Machine', 'Learning', 'cool', '!']
**Explanation**: Stop word ‘is’ has been removed
词干化:是将一个单词转化为其词根形式的过程。
**Output**: machin, learn, is, cool
**Explanation**: The word 'machine' has its suffix 'e' chopped off. The stem does not make sense as it is not a word in English. This is a disadvantage of stemming.
词汇化:与词干化不同,词汇化将单词简化为语言中存在的单词。
词干化或词汇化都可以使用。像 nltk 和 spaCy 这样的库已经实现了词干分析器和词条分类器。这些都是基于基于规则的方法构建的。
斯特梅尔比词条解释器更容易构建,因为后者在构建词典时需要深厚的语言学知识来查找单词的词条。
对于将单词解析为其词条的词条化,需要单词的词性。这有助于将单词转换成合适的词根形式。然而,要做到这一点,它需要额外的计算语言学能力,如词性标注器。
有关 python 中词汇化的更多示例,请查看这个博客,有关词干化和词汇化之间差异的详细解释,请查看这个博客
变元化优于词干化,因为变元化对单词进行词法分析。
**Output**: machine, care
**Explanation**: The word Machine transforms to lowercase and retains the same word unlike Stemming. Also, the word caring is transformed to its lemma 'care' as the parts of speech variable (pos) is verb(v)
总之,这些是自然语言处理中的文本预处理步骤。可以使用各种 python 库,如 nltk 、 spaCy 和 TextBlob 。请参考他们的文档并试用它们。
关于我
我对位于湾区的数据科学充满热情。我的重点是学习自然语言处理的最新技术。请随时在 LinkedIn 上与我联系
参考资料:
[1]https://www . coursera . org/lecture/language-processing/text-预处理-SCd4G
【2】https://www.nltk.org/
文本预处理步骤和通用可重用流水线
所有文本预处理步骤的描述和可重用文本预处理管道的创建
在向任何 ML 模型提供某种数据之前,必须对其进行适当预处理。你一定听过这个谚语:Garbage in, garbage out
(GIGO)。文本是一种特殊的数据,不能直接输入到大多数 ML 模型中,所以在输入到模型中之前,你必须以某种方式从中提取数字特征,换句话说就是vectorize
。矢量化不是本教程的主题,但您必须了解的主要内容是,GIGO 也适用于矢量化,您只能从定性预处理的文本中提取定性特征。
我们将要讨论的事情:
- 标记化
- 清洁
- 正常化
- 词汇化
- 汽蒸
最后,我们将创建一个可重用的管道,您可以在您的应用程序中使用它。
Kaggle 内核:https://www . ka ggle . com/balat mak/text-预处理-步骤-通用-管道
让我们假设这个示例文本:
An explosion targeting a tourist bus has injured at least 16 people near the Grand Egyptian Museum,
next to the pyramids in Giza, security sources say E.U.
South African tourists are among the injured. Most of those hurt suffered minor injuries,
while three were treated in hospital, N.A.T.O. say.
http://localhost:8888/notebooks/Text%20preprocessing.ipynb
@nickname of twitter user and his email is email@gmail.com .
A device went off close to the museum fence as the bus was passing on 16/02/2012.
标记化
Tokenization
——文本预处理步骤,假设将文本分割成tokens
(单词、句子等)。)
看起来你可以使用某种简单的分隔符来实现它,但是你不要忘记,在许多不同的情况下,分隔符是不起作用的。例如,如果您使用带点的缩写,那么.
用于将标记化成句子的分隔符将会失败。所以你必须有一个更复杂的模型来达到足够好的结果。通常这个问题可以通过使用nltk
或spacy
nlp 库来解决。
NLTK:
from nltk.tokenize import sent_tokenize, word_tokenize
nltk_words = word_tokenize(example_text)
display(f"Tokenized words: **{nltk_words}**")
输出:
Tokenized words: ['An', 'explosion', 'targeting', 'a', 'tourist', 'bus', 'has', 'injured', 'at', 'least', '16', 'people', 'near', 'the', 'Grand', 'Egyptian', 'Museum', ',', 'next', 'to', 'the', 'pyramids', 'in', 'Giza', ',', 'security', 'sources', 'say', 'E.U', '.', 'South', 'African', 'tourists', 'are', 'among', 'the', 'injured', '.', 'Most', 'of', 'those', 'hurt', 'suffered', 'minor', 'injuries', ',', 'while', 'three', 'were', 'treated', 'in', 'hospital', ',', 'N.A.T.O', '.', 'say', '.', 'http', ':', '//localhost:8888/notebooks/Text', '%', '20preprocessing.ipynb', '@', 'nickname', 'of', 'twitter', 'user', 'and', 'his', 'email', 'is', 'email', '@', 'gmail.com', '.', 'A', 'device', 'went', 'off', 'close', 'to', 'the', 'museum', 'fence', 'as', 'the', 'bus', 'was', 'passing', 'on', '16/02/2012', '.']
空间:
import spacy
import en_core_web_sm
nlp = en_core_web_sm.load()
doc = nlp(example_text)
spacy_words = [token.text for token **in** doc]
display(f"Tokenized words: **{spacy_words}**")
输出:
Tokenized words: ['\\n', 'An', 'explosion', 'targeting', 'a', 'tourist', 'bus', 'has', 'injured', 'at', 'least', '16', 'people', 'near', 'the', 'Grand', 'Egyptian', 'Museum', ',', '\\n', 'next', 'to', 'the', 'pyramids', 'in', 'Giza', ',', 'security', 'sources', 'say', 'E.U.', '\\n\\n', 'South', 'African', 'tourists', 'are', 'among', 'the', 'injured', '.', 'Most', 'of', 'those', 'hurt', 'suffered', 'minor', 'injuries', ',', '\\n', 'while', 'three', 'were', 'treated', 'in', 'hospital', ',', 'N.A.T.O.', 'say', '.', '\\n\\n', 'http://localhost:8888/notebooks', '/', 'Text%20preprocessing.ipynb', '\\n\\n', '@nickname', 'of', 'twitter', 'user', 'and', 'his', 'email', 'is', 'email@gmail.com', '.', '\\n\\n', 'A', 'device', 'went', 'off', 'close', 'to', 'the', 'museum', 'fence', 'as', 'the', 'bus', 'was', 'passing', 'on', '16/02/2012', '.', '\\n']
在 spacy 输出标记化中,而不是在 nltk 中:
{'E.U.', '\\n', 'Text%20preprocessing.ipynb', 'email@gmail.com', '\\n\\n', 'N.A.T.O.', 'http://localhost:8888/notebooks', '@nickname', '/'}
在 nltk 中但不在 spacy 中:
{'nickname', '//localhost:8888/notebooks/Text', 'N.A.T.O', ':', '@', 'gmail.com', 'E.U', 'http', '20preprocessing.ipynb', '%'}
我们看到spacy
标记了一些奇怪的东西,比如\n
、\n\n
,但是能够处理 URL、电子邮件和类似 Twitter 的提及。此外,我们看到nltk
标记化的缩写没有最后的.
清洁
Cleaning
is 步骤假设删除所有不需要的内容。
删除标点符号
当标点符号不能为文本矢量化带来附加值时,这可能是一个好的步骤。标点符号删除最好在标记化步骤之后进行,在此之前进行可能会导致不良影响。TF-IDF
、Count
、Binary
矢量化的好选择。
让我们假设这一步的文本:
@nickname of twitter user, and his email is email@gmail.com .
在标记化之前:
text_without_punct = text_with_punct.translate(str.maketrans('', '', string.punctuation))
display(f"Text without punctuation: **{text_without_punct}**")
输出:
Text without punctuation: nickname of twitter user and his email is emailgmailcom
在这里,您可以看到用于正确标记化的重要符号已被删除。现在电子邮件无法正常检测。正如您在Tokenization
步骤中提到的,标点符号被解析为单个符号,所以更好的方法是先进行符号化,然后删除标点符号。
import spacy
import en_core_web_sm
nlp = en_core_web_sm.load()doc = nlp(text_with_punct)
tokens = [t.text for t **in** doc]*# python based removal*
tokens_without_punct_python = [t for t **in** tokens if t **not** **in** string.punctuation]
display(f"Python based removal: **{tokens_without_punct_python}**")# spacy based removal
tokens_without_punct_spacy = [t.text for t **in** doc if t.pos_ != 'PUNCT']
display(f"Spacy based removal: **{tokens_without_punct_spacy}**")
基于 Python 的移除结果:
['@nickname', 'of', 'twitter', 'user', 'and', 'his', 'email', 'is', 'email@gmail.com']
基于空间的移除:
['of', 'twitter', 'user', 'and', 'his', 'email', 'is', 'email@gmail.com']
这里你可以看到python-based
移除比 spacy 更有效,因为 spacy 将@nicname
标记为PUNCT
词性。
停止单词删除
Stop words
通常指一种语言中最常见的词,通常不会带来额外的意义。没有一个所有 nlp 工具都使用的通用停用词列表,因为这个术语的定义非常模糊。尽管实践已经表明,当准备用于索引的文本时,这一步骤是必须的,但是对于文本分类目的来说可能是棘手的。
空间停止字数:312
NLTK 停止字数:179
让我们假设这一步的文本:
This movie is just not good enough
空间:
import spacy
import en_core_web_sm
nlp = en_core_web_sm.load()text_without_stop_words = [t.text for t **in** nlp(text) if **not** t.is_stop]
display(f"Spacy text without stop words: **{text_without_stop_words}**")
没有停用词的空白文本:
['movie', 'good']
NLTK:
import nltk
nltk_stop_words = nltk.corpus.stopwords.words('english')
text_without_stop_words = [t for t **in** word_tokenize(text) if t **not** **in** nltk_stop_words]
display(f"nltk text without stop words: **{text_without_stop_words}**")
无停用词的 NLTK 文本:
['This', 'movie', 'good', 'enough']
这里你看到 nltk 和 spacy 的词汇量不一样,所以过滤的结果也不一样。但我想强调的主要一点是,单词not
被过滤了,这在大多数情况下是没问题的,但在你想确定这个句子的极性的情况下not
会带来额外的含义。
对于这种情况,您可以在空间库中设置可以忽略的停用词。在 nltk 的情况下,您可以删除或添加自定义单词到nltk_stop_words
,它只是一个列表。
import en_core_web_sm
nlp = en_core_web_sm.load()
customize_stop_words = [
'not'
]
for w **in** customize_stop_words:
nlp.vocab[w].is_stop = False
text_without_stop_words = [t.text for t **in** nlp(text) if **not** t.is_stop]
display(f"Spacy text without updated stop words: **{text_without_stop_words}**")
没有更新停用词的空白文本:
['movie', 'not', 'good']
正常化
像任何数据一样,文本也需要规范化。如果是文本,则为:
- 将日期转换为文本
- 数字到文本
- 货币/百分比符号到文本
- 缩写扩展(内容相关)NLP —自然语言处理、神经语言编程、非线性编程
- 拼写错误纠正
总而言之,规范化是将任何非文本信息转换成文本等效信息。
为此,有一个很棒的库——normalize。我将从这个库的自述文件中向您展示这个库的用法。这个库基于nltk
包,所以它需要nltk
单词标记。
让我们假设这一步的文本:
On the 13 Feb. 2007, Theresa May announced on MTV news that the rate of childhod obesity had risen from 7.3-9.6**% i**n just 3 years , costing the N.A.T.O £20m
代码:
from normalise import normalise
user_abbr = {
"N.A.T.O": "North Atlantic Treaty Organization"
}
normalized_tokens = normalise(word_tokenize(text), user_abbrevs=user_abbr, verbose=False)
display(f"Normalized text: {' '.join(normalized_tokens)}")
输出:
On the thirteenth of February two thousand and seven , Theresa May announced on M T V news that the rate of childhood obesity had risen from seven point three to nine point six % in just three years , costing the North Atlantic Treaty Organization twenty million pounds
这个库中最糟糕的事情是,目前你不能禁用一些模块,如缩写扩展,它会导致像MTV
- > M T V
这样的事情。但是我已经在这个库上添加了一个适当的问题,也许过一会儿就可以修复了。
脱镁和汽蒸
Stemming
是将单词的词形变化减少到其词根形式的过程,例如将一组单词映射到同一个词干,即使该词干本身在语言中不是有效单词。
Lemmatization
与词干不同,适当减少词根变化,确保词根属于该语言。在引理化中,词根称为引理。一个词条(复数词条或词条)是一组单词的规范形式、词典形式或引用形式。
让我们假设这一步的文本:
On the thirteenth of February two thousand and seven , Theresa May announced on M T V news that the rate of childhood obesity had risen from seven point three to nine point six % in just three years , costing the North Atlantic Treaty Organization twenty million pounds
NLTK 词干分析器:
import numpy as np
from nltk.stem import PorterStemmer
from nltk.tokenize import word_tokenizetokens = word_tokenize(text)
porter=PorterStemmer()# vectorizing function to able to call on list of tokens
stem_words = np.vectorize(porter.stem)stemed_text = ' '.join(stem_words(tokens))
display(f"Stemed text: **{stemed_text}**")
带词干的文本:
On the thirteenth of februari two thousand and seven , theresa may announc on M T V news that the rate of childhood obes had risen from seven point three to nine point six % in just three year , cost the north atlant treati organ twenti million pound
NLTK 术语化:
import numpy as np
from nltk.stem import WordNetLemmatizer
from nltk.tokenize import word_tokenizetokens = word_tokenize(text)wordnet_lemmatizer = WordNetLemmatizer()# vectorizing function to able to call on list of tokens
lemmatize_words = np.vectorize(wordnet_lemmatizer.lemmatize)lemmatized_text = ' '.join(lemmatize_words(tokens))
display(f"nltk lemmatized text: **{lemmatized_text}**")
NLTK 词条化文本:
On the thirteenth of February two thousand and seven , Theresa May announced on M T V news that the rate of childhood obesity had risen from seven point three to nine point six % in just three year , costing the North Atlantic Treaty Organization twenty million pound
空间引理化;
import en_core_web_sm
nlp = en_core_web_sm.load()lemmas = [t.lemma_ for t **in** nlp(text)]
display(f"Spacy lemmatized text: {' '.join(lemmas)}")
Spacy 词条化文本:
On the thirteenth of February two thousand and seven , Theresa May announce on M T v news that the rate of childhood obesity have rise from seven point three to nine point six % in just three year , cost the North Atlantic Treaty Organization twenty million pound
我们看到spacy
比 nltk 好得多,其中一个例子risen
- > rise
,只有spacy
处理了它。
可重复使用管道
现在是我最喜欢的部分!我们将创建一个可重用的管道,您可以在您的任何项目中使用它。
import numpy as np
import multiprocessing as mp
import string
import spacy
import en_core_web_sm
from nltk.tokenize import word_tokenize
from sklearn.base import TransformerMixin, BaseEstimator
from normalise import normalise
nlp = en_core_web_sm.load()
class **TextPreprocessor**(BaseEstimator, TransformerMixin):
def __init__(self,
variety="BrE",
user_abbrevs={},
n_jobs=1):
*"""*
*Text preprocessing transformer includes steps:*
*1\. Text normalization*
*2\. Punctuation removal*
*3\. Stop words removal*
*4\. Lemmatization*
*variety - format of date (AmE - american type, BrE - british format)*
*user_abbrevs - dict of user abbreviations mappings (from normalise package)*
*n_jobs - parallel jobs to run*
*"""*
self.variety = variety
self.user_abbrevs = user_abbrevs
self.n_jobs = n_jobs
def fit(self, X, y=None):
return self
def transform(self, X, *_):
X_copy = X.copy()
partitions = 1
cores = mp.cpu_count()
if self.n_jobs <= -1:
partitions = cores
elif self.n_jobs <= 0:
return X_copy.apply(self._preprocess_text)
else:
partitions = min(self.n_jobs, cores)
data_split = np.array_split(X_copy, partitions)
pool = mp.Pool(cores)
data = pd.concat(pool.map(self._preprocess_part, data_split))
pool.close()
pool.join()
return data
def _preprocess_part(self, part):
return part.apply(self._preprocess_text)
def _preprocess_text(self, text):
normalized_text = self._normalize(text)
doc = nlp(normalized_text)
removed_punct = self._remove_punct(doc)
removed_stop_words = self._remove_stop_words(removed_punct)
return self._lemmatize(removed_stop_words)
def _normalize(self, text):
*# some issues in normalise package*
try:
return ' '.join(normalise(text, variety=self.variety, user_abbrevs=self.user_abbrevs, verbose=False))
except:
return text
def _remove_punct(self, doc):
return [t for t **in** doc if t.text **not** **in** string.punctuation]
def _remove_stop_words(self, doc):
return [t for t **in** doc if **not** t.is_stop]
def _lemmatize(self, doc):
return ' '.join([t.lemma_ for t **in** doc])
此代码可用于 sklearn 管道。
测量的性能:在 22 分钟内在 4 个进程上处理了 2225 个文本。甚至没有接近快!这导致了规范化部分,库没有充分优化,但产生了相当有趣的结果,并可以为进一步的矢量化带来额外的价值,所以是否使用它取决于您。
我希望你喜欢这篇文章,我期待你的反馈!
Kaggle 内核:https://www . ka ggle . com/balat mak/text-预处理-步骤-通用-管道