创作不易,有用的话点个赞
先直接贴代码,我们再慢慢分析,代码来自openai的图像分类模型的一小段
def bytes_to_unicode():
"""
Returns list of utf-8 byte and a corresponding list of unicode strings.
The reversible bpe codes work on unicode strings.
This means you need a large # of unicode characters in your vocab if you want to avoid UNKs.
When you're at something like a 10B token dataset you end up needing around 5K for decent coverage.
This is a signficant percentage of your normal, say, 32K bpe vocab.
To avoid that, we want lookup tables between utf-8 bytes and unicode strings.
And avoids mapping to whitespace/control characters the bpe code barfs on.
"""
bs = list(range(ord("!"), ord("~")+1))+list(range(ord("¡"), ord("¬")+1))+list(range(ord("®"), ord("ÿ")+1))
cs = bs[:]
n = 0
for b in range(2**8):
if b not in bs:
bs.append(b)
cs.append(2**8+n)
n += 1
cs = [chr(n) for n in cs]
return dict(zip(bs, cs))
openai觉得图像分类,就是输入文本,然后给你一张相似的照片,例如
a facial photo of a tabby cat
这其实对文本语义文本推理要求不是很高,所以我们不需要训练出一个太长的词汇表,例如gpt2的50000多个词汇,不需要。
我们只需要一些简单的词汇表,我们可以指定我们需要哪些词汇,首先26个英文字母,一些分隔符,或者你还想兼容其它语言,都可以加,这里兼容了英语法语西班牙语,你觉得重要的语言字符都给一个独立的下标index去对待这个字符,所以就有了如下代码:
bs = list(range(ord("!"), ord("~")+1))+list(range(ord("¡"), ord("¬")+1))+list(range(ord("®"), ord("ÿ")+1))
print(list(range(ord("!"), ord("~")+1)))
print(list(range(ord("¡"), ord("¬")+1)))
print(list(range(ord("®"), ord("ÿ")+1)))
打印如下,ord(“!”)就是获取一个字符在unicode编码世界中的一个下标,可以看到对你重要的字符都在下面,你可以随意更改上面的字符。
[33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126]
[161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172]
[174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255]
但是实际上当你训练好模型之后,就算你要求用户使用英语法语西班牙语,但是用户可能会使用其它语言去提问,不在我们上面的区间,所以我们要兼容用户输入一些其他语言,我们想使用utf-8去实现这种兼容性。
这里讲一个东西:由于我们没有对其他语言独立给一个位置,所以当使用其他语言去跟模型提问的时候,效果可能不会很好,但我们针对的用户主要是英文法语西班牙语,这里主要是实现兼容性而已。
上面我们给下标的都只是单个字符,但是如果你觉得abc这个连词很重要,你也可以给abc一个单独的index,一个单独的index,意味着这个词有一个单独的词向量去训练,例如abc就有个单独的词向量,但是def没有,那么构成def的词向量是由三个单独的词向量组成,我认为,单独的一个词向量比多个组成的效果要好,表达意义要更准确,因为def是一个词汇,dbp也是一个词汇,他们是不同的意思,但是共享了d这个字符,d既要兼顾def的意思又要兼顾dbp的意思,很可能这两个词汇的意思又完全不相关不交集,那么d这个字符的词向量就被分散了,所以我们跟gpt问问题的时候,用英文问会更好,因为英文可以更准确表达我们的意思,而中文其实更像是很多无关的其他字符拼合起来的意思。
utf-8怎么表示文字?使用四种长度的数组表示一个符号,就是长度为1,2,3,4,每个位置取0到127中其中一个数字,可以表示计算机世界中所有词汇。如下:
【0-127】
【0-127,0-127】
【0-127,0-127,0-127】
【0-127,0-127,0-127,0-127】
原先已经拥有字符的下标,我们不去改它了,继续让他使用unicode编码的下标即可。
遍历 2的8次方 次,当缺少下标的时候,我们将最后一个字符顺序递增叠加上去,代码就是:
for b in range(2**8):
if b not in bs:
# 不存在的下标,就把下标append进去
bs.append(b)
cs.append(2**8+n) # 但是我append进去的字符却不是对应下标的unicode字符,因为我不喜欢......,我把第2**8+n字符叠加上去
n += 1
打印bs
[33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64,
65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96,
97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122,
123, 124, 125, 126, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183,
184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208,
209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233,
234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 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, 127, 128, 129, 130,
131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155,
156, 157, 158, 159, 160, 173]
打印cs
['!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':',
';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '¡', '¢', '£', '¤', '¥', '¦', '§', '¨', '©', 'ª',
'«', '¬', '®', '¯', '°', '±', '²', '³', '´', 'µ', '¶', '·', '¸', '¹', 'º', '»', '¼', '½', '¾', '¿', 'À', 'Á', 'Â', 'Ã', 'Ä', 'Å',
'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ð', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', '×', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'Þ', 'ß',
'à', 'á', 'â', 'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ð', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', '÷', 'ø', 'ù',
'ú', 'û', 'ü', 'ý', 'þ', 'ÿ', 'Ā', 'ā', 'Ă', 'ă', 'Ą', 'ą', 'Ć', 'ć', 'Ĉ', 'ĉ', 'Ċ', 'ċ', 'Č', 'č', 'Ď', 'ď', 'Đ', 'đ', 'Ē', 'ē',
'Ĕ', 'ĕ', 'Ė', 'ė', 'Ę', 'ę', 'Ě', 'ě', 'Ĝ', 'ĝ', 'Ğ', 'ğ', 'Ġ', 'ġ', 'Ģ', 'ģ', 'Ĥ', 'ĥ', 'Ħ', 'ħ', 'Ĩ', 'ĩ', 'Ī', 'ī', 'Ĭ', 'ĭ',
'Į', 'į', 'İ', 'ı', 'IJ', 'ij', 'Ĵ', 'ĵ', 'Ķ', 'ķ', 'ĸ', 'Ĺ', 'ĺ', 'Ļ', 'ļ', 'Ľ', 'ľ', 'Ŀ', 'ŀ', 'Ł', 'ł', 'Ń'
]
这就是我们仅有256个词汇表的token。
我们继续加深学习,上面形成的都是单个字符,在大量的文本语料当中,合并高频词汇
假设我们的文本为,
corpus = [
“low”,
“lower”,
“newest”,
“widest”
]
我们尝试使用BPE算法进行高频词汇的合并,我们把单词的结尾也当成是一个字符</w>:
lo 2个,low 2个,es 2个,est 2个 ,est</w> 2个,后面再长就只出现一次。这样子逐步拉长字符窗口,最终我们会统计到不同长度的单词字符的出现的次数,出现次数较高我们希望单独占一个词汇表位置index,如果某个单词太长比较生僻导致出现次数很低(例如erinaceous)我们不想它单独占一个位置,因为它很少用,避免浪费宝贵的词汇表位置,那么 erinaceous 将使用 eri na ce ous四个词汇替代。
BPE算法如下
from collections import defaultdict, Counter
import re
def get_stats(vocab):
"""计算字符对的频率"""
pairs = defaultdict(int)
for word, freq in vocab.items():
symbols = word.split()
for i in range(len(symbols) - 1):
pairs[(symbols[i], symbols[i + 1])] += freq
return pairs
def merge_vocab(pair, vocab):
"""合并词汇表中的字符对"""
bigram = re.escape(' '.join(pair))
replacement = ''.join(pair)
new_vocab = {}
for word in vocab:
new_word = re.sub(bigram, replacement, word)
new_vocab[new_word] = new_vocab.get(new_word, 0) + vocab[word]
return new_vocab
def bpe(corpus, num_merges):
"""执行 BPE 算法"""
# 初始化词汇表
vocab = Counter([' '.join(word) + ' </w>' for word in corpus])
for i in range(num_merges):
pairs = get_stats(vocab)
if not pairs:
break
# 找到频率最高的字符对
best_pair = max(pairs, key=pairs.get)
vocab = merge_vocab(best_pair, vocab)
print("vocab=")
print(vocab)
return vocab
# 示例文本
corpus = [
"low",
"lower",
"newest",
"widest"
]
# 分词
corpus = [list(word) for word in corpus]
print("========")
# 执行 BPE
vocab = bpe(corpus, num_merges=10)
print("========", vocab)
# 输出词汇表
for word, freq in vocab.items():
print(f"{word}: {freq}")
我截一段实际上训练出来的词汇表
越到后面,记录的词汇窗口越长 ,前期 i n 中间为什么会有空格,在前期,我们的窗口只有1长度,窗口1使用完之后,窗口会慢慢变成2,在文本训练语料中i的下一个字符是n,所以这里相当于做一个缓存记录一下,因为下一次我计算的字符就是 in 的出现频率 。当你发现下面 en 合并在一起的时候,前面肯定会有一个 e n, 如下就是。
i n
t h
a n
r e
a r
e r
th e</w>
in g</w>
o u
o n
s t
o r
e n
...
...
...
ran veer
ti k</w>
gover n</w>
qu al</w>
beck y
anthropo logy</w>
att en</w>
grocer ies</w>
de bit</w>
war p</w>
在这个长达26万多行的文本当中,越到后面,词汇越长,出现的次数越少,当你想构建自己的token词汇表的时候,也不是需要全部把26万多行的文本全部当成token,我们可以截取前 50000 个即可,然后通过算法进行拆分形成一个词汇表。
我们将最开始的那个单个字符的词汇表和别人训练好的词汇表的前5w个集合在一起,因为5w个集合前面单个词汇我们已经有了,所以我们将空格去掉然后加入到我们的词汇表即可,如下
最终构造好的词汇表截图如下:截图一部分,下图我利用原始的单个字符+</w>又构造了一次,是单词的结尾的意思,把 单个字母+</w> 当成是一个字符,当然你也可以不加。因为也是一个字符,这样子就算出现任何词汇,我们的词汇表就都可以承接得了了。
好了,我们经过很长时间,整合了很多词汇,现在我们使用一下,这些字符都能够在词汇表单独找到
print(tokenizer.encode("I LOVE you !!!"))
数据变化为,先清空多余的空格,然后转小写,再分别去词汇表查找。
i love you !!!
['i', 'love', 'you', '!!!']
[328, 793, 592, 995]
再来一个,lovepoiuy不能够在词汇表单独找到,所以返回的索引多了
print(tokenizer.encode("I LOVEPOIUY you !!!"))
数据变化为,先清空多余的空格,然后转小写,再分别去词汇表查找。
i lovepoiuy you !!!
['i', 'lovepoiuy', 'you', '!!!']
[328, 2618, 628, 14292, 344, 592, 995]
训练完成之后,得到一个文本,这种文本需要搭配开发者提供的python的脚本去读取,因为可能每个开发者的数据结构和算法可能不一样,就是需要开发者提供decoder和encoder方法。