文章用于记录学习CS336的一些笔记以及学习过程中出现的问题

assignment 1

tokenizer

首先是关于分词器的一些实现,assignment 1 这里主要是采用了BPE的算法去实现分词,具体实现如下:

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
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
def get_tokenizer(
vocab: dict[int, bytes],
merges: list[tuple[bytes, bytes]],
special_tokens: list[str] | None = None,
) -> Any:
"""Given a vocabulary, a list of merges, and a list of special tokens,
return a BPE tokenizer that uses the provided vocab, merges, and special tokens.

Args:
vocab (dict[int, bytes]): The tokenizer vocabulary, a mapping from int (token ID in the vocabulary)
to bytes (token bytes)
merges (list[tuple[bytes, bytes]]): BPE merges. Each list item is a tuple of bytes (<token1>, <token2>),
representing that <token1> was merged with <token2>.
Merges are ordered by order of creation.
special_tokens (list[str] | None): A list of string special tokens for the tokenizer. These strings will never
be split into multiple tokens, and will always be kept as a single token.

Returns:
A BPE tokenizer that uses the provided vocab, merges, and special tokens.
"""
bpe_tokenizer=tokenizer.BPE_Tokenizer(vocab,merges,special_tokens)

return bpe_tokenizer


def run_train_bpe(
input_path: str | os.PathLike,
vocab_size: int,
special_tokens: list[str],
**kwargs,
) -> tuple[dict[int, bytes], list[tuple[bytes, bytes]]]:
"""Given the path to an input corpus, run train a BPE tokenizer and
output its vocabulary and merges.

Args:
input_path (str | os.PathLike): Path to BPE tokenizer training data.
vocab_size (int): Total number of items in the tokenizer's vocabulary (including special tokens).
special_tokens (list[str]): A list of string special tokens to be added to the tokenizer vocabulary.
These strings will never be split into multiple tokens, and will always be
kept as a single token. If these special tokens occur in the `input_path`,
they are treated as any other string.

Returns:
tuple[dict[int, bytes], list[tuple[bytes, bytes]]]:
vocab:
The trained tokenizer vocabulary, a mapping from int (token ID in the vocabulary)
to bytes (token bytes)
merges:
BPE merges. Each list item is a tuple of bytes (<token1>, <token2>),
representing that <token1> was merged with <token2>.
Merges are ordered by order of creation.
"""
with open(input_path,"rb") as f:
text=f.read().decode("utf-8")

if special_tokens:
sorted_specials = sorted(special_tokens, key=len, reverse=True)
sp_pattern = "|".join(map(regex.escape, sorted_specials))
documents = regex.split(sp_pattern, text)
else:
documents = [text]

PAT = r"""'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"""
pattern = regex.compile(PAT)

chunk_counts: dict[tuple[int, ...], int] = Counter()
for doc in documents:
if not doc:
continue
for m in pattern.finditer(doc):
token_str = m.group()
if token_str:
token_tuple = tuple(token_str.encode("utf-8"))
chunk_counts[token_tuple] += 1

vocab={i:bytes([i]) for i in range(256)}
merges=[]
for sp_token in special_tokens:
vocab[len(vocab)]=sp_token.encode("utf-8")

merge_num=vocab_size-len(vocab)

for _ in range(merge_num):
stats={}
for chunk, count in chunk_counts.items():
for i in range(len(chunk) - 1):
pair = (chunk[i], chunk[i + 1])
stats[pair] = stats.get(pair, 0) + count

if not stats:
break

pair = max(stats, key=lambda p: (stats[p], vocab[p[0]], vocab[p[1]]))

new_token_id=len(vocab)
new_chunk_counts: dict[tuple[int, ...], int] = {}
for chunk, count in chunk_counts.items():
if pair[0] not in chunk or pair[1] not in chunk:
new_chunk_counts[chunk] = new_chunk_counts.get(chunk, 0) + count
continue
new_chunk = tokenizer._merge_chunk(chunk, pair, new_token_id)
new_chunk_counts[new_chunk] = (
new_chunk_counts.get(new_chunk, 0) + count
)

chunk_counts = new_chunk_counts

pair_bytes = vocab[pair[0]]+vocab[pair[1]]

vocab[new_token_id]=pair_bytes

merges.append((vocab[pair[0]],vocab[pair[1]]))

return vocab,merges

分词器类实现:

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
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
import regex

def _merge_chunk(
chunk: tuple[int, ...], pair: tuple[int, int], new_id: int
) -> tuple[int, ...]:
"""对单个 tuple 序列执行 pair 替换合并"""
new_chunk = []
i = 0
p0, p1 = pair
chunk_len = len(chunk)

while i < chunk_len:
if i < chunk_len - 1 and chunk[i] == p0 and chunk[i + 1] == p1:
new_chunk.append(new_id)
i += 2
else:
new_chunk.append(chunk[i])
i += 1

return tuple(new_chunk)


class BPE_Tokenizer:

def __init__(self,vocab,merges,special_tokens):
self.vocab=vocab
self.merges=merges
self.special_tokens=special_tokens

def decode(self,ids):
text=b"".join(self.vocab[i] for i in ids).decode("utf-8",errors="replace")
return text

def encode(self,text):
vocab_reverse={token : i for i,token in self.vocab.items()}
pair_reverse={pair:i for i,pair in enumerate(self.merges)}

if self.special_tokens:
sorted_sp_tokens=sorted(self.special_tokens,key=len,reverse=True)
sp_pattern='('+'|'.join(map(regex.escape,sorted_sp_tokens))+')'
chunks=regex.split(sp_pattern,text)

else:
chunks=[text]

PAT=r"""'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"""

pre_pattern=regex.compile(PAT)


ids=[]
for chunk in chunks:
if not chunk:
continue

chunk_bytes=chunk.encode("utf-8")

if self.special_tokens and chunk in self.special_tokens and chunk_bytes in vocab_reverse:
ids.append(vocab_reverse[chunk_bytes])
continue

for match in pre_pattern.finditer(chunk):
piece_token=match.group().encode("utf-8")

tokens=[bytes([b]) for b in piece_token]

while(len(tokens)>=2):
new_tokens=[]
pairs=[(tokens[i],tokens[i+1]) for i in range(len(tokens)-1)]
valid_pairs = [pair for pair in pairs if pair in pair_reverse]
if not valid_pairs:
break

best_pair = min(valid_pairs, key=lambda p: pair_reverse[p])

i=0

while(i<len(tokens)):
if i<len(tokens)-1 and best_pair[0]==tokens[i] and best_pair[1]==tokens[i+1]:
new_tokens.append(tokens[i]+tokens[i+1])
i+=2
else:
new_tokens.append(tokens[i])
i+=1
tokens=new_tokens

ids.extend(vocab_reverse[token] for token in tokens)
return ids


def encode_iterable(self,f):
for line in f:
for token_id in self.encode(line):
yield token_id

这里介绍几个不熟悉的方法的作用:

1
2
3
4
5
regex.split:将字符串按照指定的规则进行分割,这里主要是通过special token将除了special token外的文本分离了出来

PAT = r"""'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+""":子词切分模式,将文本按照特定子词进行切分

pattern.finditer(doc):返回一个迭代器,这里是将所有doc按照PAT做切分

训练时的整个过程:

先将整个text去除sp_token后得到document列表(不包含sp_token),然后进行预处理,将document中每一个doc进行子词切分,得到字典chunk_counts,其中记录了每一个子词在文本中的出现次数,然后得到初始的vocab后进行迭代,每次迭代过程中先统计所有chunk中出现频率最高的两个子词(vocab字典中的),然后用统计结果更新chunk_counts和vocab,同时记录merges(即合并的记录)