跳转至

没有模型训练情况下用BERT做文本分类

NLP(Natural Language Processing,自然语言处理)是研究计算机与人类语言之间相互作用的人工智能领域,特别是如何对计算机进行编程,以处理和分析大量的自然语言数据。NLP常被应用于文本数据的分类。文本分类是指根据文本数据的内容对其进行分类的问题。为了进行分类用例,你需要一个标签化的数据集来进行机器学习模型的训练。那么,如果你没有预先分类的数据集,怎么办呢?

这种情况在现实世界中发生的频率比你想象的要高。如今,人工智能被炒得沸沸扬扬,以至于企业即使没有数据也想使用它。特别是,大多数非技术人员并没有完全理解 "目标变量 "的概念,以及它在监督式机器学习中的应用。那么,当你有文本数据但没有标签时,如何建立一个分类器呢?在本教程中,我将解释一种应用W2V和BERT通过词向量相似度对文本进行分类的策略。 我将介绍一些有用的Python代码,这些代码可以很容易地应用在其他类似的情况下(只需复制、粘贴、运行),并通过每一行代码的注释进行演练,以便你可以复制这个例子(链接到下面的完整代码)。

Natural Language Processing - Text Classification example

我将使用 新闻类别数据集,其中提供给你从HuffPost获得的2012年到2018年的新闻标题,并要求你将其归入正确的类别,因此这是一个多类分类问题。

具体来说,我将分以下步骤介绍:
- 设置: 导入包,读取数据。
- 预处理: 清理文本数据。
- 创建目标簇:使用Word2Vec与gensim建立目标变量。
- 特征工程。Word Embedding与变换器和BERT。
- 模型设计与测试:通过余弦相似性将观测值分配到聚类,并评估性能。
- 可解释性:了解模型如何产生结果。

设置

首先,我需要导入以下包:

## for data
import json
import pandas as pd
import numpy as np
from sklearn import metrics, manifold
## for processing
import re
import nltk
## for plotting
import matplotlib.pyplot as plt
import seaborn as sns
## for w2v
import gensim
import gensim.downloader as gensim_api
## for bert
import transformers

数据集包含在 json 文件中,因此我将首先将其读入包含 json 的字典列表,然后将其转换为Panda dataframe。

1
2
3
4
5
6
lst_dics = []
with open('data.json', mode='r', errors='ignore') as json_file:
    for dic in json_file:
        lst_dics.append( json.loads(dic) )
## print the first one
lst_dics[0]

原始数据集包含 30 多个类别,但出于本教程的目的,我将使用 3 个子集:娱乐、政治和技术。

1
2
3
4
5
6
7
8
## create dtf
dtf = pd.DataFrame(lst_dics)
## filter categories
dtf = dtf[ dtf["category"].isin(['ENTERTAINMENT','POLITICS','TECH'])        ][["category","headline"]]
## rename columns
dtf = dtf.rename(columns={"category":"y", "headline":"text"})
## print 5 random rows
dtf.sample(5)

如您所见,数据集还包括标签。我不会将其用于建模,仅用于性能评估。

因此,我们有一些原始文本数据,我们的任务是将其归类为3类(娱乐,政治,技术),我们一无所知。以下是我计划做的:

  • 清洁数据并将其嵌入矢量空间,
  • 为每个类别创建主题群集并将其嵌入向量空间,
  • 计算每个文本向量和主题群集之间的相似性,然后将其分配到最近的群集。

这就是为什么我称之为"一种无人监督的文本分类"。这是一个非常基本的想法,但执行可能很棘手。

现在一切都好了,让我们开始吧。

预处理

绝对的第一步是预先处理数据:清理文本、删除停止词和词法化。我将编写一个函数并将其应用于整个数据集。

'''
Preprocess a string.
:parameter
    :param text: string - name of column containing text
    :param lst_stopwords: list - list of stopwords to remove
    :param flg_stemm: bool - whether stemming is to be applied
    :param flg_lemm: bool - whether lemmitisation is to be applied
:return
    cleaned text
'''
def utils_preprocess_text(text, flg_stemm=False, flg_lemm=True, lst_stopwords=None):
    ## clean (convert to lowercase and remove punctuations and   
    characters and then strip)
    text = re.sub(r'[^\w\s]', '', str(text).lower().strip())

    ## Tokenize (convert from string to list)
    lst_text = text.split()    
    ## remove Stopwords
    if lst_stopwords is not None:
        lst_text = [word for word in lst_text if word not in 
                    lst_stopwords]

    ## Stemming (remove -ing, -ly, ...)
    if flg_stemm == True:
        ps = nltk.stem.porter.PorterStemmer()
        lst_text = [ps.stem(word) for word in lst_text]

    ## Lemmatisation (convert the word into root word)
    if flg_lemm == True:
        lem = nltk.stem.wordnet.WordNetLemmatizer()
        lst_text = [lem.lemmatize(word) for word in lst_text]

    ## back to string from list
    text = " ".join(lst_text)
    return text

如果给定的话,该函数会从语料库中删除一组单词。我可以用nltk创建一个英语词汇的通用停词列表(我们可以通过增加或删除单词来编辑这个列表)。

lst_stopwords = nltk.corpus.stopwords.words("english")
lst_stopwords

现在,我应将该功能应用于整个数据集,并将结果存储在名为"text_clean"的新列中,我将用作新的语料。

1
2
3
4
dtf["text_clean"] = dtf["text"].apply(lambda x: 
          utils_preprocess_text(x, flg_stemm=False, flg_lemm=True, 
          lst_stopwords=lst_stopwords))
dtf.head()

我们有我们预先处理的语料库,因此下一步是建立目标分类。基本上,我们在这里:

创建目标群集

本节的目的是创建一些关键字,这些关键字可以表示每个类别的上下文。通过执行一些文本分析,您可以轻松地发现 3 个最常见的单词是"电影"、"trump"和"苹果"(对于详细的文本分析教程,您可以检查 用 NLP 做文本分析和特征工程。我建议从这些关键字开始。

让我们以政治类为例:"trump"一词可能有不同的含义,因此我们需要添加关键字以避免多面体问题(即"唐纳德"、"共和"、"白宫"、"奥巴马")。此任务可以手动执行,也可以使用预先训练的 NLP 模型的帮助。您可以从这样的 gensim-data 中加载预先训练的 Word 嵌入模型:

nlp = gensim_api.load("glove-wiki-gigaword-300")

gensim 包具有非常方便的功能,可将任何给定单词的最相似单词返回到词汇中。

我将用它来为每个类别创建关键字典:

## Function to apply
def get_similar_words(lst_words, top, nlp):
    lst_out = lst_words
    for tupla in nlp.most_similar(lst_words, topn=top):
        lst_out.append(tupla[0])
    return list(set(lst_out))
## Create Dictionary {category:[keywords]}
dic_clusters = {}
dic_clusters["ENTERTAINMENT"] = get_similar_words(['celebrity','cinema','movie','music'], 
                  top=30, nlp=nlp)
dic_clusters["POLITICS"] = get_similar_words(['gop','clinton','president','obama','republican']
                  , top=30, nlp=nlp)
dic_clusters["TECH"] = get_similar_words(['amazon','android','app','apple','facebook',
                   'google','tech'], 
                   top=30, nlp=nlp)
## print some
for k,v in dic_clusters.items():
    print(k, ": ", v[0:5], "...", len(v))

让我们尝试通过降纬算法(即 TSNE)在 2D 空间中可视化这些关键字。我们希望确保集群彼此之间有良好的分离。

## word embedding
tot_words = [word for v in dic_clusters.values() for word in v]
X = nlp[tot_words]

## pca
pca = manifold.TSNE(perplexity=40, n_components=2, init='pca')
X = pca.fit_transform(X)

## create dtf
dtf = pd.DataFrame()
for k,v in dic_clusters.items():
    size = len(dtf) + len(v)
    dtf_group = pd.DataFrame(X[len(dtf):size], columns=["x","y"], 
                             index=v)
    dtf_group["cluster"] = k
    dtf = dtf.append(dtf_group)

## plot
fig, ax = plt.subplots()
sns.scatterplot(data=dtf, x="x", y="y", hue="cluster", ax=ax)
ax.legend().texts[0].set_text(None)
ax.set(xlabel=None, ylabel=None, xticks=[], xticklabels=[], 
       yticks=[], yticklabels=[])
for i in range(len(dtf)):
    ax.annotate(dtf.index[i], 
               xy=(dtf["x"].iloc[i],dtf["y"].iloc[i]), 
               xytext=(5,2), textcoords='offset points', 
               ha='right', va='bottom')

酷,他们看起来彼此足够孤立。娱乐集群比政治集群更接近科技集群,这是有道理的,因为像"苹果"和"Youtube"这样的词可以出现在科技和娱乐新闻中。

特征工程

是时候将我们预先处理的语料库和我们创建的目标群集嵌入到同一向量空间中了。基本上,我们正在这样做:

是的,我用 BERT 的确,您可以使用任何单词嵌入模型(即 Word2Vec、Glove,...),即使是我们已经加载来定义关键字的模型,那么为什么要费心使用如此沉重而复杂的语言模型呢?这是因为 BERT 不应用固定嵌入,而是查看整个句子,然后为每个单词分配嵌入。因此,BERT 分配给一个单词的载体是整个句子的函数,因此单词可以根据上下文具有不同的向量。

我将用transformers加载原始预先训练的 BERT 版本,并举一个动态嵌入示例:

1
2
3
tokenizer = transformers.BertTokenizer.from_pretrained('bert-base-
            uncased', do_lower_case=True)
nlp = transformers.TFBertModel.from_pretrained('bert-base-uncased')

让我们使用该模型将字符串"river bank"转换为矢量,并打印分配给"bank"一词的载体:

txt = "river bank"
## tokenize
idx = tokenizer.encode(txt)
print("tokens:", tokenizer.convert_ids_to_tokens(idx))
print("ids   :", tokenizer.encode(txt))
## word embedding
idx = np.array(idx)[None,:]
embedding = nlp(idx)
print("shape:", embedding[0][0].shape)
## vector of the second input word
embedding[0][0][2]

如果您对字符串"financial bank"也这样做,您将看到分配给"bank"一词的载体因上下文而不同。请注意,BERT 令牌在句子的开头和结尾插入特殊令牌,其矢量空间的维度为 768(为了更好地了解 BERT 如何处理文本,您可以检查 文章

话虽如此,计划是使用 BERT Word 嵌入来用阵列表示每个文本(形状:令牌数 x 768),然后将每篇文章汇总为平均向量。

因此,最终特征矩阵将是具有形状的阵列:文档数(或平均向量)x 768。

## function to apply
def utils_bert_embedding(txt, tokenizer, nlp):
    idx = tokenizer.encode(txt)
    idx = np.array(idx)[None,:]  
    embedding = nlp(idx)
    X = np.array(embedding[0][0][1:-1])
    return X
## create list of news vector
lst_mean_vecs = [utils_bert_embedding(txt, tokenizer, nlp).mean(0) 
                 for txt in dtf["text_clean"]]
## create the feature matrix (n news x 768)
X = np.array(lst_mean_vecs)

我们可以对目标群集中的关键字进行相同的处理。事实上,每个标签都由帮助 BERT 理解群集内上下文的单词列表标识。因此,我将创建一个字典标签:集群平均向量(cluster mean vector)。

dic_y = {k:utils_bert_embedding(v, tokenizer, nlp).mean(0) for k,v
         in dic_clusters.items()}

我们开始只是一些文本数据和3个字符串("娱乐 Entertainment","政治 Politics","技术 Tech"),现在我们有一个特征矩阵和一个目标标签。

模型设计与测试

最后,是时候构建一个模型,根据每个目标群集的相似性对新闻进行分类。

我将使用 Cosine相似性,一种基于两个非零向量之间角度余弦的相似性。您可以轻松地使用 scikit-learn的 cosine 相似性实现,它需要 2 个阵列(或向量),并返回一个数组(或单个值)。在这种情况下,输出将是一个具有形状的矩阵:新闻数量 x 标签数量(3, Entertainment/Politics/Tech)。换句话说,每行将代表一篇文章,并包含与目标群集的相似度的数值。

为了运行通常的评估指标(Accuracy, AUC, Precision, Recall...),我们必须重规范每行的分数,以便将值总和为1,并决定一个类别来标记文章。我会选择得分最高的一个,但最好设定一些最低阈值,并排除分数非常低的预测。

#--- Model Algorithm ---#
## compute cosine similarities
similarities = np.array(
            [metrics.pairwise.cosine_similarity(X, y).T.tolist()[0] 
             for y in dic_y.values()]
            ).T
## adjust and rescale
labels = list(dic_y.keys())
for i in range(len(similarities)):
    ### assign randomly if there is no similarity
    if sum(similarities[i]) == 0:
       similarities[i] = [0]*len(labels)
       similarities[i][np.random.choice(range(len(labels)))] = 1
    ### rescale so they sum = 1
    similarities[i] = similarities[i] / sum(similarities[i])

## classify the label with highest similarity score
predicted_prob = similarities
predicted = [labels[np.argmax(pred)] for pred in predicted_prob]

就像在经典的监督使用案例中一样,我们有一个具有预测概率的对象(在这里,它们经过调整的相似性分数),而另一个对象具有预测标签。让我们来看看我们是如何做的:

y_test = dtf["y"].values
classes = np.unique(y_test)
y_test_array = pd.get_dummies(y_test, drop_first=False).values

## Accuracy, Precision, Recall
accuracy = metrics.accuracy_score(y_test, predicted)
auc = metrics.roc_auc_score(y_test, predicted_prob, 
                            multi_class="ovr")
print("Accuracy:",  round(accuracy,2))
print("Auc:", round(auc,2))
print("Detail:")
print(metrics.classification_report(y_test, predicted))

## Plot confusion matrix
cm = metrics.confusion_matrix(y_test, predicted)
fig, ax = plt.subplots()
sns.heatmap(cm, annot=True, fmt='d', ax=ax, cmap=plt.cm.Blues, 
            cbar=False)
ax.set(xlabel="Pred", ylabel="True", xticklabels=classes, 
       yticklabels=classes, title="Confusion matrix")
plt.yticks(rotation=0)
fig, ax = plt.subplots(nrows=1, ncols=2)

## Plot roc
for i in range(len(classes)):
    fpr, tpr, thresholds = metrics.roc_curve(y_test_array[:,i],  
                           predicted_prob[:,i])
    ax[0].plot(fpr, tpr, lw=3, 
              label='{0} (area={1:0.2f})'.format(classes[i], 
                              metrics.auc(fpr, tpr))
               )
ax[0].plot([0,1], [0,1], color='navy', lw=3, linestyle='--')
ax[0].set(xlim=[-0.05,1.0], ylim=[0.0,1.05], 
          xlabel='False Positive Rate', 
          ylabel="True Positive Rate (Recall)", 
          title="Receiver operating characteristic")
ax[0].legend(loc="lower right")
ax[0].grid(True)

## Plot precision-recall curve
for i in range(len(classes)):
    precision, recall, thresholds = metrics.precision_recall_curve(
                 y_test_array[:,i], predicted_prob[:,i])
    ax[1].plot(recall, precision, lw=3, 
               label='{0} (area={1:0.2f})'.format(classes[i], 
                                  metrics.auc(recall, precision))
              )
ax[1].set(xlim=[0.0,1.05], ylim=[0.0,1.05], xlabel='Recall', 
          ylabel="Precision", title="Precision-Recall curve")
ax[1].legend(loc="best")
ax[1].grid(True)
plt.show()

好吧,我首先要说,这不是我见过的最好的准确率。另一方面,考虑到我们没有训练任何模型,我们甚至编造了分类标签,这样看结果就不算太坏。主要的问题是超过4k的政治观察被分类为娱乐,但这些可以很容易地通过微调这两个类别的关键字来改善。

可解释性

让我们尝试了解是什么导致我们的算法可以将新闻分类划分为某类,而不是其他类别。让我们从语料库中随机观察:

1
2
3
4
5
i = 7
txt_instance = dtf["text_clean"].iloc[i]
print("True:", y_test[i], "--> Pred:", predicted[i], "| 
      Similarity:", round(np.max(predicted_prob[i]),2))
print(txt_instance)

training-text-model-classification-bert__21Mar13104638740484_1.png

这是正确分类的政治观察。也许,"republican"和"clinton"这两个词给了BERT正确的暗示。我将在2D空间中可视化文章的平均向量,并绘制与目标群集的Top相似性。

## create embedding Matrix
y = np.concatenate([embedding_bert(v, tokenizer, nlp) for v in 
                    dic_clusters.values()])
X = embedding_bert(txt_instance, tokenizer,
                   nlp).mean(0).reshape(1,-1)
M = np.concatenate([y,X])

## pca
pca = manifold.TSNE(perplexity=40, n_components=2, init='pca')
M = pca.fit_transform(M)
y, X = M[:len(y)], M[len(y):]

## create dtf clusters
dtf = pd.DataFrame()
for k,v in dic_clusters.items():
    size = len(dtf) + len(v)
    dtf_group = pd.DataFrame(y[len(dtf):size], columns=["x","y"], 
                             index=v)
    dtf_group["cluster"] = k
    dtf = dtf.append(dtf_group)

## plot clusters
fig, ax = plt.subplots()
sns.scatterplot(data=dtf, x="x", y="y", hue="cluster", ax=ax)
ax.legend().texts[0].set_text(None)
ax.set(xlabel=None, ylabel=None, xticks=[], xticklabels=[], 
       yticks=[], yticklabels=[])
for i in range(len(dtf)):
    ax.annotate(dtf.index[i], 
               xy=(dtf["x"].iloc[i],dtf["y"].iloc[i]), 
               xytext=(5,2), textcoords='offset points', 
               ha='right', va='bottom')

## add txt_instance
ax.scatter(x=X[0][0], y=X[0][1], c="red", linewidth=10)
           ax.annotate("x", xy=(X[0][0],X[0][1]), 
           ha='center', va='center', fontsize=25)

## calculate similarity
sim_matrix = metrics.pairwise.cosine_similarity(X, y)

## add top similarity
for row in range(sim_matrix.shape[0]):
    ### sorted {keyword:score}
    dic_sim = {n:sim_matrix[row][n] for n in 
               range(sim_matrix.shape[1])}
    dic_sim = {k:v for k,v in sorted(dic_sim.items(), 
                key=lambda item:item[1], reverse=True)}
    ### plot lines
    for k in dict(list(dic_sim.items())[0:5]).keys():
        p1 = [X[row][0], X[row][1]]
        p2 = [y[k][0], y[k][1]]
        ax.plot([p1[0],p2[0]], [p1[1],p2[1]], c="red", alpha=0.5)
plt.show()

我把要关注的群集放大:

总的来说,我们可以说均值向量与政治集群非常相似。让我们把文章分解成token,看看哪些token "激活 "了正确的集群。

## create embedding Matrix
y = np.concatenate([embedding_bert(v, tokenizer, nlp) for v in 
                    dic_clusters.values()])
X = embedding_bert(txt_instance, tokenizer,
                   nlp).mean(0).reshape(1,-1)
M = np.concatenate([y,X])

## pca
pca = manifold.TSNE(perplexity=40, n_components=2, init='pca')
M = pca.fit_transform(M)
y, X = M[:len(y)], M[len(y):]

## create dtf clusters
dtf = pd.DataFrame()
for k,v in dic_clusters.items():
    size = len(dtf) + len(v)
    dtf_group = pd.DataFrame(y[len(dtf):size], columns=["x","y"], 
                             index=v)
    dtf_group["cluster"] = k
    dtf = dtf.append(dtf_group)

## add txt_instance
tokens = tokenizer.convert_ids_to_tokens(
               tokenizer.encode(txt_instance))[1:-1]
dtf = pd.DataFrame(X, columns=["x","y"], index=tokens)
dtf = dtf[~dtf.index.str.contains("#")]
dtf = dtf[dtf.index.str.len() > 1]
X = dtf.values
ax.scatter(x=dtf["x"], y=dtf["y"], c="red")
for i in range(len(dtf)):
     ax.annotate(dtf.index[i], 
                 xy=(dtf["x"].iloc[i],dtf["y"].iloc[i]), 
                 xytext=(5,2), textcoords='offset points', 
                 ha='right', va='bottom')

## calculate similarity
sim_matrix = metrics.pairwise.cosine_similarity(X, y)

## add top similarity
for row in range(sim_matrix.shape[0]):
    ### sorted {keyword:score}
    dic_sim = {n:sim_matrix[row][n] for n in 
               range(sim_matrix.shape[1])}
    dic_sim = {k:v for k,v in sorted(dic_sim.items(), 
                key=lambda item:item[1], reverse=True)}
    ### plot lines
    for k in dict(list(dic_sim.items())[0:5]).keys():
        p1 = [X[row][0], X[row][1]]
        p2 = [y[k][0], y[k][1]]
        ax.plot([p1[0],p2[0]], [p1[1],p2[1]], c="red", alpha=0.5)
plt.show()

正如我们所想的那样,文中有一些词明显与政治群有联系,但还有一些词与娱乐的通用上下文比较相似。

结论

本文是一个教程,演示如何在没有标签训练集时进行文本分类。 我使用了一个预先训练的Word Embedding模型来建立一组关键词来对目标变量进行语境化。然后,我用预先训练好的BERT语言模型将这些词和语料在同一个向量空间中进行转换。最后,我计算了文本和关键词之间的余弦相似度,以确定每篇文章的上下文,我用这些信息给新闻打上标签。 这种策略并不是最有效的,但它绝对是高效的,因为它可以让你快速提供良好的结果。此外,一旦获得了一个标签化的数据集,这个算法可以作为监督模型的基线。

其他参考资料:
- Text Analysis & Feature Engineering with NLP
- Text Classification with NLP: Tf-ldf vs Word2Vec vs BERT

凡本网注明"来源:XXX "的文/图/视频等稿件,本网转载出于传递更多信息之目的,并不意味着赞同其观点或证实其内容的真实性。如涉及作品内容、版权和其它问题,请与本网联系,我们将在第一时间删除内容!
作者: Mauro Di Pietro
来源: https://towardsdatascience.com/text-classification-with-no-model-training-935fe0e42180