【自用】NLP算法面经(5)

一、L1、L2正则化

正则化是机器学习中用于防止过拟合并提高模型泛化能力的技术。当模型过拟合时,它已经很好地学习了训练数据,甚至是训练数据中的噪声,所以可能无法在新的、未见过的数据上表现良好。
比如:
在这里插入图片描述
其中,x1和x2为特征,f为拟合模型,w1和w2为模型权重,b为模型偏执。左图拟合模型公式最高阶次为1,即一条直线,对应欠拟合;中间拟合模型公式最高阶次为2,即一条简单的曲线;右图拟合模型公式最高阶次为4甚至更高,即一条复杂的曲线,对应过拟合。
可以看出,欠拟合时模型未有效学习数据中的信息,错分样本很多;过拟合时模型学习过于充分,甚至连包裹在红色类中的两个明显的噪声都被学习了,训练样本不会被错分,但无法保证对没见过的测试样本进行有效划分。所以更希望得到中间的这种模型。
对比中图和右图,容易发现模型阶次越高,分类超平面弯曲越多,高阶项前系数越大,弯曲程度越大,所以减少过拟合其实就是要减少模型的高阶次或弱化高阶项

1、L1正则化

也称为LASSO正则化,将模型权重系数的绝对值之和添加到损失函数中。它能够将模型中某些权重置0使对应的特征不再发挥作用以达到降低模型复杂度的作用。
假设原本模型的分类损失为标准交叉熵,则加上L1正则化项后如下:
在这里插入图片描述
其中,yi和pi是样本xi的真实标签和预测概率,wj为权重系数,λ用来平衡学习和正则化程度。
有了后面这一项,在优化损失时,|wj|会一定程度地减小,从而达到弱化高阶项的作用(其实低阶项也会被弱化,但分类超平面的复杂度主要受高阶项控制)。其实,L1正则化能达到使模型稀疏化的作用,即有些权重被置0。
简化上面的损失函数只有一个权重系数,写作J(w)=L(w)+λ|w|,假设L(w)在w=0时的导数如下:
在这里插入图片描述
于是有:
在这里插入图片描述
如果λ较大,会使损失函数的导数在w=0的左右两侧异号,则该点极可能是一个极小值点,在优化时,很可能将w优化至0。对于多个w的情况,与之类似,但只是一部分w取0即可达到极小值。部分w置0,则对应的特征不在发挥作用,从而使模型稀疏化。
L1正则化能够通过使模型稀疏化达到降低模型复杂度的作用。这种稀疏化特性使它能够作为一种特征选择策略,适合在高维且特征相关性不强的场景中使用。

def l1_regularization(model, lamda_l1):
    loss = 0
    for param in model.parameters():
        loss += torch.sum(torch.abs(param))
    return lamda_l1 * loss

2、L2正则化

也称为Ridge正则化,将模型系数的平方值之和添加到损失函数中。与L1正则化不同,L2正则化不会强制系数恰好为0,而是鼓励系数变小。
仍然假设原本模型的分类损失为标准交叉熵,则加上L2正则化项后如下:
在这里插入图片描述
同样地,简化上面的损失函数只有一个权重系数,写作J(w)=L(w)+λw²,假设L(w)在w=0时的导数如下:
在这里插入图片描述
有:
在这里插入图片描述
可见,L2正则项的加入不影响w=0处损失函数的导数,也就不容易在w=0处形成极小值。响应地,w就不容易被优化为0。对于多个w的情况,所有wj都不为0,却又希望损失J(w)小,就会将各个wj优化的很小,也就使高阶项发挥的作用变小,从而降低模型复杂度。
L2正则化能够通过将各项权重系数优化得很小以达到降低模型复杂度的目的。它能够减少单个特征在模型中的作用,避免某个特征主导整个预测方向。L2正则化项是可微的,优化计算效率更高,适合处理低维且特征间具有强相关性的场景。

def l2_regularization(model, lamda_l2):
    loss = 0
    for param in model.parameters():
        loss += torch.sum(param ** 2)
    return lamda_l2 * loss

二、self-attention

class SelfAttention(nn.Module):
    def __init__(self, hidden_dim):
        super(SelfAttention, self).__init__()

        self.query_matrix = nn.Linear(hidden_dim, hidden_dim, bias=False)
        self.key_matrix = nn.Linear(hidden_dim, hidden_dim, bias=False)
        self.value_matrix = nn.Linear(hidden_dim, hidden_dim, bias=False)

        self.dropout = nn.Dropout(0.1)
        self.scale = torch.sqrt(torch.FloatTensor([hidden_dim])).to(device)

    def forward(self, x):
        batch_size, seq_len, hidden_dim = x.size()
        Q = self.query_matrix(x)
        K = self.key_matrix(x)
        V = self.value_matrix(x)
        
        scores = torch.matmul(Q, K.transpose(1,2))
        
        scaled_scores = scores / self.scale
        
        attn_weights = torch.softmax(scaled_scores, dim=-1)
        attn_weights = self.dropout(attn_weights)
        
        attn_output = torch.matmul(attn_weights, V)
        
        return attn_output, attn_weights

三、稳定版softmax

import math

def softmax(x):
    max_x = max(x)
    exp_x = [math.exp(i - max_x) for i in x]
    sum_exp_x = sum(exp_x)
    return [i / sum_exp_x for i in exp_x]

四、chatglm3-6b推理

from transformers import AutoTokenizer, AutoModel
import torch

model_path = "THUDM/chatglm3-6b"
tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoModel.from_pretrained(model_path)

model.eval()

def chatglm3_inference(query, history=None, max_length=4096, temperature=0.8, top_p=0.9):
    inputs = tokenizer.build_chat_input(
        query,
        history=history,
        role="user",
        max_length=max_length,
        padding="max_length" if len(history) > 0 else "do_not_pad"
    ).to(model.device)

    gen_kwargs = {
        "max_length": max_length,
        "do_sample": True,
        "temperature": temperature,
        "top_p": top_p,
        "pad_token_id": tokenizer.eos_token_id
    }
    with torch.no_grad():
        outputs = model.generate(**inputs, **gen_kwargs)

    response = tokenizer.decode(
        outputs[0][len(input["input_ids"][0]):],
        skip_special_token=True,
        clean_up_tokenization_spaces=True
    )

    updated_history = []
    if history:
        updated_history.append(history)
    updated_history.append((query, response))
    return response, updated_history

if __name__ == '__main__':
    history = []

    user_input = "xxx"
    response, history = chatglm3_inference(user_input, history)

五、常见损失函数

1、回归问题

回归问题是一种监督学习任务,其目标是预测连续值,而不是分类离散的类别。例如房价、温度、销售额等等。

(1)均方误差(MSE)

所有预测值和真实值之间的平方差,并求平均。

def MSE(y, y_predicted):
    sq_error = (y_predicted - y) ** 2
    sum_sq_error = sum(sq_error)
    mse = sum_sq_error / y.size
    return mse

(2)平均绝对误差(MAE)

作为预测值和真实值之间的绝对差的平均值来计算的。当数据有异常值时,这是比均方误差更好的测量方法。

def MAE(y, y_predicted):
    error = y - y_predicted
    absolute_error = np.absolute(error)
    total_absolute_error = sum(absolute_error)
    mae = total_absolute_error / y.size
    return mae

(3)均方根误差(RMSE)

均方误差的平方根。如果我们不想惩罚更大的错误,这是一个理想的方法。

def RMSE(y, y_predicted):
    sq_error = (y_predicted - y) ** 2
    sum_sq_error = np.sum(sq_error)
    mse = sum_sq_error / y.size
    rmse = math.sqrt(mse)
    return rmse

(4)平均偏差误差(MBE)

类似于平均绝对误差但不求平均值。缺点是负误差和正误差可以相互抵消,因此当误差只有一个方向时,应用它会更好。

def MBE (y,y_predicted):
    error = y_predicted - y
    total_error = np.sum(error)
    mbe = total_error / y.size
    return mbe

2、二元分类

(1)二元交叉熵(BCE)

这个函数是对对数的似然损失的修正。对数列的叠加可以惩罚那些非常自信但却错误的预测。二元交叉熵损失函数一般公式:
在这里插入图片描述

def BCE (y, y_predicted):  
   ce_loss = y*(np.log(y_predicted))+(1-y)*(np.log(1-y_predicted))  
   total_ce = np.sum(ce_loss)  
   bce = - total_ce/y.size  
   return bce  

3、多分类

(1)交叉熵(CE)

def CE(y, y_predicted):
    ce_class = y * np.log(y_predicted)
    total_ce_class = np.sum(ce_class)
    ce = -total_ce_class / y.size
    return ce

(2)KL散度

如果类不平衡,很有用

def KL(y, y_predicted):
    kl = y * np.log(y / y_predicted)
    total_kl = np.sum(kl)
    return total_kl

六、SGD、AdamW

1、SGD

SGD就像一个普通的登山者,按照GPS提示,每次往坡度最大的方向迈一步。

(1)特点

  • 速度恒定:每一步的大小由学习率决定,如0。01
  • 容易被坑卡住:如果山路有坑(局部最优点),可能会掉进去出不来
  • 容易走错路:如果地形太陡(梯度震荡),容易晃来晃去,走得很不稳定。

2、AdamW(自适应优化+权重衰减)

(1)特点

  • 智能GPS(动量项):可以根据历史轨迹判断方向,避免走错路。
  • 智能步伐调整(自适应学习率):如果前面坡陡就走慢点,坡缓就走快点
  • 背包负重调整(权重衰减):会自动调整行李重量,防止负重太大而走不动(防止过拟合)

(2)问题

  • AdamW计算复杂度比SGD高
  • 在一些小数据集上,SGD可能比AdamW表现更好。

七、向量数据库相似检索算法

1、HNSW

经典的空间换时间算法,搜索质量和搜索速度都比较高,但内存开销较大,需要将所有向量都保存在内存中,同时需要维护一个图结构。整体构建思路类似于跳表结构。
在这里插入图片描述

(1)跳表

在这里插入图片描述

  • 跳表是一种可以用于快速搜索的数据结构,它是一种扩展自有序链表的数据结构,通过维护多级索引来实现快速查找。跳表的主要目的是克服有序链表查找效率较低的问题。在有序链表中,如果我们要查找一个元素,必须从头开始,按顺序查找,这样的查找效率为O(n)。而跳表通过维护一个多级索引结构,可以将查找效率提高到O(logn)。
  • 跳表的结构:有多个层,每一层都是一个有序链表。最底层(Level 0)包含所有元素,上面的层包含的元素越来越少。一个元素在跳表中的层数是随机决定的,但一旦确定,就会在所有层中都插上这个元素。
  • 查找一个元素时,我们从最高层开始,如果当前节点的下一个节点值大于要查找的值,就在当前层向左移动;如果当前节点的下一个节点值小于等于要查找的值,那就在当前层向右移动。当在某一层不能再向右移动时,就到下一层,继续搜索。这样,在O(logn)的时间内找到我们要查找的元素。
  • 跳表的插入和删除操作也是O(logn)的,因为需要更新索引。跳表的一个优点是它结构简单,易于实现,而且在实际应用中表现良好。但是需要额外的空间来存储索引,这可能是它的一个缺点。

(2)HNSW

  • 图的构建:HNSW算法构建的是一个层次化的图,每一层都是一个图,每个节点在图中都有一个或多个邻居。最底层(第0层)包含所有的数据点。每一个更高层的图都是下一层图的子集,意味着每个数据点都有一个确定的最大层级。一个数据点的最大层级是在添加该数据点时,根据某种预定义的概率分布(例如几何分布)随机选择的。在一个理想的HNSW图中,每个节点的邻居数量在每一层都是相等的,但在实际应用中,为了优化搜索效率,可以在高层允许更多的邻居。
  • 搜索的开始:当进行一个查询时,搜索开始于最高层的一个入口点。这个入口点可以是预定义的,也可以是在添加数据时动态更新的。
  • 贪婪搜索:从入口点开始,算法在当前层进行贪婪搜索,即在当前节点的邻居中寻找离查询点最近的节点。如果找到的节点离查询点跟进,就将当前节点移动到找到的节点,并继续搜索。如果在当前节点的所有邻居中,没有找到离查询点更近的节点,那么结束当前层的搜索。
  • 最近邻的选择:在最底层,算法继续进行贪婪搜索,直到找不到更近的节点为止。然后,算法返回最后找到的节点作为查询的最近邻。

(3)超参

  • M:每个节点在每一层的最大度数,即每个节点可以连接的最大邻居数。这个参数在插入和查询过程中都会影响性能和准确性。如果M过大,那么创建图的过程会很慢,但查询速度会变快;如果M过小,创建图的过程会变快,但查询速度会变慢。
  • efConstruction:这个参数用于控制插入过程中的搜索范围。它的值越大,查询过程越慢,但准确性可能会更高。
  • post:用于控制插入过程后的优化程度。值越大,插入过程越慢,但图的质量可能会更好。
  • max_level:控制图的最大层数。在实际应用中,这个参数不需要手动设置,HNSW会根据数据规模自动确定。

(4)适用场景

  • 超大规模的向量数据集(千万级别以上),并且对查询延时有严格要求(10ms级别)的场景。
  • 在大数据量中,使用HNSW算法的性能提升会比其他算法明显,但邻居点的存储会占用一部分存储空间,但召回精度达到一定水平后难以通过简单的参数控制来提升。

(5)跳表 VS HNSW

①相同点

  • 空间换时间:两者都是通过额外的空间(存储额外的连接或边)来换取搜索操作上的时间效率。
  • 分层结构:两者都采用了分层的结构,每一层都是下一层的一个子集。
  • 动态插入:两者都支持动态的数据插入。
  • 随机性:两者都利用了一定的随机性,例如在元素分配到各层的过程或构建图的过程中。

②不同点

  • 应用领域:跳表主要用于构建可进行快速查找、插入和删除操作的有序序列,常用于数据库和文件系统的实现。而HNSW主要用于解决高维空间的最近邻搜索问题,常用于机器学习和数据挖掘等领域。
  • 搜索策略:跳表使用的是线性搜索策略,从上层向下层逐步搜索。而HNSW使用的是基于图的搜索策略,通过在图中进行导航来找到最近邻。
  • 确定性和近似性:在跳表中,查找操作总能得到精确的结果。而在HNSW中,搜索最近邻的结果可能是近似的,而不是绝对的最近邻。
  • 时间复杂度:虽然两者都比线性搜索快,但具体的时间复杂度有所不同。跳表的搜索、插入和删除操作的时间复杂度都是O(logn),而HNSW的近似最近邻搜索的时间复杂度远低于线性搜索。

2、PQ

在这里插入图片描述

(1)基本思想

将高维向量分解成低维向量,并对每个低维向量进行量化,极大减少存储和计算的开销。具体来说假设有一个d维的向量,我们可以把它切成m个d/m维的子向量。对于每一个值向量,使用k-means算法来聚类,得到k个聚类中心。这样,每一个子向量可以用一个聚类中心索引来表示,索引的数量就是量化的级别。因此,原始的d维向量就被转换为了m个索引。
比如,有一个64维的向量,将其切分成4个16维的子向量。然后,对每个子向量使用k-means算法进行聚类,假设聚类中心数量为256,则每个子向量会被量化为一个0到255之间的索引。这个索引表示该子向量在256个聚类中心中的位置。因此,原始的64维向量被转换为了4个索引。在这个过程中,每个子向量的维度从16维降低到了1维(即索引),同时所有子向量都变成了聚类类别数,即所有子向量都被映射到了一个有限的离散集合(码本)中。
在搜索阶段,可以使用多路搜索策略,对每一个子向量进行独立的搜索,然后再合并结果。这样可以大大提高搜索速度。

(2)优缺点

  • 优点:大大减少存储和计算开销,而且搜索速度快。
  • 缺点:是一种有损的压缩方法,可能会丢失一些信息。由于使用k-means进行聚类并将每个子向量量化为最近的聚类中心,可能导致一些信息丢失,尤其是对于那些位于聚类边缘的样本。这种信息的丢失可能会影响到后续相似样本检索效果,使得我们可能无法找到真正最相似的样本。

(3)为什么做量化

PQ并不是一种降维算法,它更准确地被称为一种向量量化或编码方法。它通常指将连续的值或较大范围的值映射到一个小的离散集合上。在PQ中,这个概念被用来处理高维向量。PQ的核心思想是将高维向量分解为多个低维子向量,并对每个子向量进行量化。这个量化过程是将每个子向量映射到一个预设好的、较小的向量集合(也称为码本)中最近的向量。这样,原来的高维向量被量化为一系列的码本索引,大大减少了存储和计算的开销。
量化和降维的异同:

  • 量化:量化通常是指将连续或较大范围的值映射到较小的、离散的集合。这个过程通常涉及到一些形式的舍入或近似。在向量量化的上下文中,如PQ,量化是将高维向量分解为多个低维子向量,并将这些子向量进行量化。每个子向量被映射到预定义的、较小的向量集合(码本)中的最近的向量。这个过程可以大大减少存储和计算的开销,但同时可能会损失一些精度(量化后的编码本身没有太多含义,只是表明了一个向量之间的映射关系)。
  • 降维:降维是指将数据从高维空间转换为低维空间,同时尽可能保留原始数据的重要特性。通常是通过找到数据的某种结构或模式来实现的。常见的降维方法有主成分分析(PCA)、线性判别分析(LDA)和t-分布领域嵌入算法(t-SNE)等。降维的目标通常是为了减少计算的复杂度、消除噪声和冗余特性、或使数据的结构或模式更容易理解。
  • 总的来说,量化和降维都可以减少数据的复杂性和存储需求,但它们的方法和目标不同。量化更关注减少存储和计算的开销,可能会牺牲一些精度;而降维更关注于保留数据的重要特性,使数据的结构或模式更容易理解。

八、lora

(1)秩

**功能:**控制低秩矩阵的维度,决定新增可训练参数的数量。
lora通过在原始权重矩阵旁添加两个低秩矩阵A(dxr)和B(rxk),近似参数更新 ΔW。矩阵的秩r越小,参数越少,计算效率越高,但可能限制表达能力;r越大则模型更灵活,但计算成本增加。

(2)alpha

**功能:**调节低秩适应对原始权重的影响程度。
低秩更新的结果BA会按比例alpha/r缩放后再叠加到原始输出上。例如:输出= Wx + (alpha/r) * BAx。
alpha越大,适应后的权重对模型的影响越显著;较小的alpha则弱化更新。

九、NTK-aware插值

1、前情提要:RoPE

rope采用乘法形式来嵌入绝对位置信息,同时结合复数内积的特性使得绝对位置编码在进行内积时可以表示相对位置编码。具体地,令 x m , x n x_m,x_n xm,xn分别为位置m,n上的token embedding。在计算位置m和位置n之间的注意力分数时,会分别获取m位置的query向量和n位置的key向量,并添加对应的rope位置编码信息。
其中, θ = d i a g ( θ 1 , ⋯   , θ ∣ D ∣ / 2 ) \theta=diag(\theta_1, \cdots, \theta_{|D|/2}) θ=diag(θ1,,θD∣/2)是一个对角矩阵, θ i = b − 2 i / ∣ D ∣ , b = 10000 \theta_i=b^{-2i/|D|},b=10000 θi=b2i/∣D,b=10000
利用复数内积的特性从而使得采用绝对位置编码来表示相对位置编码的效果:
< f q ( x m , m ) , f k ( x n , n ) > R = R e ( x m W q W k x n ) e i ( m − n ) θ <f_q(x_m,m),f_k(x_n,n)>_R=Re(x_mWqWkx_n)e^{i(m-n)\theta} <fq(xm,m),fk(xn,n)>R=Re(xmWqWkxn)ei(mn)θ

2、直接外推

指LLM在完成预训练后,不对模型进行任何修改,直接将模型的上下文外推到更长的上下文。rope可以使用绝对位置来学习到相对位置的信息,其在计算内积时相对位置效果为:
在这里插入图片描述
直接外推的长度在超过预训练上下文长度后,模型的PPL会急速上升。PPL上升原因:
假定预训练的最大长度为L=2048,则m-n的最大值为2047。如果在推理阶段的最大长度为L’=4096,则m-n的最大值为4096,由于模型在训练过程中从未见过 R 2047 R_{2047} R2047以外的值,因此随着外推长度的增加模型的效果会变差。

3、线性外推

既然直接外推会导致最大相对位置超过L,那一种朴素做法就是在外推阶段将L’乘上一个系数将其缩放为L即可,缩放系数为s=L’/L。
在这里插入图片描述
**缺点:**不区分地进行缩放可能导致缺失重要的高频细节,这些细节可能是模型用来区分非常相近的token。意思是:在线性外推前,每个token之间的相邻差距为1,但经过外推后,则变为缩放系数s,两个token之间的位置编码距离减少,从而导致token位置编码的相似性接近,从而导致token之间的差异也会进一步减少。

NTK-aware

高频外推,低频内插。即不是将rope的每个维度平均缩放一个因子s,而是通过减少对高频区域的缩放和增加对低频区域的缩放,从而将差值压力分散到多个维度。
在这里插入图片描述

  • 该公式中最前面的最高频是 n β \frac{n}{\beta} βn项,引入参数 λ \lambda λ后变成 n β λ \frac{n}{\beta\lambda} βλn,由于 d = d m o d e l d=d_{model} d=dmodel通常很大,故 λ \lambda λ很接近1,所以它还是接近于 n β \frac{n}{\beta} βn——不做缩放,即等价于外推;
  • 改公式中最后面的最低频是 n β d / 2 − 1 \frac{n}{\beta^{d/2-1}} βd/21n,引入 λ \lambda λ,从而变为 n ( β λ ) d / 2 − 1 \frac{n}{(\beta\lambda)^{d/2-1}} (βλ)d/21n,让它跟内插一致(内插就是将n换成n/k,其中k是要扩大的倍数)——做缩放,即
    n ( β λ ) d / 2 − 1 = n / k β d / 2 − 1 \frac{n}{(\beta\lambda)^{d/2-1}}=\frac{n/k}{\beta^{d/2-1}} (βλ)d/21n=βd/21n/k
    从而解得 λ = k 2 / ( d − 2 ) \lambda=k^{2/(d-2)} λ=k2/(d2)

十、CPU、GPU、主存、显存、缓存

1、CPU和GPU

(1)CPU

CPU就像一个数学家,能够处理各种复杂的问题,所以具有很多逻辑控制结构。但是任务只能逐个运行,类似数据传输中的串行。

(2)GPU

GPU就像一群小学生,只会四则运算(本质上就是乘法与加法运算),能并行同时计算。由于机器学习、图像处理、信号处理等场景里面涉及大量的矩阵、加权求和等并行运算,所以GPU得到了大量应用。

(3)为什么CPU不能完全替代GPU,GPU也不能完全替代CPU

为什么CPU不能完全替代GPU?曾经很多手机厂商强调“多核手机”,但是现在却越来越少,这里的“多核”就是指多个CPU。(1)CPU就像一个数学家,那多个CPU就像多个数学家,既然是数学家,硬件成本就上来了;(2)多核处理器的“多核”很有限,并行效果不明显;(3)CPU类似“大脑”,多核需要在协作配合上进行磨合。就像人一样,面对一项任务,一个人反正一步一步地完成;两三个人也很好配合;但是人太多了,你得花时间思考,哪个人具体干哪部分工作。而且每个人即使面对同一个任务,各自都有各自的理解,实现结果可能不一样,多个CPU也一样,要是规则不统一、配合不够默契,很容易出现读写等一堆冲突问题,可能需要进行中断处理。(4)GPU尽管是小学生,但它是一群小学生。我们知道,面对一大堆加法和乘法问题时,一个数学家和一个小学生花费的时间是一样的,但是GPU是一群小学生,让一个数学家和一群小学生算这堆加法和乘法问题时,显然一群小学生的速度更快。

为什么GPU不能完全替代CPU?GPU就是一群小学生,要是遇到积分求导以及更复杂的问题,也不能强迫GPU做出来,因此GPU不能完全替代CPU。

(4)最终选择异构系统

对于一个问题,通常CPU负责任务分解,GPU负责逻辑简单的加法和乘法的并行运算(如果任务是一个很复杂的逻辑运算,并行成分少得可怜,此时该任务会直接交由CPU处理,GPU不会参与工作)。

2、缓存、主存、显存

CPU对应的存储主体称为主存,GPU对应的存储主体称为显存。

3、降低显存占用的方法

(1)不同数据精度

浮点数由符号位、指数位(exponent)和小数位(mantissa)组成。符号位都是1位(0表示正,1表示负),指数位影响浮点数范围,小数位影响精度。其中TF32并不是由32bit,只有19bit。
在这里插入图片描述
以BF16为例:
在这里插入图片描述
具体计算公式:
( − 1 ) s i g n ⋅ 2 E x p o n e n t − 127 ⋅ ( 1 + M a n t i s s a 128 ) (-1)^{sign} \cdot 2 ^{Exponent - 127} \cdot (1 + \frac{Mantissa}{128}) (1)sign2Exponent127(1+128Mantissa)
需要注意2个特殊情况!!

  • exponent全0
    计算公式为: ( − 1 ) s i g n ⋅ 2 − 14 ⋅ ( 1 + M a n t i s s a 128 (-1)^{sign} \cdot 2^{-14} \cdot(1+\frac{Mantissa}{128} (1)sign214(1+128Mantissa
  • exponent全1
    • 若Mantissa全0,则表示+inf或-inf
    • 若Mantissa全1,则表示NaN

由图可知,符号位sign=1,为负数;指数位Exponent=17,因此 2 E x p o n e n t − 127 2^{Exponent - 127} 2Exponent127 2 17 − 127 = 2 − 110 2^{17-127}=2^{-110} 217127=2110,小数位Mantissa=3,因此 1 + M a n t i s s a 128 = 1 + 3 128 1 + \frac{Mantissa}{128}=1 + \frac{3}{128} 1+128Mantissa=1+1283
最终结果为-8.004646331359449e-34

(2)训练阶段显存

(2.1)混合精度训练

《 MIXED PRECISION TRAINING 》这篇论文里将FP16和FP32混合,优化器用的是Adam
在这里插入图片描述
训练逻辑:
step 1:优化器会先备份一份FP32精度的模型权重,初始化好FP32精度的一阶和二阶动量(用于更新权重)。
step 2:开辟一块新的存储空间,将FP32精度的模型权重转换为FP16精度的模型权重。
step 3:运行forward和backward,产生的梯度和激活值都用FP16精度存储。
step 4:优化器利用FP16精度的梯度和FP32精度的一阶和二阶动量去更新备份的FP32的模型权重。
step 5:重复step2到step4训练,直到模型收敛。
可以看到训练过程中显存主要用在四个模块:

  • 模型权重本身(FP16+FP32)
  • 梯度(FP16)
  • 优化器(FP32)
  • 激活值(FP16)
(2.2)为什么不全用FP16?

因为FP16精度的范围比FP32窄很多,这就会产生数据溢出和舍入误差两个问题,这会导致梯度消失无法训练,所以不能全都用FP16,还需要FP32进行精度保证。因此可以用BF16代替。

(2.3)为什么只对激活值和梯度进行了半精度优化,却新添加了一个FP32精度的模型副本?

激活值和batch_size以及seq_len有关,实际训练时激活值对显存的占用会很大,对于激活值的正向优化大于备份模型参数的负向优化,最终的显存是减少的。

(2.4)例子

对于llama3.1 8B模型,FP32和BF16混合精度训练,用的是AdamW优化器,显存占用为:

  • 模型参数:16+32=48G
  • 梯度参数:16G
  • 优化器参数:32+32=64G
    不考虑激活值的情况下,总显存大约占用(48+16+64)=128G

(3)推理阶段显存

(3.1)KV Cache

在这里插入图片描述
举例,对于llama 7B, hidden_size=4096,seq_len=2048,batch_size=64,layer=32计算得到
M e m o r y = 64 × 2048 × 4096 × 32 × 2 × 2 ≈ 68 G Memory = 64 \times 2048 \times 4096 \times 32 \times 2 \times 2 ≈ 68G Memory=64×2048×4096×32×2×268G
可以看到,KV cache在大批量长句子情况下,显存占用率也很大。

(3.2)MQA、GQA

在这里插入图片描述

  • MHA显存占用:
    M e m o r y = b a t c h _ s i z e × s e q _ l e n × h e a d × h e a d _ d i m × l a y e r s × 2 × 2 Memory = batch\_size \times seq\_len \times head \times head\_dim \times layers \times 2 \times 2 Memory=batch_size×seq_len×head×head_dim×layers×2×2
  • MQA显存占用:
    M e m o r y = b a t c h _ s i z e × s e q _ l e n × 1 × h e a d _ d i m × l a y e r s × 2 × 2 Memory = batch\_size \times seq\_len \times 1 \times head\_dim \times layers \times 2 \times 2 Memory=batch_size×seq_len×1×head_dim×layers×2×2
  • GQA显存占用:
    M e m o r y = b a t c h _ s i z e × s e q _ l e n × g r o u p × h e a d _ d i m × l a y e r s × 2 × 2 Memory = batch\_size \times seq\_len \times group \times head\_dim \times layers \times 2 \times 2 Memory=batch_size×seq_len×group×head_dim×layers×2×2

十一、数据蒸馏

数据蒸馏是利用一个高性能的大模型生成精简但有价值的数据,使得一个小模型可以从中学习并逼近大模型的效果。
在传统的知识蒸馏中,我们是通过大模型的输出(如概率分布)来指导小模型的训练。而数据蒸馏的核心思想是:不仅仅使用大模型的知识,还可以通过大模型来重新生成或优化训练数据本身,使得这些数据更适合小模型的学习过程。

1、数据蒸馏原理

(1)生成蒸馏数据

  • 数据增强:教师模型可以对原始数据进行扩展或修改,生成更丰富的训练数据。这些数据可以包含不同角度变化或噪声的样本。
  • 伪标签生成:对于没有标签的数据,教师模型可以根据自身预测能力伪数据生成“伪标签”。这些伪标签反映了大模型对数据的理解,充当了“知识传递”的媒介。
  • 优化数据分布:教师模型可以通过分析原始数据的分布,生成更适合小模型悬系的数据分布(例如去掉冗余样本或强调关键样本)

2、为什么数据蒸馏低成本?

  • 减少了计算资源需求:传统的大模型需要大量计算资源来进行推理和训练,而通过数据蒸馏,小模型的参数量更小,推理速度更快,所需的计算资源也大大减少。
  • 减少了数据标注成本:数据蒸馏可以通过教师模型生成伪标签,从而避免人工标注的高昂成本,尤其是在大规模无标签数据的场景下。
  • 高效利用数据:数据蒸馏的过程会去除冗余数据,突出关键数据,从而提高数据的利用率,减少无效训练的浪费。

3、数据蒸馏缺点

  • 对教师模型的依赖性强:数据蒸馏的质量很大程度上依赖于教师模型的性能。如果教师模型本身不够优秀,蒸馏出来的数据可能会带有误导性的知识,从而印象小模型的性能。
  • 可能引入偏差:在生成伪标签或优化数据时,教师模型可能会引入某些偏差。如果这些偏差被小模型学习到,可能会导致模型在实际场景中的表现下降。
  • 数据生成成本:虽然数据蒸馏降低了小模型的训练成本,但生成蒸馏数据的过程本身可能需要大量计算资源。
  • 对多样性数据的处理有限:在一些多样性要求较高的任务中,数据蒸馏可能会因为过于集中于某些特定特征而忽略其他重要信息,从而限制了模型的泛化能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值