一、文本分析流程
Pipeline
原始文本(网页文本、新闻、...) -> 分词(中文、英文) -> 清洗(无用的标签 !¥ 停用词.....) -> 标准化(英文时态等) -> 特征提取(tf-idf、word2vec) -> 建模(分类算法、相似度算法) -> 评估过程
二、分词工具
英文没有分词、中文主要以下工具进行分词
jieba分词 https://github.com/fxsjy/jieba
SnowNLP https://github.com/isnowfy/snownlp
HanNLP https://github.com/hankcs/HanLP
分词方法:
(一)最大匹配(Max Matching)
前向最大匹配(forward-max matching)
例子: 我们经常有意见分歧(输入) max-len = 5
词典: ["我们","经常","有","有意见","意见","分歧"]
流程:
1、【我们经常有】意见分歧 未在词典
【我们经常】有意见分歧
【我们经】常有意见分歧
【我们】经常有意见分歧 【我们】在词典中
2、【经常有意见】-》【经常有意】见 -》【经常有】意见 -> 【经常】有意见
3、【有意见分歧】-》【有意见分】歧 -》 【有意见】分歧
4、【分歧】
分词结果:我们 | 经常 | 有意见 | 分歧
贪心算法-局部最优 动态规划-全局最优
max-len 一般设置为5-10 ,或者画图看一下字典里面的词语长度横坐标为词的长度,纵坐标为该词的长度的个数
(二) 后向最大匹配
例子: 我们经常有意见分歧
词典: ["我们","经常","有","有意见","意见","分歧"]
流程:
1、[有意见分歧] -> [意见分歧] -> [见分歧] -> [分歧]
2、[经常有意见] -> [常有意见] -> [有意见]
3、[我们经常] -> [们经常] -> [经常]
4、[我们]
分词结果: 我们 | 经常 | 有意见 | 分歧
前向匹配和后向匹配结果是一样的(90%)
最大匹配的缺点:
1)不能考虑短词语的情况
2) 贪心算法-> 局部最优
3) 效率比较低 依赖于超参数max-len
4) 不能更好地处理歧义的情况(不考虑语义)
(三) 基于语义的分词工具
例子: 经常有意见分歧
词典: 【'有',"有意见","意见",”分歧“,"见","意","经常"】
语言模型
输入 ----> step 1 - 生成所有可能的分割 (递归 + 匹配) -----> step 2 - 选择其中最好的(语言模型)
语言模型 - unigram model
s1: 经常 | 有 | 意见 | 分歧
s2: 经常 | 有意见 | 分歧
p(s1) = p(经常) * p(有) * p(意见) * p(分歧)
p(s2) = p(经常) * p(有意见) * p(分歧)
p(s1) >= p(s2)
Underflow:数值超出类型范围的最小值,所以上述概率两边需要同时去对数,避免underflow的情况
logp(s1) = logp(经常) + logp(有) + logp(意见) + log p(分歧)
p(经常) 、p(有) 、 p(意见) 、 p(分歧) 单词概率会在所有语料中统计得到
缺点:生成所有分割,可能性太多,效率比较低
(四) 维特比算法
第一步和第二步融合一下,加快效率 ----- 维特比算法
分词小结:
- 基于匹配规则的方法
- 基于概率统计的方法(LM,HMM,CRF....)
- 分词可以认为是已经解决的问题
三、拼写纠错(Spell Correction)
(一) 识别错别字
例如:
用户输入: 天起 -----> 改正------> 天气
改正过程具体做法:
定义三个操作: Add Delete Replace
用户输入 | 候选 | 编辑距离(Edit Distance) str1 -> str2 |
therr | there | 1 |
thesis theirs the
词典 | 2 2 2
...... |
1) query 单词与词库进行匹配 2)找出编辑距离最小的单词
Alternative Way:
之前的方法: 用户输入 --> 从词典中寻找编辑距离最小的 --> 返回
现在的方法: 用户输入 --> 生成编辑距离为1,2的字符串 (Candidate) ---> 过滤 (filter) ----> 返回
""" 生成编辑距离为1和2的字符串 """
def generate_edit_one(str1):
"""
给定一个字符串, 生成编辑距离为1的字符串
"""
letters = 'abcdefghijklmnopqrstuvwxyz'
splits = [(str1[:i],str1[i:]) for i in range(len(str1) + 1)]
inserts = [L + c + R for L,R in splits for c in letters]
deletes = [L + R[1:] for L,R in splits if R]
replaces = [L + c + R[1:] for L,R in splits if R for c in letters]
return set(inserts + deletes + replaces)
def generate_edit_two(str1):
"""
给定一个字符串,生成编辑距离小于等于2的字符串
"""
return [e2 for e1 in generate_edit_one(str1) for e2 in generate_edit_one(e1)]
过滤:
问题定义: 给定一个字符串s, 我们要找出最有可能成为正确的字符串c, 也就是c' = argmaxp(c|s)
简化:c' = argmax p(c|s) => c' = argmax p(s|c) * p(c) / p(s) (p(s)常数) => c' = argmax p(s|c) * p(c)
p(s|c) 、p(c)基于已有词库统计得到
(二) 用错单词(基于上下文语义-语言模型)
四、过滤词(Filtering Words)
对于NLP应用,我们通常先把停用词、出现频率很低的词汇过滤掉
不同的应用需要修正停用词库-类似特征筛选的过程
五、Stemming算法 英文用的比较多、中文比较少 one way to normalize
went,go,going
fly,flies
deny,denied,denying
fast,faster,fastest
不同形态单词归成一个单词
"""应用例子"""
from nltk.stem.porter import *
stemmer = PorterStemmer()
test_strs = ['flies','dies']
singles = [stemmer.stem(word) for word in test_strs]
print(' '.join(singles))
六、文本表示(Word Representation)
文本表示-(单词表示、句子表示)
one-hot representation
词典:[我们、去、爬山、今天、你们、昨天、跑步]
每个单词的表示:
我们: [1,0,0,0,0,0,0]
爬山: [0,0,1,0,0,0,0]
跑步: [0,0,0,0,0,0,1]
昨天: [0,0,0,0,0,1,0]
句子的表示方法(sentence representation - boolean):
我们 今天 去 爬山: [1,1,1,1,0,0,0]
你们 昨天 跑步: [0,0,0,0,1,1,1]
你们 又 去 爬山 又 去 跑步 : [0,1,1,1,0,1,0,1]
句子的表示方法(sentence representation - count):
你们 又 去 爬山 又 去 跑步 : [0,2,2,1,0,1,0,1]
缺点:1、不考虑单词顺序 2、稀疏矩阵 3、频繁出现无用的词
为了防止某些无用的词频比较大,可以使用log(item+1)
Sentence Similarity
1、计算距离(欧式距离):d = |s1-s2|
s1 = (1,0,1,1,0,0,0,0) s2 = (0,0,0,0,0,1,1,1) d(s1,s2) = (1^2 + 0^2 + .....)^(1/2)
欧式距离只考虑大小,不考虑方向,常用的相似度是余弦相似度
2、计算相似度(余弦相似度):sim = s1*s2/*(|s1|*|s2|)
总结:词出现的越多并不一定是越重要,并不是出现的越少越不重要
解决办法:不仅需要考虑词,还需要考虑词的权重
Tf-idf Representation
tfidf(w) = tf(d,w)*idf(w)
tf(d,w) -次数 文档d中w的词频 idf(w) =log N/N(w) - 词的重要性
N:语料库中的文档总数
N(w):词语W出现在多少个文档
例子:
文档1: 今天 上 NLP 课程
文档2:今天 的 课程 有 意思
文档3:数据 课程 也 有 意思
step 1: 词库的构建
dic = {今天,上,NLP, 课程, 的,有,意思,数据,也}
从词典中看,d2 = (1*log3/2,上没有出现-0,0,1*log3/3,log3/1,log3/2,log3/2,0,0)
七、词向量
语义之间的相似度
我们 爬山 运动 昨天
我们: [0,1,0,0,0,0]
爬山: [0,0,1,0,0,0]
运动: [1,0,0,0,0,0]
昨天: [0,0,0,1,0,0]
1、欧式距离
d(我们,爬山) = d(爬山,运动) = ..... = 2^(1/2)
说明欧式距离相似度针对one-hot不适合做两个词语义之间的相似度的,因为都是2^(1/2)
2、余弦距离
d(我们,爬山) = d(我们,运动) = d(运动,爬山) = .... = 0
说明余弦相似度针对one-hot不适合做两个词语义之间的相似度的,因为都是0
所以如果单词采用one-hot表示,不能表达单词之间的相似度
还存在另一个问题:稀疏性
向量的大小是与词典的大小相同的,如果词典比较大,句子比较短,会出现大量的0
小结:
onehot不能表示语义之间的相似性,存在稀疏性
从one-hot 表示 到 分布式 表示
One-hot 表示:
我们: [1,0,0,0,0,0,0]
爬山: [0,0,1,0,0,0,0]
运动: [0,0,0,0,0,0,1]
昨天: [0,0,0,0,0,1,0]
分布式表示:
我们: [0.1,0.2,0.4,0.2]
爬山: [0.2,0.3,0.7,0.1]
运动: [0.2,0.3,0.6,0.2]
昨天: [0.5,0.9,0.1,0.3]
one-hot 向量长度是词典长度,分布式向量长度是自己定义的长度,可以为100,200,300,解决了稀疏性问题
针对分布式表示,计算两个词之间的相似度:
d(我们,爬山) = (0.12)^(1/2)
d(运动,爬山) = (0.02)^(1/2)
d(我们,爬山) > d(运动,爬山)
分布式表示方法-针对单词,称为词向量(Word2Vec)
词向量是分布式表示方法的一种
Q1:100维的One-hot表示法最多可以表达多少个不同的单词? -100个
Q2:100维的分布式表示法最多可以表达多少个不同的单词? -无数个
分布式表示是怎么学习来的?
学习词向量:
输入(Input):字符串(将所有文本的分词之后的单词汇总集合) -> 深度模型(Skip-Gram、Glove、CBOW、RNN、LSTM、MF-矩阵分解、Gaussian Embedding)训练长短 -> 词的分布式表示 事先需要定义一下词向量维度
词向量(Word2vec)某种意义上理解成词的意思(Meaning)
从词向量到句向量:
"我们去运动"
我们:(0.1,0.2,0.1,0.3)
去:(0.3,0.2,0.15,0.2)
运动:(0.2,0.15,0.4,0.7)
"我们去运动" 句子向量:
平均法则:
sentence_embedding = avg_embedding = (0.6/3,0.55/3,0.65/3,1.2/3) = (0.2,0.18,0.22,0.4)
八、QA System
How do you like NLP?
Question ----> 相似度匹配 ------> 知识库(<Question1,answer1>,<Question2,answer2>.....)
<---- 返回相似度比较高的 <------
存在问题:每个相似度需要计算O(N) 不满足实时的要求
核心思路:层次过滤思想
Question -----> 过滤器 ----> 知识库(<question1,answer1>,<question2,answer2>..) ----> 相似度匹配(<question2,answer2>,<question18,answer18>.....)
过滤器主要做法--倒排表
背景:搜索引擎爬取文档
例如:四个文档
doc_1、我们、今天、运动
doc_2、我们、昨天、运动 -----> 词典:[我们、今天、运动、昨天、上、课、什么] ---> 我们:[doc_1,dic_2],今天:[doc_1],运动:[doc_1,doc_2],昨天:[doc_2] .....
doc_3、你们、上课
doc_4、你们、上、什么、课
如:百度的搜索引擎:
用户输入:运动 如果按照原来的方法需要遍历所有文档,但是按照倒排表只需要遍历doc_1,doc_2,按照pagerank值展示出来相关网页即可
用户输入句子: 我们 课
我们: [doc_1,doc_2]
课:[doc_3,doc_4]
我们 & 上课 = None
直接返回: [doc_1,doc_2,doc_3,doc_4]
"How do you like NLPCamp?"
Question -----> 过滤器 ----> 知识库(<question1,answer1>,<question2,answer2>..) ----> 相似度匹配(<question2,answer2>,<question18,answer18>.....)
How do
you like => 包含这些词的Question(原100,筛选之后可能剩余20, 至少包含其中一个单词)
MLPCamp