词语的文本相似度

一.基于词典

人为构建,比较主观,不利于维护

1.1 基于词林

1.1.1 结构

扩展版同义词词林分为5层结构,如图,随着级别的递增,词义刻画越来越细,到了第五层,每个分类里词语数量已经不大,很多只有一个词语,已经不可再分,可以称为原子词群、原子类或原子节点。不同级别的分类结果可以为自然语言处理提供不同的服务,例如第四层的分类和第五层的分类在信息检索、文本分类、自动问答等研究领域得到应用。有研究证明,对词义进行有效扩展,或者对关键词做同义词替换可以明显改善信息检索、文本分类和自动问答系统的性能。

下载后的词典文件如下所示:

1
2
3
4
5
Aa01A01= 人 士 人物 人士 人氏 人选
Aa01A02= 人类 生人 全人类
Aa01A03= 人手 人员 人口 人丁 口 食指
Aa01A04= 劳力 劳动力 工作者
Aa01A05= 匹夫 个人

表中的编码位是按照从左到右的顺序排列。第八位的标记有3 种,分别是“=”、“#”、“@”, “=”代表“相等”、“同义”。末尾的“#”代表“不等”、“同类”,属于相关词语。末尾的“@”代表“自我封闭”、“独立”,它在词典中既没有同义词,也没有相关词。

源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
class WordSimilarity2010(SimilarBase):
'''
本类根据下面的论文方法:
基于同义词词林的词语相似度计算方法,田久乐, 赵 蔚(东北师范大学 计算机科学与信息技术学院, 长春 130117 )
计算两个单词所有编码组合的相似度,取最大的一个
'''

def __init__(self):
super(WordSimilarity2010, self).__init__()
self.a = 0.65
self.b = 0.8
self.c = 0.9
self.d = 0.96
self.e = 0.5
self.f = 0.1
self.degree = 180
self.PI = math.pi

def similarity(self, w1, w2):
'''
判断两个词的相似性。
:param w1: [string]
:param w2: [string]
:return: [float]0~1之间。
'''

code1 = self._data.get(w1, None)
code2 = self._data.get(w2, None)

if not code1 or not code2:
return 0 # 只要有一个不在库里则代表没有相似性。

# 最终返回的最大相似度
sim_max = 0

# 两个词可能对应多个编码
for c1 in code1:
for c2 in code2:
cur_sim = self.sim_by_code(c1, c2)
# print(c1, c2, '的相似度为:', cur_sim)
if cur_sim > sim_max:
sim_max = cur_sim

return sim_max

def sim_by_code(self, c1, c2):
"""
根据编码计算相似度
"""

# 先把code的层级信息提取出来
clayer1 = self._parse_code(c1)
clayer2 = self._parse_code(c2)

common_layer = self.get_common_layer(clayer1,clayer2)
length = len(common_layer)

# 如果有一个编码以'@'结尾,那么表示自我封闭,这个编码中只有一个词,直接返回f
if c1.endswith('@') or c2.endswith('@') or 0 == length:
return self.f

cur_sim = 0
if 6 <= length:
# 如果前面七个字符相同,则第八个字符也相同,要么同为'=',要么同为'#''
if c1.endswith('=') and c2.endswith('='):
cur_sim = 1
elif c1.endswith('#') and c2.endswith('#'):
cur_sim = self.e
else:
k = self.get_k(clayer1, clayer2)
n = self.get_n(common_layer)
if 1 == length:
cur_sim = self.sim_formula(self.a, n, k)
elif 2 == length:
cur_sim = self.sim_formula(self.b, n, k)
elif 3 == length:
cur_sim = self.sim_formula(self.c, n, k)
elif 4 == length:
cur_sim = self.sim_formula(self.d, n, k)

return cur_sim

def sim_formula(self, coeff, n, k):
"""
计算相似度的公式,不同的层系数不同
"""
return coeff * math.cos(n * self.PI / self.degree) * ((n - k + 1) / n)

def get_common_layer(self, ca, cb):
'''
返回相应的layer层
:param ca: [list(str)] 分解后的编码。
:param cb: [list(str)] 分解后的编码。
:return: [list(str)]列表代表相应的根编码。
'''
common_layer = []

for i, j in zip(ca, cb):
if i == j:
common_layer.append(i)
else:
break
return common_layer

def get_k(self, c1, c2):
"""
返回两个编码对应分支的距离,相邻距离为1
"""
if c1[0] != c2[0]:
return abs(ord(c1[0]) - ord(c2[0]))
elif c1[1] != c2[1]:
return abs(ord(c1[1]) - ord(c2[1]))
elif c1[2] != c2[2]:
return abs(int(c1[2]) - int(c2[2]))
elif c1[3] != c2[3]:
return abs(ord(c1[3]) - ord(c2[3]))
else:
return abs(int(c1[4]) - int(c2[4]))

def get_n(self, common_layer):
'''
返回相应结点下有多少个同级子结点。
:param common_layer: [listr(str)]相同的结点。
:return: int
'''

end_node = self._code_tree
for t_node_name in common_layer:
end_node = end_node[t_node_name]

if not isinstance(end_node, dict):
return end_node
return len(end_node.keys())

1.1.2 使用

环境准备:pip install WordSimilarity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from word_similarity import WordSimilarity2010
import time

ws_tool = WordSimilarity2010()
start = time.time()
b_a = "联系方式"
b_b = "电话"
sim_b = ws_tool.similarity(b_a, b_b)
print(b_a, b_b, '相似度为', sim_b)
end = time.time()
print("运行时间:"+str(end-start))
b_a = "手机"
b_b = "电话"
sim_b = ws_tool.similarity(b_a, b_b)
print(b_a, b_b, '相似度为', sim_b)
end = time.time()
print("运行时间:"+str(end-start))
1
2
3
4
联系方式 电话 相似度为 0
运行时间:5.793571472167969e-05
手机 电话 相似度为 0.30484094213212237
运行时间:0.0001442432403564453

1.2 基于知网与词林的词语语义相似度计算

1.2.1 原理

综合了词林cilin与知网hownet的相似度计算方法,采用混合策略,混合策略具体可以参考源码,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from hownet.howNet import How_Similarity
from cilin.V3.ciLin import CilinSimilarity

class HybridSim():
'''
混合相似度计算策略。使用了词林与知网词汇量的并集。扩大了词汇覆盖范围。
'''
ci_lin = CilinSimilarity() # 实例化词林相似度计算对象
how_net = How_Similarity() # 实例化知网相似度计算对象
Common = ci_lin.vocab & how_net.vocab
A = how_net.vocab - ci_lin.vocab
B = ci_lin.vocab - how_net.vocab

@classmethod
def get_Final_sim(cls, w1, w2):
lin = cls.ci_lin.sim2018(w1, w2) if w1 in cls.ci_lin.vocab and w2 in cls.ci_lin.vocab else 0
how = cls.how_net.calc(w1, w2) if w1 in cls.how_net.vocab and w2 in cls.how_net.vocab else 0

if w1 in cls.Common and w2 in cls.Common: # 两个词都被词林和知网共同收录。
# print('两个词都被词林和知网共同收录。', end='\t')
# print(w1, w2, '词林改进版相似度:', lin, end='\t')
# print('知网相似度结果为:', how, end='\t')
return lin * 1 + how * 0 # 可以调节两者的权重,以获取更优结果!!

if w1 in cls.A and w2 in cls.A: # 两个词都只被知网收录。
return how
if w1 in cls.B and w2 in cls.B: # 两个词都只被词林收录。
return lin

if w1 in cls.A and w2 in cls.B: # 一个只被词林收录,另一个只被知网收录。
print('触发策略三,左词为知网,右词为词林')
same_words = cls.ci_lin.code_word[cls.ci_lin.word_code[w2][0]]
if not same_words:
return 0.2
all_sims = [cls.how_net.calc(word, w1) for word in same_words]
print(same_words, all_sims, end='\t')
return max(all_sims)

if w2 in cls.A and w1 in cls.B:
print('触发策略三,左词为词林,右词为知网')
same_words = cls.ci_lin.code_word[cls.ci_lin.word_code[w1][0]]
if not same_words:
return 0.2
all_sims = [cls.how_net.calc(word, w2) for word in same_words]
print(w1, '词林同义词有:', same_words, all_sims, end='\t')
return max(all_sims)

if w1 in cls.A and w2 in cls.Common:
print('策略四(左知网):知网相似度结果为:', how)
same_words = cls.ci_lin.code_word[cls.ci_lin.word_code[w2][0]]
if not same_words:
return how
all_sims = [cls.how_net.calc(word, w1) for word in same_words]
print(w2, '词林同义词有:', same_words, all_sims, end='\t')
return 0.6 * how + 0.4 * max(all_sims)

if w2 in cls.A and w1 in cls.Common:
print('策略四(右知网):知网相似度结果为:', how)
same_words = cls.ci_lin.code_word[cls.ci_lin.word_code[w1][0]]
if not same_words:
return how
all_sims = [cls.how_net.calc(word, w2) for word in same_words]
print(same_words, all_sims, end='\t')
return 0.6 * how + 0.4 * max(all_sims)

if w1 in cls.B and w2 in cls.Common:
print(w1, w2, '策略五(左词林):词林改进版相似度:', lin)
same_words = cls.ci_lin.code_word[cls.ci_lin.word_code[w1][0]]
if not same_words:
return lin
all_sims = [cls.how_net.calc(word, w2) for word in same_words]
print(w1, '词林同义词有:', same_words, all_sims, end='\t')
return 0.6 * lin + 0.4 * max(all_sims)

if w2 in cls.B and w1 in cls.Common:
print(w1, w2, '策略五(右词林):词林改进版相似度:', lin)
same_words = cls.ci_lin.code_word[cls.ci_lin.word_code[w2][0]]
if not same_words:
return lin
all_sims = [cls.how_net.calc(word, w1) for word in same_words]
print(w2, '词林同义词有:', same_words, all_sims, end='\t')
return 0.6 * lin + 0.4 * max(all_sims)

print('对不起,词语可能未收录,无法计算相似度!')
return -1

1.2.2 使用

参考https://github.com/yaleimeng/Final_word_Similarity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from Hybrid_Sim import HybridSim
from Pearson import *

import time


if __name__ == '__main__':

print('词林词汇量', len(HybridSim.ci_lin.vocab ),'\t知网词汇量', len(HybridSim.how_net.vocab))
print('两者总词汇量',len(HybridSim.ci_lin.vocab | HybridSim.how_net.vocab),'\t重叠词汇量', len(HybridSim.Common))
b_a = "联系方式"
b_b = "电话"
start = time.time()
hybrid = HybridSim.get_Final_sim(b_a, b_a)
end = time.time()
print(b_a+" "+b_b+"相似度为:", hybrid)
print("运行时间:"+str(end-start))
b_a = "手机"
b_b = "电话"
start = time.time()
hybrid = HybridSim.get_Final_sim(b_a, b_a)
end = time.time()
print(b_a+" "+b_b+"相似度为:", hybrid)
print("运行时间:"+str(end-start))

1
2
3
4
5
6
7
词林词汇量 77498 	知网词汇量 53336
两者总词汇量 85817 重叠词汇量 45017
对不起,词语可能未收录,无法计算相似度!
联系方式 电话相似度为: -1
运行时间:3.504753112792969e-05
手机 电话相似度为: 1.0
运行时间:0.019332408905029297

二.基于词向量

基于样本构建,利于维护

2.1 基于word2vec

2.2.1 原理

word2vec的原理和词向量获取过程不在此赘述,在本部分主要讲解基于word2vec的词向量如何计算词语相似度。源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def similarity(self, w1, w2):
"""Compute cosine similarity between two keys.

Parameters
----------
w1 : str
Input key.
w2 : str
Input key.

Returns
-------
float
Cosine similarity between `w1` and `w2`.

"""
return dot(matutils.unitvec(self[w1]), matutils.unitvec(self[w2]))

2.2.2 使用

训练

1
2
3
4
5
6
7
8
9
10
11
from gensim.models.word2vec import Word2Vec
import pandas as pd
from gensim import models
import jieba
###train
data=pd.read_csv(data_path)
sentences=data.tolist()
model= Word2Vec()
model.build_vocab(sentences)
model.train(sentences,total_examples = model.corpus_count,epochs = 5)
model.save(model_path)

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from gensim import models
import time

if __name__ == '__main__':
model=models.Word2Vec.load(model_path)
start = time.time()
b_a = "联系方式"
b_b = "电话"
sim_b = model.wv.n_similarity(b_a, b_b)
end = time.time()
start = time.time()
print(b_a, b_b, '相似度为', sim_b)
print("运行时间:" + str(end - start))
b_a = "手机"
b_b = "电话"
sim_b = model.wv.n_similarity(b_a, b_b)
end = time.time()
print(b_a, b_b, '相似度为', sim_b)
print("运行时间:" + str(end - start))
1
2
3
4
联系方式 电话 相似度为 -0.014857853
运行时间:-4.76837158203125e-07
手机 电话 相似度为 0.1771852
运行时间:0.0004227161407470703

参考文献

https://blog.csdn.net/sinat_33741547/article/details/80016713

https://github.com/yaleimeng/Final_word_Similarity

bert(Pre-training of Deep Bidirectional Transformers for Language Understanding)

https://arxiv.org/abs/1810.04805

1 结构

整体结构如上图,基本单元为Transformer 的encoder部分。作者对结构的描述为:BERT’s model architecture is a multi-layer bidirectional Transformer encoder。

2 Input/Output Representations

[CLS]表征句子开始,[SEP]表示句子结束以及分割两个句子

Token Embedding为词向量的表示,Position Embedding为位置信息,Segment Embedding表示A,B两句话,最后的输入向量为三者相加。比起transformer多一个Segment Embedding。

具体例子:https://www.cnblogs.com/d0main/p/10447853.html

3 预训练任务

1 Masked LM

standard conditional language models can only be trained left-to-right or right-to-left , since bidirectional conditioning would allow each word to indirectly “see itself”.In order to train a deep bidirectional representation,MLM

The training data generator chooses 15% of the token positions at random for prediction. If the i-th token is chosen, we replace the i-th token with (1) the [MASK] token 80% of the time (2) a random token 10% of the time (3) the unchanged i-th token 10% of the time.

2 Next Sentence Prediction (NSP)

In order to train a model that understands sentence relationships

choosing the sentences A and B for each pretraining example, 50% of the time B is the actual next sentence that follows A (labeled as IsNext), and 50% of the time it is a random sentence from the corpus (labeled as NotNext).

4 Fine-tuning BERT

For each task, we simply plug in the task specific inputs and outputs into BERT and finetune all the parameters end-to-end.

输入: 可以为句子对或者单句,取决于特定任务

输出:At the output, the token representations are fed into an output layer for token level tasks, such as sequence tagging or question answering, and the [CLS] representation is fed into an output layer for classification, such as entailment or sentiment analysis.

5 常见问题

1 bert为什么双向,gpt单向?

1.结构的不同

因为BERT用了transformer的encoder,在编码某个token的时候同时利用了其上下文的token,但是gptT用了transformer的decoder,只能利用上文

2.预训练任务的不同

2 为什么bert长度固定?

因为bert是基于transformer encoder的,不同位置的词语都是并行的,所以长度要提前固定,不可变

bert的输入输出长度为max_length,大于截断,小于padding,max_length的最大值为512

3 为什么bert需要补充位置信息?

因为是并行,不像迭代,没有天然的位置信息,需要补充position embedding。

 NLP PTM
  
 PTM

Tokenization

对于中文和英文而言,由于语言差异导致算法也有差异。对于中文,存在字粒度和词粒度。对于英文,存在三个级别的粒度,character level,subword level,word level。下面主要阐述中文的词粒度和英文的subword level。

一、中文

1.1 原理

https://zhuanlan.zhihu.com/p/146792308

1.2 常见中文分词工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# #####stanfordcorenlp
from timeit import default_timer as timer
from stanfordcorenlp import StanfordCoreNLP
tic = timer()
path="XXXXX"
nlp_zh = StanfordCoreNLP(path,lang='zh')#模型文件路径
sentence = "搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来"
tic = timer()
print ('Tokenize:', nlp_zh.word_tokenize(sentence))
toc = timer()
print(toc - tic) # 输出的时间,秒为单位
#########thulac
import thulac
thu1 = thulac.thulac(seg_only=True) #默认模式
tic = timer()
text = thu1.cut("搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来", text=True).split(" ") #进行一句话分词
toc = timer()
print(text)
print(toc - tic)
####jieba
import jieba
tic = timer()
print(jieba.lcut(str("搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来")))
toc = timer()
print(toc - tic)
1
2
3
4
5
6
Tokenize: ['搜索', '引擎', '会', '通过', '日志', '文件', '把', '用户', '每次', '检索', '使用', '的', '所有', '检索', '串都', '记录', '下来']
运行时间:22.68650701455772
['搜索引擎会', '通过', '日志', '文件', '把', '用户', '每', '次', '检索', '使用', '的', '所有', '检索', '串', '都', '记录', '下', '来']
运行时间:0.0016864966601133347
['搜索引擎', '会', '通过', '日志', '文件', '把', '用户', '每次', '检索', '使用', '的', '所有', '检索', '串', '都', '记录下来']
运行时间:0.9094752036035061

观察结果,可以看出thulac分词效率最高,jieba分词的精度和效率比较平衡,stanfordcorenlp分词粒度很细,但是速度慢

二、英文

SubWord算法如今已成为一个重要的NLP模型的提升算法。其主要优势如下:

1.word level存在OOV问题,一旦碰到就是back off to a dictionary,无法很好地处理未知和罕见词汇
2.Character level可以解决OOV,但是相比于 word-level , Character-level 的输入句子变长,使得数据变得稀疏,而且对于远距离的依赖难以学到,训练速度降低。
常见的SubWord算法有:BPE,WordPiece,Unigram Language Model等

2.1 BPE

全称为Byte Pair Encoding,算法来自paper《Neural Machine Translation of Rare Words with Subword Units》。

2.1.1 构建BPE subword词表

原理

  1. 准备足够大的训练语料
  2. 确定期望的subword词表大小
  3. 将单词拆分为字符序列并在末尾添加后缀“ </ w>”并且统计单词频率。停止符”</w>”的意义在于表示subword是词后缀。具体来说,不加”</w>”可以出现在词首,加了”</w>”只能位于词尾。例如,“ low”的频率为5,那么我们将其改写为“ l o w </ w>”:5
  4. 统计每一个连续字节对的出现频率,选择最高频者合并成新的subword
  5. 重复第4步直到达到第2步设定的subword词表大小或下一个最高频的字节对出现频率为1

注意,每次合并后词表可能出现3种变化:

  • +1,表明加入合并后的新字词,同时原来的2个子词还保留(2个字词都不是完全随着另一个字词的出现而紧跟着出现)
  • +0,表明加入合并后的新字词,同时原来的2个子词中一个保留,一个被消解(只有一个字词完全随着另一个字词的出现而紧跟着出现)
  • -1,表明加入合并后的新字词,同时原来的2个子词都被消解(2个字词同时连续出现)

例子:

训练语料为:

1
{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w e s t </w>': 6, 'w i d e s t </w>': 3}

Iter 1, 最高频连续字节对”e”和”s”出现了6+3=9次,合并成”es”,输出:

1
{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w es t </w>': 6, 'w i d es t </w>': 3}

Iter 2, 最高频连续字节对”es”和”t”出现了6+3=9次, 合并成”est”,输出:

1
{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w est </w>': 6, 'w i d est </w>': 3}

Iter 3, 以此类推,最高频连续字节对为”est”和”</w>” ,合并后输出:

1
{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w est</w>': 6, 'w i d est</w>': 3}

……

Iter n, 继续迭代直到达到预设的subword词表大小或下一个最高频的字节对出现频率为1。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import re, collections
def get_stats(vocab):
pairs = collections.defaultdict(int)
for word, freq in vocab.items():
symbols = word.split()
for i in range(len(symbols)-1):
pairs[symbols[i],symbols[i+1]] += freq
return pairs
def merge_vocab(pair, v_in):
v_out = {}
bigram = re.escape(' '.join(pair))
p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
for word in v_in:
w_out = p.sub(''.join(pair), word)
v_out[w_out] = v_in[word]
return v_out
vocab = {'l o w </w>' : 5, 'l o w e r </w>' : 2,
'n e w e s t </w>':6, 'w i d e s t </w>':3}
num_merges = 10
for i in range(num_merges):
pairs = get_stats(vocab)
best = max(pairs, key=pairs.get)
vocab = merge_vocab(best, vocab)
print(best)

1
2
3
4
5
6
7
8
9
10
('e', 's')
('es', 't')
('est', '</w>')
('l', 'o')
('lo', 'w')
('n', 'e')
('ne', 'w')
('new', 'est</w>')
('low', '</w>')
('w', 'i')

2.1.2 编解码

编码

1.将subword词表按照子词长度由大到小排序。

2.对于每个单词,遍历排好序的subword词表,寻找是否有token是当前单词的子字符串。最终,我们将迭代所有token,并将所有子字符串替换为token。

3.如果仍然有子字符串没被替换但所有token都已迭代完毕,则将剩余的子字符串替换为特殊token,如

例子:

1
2
3
4
5
6
7
8
9
10
# 给定单词序列
[“the</w>”, “highest</w>”, “mountain</w>”]

# 假设已有排好序的subword词表
[“errrr</w>”, “tain</w>”, “moun”, “est</w>”, “high”, “the</w>”, “a</w>”]

# 迭代结果
"the</w>" -> ["the</w>"]
"highest</w>" -> ["high", "est</w>"]
"mountain</w>" -> ["moun", "tain</w>"]

编码的计算量很大。对于已知数据,我们可以pre-tokenize所有单词,并在词典中保存单词和tokenize的结果。如果存在字典中不存在的未知单词,可以应用上述编码方法对单词进行tokenize,然后将新单词以及tokenize的结果添加到字典中备用。

解码

将所有的subword拼在一起。

例子:

1
2
3
4
5
# 编码序列
[[“the</w>”], [“high”, “est</w>”], [“moun”, “tain</w>”]]

# 解码序列
[“the</w>”, “highest</w>”, “mountain</w>”]

2.1.3 和embedding结合

1.构建词表,假设有subword词表:[“errrr</w>”, “tain</w>”, “moun”, “est</w>”, “high”, “the</w>”, “a</w>”]

2.编码,词语”highest”编码成[“high”, “est</w>”]

3.向量表示,$[E_{high},\ E_{est(/w)}]$]

https://www.cnblogs.com/d0main/p/10447853.html

2.2 WordPiece

算法来自于《Google’s Neural Machine Translation System: Bridging the Gap between Human and Machine Translation》

WordPiece算法可以看作是BPE的变种。不同点在于,WordPiece基于概率生成新的subword而不是下一最高频字节对。

2.2.1 原理

  1. 准备足够大的训练语料
  2. 确定期望的subword词表大小
  3. 将单词拆分成字符序列
  4. 基于第3步数据训练语言模型
  5. 从所有可能的subword单元中选择加入语言模型后能最大程度地增加训练数据概率的单元作为新的单元
  6. 重复第5步直到达到第2步设定的subword词表大小或概率增量低于某一阈值

2.3 ULM

2.4 char n-gram

https://arxiv.org/abs/1607.04606

2.5 Byte-Level BPE

《Neural Machine Translation with Byte-Level Subwords》

3.总结

我们在进行中文NLP任务的时候,目前基本都是字粒度;英文的话大多数是使用subword的wordpiece。

参考

https://zhuanlan.zhihu.com/p/112444056

https://arxiv.org/pdf/1508.07909.pdf

https://zhuanlan.zhihu.com/p/38130825

https://zhuanlan.zhihu.com/p/86965595

https://blog.csdn.net/zhangxiaolinxin/article/details/107052054?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_title~default-0.base&spm=1001.2101.3001.4242

https://www.cnblogs.com/mj-selina/p/13687291.html

TensorFlow,pytorch,cuda,cudnn,显卡驱动之间的区别以及对应关系

一.概念理解

显卡驱动连接操作系统与底层硬件。

CUDA和NVIDIA的显卡驱动程序完全是两个不同的概念。CUDA是NVIDIA推出的用于自家GPU的并行计算框架,也就是说CUDA只能在NVIDIA的GPU上运行,而且只有当要解决的计算问题是可以大量并行计算的时候才能发挥CUDA的作用。CUDA的本质是一个工具包(ToolKit)。

cuDNN是一个SDK,是一个专门用于神经网络的加速包,注意,它跟我们的CUDA没有一一对应的关系,即每一个版本的CUDA可能有好几个版本的cuDNN与之对应,但一般有一个最新版本的cuDNN版本与CUDA对应更好。

TensorFlow为谷歌推出的深度学习框架,pytorch是Facebook 推出的深度学习框架。

二.版本对应关系

深度学习框架基于GPU运算效率远高于CPU,但是需要满足框架的版本和cuda,cudnn以及显卡驱动版本匹配才可以正常工作。

tensorflow

pytorch

cuDNN与CUDA

CUDA和NVIDIA显卡驱动关系

三.常用命令

查看GPU型号

1
lspci | grep -i nvidia

查看NVIDIA驱动版本

1
cat /proc/driver/nvidia/version

Python 查看pytorch版本、判断CUDA是否可用

1
2
3
import torch 
print(torch.__version__)
print(torch.cuda.is_available())

查看cuda版本

1
2
cat /usr/local/cuda/version.txt
conda list | grep cuda

Tensorflow中查看GPU是否可用

1
2
import tensorflow as tf
tf.test.is_gpu_available()

四.参考文献

https://blog.csdn.net/caiguanhong/article/details/112184290

文本表示

文本表示的表示形式可以是单一数值(基本没人用),可以是向量(目前主流),好奇有没有高纬tensor表示的?下文是基于向量表示的。

1.词语表示

1.1 one hot

举个例子,有样本如下:

​ Jane wants to go to Shenzhen.

​ Bob wants to go to Shanghai.

基于上述两个文档中出现的单词,构建如下一个词典:

Vocabulary= [Jane, wants, to, go, Shenzhen, Bob, Shanghai]

那么wants 可以表示为

1
[0,1,0,0,0,0,0]

1.2 word embedding

词向量模型是考虑词语位置关系的一种模型。通过大量语料的训练,将每一个词语映射到高维度的向量空间当中,使得语意相似的词在向量空间上也会比较相近,举个例子,如

上表为词向量矩阵,其中行表示不同特征,列表示不同词,Man可以表示为

1
[-1,0.01,0.03,0.09]

性质:$emb_{Man}-emb_{Women}\approx emb_{King}-emb_{Queen}$

常见的词向量矩阵构建方法有,word2vec,GloVe

2.句子表示

2.1 词袋模型

词袋模型不考虑文本中词与词之间的上下文关系,仅仅只考虑所有词的权重。而权重与词在文本中出现的频率有关。

例句:

​ Jane wants to go to Shenzhen.

​ Bob wants to go to Shanghai.

基于上述两个文档中出现的单词,构建如下一个词典:

Vocabulary= [Jane, wants, to, go, Shenzhen, Bob, Shanghai]

那么上面两个例句就可以用以下两个向量表示,其值为该词语出现的次数:

1
2
[1,1,2,1,1,0,0]
[0,1,2,1,0,1,1]

2.2 Sentence Embedding

2.2.1 评价工具

SentEval is a popular toolkit to evaluate the quality of sentence embeddings.

2.2.2 常见方法

sentence BERT

BERT-flow

https://zhuanlan.zhihu.com/p/444346578

参考文献

https://zhuanlan.zhihu.com/p/353187575

https://www.jianshu.com/p/0587bc01e414

https://www.cnblogs.com/chenyusheng0803/p/10978883.html

fasttext

1、文本分类

1.1 n-gram

由于Bag of words不考虑词语的顺序,因此引入bag of n-gram。针对英文,词内的是char n-gram,用于词向量;词之间的是word n-gram,用于分类;对于中文,存在词粒度和字粒度。

举个例子,句子A为”今天天气真不错”,这里以词粒度举例,先分词为[“今天”,”天气”,”真“,”不错“]

uni-gram:今天 天气 真 不错

2-gram为:今天/天气 天气/真 真/不错

3-gram为:今天/天气/真 天气/真/不错

由于n-gram的量远比word大的多,完全存下所有的n-gram也不现实。FastText采用了hashing trick的方式,如下图所示:

用哈希的方式既能保证查找时O(1)的效率,又可能把内存消耗控制在O(buckets * dim)范围内。不过这种方法潜在的问题是存在哈希冲突,不同的n-gram可能会共享同一个embedding。如果buckets取的足够大,这种影响会很小。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def build_dataset(config, ues_word):
if ues_word:
tokenizer = lambda x: x.split(' ') # word-level
else:
tokenizer = lambda x: [y for y in x] # char-level
if os.path.exists(config.vocab_path):
vocab = pkl.load(open(config.vocab_path, 'rb'))
else:
vocab = build_vocab(config.train_path, tokenizer=tokenizer, max_size=MAX_VOCAB_SIZE, min_freq=1)
pkl.dump(vocab, open(config.vocab_path, 'wb'))
print(f"Vocab size: {len(vocab)}")

def biGramHash(sequence, t, buckets):
t1 = sequence[t - 1] if t - 1 >= 0 else 0
return (t1 * 14918087) % buckets

def triGramHash(sequence, t, buckets):
t1 = sequence[t - 1] if t - 1 >= 0 else 0
t2 = sequence[t - 2] if t - 2 >= 0 else 0
return (t2 * 14918087 * 18408749 + t1 * 14918087) % buckets

def load_dataset(path, pad_size=32):
contents = []
with open(path, 'r', encoding='UTF-8') as f:
for line in tqdm(f):
lin = line.strip()
if not lin:
continue
content, label = lin.split('\t')
words_line = []
token = tokenizer(content)
seq_len = len(token)
if pad_size:
if len(token) < pad_size:
token.extend([PAD] * (pad_size - len(token)))
else:
token = token[:pad_size]
seq_len = pad_size
# word to id
for word in token:
words_line.append(vocab.get(word, vocab.get(UNK)))

# fasttext ngram
buckets = config.n_gram_vocab
bigram = []
trigram = []
# ------ngram------
for i in range(pad_size):
bigram.append(biGramHash(words_line, i, buckets))
trigram.append(triGramHash(words_line, i, buckets))
# -----------------
contents.append((words_line, int(label), seq_len, bigram, trigram))
return contents # [([...], 0), ([...], 1), ...]

train = load_dataset(config.train_path, config.pad_size)
dev = load_dataset(config.dev_path, config.pad_size)
test = load_dataset(config.test_path, config.pad_size)
return vocab, train, dev, test

1.2 网络结构

fasttext

模型结构上word2vec的cbow模型很像

输入层:举个例子,输入文本”今天天气真不错”,词粒度的2-gram为

中间层:线形层+relu作为激活函数

输出层:为简单的线形层

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Model(nn.Module):
def __init__(self, config):
super(Model, self).__init__()
if config.embedding_pretrained is not None:
self.embedding = nn.Embedding.from_pretrained(config.embedding_pretrained, freeze=False)
else:
self.embedding = nn.Embedding(config.n_vocab, config.embed, padding_idx=config.n_vocab - 1)
self.embedding_ngram2 = nn.Embedding(config.n_gram_vocab, config.embed)
self.embedding_ngram3 = nn.Embedding(config.n_gram_vocab, config.embed)
self.dropout = nn.Dropout(config.dropout)
self.fc1 = nn.Linear(config.embed * 3, config.hidden_size)
# self.dropout2 = nn.Dropout(config.dropout)
self.fc2 = nn.Linear(config.hidden_size, config.num_classes)

def forward(self, x):

out_word = self.embedding(x[0])
out_bigram = self.embedding_ngram2(x[2])
out_trigram = self.embedding_ngram3(x[3])
out = torch.cat((out_word, out_bigram, out_trigram), -1)

out = out.mean(dim=1)
out = self.dropout(out)
out = self.fc1(out)
out = F.relu(out)
out = self.fc2(out)
return out

1.3 分层softmax

对于分类问题,神经网络的输出结果需要经过softmax将其转为概率分布后才可以利用交叉熵计算loss

由于普通softmax的计算效率比较低,计算效率为$O(Kd)$使用分层的softmax时间复杂度可以达到$dlogK$,$K$为分类的数量,$d$为向量的维度

1.3.1 普通softmax

假设输出为$Y_{pred}=[y_1,y_2,…,y_K]$,则$P_{y_i}$为

其中$y_i$的维度为$d$,从公式可以看出计算效率为$O(Kd)$

1.3.2 分层softmax

霍夫曼树可以参考 https://zhuanlan.zhihu.com/p/154356949

为什么要霍夫曼,普通的不行?

分层softmax核心思想为利用训练样本构建霍夫曼树,如下

fasttext

树的结构是根据不同类在样本中出现的频次构造的,即频次越大的节点距离根节点越近。$K$个不同的类组成所有的叶子节点,$K-1个$内部节点作为参数。从根节点到某个叶子节点$y_i$经过的节点和边形成一条路径,路径长度表示为 $L_{y_i}$,$n_{(y_i,j)}$表示路径上的节点,那么

从公式可以看出时间复杂度降低至$dlogK$。

以图中$y_2$为例:

从根节点走到叶子节点 $y_2$ ,实际上是在做了3次逻辑回归。

2.训练词向量

https://arxiv.org/abs/1607.04606

参考

https://arxiv.org/abs/1607.01759

https://zhuanlan.zhihu.com/p/32965521

https://blog.csdn.net/qq_27009517/article/details/80676022

http://alex.smola.org/papers/2009/Weinbergeretal09.pdf

https://arxiv.org/abs/1607.04606

fasttext工具 https://github.com/facebookresearch/fastText

transformer(attention is all your need)

1.We propose a new simple network architecture, the Transformer, based solely on attention mechanisms, dispensing with recurrence and convolutions entirely.

2.Experiments on two machine translation tasks show these models to be superior in quality while being more parallelizable and requiring significantly less time to train.

1 Positional Encoding

in order for the model to make use of the order of the sequence, we must inject some information about the relative or absolute position of the tokens in the sequence. There are many choices of positional encodings, learned and fixed [9]. In this work, we use sine and cosine functions of different frequencies:

详细可参考 https://wmathor.com/index.php/archives/1453/

2 Attention

其中不同颜色表示不同head,颜色深浅表示词的关联程度。

不同head表示不同应用场景 ,单一head表示某个场景下,各个字之间的关联程度

1 Scaled Dot-Product Attention

$d_{k}$ : keys of dimension

为什么scale?We suspect that for large values of dk, the dot products grow large in magnitude, pushing the softmax function into regions where it has extremely small gradients.

Mask

可以分为两类:Attention Mask和Padding Mask,接下来具体讲解。

1.Attention Mask

ensures that the predictions for position i can depend only on the known outputs at positions less than i.

sotfmax前要mask,上三角mask掉

2.Padding Mask

Padding位置上的信息是无效的,所以需要丢弃。

过程如下图示:

2 Multi-Head Attention

Multi-head attention allows the model to jointly attend to information from different representation subspaces at different positions.

3 Applications of Attention in our Model

1.encoder-decoder attention layers

结构:queries come from the previous decoder layer,and the memory keys and values come from the output of the encoder.

目的:This allows every position in the decoder to attend over all positions in the input sequence.

2.encoder contains self-attention layers

结构:keys, values and queries come from the same place

目的:Each position in the encoder can attend to all positions in the previous layer of the encoder.

3.self-attention layers in the decoder

结构:keys, values and queries come from the same place

目的:allow each position in the decoder to attend to all positions in the decoder up to and including that position

3 Encoder and Decoder Stacks

1 encoder

1).Input Embedding与Positional Encoding

2). multi-head attention

3). 残差连接与 Layer Normalization

4). FeedForward

5). 残差连接与 Layer Normalization

其中$ X_{hidden} \in \mathbb{R}^{batch_size \ \ seq_len \ \ embed_dim} $

2 decoder

我们先从 HighLevel 的角度观察一下 Decoder 结构,从下到上依次是:

  • Masked Multi-Head Self-Attention
  • Multi-Head Encoder-Decoder Attention
  • FeedForward Network

4 常见问题

1 并行化

训练encoder,decoder都并行,测试encoder并行,decoder不是并行

https://zhuanlan.zhihu.com/p/368592551

2 self-attention和普通attention的区别

取决于query和key是否在一个地方

3 Why Self-Attention

Motivating our use of self-attention we consider three desiderata.

1.One is the total computational complexity per layer.

2.Another is the amount of computation that can be parallelized, as measured by the minimum number of sequential operations required.

3.The third is the path length between long-range dependencies in the network

参考

https://arxiv.org/abs/1706.03762

大佬详解: https://jalammar.github.io/illustrated-transformer/

hexo_intro

1.部署

1
2
3
hexo clean 
hexo g
hexo d

2.创建文章

1
hexo new "XXX"

3.常见问题

Error: pandoc exited with code 7: pandoc: Unknown extension: smart

解决:卸载pandoc

1
npm un hexo-renderer-pandoc —save

error:spawn failed

1.删除.deploy_git文件夹

2.执行

1
git config --global core.autocrlf false

hexo 图片显示问题

1、在_config.yml设置post_asset_folder为true

hexo new “paper_name”时会创建paper_name.md和paper_name的文件夹,将图片放在paper_name的文件夹

2、安装插件asset-image
npm install https://github.com/CodeFalling/hexo-asset-image
3、设置图片为相对路径

注意修改图片路径中的 \ 为 / ,并且不带 . 或者 . /

  

分类任务的衡量指标

一、二分类

1.1 confusion matrix

1.2 accuracy

accuracy 衡量全局分类正确的数量占总样本的比例

1.3 precision

precision为预测正确正样本数占预测的全部正样本数的比例,即系统判定为正样本的正确率。通俗地说,假如医生给病人检查,医生判断病人有疾病,然后医生判断的正确率有多少。

1.4 recall

recall为预测正确的正样本数量占真实正样本数量的比例,即衡量正样本的召回比例。通俗说,假如有一批病人,医生能从中找出病人的比例

1.5 F1

由于precision和recall往往是矛盾的,因此为了综合考虑二者,引入F1,即为precision和recall的调和平均

当$precision$和$recall$的任一个值为0,$F_1$都为0

之所以采用调和平均,是因为调和平均数受极端值影响较大,更适合评价不平衡数据的分类问题

通用的F值表达式:

除了$F_1$分数之外,$F_2$ 分数和$F_{0.5}$分数在统计学中也得到大量的应用。其中,$F_2$分数中,召回率的权重高于精确率,而$F_{0.5}$分数中,精确率的权重高于召回率。

1.6 ROC

roc曲线:接收者操作特征(receiver operating characteristic), roc曲线上每个点反映某个阈值下的FPR和TPR的组合。

横轴:$FPR$,叫做假正类率,表示预测为正例但真实情况为反例的占所有真实情况中反例的比率,公式为$FPR=\frac{FP}{TN+FP}$。

纵轴:$TPR$ ,叫做真正例率,表示预测为正例且真实情况为正例的占所有真实情况中正例的比率,公式为​

$TPR=\frac{TP}{TP+FN}$。

1.7 AUC

$AUC$(Area under Curve):ROC曲线下的面积,数值可以直观评价分类器的好坏,值越大越好,对于二分类,结果介于0.5和1之间,1为完美分类器,0.5是因为二分类分类效果最差也是0.5。

二、多分类

2.1 混淆矩阵

2.2 accuracy

2.3 某个类别的precision,recall,F1

与二分类公式一样

2.4 系统的precision,recall,F1

系统的precision,recall,$F_1$需要综合考虑所有类别,即同时考虑猫、狗、猪的precision,recall,$F_1$。有如下几种方案:

2.4.1 Macro average

2.4.2 Weighted average

对macro的推广

2.4.3 Micro average

2.5 ROC

对于多分类分类器整体效果的ROC如上micro或者macro曲线,其余3条描述单个类别的分类效果。对于多分类,ROC上的点,同样是某个阈值下的FPR和TPR的组合。

对于多分类的$FPR$,$TPR$,有几种计算方式

a. micro average

b. macro average

2.6 AUC

$AUC$依旧为ROC曲线下的面积,对于多分类个人认为取值范围为[0,1]。

三.代码

accuracy,precision,recall,F1

1
2
3
4
5
from sklearn.metrics import precision_recall_fscore_support, accuracy_score
def eval_acc_f1(y_true, y_pred):
acc = accuracy_score(y_true, y_pred)
prf = precision_recall_fscore_support(y_true, y_pred, average="macro")
return acc, prf

ROC和AUC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# 引入必要的库
import numpy as np
import matplotlib.pyplot as plt
from itertools import cycle
from sklearn import svm, datasets
from sklearn.metrics import roc_curve, auc
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import label_binarize
from sklearn.multiclass import OneVsRestClassifier
from scipy import interp

# 加载数据
iris = datasets.load_iris()
X = iris.data
y = iris.target
# 将标签二值化
y = label_binarize(y, classes=[0, 1, 2])
# 设置种类
n_classes = y.shape[1]

# 训练模型并预测
random_state = np.random.RandomState(0)
n_samples, n_features = X.shape

# shuffle and split training and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.5,random_state=0)

# Learn to predict each class against the other
classifier = OneVsRestClassifier(svm.SVC(kernel='linear', probability=True,
random_state=random_state))
y_score = classifier.fit(X_train, y_train).decision_function(X_test)

# 计算每一类的ROC
fpr = dict()
tpr = dict()
roc_auc = dict()
for i in range(n_classes):
fpr[i], tpr[i], _ = roc_curve(y_test[:, i], y_score[:, i])
roc_auc[i] = auc(fpr[i], tpr[i])

# Compute micro-average ROC curve and ROC area(方法二)
fpr["micro"], tpr["micro"], _ = roc_curve(y_test.ravel(), y_score.ravel())
roc_auc["micro"] = auc(fpr["micro"], tpr["micro"])

# Compute macro-average ROC curve and ROC area(方法一)
# First aggregate all false positive rates
all_fpr = np.unique(np.concatenate([fpr[i] for i in range(n_classes)]))
# Then interpolate all ROC curves at this points
mean_tpr = np.zeros_like(all_fpr)
for i in range(n_classes):
mean_tpr += interp(all_fpr, fpr[i], tpr[i])
# Finally average it and compute AUC
mean_tpr /= n_classes
fpr["macro"] = all_fpr
tpr["macro"] = mean_tpr
roc_auc["macro"] = auc(fpr["macro"], tpr["macro"])

# Plot all ROC curves
lw=2
plt.figure()
plt.plot(fpr["micro"], tpr["micro"],
label='micro-average ROC curve (area = {0:0.2f})'
''.format(roc_auc["micro"]),
color='deeppink', linestyle=':', linewidth=4)

plt.plot(fpr["macro"], tpr["macro"],
label='macro-average ROC curve (area = {0:0.2f})'
''.format(roc_auc["macro"]),
color='navy', linestyle=':', linewidth=4)

colors = cycle(['aqua', 'darkorange', 'cornflowerblue'])
for i, color in zip(range(n_classes), colors):
plt.plot(fpr[i], tpr[i], color=color, lw=lw,
label='ROC curve of class {0} (area = {1:0.2f})'
''.format(i, roc_auc[i]))

plt.plot([0, 1], [0, 1], 'k--', lw=lw)
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Some extension of Receiver operating characteristic to multi-class')
plt.legend(loc="lower right")
plt.show()

参考资料:

https://blog.csdn.net/Orange_Spotty_Cat/article/details/80520839

https://zhuanlan.zhihu.com/p/147663370

https://zhuanlan.zhihu.com/p/81202617

https://zhuanlan.zhihu.com/p/266386193


:D 一言句子获取中...