参考博文:
超干货!一文概览 NLP 算法_AI科技大本营的博客-CSDN博客
说明:本文参考了许多博文,但是学习是基于上述博文的。其它参考的博文或资料会在文章相应的段落中指出。
NLP,Nature Language Processing,自然语言处理,就是用计算机来分析和生成自然语言(文本、语音),目的是让人类可以用自然语言形式跟计算机系统进行人机交互,从而更便捷、有效地进行信息管理。
NLP在实际场景中有很多方面的应用,比如翻译,包括语音翻译,文本翻译;再比如文本归类等等。不同的应用场景,NLP的处理过程可能会略有不同,但主体思路是一致的。本文的参考博文中,是通过一个识别垃圾短信(属于文本归类)的应用来描述NLP的过程的。本文试图通过这个例子,理解NLP的主体思路。
和许多机器学习的思路一致,例子中的NLP通过对很多有标记(spam,ham)的短信进行迭代学习,识别出什么样的短信才是垃圾(spam)短信,从而得到一个判定垃圾短信的模型,然后可以用这个模型去识别一些新的、需要鉴定的短信。这里的关键是,机器怎样理解短信(文本)的内容,参考博文中提到的“NLP任务可以大致分为词法分析、句法分析、语义分析三个层面。”,我理解都属于这个关键。这也是NLP区别于其它机器学习类问题的显著特征之一。
归纳一下,示例中的NLP过程可以分为以下几步
下面的内容,会基于对参考博文中的示例程序进行详细的实践过程,来一步步理解NLP的过程。
我本地没有安装Jupyter Notebook,而是使用了Google Colab提供的Jupyter Notebook,用法比较简单,不用本地安装什么程序。用法详见我另一篇博文Google Colab Jupyter Notebook
import pandas as pd
import numpy as np
import matplotlib.pyplot as plturl = "/%E4%B8%80%E6%96%87%E6%A6%82%E8%A7%88NLP%E7%AE%97%E6%B3%95%EF%BC%88Python%EF%BC%89/data/spam.csv"
spam_df = pd.read_csv(url, header=0, encoding="ISO-8859-1")# show data, a statistic view of "ham" v.s. "spam" lables
# using pandas.Series.plot to plot data diagram
# in raw data, col 'label' will be tagged as "ham" or "spam"
_, ax = plt.subplots(1,2,figsize=(8,4))
spam_df['label'].value_counts().plot(ax=ax[0], kind="bar", rot=90, title='label');
spam_df['label'].value_counts().plot(ax=ax[1], kind="pie", rot=90, title='label', ylabel='');
print("Dataset size: ", spam_df.shape)spam_df.head(5) # return first 5 rows
对上述程序的说明:
上述代码运行出来的效果如下
参考博文的示例中,在这个阶段执行了以下步骤:
上述动作,我理解对分析垃圾短信这个目标都是不影响的。至于其它的文本处理问题,要视具体的情况而定。
def clean_non_english_xdig(txt,isstem=True, gettok=True):txt = re.sub('[0-9]', 'x', txt) # 去数字替换为xtxt = txt.lower() # 统一小写txt = re.sub('[^a-zA-Z]', ' ', txt) #去除非英文字符并替换为空格word_tokens = word_tokenize(txt) # 分词(tokenize)if not isstem: #是否做词干还原filtered_word = [w for w in word_tokens if not w in stop_words] # 删除停用词else:filtered_word = [stemmer.stem(w) for w in word_tokens if not w in stop_words] # 删除停用词及词干还原if gettok: #返回为字符串或分词列表return filtered_word #返回分词列表else:return " ".join(filtered_word) #返回为字符串spam_df['token'] = ssage.apply(lambda x:clean_non_english_xdig(x))
上述代码中,我们使用了很多Python模块,包括
re:正则表达式模块。这个是Python提供的内置模块,详见re --- 正则表达式操作 — Python 3.12.0 文档。前面提到的替换数字、去除非英文字符都用到这个模块
import nltk # nltk: Natural Language Toolkit
nltk.download('stopwords')from nltk import word_tokenize
pus import stopwords
from nltk.stem import SnowballStemmer
import re # regular expression# get stemmer(词干还原器) for English
stemmer = SnowballStemmer('english')# stopwords.words('english') will return all stopwords in English
stop_words = set(stopwords.words('english'))#print(stop_words)
关于停用词,
关于词干,这个应该说是英语特有的(其它表音的词汇可能也有)。比如英文单词中很多含义相同的名词和形容词就差在一个后缀上,此时这两个词的共同部分就是一个词干。还原词干也是为了提高处理的效率。
计算机怎么理解词义,答案是词向量。因此,这个步骤的关键就是产生词向量。
词向量产生的方法,参考博文中提到了两种:One-hot编码、词嵌入分布式方法。我理解One-hot编码应该不是我们接下来讲到的方法,而且在实际应用中应该几乎用不到,所以可以直接跳过。词嵌入分布式方法,就是我们假设整个词库中存在一组“基础词汇”,所有词汇都可以通过这些“基础词汇”来解释。就好像在色彩领域中,我们可以用三原色的比例搭配来表达所有的颜色一样;所有词汇也可以用这些“基础词汇”的比例搭配构成,所有这些比例的值、放在一起就构成了词向量。
以参考博文中的图为例,"living being", "feline", "human",..."plural"就是一组“基础词汇”,"cat"就是目标词汇。目标词汇和基础词汇关系越紧密,表示出来的系数越高;反之系数越低。如果是否定关系,则表示为负数,否定程度越高,负数的绝对值越大。
词向量具有一个性质:如果两个词的词向量所在的线性空间中的欧氏距离越近,表明这两个词的词义更接近。上图展示了一个七维到两维的转换,这个我不是太理解。但我认为,即使在七维的实数线性空间(欧氏空间)中,cat、kitten这两个向量之间的距离也是接近的。
对于计算机来说,要想得到一个词汇的词向量,显然也是需要通过大量的实际文本来学习的,也就是文中提到的“通过神经网络学习构造一个低维、稠密,隐含词语间关系的向量表示”。因此,词向量的产生也需要一个机器学习模型,现有的常见有Word2Vec、Fasttext、Bert等模型。示例中使用了FastText作为词向量的生成模型。
关于FastText模型的原理,这也是一个很大的主题,这里就不展开了,后面有机会再深入研究。至于本专题,只需要理解,通过FastText模型产生了每个词的词向量,这个就可以看作计算机所理解的词义。
代码非常简单
dels import FastText# for dimension size of the word vector(vector_size): > 8.33*logN, N is the mount
# of words in the total vocabulary (about 100000 for English)
fmodel = FastText(ken, vector_size=100, sg=1, window=3, min_count=1, epochs=10, min_n=3, max_n=6,word_ngrams=1,workers=12)
print("输出hello的词向量",fmodel.wv['hello']) # 词向量
其中,vector_size=100就是我们设定的词向量的维度,这个值越大,对某个词的解释就越清楚。上例中也顺便打印了hello这个词对应的词向量
输出hello的词向量 [-0.06486855 -0.11669812 0.01481049 0.11959482 0.01505752 -0.211060330.0508242 0.534427 0.04983698 -0.4678988 -0.07739533 -0.2590439-0.27452448 0.34640867 0.05644388 0.0411542 -0.17742024 -0.14724205-0.40772647 -0.32718262 -0.38424245 0.02967255 0.4522676 -0.359254450.02203463 -0.14818795 -0.29914513 0.15254086 -0.11818233 -0.2659126-0.29137042 0.03555868 0.75406 0.16979928 -0.06291115 0.194953590.21627317 0.11791238 -0.24001747 0.1328767 -0.20400654 -0.4325799-0.15280439 0.04770649 -0.54049116 -0.4499988 -0.21762426 -0.04849562-0.09015825 0.465206 0.31826168 -0.2359138 0.09191308 -0.12536253-0.15756528 -0.23976502 0.4348795 0.13041997 -0.36585805 0.28864990.44637403 -0.27144343 -0.24118404 0.19216292 0.37490252 0.417626380.16895205 0.11977152 0.06955243 0.320308 -0.10889991 0.09682916-0.07709634 -0.39132398 0.43337774 -0.37753922 0.16335873 -0.3171749-0.29864448 0.24556115 -0.19857529 -0.06220499 -0.00083902 -0.04367374-0.23262507 0.02321449 -0.06271665 0.17982881 -0.16868348 -0.1960884-0.19682828 -0.06968057 -0.04842337 0.15578814 0.31849754 0.3703826-0.3100309 0.04458625 0.05850064 0.5650316 ]
计算机理解的词义是词向量,那么计算机理解的句义就是句向量。问题转化为怎么基于词向量产生句向量。
关于这个问题,可以参考以下博文:(Word2Vec)怎么将得到的词向量变成句子向量,以及怎么衡量得到词向量的好坏_词向量转换到句子向量-CSDN博客这里简单说一下前两种计算方法:
TF-IDF的解释:TF词频(Term Frequency),IDF逆向文件频率(Inverse Document Frequency)。TF表示词条在某个文档中出现的频率,频率越高说明这个词条具有很好的类别区分能力。IDF的主要思想是:如果包含某个词条的文档越少,IDF越大,则说明该词条具有很好的类别区分能力。因此,TF-IDF结合在一起,表达的意思是:如果某个词或短语在一篇文章中出现的频率高,并且在其他文章中很少出现,则认为此词或者短语具有很好的类别区分能力,适合用来分类。详见:tf-idf_百度百科 (baidu)
参考博文的示例采用了平均词向量的方法。
# 对每个句子的所有词向量取均值,来生成一个句子的vector
def build_sentence_vector(sentence,w2v_model,size=100):sen_vecs((size,))count=0for word in sentence:try:sen_vec+=w2v_model[word]#.reshape((1,size))count+=1except KeyError:continueif count!=0:sen_vec/=countreturn sen_vec# 句向量
sents_vec = []
for sent in spam_df['token']:sents_vec.append(build_sentence_vector(sent,fmodel.wv,size=100))print(len(sents_vec))
#for x in range(5):
# print(sents_vec[x])
说明:
在训练数据之前,对于标签,还需要进行处理。原始数据中的标签是字符串{"spam","ham"},这个不利于计算机处理。这实际上是一个二分类问题,可以将标签转化为{0, 1},从而用二分类的模型去处理。比如定义:"spam" = 1, "ham" = 0。
# convert col 'lable' to '0'(not spam) or '1'(spam)
spam_df['label'] = (spam_df.label=='spam').astype(int)
# print(spam_df['label'])
spam_df.head(3)
spam_df.head(3)显示了头三行的内容,可以看到,label列已经被转化为0,1这样的值了
于是,我们用前面产生的每一条短消息对应的句向量作为输入,转化后的label(值为0, 1)作为标签,然后可以开始训练模型了。
Logistic 回归(Logistic Regression,LR)是一种常用的处理二分类问题的线性模型。示例中使用了LightGBM(Light Gradient Boosting Machine)模型,但也包含了使用Logistic 回归模型的代码(被注释了,可以解开注释使用)。
del_selection import train_test_split
from lightgbm import LGBMClassifier
from sklearn.linear_model import LogisticRegressiontrain_x, test_x, train_y, test_y = train_test_split(sents_vec, spam_df.label,test_size=0.2,shuffle=True,random_state=42)
result = []
clf = LGBMClassifier(class_weight='balanced',n_estimators=300, num_leaves=64, reg_alpha= 1,reg_lambda= 1,random_state=42)
#clf = LogisticRegression(class_weight='balanced',random_state=42)clf.fit(train_x,train_y)
说明:
所谓模型评价,通俗点说,就是训练出来的模型,如果拿去用到新的数据上测试,看看模型预测出来的结果是否和实际情况一致。在前面的模型训练中,我们预留了20%的数据用作做这个模型测试。
对于二分类问题,ROC曲线是一种非常流程的模型评价方法,示例中也使用了这种方法。
关于ROC曲线,下面这篇文章讲得比较透彻
详解机器学习模型评估ROC曲线! (360doc)
这里简单说一下。
对于一个二分类模型,假定负样本为0,正样本为1,于是模型评价结果y_hat就有(0,1)两种取值,而实际标签y也有(0, 1)两种取值,两两组合,会有以下4种情况:
再定义:
其中,N是真实的负样本个数,P是真实的正样本个数。于是:FPR表示了、所有负样本中预测错误的比例;TPR表示了、所有正样本中预测正确的比例。
明确了FPR和TPR的概念之后,ROC曲线是这样绘制的:以FPR为横坐标、TPR为纵坐标,按照判决正负样本的阈值从1开始、一直变化到0,得到的(FPR, TPR)的点连接而成的曲线。其中,这个阈值的含义是:当 y_hat > 阈值,认为是正样本;反之是负样本。
特殊地:当阈值=1时,所有样本都判决为负样本,于是TP=0、FP=0,从而TPR=0、FPR=0,对应坐标原点(0, 0)。当阈值=0时,所有样本都判决为正样本,所有正样本也都会被模型判定为正样本,于是TPR=TP/P=1 (对于P中每一个样本,模型都判断对了,TP=P);而所有负样本也都会被模型判定为正样本,于是FPR=FP/N=1(对于N中每一个样本,模型都判断错了,FP=N)。于是当阈值=0时,(FPR, TPR)对应坐标轴上的点(1, 1)。
下面看代码
ics import auc,roc_curve,f1_score,precision_score,recall_score
def model_metrics(model, x, y,tp='auc'):""" 评估 """yhat = model.predict(x) # the esmatied y (0 or 1) for each xyprob = model.predict_proba(x)[:,1] # return the predicted probability [0, 1]# pos_label: None, if y_true is in {-1, 1}; 1 if y_true is in {0, 1}fpr,tpr,_ = roc_curve(y, yprob, pos_label=1)metrics = {'AUC':auc(fpr, tpr),'KS':max(tpr-fpr),'f1':f1_score(y,yhat),'P':precision_score(y,yhat),'R':recall_score(y,yhat)}roc_auc = auc(fpr, tpr)plt.plot(fpr, tpr, 'k--', label='ROC (area = {0:.2f})'.format(roc_auc), lw=2)plt.xlim([-0.05, 1.05]) # 设置x、y轴的上下限,以免和边缘重合,更好的观察图像的整体plt.ylim([-0.05, 1.05])plt.xlabel('False Positive Rate')plt.ylabel('True Positive Rate') # 可以使用中文,但需要导入一些库即字体plt.title('ROC Curve')plt.legend(loc="lower right")return metricsprint('train ',model_metrics(clf, train_x, train_y,tp='ks'))
print('test ',model_metrics(clf, test_x,test_y,tp='ks'))
说明:
下面是运行结果,可以看出对于训练集,AUC=1,已经是理想情况了。对于测试集,AUC也已经达到了0.99,非常好的结果。从ROC曲线的形状来看,都非常接近左上角的顶点(0, 1)。
本文发布于:2024-02-02 06:21:35,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170682609641947.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |