手撸 chatgpt 大模型:详解 OpenAI 用于训练 gpt3 模型的数据预处理算法:BPE

ChatGPT使用了另一种复杂的分词方案,叫做“字节对编码”(Byte Pair Encoding,简称BPE),最初这是一种数据压缩算法,让我们来看看它的过程。BPE在多次迭代中合并频繁使用的字符对。例如,给定字符串:

“low low low low low lower lower newest newest newest newest newest newest widest widest widest”

单词频率统计如下:

{ “low”: 5, “lower”: 2, “newest”: 6, “widest”: 3, }

现在,我们将单词拆分成字符,字符集将是初始词汇表: {l, o, w, e, r, n, s, t, i, d}

每个单词拆分成字符集合如下:

{ “l o w”: 5, “l o w e r”: 2, “n e w e s t”: 6, “w i d e s t”: 3, }

现在,我们来看每对相邻字符对。例如,“l o w”有两个相邻字符对“lo”和“ow”,由于“lo”出现在“l o w”和“l o w e r”中,前者的频率为5,后者的频率为2,那么字符对“lo”的频率为7。通过这种方式,我们得到以下统计:

{ “l o”: 7, “o w”: 7, “w e”: 8, “e r”: 2, “n e”: 6, “e w”: 6, “e s”: 9, “s t”: 9, “w i”: 3, “i d”: 3, “d e”: 3, }

现在我们可以看到最频繁的字符对是“e s”和“s t”,然后我们将“e s”合并为一个单位,命名为“es”,并将“es”添加到词汇表中: {l, o, w, e, r, n, s, t, i, d, es} 每个单词的字符集合变为:

{ “l o w”: 5, “l o w e r”: 2, “n e w es t”: 6, “w i d es t”: 3, }

此时最频繁的字符对是“es”和“t”,然后我们将它们合并为“est”,并将其添加到词汇表中: {l, o, w, e, r, n, s, t, i, d, es, est} 每个单词的字符集合变为:

{ “l o w”: 5, “l o w e r”: 2, “n e w est”: 6, “w i d est”: 3 }

这时最频繁的字符对是“l o”,我们将它们添加到词汇表中:

{l, o, w, e, r, n, s, t, i, d, es, est, lo}

单词集合为:

{ “lo w”: 5, “lo w e r”: 2, “n e w est”: 6, “w i d est”: 3 }

现在很容易看出,最频繁的字符对是“lo”和“w”,将它们合并并添加到词汇表中: {l, o, w, e, r, n, s, t, i, d, es, est, lo, low}

单词字符集合为: { “low”: 5, “low e r”: 2, “n e w est”: 6, “w i d est”: 3 }

通过这种方式,我们可以继续迭代,直到达到预设的次数或者词汇表达到预期的大小。接下来我们来看如何使用代码实现这个过程:

from collections import defaultdict
# 计算单词频率并将单词拆分成字符集合
def get_vocab(data):
  vocab = defaultdict(int)
  for word in data.split():
          vocab[' '.join(list(word))] += 1
  return vocab

vocab = get_vocab("low low low low low lower lower newest newest newest newest newest newest widest widest widest")
print(vocab)

运行上面的代码,我们得到以下结果:

defaultdict(<class 'int'>, {'l o w': 5, 'l o w e r': 2, 'n e w e s t': 6, 'w i d e s t': 3})

接下来我们可以计算相邻字符对的频率:

# 计算相邻字符对的频率
from collections import Counter
def get_stats(vocab):
  pairs = Counter()
  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

pairs = get_stats(vocab)
print(pairs)

运行上述代码后,我们得到以下结果:

Counter({('e', 's'): 9, ('s', 't'): 9, ('w', 'e'): 8, ('l', 'o'): 7, ('o', 'w'): 7, ('n', 'e'): 6, ('e', 'w'): 6, ('w', 'i'): 3, ('i', 'd'): 3, ('d', 'e'): 3, ('e', 'r'): 2})

接着我们可以找到频率最高的字符对,并将它们合并为一个:

# 合并频率最高的字符对
def merge_vocab(pair, vocab):
  new_vocab = {}
  neighboring_chars = ' '.join(pair)
  # 将两个字符合并为一个
  replacement = ''.join(pair) 
  for word in vocab:
    new_word = word.replace(neighboring_chars, replacement)
    new_vocab[new_word] = vocab[word] 

  return new_vocab

most_frequent = max(pairs, key=pairs.get)
new_vocab = merge_vocab(most_frequent, vocab)
print(new_vocab)

运行上述代码,我们得到以下结果:

{'l o w': 5, 'l o w e r': 2, 'n e w es t': 6, 'w i d es t': 3}

现在我们将所有步骤合并,并对给定次数进行迭代:

# 合并所有步骤并进行给定次数的迭代
def byte_pair_encoding(vocab, num_merges):
  for i in range(num_merges):
    pairs = get_stats(vocab)
    if not pairs:
      break
    most_frequent = max(pairs, key=pairs.get)
    vocab = merge_vocab(most_frequent, vocab)
    print(f"合并: {i+1}, 最频繁的对: {most_frequent}")
  return vocab

result_vocab = byte_pair_encoding(vocab, 20)
print("最终词汇表")
for word in result_vocab:
  print(f"{word}: {result_vocab[word]}")

运行上述代码后,我们得到以下结果:

merget: 1, most frequent: ('e', 's')
merget: 2, most frequent: ('es', 't')
merget: 3, most frequent: ('l', 'o')
merget: 4, most frequent: ('lo', 'w')
merget: 5, most frequent: ('n', 'e')
merget: 6, most frequent: ('ne', 'w')
merget: 7, most frequent: ('new', 'est')
merget: 8, most frequent: ('w', 'i')
merget: 9, most frequent: ('wi', 'd')
merget: 10, most frequent: ('wid', 'est')
merget: 11, most frequent: ('low', 'e')
merget: 12, most frequent: ('lowe', 'r')
final vocab
low: 5
lower: 2
newest: 6
widest: 3

从结果中我们可以理解,字节对编码实际上是一种数据压缩方法,它通过给定单词及其重复次数来消除数据中的单词重复。我们这里的例子仅用于说明,完整的实现相当复杂,但幸运的是,已经有现成的库可以使用,我们可以安装以下库来使用BPE算法:

pip install tiktoken

然后我们可以通过以下代码选择给定的分词器:

import tiktoken
tokenizer = tiktoken.get_encoding('gpt2')

让我们尝试以下的编码算法:

text = ("Hello, do you like a cup of chinese tea? *|endoftext|* In the sunlit terraces"
       "of someunknowPlace.")
integers = tokenizer.encode(text, allowed_special={"*|endoftext|*"})
print(integers)
strings = tokenizer.decode(integers)
print(strings)

运行上述代码后,我们得到以下结果:

[15496, 11, 466, 345, 588, 257, 6508, 286, 442, 3762, 8887, 30, 1635, 91, 437, 1659, 5239, 91, 9, 554, 262, 4252, 18250, 8812, 2114, 1659, 617, 2954, 2197, 27271, 13]
Hello, do you like a cup of chinese tea? *|endoftext|* In the sunlit terracesof someunknowPlace.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值