• 记录下分词器是怎么做的
  • 主要参考来源tokenizer_summary和其他的做法

一、transformers里面的分词器

二、如何将句子分成一个个token

1、基于空格分词

  • 最简单的方法,使用空格分词,bert的pretokenizer就是这么做的
1
2
3
"Don't you love 🤗 Transformers? We sure do.".split(" ")
# 结果
["Don't", 'you', 'love', '🤗', 'Transformers?', 'We', 'sure', 'do.']
  • 上面的做法有一点不好,没有考虑词根的问题,如Transformers?Transformer是一样的,而且没有把标点符号单独提取出来

2、基于规则分词

  • 比如spacy和Moses
1
2
3
4
5
6
import spacy 
nlp = spacy.load("en")
document = nlp("Don't you love 🤗 Transformers? We sure do.")
[str(document[i]) for i in range(len(document))]
# 结果
['Do', "n't", 'you', 'love', '🤗', 'Transformers', '?', 'We', 'sure', 'do', '.']
  • 注意,基于空格或规则分词的方法,很容易导致词表大小爆炸

3、subword的分词

  • bert就是这样分词的
1
2
3
4
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
tokenizer.tokenize("Don't you love 🤗 Transformers? We sure do.")
# 结果
['don',"'",'t','you','love','[UNK]','transformers','?','we','sure', 'do','.']

三、BPE分词

  • byte pair encoding,字节对

  • BPE是怎么分词的呢

    • 首先需要一个pre-tokenizer,事先给定一个分词器,比如空格分词器、规则分词器
    • pre-tokenizer分词之后,统计每个token的词频
    • 再使用bpe创建一个base vocabulary(基本词汇表)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# bpe的base vocabulary,如网址给的例子,事实上应该包括26个英文字母、中文字符、阿拉伯数字等等
["b", "g", "h", "n", "p", "s", "u"]

# 下一步拿到pre-tokenizer的词表,如
("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)

# 将词表分成一个个字符
("h" "u" "g", 10), ("p" "u" "g", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "u" "g" "s", 5)

# 再将每个字符进行合并
# 先统计以"h"开头的字符有多少个,如"hu"有10+5个
# 再统计以"p"开头的字符有多少个,如"pu"有5+5个
# 最多次数的是,"ug",10+5+5=20个

# 所以将"ug"放入到base vocabulary中去,
["b", "g", "h", "n", "p", "s", "u", "ug"]

# 放入一个token之后,要把词表重新汇总,变成
("h" "ug", 10), ("p" "ug", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "ug" "s", 5)

# 进行下一个token的合并
  • 上面的base vocabulary该怎么选呢,比如将所有的unicode字符当成base vocabulary,对于英文来说,base vocabulary就有256个字符

WordPiece

四、HMM分词

  • 这是基于统计的分词方法,实际上也是个序列标注的问题
  • 我们假设输入的序列为$x = (x_1, \dots, x_n) = ( \text{假设这个一个序列} )$,那么$y=(y_1, \dots, y_n) = (\text{B,E,B,E,S,S,B,E})$
  • 其中,$y_i \in {(\text{B, M, E, M, S})}$,即某个字符是开始、中间、结尾、单个字符,就是个序列标注的问题
  • 那么写出条件概率,下面是一阶马尔科夫的概率
  • HMM是怎么分词的呢?
  • 假设我们已经有分好词的语料库,如下:
1
2
3
4
5
6
7
1986年 ,
十亿 中华 儿女 踏上 新 的 征 程 。
过去 的 一 年 ,
是 全国 各族 人民 在 中国 共产党 领导 下 ,
在 建设 有 中国 特色 的 社会主义 道路 上 ,
坚持 改革 、 开放 ,
团结 奋斗 、 胜利 前进 的 一 年 。
  • 根据上面的语料库进行统计
    • 对每个单词(用text.split(" "))得到的词语,标注单词,长度为1就是$\text{S}$,长度为2就是$\text{B,E}$,长度大于2就是$\text{B,M} ,\dots, \text{E}$
    • 统计五个矩阵
    • 第一个是state_list,即每个字符出现过的状态
    • 第二个是observepro_dict,即每个状态中,每个字符出现的次数
    • 第三个是count_dict,每个状态出现的次数
    • 第四个是startpro_dict,即每个状态,在词语中开头的频率,即只有$\text{B,S}$
    • 第五个是Transprob_dict,即每个状态,转到下一个状态的频率
  • 统计完之后,需要将频率转换成概率,应该是归一化,但是代码里面有取log
  • 当有新的序列进来时,如$x = (x_1, \dots, x_n) = ( \text{假设这个一个序列} )$
  • HMM的代码参考了github的写法
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
def viterbi(obs, states, start_p, trans_p, emit_p, State_list):
V = [{}]
path = {}
mem_path = [{}]

for y in State_list.get(obs[0],states):
V[0][y] = start_p[y] + emit_p[y].get(obs[0],MIN_FLOAT)
path[y] = [y]
mem_path[0][y] = ''

for t in range(1,len(obs)):
V.append({})
mem_path.append({})
newpath = {}
# 上一个字符,可能的状态,这里的if,判断这个状态会不会跳转到其他状态
prev_states = [ x for x in mem_path[t - 1].keys() if len(trans_p[x]) > 0 ]
# 记录,上一个字符的状态,可能转移的所有的状态
prev_states_expect_next = set( (y for x in prev_states for y in trans_p[x].keys()) )
# 以往语料库,这个出现过的状态,但是没有在prev_states_expect_next
obs_states = set( State_list.get(obs[t], states) ) & prev_states_expect_next
# 如果obs_states是空的话,就重置为states
#
if not obs_states:
obs_states = prev_states_expect_next if prev_states_expect_next else states
print(obs[t], "not obs_states")

for y in obs_states: #从y0 -> y状态的递归
# 找到当前可能的状态,这个词的概率
# a = emit_p[y].get(obs[t], MIN_FLOAT)

(prob, state) = max([
(V[t-1][y0]+trans_p[y0].get(y,MIN_INF) + emit_p[y].get(obs[t],MIN_FLOAT) ,y0) for y0 in prev_states
])

V[t][y] =prob
newpath[y] = path[state] + [y]
mem_path[t][y] = state

path = newpath #记录状态序列
print(len(mem_path))
print(mem_path[-1].keys())
last = [(V[-1][y], y) for y in ['E', 'M', 'B']]
# if len(last)==0:
# print obs
prob, state = max(last)
route = [None] * len(obs)
i = len(obs) - 1
while i >= 0:
route[i] = state
state = mem_path[i][state]
i -= 1
#(prob, state) = max([(V[len(obs) - 1][y], y) for y in states]) #在最后一个位置,以y状态为末尾的状态序列的最大概率
return (prob,route) #返回概率和状态序列

五、CRF分词