gpt2Tokenizer

怎么构造Token表

construct token

nn.Embedding,根据字符对应的序号取,是可训练的
Language Models are Unsupervised Multitask Learners
Token是大模型的原子,一切都以标记为单位,与标记有关,不要忽略它!!

非英语语言表现较差,英语比非英语多得多训练数据,得到更长的优质token
gpt2写python不利,太多缩进,gpt2把空格都当成了token

同样的文本,gpt4的token数量比gpt2少一半,这是因为其token数量是gpt2的两倍,但这并不是好事,这意味着embedding表会非常大
但有个最佳点,所有词汇表中的token数量恰到好处,适当密集且高效
gpt4对python的空格处理已经相当搞笑,多个空格为一个token,这样能看到前面更多的token

unicode是一个包含150000个不同代码点的词汇表,它不一定我是我们想用的稳定表示,三种编码方式UTF-8,16,32,变长编码
如果使用utf8,那转向字节对编码算法,允许我们将这些字节序列压缩到一个可变的量,找到最频繁的token对,用一个新token代替,并将其加入词汇表

压缩多少次,这是一个超参数
这是大模型训练的预处理阶段,更多的同一个语言,有利于压缩token的数量

担心词与标点符号被认为一个token,比如dog.,dog!,dog?这是我们不希望看到的,他们不应该合并

特别的token(endoftext用于分隔文档)
后期需要用到special token,用于分隔/特定顺序。比如gpt4中的FIM_MIDDLE(FIM表示填充,fill in middle)
specialToken有助于我们微调大模型以达到特定的目的

另一个常用的方法

sentencepiece

全面,在训练和推理方面都很高效。llama使用。
stp在unicode代码点上直接运行BPE,然后有选项
character_coverage去为出现很少的很稀有的代码点(遇到了训练时候没见过的),将它们映射到UNKnown,如果开了byte_fallback就变为utf-8
句子开头额外出现一个粗体下划线,这么做优点是对于tiktoken,同一个单词,他在句子开头出现和中间出现完全不同,在开头加个下划线,再把空格变下划线,那分词器会将他们理解为同一个词

为什么词汇表大小不能无限增长?

  1. 计算成本增加,参数增加
  2. 部分token出现得很少,那么关于它那部分的embedding训练不足
  3. 压缩得太厉害,一个token可能被展开成为成千上万个,transformer没有时间实际恰当地处理这些信息
    经验10000-100000

Learning to Compress Prompts with Gist Tokens

如果要新增token,那么要保持整个模型冻结,只训练新标记机器嵌入表示。将很长的提示符压缩为几个新的要点标记,测试时候只需要丢弃旧的token,用新token重排表示prompt即可

gpt2 problem

在算数时候将一串连续数字划分为1,2,3,4长度的数字是随机的

其中标记要么是您完成下一个标记的第一个字符,要么就是很长的token,被拆分成一段段

有特定的触发词,模型查看包含它的词就会变得混乱。怎么做到的?词汇表和训练数据集不一致,训练数据集缺乏该词汇

值得去理解tokenization

完整代码

import regex as re


tokens = 'Unicode! 🅤🅝🅘🅒🅞🅓🅔‽ 🇺‌🇳‌🇮‌🇨‌🇴‌🇩‌🇪! 😄 The very name strikes fear and awe into the hearts of programmers worldwide. We all know we ought to “support Unicode” in our software (whatever that means—like using wchar_t for all the strings, right?). But Unicode can be abstruse, and diving into the thousand-page Unicode Standard plus its dozens of supplementary annexes, reports, and notes can be more than a little intimidating. I don’t blame programmers for still finding the whole thing mysterious, even 30 years after Unicode’s inception.'.encode('utf-8')
tokens = map(int ,tokens)
tokens = list(tokens)
def get_stats(ids):
    counts = {}
    for pair in zip(ids, ids[1:]):
        counts[pair] = counts.get(pair, 0) + 1
    return counts

stats = get_stats(tokens)
# print(stats)
sorted(((v, k) for (k, v) in stats.items()), reverse=True)
top_pair = max(stats, key=stats.get)
print(top_pair)

def merge(ids, pair, idx):
    newids = []
    i = 0
    while i < len(ids):
        if i < len(ids) - 1 and ids[i] == pair[0] and ids[i+1] == pair[1]:
            newids.append(idx)
            i += 2
        else:
            newids.append(ids[i])
            i += 1
    return newids

tokens2 = merge(tokens, top_pair, 256)
print(f'original: {len(tokens)} tokens, new: {len(tokens2)} tokens')
# 转换多少次,这是一个超参数,需要在实际应用中进行调整

vocab_size = 276
num_merges = vocab_size - 256
ids = list(tokens)

merges = {}
for i in range(num_merges):
    stats = get_stats(tokens)
    pair = max(stats, key=stats.get)
    idx = 256 + i
    # print(f'merging {pair} into a new token {idx}')
    ids = merge(ids, pair, idx)
    merges[pair] = idx

vocab = {idx : bytes([idx]) for idx in range(256)}
for (p0, p1), idx in merges.items():
    vocab[idx] = vocab[p0] + vocab[p1]
def decode(ids):
    tokens = b"".join(vocab[idx] for idx in ids)
    text = tokens.decode('utf-8', errors="replace")
    return text

def encode(text):
    tokens = list(text.encode("utf-8"))
    while len(tokens) >= 2:
        stats = get_stats(tokens)
        pair = min(stats, key=lambda p: merges.get(p, float('inf'))) # min merge priority
        if pair not in merges:
            break
        idx = merges[pair]
        tokens = merge(tokens, pair, idx)
    return tokens

print(decode(encode("hello world")))


gpt2pat = re.compile(r"""'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+""")
print(re.findall(gpt2pat, 'hello"s you sun are seek '))

魔改transformer

大体结构

wte是输出嵌入,但它实际上是token嵌入
wpe是位置嵌入

这两条信息将被添加,然后进入transformer
h表示隐藏, 表示所有灰色块
lmhead是末尾的线性部分

注意力是规约,MLP是映射

激活函数改进

relu->gelu,主要区别是0点附近变成连续
llama3使用swiglu

多头注意力机制

原始版本就是单纯地连接输出向量
Andrej将QKV合为一体进行拆分,提高pytorch地处理速度

查询和键值相互作用引起注意力,自回归掩码,确保标记仅关注以前的标记,softmax标准化注意力

class GPTConfig:
    block_size: int = 1024  # max sequence length
    vocab_size: int = 50257  # number of tokens: 50,000 BPE merges + 256 bytes tokens + 1 <|endof
    n_layer: int = 12  # number of layers
    n_head: int = 12  # number of heads
    n_embd: int = 768  # embedding dimension

参数设置

B个序列,最大长度不超过T
标记数量不能超过最长块
事实上,类似dropout,BN层在模型评估和训练模式下有比较大的差别

预测

while x.size(1) < max_length:
    # forward the model to get the logits
    with torch.no_grad():
        logits = model(x)  # (B, T, vocab_size)
    
    # take the logits at the last position
    logits = logits[:, -1, :]  # (B, vocab_size)
    
    # get the probabilities
    probs = F.softmax(logits, dim=-1)
    
    # do top-k sampling of 50 (huggingface pipeline default)
    # topk_probs here becomes (5, 50), topk_indices is (5, 50)
    topk_probs, topk_indices = torch.topk(probs, 50, dim=-1)
    
    # select a token from the top-k probabilities
    ix = torch.multinomial(topk_probs, 1)  # (B, 1)
    
    # gather the corresponding indices
    xcol = torch.gather(topk_indices, -1, ix)  # (B, 1)
    
    # append to the sequence
    x = torch.cat((x, xcol), dim=1)

# print the generated text
for i in range(num_return_sequences):
    tokens = x[i, :max_length].tolist()
    decoded = enc.decode(tokens)
    print(">", decoded)

token

GPT2压缩率大致为3比1,因此1000个字符大约是300个标记
额外加载多一个标记,原buf作为输入,偏移一位作为标签(懂吧),预测下一位
计算损失使用交叉熵函数即可

怎么判断初始化是不是好的?如果是好的,那概率分布大致是分散的,也就是每个概率都相等
adamW修正误差,是adam的改进版本,保留了m和v,两个缓冲区

训练

损失是单个元素的张量,item是获取元素然后送回到CPU内存
对于模型来说直接调用todevice就能移动到GPU上,但是对于普通张量返回的是一个指针,需要重新赋值
在早期调试阶段运行的大多数优化中,学习率设为3e-4是一个不错的默认值

如果出现了不在词汇表中的,获取所有从未出现的logit的偏差并将它们驱动到负无穷大来获得不错的收益

如果你有两个语义相似的token,你会期望它们在transformer的输出中获得相同的概率,所以相似的token应该具有相似的嵌入向量
所以wte张量基本被使用了两次,在transformer的底部和顶部
同时不用训练这么多参数,训练会更有效率(共享)

模块有默认初始化函数

自定义初始化函数用self.apply + 初始化函数
gpt2觉得残差流会累积误差,残差网络的每个块会贡献方差,然后累加

int8用于推理,而不是训练,因为int8具有均匀的间距

batch_size,还有一些通道数取2的幂,因为cuda是按2的幂分配SM的
如果迫不得已,请增加到能被64整除

梯度norm:使得各层的梯度上限不超过1.0,这样可以使得模型对于比较大的梯度不敏感

weight_decay(分类讨论):我们可以设置维度大于等于2的进行正则化,一维就没必要了。

numel()是统计张量中元素的总数,adamw中有fusion选项提供,内核融合

用micro step拼凑起big step,累加梯度

warmup

torch.compile和自定义验证竟然冲突??

contiguous保证向量在内存中连续存储,便于view展开

模型编译的问题在于它确实使我们的代码更快,但破坏了评估代码和采样代码(在作者的代码中)

每一定步数保存检查点,模型的状态字典,还可以保存adam的状态字典,因为有超参数b和w

GPU

采用TF32,32位张量拥有19位(1符号,8阶码,10浮动),对于普通FP32来说截断了13位
支持输入FP32,输出FP32,但是内部计算过程中会被截断成TF32以更快地执行操作

启动TF32:
torch.set_float32_matmul_precision(‘high’)

使用autocast上下文管理器:
PyTorch 提供的一个上下文管理器,用于在支持的硬件上自动执行混合精度训练
with torch.autocast(device_type=‘cuda’, dtype=torch.bfloat16):
output
loss
loss.backward()

torch.compile:
用于将 PyTorch 模型编译为更高效的表示形式,以便在特定硬件上进行更快的执行,
编译会分析模型,确切知道模型收益于什么,不会像python一样一层一层地走,而是总体优化,编译整个模型成为一个没有py解释器参与的对象

举个例子,当你完成一层计算的时候,python并不知道接下来就要用到其中的数据,所以将其写回GPU内存,然后再取出来,时间成本在内存带宽上严重浪费。而经过编译后的模型就比较聪明了,他会保留数据到cache之中

GPU SRAM: 19 TB/s (20 MB)
SRAM GPU HBM: 1.5 TB/s (40 GB)
HBM Main Memory DRAM: 12.8 GB/s (>1TB) (CPU DRAM)

内核融合:将多个独立的内核操作(卷积,激活函数,归一化等)融合成一个单一的内核操作,从而减少内存访问次数,降低计算开销提高数据局部性

分布式数据并行

# run the training loop
from torch.distributed import init_process_group, destroy_process_group

# set up DDP (distributed data parallel).
# torchrun command sets the env variables RANK, LOCAL_RANK, and WORLD_SIZE
ddp = int(os.environ.get('RANK', -1)) != -1  # is this a ddp run?

if ddp:
    # use of DDP atm demands CUDA, we set the device appropriately according to rank
    assert torch.cuda.is_available(), "for now i think we need CUDA for DDP"
    init_process_group(backend='nccl')
    ddp_rank = int(os.environ['RANK'])
    ddp_local_rank = int(os.environ['LOCAL_RANK'])
    ddp_world_size = int(os.environ['WORLD_SIZE'])
    device = f'cuda:{ddp_local_rank}'
    torch.cuda.set_device(device)
    master_process = ddp_rank == 0  # this process will do logging, checkpointing etc.
else:
    # vanilla, non-DDP run
    ddp_rank = 0
    ddp_local_rank = 0
    ddp_world_size = 1
    master_process = True

# attempt to autodetect device
device = "cpu"
if torch.cuda.is_available():
    device = "cuda"
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
    device = "mps"

print(f"using device: {device}")

使用ddp, torchrun配合,有点像并行程序设计

DDP在反向传播时候,对梯度同步并广播求平均,同步到每个GPU
向后同步当微小步是最后时候才设置为True打开

for step in range(max_steps):
    t0 = time.time()
    optimizer.zero_grad()
    loss_accum = 0.0

    for micro_step in range(grad_accum_steps):
        x, y = train_loader.next_batch()
        x, y = x.to(device), y.to(device)

        with torch.autocast(device_type=device, dtype=torch.bfloat16):
            logits, loss = model(x, y)
            # we have to scale the loss to account for gradient accumulation,
            # because the gradients just add on each successive backward().
            # addition of gradients corresponds to a SUM in the objective, but
            # instead of a SUM we want MEAN. Scale the loss here so it comes out right
            loss = loss / grad_accum_steps
            loss_accum += loss.detach()

        if ddp:
            model.require_backward_grad_sync = (micro_step == grad_accum_steps - 1)

        loss.backward()

    if ddp:
        dist.all_reduce(loss_accum, op=dist.ReduceOp.AVG)

    norm = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)

    # determine and set the learning rate for this iteration
    lr = get_lr(step)
    for param_group in optimizer.param_groups:
        param_group['lr'] = lr

    optimizer.step()
    torch.cuda.synchronize()  # wait for the GPU to finish work

数据集

Reddit,社交网络
FineWeb~

在GPT2时代,对原始输入数据内容不太重视,
但现在,对重复数据删除,过滤,质量过滤等方面的良好实践进行了更多的审查
优质token很重要!!

数据加载时候可以考虑随机打乱,每个新时期的每个分片中排列文档
可以在文档如何相互跟随方面引入一些随机性

测试集

HelloSwaq,来判断哪个是句子的自然延续

微调

此时我们得到的只是预训练的大模型,如果想要对话还需要进行微调
时候可以考虑随机打乱,每个新时期的每个分片中排列文档
可以在文档如何相互跟随方面引入一些随机性

测试集

HelloSwaq,来判断哪个是句子的自然延续

微调

此时我们得到的只是预训练的大模型,如果想要对话还需要进行微调
加入对话的训练集,有一个用户,助手之类的结构

完整代码

import math
import time

import torch
import torch.nn as nn
from torch.nn import functional as F

class CausalSelfAttention(nn.Module):
    def __init__(self, config):
        assert config.n_embd % config.n_head == 0
        self.c_attn = nn.Linear(config.n_embd, config.n_embd * 3)
        self.n_head = config.n_head
        self.c_proj = nn.Linear(config.n_embd, config.n_embd)
        self.n_embd = config.n_embd
        self.register_buffer("bias", torch.tril(torch.ones(config.block_size, config.block_size))
                             .view(1, 1, config.block_size, config.block_size))

        self.c_proj.NANOGPT_SCALE_INIT = 1  # 不会与之前东西冲突

    def forward(self, x):
        B, T, C = x.size()
        qkv = self.c_attn(x)
        q, k, v = qkv.split(C, dim=2)
        q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
        k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
        v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)

        # att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
        # att = att.masked_fill(self.bias[:, :, :T, :T] == 0, float('-inf'))
        # att = F.softmax(att, dim=-1)
        # y = att @ v

        # flashAttention
        y = F.scaled_dot_product_attention(q, k, v, is_causal=True)

        y = y.transpose(1, 2).contiguous().view(B, T, C)
        y = self.c_proj(y)
        return y

class MLP(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.c_fc = nn.Linear(config.n_embd, config.n_embd * 4)
        self.gelu = nn.GELU(approximate='tanh')
        self.c_proj = nn.Linear(config.n_embd * 4, config.n_embd)
        self.c_proj.NANOGPT_SCALE_INIT = 1 # 不会与之前东西冲突

    def forward(self, x):
        x = self.gelu(self.c_fc(x))
        x = self.c_proj(x)
        return x
class Block(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.ln1 = nn.LayerNorm(config.n_embd)
        self.attn = CausalSelfAttention(config)
        self.ln2 = nn.LayerNorm(config.n_embd)
        self.mlp = MLP(config)

    def forward(self, x):
        x = x + self.attn(self.ln1(x))
        x = x + self.mlp(self.ln2(x))
        return x

class GPTConfig:
    block_size: int = 1024
    vocab_size: int = 50257
    n_layer: int = 12
    n_head: int = 12
    n_embd: int = 768

class GPT(nn.Module):

    def __init__(self, config):
        super().__init__()
        self.config = config

        self.transformer = nn.ModuleDict(dict(
            wte = nn.Embedding(config.vocab_size, config.n_embd),
            wpe = nn.Embedding(config.block_size, config.n_embd),
            h = nn.ModuleList([Block(config) for _ in range(config.n_layer)]),
            ln_f = nn.LayerNorm(config.n_embd)
        ))

        self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)

        # 共享权重
        self.transformer['wte'].weight = self.lm_head.weight

        # 初始化权重
        self.apply(self._init_weights)

    def _init_weights(self, module):
        if isinstance(module, nn.Linear):
            std = 0.02
            if hasattr(module, 'NANOGPT_SCALE_INIT'):
                std *= (2 * self.config.n_layer) ** -0.5
            torch.nn.init.normal_(module.weight, mean=0.0, std=std)
            if module.bias is not None:
                torch.nn.init.zeros_(module.bias)
        elif isinstance(module, nn.Embedding):
            torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
            if module.padding_idx is not None:
                torch.nn.init.zeros_(module.weight[module.padding_idx])


    def forward(self, idx, targets=None):
        b, t = idx.size()
        assert t <= self.config.block_size, 'Cannot forward, model block size is exhausted.'

        h = self.transformer['wte'](idx) + self.transformer['wpe'](torch.arange(t, device=idx.device))
        for block in self.transformer['h']:
            h = block(h)
        h = self.transformer['ln_f'](h)
        lm_logits = self.lm_head(h)

        if targets is None:
            return lm_logits
        loss = F.cross_entropy(lm_logits.view(-1, lm_logits.size(-1)), targets.view(-1))
        return loss

model = GPT(GPTConfig())
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, betas=(0.9, 0.95), eps=1e-8)

for i in range(50):
    t0 = time.time()
 
    torch.cuda.synchronize()

    optimizer.zero_grad()
    with torch.autocast(device_type='cpu', dtype=torch.bfloat16):
        logits, loss = model(input_ids, labels)

    loss.backward()
    norm = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)

    # 设置学习率
    lr = get_lr()




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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值