赞
踩
在使用神经网络处理自然语言处理任务时,我们首先需要对数据进行预处理,将数据从字符串转换为神经网络可以接受的格式,一般会分为如下几步:
(1)分词:使用分词器对文本数据进行分词(字、字词)得到token;
(2)构建词典:根据数据集分词的结果,构建词典映射,形成一个vocabulary dict用于存放每一个token(这一步并不绝对,如果采用预训练词向量,词典映射要根据词向量文件进行处理),vocabulary dict形式如下:
{token1:0,token2:1,token3:2,token4:4........}
(3)数据转换:根据构建好的词典,将分词处理后的数据做映射,将文本序列转换为数字序列;
(4)数据填充与截断:在以batch输入到模型的方式中,需要对过短的数据进行填充,过长的数据进行截断,保证数据长度符合模型能接受的范围,同时batch内的数据维度大小一致。
例如“我来做中国”会被分成[我,来,自,中国]
优点:能够保存较为完整的语义信息
缺点:
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边无法处理
例如“我来做中国”会被分成[我,来,自,中,国]
一个简单的方法就是将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之类的网络结构,这样计算起来非常慢;
为了两全其美,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也会不同。
导入分词器
- from transformers import AutoTokenizer
-
- tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
- print("tokenizer:\n", tokenizer)
- '''
- BertTokenizerFast(name_or_path='bert-base-chinese',
- vocab_size=21128, model_max_length=512, is_fast=True,
- padding_side='right', truncation_side='right', special_tokens={'unk_token': '[UNK]', 'sep_token': '[SEP]', 'pad_token':
- '[PAD]', 'cls_token': '[CLS]', 'mask_token': '[MASK]'})
- '''
-
- 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]]}

常用方式
- # 普通编码
- print("="*100)
- sents= ["选择珠江花园的原因就是方便。", "笔记本的键盘确实爽。"]
- out = tokenizer.encode(text=sents[0],
- text_pair=sents[1],
- padding="max_length",
- max_length=30,
- truncation=True,
- add_special_tokens=True,
- return_tensors=None)
- print(out)
- print(tokenizer.decode(out))
-
- # 增强编码
- print("="*100)
- sents= ["选择珠江花园的原因就是方便。", "笔记本的键盘确实爽。"]
- out = tokenizer.encode_plus(text=sents[0],
- text_pair=sents[1],
- padding="max_length",
- max_length=30,
- truncation=True,
- add_special_tokens=True,
- return_tensors=None,
- return_token_type_ids=True,
- return_attention_mask=True,
- return_special_tokens_mask=True,
- return_length=True)
- for k,v in out.items():
- print(k, ":", v)
- print(tokenizer.decode(out["input_ids"]))
- '''
- 返回值
- input_ids:编码后的词
- token_type_ids含义:第一个句子和特殊符号位置是0,第二个句子的位置是1
- special_tokens_mask:特殊符号位置是1,其余是0
- attention_mask:padding的位置是0,其余位置是1
- length: 句子长度
- '''
-
-
- # 增强编码, 批量数据时采用 batch_tokenizer.encoder_plus()函数
- print("/"*100)
- sents= ["选择珠江花园的原因就是方便。", "笔记本的键盘确实爽。"]
- out = tokenizer.batch_encode_plus(batch_text_or_text_pairs=[sents[0],sents[1]],
- padding="max_length",
- max_length=30,
- truncation=True,
- add_special_tokens=True,
- return_tensors=None,
- return_token_type_ids=True,
- return_attention_mask=True,
- return_special_tokens_mask=True,
- return_length=True)
- for k,v in out.items():
- print(k, ":", v)
- print(tokenizer.decode(out["input_ids"][0]))
- print(tokenizer.decode(out["input_ids"][1]))

encode函数返回值含义
- input_ids:编码后的词
- token_type_ids含义:第一个句子和特殊符号位置是0,第二个句子和其特殊符号的位置是1
- special_tokens_mask:特殊符号位置是1,其余是0
- attention_mask:padding的位置是0,其余位置是1
- length: 句子长度
获取字典和添加新词/新符号
- #获取字典
- zidian = tokenizer.get_vocab()
- print(type(zidian))
- print(len(zidian))
- print("月光" in zidian)
-
- # 添加新词
- tokenizer.add_tokens(new_tokens=["月光", "希望"])
-
- # 添加新符号
- # [EOS]不添加的话,在字典中为[UNK]表示未知符号
- tokenizer.add_special_tokens({'eos_token':'[EOS]'})
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。