文本生成评价指标

1.BLEU

BLEU,全称为bilingual evaluation understudy,一般用于机器翻译和文本生成的评价,比较候选译文和参考译文里的重合程度,重合程度越高就认为译文质量越高,取值范围为[0,1]。

优点

  • 它的易于计算且速度快,特别是与人工翻译模型的输出对比;
  • 它应用范围广泛,这可以让你很轻松将模型与相同任务的基准作对比。

缺点

  • 它不考虑语义,句子结构
  • 不能很好地处理形态丰富的语句(BLEU原文建议大家配备4条翻译参考译文)
  • BLEU 指标偏向于较短的翻译结果(brevity penalty 没有想象中那么强)

1.1 完整式子

BLEU完整式子为:

1.2 $BP$

目的:$n-gram$匹配度可能会随着句子长度的变短而变好,比如,只翻译了一个词且对了,那么匹配度很高,为了避免这种评分的偏向性,引入长度惩罚因子

Brevity Penalty为长度惩罚因子,其中$l_c$表示机器翻译的译文长度,$l_s$表示参考答案的有效长度

1.3 $P_n$

人工译文表示为$s_j$,其中$j \in M$,$M$表示共有$M$个参考答案
翻译译文表示$c_i$,其中$i \in E$,$E$表示共有$E$个翻译
$n-gram$表示$n$个单词长度的词组集合,令$k$ 表示第$k$ 个词组,总共$K$个
$h_k(c_i)$表示第$k$个词组在翻译译文$c_i$出现的次数
$h_k(s_{j})$表示第$k$个词组在参考答案$s_{j}$出现的次数

举例如下,例如:

    原文:今天天气不错

    机器译文:It is a nice day today

    人工译文:Today is a nice day

$1-gram$:

 可以看到机器译文一共6个词,有5个词语都命中的了参考译文,$P_1=\frac{5}{6}$

$3-gram$:

机器译文一共可以分为4个$3-gram$的词组,其中有2个可以命中参考译文,那么$P_3=\frac{2}{4}$

1.4 $W_n$

$W_n$表示$P_n$的权重,一般为加权平均,即$W_n=\frac{1}{N}$,其中$N$为$gram$的数量,一般不大于4

2 ROUGE

Recall-Oriented Understudy for Gisting Evaluation,可以看做是BLEU 的改进版,专注于召回率而非精度。换句话说,它会查看有多少个参考译句中的 n 元词组出现在了输出之中。

ROUGE大致分为四种(常用的是前两种): - ROUGE-N (将BLEU的精确率优化为召回率) - ROUGE-L (将BLEU的n-gram优化为公共子序列) - ROUGE-W (将ROUGE-L的连续匹配给予更高的奖励) - ROUGE-S (允许n-gram出现跳词(skip))

注意:

关于rouge包给出三个结果,而论文只有一个值,比如

1
[{'rouge-1': {'r': 1.0, 'p': 1.0, 'f': 0.999999995}, 'rouge-2': {'r': 0.0, 'p': 0.0, 'f': 0.0}, 'rouge-l': {'r': 1.0, 'p': 1.0, 'f': 0.999999995}}]

用“r”,recall就好了

3 NIST

4 METEOR

5 TER

参考文献

https://blog.csdn.net/qq_36533552/article/details/107444391

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

https://arxiv.org/pdf/2006.14799.pdf

https://www.cnblogs.com/by-dream/p/7679284.html

https://blog.csdn.net/qq_30232405/article/details/104219396

https://blog.csdn.net/u013521274/article/details/89460322

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

attention seq2seq

1.结构

左边为encoder,对输入文本编码,右边为decoder,解码并应用。

整个流程的图解可以参考https://blog.csdn.net/weixin_44388679/article/details/102575223 中的“四、图解Attention Seq2Seq”,非常详细。

2.Teacher Forcing

在训练阶段,如果使用Teacher Forcing策略,那么目标句子单词的word embedding使用真值,否则使用预测结果;至于预测阶段不能使用Teacher Forcing。

beam search本质为介于蛮力与贪心之间的策略。对于贪心,每一级的输出只选择top1的结果作为下一级输入,然后top1的结果只是局部最优,不一定是全局最优,精度可能较低。对于蛮力,每级将全部结果输入下级,假设$L$为词表大小,那么最后一级的数据量为$L^{m}$,$m$为decoder 的cell数量,计算效率太低。对于beam search,每级选择top k作为下级输入,综合了效率和精度。

4 常见问题

0 为什么rnn based seq2seq不需要额外添加位置信息?

天然有位置信息(迭代顺序)

1 为什么rnn based seq2seq输入输出长度可变?

因为rnn based seq2seq是迭代进行的,所以长度可变

2 训练的时候要padding吗?

不用padding

参考

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

https://www.cnblogs.com/liuxiaochong/p/14399416.html

https://blog.csdn.net/thriving_fcl/article/details/74853556

词语的文本相似度

一.基于词典

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

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

文本表示

文本表示的表示形式可以是单一数值(基本没人用),可以是向量(目前主流),好奇有没有高纬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/


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