llama2c(3)之tokenizer和encode

12 篇文章 0 订阅

2、tokenizer和编码

2.1 tokenizer

tokenizer的model不同于Transformer的model,Transformer的model是一些权重参数,tokenizer的model是一些分词,将一个句子分成分词的中介(模型)。,具体就是tokenizer.py文件打印出来的如token数组所示

Tokenizer tokenizer;
build_tokenizer(&tokenizer, tokenizer_path, transformer.config.vocab_size);

是从tokenizer_path把Tokenizer 结构体中的内容填满

2.1.1 build前的tokenizer

(gdb) print tokenizer
$1 = {vocab = 0x0, vocab_scores = 0x0, sorted_vocab = 0x0, vocab_size = 0, max_token_length = 0, byte_pieces = '\000' <repeats 511 times>}

2.1.2 build中的tokenizer

(gdb) print *t->byte_pieces@5   //输出数组数值要加*
$17 = "\000\000\001\000\002"

一个数字和一个0搭配好

(gdb) print t->byte_pieces[0]
$11 = 0 '\000'
(gdb) print t->byte_pieces[1]
$12 = 0 '\000'
(gdb) print t->byte_pieces[2]
$9 = 1 '\001'
(gdb) print t->byte_pieces[3]
$10 = 0 '\000'
if (fread(t->vocab_scores + i, sizeof(float), 1, file) != 1) { fprintf(stderr, "failed read\n"); exit(EXIT_FAILURE);}
if (fread(&len, sizeof(int), 1, file) != 1) { fprintf(stderr, "failed read\n"); exit(EXIT_FAILURE); }
if (fread(t->vocab[i], len, 1, file) != 1) { fprintf(stderr, "failed read\n"); exit(EXIT_FAILURE); }
t->vocab[i][len] = '\0'; // add the string terminating token

函数
“fread函数原型
size_t fread ( void * buffer , size_t size , size_t count , FILE * stream ) ;
fread参数
buffer
用于接收数据的内存地址
size
要读的每个数据项的字节数,单位是字节
count
要读count个数据项,每个数据项size个字节.
stream
输入流”
fread() 函数内部会自动处理文件指针的位置移动,也即是下一个地址

2.1.3 build后的输出

(gdb) print tokenizer
$30 = {vocab = 0x7f3c2cdbc010, vocab_scores = 0x559d23c249e0, sorted_vocab = 0x0, vocab_size = 32000, max_token_length = 27, 
  byte_pieces = "\000\000\001\000\002\000\003\000\004\000\005\000\006\000\a\000\b\000\t\000\n\000\v\000\f\000\r\000\016\000\017\000\020\000\021\000\022\000\023\000\024\000\025\000\026\000\027\000\030\000\031\000\032\000\033\000\034\000\035\000\036\000\037\000 \000!\000\"\000#\000$\000%\000&\000'\000(\000)\000*\000+\000,\000-\000.\000/\000\060\000\061\000\062\000\063\000\064\000\065\000\066\000\067\000\070\000\071\000:\000;\000<\000=\000>\000?\000@\000A\000B\000C\000D\000E\000F\000G\000H\000I\000J\000K\000L\000M\000N\000O\000P\000Q\000R\000S\000T\000U\000V\000W\000X\000Y\000Z\000[\000\\\000]\000^\000_\000`\000a\000b\000c\000"...}

具体地,

//地址和内容
(gdb) p *tokenizer->vocab@10  
$33 = {0x559d23c44e00 "<unk>", 0x559d23c44e20 "\n<s>\n", 0x559d23c44e40 "\n</s>\n", 0x559d23c44e60 "<0x00>", 0x559d23c44e80 "<0x01>", 0x559d23c44ea0 "<0x02>", 
  0x559d23c44ec0 "<0x03>", 0x559d23c44ee0 "<0x04>", 0x559d23c44f00 "<0x05>", 0x559d23c44f20 "<0x06>"}
(gdb) p tokenizer->vocab[0]
$32 = 0x559d23c44e00 "<unk>"
(gdb) p tokenizer->vocab[1]
$36 = 0x559d23c44e20 "\n<s>\n"
(gdb) p tokenizer->vocab[2]
$37 = 0x559d23c44e40 "\n</s>\n"

3 玩玩sentencepiece

3.1 tokenizer.py (这个应该先看)

tokens[20100:2100]
[b' \xd0\xbc\xd1\x96\xd0\xb6', b'\xc3\xa9bec', b' clip', b' Nice', b' neben', b' assass', b'itories', b' unity', b' \xd0\xb5\xd0\xbd', b' Institut', b' internationale', b' \xd0\xbd\xd0\xb0\xd1\x83\xd0\xba', b' comand', b' kleine', ...]
tokens[0:10]
[b'<unk>', b'\n<s>\n', b'\n</s>\n', b'<0x00>', b'<0x01>', b'<0x02>', b'<0x03>', b'<0x04>', b'<0x05>', b'<0x06>']
        with open(tokenizer_bin, 'wb') as f:
            f.write(struct.pack("I", max_token_length))   # 写入最长的token
            for bytes, score in zip(tokens, scores):
                f.write(struct.pack("fI", score, len(bytes))) # 写入每个词元的得分、词元长度以及实际字节内容
                f.write(bytes)

3.2 测试一下分词的能力

from sentencepiece import SentencePieceProcessor
model_path = "/mnt/workspace/llama2.c/tokenizer.model"
sp_model = SentencePieceProcessor(model_file=model_path)
mm = sp_model.EncodeAsPieces("I love you, baby")
nn = sp_model.encode_as_ids("I love you, baby")
print("分词:",mm)
print("编码",nn)
print("解码",sp_model.decode_ids([306, 5360, 366, 29892, 24354]))
(base) /mnt/workspace> python /mnt/workspace/llama2.c/test_sen.py
分词: ['▁I', '▁love', '▁you', ',', '▁baby']
编码 [306, 5360, 366, 29892, 24354]
解码 I love you, baby

4、再来看看generate

void generate(Transformer *transformer, Tokenizer *tokenizer, Sampler *sampler, char *prompt, int steps)

参数解释:
三个被填充好内容的结构体:*transformer,*tokenizer,*sampler

(gdb) print steps
$4 = 256
(gdb) print prompt
$5 = 0x561f3e0313b6 "Hello"

4.1 encode

调用

encode(tokenizer, prompt, 1, 0, prompt_tokens, &num_prompt_tokens);

定义

void encode(Tokenizer* t, char *text, int8_t bos, int8_t eos, int *tokens, int *n_tokens) 
参数解释:1为bos,0为eos, int *tokens为prompt分配的内存空间,int *n_tokens为0

S1:将bin文件读进来赋值给t->sorted_vocab,包括string,id,然后进行排序
其中,分配空间sizeof(TokenIndex)

typedef struct {
    char *str;
    int id;
} TokenIndex;

排序后的t->sorted_vocab

(gdb) print *t->sorted_vocab@10
$11 = {{str = 0x561f3e49fe40 "\n</s>\n", id = 2}, {str = 0x561f3e49fe20 "\n<s>\n", id = 1}, {str = 0x561f3e58a4b0 "\r", id = 30004}, {str = 0x561f3e589410 " ", 
    id = 29871}, {str = 0x561f3e4d4a80 " \r", id = 6756}, {str = 0x561f3e4a1e60 "  ", id = 259}, {str = 0x561f3e4acfc0 "   ", id = 1678}, {
    str = 0x561f3e4a1f80 "    ", id = 268}, {str = 0x561f3e4a3240 "     ", id = 418}, {str = 0x561f3e4a4160 "      ", id = 539}}
gdb) print t->sorted_vocab[0]
$12 = {str = 0x561f3e49fe40 "\n</s>\n", id = 2}
(gdb) print t->sorted_vocab[1]
$13 = {str = 0x561f3e49fe20 "\n<s>\n", id = 1}
(gdb) print t->sorted_vocab[2]
$14 = {str = 0x561f3e58a4b0 "\r", id = 30004}

S2:初始化tokens(专门存放整数的),有bos,tokens第一个数赋值为1;如果test[0]不是结束符,就将空格的id给tokens数的第二个数。其中,n_tokens作为tokens的索引再变化。

部分代码就是如下
1)tokens
不确定的初始值

(gdb) print *tokens@10
$25 = {-1820597504, 32645, -1820597504, 32645, 
  1554046432, 22025, 1554046432, 22025, 
  1207995313, 247541}

有bos,将第一个元素赋为1,第二个仍为随机值

if (bos) tokens[(*n_tokens)++] = 1;
(gdb) print tokens[0]
$27 = 1
(gdb) print tokens[1]
$28 = 32645
   if (text[0] != '\0') {
        int dummy_prefix = str_lookup(" ", t->sorted_vocab, t->vocab_size);
        tokens[(*n_tokens)++] = dummy_prefix;
    }
(gdb) print dummy_prefix
$29 = 29871

打印排序前的vocab,对应的起。

(gdb) p  t->vocab[29871]
$33 = 0x56095caf8410 " "

S3:遍历输入字符串,如果不是延续字符,将“字符+\0”的形式从索引0开始存到str_buffer中;判断后面的字符是不是延续字符,是,继续加到str_buffer中。(保证str_buffer的格式:要么是只有一个非延续字符,要么是非延续字符+延续字符??)不是,返回当前字符的id,赋给tokens。(将Hello分成一个个字符搞的对应id,也就是tokens里的数值

部分代码解释如下:

(gdb) print *c
$34 = 72 'H'
(*c & 0xC0) != 0x80

是一段在处理UTF-8编码时常见的位运算表达式,用于检查给定的字符(指向字符的指针 c)是否是一个UTF-8编码中的延续字节。
在UTF-8编码中,每个Unicode码点(字符)被编码为1到4个字节。每个字节的第一个比特位决定了该字节的角色:
首字节(Leading byte):它以 “11xxxxxx” 的形式开始,其中 x 表示可变位。
延续字节(Continuationbyte):它们以 “10xxxxxx” 的形式开始。
xC0 的二进制表示是 11000000
如果这两个比特位不等于 0x80(即二进制的 10000000),那么我们可以确定当前字符不是延续字节,(是延续字节的话,那么是10&11=10,那么就是0x80了), 而是一个首字节或ASCII字符。
> 当运行为Hello中的H
依然对得齐

 (gdb) print *tokens@3
$5 = {1, 29871, 29950}

(gdb) p  t->vocab[29871]
$33 = 0x56095caf8410 " "
(gdb) p  t->vocab[29950]
$6 = 0x558aa1290df0 "H"

到此Hello都有个token存到tokens中。如下:

(gdb) print *tokens@8
$13 = {1, 29871, 29950, 29872, 29880, 29880, 
  29877, 21898}
(gdb) p  t->vocab[29872]
$10 = 0x558aa1290430 "e"
(gdb) p  t->vocab[29880]
$11 = 0x558aa1290530 "l"
(gdb) p  t->vocab[29877]
$14 = 0x558aa12904d0 "o"

S4:合并,根据 vocab_scores 中的得分合并最佳连续对,如果有合并,那么tokens肯定是要变化的。具体地,根据tokens[i], tokens[i+1]在 t->vocab找到字符串,并将两个存到str_buffer。然后,去t->sorted_vocab找,看有没有。没有继续循环
部分代码如下:

for (int i=0; i < (*n_tokens-1); i++)
(gdb) print *n_tokens
$16 = 7```

```c
 sprintf(str_buffer, "%s%s", t->vocab[tokens[i]], t->vocab[tokens[i+1]]);
 将tokens里的连续索引,如i=0时,tokens[i]和tokens[i+1]分别为129871,如上面S3输出两个对应值,如下:
 (gdb) p t->vocab[1]
$12 = 0x562c6aa9de20 "\n<s>\n"
 (gdb) p t->vocab[29871]
$21 = 0x558aa1290410 " "

(gdb) print str_buffer@5
$6 = {0x562c6aa9ce20 "\n<s>\n ", 0x7ffedb73e490 "`\350s\333\376\177", 
  0x562c68fda57f <generate+152> "\213E\270\205\300\177-H\213\005\263:", 
  0x3e800 <error: Cannot access memory at address 0x3e800>, 
  0x10000003e7f <error: Cannot access memory at address 0x10000003e7f>}
这个一看在sorted_vocab没有

S4_1:有的话,如下

i=1,空格+H
(gdb) print str_buffer@5
$15 = {0x562c6aa9ce20 " H", 0x7ffedb73e490 “`\350s\333\376\177”,
(gdb) print id
$16 = 379
确实有
(gdb) p t->vocab[379]
$21 = 0x562c6aaa0d60 " H"
(gdb) p t->vocab_scores[id]
$22 = -120
然后,判断分数,由于在vocab中,一个字符串和分数是配套好的,如果分数>best_score
那么将分数、id、idx都赋给best,记录分数和位置。

S_5:改变tokens的i个数和数值,While(1)是将Hello有一个合并一个,多个就合并最后一个,一个就合并哪一个,知道没有为止

// merge the consecutive pair (best_idx, best_idx+1) into new token best_id
tokens[best_idx] = best_id;
// delete token at position best_idx+1, shift the entire sequence back 1
for (int i = best_idx+1; i < (*n_tokens-1); i++) {
      tokens[i] = tokens[i+1]; //best_idx+1后面的全部前移一个,那么n_token也减少一个
        }
        (*n_tokens)--; // token length decreased
 }

这儿代码好像只对最后一对起效果??
best_id就是tokens里面的数值
best_idx是tokens里的索引
(gdb) p best_id
$26 = 295
(gdb) p best_idx
$25 = 3

原来的tokens

(gdb) print *tokens@8
$13 = {1, 29871, 29950, 29872, 29880, 29880, 
  29877, 21898}
(gdb) p tokens[3]
$5 = 29872

代码更新后,
tokens[3] = 295;

 (gdb) print *tokens@8
$7 = {1, 29871, 29950, 295, 29880, 29880, 29877, 21972}
(gdb) p t->vocab[295]
$8 = 0x55d42b07b2e0 "el"

for (int i = best_idx+1; i < (*n_tokens-1); i++) {
tokens[i] = tokens[i+1];
}

$9 = {1, 29871, 29950, 295, 29880, 29877, 29877, 21972} 少tokens[4],即29880

我没有关tokens多大哈
现在是H el l o了

(gdb) p t->vocab[29950]
$14 = 0x55d42b162df0 "H"
(gdb) p t->vocab[295]
$15 = 0x55d42b07b2e0 "el"
(gdb) p t->vocab[29880]
$16 = 0x55d42b162530 "l"
(gdb) p t->vocab[29877]
$18 = 0x55d42b1624d0 "o"

继续

这段代码是在执行某种合并操作,例如在自定义词元生成算法中(如Byte Pair Encoding, BPE),将连续的两个词元合并成一个新的词元。下面是一个具体的例子来说明这段代码的作用:

假设我们有一个由小到大排序后的词元序列 tokens,当前包含以下内容:

tokens = [ "a", "b", "c", "ab", "cd", "ef" ]
n_tokens = 6

现在,在某个迭代中,找到了最佳要合并的连续对 (best_idx, best_idx+1),比如是索引为2和3的位置,即 "c""ab",它们合并后的新词元 ID 是 best_id,值为 "cab"

执行上述代码段之后:

  1. 将合并后的新词元ID替换原序列中的第一个词元:

    tokens[best_idx] = best_id; // tokens[2] = "cab"
    

    更新后的 tokens 序列为:["a", "b", "cab", "cd", "ef"]

  2. 删除合并后不再需要的第二个词元,并将后面的词元向前移动一位://tokens[i]被后面的覆盖,因此没了

    for (int i = best_idx+1; i < (*n_tokens-1); i++) {
        tokens[i] = tokens[i+1]; 
    }
    

    执行循环后,tokens 序列变为:["a", "b", "cd", "ef", "ef"]

  3. 减少词元数量计数器:

    (*n_tokens)--; // token length decreased
    

    现在 n_tokens 的值变为5,表示序列中剩余的有效词元数量。

最终更新后的词元序列是:["a", "b", "cd", "ef"]。通过这样的合并操作,可以逐步构建出一个更复杂的词汇表或编码方案。

一直到输出

(gdb) p *n_tokens
$2 = 2
(gdb) p *tokens@2
$4 = {1, 15043}
(gdb) p t->vocab[15043]
$5 = 0x559ff19aa660 " Hello"
(gdb) p t->vocab[1]
$9 = 0x559ff1934e20 "\n<s>\n"

还有个结束的,

 `if (eos) tokens[(*n_tokens)++] = 2;`

**

最后返回来的值

**

(gdb) p num_prompt_tokens
$4 = 2
(gdb) p *prompt_tokens@2
$3 = {1, 15043}
(gdb)  p tokenizer->vocab[1]
$8 = 0x55dad3df3e20 "\n<s>\n"
(gdb) p tokenizer->vocab[15043]
$5 = 0x55dad3e69660 " Hello"

参考链接:
https://blog.csdn.net/qq_27149279/article/details/131976119
https://mp.weixin.qq.com/s/WqhYL_k_JAUzvI3mQtRhMw
https://blog.csdn.net/qq_37771209/article/details/127664462

  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值