makemore: bigram

似然:使概率的乘积最大 对数似然:使log(概率)的和最大 负对数似然:使-log(概率)的和最小 取负对数似然作为loss函数

对各种输出计数 -> 计算各种输出的概率 -> 按照概率抽样 输入给softmax的logits是对各种输出的计数的对数(倒推出来的意义) -> exp(logit)就是计数 -> softmax就是各种输出的概率 -> 按照概率抽样

广播机制:维度右对齐,然后左边补1

makemore: mlp

loss = F.cross_entropy(logits, Y) <=等价于=>

counts = logits.exp()
prob = counts / counts.sum(1, keepdims=True)
loss = -prob[torch.arange(N), Y].log().mean()

makemore: batchnorm

权重矩阵不初始化为0,初始化为很小的数字,用于对称性破坏

论文Delving deep into rectifiers: Surpassing human-level performance on ImageNet classification中初始化的缩放因子,有2因为激活函数ReLu基本上丢弃了一半的分布并将其限制在0。 torch.nn.init.kaiming_normal_()实现了论文中的初始化,是常见的神经网络初始化方法,ReLu对应的gain是。 ref: 各激活函数的gain

将隐藏层在激活之前进行标准化:

hpreact = embcat @ @1 + b1 # hidden layer pre-activation
bnmean_i = hpreact.mean(0, keepdim=True) # 此批次的
bnstd_i = hpreact.std(0, keepdim=True)
bngain, bnbias = torch.ones((1, n_hidden)), torch.zeros((1, n_hidden))
hpreact = bngain * (hpreact - bnmean_i) / bnstd_i + bnbias  # 标准化
# <- 这儿更新 bnmean_running 和 bnstd_running
h = torch.tanh(hpreact) # hidden layer

这就是BatchNorm

推断时用在整个训练集计算的bn_meanbn_std。 或者用指数移动平均作近似 bn_mean_runningbn_std_running

# 初始化
bnmean_running = torch.zeros((1, n_hidden))
bnstd_running = torch.ones((1, n_hidden)) 
# 更新
with torch.no_grad():
  # 这里momentum为0.001
  bnmean_runnng = 0.999 * bnmean_running + 0.001 * bnmean_i
  bnstd_runnng = 0.999 * bnstd_running + 0.001 * bnstd_i

线性层或卷积层 => BatchNorm => 非线性激活 这里线性 层或卷积层可以省略bias,因为假使加上bias,也会在BatchNorm中归一化时被减掉。

BatchNorm缺点:在前向传播时跟批次样本耦合

makemore: wavnet

卷积基本上可以认为是一个for循环,在某些输入序列的空间上应用一个小的线性滤波器

build GPT

transformer的输入

  • 从训练文本中随机选batch_size个block,block长block_size+1。block可重叠,互相独立。
  • 每个block贡献block_size个示例:从长1的序列推下一个、从长2的序列推下一个、……、从长block_size的序列推下一个。
def get_batch(split):
    data = train_data if split == 'train' else val_data
    ix = torch.randint(len(data) - block_size, (batch_size,))
    x = torch.stack([data[i:i+block_size] for i in ix])
    y = torch.stack([data[i+1:i+1+block_size] for i in ix])
    return x, y

注解:

ix = torch.randint(len(data) - block_size, (batch_size,))

其中 max(ix) = len(data)-block_size-1,块[max(ix), len(data))block_size+1

x = torch.stack([data[i:i+block_size] for i in ix])
y = torch.stack([data[i+1:i+1+block_size] for i in ix])

一个block贡献block_size个示例:

data[i:i+1] => data[i+1]
data[i:i+2] => data[i+2]
...
data[i:i+block_size] => data[i+block_size]

x和y的形状都是

batch_size * block_size
nn.Embedding(vocab_size, n_embd)
  • 输入是任意形状的、值为[0,vocab_size)的张量,比如形状为(a,b)
  • 输出会添加一维(a,b,n_embd)
nn.Linear(n_embd, vocab_size)
  • 输入的最后一维为n_embd,比如形状为(a,b,n_embd)
  • 输出的最后一维替换为vocab_size,比如变为(a,b,vocab_size)

self-attention

B,T,C 指 batch, time, channel

用下三角矩阵,计算矩阵行的累加均值

# x形状是(B,T,C),计算x行的累加均值
wei = torch.tril(torch.ones(T,T))
wei = wei / wei.sum(1, keepdim=True) # --- (1)
out = wei @ x # (T,T) @ (B,T,C) --右对齐广播--> (B,T,T) @ (B,T,C) --> (B,T,C)

扩展成用softmax

import torch.nn.functional as F
 
tril = torch.tril(torch.ones(T,T))
wei = torch.zeros((T,T))
wei = wei.masked_fill(tril == 0, float('-inf')) # 不要的标为-inf
wei = F.softmax(wei, dim=-1)  # --- (2),这4行等同于(1)的2行
out = wei @ x

扩展成加权和,就是self-attention

# let's see a single Head perform self-attention
head_size = 16
key = nn.Linear(C, head_size, bias=False)
query = nn.Linear(C, head_size, bias=False)
k = key(x) # (B,T,head_size)
q = query(x) # (B,T,head_size)
wei = q @ k.transpose(-2-1) # (B,T,head_size) @ (B,head_size,T) ---> (B,T,T)
wei *= head_size**-0.5 # 保持wei方差为1
 
tril = torch.tril(torch.ones(T,T))
# wei = torch.zeros((T,T))
wei = wei.masked_fill(tril == 0, float('-inf'))
wei = F.softmax(wei, dim=-1)
# out = wei @ x
# 不聚合x,聚合v(x通过线性层后的值)
value = nn.Linear(C, head_size, bias=False)
v = value(x) # (B,T,head_size)
out = wei @ v # (B,T,head_size)
  • attention是节点间的沟通机制,每个节点加权聚合所有指向它的节点的信息
  • attention的节点没有空间的概念,所以要加上位置编码
  • batch中的block互相独立不沟通,block中的节点互相沟通
  • encoder注意力块中删除masked_fill()那行代码,允许所有节点沟通;在decoder注意力块中需要masked_fill(),以保证自回归时不使用未来信息
  • self-attention指query都来自x,key和value也来自x;cross-attention指query来自x,而key和value来自他处(比如来自encoder块)
  • 缩放点积attention,还需要wei / sqrt(head_size),以保证wei方差为1

transformer的优化

def forward(self, x):
    x = x + self.sa(self.ln1(x))
    x = x + self.ffwd(self.ln2(x))
    return ×

残差网络

x = x + XXXLayer(x)XXXLayer(x)可以认为是计算分支

LayerNorm

pre-norm,在转换层之前使用LayerNorm(原论文在之后使用)。 LayerNorm在特征维度做规范化(pytorch中特征维度在dim=1)。

Dropout

Dropout训练了一堆子网络的ensemble,可以认为是一种正则化技术

  • 对残差网络的计算分支Dropout
  • 对self-attention的权重weiDropout

build GPT Tokenizer

BPE, Byte Pair Encoding

递归地将最常见的两个token合成一个token,wiki

参考