分词的一般流程
在使用神经网络处理自然语言处理任务时,我们首先需要对数据进行预处理,将数据从字符串转换为神经网络可以接受的格式,一般会分为如下几步:
(1)分词:使用分词器对文本数据进行分词(字、字词)得到token;
(2)构建词典:根据数据集分词的结果,构建词典映射,形成一个vocabulary dict用于存放每一个token(这一步并不绝对,如果采用预训练词向量,词典映射要根据词向量文件进行处理),vocabulary dict形式如下:
{token1:0,token2:1,token3:2,token4:4........}
(3)数据转换:根据构建好的词典,将分词处理后的数据做映射,将文本序列转换为数字序列;
(4)数据填充与截断:在以batch输入到模型的方式中,需要对过短的数据进行填充,过长的数据进行截断,保证数据长度符合模型能接受的范围,同时batch内的数据维度大小一致。
分词的三种粒度
(1)粗糙的有词粒度(word)
例如“我来做中国”会被分成[我,来,自,中国]
优点:能够保存较为完整的语义信息
缺点:
1、词汇表会非常大,大的词汇表对应模型需要使用很大的embedding层,这既增加了内存,又增加了时间复杂度。通常,transformer模型的词汇量很少会超过50,000,特别是如果仅使用一种语言进行预训练的话,而transformerxl使用了常规的分词方式,词汇表高达267735;
2、 word-level级别的分词略显粗糙,无法发现更加细节的语义信息,例如模型学到的“old”, “older”, and “oldest”之间的关系无法泛化到“smart”, “smarter”, and “smartest”。
3、word-level级别的分词对于拼写错误等情况的鲁棒性不好;
4、 oov(out of vocabulary)问题不好解决,例如分词时数据集中只有cat,测试时遇到cats边无法处理
(2)精细的有字粒度(char)
例如“我来做中国”会被分成[我,来,自,中,国]
一个简单的方法就是将word-level的分词方法改成 char-level的分词方法,对于英文来说,就是字母界别的,比如 "China"拆分为"C","h","i","n","a",对于中文来说,"中国"拆分为"中","国",
优点:
1、这可以大大降低embedding部分计算的内存和时间复杂度,以英文为例,英文字母总共就26个,中文常用字也就几千个。
2、char-level的文本中蕴含了一些word-level的文本所难以描述的模式,因此一方面出现了可以学习到char-level特征的词向量FastText,另一方面在有监督任务中开始通过浅层CNN、HIghwayNet、RNN等网络引入char-level文本的表示;
缺点:
1、但是这样使得任务的难度大大增加了,毕竟使用字符大大扭曲了词的意义,一个字母或者一个单中文字实际上并没有任何语义意义,单纯使用char-level往往伴随着模型性能的下降;
2、增加了输入的计算压力,原本”I love you“是3个embedding进入后面的cnn、rnn之类的网络结构,而进行char-level拆分之后则变成 8个embedding进入后面的cnn或者rnn之类的网络结构,这样计算起来非常慢;
(3)现在最常用的是子词粒度sub-word,介于两者之间。
为了两全其美,transformer使用了混合了char-level和word-level的分词方式,称之为subword-level的分词方式。
subword-level的分词方式遵循的原则是:尽量不分解常用词,而是将不常用词分解为常用的子词,
例如,"annoyingly"可能被认为是一个罕见的单词,并且可以分解为"annoying"和"ly"。"annoying"并"ly"作为独立的子词会更频繁地出现,同时,"annoyingly"是由"annoying"和"ly"这两个子词的复合含义构成的复杂含义,这在诸如土耳其语之类的凝集性语言中特别有用,在该语言中,可以通过将子词串在一起来形成(几乎)任意长的复杂词。
subword-level的分词方式使模型相对合理的词汇量(不会太多也不会太少),同时能够学习有意义的与上下文无关的表示形式(另外,subword-level的分词方式通过将词分解成已知的子词,使模型能够处理以前从未见过的词(oov问题得到了很大程度上的缓解)。
subword-level又分为不同的切法,这里就到huggingface的tokenizers的实现部分了,常规的char-level或者word-level的分词用spacy,nltk之类的工具包就可以胜任了。
subword的分词往往包含了两个阶段,一个是encode阶段,形成subword的vocabulary dict,一个是decode阶段,将原始的文本通过subword的vocabulary dict 转化为 token的index然后进入embedding层。主要是因为不同的model可能在分token层面做了一些微调,并且根据使用的语料的不同,最后的subword vocabulary dict也会不同。
transfomers中tokenizer使用示例
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
sentence = "我是中国人"
# 查看分词结果
tokens = tokenizer.tokenize(sentence)
print("tokens:\n", tokens) # ['我', '是', '中', '国', '人']
# 查看词典。词典是在预训练模型时就构建好了的
# print("词典\n", tokenizer.vocab)
# 根据词典将词序列转化为数字序列
ids = tokenizer.convert_tokens_to_ids(tokens=tokens)
print("ids\n", ids) # [2769, 3221, 704, 1744, 782]
# 直接调用encode方法也能实现上述目标,但会自动增加起始和终止数字序号
ids = tokenizer.encode(sentence)
print("ids\n", ids) #[101, 2769, 3221, 704, 1744, 782, 102]
#填充
ids = tokenizer.encode(sentence, padding="max_length", max_length=12)
print("ids\n", ids) # [101, 2769, 3221, 704, 1744, 782, 102, 0, 0, 0, 0, 0]
# 裁剪
ids = tokenizer.encode(sentence, max_length=5, truncation=True)
print("ids\n", ids) # [101, 2769, 3221, 704, 102]
# attention_mask 与 token_type_id
ids = tokenizer.encode(sentence, padding="max_length", max_length=15)
attention_mask = [1 if idx != 0 else 0 for idx in ids]
token_type_ids = [0] * len(ids)
print(attention_mask) # [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
print(token_type_ids) # [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# 两种快速调用方式
# 快速调用方式1
inputs = tokenizer.encode_plus(sentence, padding="max_length", max_length=15)
print(inputs)
# {
# 'input_ids': [101, 2769, 3221, 704, 1744, 782, 102, 0, 0, 0, 0, 0, 0, 0, 0],
# 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
# }
# 快速调用方式2, 直接调用tokenizer本身
inputs = tokenizer(sentence, padding="max_length", max_length=15)
# batch数据和单条数据使用方式是一模一样的
sens = ["我是中国人",
"追逐梦想的心,比梦想本身,更可贵"]
res = tokenizer(sens, padding="max_length", max_length=12)
print(res)
#{'input_ids': [[101, 2769, 3221, 704, 1744, 782, 102, 0, 0, 0, 0, 0],
# [101, 6841, 6852, 3457, 2682, 4638, 2552, 8024, 3683, 3457, 2682, 3315, 6716, 8024, 3291, 1377, 6586, 102]],
# 'token_type_ids': [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
# 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
# [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]}
参考链接
tokenizers小结
Transformers中Tokenizer模块快速使用