NLP基础知识
- 1 如何衡量机器学习分类模型
- 2 词袋模型和TFIDF模型
- 3 Word2Vec模型和Doc2Vec模型
- 4 自己动手训练word2vec模型(略)
- 5 使用多层感知机进行文档分类
- 6 使用fasttext进行文档分类
- 7 使用LDA进行文档主题建模
- 8 使用Jieba进行中文词性标注
- 9 使用TextRank和TFIDF进行关键字自动提取
- 10 文档相似度
- 11 NLP应用案例
1 如何衡量机器学习分类模型
混淆矩阵
准确率与召回率
准确度与F1-Score
ROC与AUC
- ROC(Receiver Operating Characteristic Curve)受试者工作特征曲线,以真阳性率为纵坐标,假阳性率为横坐标绘制的曲线,是反映灵敏性和特效性连续变量的综合指标。一般认为ROC越光滑说明分类算法过拟合的概率越低,越接近左上角说明分类性能越好。
- AUC(Area Under the Receiver Operating Characteristic Curve)就是量化衡量ROC分类性能的指标,如图1-2 所示,物理含义是ROC曲线的面积,AUC越大越好。
2 词袋模型和TFIDF模型
词袋模型
文本特征提取有两个非常重要的模型:
词集模型:单词构成的集合,集合自然每个元素都只有一个,也即词集中的每个单词都只有一个。
词袋模型:在词集的基础上如果一个单词在文档中出现不止一次,统计其出现的次数(频数)。
两者本质上的区别,词袋是在词集的基础上增加了频率的维度,词集只关注有和没有,词袋还要关注有几个。
TF-IDF模型
TF-IDF是一种统计方法,用以评估某一字词对于一个文件集或一个语料库的重要程度。
主要思想
字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。
TF-IDF的主要思想是,如果某个词或短语在一篇文章中出现的频率TF(Term Frequency,词频),词频高,并且在其他文章中很少出现,则认为此词或者短语具有很好的类别区分能力,适合用来分类。
TF-IDF实际上是:TF * IDF。
TF表示词条在文档d中出现的频率。
IDF(inverse document frequency,逆向文件频率)的主要思想是:如果包含词条t的文档越少,也就是n越小,IDF越大,则说明词条t具有很好的类别区分能力。如果某一类文档C中包含词条t的文档数为m,而其他类包含t的文档总数为k,显然所有包含t的文档数n=m+k,当m大的时候,n也大,按照IDF公式得到的IDF的值会小,就说明该词条t类别区分能力不强。
但是实际上,如果一个词条在一个类的文档中频繁出现,则说明该词条能够很好代表这个类的文本的特征,这样的词条应该给它们赋予较高的权重,并选来作为该类文本的特征词以区别与其他类文档。
词汇表模型
词袋模型可以很好的表现文本由哪些单词组成,但是却无法表达出单词之间的前后关系,于是人们借鉴了词袋模型的思想,使用生成的词汇表对原有句子按照单词逐个进行编码。
3 Word2Vec模型和Doc2Vec模型
Word2Vec:
是Google在2013年开源的一款将词表征为实数值向量的高效工具,采用的模型有CBOW(Continuous Bag-Of-Words,即连续的词袋模型)和Skip-Gram 两种。
Doc2Vec:
与Word2Vec不同的地方是,Doc2Vec处理的每个英文段落,需要使用一个唯一的标识标记,并且使用一种特殊定义的数据格式保存需要处理的英文段落,这种数据格式定义如下:
SentimentDocument = namedtuple('SentimentDocument', 'words tags')
4 自己动手训练word2vec模型(略)
5 使用多层感知机进行文档分类
概述:
反向传播:如果对预测错误的神经元施加惩罚,从输出层开始层层向上查找预测错误的神经元,微调这些神经元对应的权重,达到修复错误的目的,这样的算法就叫做反向传播算法。本书介绍的是神经网络中最简单的一种形式,即多层感知机。
特征提取:
-
词袋&TFIDF
特征提取的方式采用词袋结合TFIDF的方式。
#切割词袋
vectorizer = CountVectorizer()
# 该类会统计每个词语的tf-idf权值
transformer = TfidfTransformer()
x = transformer.fit_transform(vectorizer.fit_transform(x))
-
n-gram&TFIDF
特征提取还可以使用词袋模型的加强版n-gram,比如最常见的2-gram,这样可以更好的提取单词前后之间的关系。
#切割词袋
vectorizer = CountVectorizer(ngram_range=(2,2))
# 该类会统计每个词语的tf-idf权值
transformer = TfidfTransformer()
x = transformer.fit_transform(vectorizer.fit_transform(x))
- one-hot
#转换成one hot编码
y=to_categorical(t, num_classes=3)
训练与效果验证:
本例中设计的隐藏层有两层,结点数分为为5和3,通常最后一层的结点数与标签类型数相同。
#mlp
clf = MLPClassifier(solver='lbfgs',
alpha=1e-5,
hidden_layer_sizes=(5, 3),
random_state=1)
比较重要的几个参数的定义
- hidden_layer_sizes,表示隐藏层的结构
- activation,激活函数,{‘identity’, ‘logistic’, ‘tanh’, ‘relu’}, 默认relu
- solver,优化方式,{‘lbfgs’, ‘sgd’, ‘adam’}, 默认adam。lbfgs使用quasi-Newton方法的优化器,sgd使用随机梯度下降,adam也是一种随机梯度的优化器
- alpha,可选的,默认0.0001,正则化项参数
效果验证使用5折交叉验证,考核的指标是f1和accuracy。使用cross_val_score函数可以非常方便的实现交叉验证的功能,其中cv参数指定交叉验证的方式,比如5代表5折交叉验证。
scores = cross_val_score(clf, x, y, cv = 5,scoring='f1_micro')
print("f1: %0.2f (+/- %0.2f)" % (scores.mean(), scores.std() * 2))
scores = cross_val_score(clf, x, y, cv = 5,scoring='accuracy')
print("accuracy: %0.2f (+/- %0.2f)" % (scores.mean(), scores.std() * 2))
6 使用fasttext进行文档分类
fasttext原理
fasttext提供了一种有效且快速的方式生成词向量以及进行文档分类。
fasttext模型输入一个词的序列,输出这个词序列属于不同类别的概率。
fasttext模型架构和Word2Vec中的CBOW模型很类似。不同之处在于,fasttext预测标签,而CBOW模型预测中间词。fasttext设计的初衷就是为了作为一个文档分类器,副产品是也生成了词向量。
fasttext特性
-
n-gram
在词袋模型中,把单词当做独立的个体,没有考虑词前后的关系。比如"我打你"和“你打我“,使用词袋模型的话,这两句话是完全一样的。 词袋的特征为:[“我”,“打“,”你”]
"我打你"和“你打我“对应的特征向量均为[1,1,1]
n-gram是对词袋模型的一种改善,它会关注一个单词的前后关系,比如n-gram中最常见的2-gram,就关注单词的前一个词,
比如"我打你",就可以拆分为"我打"和"打你"。这两句话一起建模的话,2-gram对应的特征为:[“我打”,“打你”,“你打”,“打我”]
"我打你"对应的特征向量为:[1,1,0,0],"你打我"对应的特征向量为:[0,0,1,1]
与Word2Vec使用词袋模型不同,fasttext使用了n-gram模型,因此fasttext可以更有效的表达词前后的之间的关系。
高效率
fasttext在使用标准多核CPU的情况下10分钟内处理超过10亿个词汇,特别是与深度模型对比,fastText能将训练时间由数天缩短到几秒钟。使用一个标准多核CPU,得到了在10分钟内训练完超过10亿词汇量模型的结果。
安装fasttext
预训练模型
facebook已经基于其收集的海量语料,训练好了fasttext的词向量模型,目前已经支持了150多种语言。有需要的读者可以直接下载并使用,对应的链接为:https://github.com/facebookresearch/fastText/blob/master/docs/crawl-vectors.md
例子
- 文件清洗
- 文档分类
fasttext对训练和测试的数据格式有一定的要求,**数据文件和标签文件要合并到一个文件里面。**文件中的每一行代表一条记录,同时每条记录的最后标记对应的标签。默认情况下标签要以__label__开头,比如:
这是一条测试数据 __label__1
加载数据清洗后的数据和标签,随机划分成训练数据和测试数据,其中测试数据占20%。
SogouTCE_kv=load_SogouTCE()
x,y=load_selecteddata(SogouTCE_kv)
# 分割训练集和测试集
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2)
按照fasttext的格式要求保存成训练数据和测试数据。
#按照fasttest的要求生成训练数据和测试数据
dump_file(x_train,y_train,"../data/sougou_train.txt")
dump_file(x_test, y_test, "../data/sougou_test.txt")
查看训练数据文件的内容,举例如下:
2 0 1 2 款 长安 标致 雪铁龙 D S 4 / D S 5 九寨沟 试驾 __label__79
- 训练模型
下面开始训练fasttext模型。
# train_supervised uses the same arguments and defaults as the fastText cli
model = train_supervised(
input="../data/sougou_train.txt", epoch=25, lr=0.6, wordNgrams=2, verbose=2, minCount=1
)
其中比较重要的几个参数的含义为:
input;表示训练数据文件的路径
epoch:表示训练的次数
lr:表示初始的学习速率
wordNgrams:表示n-gram的值,一般使用2,表示2-gram
minCount:表示参与计算的单词的最小出现次数。
- 验证效果
fasttext默认情况下会计算对应的准确率和召回率。
def print_results(N, p, r):
print("N\t" + str(N))
print("P@{}\t{:.3f}".format(1, p))
print("R@{}\t{:.3f}".format(1, r))
使用测试数据文件进行校验。
print_results(*model.test("../data/sougou_test.txt"))
运行程序,显示加载了36M的单词,其中包含288770的单词组合,标记类型一共3种。
Read 36M words
Number of words: 288770
Number of labels: 3
验证效果如下所示,准确率为99.0%,召回率为99.0%,对应的F1计算为99.0%,效果非常不错。
Progress: 100.0% words/sec/thread: 626183 lr: 0.000000 loss: 0.005640 ETA: 0h 0m
N 71107
P@1 0.990
R@1 0.990
7 使用LDA进行文档主题建模
LDA(Latent Dirichlet Allocation)是一种文档主题模型,包含词、主题和文档三层结构。
LDA认为一篇文档由一些主题按照一定概率组成,一个主题又由一些词语按照一定概率组成。早期人们用词袋模型对一篇文章进行建模,把一篇文档表示为若干单词的计数。
LDA本质上把词袋模型进行了降维,把一篇文档以主题的形式进行了表示。主题的个数通常为几百,这就把文档使用了维数为几百的向量进行了表示,大大加快了训练速度,并且相对不容易造成过拟合。从某种程度上来说,主题是对若干词语的抽象表示。
以最近一部电视剧《南方有乔木》为例。假设一篇文章介绍了这部电视剧的主要内容。我们可以把这篇文章表示为:0.30*“创业”+0.3*“三角恋”+0.2*“无人机”
然后我们可以把三角恋表示为:0.4*“南乔”+0.3*“时樾”+0.3*“安宁”
需要指出的是,计算出文档、主题以及词语之间的表示关系,需要基于大量的文档,这样才具有普遍的意义。LDA正是提供了这种算法,自动从训练文档中计算出这种对应关系。
- 计算主题
我们使用gensim提供的API进行LDA计算。首先从语料中提取字典,并用该字典把预料转换成词袋。
# 得到文档-单词矩阵 (直接利用统计词 频得到特征)
dictionary = corpora.Dictionary(content)
# 将dictionary转化为一个词袋,得到文档-单词矩阵
texts = [dictionary.doc2bow(text) for text in content]
然后进行LDA计算,演示期间设置只计算5个主题,通常生产环境经验值为200。
num_topics=5
lda = models.ldamodel.LdaModel(corpus=texts, id2word=dictionary, num_topics=num_topics)
其中比较重要的几个参数含义如下:
corpus,计算LDA的语料
id2word,语料对应的字典
num_topics,计算的主题的数量
#我们打印前5个主题。
for index,topic in lda.print_topics(5):
print topic
主题内容如下所示。
0.007*"月" + 0.006*"中" + 0.005*"年" + 0.005*"日" + 0.004*"公司" + 0.004*"时间" + 0.003*"北京" + 0.003*"比赛" + 0.002*"中国" + 0.002*"记者"
0.006*"月" + 0.006*"发展" + 0.005*"年" + 0.005*"日" + 0.005*"中" + 0.005*"中国" + 0.004*"工作" + 0.004*"说" + 0.003*"记者" + 0.003*"建设"
0.008*"月" + 0.007*"市场" + 0.006*"经济" + 0.005*"增长" + 0.004*"元" + 0.004*"中国" + 0.004*"企业" + 0.004*"产品" + 0.004*"年" + 0.004*"记者"
0.011*"日" + 0.011*"月" + 0.007*"记者" + 0.005*"时" + 0.005*"年" + 0.004*"公司" + 0.004*"说" + 0.004*"中" + 0.003*"发现" + 0.002*"亿元"
0.006*"o" + 0.006*"i" + 0.006*"月" + 0.005*"日" + 0.005*"e" + 0.005*"说" + 0.005*"n" + 0.005*"中" + 0.004*"中国" + 0.004*"a"
如果需要设置每个话题对应的关键字的个数,可以通过参数num_words设置,默认为10,这里的num_topics参数容易导致误解,它的含义是显示排名前几个话题,类似topN参数。
print_topics(num_topics=20, num_words=10)
词袋处理后的结果,使用TFIDF算法处理后,可以进一步提升LDA的效果。
# 利用tf-idf来做为特征进行处理
texts_tf_idf = models.TfidfModel(texts)[texts]
lda = models.ldamodel.LdaModel(corpus=texts_tf_idf, id2word=dictionary, num_topics=num_topics)
运行的效果如下所示。
0.001*"比赛" + 0.001*"联赛" + 0.001*"意甲" + 0.001*"主场" + 0.001*"轮" + 0.001*"赛季" + 0.001*"时间" + 0.001*"孩子" + 0.001*"北京" + 0.000*"航天员"
0.001*"叙利亚" + 0.001*"奥运会" + 0.001*"伦敦" + 0.000*"选手" + 0.000*"中国" + 0.000*"说" + 0.000*"男子" + 0.000*"米" + 0.000*"北京" + 0.000*"日"
0.000*"梅西" + 0.000*"林书豪" + 0.000*"鲁尼" + 0.000*"稀土" + 0.000*"钓鱼岛" + 0.000*"巴萨" + 0.000*"试用" + 0.000*"常规赛" + 0.000*"蛋黄派" + 0.000*"尼克斯"
0.002*"体育" + 0.001*"搜狐" + 0.001*"北京" + 0.001*"o" + 0.001*"时间" + 0.001*"n" + 0.001*"i" + 0.001*"日" + 0.001*"C" + 0.001*"e"
0.001*"市场" + 0.001*"经济" + 0.001*"增长" + 0.001*"投资" + 0.001*"亿元" + 0.001*"基金" + 0.001*"公司" + 0.001*"银行" + 0.001*"企业" + 0.001*"同比"
使用LDA提取文档特征
通常LDA的结果可以作为进一步文档分类、文档相似度计算以及文档聚类的依据,可以把LDA当做一种特征提取方法。
#获取语料对应的LDA特征
corpus_lda = lda[texts_tf_idf]
#打印0号文档对应的LDA值
print corpus_lda[0]
输出0号文档对应的LDA值如下,即把0号文档以5个话题形式表示。
[(0, 0.019423252), (1, 0.019521076), (2, 0.92217809), (3, 0.01954053), (4, 0.019337002)]
这里需要解释的是,无论是词袋模型还是LDA生成的结果,都可能存在大量的0,这会占用大量的内存空间。因此默认情况下,词袋以及LDA计算的结果都以稀疏矩阵的形式保存。稀疏矩阵的最小单元定义为:(元素所在的位置,元素的值)
比如一个稀疏矩阵只有0号和2号元素不为0,分别为1和5,那么它的表示方法如下:[(0,1),(2,5)]
使用多核计算
LDA在生产环境中运行遇到的最大问题就是默认只能使用单核资源,运行速度过慢。gensim针对这一情况也提供了多核版本。
【详细见链接】
8 使用Jieba进行中文词性标注
词性指以词的特点作为划分词类的根据。现代汉语的词可以分为两类14种词性。
常见词性分类
词性分类又叫词性标注(Part-Of-Speech tag, POS-tag),常见的词性标准类型如下:
使用Jieba词性分类
Jieba下进行词性分类非常简便。
seg_lig = jieba.posseg.cut(text)
for w,tag in seg_lig:
print "%s /%s" % (w,tag)
以经典句子为例,“我爱北京*“,词性分类的结果为:
我 /r
爱 /v
北京 /ns
* /ns
9 使用TextRank和TFIDF进行关键字自动提取
TextRank
TextRank的思路来自于PageRank。PageRank最开始用来计算网页的重要性。整个互联网可以看作一张有向图,节点是网页。如果网页A存在到网页B的链接,那么有一条从网页A指向网页B的有向边,指向同一个网页的链接越多,该网页的重要性或者说PageRank值更大。
综合考虑Title和Keywords等其它因素之后,Google通过PageRank来调整结果,使那些更重要的网页在搜索结果排名更靠前。
TextRank的原理和PageRank类似,一篇文档去掉停用词以后,每个单词相当于一个网页,有时候也会指定某些词性的单词才参与计算TextRank,比如名词和动词。
网页有明显的指向关系,但是文档的单词之间只有前后关系,所以要指定一个滑动的窗口大小,比如前后5个单词。在滑动窗口内的单词之间的前后关系当做网页之间的指向关系,参与TextRank的计算。
TF-IDF
TFIDF同样可以用于提取关键字。TFIDF的一个基本假设是,一个单词的重要性由词频决定,如果一个单词在一句话里出现频率高,同时在其他句子里出现频率低,那么这个单词对这句话就非常重要,对于一个文档也是如此。
提取关键字
1TextRank
#使用TextRank提取关键字
# 引入TextRank关键词抽取接口
textrank = analyse.textrank
# 基于TextRank算法进行关键词抽取
keywords = textrank(text)
# 输出抽取出的关键词
for keyword in keywords:
print keyword + "/"
提取的结果如下所示,自动化提取关键字的结果差强人意,其中"只能"、"据介绍"这些完全可以省略,另外通常关键字个数需要控制在10个以内。
叙军/
远程/
空袭/
电视台/
战术/
反击/
空军/
现代化/
叙利亚/
地对地/
只能/
武器/
发动/
弹道导弹/
任务/
国家/
据介绍/
法国/
进行/
当属/
Jieba提供了接口,设置关键字的个数以及提取的关键字的词性,比如:
topK,指定关键字的个数
allowPOS,指定关键字的词性,常见的词性包括:
n 名词
nr 人名
ns 地名
nz 其它专名
t 时间词
v 动词
vd 副动词
vn 名动词
我们只提取10个关键字,且只关注名词和动词以及名动词。
# 基于TextRank算法进行关键词抽取
keywords = textrank(text,topK = 10, withWeight = False, allowPOS = ('n','ns','vn','v'))
结果
叙军/
远程/
空袭/
电视台/
战术/
反击/
空军/
现代化/
叙利亚/
地对地/
2 TFIDF
# TFIDF
keywords_tfidf = analyse.extract_tags(text,topK = 10, withWeight = False, allowPOS = ('n','ns','vn','v','nz'))
# 输出抽取出的关键词
for keyword in keywords_tfidf:
print keyword + "/"
生成的结果如下所示,与TextRank相比差别不大。
叙军/
地对地/
空袭/
弹道导弹/
远程/
叙利亚/
电视台/
反击/
战术/
撒手锏/
10 文档相似度
文档相似度指的是两篇文档之间的相似程度,也被称为文档距离。文档相似度通常是文本聚类、信息检索等NLP任务的基础,常见的计算文档距离的方法包括simhash和余弦距离。
simhash算法
simhash是由Charikar在2002年提出来的,论文名为《Similarity estimation techniques from rounding algorithms》。Google基于simhash在海量网页中进行相似度计算并去重。
通常对比两个文档是否相同时,会计算对应的hash值,常见的算法包括md5和sha256。实际使用中,对于检测文档是否被篡改时,使用hash值具有不错的表现。但是当文档内容因为修改少许文字,插入广告甚至只是修改了标点符合和错别字,都会导致hash值改变,可是文档的核心内容并未发生改变。如何使用数学的方法表征这种文档相似性呢?simhash的设计初衷就是使用一种所谓局部hash的方法,可以既可以敏感的识别文档的少许修改又可以识别出文档的大多数内容相同。
simhash的一种典型实现就是将一个文档最后转换成一个64位的字节的特征字或者说simhash值,然后判断重复只需要判断他们的特征字的距离是不是小于3,就可以判断两个文档是否相似。这个距离使用海明距离,即两个simhash值取异或后二进制中1的个数。大家可以结合自身业务特点修改simhash值的位数以及判断文档相似性的海明距离的值。
如图所示,计算6位simhash值典型的实现算法为:
- 将Doc分词和计算权重,抽取出n个(关键词,权重)对,即图中的(feature, weight)
- 计算关键词的hash,生成图中的(hash,weight),并将hash和weight相乘,这一过程是对hash值加权【没看懂为啥?为什么要把hash的每一位跟权重相乘?并且为什么1表示w,而0表示-w】
- 将hash和weight相乘的值相加,比如图中的[13, 108, -22, -5, -32, 55],并最终转换成simhash值110001,转换的规则为正数为1负数为0
simhash库
simhash具有多种实现,常用的一种已经部署在pip源上了,直接安装即可。
pip install simhash
例子【略】
余弦距离
假设向量a、b的坐标分别为(x1,y1)、(x2,y2) ,则对应的余弦距离为(二维空间):
设向量 A = (A1,A2,…,An),B = (B1,B2,…,Bn) 。推广到多维:
夹角越小,余弦值越接近于1,它们的方向更加吻合,则越相似。可见余弦距离在0和1之间且约接近1说明越两者越相似。
例子【略】
11 NLP应用案例
预测Yelp美食评分【待更新】