《大语言模型开发:用开源模型开发本地系统》阅读报告(1)
<div class='showpostmsg'> 本帖最后由 Aclicee 于 2024-12-7 12:28 编辑<p> 我们于在11月24日收到的《大语言模型开发:用开源模型开发本地系统》一书,书籍包装完整,印刷清晰,装帧精美,给人以良好的第一印象。</p>
<div style="text-align: center;"></div>
<p> 按照既定计划,我们今天向各位老师汇报对书本第一部分的阅读报告,这部分内容聚焦于Transformer模型的详细构成。由于时间限制,我们尚未将Transformer模型应用于具体任务的操作实践,敬请期待我们在后续报告中对实际操作的深入探讨。</p>
<p> 在深入剖析Transformer模型之前,本书对Pytorch和深度学习的基础方法进行了全面的介绍。鉴于我们在之前的《人工智能实践教程》测评中已经详细汇报过这部分内容,并且论坛中已有众多专家老师的宝贵分享,我们将不再对此进行重复讨论。</p>
<p> Transformer架构以其独特的设计在自然语言处理领域占据着举足轻重的地位。一个典型的Transformer模型由以下几个核心部分组成:自注意力机制、多头自注意力、位置编码、前馈神经网络、归一化层以及残差连接。接下来的报告中,我们将按照Transformer的工作流程,逐一深入学习这些组成部分。</p>
<div style="text-align: center;"></div>
<p> 在这些组件中,<strong><span style="color:#e74c3c;">位置编码</span></strong>和<strong><span style="color:#e74c3c;">注意力机制</span></strong>尤为关键。位置编码的引入是为了使Transformer能够理解输入序列中各元素之间的前后关系,而注意力机制则赋予模型聚焦于更为重要的信息维度的能力,从而深刻理解输入数据的相关性和影响。这两个模块是Transformer模型能够准确捕捉序列特征并进行有效预测的关键。</p>
<p><u><strong>1. 词嵌入</strong></u></p>
<p> 词嵌入是自然语言处理领域中的一项关键技术,它通过无监督学习算法从大规模文本数据中提取词汇的向量表示。这种表示能够捕捉词汇之间的语义和句法关系,为后续的模型训练和语言理解任务提供基础。</p>
<p> 在Transformer模型中,词嵌入是与模型参数一同训练得到的,这样可以在特定任务的上下文中优化词嵌入,以更好地适应任务需求。</p>
<p> Transformer模型的词嵌入由两部分组成:<strong><span style="color:#e74c3c;">标记嵌入(Token Embeddings)</span></strong>和<strong><span style="color:#e74c3c;">位置编码(Positional Encoding)</span></strong>。</p>
<ul>
<li>
<p><b>标记嵌入</b>:这部分将输入序列中的每个标记(token)映射到一个高维空间中的向量。通过这种方式,语义上相似的标记在向量空间中的距离更近,从而反映出它们之间的相似性。</p>
</li>
<li>
<p><b>位置编码</b>:由于Transformer模型本身不具备处理序列顺序的能力,位置编码被引入以赋予模型对输入序列中标记位置的感知能力。这使得模型能够区分相同标记在不同位置时的上下文含义。</p>
</li>
</ul>
<p> Transformer模型中的位置编码通常采用正弦和余弦函数的组合来实现。这种方法利用了三角函数的周期性特性,为每个位置生成唯一的编码,从而蕴含位置信息。具体公式如下:</p>
<p style="text-align: center;"><img alt="PE_{pos,2i} = sin(\frac{pos}{10000^{\frac{2i}{d_{model}}}})" src="https://bbs.eeworld.com.cn/gif.latex?PE_%7Bpos%2C2i%7D%20%3D%20sin%28%5Cfrac%7Bpos%7D%7B10000%5E%7B%5Cfrac%7B2i%7D%7Bd_%7Bmodel%7D%7D%7D%7D%29" /></p>
<p style="text-align: center;"><img alt="PE_{pos,2i+1} = cos(\frac{pos}{10000^{\frac{2i}{d_{model}}}})" src="https://bbs.eeworld.com.cn/gif.latex?PE_%7Bpos%2C2i&plus;1%7D%20%3D%20cos%28%5Cfrac%7Bpos%7D%7B10000%5E%7B%5Cfrac%7B2i%7D%7Bd_%7Bmodel%7D%7D%7D%7D%29" /></p>
<p> 其中,<span style="color:#e74c3c;"><strong><em>pos</em></strong></span>表示位置, <span style="color:#e74c3c;"><strong><em>i</em></strong></span>表示维度,<strong><em><span style="color:#e74c3c;">d_model</span></em></strong>表示模型的维度。这种编码方式确保了不同位置的编码向量具有不同的值,且随着位置的变化,编码向量呈现出周期性变化。</p>
<div style="text-align: center;"></div>
<p> 以下是实现正弦位置编码的参考代码:</p>
<pre>
<code>import torch
import torch.nn as nn
def precompute_freq_cis(dim, seqlen, theta=10000.0):
freqs = 1.0 / (theta**(torch.arange(0, dim, 2)[:(dim//2)].float() / dim))
t = torch.arange(seqlen)
freqs = torch.outer(t, freqs).float()
return freqs
embedding_dim = 8
sequence_length = 5
embeddings = torch.randn(sequence_length * embedding_dim).view(sequence_length, embedding_dim)
freqs = precompute_freq_cis(embedding_dim, sequence_length)
pe = torch.zeros((sequence_length, embedding_dim))
pe[:,0::2] = torch.sin(freqs)
pe[:,1::2] = torch.cos(freqs)
pe_out = token_embedding + pe</code></pre>
<p> 这段代码首先创建了一个位置编码矩阵,然后通过正弦和余弦函数为每个位置生成了一个唯一的编码。这样,即使是相同的词,在句子中不同位置的编码也会有所不同,从而帮助模型理解词序的重要性。</p>
<p><u><strong>2. 自注意力机制</strong></u></p>
<p> 自注意力机制(Self-Attention Mechanism),也称为内部注意力机制,是一种允许输入序列中的每个元素根据其与其他元素的相似度动态分配权重的技术。这种机制对于处理长序列数据尤为重要,因为它能够捕捉序列内部的长距离依赖关系,同时提高了模型的计算效率和可解释性。</p>
<div style="text-align: center;"></div>
<p data-pm-slice="1 1 []"> 自注意力机制的计算过程涉及以下步骤:</p>
<ul>
<li blockid="eYWVSecK9HJ">
<p marginbottom="0px" marginleft="0px" margintop="0px"><b>映射到查询(Q)、键(K)和值(V)向量</b>:输入序列首先被映射到查询向量(Q)、键向量(K)和值向量(V)。这一步骤通常通过线性变换实现,其中权重是可学习的。</p>
</li>
<li blockid="Yz6jSKGQVSD">
<p marginbottom="0px" marginleft="0px" margintop="0px"><b>计算注意力分数</b>:通过计算Q和K的点积来衡量序列中各元素之间的相似度,得到一个原始的注意力分数矩阵。</p>
</li>
<li blockid="YRCPRWDht9H">
<p marginbottom="0px" marginleft="0px" margintop="0px"><b>Softmax归一化</b>:对原始注意力分数应用Softmax函数进行归一化,使得每一行的和为1。这一步骤确保了模型在计算加权平均时,能够根据元素间的相似度合理分配权重。</p>
</li>
<li blockid="JUDwTKJ1j1J">
<p marginbottom="0px" marginleft="0px" margintop="0px"><b>加权平均</b>:将归一化后的注意力分数与值向量V相乘,得到加权平均的结果。这一步骤反映了每个输入元素根据其与其他元素的相似度关系,调整其嵌入表示。</p>
</li>
</ul>
<p> 公式可以表示为:</p>
<p style="text-align: center;"><img alt="Attention(Q,K,V) = Softmax(\frac{QK^T}{\sqrt{d_k}})V" src="https://bbs.eeworld.com.cn/gif.latex?Attention%28Q%2CK%2CV%29%20%3D%20Softmax%28%5Cfrac%7BQK%5ET%7D%7B%5Csqrt%7Bd_k%7D%7D%29V" /></p>
<p> 其中d_k表示值向量的维度。以下是自注意力机制的初步实现代码:</p>
<pre>
<code>query_matrix = nn.Linear(embedding_dim, embedding_dim)
key_matrix = nn.Linear(embedding_dim, embedding_dim)
value_matrix = nn.Linear(embedding_dim, embedding_dim)
query_vectors = query_matrix(embeddings)
key_vectors = key_matrix(embeddings)
value_vectors = value_matrix(embeddings)
scores = torch.matmul(query_vectors, key_vectors.transpose(-2,-1)) / torch.sqrt(torch.tensor(embedding_dim, dtype=torch.float32))
softmax = nn.Softmax(dim=-1)
attention_weights = softmax(scores)
output = torch.matmul(attention_weights, value_vectors)</code></pre>
<p> 在实际应用中,Transformer模型通常采用<strong><span style="color:#e74c3c;">多头自注意力机制</span></strong>,即同一输入序列被分割成多个Q、K、V的组合,每个“头”独立计算注意力,最后将所有头的输出拼接起来。这种设计增强了模型对不同子空间信息的捕捉能力。</p>
<pre>
<code>num_attention_heads = 2
output_copy = output.clone()
m_output = torch.concat((output, output_copy), dim=1)
output_matrix = nn.Linear(num_attention_heads*embedding_dim, num_attention_heads*embedding_dim)
out_vectors = output_matrix(m_output)
print(embeddings)
print(out_vectors)</code></pre>
<p> 为了加速自注意力机制的计算过程,可以采用<strong><span style="color:#e74c3c;">ColumnParallelLinear</span></strong>和<strong><span style="color:#e74c3c;">RowParallelLinear</span></strong>等方法进行并行运算。这些技术通过将矩阵分解为多个较小的子矩阵,利用现代硬件的并行处理能力,从而提高计算效率。</p>
<p><u><strong>3. 残差连接和归一化</strong></u></p>
<p> 在深度学习中,随着网络层数的增加,梯度消失或梯度爆炸的问题会导致训练深层网络变得困难。残差连接(Residual Connection)和层归一化(Layer Normalization)是解决这些问题的两种有效机制,它们在Transformer模型中发挥着重要作用。</p>
<p> 残差连接最初在ResNet中提出,用于缓解深层网络训练中的梯度消失问题。其核心思想是将输入直接添加到网络中某一层的输出上,这样网络就可以学习到恒等映射(Identity Mapping),从而使得深层网络的训练变得更加容易。在Transformer中,残差连接用于将多头自注意力层或前馈网络层的输入与输出相加,然后再进行后续的层归一化和下一层的计算。</p>
<div style="text-align: center;"></div>
<p> 公式表示为:</p>
<p style="text-align: center;"><img alt="output = input +FeedForward(input)" src="https://bbs.eeworld.com.cn/gif.latex?output%20%3D%20input%20&plus;FeedForward%28input%29" /></p>
<p> 其中,<strong><span style="color:#e74c3c;">FeedForward(X)</span></strong>表示经过一层或多层网络的变换。</p>
<p> 层归一化(Layer Normalization)是一种归一化技术,它对每个样本的每个层的输出进行归一化处理,使得输出的分布具有稳定的均值和方差。与批量归一化(Batch Normalization)不同,层归一化是在单个样本的<strong><span style="color:#e74c3c;">层级上</span></strong>进行归一化,而不是在整个批次上。这使得层归一化对于小批量大小或非独立同分布的数据更加鲁棒。</p>
<p> 层归一化的计算公式为:</p>
<p style="text-align: center;"><img alt="LN(x_i) = \gamma(\frac{x_i-\mu_i}{\sqrt{\sigma_i^2}+\epsilon}) + \beta" src="https://bbs.eeworld.com.cn/gif.latex?LN%28x_i%29%20%3D%20%5Cgamma%28%5Cfrac%7Bx_i-%5Cmu_i%7D%7B%5Csqrt%7B%5Csigma_i%5E2%7D&plus;%5Cepsilon%7D%29%20&plus;%20%5Cbeta" /></p>
<p> 其中,<strong><span style="color:#e74c3c;">x_i</span></strong>是第个i样本的输出,<span style="color:#e74c3c;"><strong>μ_i</strong></span>和<strong><span style="color:#e74c3c;">σ_i</span></strong>分别是该样本输出的均值和方差,<strong><span style="color:#e74c3c;">ε</span></strong>是一个很小的常数,防止分母为零,<strong><span style="color:#e74c3c;">γ</span></strong>和<strong><span style="color:#e74c3c;">β</span></strong>是可学习的参数,用于对归一化后的输出进行缩放和平移。</p>
<p> 在Transformer中,层归一化通常在残差连接之后进行,用于减少层与层之间数据分布的差异,加速训练过程,并保持数据的分布稳定性。</p>
<p><u><strong>4. 前馈网络</strong></u></p>
<p> 前馈网络(Feed-Forward Network,FFN)是Transformer模型中的关键组件之一,主要用于对自注意力模块的输出进行深度特征提取和非线性变换。这一步骤对于模型学习和表示复杂的语义信息至关重要,能够有效提升模型的性能。</p>
<p> 在Transformer中,每个前馈网络由两个线性变换组成,中间夹着一个ReLU激活函数。具体来说,前馈网络的结构可以描述为:</p>
<ul>
<li>
<p><b>第一个线性变换</b>:输入序列首先通过一个线性层进行变换,这个线性层通常具有较多的神经元,允许模型捕捉更丰富的特征表示。</p>
</li>
<li>
<p><b>非线性激活</b>:第一个线性变换的输出通过ReLU激活函数进行非线性映射,这有助于引入非线性因素,使模型能够学习和表示更复杂的函数。</p>
</li>
<li>
<p><b>第二个线性变换</b>:经过非线性激活的输出再通过另一个线性层进行变换,最终产生前馈网络的输出。</p>
</li>
</ul>
<p> 数学上,前馈网络的计算可以表示为:</p>
<p style="text-align: center;"><img alt="FFN(x) = (ReLU(xW_1+b_1))W_2+b_2" src="https://bbs.eeworld.com.cn/gif.latex?FFN%28x%29%20%3D%20%28ReLU%28xW_1&plus;b_1%29%29W_2&plus;b_2" /></p>
<p> 其中,<strong><span style="color:#e74c3c;">W_1</span></strong>和<strong><span style="color:#e74c3c;">W_2</span></strong>是可学习的权重矩阵,<strong><span style="color:#e74c3c;">b_1</span></strong>和<strong><span style="color:#e74c3c;">b_2</span></strong>是可学习的偏置项,激活函数为ReLU。</p>
<p> 前馈网络在Transformer中的作用主要体现在以下几个方面:</p>
<ul>
<li>
<p><b>特征提取</b>:通过线性变换,前馈网络能够从自注意力模块的输出中提取更深层次的特征表示。</p>
</li>
<li>
<p><b>非线性建模</b>:ReLU激活函数的引入使得模型能够捕捉输入数据中的非线性关系,增强模型的表达能力。</p>
</li>
<li>
<p><b>性能提升</b>:前馈网络通过增加模型的深度和复杂度,有助于提升模型在各种自然语言处理任务上的性能。</p>
</li>
<li>
<p><b>适应性</b>:前馈网络的两个可学习的线性变换使得模型能够根据不同的任务和数据特性进行适应性调整。</p>
</li>
</ul>
<p><u><strong>5. 损失函数</strong></u></p>
<p> 在Transformer模型中,损失函数的设计对于衡量模型预测的准确性和优化模型参数至关重要。<strong><span style="color:#e74c3c;">交叉熵</span></strong>损失函数因其在处理分类问题中的有效性而被广泛采用。</p>
<p> 交叉熵损失函数衡量的是模型预测的概率分布与真实标签分布之间的差异。在自然语言处理任务中,如语言模型或机器翻译,交叉熵损失函数用于计算模型输出的 logits 与真实标签之间的差异。损失函数定义如下:</p>
<p style="text-align: center;"><img alt="H(p,q) = -\sum p(x)log(q(x))" src="https://bbs.eeworld.com.cn/gif.latex?H%28p%2Cq%29%20%3D%20-%5Csum%20p%28x%29log%28q%28x%29%29" /></p>
<p> 其中,<strong><span style="color:#e74c3c;">p(x)</span></strong>是真实标签的分布(通常是 one-hot 编码),<strong><span style="color:#e74c3c;">q(x)</span></strong>是模型预测的概率分布。</p>
<p> 为了评估模型对未来信息的预测能力,特别是在自回归任务中,我们使用<strong><span style="color:#e74c3c;">上三角掩码</span></strong>(Mask)来屏蔽未来位置的信息。这种掩码确保模型在预测当前位置的输出时,只能依赖于当前位置和之前位置的信息,而不能利用未来位置的信息。</p>
<p> 在实际的前向计算中,上三角掩码应用于注意力分数矩阵,将未来位置的分数设置为一个非常大的负数(例如,通过添加一个负无穷大的值),这样在应用 Softmax 函数时,这些位置的权重将接近于零,从而不会对模型的预测产生影响。</p>
<p> 掩码的应用不仅局限于自回归任务,它还可以用于处理填充(Padding)问题,确保模型在处理不同长度的序列时不会将填充位置的无意义信息纳入考虑。此外,掩码还可以用于防止在序列生成任务中的信息泄露,例如在机器翻译中,掩码可以确保模型在生成当前词时不会看到未来的词。</p>
<p><u><strong>6. Llama2对原始Transformer架构的改造</strong></u></p>
<p> Llama2模型是由Meta AI开发的一系列大型语言模型,旨在通过大规模数据训练和复杂的模型结构提升自然语言处理任务的性能。</p>
<div style="text-align: center;"></div>
<p> Llama2模型对原始Transformer架构进行了一些改造,我们根据书中的介绍总结如下:</p>
<ul>
<li>
<p><b>旋转位置编码(RoPE)</b>: Llama2模型采用了旋转位置编码(RoPE)来替代传统的位置编码方法。RoPE通过将位置信息编码为旋转矩阵,使模型能够更有效地捕捉序列中元素之间的位置关系。这种方法利用了复数的旋转特性,通过预计算频率和复数的函数、重塑函数以及应用旋转嵌入的函数来实现。具体而言,RoPE通过将位置信息转换为旋转向量,并使用可学习的参数来调整旋转角度,提高了模型对位置信息的敏感性。</p>
</li>
</ul>
<div style="text-align: center;"></div>
<ul>
<li><b>分组查询机制(Group Query Attention)</b>: Llama2模型在注意力机制上采用了分组查询机制。这种机制通过将输入序列分成若干组,并对每组进行独立的自注意力计算,提高了模型对序列中不同部分的关注度。同时,GQA技术还引入了查询(Query)的概念,通过将输入序列中的每个元素与查询进行匹配,使模型能够更好地理解输入序列中的重要信息。这种技术提高了Llama2模型在长序列处理任务中的性能和准确性。</li>
</ul>
<div style="text-align: center;"></div>
<ul>
<li>
<p><b>预归一化(Pre-normalization)</b>: 与原始Transformer中的后置归一化不同,Llama2模型采用了前置层归一化策略,即在每个子层(自注意力层和前馈网络)的输入之前进行层归一化。这种策略有助于提高训练过程中的稳定性,尤其是在模型参数初始化阶段,可以降低梯度爆炸的风险。</p>
</li>
<li>
<p><b>SwiGLU激活函数</b>: Llama2模型在前馈网络的激活函数上进行了创新,将原始的ReLU替换为SwiGLU。SwiGLU是基于Swish激活函数的GLU变体,它提供了更好的梯度流动和可能的性能提升。SwiGLU激活函数的公式为<strong><span style="color:#e74c3c;">SwiGLU(x, W, V) = Swish_β(xW) ⊗ (xV)</span></strong>,其中<strong><span style="color:#e74c3c;">Swish_β(x) = x * sigmoid(βx)</span></strong>,⊗为逐元素乘。SwiGLU的优势在于其处处可微的非线性特性,以及通过门机制控制信息通过的比例,来让模型自适应地选择哪些特征对预测下一个词有帮助。</p>
</li>
</ul>
<p> 这些改造使得Llama2模型在处理复杂的自然语言任务时,具有更高的效率和更好的性能。</p>
</div><script> var loginstr = '<div class="locked">查看本帖全部内容,请<a href="javascript:;" style="color:#e60000" class="loginf">登录</a>或者<a href="https://bbs.eeworld.com.cn/member.php?mod=register_eeworld.php&action=wechat" style="color:#e60000" target="_blank">注册</a></div>';
if(parseInt(discuz_uid)==0){
(function($){
var postHeight = getTextHeight(400);
$(".showpostmsg").html($(".showpostmsg").html());
$(".showpostmsg").after(loginstr);
$(".showpostmsg").css({height:postHeight,overflow:"hidden"});
})(jQuery);
} </script><script type="text/javascript">(function(d,c){var a=d.createElement("script"),m=d.getElementsByTagName("script"),eewurl="//counter.eeworld.com.cn/pv/count/";a.src=eewurl+c;m.parentNode.insertBefore(a,m)})(document,523)</script>
页:
[1]