My notes and records during learing CS224N(NLP)
Lecture 1: Word Vectors
词义->上下文
对人而言,理解语言是一种简单的事情,但是计算机不行,只有把词元编码成向量才能被计算和理解,于是要探究如何编码词元
最原始的表示方式:one-hot编码:
给出一个共计有$R$个词元的词表$V$,可以把每一个词元变成一个长度为$R$的向量$v$,假定这个次元的位置是0,那么可以表示成$[1,0,...,0]$
显而易见,这不是一种”良好的”编码方式,因为随着词表$V$的扩大,向量长度也会无限制增大,另一方面,每一个向量之间是正交的,所以无法表示出词与词语义上的关联
这种编码方式带来的”稀疏和高维”正是计算机理解语言不希望看到的
于是转向词元在语句中的位置,一个词与其他词之间的关系可以通过上下文来描述,顺着这个思路,引出Word2vec
Word2vec
词元可以分为两类,一类是中心词,一类是上下文,它们类别的界定是由”正在考虑哪个词”决定的
e.x.p. : … problems turn into banking crises
先考虑into这个词,考虑它他就是中心词,周围的problems,turn这些就是context,我们另$w_t$表示中心词into,则可以用$w_{t+1}$表示problems
接下来我们限制一个中心词的上下文窗口$m$,也就是对于一个中心词$w_t$,只考虑从$[w_{t-m},w_{t+m}]$区间的词元向量,并计算似然概率:
$$
L(\theta)=\prod_{t=1}^{T}\prod_{\substack{-m\le j\le m\\\\j\ne 0}} P(w_{t+j}\mid w_t;\theta)
$$
这个公式指的就是对于句子中的每一个中心词,都计算上下文窗口为$m$的区域的似然概率
下一步将其转换为便于计算机处理的加法形式,也就是取对数:
$$
J(\theta) = -\frac{1}{T}logL(\theta) = -\frac{1}{T}\sum_{t=1}^{T}\sum_{-m\le j\le m}^{}logP(w_{t+j}|w_t,\theta)
$$
确定了优化目标,还有一个问题没解决,就是怎么计算这个$P(w_{t+j}|w_t,\theta)$的值,在解决之前,先给出两个定义:
$v_w$:$w$是中心向量时的表示
$u_w$:$w$是上下文向量时的表示
对于中心向量$c$和上下文向量$o$:
$$
P(o|c) = softmax(u_o^Tv_c)
$$
即计算中心词向量和上下文向量的点积,用于评估而二者语义距离
这里我详细讲一下,点积的定义是$$
a\cdot b = |a| |b|cos\theta
$$
那么语义上越近的向量,余弦值越大,而越是无关的向量之间的余弦值越小甚至是负数
但是如果用点积直接表示这个距离,不会有$a,b$模长的影响吗?实际上是不会的,因为模长包含着另外的自由度的信息(后面细究),所以可以用其值直接对应距离
传统机器学习中,常用的优化方式梯度下降,但是你真有钱和算力对着数量极大的词表一点一点GD吗?显然不行,下面来介绍随机梯度下降($SGD$):
每次只采样一个小的batch用于更新梯度,这也是现代dl中的常用思想
Training trick
负采样
我们考虑一个问题,在上文提到的计算$P(c|o)$的地方,实际上我们采用了一个$softmax$的归一化操作,这会导致机器去计算一个中心词的所有context的概率,带来非常大的计算开销
这时考虑将这个多分类->二分类,也就是负采样,具体来说,把最小化损失函数$L$的目标换成训练模型二分类的能力,给定一个词元对$(o,c)$,$o$有可能是真实上下文也可能是噪声,打个比方,(deep,research)是一个真实的词对,而(deep,you)就是一个假的词对,负采样目标就是训练出模型的判别能力,损失函数表达式如下:
$$
J = -log\sigma(u_o^Tv_c) - \sum_{k\in K}^{}-log\sigma(-u_o^Tv_c)
$$
即最大化正确词对,最小化虚假词对概率
再对这个词表进行采样,当然这个方法也有着很大的缺陷,那就是每次更新的时候只会涉及到中心词和它的上下文窗口中的词,最终更新的向量少,导致了梯度的高维和稀疏性,这对training来讲是灾难性的
于是提出了一种共性矩阵的方法,还是假设一个共有$v$个词元的词表$V$,维护一个$R^{v \times v}$的矩阵,其中$X_{ij}$表示第$i$个词和第$j$个词出现在一条上下文中的次数
但是到这里还远远不够,这个矩阵还是高维且稀疏的,跟前面提到的本质相同,最巧妙的在于对矩阵应用奇异值分解:
$$
X=U \Sigma V^T
$$
计算出奇异值矩阵之后,选定一个低于$v$的维度$k$,然后取出前$k$行作为新的低秩矩阵的分解矩阵,即:
$$
E=U_kV_k
$$
得到的这个E就是符合我们预期的低维的、稠密的矩阵,也就是最后编码后的词矩阵
GloVe
wait to update…
Lecture 2: neural nets
矩阵求导
在本章最开始,我们约定矩阵求导的时候方向遵照雅可比原则,即对于一个$x \in R^n,y\in R^m$,$\frac{\partial y}{\partial x} \in R^{m \times n}$,如果对后面的求导的转置有问题,先想想这里
给定一个有$n$个输入和一个输出的函数$f(\textbf{x})$,其表达式为:$$
f(\textbf{x}) = f(x_1,x_2,...,x_n)
$$
那么对函数$f(\textbf{x})$求导,有:
$$
\frac{\partial f}{\partial \textbf{x}} = [\frac{\partial f}{\partial x_1},\frac{\partial f}{\partial x_2},...,\frac{\partial f}{\partial x_n}]
$$
接下来我们推广到一个具有$m$个输出和$n$个输入的函数:
$$
\mathbf {f(x)} = [f_1(x_1,x_2,...,x_n),...,f_m(x_1,x_2,...,x_n)]
$$
此时$f$对$x$的梯度就是一个$m \times n$的矩阵:
$$
\frac{\partial \mathbf{f}}{\partial \mathbf{x}} =
\begin{bmatrix}
\frac{\partial f_1}{\partial x_1} & \cdots & \frac{\partial f_1}{\partial x_n}
\vdots & \ddots & \vdots
\frac{\partial f_m}{\partial x_1} & \cdots & \frac{\partial f_m}{\partial x_n}
\end{bmatrix}
$$
也就是说,对于$(\frac{\partial f}{\partial x})_{ij} = \frac{\partial f_i}{\partial x_j} $
模型定义
接下来我们会推导神经网络传播过程的梯度推导,首先我们先明确函数指代:
$$
J(\theta) = \sigma(s) = \frac{1}{1+e^{-s}} \\
\mathbf{s = u^T h}\\
\mathbf{h} = f(\mathbf{z}) \\
\mathbf{z} = \mathbf{W} \mathbf{x} + \mathbf{b}
$$
其中$f$是一个激活函数,可以是ReLU/tanh这些激活函数,然后$\mathbf{z}$是由$\mathbf{x}$线性变换而来
$s$是对于输入$\mathbf{x}$的打分值,然后$\mathbf{u}$是输入层的权重表示
然后我们来看$\frac{\partial \mathbf{h}}{\partial \mathbf{z}}$的计算,这两个向量都是长度为$n$的列向量,求导结果是一个$n \times n$的矩阵:
$$
(\frac{\partial h}{\partial z})_{ij} = \frac{\partial h_i}{\partial z_j}
$$
显然根据偏导数的求导法则,当且仅当$i=j$的时候有:
$$
\frac{\partial f_i}{\partial z_j} = f'(z_i)
$$
而其它情况下导数值全部为0,即:
$$
\frac{\partial \mathbf{f}}{\partial \mathbf{z}} =
\begin{bmatrix}
f'(z_1) & \cdots & 0\\
\vdots & \ddots & \vdots\\
0 & \cdots & f'(z_n)
\end{bmatrix} = diag(\mathbf{f}'(z))
$$
接下来我们看这样一个求导:$$
\frac{\partial \mathbf{(Wx+b)}}{\partial \mathbf{x}} = \mathbf {W}
$$
我们进行一个拆分:
$$
\frac{\partial \mathbf{(Wx + b)}}{\partial \mathbf{x}} = \frac{\partial \mathbf{(Wx)}}{\partial \mathbf{x}} + \frac{\partial \mathbf{b}}{\partial \mathbf{x}}
$$
显然这个$\mathbf{b}$与$\mathbf{x}$是没有关系的,所以第二项的导数值为0,所以它的值只由第一项决定,下面只需要看一看这个东西的值就行了:
我们设矩阵$$
\mathbf{W}=\begin{bmatrix}
w_{11} & \cdots & w_{1n} \\
\vdots & \ddots & \vdots \\
w_{n1} & \cdots & w_{nn}
\end{bmatrix}
$$
计算$\mathbf{Wx}$的值
$$
\mathbf{Wx} = \begin{bmatrix}
w_{11} & \cdots & w_{1n}
\vdots & \ddots & \vdots
w_{n1} & \cdots & w_{nn}
\end{bmatrix}
\begin{bmatrix}
x_1
\vdots
x_n
\end{bmatrix} =
\begin{bmatrix}
w_{11}x_1+w_{12}x_2+...+w_{1n}x_n
\vdots
w_{n1}x_1+w_{n2}x_2+...+w_{nn}x_n
\end{bmatrix}
$$
对于$\frac{\partial \mathbf{Wx}}{\partial \mathbf{x}}$每一项的计算,直接看:
$$
(\frac{\partial \mathbf{Wx}}{\partial \mathbf{x}})_{ij} =
\frac{\partial (\mathbf{Wx})_i}{\partial (\mathbf{x})_j} = w_{ij}
$$
(这是显然的,可以通过随便验证一个位置赖搞清楚)
至此我们就得到了上面的结论,即$\frac{\partial \mathbf{(Wx+b)}}{\partial \mathbf{x}} = \mathbf {W}$
与上文同理,我们还能够得到一个结论:
$$
\frac{\partial \mathbf{(Wx+b)}}{\partial \mathbf{b}} = \mathbf{I}
$$
即线性变换后的结果对bias项求导结果为恒等矩阵$\mathbf{I}$
相似的,还能得到一个结论:
$$
\frac{\partial \mathbf{(u^Th)}}{\partial \mathbf{u}} = \mathbf{h^T}
$$
其实还需要求一个导数,这也是我一开始完全搞不懂的点,就是$\frac{\partial \mathbf{z}}{\partial \mathbf{W}}$是多少,先说结论,值为$\mathbf{x^T}$,下面给推导:
根据本节就开始的原则,这应该是一个$n \times n \times n$的矩阵,但是用了优化方法变到二维了
$$
\mathbf{z_k} = \sum_{t=1}^{n} \mathbf{W_{kt} x_t + b_k}
\frac{\partial \mathbf{z_k}}{\partial \mathbf{W_{ij}}} = \mathbf{x_j} (当且仅当k=i时)
$$
这是显然的,所以说对于$k \in [1,n]$,都有:
$$
\frac{\partial \mathbf{z_k}}{\partial \mathbf{W_{ij}}} = \begin{bmatrix}
0 & 0 & \cdots & 0
\vdots & \vdots & \ddots & \vdots
x_1 & x_2 & \cdots & x_n
\vdots & \vdots & \ddots & \vdots
0 & 0 & \cdots & 0
\end{bmatrix} (此时只有第k行是x^T,其余全是0)
$$
所以我们认为它的值就是$\mathbf{x^T}$
Lecture 3 : rnnlm
Language modeling:给定一个长为$t$的序列$x_1,x_2,...,x_t$,预测出下一个token $x_{t+1}$,其中$x_{t+1}$可以是词表$V$中任意元素
中间讲了一个n-gram的方法,感觉没啥意思,跳过了,直接看RNN
RNN
依旧是一个Language modeling,在给定前$t$个tokens之后,预测第$t+1$的token的流程如下:
对每一个token做编码(one-hot或其他):
$$
\mathbf{x_t} \in R^{|V|}
$$
词嵌入:
$$
\mathbf{e_t} = \mathbf{E x_t}
$$
计算隐藏层,并给定0号隐层的记号为$h_0$:
$$
h_t = \sigma(\mathbf{W_h h_{t-1}+W_e e_t + b_1})
$$
计算概率分数$y$:
$$
y = softmax(\mathbf{U h_t + b_2})
$$
直接来分析这种做法的问题,然后为后面的Transformer结构铺垫:
- 递归式的计算梯度非常耗时和计算资源
- 在lm模型中,第$t+1$个token的测试质量依赖于前$t$个的损失函数$J^t(\theta)$计算,显然的是,这是一个非常长的链式法则(原因藏在$h_t$的表达式当中)
- 上一点会带来梯度爆炸/消失的问题
梯度裁剪(clip):比如说我们采取SGD的更新方法,提前设定好一个阈值$|g|$,如果某一步的梯度$|g_t|$大于这个阈值,则在学习率更新之前进行裁剪:$$ |g_t| = \frac{|g_t|}{|g|} $$
Lecture 4:transformers
注意力机制
核心的思想就是在decoder解码的时候加入一个类似于提示的东西,这个权重用于衡量下一个decode出来的token与前面的token有多大的关系
self-attention : key, value, query
对于$[w_1,w_2,...,w_n] \in R^{|V|}$有:
$$
\mathbf{x_i} = \mathbf{E}w_i \in R^d
$$
接下来给定三个矩阵(qkv),分别是:
$$
Q,K,V \in R^{d \times d}
$$
并记:
$$
q_i = Qx_i \\
k_i = Kx_i \\
v_i = Vx_i
$$
计算每一个token对之间的相似度并用softmax函数归一化:
$$
e_{ij} = q_i^Tk_j\\
\alpha_{ij} = \frac{exp(e_{ij})}{\sum_{j'}^{}exp(e_{ij'})}
$$
最后加入v矩阵计算权重
$$
o_i = \sum_{j}^{}\alpha_{ij}v_j
$$
至此我们得到了原始词$w_i$的表达向量,即$o_i \in R^{d}$
位置编码
只有注意力机制不够,因为attention只描述了token之间的关系和联系,但是具体在一个文本当中,词的位置也是至关重要的,所以在最终的词嵌入时,还需要加入位置编码
正余弦位置编码:对于奇偶数的位置分别使用正弦和余弦来编码:
$$
PE(pos,2i) = sin(\frac{pos}{10000^{\frac{2i}{d}}})\\
PE(pos,2i+1) = cos(\frac{pos}{10000^{\frac{2i}{d}}})
$$
非线性层
实际上到现在为止,self-attention还是局限于一个加权平均的过程,并没有所谓的非线性,这一步我们来解决这个问题:
加前馈神经网络FFN,表达式为:
$$
m_i = MLP(o_i) = \mathbf{W_2} ReLU(\mathbf{W_1}o_i + b_1) + b_2
$$
就是线性层+激活函数+线性层,这是一个很经典的设计
掩码
我们还需要考虑在实际情况中,预测第$t+1$个token的时候,只有前$t$个token是可见的,而后面的所有token都看不到,这就需要掩码遮住后面的token,也就是在注意力中让后面的项消失权重
还记得在self-attention中计算token对相似度这一步吗,在这里我们加入掩码,也就是对于$i$之后的所有token的相似度都变成$- \infty$:
$$
e_{ij} =
\begin{cases}
q_i^Tk_j ,& j \le i \\
-\infty , & j > i
\end{cases}
$$
总结一下,到这里讲的所有内容可以汇总成一张图片
矩阵表达的self-attention
我们接下来看矩阵形式的自注意力计算:
令$\mathbf{X} = [x_1;x_2;...;x_n] \in R^{n \times d}$
注意到$XQ,XK,XV \in R^{n \times d}$
最终输出的结果的表达式为:
$$
output = softmax(XQ(XK)^T)XV \in R^{n \times d}
$$
多头注意力
在语义上,我们希望注意力能够注意到很多维度的信息,而不仅仅是笼统的用一个”大的”注意力一概表示,于是引入了多头注意力的方法:
实际上,与self-attention不同之处仅仅是$QKV$矩阵被分成了$h$个头:
$$
Q,K,V \in R^{d \times d} \to Q_l,K_l,K_l \in R^{d \times \frac{d}{h}}
$$
这里需要联想到矩阵乘法的一个性质:
这个性质用矩阵计算很好推,只需要想着有这么个东西就行
相应的,输出的向量变成了:
$$
output = [o_1,o_2,...,o_h]\\
o_i = softmax(XQ_l(XK_l)^T)XV_l\\
o_i \in R^{n \times \frac{d}{h}}
$$
计算性质
多头注意力引入后,我们要采取一种transpose+reshape的方法来优化计算:
$$
reshape : XQ,XK,XV \in R^{n \times d} \to R^{n \times h \times \frac{d}{h}}\\
transpose:R^{n \times h \times \frac{d}{h}} \to R^{h \times n \times \frac{d}{h}}
$$
剩下的参考这张图就行,想的时候就分开想$(XQ)_i,(XK)_i,(XV)_i$的维度和矩阵乘法就行
点积约束
首先回顾attention的公式:
$$
o_i = softmax(XQ_i(XK_i)^T)XV_i
$$
对于softmax这个函数,有这样一个性质,首先假设一组分数分布是$softmax[x_1,x_2,...,x_n]$,如果同时给序列中的数放大$k$倍($k>1$),这个分布会趋于锐化,因为softmax会把分布中的值送到指数运算中,造成了模型倾向生成的token的概率非常大,甚至会接近one-hot的情况,这不是我们希望的,所以在softmax中加入约束项:
$$
o_i = softmax(\frac{XQ_i(XK_i)^T}{\sqrt{d_k}})XV_i
$$
我们记这里$d_k = \frac{d}{h}$,这样就会让这个分布变得稳定
残差连接
一个普通的情况是:x经过某一层,学得一个layer(x),即:
$$
x \to layer \to layer(x)
$$
残差连接的思想是只学x经过这一层的时候学到了什么,就是变化量:
$$
x \to layer \to x + layer(x)
$$
这样的好处是在回传的时候,由于有一个恒等的部分对应梯度是1,这样比较好学,即使这一层学不到什么东西,由于只是差值,所以影响不是很大
Layer normalization
加速模型训练的方法
下面给出归一化的流程,给定$x \in R^d$,有:
$$
\mu = \frac{1}{d} \sum_{i=1}^{d}x_i \\
\sigma = \frac{1}{d} \sum_{i=1}^{d} (x_i - \mu)^2
$$
然后定义两个可学习的参数$\beta , \gamma$,后续用于训练
$$
output = \gamma \times \frac{x - \mu}{\sqrt{\sigma + \epsilon}} + \beta
$$
作业实现记录(codes only)
hw3 : Transformer
详细的
MLP forward
简单的多层感知机模型,两个线性层中间套一个激活函数就行
def forward(self,x):
x = self.fc1(x)
x = self.gelu(x)
x = self.fc2(x)
return x
CausalAttention forward
实现一个最基本的多头注意力机制,写这个东西的时候注意从输入到输出的维度没有变化,只是带着的语义信息变了
整个流程如下:先计算出q / k / v矩阵,然后缩放加掩码mask,过softmax函数算分,然后乘v矩阵
def forward(self,x);
B,T,C = x.size()
q = self.w_q(x).reshape(B,T,self.n_heads,self.d_attention).transpose(1,2)
k = self.w_k(x).reshape(B,T,self.n_heads,self.d_attention).transpose(1,2)
v = self.w_v(x).reshape(B,T,self.n_heads,self.d_attention).transpoes(1,2)
logits = q @ k.transpose(-2,-1) / math.sqrt(self.d_attention)
mask = self.causal_mask[:, :, :T, :T]
scores = logits.masked_fill(mask == 0, float("-inf"))
scores = torch.softmax(scores,dim = -1)
output = scores @ v
output = output.transpose(1,2).reshape(B,T,C)
output = self.w_o(output)
return output
注意一些细节上的东西,开多头之后相当于每个头都有T个token,然后每个token被简化到了d_attention维,所以过完注意力层之后需要转换维度,也就是transpose()函数,发生的具体变化是:(B,T,C) -> (B,T,nh,nd) -> (B,nh,T,nd),再矩阵相乘(先对齐维度)得到T个token之间的关联度矩阵,过掩码后一起softmax得到注意力分数,乘上v矩阵完成维度还原,最后一个线性层收尾
$$
output = Linear(softmax(\frac{QK^T}{\sqrt{d_{attention}}} + mask)V)
$$
至此我们完成了最重要的注意力部分,后面要把它作为module去拼接到网络中去
Decoder Block
非常容易的一步,只需要做pre-LN和拼接模块就行
def forward(self,x):
x = x + self.attention(self.pre_layer_norm(x))
x = x + self.mlp(self.post_layer_norm(x))
return x
注意现在的架构中99.9%采用pre-LN的方式,这样即使学不到什么东西也可以进行梯度回传,保留大部分之前的内容
Transformer
ok至此我们写完了所有的组件了,开始拼接
回顾transformer架构,一段文本是怎么变成一个具有语义信息的token序列的呢?
传入的x是tokenizer后的向量,shape是(B,T),然后做词嵌入和位置编码,接着进入n个decoder block中学习潜在的语义关系,最后经由一个归一化层和一个线性层得到一个序列,这个序列包含的就是输入的序列中的T个token和词表vocab中的关联性分数,后面用于生成
def forward(self,x):
B,T = x.shape()
pos = torch.arange(T,device = x.device)
input = self.embeddings(x) + self.position_embedding(pos)
for block in elf.backbone:
input = block(input)
logits = self.final_layer_norm(input)
logits = self.lm_head(logits)
return logits
generate
解决了网络架构的问题之后,我们来看next-token-predict的时候是怎么做的
经过transformer网络得到的是一个shape为(B,T,vocab_size)的tensor,这代表了每一个seq中每一处token和整个词表中的关联的token之间的关联度,既然我们要做的是next-token predict,那么只需要取出整个seq中的最后一个token对应的词表的打分,然后argmax找下表就完事了
@torch.no_grad()
def generate(
self,
x: Int[Tensor, "batch_size seq_len"],
num_new_tokens: int,
) -> Int[Tensor, "batch_size seq_len+num_new_tokens"]:
# TODO, complete
for _ in range(num_new_tokens):
logits = self(x)
logits = logits[:,-1,:]
next_token= torch.argmax(logits,dim = -1, keepdim = True)
x = torch.cat([x,next_token] , dim = -1)
return x
get_batch_loss
这个是train loop中的一个关键点,如何将交叉熵作为损失函数实现出来,逻辑就是先取出前T-1个token然后送入网络预测词表,然后再取出后T-1个token作为答案算交叉熵
def get_loss_on_batch(
self,
input_ids: Int[Tensor, "batch_size seq_len"],
) -> Float[Tensor, ""]:
B , T = input_ids.shape()
inputs = input_ids[: , : -1]
logits = self(inputs)
target = input_ids[: , 1 : ]
logits = logits.reshape(-1,self.vocab_size)
targets = targets.reshape(-1)
return F.cross_entropy(logits,targets)
在实现过程中有一些小的细节,就是这个cross_entropy函数要求的是一个一维的tensor和一个二维的tensor进入计算,所以我们需要展平(也就是代码中的reshape(-1)自动推导出第一维)
至此,我们完成了a3的全部代码!彻底理解了tarnsformer的运作流程!
a4由于需要花自己的钱买API KEY,故直接放弃不做了…