mini-gpt

karpathy 的300行mini-gpt

karpathy 300行实现了mini-gpt,是一个很好的学习范例。

1
2
3
4
5
6
7
class NewGELU(nn.Module):
"""
Implementation of the GELU activation function currently in Google BERT repo (identical to OpenAI GPT).
Reference: Gaussian Error Linear Units (GELU) paper: https://arxiv.org/abs/1606.08415
"""
def forward(self, x):
return 0.5 * x * (1.0 + torch.tanh(math.sqrt(2.0 / math.pi) * (x + 0.044715 * torch.pow(x, 3.0))))

ReLU被GELU这样更好的激活函数替代了。

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
class CausalSelfAttention(nn.Module):
"""
A vanilla multi-head masked self-attention layer with a projection at the end.
It is possible to use torch.nn.MultiheadAttention here but I am including an
explicit implementation here to show that there is nothing too scary here.
"""

def __init__(self, config):
super().__init__()
assert config.n_embd % config.n_head == 0
# key, query, value projections for all heads, but in a batch
self.c_attn = nn.Linear(config.n_embd, 3 * config.n_embd)
# output projection
self.c_proj = nn.Linear(config.n_embd, config.n_embd)
# regularization
self.attn_dropout = nn.Dropout(config.attn_pdrop)
self.resid_dropout = nn.Dropout(config.resid_pdrop)
# causal mask to ensure that attention is only applied to the left in the input sequence
self.register_buffer("bias", torch.tril(torch.ones(config.block_size, config.block_size))
.view(1, 1, config.block_size, config.block_size))
self.n_head = config.n_head
self.n_embd = config.n_embd

def forward(self, x):
B, T, C = x.size() # batch size, sequence length, embedding dimensionality (n_embd)

# calculate query, key, values for all heads in batch and move head forward to be the batch dim
q, k ,v = self.c_attn(x).split(self.n_embd, dim=2)
k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs)
q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs)
v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs)

# causal self-attention; Self-attend: (B, nh, T, hs) x (B, nh, hs, T) -> (B, nh, T, T)
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)
att = self.attn_dropout(att)
y = att @ v # (B, nh, T, T) x (B, nh, T, hs) -> (B, nh, T, hs)
y = y.transpose(1, 2).contiguous().view(B, T, C) # re-assemble all head outputs side by side

# output projection
y = self.resid_dropout(self.c_proj(y))
return y

从karpathy的代码告诉我们,要把注意力机制从零开始理清楚,So,let’s Go!

注意力机制

注意力机制的发展追溯至19世纪90年代,把注意力分类成自主的和非自主的。

非自主性注意力

一个人刷朋友圈,看到通过机器学习专业的同班心仪对象post酒吧喝酒的自拍,非自住的注意力就被drag到这里,产生了一系列想法,比如去喝点,或者他/她和谁在喝?非自主性注意力正在发挥效果。

自主性注意力

这个时候,受自我驱动力的影响,脑子提示这个时候有些阅读任务没有完成,于是转头看向床头那本尚未翻完的《深度学习》,自主性注意力正在发挥效果。

非自主与自主性相互融合与碰撞

最后,在两种注意力的对撞和冲击下,这个人带着手机出门去那个bar和对象喝一杯,在车上观看着《深度学习》,思索着这一切,到了bar后和他/她来了一场关于机器学习的哲学辩论。
在这个不恰当的例子里, query是自我驱动力(自主性注意力),key是目之所视的一切(非自主注意力),
query和key的结合,通过脑皮层,最终得出了这个值(”why not just go for a drink while reading at texi and think abou it)

最简单的注意机制,注意力汇聚机制

注意力汇聚机制是最简单的注意力机制。
图:(todo: 已经画好了)

也就是对于一个query,通过a函数遍历每个k,a函数的功能在于找到比较相似的key值。
a(q, k1) + a(q, k2) + a(q, k3) + a(q, k4) …. + a(q, kn), 这里的q和k的到的a值就是注意力权重值也就是attention weight。
在上述的例子里:
key是非自主注意力(也就是躺床上看到的一切,朋友圈里的同个机器学习专业的心仪对象k1,床边的书k2,桌子上的台灯k3,衣柜里的袜子k4)
q是自主注意力(也就是需要翻完深度学习这本书)

注意力评分函数

这个a函数的特点是:如果q和k越接近,则得到的值越大,反之越小。
这类注意力评分函数有许多选择先,按下不表。

a(q,k1) = 0.3 这个q和k1还是有点关系的,毕竟是同一个专业。
a(q,k2) = 0.8 关系很高,这本书是专业书籍。
a(q,k3) = 0 毫无关系
a(q,k4) = 0 毫无关系

最后k1对应的v1,肯定是date with her, k2对应的v2是把这本书学习完。
F(q) = 0.3 x v1 + 0.8 x v2 混合而成最终的行为。

引入w参数到注意力评分函数

但是下一步参数w,因为训练就是迭代w到自己满意的地步。
公式:

引入参数后特点是,w是可以学习的,可以变化的,举个例子,经过一段时间的date,发现这个同系的crush不太适合自己,对其已经下头。
而且频繁去bar,已经把自己喝坏了。所以w值正在下降。

经过一段时间后:
a(q,k1) x w1 = 0.01 w1发挥了作用,下头了兄弟。
a(q,k2) x w2 = 0.9 关系很高,依旧很高,这本书是专业书籍,而且觉得自己更加必须看了。
a(q,k3) x w3 = 0 毫无关系
a(q,k4) x w4 = 0 毫无关系

F(q) = 0.01 x v1 + 0.8 x v2 混合而成的行为就像,已经对这个同专业的crush下头,不想和对方进行类似的思辨和喝酒了。
最终,pyq就小小点个赞,然后迅速放下手机,拿起床头边的这本书,开始翻起来。