《大规模语言模型理论与实践》读后感

绫波波 发布于 2025-07-20 922 次阅读


引言

语言模型的发展历程

  • 统计语言模型(Statistical Language Model,SLM)。统计语言模型使用马尔可夫假设来建立语言序列的预测模型,通常根据词序列中若干个连续的上下文单词来预测下一个词的出现概率,即根据一个固定长度的前缀来预测目标单词。具有固定上下文长度n的统计语言模型通常被称为n元(n-gram)语言模型

大语言模型基础

Transformer结构

Transformer结构是谷歌在2017年提出并首先因果干预机器翻译的神经网络模型架构。机器翻译的目标是从源语言(Source Language)转换到目标语言(Target Language)。Transformer结构完全通过注意力机制完成对源语言序列和目标语言序列全局依赖的建模。
基于Transformer的编码器和解码器结构如下图所示,左侧和右侧分别对应着编码器(Encoder)和解码器(Decoder)结构,他们均由若干个基本的Transformer块(Block)组成。其中表示进行了N次堆叠。每个Transformer块都接收一个向量序列\{x_i\}_{i=1}^{t},并输出一个等长的向量序列作为输出\{y_i\}_{i=1}^{t}。这里的x_iy_i分别对应文本序列中的一个词元(Token)的表示。y_i是当前Transformer块对输入x_i进一步整合其上下文语义后对应的输出。在从输入\{x_i\}_{i=1}^{t}到输出\{y_i\}_{i=1}^{t}的语义抽象过程中,主要涉及如下几个模块:

  • 注意力层:使用多头注意力(Multi-Head Attention)机制整合上下文语义。多头注意力并行运行多个独立注意力机制,进而从多维度捕捉序列信息。他使得序列中任意两个单词之间的依赖关系可以直接被建模而不基于传统的循环结构,从而更好地解决文本的长程依赖问题。
  • 位置感知前馈网络层(Position-wise Feed-Forward Network):通过全连接层对输入文本序列中的每个单词表示进行更复杂的变换。
  • 残差连接:对应图中的Add部分。它是一条分别作用在上述两个子层中的直连通路,被用于连接两个子层的输入和输出,使信息流动更高效,有利于模型的优化。
  • 层归一化:对应图中的Norm部分。它作用于上述两个子层的输出表示序列,对表示序列进行层归一化操作,同样起到稳定优化的作用。

嵌入表示层

对于输入的文本序列,首先通过输入嵌入层(Input Embedding)将每个单词转换为其相对应的向量表示。通常,直接对每一个单词创建一个向量表示。在送入编码器创建上下文语义之前,需要在词嵌入中加入位置编码(Positional Encoding)特征,即序列中的每一个词所在的位置都对应一个向量。这个向量会和单词表示对应相加并送入后续模块中做进一步处理。在训练过程中,模型会自动学习如何利用位置信息。
Transformer结构使用不同频率的正余弦函数来表示不同位置所对应的编码信息:

其中,pos表示单词所在的位置,2i2i+1表示位置编码向量中的对应维度,d则对应位置编码的总维度。通过这种方式计算位置编码的好处有:(1)正余弦函数的范围是[-1,+1],导出的位置编码与原有词向量相加不会使结果偏离过远而破坏原有单词的语义信息;(2)根据三角函数的基本性质,可以得知第pos+k个位置编码是第pos个位置编码的线性组合,这意味着位置编码中蕴含着单词之间的距离信息。
可以使用pytorch完成位置编码:

class PositionalEncoder(nn.Module):
    def __init__(self, d_model, max_seq_len = 80):
        super().__init__()
        self.d_model = d_model
        # 根据pos和i创建一个常量PE矩阵
        pe = torch.zeros(max_seq_len, d_model)
        for pos in range(max_seq_len):
            for i in range(0, d_model, 2):
                pe[pos, i] = math.sin(pos / (10000 ** (i/d_model)))
                pe[pos, i + 1] = math.cos(pos / (10000 ** (i/d_model)))
        pe = pe.unsqueeze(0)
        self.register_buffer('pe', pe)

        def forward(self, x):
        # 使得单词嵌入表示相对大一些
        x = x * math.sqrt(self.d_model)
        # 增加位置常量到单词嵌入表示中
        seq_len = x.size(1)
        x = x + Variable(self.pe[:,:seq_len], requires_grad=False).cuda()
        return x
注意力层

自注意力(Self-Attention)操作是基于Transformer的机器翻译模型的基本操作,在源语言的编码和目标语言的生成中频繁的被使用,以建模源语言、目标语言任意两个单词之间的依赖关系。将由单词语义嵌入及其位置编码叠加得到输入表示为\{x_i \in \mathbb{R}^{d}\}_{i=1}^{L},为了实现对上下文语义依赖的建模,引入自注意力机制涉及的三个元素:查询q_i(Query)、键k_i(Key)和值v_i(Value)。在编码输入序列的每一个单词的表示中,这三个元素用于计算上下文单词对应的权重得分。直观地说,这些权重反映了在编码当前单词的表示时,对于上下文不同部分所需要的关注程度。具体来说,如下图所示,通过三个线性变化\mathbf{W^{Q}} \in \mathbb{R}^{d×d_q} \mathbf{W^{K}} \in \mathbb{R}^{d×d_k},\mathbf{W^{V}} \in \mathbb{R}^{d×d_v},将输入序列中的每一个单词x_i转换为其对应的q_i \in \mathbb{R^{d_q}},k_i \in \mathbb{R^{d_k}},v_i \in \mathbb{R^{d_v}}向量。对于输入\{x_i \in \mathbb{R^{d}}\}^{L}_{i=1},\mathbf{Q}\mathbf{K}\mathbf{V}矩阵可以通过如下公式所示:


为了得到编码单词x_i时所需要关注的上下文信息,通过位置i查询向量与其他位置的键向量做点积得到匹配分数q_i \cdot k_1,q_i \cdot k_2,\dots, q_i \cdot k_t。为了防止过大的匹配分数在后续Softmax计算过程中导致梯度爆炸及收敛效率差的问题,这些得分会除以缩放因子\sqrt{d}以稳定优化。缩放后的得分经过Softmax归一化概率,与其他位置的值向量相乘来聚合希望关注的上下文信息,并最小化不相关信息的干扰。上述计算过程可以被形式化的表示为:

其中,\mathbf{Q} \in \mathbb{R}^{L×d_q},\mathbf{K} \in \mathbb{R}^{L×d_k},\mathbf{V} \in \mathbb{R}^{L×d_v}分别表示输入序列中不同单词的q,k,v向量拼接组成的矩阵,L表示序列长度,\mathbf{Z} \in \mathbb{R}^{L×d_v}表示自注意力操作的输出。为了进一步增强自注意力机制聚合上下文信息的能力,提出了多头注意力机制,以关注上下文的不同侧面。具体来说,上下文中的每一个单词表示x_i经过多组线性\{\mathbf{W^{Q}_{j}},\mathbf{W^{K}_{j}},\mathbf{W^{V}_{j}}\}^{N}_{j=1}映射到不同的表示子空间中,下述公式会在不同的子空间分别计算得到不同的上下文相关的单词序列表示\{\mathbf{Z}_j\}_{j=1}^{N}:

在此基础上,经过线性变化\mathbf{W^{O}} \in \mathbb{R}^{(Nd_v)×d}用于综合不同子空间中的上下文表示并形成注意力层最终的输出\{x_i \in \mathbb{R^{d}}\}^{L}_{i=1},可以得到多头自注意力(Multi-Head Self-Attention)表示:

由此可见,自注意力机制使模型能够识别不同输入部分的重要性,从而不受距离的影响,从而能够捕捉输入句子中的长距离依赖关系和复杂关系。
使用Pytorch实现的自注意力层参考代码如下:

import math

class MultiHeadAttention(nn.Moudle):
    def __init__(self, heads, d_model, dropout=0.1):
        super().__init__()

        self.d_model = d_model
        self.d_k = d_model // heads
        self.h = heads

        # 简历Q、K、V矩阵
        self.q_linear = nn.Linear(d_model, d_model)
        self.k_linear = nn.Linear(d_model, d_model)
        self.v_linear = nn.Linear(d_model, d_model)
        self.dropout = nn.Dropout(dropout)
        self.out = nn.Linear(d_model, d_model)

    def attention(q, k, v, d_k, mask=None, dropout=None):
        scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k)

        # 掩盖那些为了补全长度而增加的单元,使其通过Softmax计算后为0
        if mask is not None:
            mask = mask.unsqueeze(1)
            scores = scores.masked_fill(mask == 0, -1e9)

        scores = F.softmax(scores, dim=-1)

        # 对注意力权重做剪枝,随机把某些注意力条目设置为0,防止co-adaptation
        if dropout is not None:
            scores = dropout(scores)

        output = torch.matmul(scores, v)
        return output

    def forward(self,q,k,v,mask=None):
        bs = q.size(0)

        # 进行线性操作,划分为h个头
        k = self.k_linear(k).view(bs,-1,self.h,self.d_k)
        q = self.q_linear(q).view(bs,-1,self.h,self.d_k)
        v = self.v_linear(v).view(bs,-1,self.h,self.d_k)

        # 矩阵转置
        k = k.transpose(1,2)
        q = q.transpose(1,2)
        v = v.transpose(1,2)

        # 计算 attention
        scores = self.attention(q,k,v,self.d_k,mask,self.dropout)

        # 连接多个头并输入到最后的线性层
        concat = scores.transpose(1,2).contiguous().view(bs,-1,self.d_model)

        output = self.out(concat)

        return output
前馈层

前馈层接收自注意子层的输出作为输入,并通过一个带有ReLU激活函数的两层全连接网络对输入进行更复杂的非线性变换。实验证明,这一非线性变换会对模型最终的性能产生重要的影响。

其中,\mathbf{W}_1,\mathbf{b}_1,\mathbf{W}_2,\mathbf{b}_2表示前馈子层的参数。实验结果表明,增大前馈子层隐状态的维度有利于提高其最终的翻译结果质量。因此,前馈子层隐状态的维度一般比自注意力子层要大。
使用Pytorch实现的前馈层参考代码如下:

class FeedForward(nn.Module):
    def __init__(self, d_model, d_ff=2048, dropout = 0.1):
        super().__init__()
        # d_ff默认设置为2048
        self.linear_1 = nn.Linear(d_model, d_ff)
        self.dropout = nn.Dropout(dropout)
        self.linear_2 = nn.Linear(d_ff, d_model)

    def forward(self, x):
        x = self.dropout(F.relu(self.linear_1(x)))
        x = self.linear_2(x)
        return x
残差连接与层归一化

由Transformer结构组成的网络结构通常都非常庞大。编码器和解码器均由很多层基本的Transformer块组成,每一层中都包含复杂的非线性映射,这就导致模型训练比较困难。因此,研究人员在Transformer块中进一步引入了残差链接和层归一化技术,以进一步提升训练的稳定性。具体来说,残差连接主要是指使用一条直连通道直接对对应子层的输入连接到输出,避免在优化过程中因网络过深而产生潜在的梯度消失问题:
$$x^{l+1}=f(x^{l})+x^{l}$$
其中,x^l表示第l层的输入,f(\cdot)表示一个映射函数。此外,为了使每一层的输入/输出稳定在一个合理的范围内,层归一化技术被进一步引入每个Transformer块中:
$$LN(x)=\alpha \cdot \frac{x-\mu}{\sigma}+\beta$$
其中\mu\sigma分别表示均值和方差,用于将数据平移缩放到均值为0,方差为1的标准分布,\alpha\beta是可学习的参数。层归一化技术可以有效地缓解优化过程中潜在的不稳定、收敛速度慢等问题。
使用Pytorch实现层归一化参考代码如下:

class Norm(nn.Module):
    def __init__(self, d_model, eps = 1e-6):
        super().__init__()

        self.size = d_model

        # 层归一化包含两个可以学习的参数
        self.alpha = nn.Parameter(torch.ones(self.size))
        self.bias = nn.Parameter(torch.zeros(self.size))

        self.eps = eps

    def forward(self, x):
        norm = self.alpha * (x - x.mean(dim=-1, keepdim=True))
        / (x.std(dim=-1, keepdim=True) + self.eps) + self.bias
        return norm
编码器和解码器结构

基于上述模块,根据上图中国女给出的网络架构,编码器端较容易实现。相比于编码器端,解码器端更复杂。具体来说,解码器的每个Transformer块的第一个自注意力子层额外增加了注意力掩码,对应图中的掩码多头注意力(Masked Multi-Head Attention)部分。这主要是因为在翻译的过程中,编码器端主要用于编码源语言序列的信息,而这个序列是完全已知的,因而编码器仅需要考虑如何融合上下文语义信息。解码器端则负责生成目标语言序列,这一生成过程是自回归的,即对于每一个单词的生成过程,仅有当前单词之前的目标语言序列是可以被观测的,因此这一额外增加的掩码是用来掩盖后续的文本信息的,以防模型在训练阶段直接看到后续的文本序列,进而无法得到有效的训练。
此外,解码器端额外增加了一个多头交叉注意力(Multi-Head Cross-Attention)模块,使用交叉注意力(Cross-Attention)方法,同时接收来自编码器端的输出和当前 Transformer 块的前一个掩码注意力层的输出。查询是通过解码器前一层的输出进行投影的,而键和值是使用编码器的输出进行投影的。它的作用是在翻译的过程中,为了生成合理的目标语言序列,观测待翻译的源语言序列是什么。基于上述编码器和解码器结构,待翻译的源语言文本经过编码器端的每个 Transformer块对其上下文语义进行层层抽象,最终输出每一个源语言单词上下文相关的表示。解码器端以自回归的方式生成目标语言文本,即在每个时间步t,根据编码器端输出的源语言文本表示,以及前t-1个时刻生成的目标语言文本,生成当前时刻的目标语言单词。

知识补充

Dropout

dropout指在训练时随机切除一定比例的神经元,让它们的这次前向传播不起作用,从而逼网络不要过拟合,提高模型的泛化能力。在测试验证时再全部恢复。
使用Dropout的理由如下:

  • 解决过拟合问题:当模型在训练集上表现很好,但是在测试集上表现很差,这说明模型记住了训练数据的噪声,而非学习通用模式。
  • 神经元协同适应(Co-adaptation):传统神经网络中,神经元可能过度依赖其他特定的神经元,导致模型脆弱。Dropout通过随机丢弃神经元,打破这种依赖,迫使每个神经元独立学习有用的特征。
ReLU激活函数

ReLU激活函数的表达式为:f(x)=\max(0,x)。ReLU函数的图形形状呈现为分段线性函数,在输入为负数时输出为0,输入为正数时输出与输入成正比(即y=x

使用ReLU的优势包括:

  • 简洁且高效的计算:ReLU函数的计算方式简单直接,无需复杂的指数运算,相比于Sigmiod或者Tanh等激活函数,ReLU的计算速度更快。
  • 解决梯度消失问题:在传统的Sigmoid或者Tanh激活函数中,当输入值非常大或者非常小时,导数(梯度)变得非常小。这种现象称之为梯度消失,他会使得反向传播时的梯度在传递过程中逐层衰减,导致网络训练困难,甚至无法更新参数。而ReLU的导数在正区间为常数1,负区间为0,几乎不受到输入值大小的影响。
  • 非线性特性:ReLU函数通过截断负值区域引入了非线性特性。
  • 避免饱和问题: Sigmoid和Tanh等激活函数容易出现饱和现象,尤其是在输入值很大或很小时,函数的导数会趋近于0,从而导致梯度消失。而ReLU在正区间内没有饱和问题,输出随输入增大而线性增加。
Sigmoid激活函数

Sigmoid函数的表达式为:f(z) = /frac{1}{(1+e^{-z})}

Tanh/双曲正切激活函数

Tanh是一个双曲正切函数。Tanh和sigmoid函数的曲线相对相似。但是它比sigmoid函数更有优势:

  • 当输入较大或较小时,输出几乎是平滑的且梯度较小,这不利于权重更新。二者的区别在于输出间隔,tanh的输出间隔为1,并且整个函数以0为中心,比sigmoid函数更好。
  • 在tanh图中,负输入将被强映射为负,且零输入被映射为接近零。

注意,在一般的二元分类问题中,tanh函数用于隐藏层,而sigmoid函数用于输出层。但这并不是固定的,需要根据特定问题进行调整。


Softmax激活函数

Softmax是用于多类分类问题的激活函数。在多分类问题中,超过两个类标签则需要类成员关系。对于长度为K的任意实向量,Softmax可以将其压缩为长度为K,值在(0,1)范围内,并且向量中元素的总和为1的实向量。

Talk is cheap, show me the code.
最后更新于 2025-07-21