当前位置:   article > 正文

Tokenizer分词_tokenizer.add_tokens

tokenizer.add_tokens

分词的一般流程

在使用神经网络处理自然语言处理任务时,我们首先需要对数据进行预处理,将数据从字符串转换为神经网络可以接受的格式,一般会分为如下几步:

(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使用示例

导入分词器

  1. from transformers import AutoTokenizer
  2. tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
  3. print("tokenizer:\n", tokenizer)
  4. '''
  5. BertTokenizerFast(name_or_path='bert-base-chinese',
  6. vocab_size=21128, model_max_length=512, is_fast=True,
  7. padding_side='right', truncation_side='right', special_tokens={'unk_token': '[UNK]', 'sep_token': '[SEP]', 'pad_token':
  8. '[PAD]', 'cls_token': '[CLS]', 'mask_token': '[MASK]'})
  9. '''
  1. sentence = "我是中国人"
  2. # 查看分词结果
  3. tokens = tokenizer.tokenize(sentence)
  4. print("tokens:\n", tokens) # ['我', '是', '中', '国', '人']
  5. # 查看词典。词典是在预训练模型时就构建好了的
  6. # print("词典\n", tokenizer.vocab)
  7. # 根据词典将词序列转化为数字序列
  8. ids = tokenizer.convert_tokens_to_ids(tokens=tokens)
  9. print("ids\n", ids) # [2769, 3221, 704, 1744, 782]
  10. # 直接调用encode方法也能实现上述目标,但会自动增加起始和终止数字序号
  11. ids = tokenizer.encode(sentence)
  12. print("ids\n", ids) #[101, 2769, 3221, 704, 1744, 782, 102]
  13. #填充
  14. ids = tokenizer.encode(sentence, padding="max_length", max_length=12)
  15. print("ids\n", ids) # [101, 2769, 3221, 704, 1744, 782, 102, 0, 0, 0, 0, 0]
  16. # 裁剪
  17. ids = tokenizer.encode(sentence, max_length=5, truncation=True)
  18. print("ids\n", ids) # [101, 2769, 3221, 704, 102]
  19. # attention_mask 与 token_type_id
  20. ids = tokenizer.encode(sentence, padding="max_length", max_length=15)
  21. attention_mask = [1 if idx != 0 else 0 for idx in ids]
  22. token_type_ids = [0] * len(ids)
  23. print(attention_mask) # [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
  24. print(token_type_ids) # [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
  25. # 两种快速调用方式
  26. # 快速调用方式1
  27. inputs = tokenizer.encode_plus(sentence, padding="max_length", max_length=15)
  28. print(inputs)
  29. # {
  30. # 'input_ids': [101, 2769, 3221, 704, 1744, 782, 102, 0, 0, 0, 0, 0, 0, 0, 0],
  31. # 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  32. # 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
  33. # }
  34. # 快速调用方式2, 直接调用tokenizer本身
  35. inputs = tokenizer(sentence, padding="max_length", max_length=15)
  36. # batch数据和单条数据使用方式是一模一样的
  37. sens = ["我是中国人",
  38. "追逐梦想的心,比梦想本身,更可贵"]
  39. res = tokenizer(sens, padding="max_length", max_length=12)
  40. print(res)
  41. #{'input_ids': [[101, 2769, 3221, 704, 1744, 782, 102, 0, 0, 0, 0, 0],
  42. # [101, 6841, 6852, 3457, 2682, 4638, 2552, 8024, 3683, 3457, 2682, 3315, 6716, 8024, 3291, 1377, 6586, 102]],
  43. # 'token_type_ids': [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  44. # [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
  45. # 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
  46. # [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]}

常用方式 

  1. # 普通编码
  2. print("="*100)
  3. sents= ["选择珠江花园的原因就是方便。", "笔记本的键盘确实爽。"]
  4. out = tokenizer.encode(text=sents[0],
  5. text_pair=sents[1],
  6. padding="max_length",
  7. max_length=30,
  8. truncation=True,
  9. add_special_tokens=True,
  10. return_tensors=None)
  11. print(out)
  12. print(tokenizer.decode(out))
  13. # 增强编码
  14. print("="*100)
  15. sents= ["选择珠江花园的原因就是方便。", "笔记本的键盘确实爽。"]
  16. out = tokenizer.encode_plus(text=sents[0],
  17. text_pair=sents[1],
  18. padding="max_length",
  19. max_length=30,
  20. truncation=True,
  21. add_special_tokens=True,
  22. return_tensors=None,
  23. return_token_type_ids=True,
  24. return_attention_mask=True,
  25. return_special_tokens_mask=True,
  26. return_length=True)
  27. for k,v in out.items():
  28. print(k, ":", v)
  29. print(tokenizer.decode(out["input_ids"]))
  30. '''
  31. 返回值
  32. input_ids:编码后的词
  33. token_type_ids含义:第一个句子和特殊符号位置是0,第二个句子的位置是1
  34. special_tokens_mask:特殊符号位置是1,其余是0
  35. attention_mask:padding的位置是0,其余位置是1
  36. length: 句子长度
  37. '''
  38. # 增强编码, 批量数据时采用 batch_tokenizer.encoder_plus()函数
  39. print("/"*100)
  40. sents= ["选择珠江花园的原因就是方便。", "笔记本的键盘确实爽。"]
  41. out = tokenizer.batch_encode_plus(batch_text_or_text_pairs=[sents[0],sents[1]],
  42. padding="max_length",
  43. max_length=30,
  44. truncation=True,
  45. add_special_tokens=True,
  46. return_tensors=None,
  47. return_token_type_ids=True,
  48. return_attention_mask=True,
  49. return_special_tokens_mask=True,
  50. return_length=True)
  51. for k,v in out.items():
  52. print(k, ":", v)
  53. print(tokenizer.decode(out["input_ids"][0]))
  54. print(tokenizer.decode(out["input_ids"][1]))

 encode函数返回值含义

  1. input_ids:编码后的词
  2. token_type_ids含义:第一个句子和特殊符号位置是0,第二个句子和其特殊符号的位置是1
  3. special_tokens_mask:特殊符号位置是1,其余是0
  4. attention_mask:padding的位置是0,其余位置是1
  5. length: 句子长度

获取字典和添加新词/新符号

  1. #获取字典
  2. zidian = tokenizer.get_vocab()
  3. print(type(zidian))
  4. print(len(zidian))
  5. print("月光" in zidian)
  6. # 添加新词
  7. tokenizer.add_tokens(new_tokens=["月光", "希望"])
  8. # 添加新符号
  9. # [EOS]不添加的话,在字典中为[UNK]表示未知符号
  10. tokenizer.add_special_tokens({'eos_token':'[EOS]'})

参考链接

tokenizers小结

Transformers中Tokenizer模块快速使用

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/281462
推荐阅读
  

闽ICP备14008679号