目录

  • 前言
  • Byte Pair Encoding(BPE)
  • WordPiece
  • Unigram

前言

单词级别的tokenizer有以下缺点:

  • 单词变体算做不同单词,无法体现它们的关联

本文从代码层次解析四种常用的tokenizer(放弃了)

Byte Pair Encoding(BPE)

提出论文:Neural machine translation of rare words with subword units

以下讲解基本参考:Byte Pair Encoding

假设拥有一个含有很多单词的语料,首先统计各个单词出现的次数,以单词字符串为键、次数为值。预测同时,要对单词字符串进行两个操作:1. 首先给这个单词字符串后面加上一个结束符号"</w>", 2. 然后把单词字符串分成一个一个的字符。

比如我们有一个语料:['low','lower','newest','widest']
经过上述操作得到:{'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}

然后正试开始迭代,每一次迭代,遍历上述字典的所有键,把字符串按照空格分割成字符序列,统计所有字符对出现的次数,比如'es'这个字符对出现了6+3=9次,而它也是出现次数最多的,所以将它合并。
这是第一次迭代的结果:{'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}
接下去的迭代一摸一样。需要注意的是,在第一次迭代以前,'e''s'都作为单个token出现在语料中,在第一次迭代以后,所有的's'都通过合并而消失了,而由于l o w e r中还有'e',所以'e'依然作为一个token存在。另外,别忘了,新增了一个'es'token,总token数不变。
那么很容易想到每一轮的token数变化情况,有以下四种(假设当前轮是要去合并a和b这两个token):

  • token数不变

    • 所有的a都出现在b前面,合并之后a消失;而b前面不仅仅出现过a,合并之后b依然存在
    • 所有的b都出现在a后面,合并之后b消失;而a后面不仅仅出现过b,合并之后a依然存在
  • token数-1:a和b仅以对的形式出现,合并之后a和b消失,新增一个ab,总数-1
  • token数+1:a后面不仅仅出现过b,同样的,b前面也不仅仅出现过a,原来token数不变,又新增一个ab,总数+1

token总数可能呈现这样的变化趋势:一开始的token出现形式多样,token总数会上升;随着不断地迭代合并,token数量增加,但token出现的形式减少,token总数会慢慢减少。

下面是代码部分:

import re, collectionsdef get_vocab(corpus):vocab = collections.defaultdict(int)for word in corpus:vocab[' '.join(list(word)) + ' </w>'] += 1return vocabdef 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]] += freqreturn pairsdef 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_outdef get_tokens(vocab):tokens = collections.defaultdict(int)for word, freq in vocab.items():word_tokens = word.split()for token in word_tokens:tokens[token] += freqreturn tokenscorpus = ['low','lower','newest','widest']
vocab = get_vocab(corpus)
print('==========')
print('Tokens Before BPE')
tokens = get_tokens(vocab)
print('Tokens: {}'.format(tokens))
print('Number of tokens: {}'.format(len(tokens)))
print('==========')num_merges = 3
for i in range(num_merges):pairs = get_stats(vocab)if not pairs:breakbest = max(pairs, key=pairs.get)vocab = merge_vocab(best, vocab)print('Iter: {}'.format(i))print('Best pair: {}'.format(best))tokens = get_tokens(vocab)print('Tokens: {}'.format(tokens))print('Number of tokens: {}'.format(len(tokens)))print('==========')

输出:

==========
Tokens Before BPE
Tokens: defaultdict(<class 'int'>, {'l': 2, 'o': 2, 'w': 4, '</w>': 4, 'e': 4, 'r': 1, 'n': 1, 's': 2, 't': 2, 'i': 1, 'd': 1})
Number of tokens: 11
==========
Iter: 0
Best pair: ('l', 'o')
Tokens: defaultdict(<class 'int'>, {'lo': 2, 'w': 4, '</w>': 4, 'e': 4, 'r': 1, 'n': 1, 's': 2, 't': 2, 'i': 1, 'd': 1})
Number of tokens: 10
==========
Iter: 1
Best pair: ('lo', 'w')
Tokens: defaultdict(<class 'int'>, {'low': 2, '</w>': 4, 'e': 4, 'r': 1, 'n': 1, 'w': 2, 's': 2, 't': 2, 'i': 1, 'd': 1})
Number of tokens: 10
==========
Iter: 2
Best pair: ('e', 's')
Tokens: defaultdict(<class 'int'>, {'low': 2, '</w>': 4, 'e': 2, 'r': 1, 'n': 1, 'w': 2, 'es': 2, 't': 2, 'i': 1, 'd': 1})
Number of tokens: 10
==========

说明几个python相关:

  • collections.defaultdict() 跟dict的区别就是,不存在的键也可以直接加进去
  • re.escape() 把所有可能是正则的符号进行转义
  • re.compile(r’(?<!\S)’ + bigram + r’(?!\S)’) 首尾不是非空字符–>首尾要么啥也没有,如果有,只能是空字符。比如要合并e和s,如果不加这一句,'e sd’会被合并成 ‘esd’
  • best = max(pairs, key=pairs.get) 以字典中的值为关键字,选择值(出现次数)最大的键(token对)

接下来是解码和编码。所谓解码,就是把一个subword序列,拼回一个string,比如[“the</w>”, “high”, “est</w>”, “moun”, “tain</w>”]—解码—>the</w> highest</w> mountain</w>。这好做,下面将编码。
所谓编码,就是把一个string转换成subword序列,具体步骤为: 把迭代完的token集按照长度排序,长的在前,对于每一个string,从大大小遍历token集,一旦发现一个token出现在这个string中一次或者多次,出现这个token的地方保留下来,其它地方递归查询。特别的,假设目前匹配到的token在这个token集中的100个,那么string的其它地方只需要在第101开始的更短的token集中进行搜索。如果当前递归层的string,遍历了所有的token都没有任何匹配,就把它转成一个unknown token(’</u>‘)

下面是代码部分:

import re, collectionsdef get_vocab(corpus):vocab = collections.defaultdict(int)for word in corpus:vocab[' '.join(list(word)) + ' </w>'] += 1return vocabdef 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]] += freqreturn pairsdef 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_outdef get_tokens_from_vocab(vocab):tokens_frequencies = collections.defaultdict(int)vocab_tokenization = {}for word, freq in vocab.items():word_tokens = word.split()for token in word_tokens:tokens_frequencies[token] += freqvocab_tokenization[''.join(word_tokens)] = word_tokensreturn tokens_frequencies, vocab_tokenizationdef measure_token_length(token):if token[-4:] == '</w>':return len(token[:-4]) + 1else:return len(token)def tokenize_word(string, sorted_tokens, unknown_token='</u>'):# Ilikeeatingapplesif string == '':return []if sorted_tokens == []:return [unknown_token]string_tokens = []flag = 0for i in range(len(sorted_tokens)):token = sorted_tokens[i]token_reg = re.escape(token.replace('.', '[.]'))matched_positions = [(m.start(0), m.end(0)) for m in re.finditer(token_reg, string)]if len(matched_positions) == 0:continueflag = 1substring_end_positions = [matched_position[0] for matched_position in matched_positions]substring_start_position = 0for substring_end_position in substring_end_positions:substring = string[substring_start_position:substring_end_position]string_tokens += tokenize_word(string=substring, sorted_tokens=sorted_tokens[i+1:], unknown_token=unknown_token)string_tokens += [token]substring_start_position = substring_end_position + len(token)remaining_substring = string[substring_start_position:]string_tokens += tokenize_word(string=remaining_substring, sorted_tokens=sorted_tokens[i+1:], unknown_token=unknown_token)breakif flag == 0:return [unknown_token]return string_tokenscorpus = ['low','lower','newest','widest']vocab = get_vocab(corpus) print('==========')
print('Tokens Before BPE')
tokens_frequencies, vocab_tokenization = get_tokens_from_vocab(vocab)
print('All tokens: {}'.format(tokens_frequencies.keys()))
print('Number of tokens: {}'.format(len(tokens_frequencies.keys())))
print('==========')num_merges = 10
for i in range(num_merges):pairs = get_stats(vocab)if not pairs:breakbest = max(pairs, key=pairs.get)vocab = merge_vocab(best, vocab)
#     print('Iter: {}'.format(i))
#     print('Best pair: {}'.format(best))tokens_frequencies, vocab_tokenization = get_tokens_from_vocab(vocab)
#     print('All tokens: {}'.format(tokens_frequencies.keys()))
#     print('Number of tokens: {}'.format(len(tokens_frequencies.keys())))
#     print('==========')# Let's check how tokenization will be for a known word
word_given_known = 'newest</w>'
word_given_unknown = 'Ilikeeatingapples!</w>'sorted_tokens_tuple = sorted(tokens_frequencies.items(), key=lambda item: (measure_token_length(item[0]), item[1]), reverse=True)
sorted_tokens = [token for (token, freq) in sorted_tokens_tuple]print(sorted_tokens)
print(vocab_tokenization)
word_given = word_given_known print('Tokenizing word: {}...'.format(word_given))
if word_given in vocab_tokenization:print('Tokenization of the known word:')print(vocab_tokenization[word_given])print('Tokenization treating the known word as unknown:')print(tokenize_word(string=word_given, sorted_tokens=sorted_tokens, unknown_token='</u>'))
else:print('Tokenizating of the unknown word:')print(tokenize_word(string=word_given, sorted_tokens=sorted_tokens, unknown_token='</u>'))word_given = word_given_unknown print('Tokenizing word: {}...'.format(word_given))
if word_given in vocab_tokenization:print('Tokenization of the known word:')print(vocab_tokenization[word_given])print('Tokenization treating the known word as unknown:')print(tokenize_word(string=word_given, sorted_tokens=sorted_tokens, unknown_token='</u>'))
else:print('Tokenizating of the unknown word:')print(tokenize_word(string=word_given, sorted_tokens=sorted_tokens, unknown_token='</u>'))

输出(有个问题:’</w>'会被匹配到,但在大规模语料上训练了,应该不会有这个问题):

==========
Tokens Before BPE
All tokens: dict_keys(['l', 'o', 'w', '</w>', 'e', 'r', 'n', 's', 't', 'i', 'd'])
Number of tokens: 11
==========
['lower</w>', 'est</w>', 'low</w>', 'ne', 'w', 'i', 'd']
{'low</w>': ['low</w>'], 'lower</w>': ['lower</w>'], 'newest</w>': ['ne', 'w', 'est</w>'], 'widest</w>': ['w', 'i', 'd', 'est</w>']}
Tokenizing word: newest</w>...
Tokenization of the known word:
['ne', 'w', 'est</w>']
Tokenization treating the known word as unknown:
['ne', 'w', 'est</w>']
Tokenizing word: Ilikeeatingapples!</w>...
Tokenizating of the unknown word:
['</u>', 'i', '</u>', 'i', '</u>', 'w', '</u>']

另外,tokenize_word函数中关于flag的判断是我自己加的。

WordPiece

发表论文:Japanese and korean voice search.
参考:NLP三大Subword模型详解:BPE、WordPiece、ULM
信息论(1)——熵、互信息、相对熵
wordpiece和BPE的差异在于合并时对token对的选择:BPE是选择出现次数最大的,wordpiece衡量的是token对和单独的两个token之间的概率差,选择概率差最大的进行合并。

考虑token a和b,以及合并之后的token ab,概率差的公式如下:
p(a,b)/(p(a)∗p(b))p(a,b) / (p(a) * p(b))p(a,b)/(p(a)∗p(b))
这可以近似理解为合并前后,整个语料的互信息。即,当前选择合并的token对能够让语料的熵最小化->确定性最大化->信息量最小化->在计算机中存储所需要的编码长度最短化

Unigram

发表论文: Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates

用得上的时候再补充…

[NLP]——BPE、WordPiece、Unigram and SentencePiece相关推荐

  1. 深入理解NLP Subword算法:BPE、WordPiece、ULM ,sentencepiece

    https://zhuanlan.zhihu.com/p/86965595 https://zhuanlan.zhihu.com/p/75271211

  2. NLP Subword三大算法原理:BPE、WordPiece、ULM

    Subword算法如今已经成为了一个重要的NLP模型性能提升方法.自从2018年BERT横空出世横扫NLP界各大排行榜之后,各路预训练语言模型如同雨后春笋般涌现,其中Subword算法在其中已经成为标 ...

  3. 深入理解NLP Subword算法:BPE、WordPiece、ULM

    CHANGLOG 4/18/2020,规范化引用 3/27/2020,新增目录. 前言 Subword算法如今已经成为了一个重要的NLP模型性能提升方法.自从2018年BERT横空出世横扫NLP界各大 ...

  4. 理解 NLP Subword算法:BPE、WordPiece、ULM

    前言 Subword算法如今已经成为了一个重要的NLP模型性能提升方法.自从2018年BERT横空出世横扫NLP界各大排行榜之后,各路预训练语言模型如同雨后春笋般涌现,其中Subword算法在其中已经 ...

  5. NLP 三大Subword分词算法 (BPE、WordPiece、ULM) 原理与代码实现(面试必考知识点)

    ⭐后续有空会持续补充各subword分词算法原理与代码实现,以及面试常问知识点~先休息吃夜宵,打王者,拒绝内卷!

  6. Subword三大算法原理:BPE、WordPiece、ULM

    前言 Subword算法如今已经成为了一个重要的NLP模型性能提升方法.自从2018年BERT横空出世横扫NLP界各大排行榜之后,各路预训练语言模型如同雨后春笋般涌现,其中Subword算法在其中已经 ...

  7. 强得离谱!串烧70+个Transformer模型,涵盖CV、NLP、金融、隐私计算...

    Transformer 作为一种基于注意力的编码器 - 解码器架构,不仅彻底改变了自然语言处理(NLP)领域,还在计算机视觉(CV)领域做出了一些开创性的工作.与卷积神经网络(CNN)相比,视觉 Tr ...

  8. 关于NLP相关技术全部在这里:预训练模型、图神经网络、模型压缩、知识图谱、信息抽取、序列模型、深度学习、语法分析、文本处理...

    NLP近几年非常火,且发展特别快.像BERT.GPT-3.图神经网络.知识图谱等技术应运而生. 我们正处在信息爆炸的时代.面对每天铺天盖地的网络资源和论文.很多时候我们面临的问题并不是缺资源,而是找准 ...

  9. B站【1espresso】NLP - transform、bert、HMM、NER课件

    git地址 传送门 传送门2(含bert情感分析) 仅学习使用,侵删 中文自然语言处理 Transformer模型(一) transformer是谷歌大脑在2017年底发表的论文attention i ...

  10. 深度学习与自然语言处理教程(5) - 语言模型、RNN、GRU与LSTM(NLP通关指南·完结)

    作者:韩信子@ShowMeAI 教程地址:https://www.showmeai.tech/tutorials/36 本文地址:https://www.showmeai.tech/article-d ...

最新文章

  1. 2021年高考模拟考成绩查询,2021年湖北省普通高考模拟考试成绩查询
  2. DeepMind 最新论文解读:首次提出离散概率树中的因果推理算法
  3. 原来腾讯面试题也不难,面试官:给我说一下你理解的分布式架构?
  4. Flutter-现有iOS工程引入Flutter
  5. 2018-2019-2 20175328 《Java程序设计》第十一周学习总结
  6. Netty源码注释翻译-Channel类
  7. 离线语音识别软件_6.语音板使用科大讯飞离线命令词识别
  8. Python入门--python中的global
  9. vSphere 4系列之三:vCenter Server 4.0安装
  10. 台达JAVA_wplsoft下载(台达plc编程软件)
  11. 邓小铁:博弈论研究中的学术快乐
  12. 小度wifi的使用说明
  13. c++基础--另类的分支结构
  14. 微信小程序设置单个页面自定义头部加背景图
  15. 3月20 Bundle Adjustment光束平差法概述
  16. 攻防技术第一篇之-知彼(攻击手段)
  17. mvc+xrecyclerview+SQL+自定义控件
  18. JavaScript DOM操作,就是这么简单!
  19. 数据结构题(C语言)----括弧匹配检验(check)
  20. solr DIH 设置定时索引

热门文章

  1. springmvc+mybatis 无极限树形结构 Mapperxml 映射方法
  2. 检验新买内存条的真假
  3. open-drain和push-pull的上拉速度
  4. matlab仿真超声波测距,超声波测距仪制作-Arduino中文社区 - Powered by Discuz!
  5. wex5 mysql root密码_WeX5基础
  6. 【2023秋招】9月美团校招C++岗题目
  7. 如何用ping 命令简单测试网速
  8. 阿里云叔度:一场技术人的自我修行
  9. js把HTML转成对象,将js对象转换为html
  10. 【产品经理】产品经理进阶之路(六):互联网思维详解