国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

這篇具有很好參考價(jià)值的文章主要介紹了從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

前言?

最近一直在做類ChatGPT項(xiàng)目的部署 微調(diào),關(guān)注比較多的是兩個(gè):一個(gè)LLaMA,一個(gè)ChatGLM,會(huì)發(fā)現(xiàn)有不少模型是基于這兩個(gè)模型去做微調(diào)的,說到微調(diào),那具體怎么微調(diào)呢,因此又詳細(xì)了解了一下微調(diào)代碼,發(fā)現(xiàn)微調(diào)LLM時(shí)一般都會(huì)用到Hugging face實(shí)現(xiàn)的Transformers庫的Trainer類

從而發(fā)現(xiàn),如果大家想從零復(fù)現(xiàn)ChatGPT,便得從實(shí)現(xiàn)Transformer開始,因此便開啟了本文:從零實(shí)現(xiàn)Transformer的簡(jiǎn)易版與強(qiáng)大版:從300多行到3000多行,主要分為兩個(gè)大部分

  • 參考harvard對(duì)transformer的實(shí)現(xiàn),按照Transformer每一步的原理逐步逐行從零實(shí)現(xiàn),先編碼器后解碼器,特別是注意力機(jī)制(縮放點(diǎn)積、多頭注意力)
  • 從頭到尾解讀Hugging face實(shí)現(xiàn)的Transformers庫的整體代碼架構(gòu),及逐行解讀每一行代碼,而網(wǎng)上沒有關(guān)于這個(gè)Transformers庫的代碼解讀

且本文的代碼解讀與其他代碼解讀最大的不同是:會(huì)對(duì)出現(xiàn)在本文的每一行代碼都加以注釋、解釋、說明,甚至對(duì)每行代碼中的變量都會(huì)做解釋/說明

總之,一如既往的保持對(duì)初學(xué)者的足夠友好,讓即便沒有太多背景知識(shí)的也能順暢理解本文

第一部分 從零實(shí)現(xiàn)Transformer編碼器模塊

transformer強(qiáng)大到什么程度呢,基本是17年之后絕大部分有影響力模型的基礎(chǔ)架構(gòu)都基于的transformer(比如,這里有200來個(gè),包括且不限于基于decode的GPT、基于encode的BERT、基于encode-decode的T5等等)

通過博客內(nèi)的這篇文章《Transformer通俗筆記:從Word2Vec、Seq2Seq逐步理解到GPT、BERT》,我們已經(jīng)詳細(xì)了解了transformer的原理(如果忘了,建議必復(fù)習(xí)下再看本文,當(dāng)然,如果你實(shí)在不想跳轉(zhuǎn),就只想呆在本文,也行,我努力..)

從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

如果把上圖中的各種細(xì)節(jié)也顯示出來,則如下大圖所示(此大圖來源于七月在線NLP11里倪老師講的Transformer模型源碼解讀,positional encoding、多頭等沒畫)

從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

具體說來,是一個(gè)典型的編碼器-解碼器架構(gòu)

# 定義一個(gè)基于 nn.Module 的編碼器-解碼器類
class EncoderDecoder(nn.Module):

    # 初始化方法,接收編碼器、解碼器、源嵌入、目標(biāo)嵌入和生成器作為參數(shù)
    def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
        # 調(diào)用 nn.Module 的初始化方法
        super(EncoderDecoder, self).__init__()  

        self.encoder = encoder  # 將傳入的編碼器實(shí)例保存為類屬性
        self.decoder = decoder  # 將傳入的解碼器實(shí)例保存為類屬性
        self.src_embed = src_embed  # 將傳入的源嵌入實(shí)例保存為類屬性
        self.tgt_embed = tgt_embed  # 將傳入的目標(biāo)嵌入實(shí)例保存為類屬性
        self.generator = generator  # 將傳入的生成器實(shí)例保存為類屬性
        
    # 前向傳播方法,接收源序列、目標(biāo)序列和它們的掩碼作為參數(shù)
    def forward(self, src, tgt, src_mask, tgt_mask):
        # 對(duì)源序列進(jìn)行編碼,并將編碼結(jié)果與掩碼傳遞給解碼器進(jìn)行解碼
        return self.decode(self.encode(src, src_mask), src_mask,
                            tgt, tgt_mask)
    
    # 編碼方法,接收源序列和掩碼作為參數(shù)
    def encode(self, src, src_mask):
        # 將源序列進(jìn)行嵌入,然后將嵌入后的序列和源序列掩碼傳給編碼器
        return self.encoder(self.src_embed(src), src_mask)
    
    # 解碼方法,接收編碼器輸出(memory)、源序列掩碼、目標(biāo)序列和目標(biāo)序列掩碼作為參數(shù)
    def decode(self, memory, src_mask, tgt, tgt_mask):
        # 將目標(biāo)序列進(jìn)行嵌入,然后將嵌入后的序列、編碼器輸出、源序列掩碼和目標(biāo)序列掩碼傳給解碼器
        return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)

# 定義一個(gè)基于 nn.Module 的生成器類
class Generator(nn.Module):

    # 初始化方法,接收模型維度(d_model)和詞匯表大?。╲ocab)作為參數(shù)
    def __init__(self, d_model, vocab):
        # 調(diào)用 nn.Module 的初始化方法
        super(Generator, self).__init__()  
        # 定義一個(gè)線性層,將模型的輸出維度映射到詞匯表大小
        self.proj = nn.Linear(d_model, vocab)  

    # 前向傳播方法,接收輸入 x
    def forward(self, x):
        # 將輸入 x 傳入線性層,然后對(duì)輸出應(yīng)用 log-softmax 激活函數(shù)(在最后一個(gè)維度上)
        return F.log_softmax(self.proj(x), dim=-1)

考慮到Hugging face實(shí)現(xiàn)的Transformers庫雖然功能強(qiáng)大,但3000多行,對(duì)于初次實(shí)現(xiàn)的初學(xué)者來說,理解難度比較大,因此,咱們一步步結(jié)合對(duì)應(yīng)的原理來逐行編碼實(shí)現(xiàn)一個(gè)簡(jiǎn)易版的transformer

1.1 關(guān)于輸入的處理:針對(duì)輸入做embedding,然后加上位置編碼

?為了方便后面代碼的編寫,先引入一些庫

import numpy as np          # 導(dǎo)入NumPy庫,用于進(jìn)行矩陣運(yùn)算和數(shù)據(jù)處理
import torch                # 導(dǎo)入PyTorch庫,用于構(gòu)建神經(jīng)網(wǎng)絡(luò)及相關(guān)操作
import torch.nn as nn       # 導(dǎo)入PyTorch神經(jīng)網(wǎng)絡(luò)模塊,用于構(gòu)建神經(jīng)網(wǎng)絡(luò)層
import torch.nn.functional as F  # 導(dǎo)入PyTorch神經(jīng)網(wǎng)絡(luò)函數(shù)庫,用于激活函數(shù)、損失函數(shù)等
import math, copy, time          # 導(dǎo)入數(shù)學(xué)庫、復(fù)制庫和時(shí)間庫,用于各種數(shù)學(xué)計(jì)算、復(fù)制操作和計(jì)時(shí)
from torch.autograd import Variable  # 從PyTorch自動(dòng)微分庫中導(dǎo)入Variable類,用于構(gòu)建自動(dòng)微分計(jì)算圖
import matplotlib.pyplot as plt      # 導(dǎo)入Matplotlib的pyplot模塊,用于繪制圖表和可視化
import seaborn                       # 導(dǎo)入Seaborn庫,用于繪制統(tǒng)計(jì)圖形和美化圖表
seaborn.set_context(context="talk")  # 設(shè)置Seaborn的上下文環(huán)境,設(shè)置圖表的尺寸和標(biāo)簽字體大小等
%matplotlib inline                   # IPython魔術(shù)命令,使Matplotlib繪制的圖形直接顯示在Notebook內(nèi)

1.1.1 針對(duì)輸入做embedding

對(duì)于模型來說,每一句話比如“七月的服務(wù)真好,答疑的速度很快”,在模型中都是一個(gè)詞向量,但如果每句話都臨時(shí)抱佛腳去生成對(duì)應(yīng)的詞向量,則處理起來無疑會(huì)費(fèi)時(shí)費(fèi)力,所以在實(shí)際應(yīng)用中,我們會(huì)事先預(yù)訓(xùn)練好各種embedding矩陣,這些embedding矩陣包含常用領(lǐng)域常用單詞的向量化表示,且提前做好分詞

維度1 維度2 維度3 維度4 ... 維度512
教育
機(jī)構(gòu)
在線
課程
..
服務(wù)
答疑
老師

從而當(dāng)模型接收到“七月的服務(wù)真好,答疑的速度很快”這句輸入時(shí),便可以從對(duì)應(yīng)的embedding矩陣?yán)锊檎覍?duì)應(yīng)的詞向量,最終把整句輸入轉(zhuǎn)換成對(duì)應(yīng)的向量表示

這部分的代碼 可以如下表示

# 定義一個(gè)名為Embeddings的類,繼承自PyTorch的nn.Module類
class Embeddings(nn.Module):
    # 初始化Embeddings類
    def __init__(self, d_model, vocab):
        # 調(diào)用父類nn.Module的初始化方法
        super(Embeddings, self).__init__()
        # 創(chuàng)建一個(gè)詞嵌入層,參數(shù)為詞匯表大小和詞嵌入維度
        self.lut = nn.Embedding(vocab, d_model)
        # 將詞嵌入維度保存為類屬性
        self.d_model = d_model

    # 定義前向傳播方法
    def forward(self, x):
        # 通過詞嵌入層將輸入的單詞編碼為向量,并乘以詞嵌入維度的平方根進(jìn)行縮放
        return self.lut(x) * math.sqrt(self.d_model)

1.1.2 位置編碼的實(shí)現(xiàn)

關(guān)于位置編碼的通透理解,請(qǐng)參閱此文《一文通透位置編碼:從標(biāo)準(zhǔn)位置編碼到旋轉(zhuǎn)位置編碼RoPE》

最終,再通過下面這兩行代碼完美實(shí)現(xiàn)位置編碼

        # 使用正弦和余弦函數(shù)生成位置編碼,對(duì)于d_model的偶數(shù)索引,使用正弦函數(shù);對(duì)于奇數(shù)索引,使用余弦函數(shù)。
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)

1.2?經(jīng)過「embedding + 位置編碼」后乘以三個(gè)權(quán)重矩陣得到三個(gè)向量Q K V

從下圖可知,經(jīng)過「embedding + 位置編碼」得到的輸入,會(huì)乘以「三個(gè)權(quán)重矩陣:??」得到查詢向量Q、鍵向量K、值向量V(你可以簡(jiǎn)單粗暴的理解為弄出來了三個(gè)分身)

從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

舉個(gè)例子,針對(duì)「我想吃酸菜魚」這句話,經(jīng)過embedding + 位置編碼后,可得(注:可以512維,也可以是768維,但由于transformer論文中作者設(shè)置的512維,所以除了這個(gè)酸菜魚的例子暫為768維外,其他地方均統(tǒng)一為512維)

從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

然后乘以三個(gè)權(quán)重矩陣得

從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

?為此,我們可以先創(chuàng)建4個(gè)相同的線性層,每個(gè)線性層都具有 d_model 的輸入維度和 d_model 的輸出維度

        self.linears = clones(nn.Linear(d_model, d_model), 4) 

前三個(gè)線性層分別用于對(duì) Q向量、K向量、V向量進(jìn)行線性變換(至于這第4個(gè)線性層在隨后的第3點(diǎn))

1.3 對(duì)輸入和Multi-Head Attention做Add&Norm,再對(duì)上步輸出和Feed Forward做Add&Norm

我們聚焦下transformer論文中原圖的這部分,可知,輸入通過embedding+位置編碼后,先后做以下兩個(gè)步驟

從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

  1. 針對(duì)query向量做multi-head attention,得到的結(jié)果與原query向量,做相加并歸一化
            attention = self.attention(query, key, value, mask)
            output = self.dropout(self.norm1(attention + query))
    這個(gè)相加具體是怎么個(gè)相加法呢?事實(shí)上,Add代表的Residual Connection(殘差連接),是為了解決多層神經(jīng)網(wǎng)絡(luò)訓(xùn)練困難的問題,通過將前一層的信息無差的傳遞到下一層,可以有效的僅關(guān)注差異部分,這一方法之前在圖像處理結(jié)構(gòu)如ResNet等中常常用到

    從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

    具體編碼時(shí)通過 SublayerConnection 函數(shù)實(shí)現(xiàn)此功能
    """一個(gè)殘差連接(residual connection),后面跟著一個(gè)層歸一化(layer normalization)操作"""
    class SublayerConnection(nn.Module):
        # 初始化函數(shù),接收size(層的維度大小)和dropout(dropout率)作為輸入?yún)?shù)
        def __init__(self, size, dropout):
            super(SublayerConnection, self).__init__()  # 調(diào)用父類nn.Module的構(gòu)造函數(shù)
            self.norm = LayerNorm(size)                 # 定義一個(gè)層歸一化(Layer Normalization)操作,使用size作為輸入維度
            self.dropout = nn.Dropout(dropout)          # 定義一個(gè)dropout層
    
        # 定義前向傳播函數(shù),輸入?yún)?shù)x是輸入張量,sublayer是待執(zhí)行的子層操作
        def forward(self, x, sublayer):  
            # 將殘差連接應(yīng)用于任何具有相同大小的子層
            # 首先對(duì)輸入x進(jìn)行層歸一化,然后執(zhí)行子層操作(如self-attention或前饋神經(jīng)網(wǎng)絡(luò))
            # 接著應(yīng)用dropout,最后將結(jié)果與原始輸入x相加。
            return x + self.dropout(sublayer(self.norm(x)))
    而Norm則代表了Layer Normalization,通過對(duì)層的激活值的歸一化,可以加速模型的訓(xùn)練過程,使其更快的收斂,編碼時(shí)用?LayerNorm?函數(shù)實(shí)現(xiàn)
    """構(gòu)建一個(gè)層歸一化(layernorm)模塊"""
    class LayerNorm(nn.Module):
        # 初始化函數(shù),接收features(特征維度大?。┖蚭ps(防止除以零的微小值)作為輸入?yún)?shù)
        def __init__(self, features, eps=1e-6):
            super(LayerNorm, self).__init__()  # 調(diào)用父類nn.Module的構(gòu)造函數(shù)
            self.a_2 = nn.Parameter(torch.ones(features))   # 定義一個(gè)大小為features的一維張量,初始化為全1,并將其設(shè)置為可訓(xùn)練參數(shù)
            self.b_2 = nn.Parameter(torch.zeros(features))  # 定義一個(gè)大小為features的一維張量,初始化為全0,并將其設(shè)置為可訓(xùn)練參數(shù)
            self.eps = eps                   # 將防止除以零的微小值eps保存為類實(shí)例的屬性
    
        # 定義前向傳播函數(shù),輸入?yún)?shù)x是輸入張量
        def forward(self, x):
            mean = x.mean(-1, keepdim=True)  # 計(jì)算輸入x在最后一個(gè)維度上的均值,保持輸出結(jié)果的維度
            std = x.std(-1, keepdim=True)    # 計(jì)算輸入x在最后一個(gè)維度上的標(biāo)準(zhǔn)差,保持輸出結(jié)果的維度
            # 對(duì)輸入x進(jìn)行層歸一化,使用可訓(xùn)練參數(shù)a_2和b_2進(jìn)行縮放和偏移,最后返回歸一化后的結(jié)果
            return self.a_2 * (x - mean) / (std + self.eps) + self.b_2
  2. 上面步驟得到的『輸出結(jié)果output做feed forward』之后,再與『上面步驟的原輸出結(jié)果output』也做相加并歸一化
            forward = self.feed_forward(output)
            block_output = self.dropout(self.norm2(forward + output))
            return block_output

最終這個(gè)編碼器層代碼可以完整的寫為

"""編碼器(Encoder)由自注意力(self-attention)層和前饋神經(jīng)網(wǎng)絡(luò)(feed forward)層組成"""
class EncoderLayer(nn.Module):
    # 初始化函數(shù),接收size(層的維度大小)、self_attn(自注意力層實(shí)例)
    # feed_forward(前饋神經(jīng)網(wǎng)絡(luò)實(shí)例)和dropout(dropout率)作為輸入?yún)?shù)
    def __init__(self, size, self_attn, feed_forward, dropout):
        super(EncoderLayer, self).__init__()      # 調(diào)用父類nn.Module的構(gòu)造函數(shù)
        self.self_attn = self_attn                # 將自注意力層實(shí)例保存為類實(shí)例的屬性
        self.feed_forward = feed_forward          # 將前饋神經(jīng)網(wǎng)絡(luò)實(shí)例保存為類實(shí)例的屬性

        # 創(chuàng)建兩個(gè)具有相同參數(shù)的SublayerConnection實(shí)例(用于殘差連接和層歸一化)
        self.sublayer = clones(SublayerConnection(size, dropout), 2)  
        self.size = size                          # 將層的維度大小保存為類實(shí)例的屬性

    def forward(self, x, mask):
        # 先對(duì)輸入x進(jìn)行自注意力操作
        # 然后將結(jié)果傳遞給第一個(gè)SublayerConnection實(shí)例(包括殘差連接和層歸一化)
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))

        # 將上一步的輸出傳遞給前饋神經(jīng)網(wǎng)絡(luò)
        # 然后將結(jié)果傳遞給第二個(gè)SublayerConnection實(shí)例(包括殘差連接和層歸一化),最后返回結(jié)果
        return self.sublayer[1](x, self.feed_forward)

1.3.1 縮放點(diǎn)積注意力(Scaled Dot-Product Attention)

接下來,先看下縮放點(diǎn)積注意力(Scaled Dot-Product Attention)的整體實(shí)現(xiàn)步驟

從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

  1. 為了計(jì)算每個(gè)單詞與其他單詞之間的相似度,會(huì)拿「每個(gè)單詞/token的q向量包括自身在內(nèi)所有單詞/token的k向量一一做點(diǎn)積(兩個(gè)向量之間的點(diǎn)積結(jié)果可以代表兩個(gè)向量的相似度)

    從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

    對(duì)應(yīng)到矩陣的形式上,則是矩陣Q與K矩陣的轉(zhuǎn)置做相乘
    還是拿上面那個(gè)例子:「我想吃酸菜魚」,則Q乘以K的轉(zhuǎn)置如下圖所示

    從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

    最終得到的矩陣有6行6列,從上往下逐行來看的話,每一個(gè)格子里都會(huì)有一個(gè)數(shù)值,每一個(gè)數(shù)值依次代表:
    ? 單詞我與「我 想 吃 酸 菜 魚」各自的點(diǎn)積結(jié)果或相似度,比如可能是0.3?0.2 0.2?0.1 0.1 0.1,代表編碼1時(shí)放在「我 想 吃 酸 菜 魚」上面的注意力大小
    同時(shí),可以看到模型在對(duì)當(dāng)前位置的信息進(jìn)行編碼時(shí),會(huì)過度的將注意力集中于自身的位置(當(dāng)然 這無可厚非,畢竟自己與自己最相似嘛),而可能忽略了其它位置。很快你會(huì)看到,作者采取的一種解決方案就是采用多頭注意力機(jī)制(Multi-Head Attention)
    ? 想與「我 想 吃 酸 菜 魚」各自的點(diǎn)積結(jié)果或相似度
    ? 吃與「我 想 吃 酸 菜 魚」各自的點(diǎn)積結(jié)果或相似度
    ? 酸與「我 想 吃 酸 菜 魚」各自的點(diǎn)積結(jié)果或相似度
    ? 菜與「我 想 吃 酸 菜 魚」各自的點(diǎn)積結(jié)果或相似度
    ? 魚與「我 想 吃 酸 菜 魚」各自的點(diǎn)積結(jié)果或相似度?
  2. 由于會(huì)隨著dimension的增大而增大,為避免過大,所以除以?,相當(dāng)于對(duì)點(diǎn)積的結(jié)果做下縮放

    從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

    其中,是向量的維度,且,如果只設(shè)置了一個(gè)頭,那就是模型的維度,如果設(shè)置了8個(gè)頭,則,且如果模型的維度是512維,則即等于8

    上面兩步的代碼可以如下編寫
        # torch.matmul是PyTorch庫提供的矩陣乘法函數(shù)
        # 具體操作即是將第一個(gè)矩陣的每一行與第二個(gè)矩陣的每一列進(jìn)行點(diǎn)積(對(duì)應(yīng)元素相乘并求和),得到新矩陣的每個(gè)元素
        scores = torch.matmul(query, key.transpose(-2, -1)) \
                 / math.sqrt(d_k)
  3. 接著使用 Softmax 計(jì)算每一個(gè)單詞對(duì)包括自身在內(nèi)所有單詞的 Attention值,這些值加起來的和為1(相當(dāng)于起到了歸一化的效果)

    從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

    這步對(duì)應(yīng)的代碼為
        # 對(duì) scores 進(jìn)行 softmax 操作,得到注意力權(quán)重 p_attn
        p_attn = F.softmax(scores, dim = -1)
  4. 最后再乘以矩陣,即對(duì)所有values(v1 v2 v3 v4),根據(jù)不同的attention值(???),做加權(quán)平均

    從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

    對(duì)應(yīng)到我想吃酸菜魚這個(gè)例子上,則是

    從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

  5. 最終得到單詞的輸出,如下圖所示(圖中V矩陣的4行分別代表v1 v2 v3 v4):

    從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

    上述兩步對(duì)應(yīng)的代碼為
        # 用注意力權(quán)重 p_attn 對(duì) value 向量進(jìn)行加權(quán)求和,得到最終的輸出
        return torch.matmul(p_attn, value), p_attn

同樣的方法,也可以計(jì)算出,如下圖8所示, b2就是拿q2去對(duì)其他的key做attention,最后再與其他的value值相乘取weighted sum得到,最終每個(gè)單詞都包含了上下文相關(guān)單詞的語義信息,不再只是attention計(jì)算之前,每個(gè)單詞只有它自己的信息,和上下文沒有關(guān)聯(lián)

從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

另外,這里面還有一點(diǎn)值得注意的是,可能有同學(xué)疑問:當(dāng)我們計(jì)算x1與x2、x3、x4的相似度之后,x2會(huì)再與x1、x3、x4再依次計(jì)算一遍相似度,這兩個(gè)過程中,前者算過了x1和x2的相似度,后者則再算一遍x2與x1的相似度,這不是重復(fù)計(jì)算么?其實(shí)不然,這是兩碼事,原因很簡(jiǎn)單,正如你喜歡一個(gè)人 你會(huì)覺得她對(duì)你很重要,但那個(gè)人不一定喜歡你 她不會(huì)覺得你對(duì)她有多重要..

最終,Scaled Dot-Product Attention這部分對(duì)應(yīng)的完整代碼可以寫為

'''計(jì)算“縮放點(diǎn)積注意力'''
# query, key, value 是輸入的向量組
# mask 用于遮掩某些位置,防止計(jì)算注意力
# dropout 用于添加隨機(jī)性,有助于防止過擬合
def attention(query, key, value, mask=None, dropout=None):

    d_k = query.size(-1)  # 獲取 query 向量的最后一個(gè)維度的大小,即詞嵌入的維度

    # 計(jì)算 query 和 key 的點(diǎn)積,并對(duì)結(jié)果進(jìn)行縮放,以減少梯度消失或爆炸的可能性
    scores = torch.matmul(query, key.transpose(-2, -1)) \
             / math.sqrt(d_k)

    # 如果提供了 mask,根據(jù) mask 對(duì) scores 進(jìn)行遮掩
    # 遮掩的具體方法就是設(shè)為一個(gè)很大的負(fù)數(shù)比如-1e9,從而softmax后 對(duì)應(yīng)概率基本為0
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)

    # 對(duì) scores 進(jìn)行 softmax 操作,得到注意力權(quán)重 p_attn
    p_attn = F.softmax(scores, dim = -1)

    # 如果提供了 dropout,對(duì)注意力權(quán)重 p_attn 進(jìn)行 dropout 操作
    if dropout is not None:
        p_attn = dropout(p_attn)

    # 用注意力權(quán)重 p_attn 對(duì) value 向量進(jìn)行加權(quán)求和,得到最終的輸出
    return torch.matmul(p_attn, value), p_attn

1.3.2 多頭注意力(Multi-Head Attention)

先看2個(gè)頭的例子,依然還是通過生成對(duì)應(yīng)的三個(gè)矩陣、、,然后這三個(gè)矩陣再各自乘以兩個(gè)轉(zhuǎn)移矩陣得到對(duì)應(yīng)的分矩陣,如

  • 矩陣對(duì)應(yīng)的兩個(gè)分矩陣、?
  • 矩陣對(duì)應(yīng)的兩個(gè)分矩陣為、
  • 矩陣對(duì)應(yīng)的兩個(gè)分矩陣為、

至于同理,也生成對(duì)應(yīng)的6個(gè)分矩陣、、、、、

接下來編碼時(shí),分兩步

  1. 先與做點(diǎn)積然后乘以,然后再與做點(diǎn)積再乘以,再把這兩個(gè)計(jì)算的結(jié)果相加得到

    從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

  2. 再分別與做點(diǎn)積然后乘以、然后再與做點(diǎn)積再乘以,再把這兩個(gè)計(jì)算的結(jié)果相加得到

    從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

如果是8個(gè)頭呢,計(jì)算步驟上也是一樣的,只是從2個(gè)頭變化到8個(gè)頭而已,最終把每個(gè)頭得到的結(jié)果直接concat,最后經(jīng)過一個(gè)linear變換,得到最終的輸出,整體如下所示

從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

這部分Multi-Head Attention的代碼可以寫為

'''代碼來自nlp.seas.harvard.edu,我針對(duì)每一行代碼、甚至每行代碼中的部分變量都做了詳細(xì)的注釋/解讀'''
class MultiHeadedAttention(nn.Module):
    # 輸入模型的大?。╠_model)和注意力頭的數(shù)量(h)
    def __init__(self, h, d_model, dropout=0.1):
        super(MultiHeadedAttention, self).__init__()
        assert d_model % h == 0  # 確保 d_model 可以被 h 整除

        # 我們假設(shè) d_v(值向量的維度)總是等于 d_k(鍵向量的維度)
        self.d_k = d_model // h      # 計(jì)算每個(gè)注意力頭的維度
        self.h = h                   # 保存注意力頭的數(shù)量
        self.linears = clones(nn.Linear(d_model, d_model), 4)  # 上文解釋過的四個(gè)線性層
        self.attn = None                      # 初始化注意力權(quán)重為 None
        self.dropout = nn.Dropout(p=dropout)  # 定義 dropout 層

    # 實(shí)現(xiàn)多頭注意力的前向傳播
    def forward(self, query, key, value, mask=None):
        if mask is not None:
            # 對(duì)所有 h 個(gè)頭應(yīng)用相同的 mask
            mask = mask.unsqueeze(1)
        nbatches = query.size(0)  # 獲取 batch 的大小

        # 1) 批量執(zhí)行從 d_model 到 h x d_k 的線性投影
        query, key, value = \
            [l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
             for l, x in zip(self.linears, (query, key, value))]

        # 2) 在批量投影的向量上應(yīng)用注意力
        # 具體方法是調(diào)用上面實(shí)現(xiàn)Scaled Dot-Product Attention的attention函數(shù)
        x, self.attn = attention(query, key, value, mask=mask,
                                 dropout=self.dropout)

        # 3) 使用 view 函數(shù)進(jìn)行“拼接concat”,然后做下Linear變換
        x = x.transpose(1, 2).contiguous() \
             .view(nbatches, -1, self.h * self.d_k)
        return self.linears[-1](x)  # 返回多頭注意力的輸出

1.3.3?Position-wise前饋網(wǎng)絡(luò)的實(shí)現(xiàn)

在上文,咱們逐一編碼實(shí)現(xiàn)了embedding、位置編碼、縮放點(diǎn)積/多頭注意力,以及Add和Norm,整個(gè)編碼器部分還剩最后一個(gè)模塊,即下圖框里的Feed Forward Network(簡(jiǎn)稱FFN)

從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

其中包括兩個(gè)線性變換:維度上先擴(kuò)大后縮小,最終輸入和輸出的維數(shù)為,內(nèi)層的維度為,過程中使用ReLU作為激活函數(shù)

從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

雖然線性變換在不同位置上是相同的,但它們?cè)趯优c層之間使用不同的參數(shù),相當(dāng)于使用了兩個(gè)內(nèi)核大小為1的卷積

這部分的代碼可以如下編寫

‘’‘定義一個(gè)名為PositionwiseFeedForward的類,繼承自nn.Module’‘’
class PositionwiseFeedForward(nn.Module):
    # 文檔字符串:實(shí)現(xiàn)FFN方程
    # 初始化方法,接受三個(gè)參數(shù):d_model,d_ff和dropout(默認(rèn)值為0.1)
    def __init__(self, d_model, d_ff, dropout=0.1):
        # 調(diào)用父類nn.Module的初始化方法
        super(PositionwiseFeedForward, self).__init__()  
        self.w_1 = nn.Linear(d_model, d_ff)  # 定義一個(gè)全連接層,輸入維度為d_model,輸出維度為d_ff
        self.w_2 = nn.Linear(d_ff, d_model)  # 定義一個(gè)全連接層,輸入維度為d_ff,輸出維度為d_model
        self.dropout = nn.Dropout(dropout)  # 定義一個(gè)dropout層,dropout概率為傳入的dropout參數(shù)

    # 定義前向傳播方法,接受一個(gè)輸入?yún)?shù)x
    def forward(self, x):
        # 將輸入x通過第一個(gè)全連接層w_1后,經(jīng)過ReLU激活函數(shù),再通過dropout層,最后通過第二個(gè)全連接層w_2,返回最終結(jié)果
        return self.w_2(self.dropout(F.relu(self.w_1(x))))

1.4 對(duì)整個(gè)transformer? block復(fù)制N份最終成整個(gè)encode模塊

N可以等于6或其他數(shù)值

class Encoder(nn.Module):  # 定義一個(gè)名為Encoder的類,它繼承了nn.Module類
    # 一個(gè)具有N層堆疊的核心編碼器
    # 初始化方法,接受兩個(gè)參數(shù):layer(編碼器層的類型)和N(編碼器層的數(shù)量)
    def __init__(self, layer, N):  
        super(Encoder, self).__init__()      # 調(diào)用父類nn.Module的初始化方法
        self.layers = clones(layer, N)       # 創(chuàng)建N個(gè)編碼器層的副本,并將其賦值給實(shí)例變量self.layers
        self.norm = LayerNorm(layer.size)    # 創(chuàng)建一個(gè)LayerNorm層,并將其賦值給實(shí)例變量self.norm
        
    # 定義前向傳播方法,接受兩個(gè)參數(shù):x(輸入數(shù)據(jù))和mask(掩碼)
    def forward(self, x, mask):  
        # 文檔字符串:解釋本方法的功能是將輸入(及其掩碼)依次傳遞給每一層
        for layer in self.layers:        # 遍歷self.layers中的每一個(gè)編碼器層
            x = layer(x, mask)           # 將輸入x和mask傳遞給當(dāng)前編碼器層,并將輸出結(jié)果賦值給x
        return self.norm(x)              # 對(duì)最終的輸出x應(yīng)用LayerNorm層,并將結(jié)果返回

其中的clone函數(shù)的代碼為

def clones(module, N):
    "Produce N identical layers."
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])

第二部分 從零實(shí)現(xiàn)Transformer解碼器模塊

咱們?cè)倩仡櫹聇ransformer的整個(gè)模型架構(gòu),特別是解碼器的部分,畢竟BERT外,GPT等很有影響力的模型都用的transformer decode結(jié)構(gòu)

從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

從底至上,

  • 輸入包括2部分,下方是前一個(gè)time step的輸出的embedding
    再加上一個(gè)表示位置的Positional Encoding
  • 接著是Masked Multi-Head Self-attention,masked字面意思是屏蔽

    從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

    然后做一下Add&Norm
  • 再往上是一個(gè)不帶mask的Multi-Head Attention層,它的Key、Value矩陣使用 Encoder 的編碼信息矩陣,而Query使用上一個(gè) Decoder block 的輸出計(jì)算
    然后再做一下Add&Norm
  • 繼續(xù)往上,經(jīng)過一個(gè)FFN層,也做一下Add&Norm
  • 最后做下linear變換后,通過Softmax 層計(jì)算下一個(gè)翻譯單詞的概率

由于在第一部分介紹過了embedding、positional encoding、FFN、Add&Norm、linear、softmax、multi-head attention,故本部分只重點(diǎn)介紹下Masked Multi-Head Self-attention

2.1?Masked Multi-Head Self-attention

本過程和第一部分介紹的Multi-Head self-attention基本一致,區(qū)別在于加了個(gè)mask機(jī)制

  1. 輸入經(jīng)過embedding + 位置編碼之后,還是乘以三個(gè)不同的權(quán)重矩陣:、、,依次得到三個(gè)不同的矩陣輸入:Q、K、V
  2. Q矩陣乘以K矩陣的轉(zhuǎn)置,得到,注意,緊接著會(huì)再乘以一個(gè)Mask矩陣,得到Masked Attention矩陣
  3. ?Masked Attention矩陣經(jīng)過softmax后,乘以V矩陣得到矩陣
  4. 最終把、拼接之后,再做一個(gè)linear變換得到最終的矩陣

從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

2.2 transformer解碼器架構(gòu)與整體編碼-解碼架構(gòu)的實(shí)現(xiàn)

整個(gè)解碼器架構(gòu)的代碼可以如下編寫『有一點(diǎn)值得注意的是,如下文代碼中所述

  • 在對(duì)輸入x執(zhí)行自注意力計(jì)算并進(jìn)行第一個(gè)子層的處理(帶mask),最后一個(gè)參數(shù)是tgt_mask,即x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))
  • 但對(duì)輸入x執(zhí)行源注意力計(jì)算并進(jìn)行第二個(gè)子層的處理時(shí)(不帶mask),最后一個(gè)參數(shù)是src_mask,即x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask))?
# 定義DecoderLayer類,繼承自PyTorch的nn.Module類
class DecoderLayer(nn.Module):
    # 初始化方法,接收五個(gè)參數(shù):size, self_attn, src_attn, feed_forward, dropout
    # 調(diào)用父類nn.Module的初始化方法
    def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
        super(DecoderLayer, self).__init__()

        # 將size賦值給實(shí)例變量self.size
        self.size = size

        # 將self_attn賦值給實(shí)例變量self.self_attn
        self.self_attn = self_attn

        # 將src_attn賦值給實(shí)例變量self.src_attn
        self.src_attn = src_attn

        # 將feed_forward賦值給實(shí)例變量self.feed_forward
        self.feed_forward = feed_forward

        # 使用SublayerConnection類創(chuàng)建三個(gè)子層,并存儲(chǔ)到實(shí)例變量self.sublayer中
        self.sublayer = clones(SublayerConnection(size, dropout), 3)

    # 定義前向傳播方法,接收四個(gè)參數(shù):x, memory, src_mask, tgt_mask 
    def forward(self, x, memory, src_mask, tgt_mask):

        # 將memory賦值給局部變量m
        m = memory

        # 對(duì)輸入x執(zhí)行自注意力計(jì)算并進(jìn)行第一個(gè)子層的處理
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))

        # 對(duì)輸入x執(zhí)行源注意力計(jì)算并進(jìn)行第二個(gè)子層的處理
        x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask))

        # 對(duì)輸入x執(zhí)行前饋神經(jīng)網(wǎng)絡(luò)計(jì)算并進(jìn)行第三個(gè)子層的處理,然后返回結(jié)果
        return self.sublayer[2](x, self.feed_forward)

且Decoder也是由N=6個(gè)相同層組成

class Decoder(nn.Module):
    "Generic N layer decoder with masking."
    def __init__(self, layer, N):
        super(Decoder, self).__init__()
        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)
        
    def forward(self, x, memory, src_mask, tgt_mask):
        for layer in self.layers:
            x = layer(x, memory, src_mask, tgt_mask)
        return self.norm(x)

最終,整個(gè)transformer完整模型的整體封裝代碼為

def make_model(src_vocab, tgt_vocab, N=6, 
               d_model=512, d_ff=2048, h=8, dropout=0.1):
    "Helper: Construct a model from hyperparameters."
    c = copy.deepcopy
    attn = MultiHeadedAttention(h, d_model)
    ff = PositionwiseFeedForward(d_model, d_ff, dropout)
    position = PositionalEncoding(d_model, dropout)
    model = EncoderDecoder(
        Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
        Decoder(DecoderLayer(d_model, c(attn), c(attn), 
                             c(ff), dropout), N),
        nn.Sequential(Embeddings(d_model, src_vocab), c(position)),
        nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)),
        Generator(d_model, tgt_vocab))
    
    # This was important from their code. 
    # Initialize parameters with Glorot / fan_avg.
    for p in model.parameters():
        if p.dim() > 1:
            nn.init.xavier_uniform(p)
    return model

# Small example model.
tmp_model = make_model(10, 10, 2)
None

2.3 編碼器與解碼器的協(xié)同

當(dāng)我們把編碼器和解碼器組合到一起后,看下它兩是如何一塊協(xié)作的

從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

需要注意的是

  1. Encoder中的Q、K、V全部來自于上一層單元的輸出
    而Decoder只有Q來自于上一個(gè)Decoder單元的輸出,K與V都來自于Encoder最后一層的輸出。也就是說,Decoder是要通過當(dāng)前狀態(tài)與Encoder的輸出算出權(quán)重后(計(jì)算query與各個(gè)key的相似度),最后將Encoder的編碼加權(quán)得到下一層的狀態(tài)

    比如當(dāng)我們要把“Hello Word”翻譯為“你好,世界”時(shí)
    Decoder會(huì)計(jì)算“你好”這個(gè)query分別與“Hello”、“Word”這兩個(gè)key的相似度
    很明顯,“你好”與“Hello”更相似,從而給“Hello”更大的權(quán)重,從而把“你好”對(duì)應(yīng)到“Hello”,達(dá)到的效果就是“Hello”翻譯為“你好”
  2. 且在解碼器中因?yàn)榧恿薽asked機(jī)制,自注意力層只允許關(guān)注已輸出位置的信息,實(shí)現(xiàn)方法是在自注意力層的softmax之前進(jìn)行mask,將未輸出位置的權(quán)重設(shè)置為一個(gè)非常大的負(fù)數(shù)(進(jìn)一步softmax之后基本變?yōu)?,相當(dāng)于直接屏蔽了未輸出位置的信息)

第三部分?Transformer的整個(gè)訓(xùn)練過程:預(yù)處理與迭代

3.1 預(yù)處理階段:創(chuàng)建詞匯表

具體實(shí)現(xiàn)時(shí),先創(chuàng)建批次和掩碼

class Batch:
def __init__(self, src, trg=None, pad=0):
    self.src = src  # 輸入數(shù)據(jù)源(通常為源語言)
    self.src_mask = (src != pad).unsqueeze(-2)  # 創(chuàng)建源語言的掩碼,用于忽略填充部分
    if trg is not None:  # 如果目標(biāo)語言數(shù)據(jù)存在
        self.trg = trg[:, :-1]   # 目標(biāo)語言數(shù)據(jù),去掉最后一個(gè)詞
        self.trg_y = trg[:, 1:]  # 目標(biāo)語言數(shù)據(jù),去掉第一個(gè)詞
        self.trg_mask = \
            self.make_std_mask(self.trg, pad)          # 創(chuàng)建目標(biāo)語言的掩碼,用于忽略填充部分和未來詞匯
        self.ntokens = (self.trg_y != pad).data.sum()  # 計(jì)算目標(biāo)語言中非填充詞的數(shù)量

@staticmethod
def make_std_mask(tgt, pad):
    "Create a mask to hide padding and future words."
    tgt_mask = (tgt != pad).unsqueeze(-2)                      # 創(chuàng)建目標(biāo)語言的掩碼,用于忽略填充部分
    tgt_mask = tgt_mask & Variable(
        subsequent_mask(tgt.size(-1)).type_as(tgt_mask.data))  # 使用子掩碼屏蔽未來詞匯
    return tgt_mask                                            # 返回完整的目標(biāo)語言掩碼

其中,subsequent_mask的實(shí)現(xiàn)如下所示

def subsequent_mask(size):
    "Mask out subsequent positions."
    attn_shape = (1, size, size)
    subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
    return torch.from_numpy(subsequent_mask) == 0

3.2 訓(xùn)練三部曲:隨機(jī)初始化、損失函數(shù)、反向傳播

接下來,我們創(chuàng)建一個(gè)通用的訓(xùn)練和得分函數(shù)來跟蹤損失。我們傳入一個(gè)通用的損失計(jì)算函數(shù),它也處理參數(shù)更新

def run_epoch(data_iter, model, loss_compute):
start = time.time()        # 記錄當(dāng)前時(shí)間
total_tokens = 0           # 初始化總tokens計(jì)數(shù)
total_loss = 0             # 初始化總損失
tokens = 0                 # 初始化tokens計(jì)數(shù)

# 遍歷數(shù)據(jù)集中的每個(gè)批次
for i, batch in enumerate(data_iter):     
    # 對(duì)每個(gè)批次進(jìn)行前向傳播                
    out = model.forward(batch.src, batch.trg, 
                        batch.src_mask, batch.trg_mask)   

    # 計(jì)算每個(gè)批次的損失
    loss = loss_compute(out, batch.trg_y, batch.ntokens)  
    
    # 累加損失
    total_loss += loss  
    total_tokens += batch.ntokens      # 累加tokens
    tokens += batch.ntokens            # 累加tokens

    # 每50個(gè)批次進(jìn)行一次日志記錄
    if i % 50 == 1:  
        elapsed = time.time() - start  # 計(jì)算已用時(shí)間

        # 輸出當(dāng)前批次,損失和每秒處理的tokens
        print("Epoch Step: %d Loss: %f Tokens per Sec: %f" %
                (i, loss / batch.ntokens, tokens / elapsed))  
        start = time.time()            # 重置開始時(shí)間
        tokens = 0                     # 重置tokens計(jì)數(shù)

return total_loss / total_tokens       # 返回平均損失

下面這段代碼定義了一個(gè)名為 SimpleLossCompute 的類,實(shí)現(xiàn)了簡(jiǎn)單的損失計(jì)算和訓(xùn)練函數(shù)

  • 在調(diào)用該類的實(shí)例時(shí),輸入預(yù)測(cè)輸出、目標(biāo)輸出和規(guī)范化因子,計(jì)算損失值并進(jìn)行梯度更新
  • 如果提供了優(yōu)化器,還會(huì)更新模型參數(shù)和清空梯度緩存
# 定義 SimpleLossCompute 類,實(shí)現(xiàn)簡(jiǎn)單的損失計(jì)算和訓(xùn)練函數(shù)
class SimpleLossCompute:
    # 初始化 SimpleLossCompute 類的實(shí)例
    def __init__(self, generator, criterion, opt=None):
        self.generator = generator    # 生成器,用于預(yù)測(cè)輸出
        self.criterion = criterion    # 損失函數(shù),如交叉熵?fù)p失
        self.opt = opt                # 優(yōu)化器,如 Adam

    # 定義調(diào)用 SimpleLossCompute 類實(shí)例時(shí)的操作
    def __call__(self, x, y, norm):
        x = self.generator(x)         # 生成預(yù)測(cè)輸出
        # 計(jì)算損失,這里需要將預(yù)測(cè)輸出和目標(biāo)輸出轉(zhuǎn)換為合適的形狀
        loss = self.criterion(x.contiguous().view(-1, x.size(-1)), 
                              y.contiguous().view(-1)) / norm
        loss.backward()               # 計(jì)算梯度
        if self.opt is not None:      # 如果提供了優(yōu)化器
            self.opt.step()           # 更新模型參數(shù)
            self.opt.optimizer.zero_grad()  # 清空梯度緩存
        return loss.data[0] * norm    # 返回?fù)p失值乘以規(guī)范化因子(實(shí)際損失值)

3.2.1 Adam優(yōu)化器:自動(dòng)調(diào)整學(xué)習(xí)率并具有動(dòng)量效應(yīng)

優(yōu)化器(optimizer)經(jīng)常用于在訓(xùn)練過程中更新模型參數(shù)以最小化損失函數(shù),而Adam(Adaptive Moment Estimation)是一種常用的優(yōu)化器,它結(jié)合了兩種傳統(tǒng)優(yōu)化算法的優(yōu)點(diǎn):Momentum和RMSprop

為了通俗易懂地理解Adam,可以將其比作一個(gè)賽車手。訓(xùn)練模型就像是找到一輛賽車在賽道上的最佳行駛速度和路徑,以達(dá)到最快的速度并取得優(yōu)異的成績(jī)。在這個(gè)過程中,速度的調(diào)整(即學(xué)習(xí)率)非常重要

  1. 首先,Adam像Momentum一樣,具有動(dòng)量效應(yīng)。這意味著賽車手(模型)會(huì)積累動(dòng)量,使其在下坡時(shí)更快,而在上坡時(shí)減速。這有助于模型更快地穿越平坦區(qū)域,并避免在最低點(diǎn)附近擺動(dòng)

  2. 其次,Adam像RMSprop一樣,會(huì)自適應(yīng)地調(diào)整每個(gè)參數(shù)的學(xué)習(xí)率。在我們的賽車比喻中,這就像賽車手會(huì)針對(duì)每個(gè)輪胎的摩擦系數(shù)(賽道狀況)做出相應(yīng)的速度調(diào)整。這有助于模型更快地收斂到最優(yōu)解

總之,Adam可以自動(dòng)調(diào)整學(xué)習(xí)率,并具有動(dòng)量效應(yīng)??偟膩碚f,它能幫助我們的“賽車手”在不同的賽道狀況下更快地找到最佳行駛速度和路徑,從而更快地訓(xùn)練出高效的模型

transformer原始論文便選擇的Adam作為優(yōu)化器,其參數(shù)為,和,根據(jù)以下公式,我們?cè)谟?xùn)練過程中改變了學(xué)習(xí)率:

從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

在預(yù)熱中隨步數(shù)線性地增加學(xué)習(xí)速率,并且此后與步數(shù)的反平方根成比例地減小它,設(shè)置預(yù)熱步數(shù)為4000

我們來看下具體的編碼實(shí)現(xiàn)。下面這段代碼定義了一個(gè)名為 NoamOpt 的類,實(shí)現(xiàn)了一種自適應(yīng)學(xué)習(xí)率調(diào)整策略,該策略在訓(xùn)練 Transformer 模型時(shí)常用。在訓(xùn)練的前幾個(gè)步驟(預(yù)熱期)中,學(xué)習(xí)率會(huì)線性增長(zhǎng),之后學(xué)習(xí)率會(huì)隨著步數(shù)的增加而逐漸降低。這種策略有助于模型在訓(xùn)練初期更快地收斂,同時(shí)在訓(xùn)練后期保持較低的學(xué)習(xí)率,有利于模型的穩(wěn)定訓(xùn)練。

# 定義 NoamOpt 類,實(shí)現(xiàn)自適應(yīng)學(xué)習(xí)率調(diào)整策略
class NoamOpt:
    # 初始化 NoamOpt 類的實(shí)例
    def __init__(self, model_size, factor, warmup, optimizer):
        self.optimizer = optimizer      # 優(yōu)化器對(duì)象(如 Adam)
        self._step = 0                  # 記錄優(yōu)化步數(shù)
        self.warmup = warmup            # 預(yù)熱步數(shù)
        self.factor = factor            # 縮放因子
        self.model_size = model_size    # 模型維度大小
        self._rate = 0                  # 初始學(xué)習(xí)率

    # 更新模型參數(shù)和學(xué)習(xí)率
    def step(self):
        self._step += 1                            # 優(yōu)化步數(shù)加 1
        rate = self.rate()                         # 計(jì)算當(dāng)前學(xué)習(xí)率
        for p in self.optimizer.param_groups:      # 更新優(yōu)化器中的學(xué)習(xí)率
            p['lr'] = rate
        self._rate = rate                          # 存儲(chǔ)當(dāng)前學(xué)習(xí)率
        self.optimizer.step()                      # 更新模型參數(shù)

    # 計(jì)算當(dāng)前步數(shù)的學(xué)習(xí)率
    def rate(self, step=None):
        if step is None:                           # 如果未提供步數(shù),使用當(dāng)前步數(shù)
            step = self._step
        return self.factor * \
            (self.model_size ** (-0.5) *           # 計(jì)算學(xué)習(xí)率公式中的模型維度項(xiàng)
            min(step ** (-0.5), step * self.warmup ** (-1.5)))  # 計(jì)算學(xué)習(xí)率公式中的最小值項(xiàng)

# 定義用于獲取 NoamOpt 類實(shí)例的函數(shù)
def get_std_opt(model):
    return NoamOpt(model.src_embed[0].d_model, 2, 4000,
                   torch.optim.Adam(model.parameters(), lr=0, betas=(0.9, 0.98), eps=1e-9))

最后總結(jié)一下Transformer的影響力

  • OpenAI基于它發(fā)展出了GPT,并不斷迭代出GPT2、GPT3、GPT3.5及火爆全球的 ChatGPT
  • Google則基于它發(fā)展出了在ChatGPT出現(xiàn)之前統(tǒng)治NLP各大任務(wù)的BERT,多好的青春年華!

第四部分?Hugging face社區(qū)實(shí)現(xiàn)的Transformers庫的整體解讀

目前絕大部分有影響力的大模型基本都基于transformer的架構(gòu) (這個(gè)頁面底部可以看到基于transformer的200多個(gè)有影響力的模型),既然基于transformer便得實(shí)現(xiàn)transformer

  • 而上文更多只是為了方便理解原理而做的簡(jiǎn)易版的實(shí)現(xiàn)
  • 實(shí)際運(yùn)用時(shí)基本都用的Hugging face社區(qū)實(shí)現(xiàn)的Transformers庫 「比如此文的2.2節(jié):Stanford Alpaca的微調(diào)拆解——見證LLM微調(diào)的一般模式?」:?https://github.com/huggingface/transformers/tree/main/src/transformers,功能強(qiáng)大且便捷

然要分析這么一個(gè)大庫是不容易的,如下圖所示,包括分詞等等各種功能

從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答

且光trainer.py(https://github.com/huggingface/transformers/blob/main/src/transformers/trainer.py)這一個(gè)項(xiàng)目文件的實(shí)現(xiàn)就有3858行

4.1 逐行解讀:3858行的transformers/src/transformers/trainer.py

4.1.1 導(dǎo)入一系列Python/numpy/torch里面的各種庫

  • # coding=utf-8:這行定義了此腳本文件的編碼格式為utf-8
  • 2-12. 這些行是關(guān)于版權(quán)和Apache許可證的聲明。代碼可以在遵守這些許可證條款的情況下被使用
    這是一個(gè)模塊級(jí)的docstring,解釋了這個(gè)模塊的主要功能,即創(chuàng)建一個(gè)可以輕松訓(xùn)練或微調(diào)HuggingFace Transformers模型的Trainer類
  • 15-30. 導(dǎo)入了一些常用的Python標(biāo)準(zhǔn)庫,包括對(duì)文件、操作系統(tǒng)、時(shí)間、警告等的操作,以及一些集合和類型檢查的工具
  • 33-36. 這里首先導(dǎo)入了和模型訓(xùn)練相關(guān)的集成工具。這些工具包括了報(bào)告集成回調(diào)、超參數(shù)、判斷fairscale(一個(gè)優(yōu)化PyTorch模型訓(xùn)練的庫)是否可用等
  • 39-45. 導(dǎo)入了numpy和torch以及其分布式模塊,這些是進(jìn)行深度學(xué)習(xí)計(jì)算的基礎(chǔ)庫。同時(shí)也導(dǎo)入了huggingface_hub的Repository和create_repo,它們是用于與HuggingFace模型Hub進(jìn)行交互的工具
  • 46-51. 導(dǎo)入了torch內(nèi)的nn模塊,以及torch.utils.data模塊中的DataLoader, Dataset, RandomSampler, SequentialSampler,這些是用于處理神經(jīng)網(wǎng)絡(luò)和數(shù)據(jù)的基本工具
    這行導(dǎo)入了當(dāng)前模塊的版本信息
  • 54-87. 這些行導(dǎo)入了許多與模型訓(xùn)練相關(guān)的工具和函數(shù),包括預(yù)訓(xùn)練模型和配置,數(shù)據(jù)整理,調(diào)試工具,優(yōu)化器,層標(biāo)準(zhǔn)化,分布式訓(xùn)練工具(比如deepspeed),回調(diào)函數(shù)等
  • 89-132. 這些行導(dǎo)入了一些與訓(xùn)練有關(guān)的工具和函數(shù),包括分布式策略,內(nèi)存跟蹤,優(yōu)化器名稱,訓(xùn)練參數(shù)等
  • 134-173. 這些行導(dǎo)入了一些工具和函數(shù),主要用于處理適配器,配置,權(quán)重,日志,數(shù)據(jù)集,設(shè)備檢測(cè)等
  • 175-177. 定義了默認(rèn)的回調(diào)函數(shù)列表和默認(rèn)的進(jìn)度回調(diào)函數(shù)
  • 179-189. 根據(jù)環(huán)境的可用性,可能會(huì)導(dǎo)入和Notebook, Apex, 數(shù)據(jù)集, Torch TPU, Fairscale相關(guān)的模塊
  • 191-200. 如果SageMaker模型并行可用,那么導(dǎo)入與其相關(guān)的模塊,并檢查其版本
    如果安全張量庫可用,就導(dǎo)入它
    如果性能分析工具PEFT可用,就導(dǎo)入它
  • 206-217. 如果Accelerate可用,那么導(dǎo)入與其相關(guān)的模塊,并檢查其版本
    使用TYPE_CHECKING做類型檢查,如果是,就導(dǎo)入optuna模塊
    設(shè)置了logger用于日志記錄
  • 224-230. 定義了一些常量,它們是用于保存訓(xùn)練時(shí)的參數(shù),狀態(tài),優(yōu)化器,調(diào)度器,梯度縮放器等信息的文件名

4.1.2?定義class Trainer,先做一些初始化設(shè)置

然后定義class Trainer,逐一實(shí)現(xiàn)了如下函數(shù)

  • func __init__

    硬件配置:代碼首先判斷是否需要將模型放置在特定的設(shè)備(如 GPU 或 CPU)上。一些特殊情況,如使用了模型并行、深度學(xué)習(xí)庫DeepSpeed、完全bf16或fp16評(píng)估、數(shù)據(jù)并行處理和完全分片的數(shù)據(jù)并行處理,都會(huì)對(duì)這個(gè)決定產(chǎn)生影響。

    數(shù)據(jù)預(yù)處理:然后,代碼會(huì)創(chuàng)建一個(gè)用于數(shù)據(jù)處理的 data_collator,這個(gè) data_collator 會(huì)根據(jù)是否有分詞器(tokenizer)來選擇默認(rèn)的數(shù)據(jù)整理器。這個(gè)整理器將在訓(xùn)練和驗(yàn)證過程中用于整理數(shù)據(jù)。

    優(yōu)化器與學(xué)習(xí)率調(diào)度器:然后,代碼檢查了優(yōu)化器和學(xué)習(xí)率調(diào)度器是否已經(jīng)設(shè)置,并在必要時(shí)進(jìn)行了一些配置。在這里,還進(jìn)行了一些錯(cuò)誤檢查,以防模型和優(yōu)化器參數(shù)不在同一個(gè)設(shè)備上,或者優(yōu)化器與使用的并行處理庫(如Fairscale、Deepspeed或PyTorch FSDP)不兼容。

    回調(diào)函數(shù):最后,代碼初始化了一些默認(rèn)的回調(diào)函數(shù),并在需要時(shí)創(chuàng)建了一個(gè)遠(yuǎn)程倉庫的克隆和輸出目錄。這些回調(diào)函數(shù)將在訓(xùn)練過程中的不同時(shí)間點(diǎn)被調(diào)用,可以用來做一些自定義的操作,比如在每個(gè) epoch 結(jié)束后保存模型。

    混合精度設(shè)置:代碼首先檢查是否需要使用混合精度訓(xùn)練(即使用 fp16 或 bf16)。如果需要,根據(jù)后端類型(例如 "cuda_amp" 或 "cpu_amp"),選擇正確的混合精度訓(xùn)練策略。在這里,也進(jìn)行了一些錯(cuò)誤檢查,以防混合精度訓(xùn)練與使用的并行處理庫(如SageMaker Model Parallelism)不兼容。

    標(biāo)簽平滑:然后,代碼檢查是否需要使用標(biāo)簽平滑(一種常見的防止過擬合的技巧),并在需要時(shí)設(shè)置相應(yīng)的對(duì)象。

    訓(xùn)練器狀態(tài)和控制:接下來,代碼初始化了訓(xùn)練器的狀態(tài)和控制對(duì)象,這兩個(gè)對(duì)象將在訓(xùn)練過程中用于跟蹤訓(xùn)練的進(jìn)展和控制訓(xùn)練的流程。

    其他設(shè)置:最后,代碼還進(jìn)行了一些其他的設(shè)置,比如初始化內(nèi)存跟蹤器,設(shè)置訓(xùn)練批次的大小,以及處理一些特定的訓(xùn)練參數(shù)(如 "torch_compile")

  • func add_callback
  • func pop_callback
  • func remove_callback
  • func _move_model_to_device
  • func _set_signature_columns_if_needed
  • func _remove_unused_columns
  • func _get_collator_with_removed_columns

4.1.3 訓(xùn)練數(shù)據(jù)集、驗(yàn)證數(shù)據(jù)集相關(guān)

  • func _get_train_sampler
    # 獲取訓(xùn)練采樣器
       def _get_train_sampler(self) -> Optional[torch.utils.data.Sampler]: 
            if self.train_dataset is None or not has_length(self.train_dataset): # 如果沒有訓(xùn)練數(shù)據(jù)集或訓(xùn)練數(shù)據(jù)集沒有長(zhǎng)度,返回None
                return None
    
            # 創(chuàng)建采樣器
            if self.args.group_by_length: # 如果參數(shù)設(shè)定了按長(zhǎng)度分組
                if is_datasets_available() and isinstance(self.train_dataset, datasets.Dataset): # 如果有datasets庫并且訓(xùn)練數(shù)據(jù)集是datasets.Dataset的實(shí)例
                    lengths = (
                        self.train_dataset[self.args.length_column_name]
                        if self.args.length_column_name in self.train_dataset.column_names
                        else None
                    ) # 如果訓(xùn)練數(shù)據(jù)集中有長(zhǎng)度列名,獲取長(zhǎng)度,否則長(zhǎng)度為None
                else:
                    lengths = None # 否則,長(zhǎng)度為None
                model_input_name = self.tokenizer.model_input_names[0] if self.tokenizer is not None else None # 獲取模型輸入名稱
                return LengthGroupedSampler( # 返回長(zhǎng)度分組采樣器
                    self.args.train_batch_size * self.args.gradient_accumulation_steps,
                    dataset=self.train_dataset,
                    lengths=lengths,
                    model_input_name=model_input_name,
                )
    
            else:
                return RandomSampler(self.train_dataset) # 否則,返回隨機(jī)采樣器
  • func get_train_dataloader
    # 獲取訓(xùn)練數(shù)據(jù)的 DataLoader
        def get_train_dataloader(self) -> DataLoader:
            """
            返回訓(xùn)練[`~torch.utils.data.DataLoader`]。
    
            如果`train_dataset`未實(shí)現(xiàn)`__len__`,將不使用采樣器,
            否則,使用適應(yīng)于分布式訓(xùn)練的隨機(jī)采樣器。
    
            如果想注入一些自定義行為,可以在子類中重寫此方法。
            """
            # 如果訓(xùn)練集為空,則拋出 ValueError
            if self.train_dataset is None:
                raise ValueError("Trainer: training requires a train_dataset.")
    
            # 創(chuàng)建訓(xùn)練數(shù)據(jù)集和數(shù)據(jù)整理器
            train_dataset = self.train_dataset
            data_collator = self.data_collator
            # 如果訓(xùn)練集是數(shù)據(jù)集的實(shí)例,移除未使用的列
            if is_datasets_available() and isinstance(train_dataset, datasets.Dataset):
                train_dataset = self._remove_unused_columns(train_dataset, description="training")
            # 否則,使用數(shù)據(jù)整理器移除未使用的列
            else:
                data_collator = self._get_collator_with_removed_columns(data_collator, description="training")
    
            # 定義 DataLoader 參數(shù)
            dataloader_params = {
                "batch_size": self._train_batch_size,
                "collate_fn": data_collator,
                "num_workers": self.args.dataloader_num_workers,
                "pin_memory": self.args.dataloader_pin_memory,
            }
    
            # 如果訓(xùn)練集不是迭代的數(shù)據(jù)集,設(shè)定采樣器和其他參數(shù)
            if not isinstance(train_dataset, torch.utils.data.IterableDataset):
                dataloader_params["sampler"] = self._get_train_sampler()
                dataloader_params["drop_last"] = self.args.dataloader_drop_last
                dataloader_params["worker_init_fn"] = seed_worker
    
            # 返回由 accelerator 處理過的 DataLoader
            return self.accelerator.prepare(DataLoader(train_dataset, **dataloader_params))
  • func _get_eval_sampler
        # 獲取評(píng)估數(shù)據(jù)的采樣器
        def _get_eval_sampler(self, eval_dataset: Dataset) -> Optional[torch.utils.data.Sampler]:
            # 廢棄的代碼
            if self.args.use_legacy_prediction_loop:
                # 如果是在TPU上運(yùn)行,返回 SequentialDistributedSampler
                if is_torch_tpu_available():
                    return SequentialDistributedSampler(
                        eval_dataset, num_replicas=xm.xrt_world_size(), rank=xm.get_ordinal()
                    )
                # 如果是在Sagemaker多處理器環(huán)境中運(yùn)行,返回SequentialDistributedSampler
                elif is_sagemaker_mp_enabled():
                    return SequentialDistributedSampler(
                        eval_dataset,
                        num_replicas=smp.dp_size(),
                        rank=smp.dp_rank(),
                        batch_size=self.args.per_device_eval_batch_size,
                    )
                # 其他情況下,返回順序采樣器
                else:
                    return SequentialSampler(eval_dataset)
    
            # 如果是單機(jī)環(huán)境,返回順序采樣器;否則,返回 None
            if self.args.world_size <= 1:
                return SequentialSampler(eval_dataset)
            else:
                return None
  • func get_eval_dataloader
     # 獲取評(píng)估數(shù)據(jù)的 DataLoader
        def get_eval_dataloader(self, eval_dataset: Optional[Dataset] = None) -> DataLoader:
            """
            返回評(píng)估[`~torch.utils.data.DataLoader`]。
    
            如果想注入一些自定義行為,可以在子類中重寫此方法。
    
            Args:
                eval_dataset (`torch.utils.data.Dataset`, *optional*):
                    如果提供,將覆蓋`self.eval_dataset`。如果它是一個(gè)[`~datasets.Dataset`],自動(dòng)刪除模型的`forward()`
                    方法不接受的列。必須實(shí)現(xiàn)`__len__`。
            """
            # 如果評(píng)估集為空,則拋出 ValueError
            if eval_dataset is None and self.eval_dataset is None:
                raise ValueError("Trainer: evaluation requires an eval_dataset.")
            # 創(chuàng)建評(píng)估數(shù)據(jù)集和數(shù)據(jù)整理器
            eval_dataset = eval_dataset if eval_dataset is not None else self.eval_dataset
            data_collator = self.data_collator
    
            # 如果評(píng)估集是數(shù)據(jù)集的實(shí)例,移除未使用的列
            if is_datasets_available() and isinstance(eval_dataset, datasets.Dataset):
                eval_dataset = self._remove_unused_columns(eval_dataset, description="evaluation")
            # 否則,使用數(shù)據(jù)整理器移除未使用的列
            else:
                data_collator = self._get_collator_with_removed_columns(data_collator, description="evaluation")
    
            # 定義 DataLoader 參數(shù)
            dataloader_params = {
                "batch_size": self.args.eval_batch_size,
                "collate_fn": data_collator,
                "num_workers": self.args.dataloader_num_workers,
                "pin_memory": self.args.dataloader_pin_memory,
            }
    
            # 如果評(píng)估集不是迭代的數(shù)據(jù)集,設(shè)定采樣器和其他參數(shù)
            if not isinstance(eval_dataset, torch.utils.data.IterableDataset):
                dataloader_params["sampler"] = self._get_eval_sampler(eval_dataset)
                dataloader_params["drop_last"] = self.args.dataloader_drop_last
    
            # 返回由 accelerator 處理過的 DataLoader
            return self.accelerator.prepare(DataLoader(eval_dataset, **dataloader_params))
  • func get_test_dataloader
    def get_test_dataloader(self, test_dataset: Dataset) -> DataLoader:
        """
        返回測(cè)試集的數(shù)據(jù)加載器 [`~torch.utils.data.DataLoader`]
        如果需要插入一些自定義行為,可以在子類中重寫此方法
        Args:
            test_dataset (`torch.utils.data.Dataset`, *optional*):
                要使用的測(cè)試數(shù)據(jù)集。如果它是一個(gè) [`~datasets.Dataset`],則自動(dòng)刪除 `model.forward()` 方法不接受的列。它必須實(shí)現(xiàn) `__len__`
        """
        data_collator = self.data_collator  # 獲取數(shù)據(jù)處理器
    
        # 如果datasets庫可用且test_dataset是datasets.Dataset類型,移除不必要的列
        if is_datasets_available() and isinstance(test_dataset, datasets.Dataset):
            test_dataset = self._remove_unused_columns(test_dataset, description="test")
        else:
            data_collator = self._get_collator_with_removed_columns(data_collator, description="test")
    
        # 定義數(shù)據(jù)加載器參數(shù)
        dataloader_params = {
            "batch_size": self.args.eval_batch_size,      # 批大小
            "collate_fn": data_collator,                  # 數(shù)據(jù)處理函數(shù)
            "num_workers": self.args.dataloader_num_workers,    # 工作線程數(shù)量
            "pin_memory": self.args.dataloader_pin_memory,      # 是否將數(shù)據(jù)加載器的數(shù)據(jù)放在固定的內(nèi)存區(qū)域
        }
    
        # 如果test_dataset不是可迭代數(shù)據(jù)集,添加采樣器和drop_last參數(shù)
        if not isinstance(test_dataset, torch.utils.data.IterableDataset):
            dataloader_params["sampler"] = self._get_eval_sampler(test_dataset)  # 添加采樣器
            dataloader_params["drop_last"] = self.args.dataloader_drop_last  # 是否丟棄最后不完整的批次
    
        # 返回加速器準(zhǔn)備好的數(shù)據(jù)加載器
        return self.accelerator.prepare(DataLoader(test_dataset, **dataloader_params))

4.1.4 一系列優(yōu)化器函數(shù)的實(shí)現(xiàn)

  • func create_optimizer_and_scheduler
    def create_optimizer_and_scheduler(self, num_training_steps: int):
        """
        設(shè)置優(yōu)化器和學(xué)習(xí)率調(diào)度器
    
        我們提供一個(gè)合理的默認(rèn)值,工作得很好。如果你想使用其他的,你可以在Trainer的init中通過`optimizers`傳遞一個(gè)元組,或者在子類中重寫此方法(或`create_optimizer`和/或`create_scheduler`)。
        """
        self.create_optimizer()      # 創(chuàng)建優(yōu)化器
        # 如果SageMaker版本大于等于1.10且啟用了fp16,解包優(yōu)化器
        if IS_SAGEMAKER_MP_POST_1_10 and smp.state.cfg.fp16:
            optimizer = self.optimizer.optimizer
        else:
            optimizer = self.optimizer
        self.create_scheduler(num_training_steps=num_training_steps, optimizer=optimizer)            # 創(chuàng)建學(xué)習(xí)率調(diào)度器
  • func create_optimizer
    def create_optimizer(self):
        """
        設(shè)置優(yōu)化器。
    
        我們提供一個(gè)合理的默認(rèn)值,工作得很好。如果你想使用其他的,你可以在Trainer的init中通過`optimizers`傳遞一個(gè)元組,或者在子類中重寫此方法。
        """
        # 根據(jù)是否啟用了SageMaker模型并行,選擇不同的模型
        opt_model = self.model_wrapped if is_sagemaker_mp_enabled() else self.model
    
        # 如果優(yōu)化器為空,初始化一個(gè)新的優(yōu)化器
        if self.optimizer is None:
            # 獲取待優(yōu)化參數(shù),并區(qū)分是否需要權(quán)重衰減
            decay_parameters = get_parameter_names(opt_model, ALL_LAYERNORM_LAYERS)
            decay_parameters = [name for name in decay_parameters if "bias" not in name]
            optimizer_grouped_parameters = [
                {
                    "params": [
                        p for n, p in opt_model.named_parameters() if (n in decay_parameters and p.requires_grad)
                    ],
                    "weight_decay": self.args.weight_decay,  # 權(quán)重衰減
                },
                {
                    "params": [
                        p for n, p in opt_model.named_parameters() if (n not in decay_parameters and p.requires_grad)
                    ],
                    "weight_decay": 0.0,  # 不需要權(quán)重衰減
                },
            ]
    
            # 獲取優(yōu)化器類和參數(shù)
            optimizer_cls, optimizer_kwargs = Trainer.get_optimizer_cls_and_kwargs(self.args)
    
            # 如果啟用了簡(jiǎn)單的分片DDP,使用OSS作為優(yōu)化器,否則使用獲取的優(yōu)化器
            if self.sharded_ddp == ShardedDDPOption.SIMPLE:
                self.optimizer = OSS(
                    params=optimizer_grouped_parameters,
                    optim=optimizer_cls,
                    **optimizer_kwargs,
                )
            else:
                self.optimizer = optimizer_cls(optimizer_grouped_parameters, **optimizer_kwargs)
                if optimizer_cls.__name__ == "Adam8bit":
                    import bitsandbytes
    
                    manager = bitsandbytes.optim.GlobalOptimManager.get_instance()
    
                    skipped = 0
                    for module in opt_model.modules():
                        if isinstance(module, nn.Embedding):
                            skipped += sum({p.data_ptr(): p.numel() for p in module.parameters()}.values())
                            logger.info(f"skipped {module}: {skipped/2**20}M params")
                            manager.register_module_override(module, "weight", {"optim_bits": 32})
                            logger.debug(f"bitsandbytes: will optimize {module} in fp32")
                    logger.info(f"skipped: {skipped/2**20}M params")
    
        # 如果啟用了SageMaker模型并行,使用SageMaker的分布式優(yōu)化器
        if is_sagemaker_mp_enabled():
            self.optimizer = smp.DistributedOptimizer(self.optimizer)
    
        return self.optimizer
  • func get_optimizer_cls_and_kwargs

根據(jù)提供的參數(shù),選擇并配置合適的優(yōu)化器,以便在模型訓(xùn)練中使用

  1. 首先,從給定的訓(xùn)練參數(shù)中提取優(yōu)化器參數(shù),并將它們存儲(chǔ)在一個(gè)字典中。
  2. 根據(jù)訓(xùn)練參數(shù)設(shè)定初始學(xué)習(xí)率。
  3. 針對(duì)Adam優(yōu)化器設(shè)定一組基本參數(shù)(betas和eps)。
  4. 接著,根據(jù)優(yōu)化器的類型(存儲(chǔ)在args.optim中),選擇合適的優(yōu)化器類,并更新優(yōu)化器參數(shù)。優(yōu)化器類型可能有很多種,例如Adafactor,AdamW,SGD,Adagrad等等。
  5. 該函數(shù)還支持多種不同的AdamW優(yōu)化器,例如來自HuggingFace,Torch,Apex等的版本,并根據(jù)需要更新參數(shù)。其中,對(duì)于一些特定的優(yōu)化器類型(例如,AdamW的torch_xla版本或apex的FusedAdam版本),如果相關(guān)的庫沒有被正確安裝,那么將會(huì)拋出錯(cuò)誤信息。
  6. 該函數(shù)還支持處理來自bitsandbytes庫中的優(yōu)化器(例如,AdamW,Lion等),并能夠根據(jù)參數(shù)調(diào)整其配置(例如,是否使用分頁式的優(yōu)化器,是否使用8位優(yōu)化器等)。
  7. 對(duì)于一些其他特定類型的優(yōu)化器(例如,來自torchdistx庫的AnyPrecisionAdamW優(yōu)化器),它還支持更多的參數(shù)設(shè)置。
  8. 最后,如果給定的優(yōu)化器名稱并沒有被程序識(shí)別,那么將會(huì)拋出一個(gè)ValueError。
  9. 在選擇和配置完優(yōu)化器后,該函數(shù)會(huì)返回優(yōu)化器類和優(yōu)化器參數(shù)

4.1.5 學(xué)習(xí)率相關(guān)函數(shù)的實(shí)現(xiàn)

  • func create_scheduler
        # 定義創(chuàng)建學(xué)習(xí)率調(diào)度器的函數(shù)
        def create_scheduler(self, num_training_steps: int, optimizer: torch.optim.Optimizer = None):
            """
            設(shè)置調(diào)度器。訓(xùn)練器的優(yōu)化器必須在調(diào)用此方法之前已經(jīng)設(shè)置好,或者作為參數(shù)傳遞。
    
            Args:
                num_training_steps (int): 要進(jìn)行的訓(xùn)練步數(shù)。
            """
            # 如果調(diào)度器還未設(shè)置
            if self.lr_scheduler is None:
                # 使用 get_scheduler 函數(shù)創(chuàng)建調(diào)度器
                self.lr_scheduler = get_scheduler(
                    self.args.lr_scheduler_type,
                    optimizer=self.optimizer if optimizer is None else optimizer,
                    num_warmup_steps=self.args.get_warmup_steps(num_training_steps),
                    num_training_steps=num_training_steps,
                )
            # 返回創(chuàng)建的學(xué)習(xí)率調(diào)度器
            return self.lr_scheduler
  • func num_examples
  • func _hp_search_setup
  • func _report_to_hp_search
  • func _tune_save_checkpoint
  • func call_model_init
  • func torch_jit_model_eval

4.1.6 分布式訓(xùn)練相關(guān)函數(shù)的實(shí)現(xiàn)

  • func ipex_optimize_model
    首先檢查了 Intel PyTorch Extension (IPEX) 是否可用。IPEX 是一個(gè)基于 Intel oneAPI Deep Neural Network Library (oneDNN) 的 PyTorch 擴(kuò)展庫,可以幫助在 Intel 的硬件(如 CPU)上更高效地運(yùn)行 PyTorch 程序
    如果處于訓(xùn)練模式,函數(shù)會(huì)使用 IPEX 對(duì)模型和優(yōu)化器進(jìn)行優(yōu)化;如果處于非訓(xùn)練模式(例如評(píng)估或測(cè)試),則僅對(duì)模型進(jìn)行優(yōu)化
  • func_wrap_model
    根據(jù)參數(shù)設(shè)置,可能會(huì)首先使用 IPEX 對(duì)模型進(jìn)行優(yōu)化。
    如果啟用了 Sagemaker 的模型并行,會(huì)將模型包裝為 Sagemaker 的 DistributedModel。模型并行是一種訓(xùn)練大型模型的技術(shù),它將模型的部分放在不同的 GPU 上,以克服單個(gè) GPU 內(nèi)存限制

    如果模型已經(jīng)被包裝(可能在之前的步驟中被包裝),則直接返回該模型
    使用 NVIDIA APEX(一種可以提高 GPU 利用率和擴(kuò)展訓(xùn)練的庫)進(jìn)行混合精度訓(xùn)練。這主要針對(duì) PyTorch 版本小于1.6的情況,因?yàn)?PyTorch 1.6 及以上版本已經(jīng)內(nèi)置了混合精度訓(xùn)練的支持

    如果啟用了多 GPU 訓(xùn)練,且模型不是8bit模型(即該模型不支持 int8 類型),則使用 PyTorch 的 DataParallel 對(duì)模型進(jìn)行數(shù)據(jù)并行處理。數(shù)據(jù)并行是一種將輸入數(shù)據(jù)分塊在多個(gè) GPU 上并行處理的技術(shù),可以有效地利用多個(gè) GPU 進(jìn)行訓(xùn)練。

    如果啟用了 JIT 模式評(píng)估,則對(duì)模型進(jìn)行 JIT 編譯。PyTorch 的 JIT 編譯器可以將模型編譯為中間表示(IR),然后在運(yùn)行時(shí)對(duì)其進(jìn)行優(yōu)化,從而提高模型的運(yùn)行效率。
    如果不是訓(xùn)練模式(例如評(píng)估或測(cè)試),則在這個(gè)階段返回模型,否則繼續(xù)對(duì)模型進(jìn)行進(jìn)一步的包裝
  • func auto_wrapper_callable
  • func patched_optimizer_step

4.1.7 主要訓(xùn)練入口:func train和func_inner_training_loop

  • func train
        """
        主要訓(xùn)練入口
        """
        def train(
            self,
            # 可選參數(shù),接收字符串或布爾類型,代表從哪個(gè)檢查點(diǎn)恢復(fù)訓(xùn)練
            resume_from_checkpoint: Optional[Union[str, bool]] = None,
    
            # 可選參數(shù),接收Optuna的Trial實(shí)例或者包含超參數(shù)的字典  
            trial: Union["optuna.Trial", Dict[str, Any]] = None,
    
            # 可選參數(shù),接收一個(gè)字符串列表,代表在模型輸出中需要忽略的鍵值  
            ignore_keys_for_eval: Optional[List[str]] = None,  
            **kwargs,  # 接收其他關(guān)鍵字參數(shù),用于隱藏已棄用的參數(shù)
        ):
    
            # 如果resume_from_checkpoint為False,將其設(shè)置為None
            if resume_from_checkpoint is False:  
                resume_from_checkpoint = None
    
            # 內(nèi)存指標(biāo) - 必須盡早設(shè)置
            self._memory_tracker.start()
    
            args = self.args
    
            # 設(shè)置訓(xùn)練狀態(tài)為True
            self.is_in_train = True  
    
            # do_train可能未設(shè)置,但仍然可能調(diào)用.train(),所以下面的操作是為了避免這種情況
            if (args.fp16_full_eval or args.bf16_full_eval) and not args.do_train:
                self._move_model_to_device(self.model, args.device)
    
            # 如果關(guān)鍵字參數(shù)中包含model_path
            if "model_path" in kwargs:  
                # 將model_path的值賦給resume_from_checkpoint并在kwargs中刪除model_path
                resume_from_checkpoint = kwargs.pop("model_path")  
                warnings.warn(
                    "`model_path` is deprecated and will be removed in a future version. Use `resume_from_checkpoint` "
                    "instead.",  # 發(fā)出關(guān)于model_path將在未來版本中刪除的警告
                    FutureWarning,
                )
        
            # 如果還有未處理的關(guān)鍵字參數(shù)
            if len(kwargs) > 0:  
                raise TypeError(f"train() received got unexpected keyword arguments: {', '.join(list(kwargs.keys()))}.")  # 拋出類型錯(cuò)誤
    
            # 這可能會(huì)改變隨機(jī)種子,因此需要先運(yùn)行
            self._hp_search_setup(trial)
            self._train_batch_size = self.args.train_batch_size  # 設(shè)置訓(xùn)練批次大小
    
            # 重載模型
            model_reloaded = False
            if self.model_init is not None:  # 如果模型初始化方法存在
                # 在實(shí)例化模型時(shí),必須先設(shè)置隨機(jī)種子
                enable_full_determinism(self.args.seed) if self.args.full_determinism else set_seed(self.args.seed)
    
                # 使用試驗(yàn)的超參數(shù)初始化模型
                self.model = self.call_model_init(trial)
    
                # 將模型重載標(biāo)記設(shè)置為True  
                model_reloaded = True  
    
                # 重新初始化優(yōu)化器和調(diào)度器
                self.optimizer, self.lr_scheduler = None, None
    
            # 加載可能存在的模型檢查點(diǎn)
            # 如果resume_from_checkpoint是bool類型且值為True
            if isinstance(resume_from_checkpoint, bool) and resume_from_checkpoint: 
                # 從輸出目錄中獲取最新的檢查點(diǎn) 
                resume_from_checkpoint = get_last_checkpoint(args.output_dir) 
    
                # 如果沒有找到有效的檢查點(diǎn) 
                if resume_from_checkpoint is None:  
                    raise ValueError(f"No valid checkpoint found in output directory ({args.output_dir})")  # 拋出值錯(cuò)誤
    
            # 如果resume_from_checkpoint不為None,并且SageMaker MP和DeepSpeed沒有啟用
            if resume_from_checkpoint is not None and not is_sagemaker_mp_enabled() and not self.is_deepspeed_enabled:
                # 從檢查點(diǎn)恢復(fù)模型
                self._load_from_checkpoint(resume_from_checkpoint)  
    
            # 如果模型已經(jīng)重載,將其放在正確的設(shè)備上并更新self.model_wrapped
            if model_reloaded:
                if self.place_model_on_device:
                    self._move_model_to_device(self.model, args.device)
                self.model_wrapped = self.model
    
            # 查找可執(zhí)行的批次大小
            inner_training_loop = find_executable_batch_size(
                self._inner_training_loop, self._train_batch_size, args.auto_find_batch_size
            )
    
            # 進(jìn)行內(nèi)部訓(xùn)練循環(huán)
            return inner_training_loop(
                args=args,
                resume_from_checkpoint=resume_from_checkpoint,
                trial=trial,
                ignore_keys_for_eval=ignore_keys_for_eval,
            )
  • func_inner_training_loop
  1. 首先,代碼計(jì)算了每個(gè)epoch中的訓(xùn)練步驟數(shù)量(steps_in_epoch),這可以是數(shù)據(jù)加載器的長(zhǎng)度,或者是最大步數(shù)乘以梯度累積步數(shù)。

  2. 然后,它會(huì)處理開始新的訓(xùn)練epoch,包括可能的從檢查點(diǎn)恢復(fù)訓(xùn)練的步驟。

  3. 代碼遍歷了每個(gè)訓(xùn)練步驟,每個(gè)步驟接收輸入數(shù)據(jù),并進(jìn)行以下操作:

    • 在每個(gè)需要的步驟上同步隨機(jī)數(shù)生成器的狀態(tài)
    • 跳過已經(jīng)訓(xùn)練過的步驟(如果從檢查點(diǎn)恢復(fù)訓(xùn)練)
    • 調(diào)用回調(diào)函數(shù)處理步驟的開始
    • 執(zhí)行訓(xùn)練步驟,并計(jì)算訓(xùn)練損失
    • 如果損失是NaN或Inf(無窮),則根據(jù)前面記錄的損失進(jìn)行調(diào)整
    • 計(jì)算浮點(diǎn)運(yùn)算的數(shù)量
    • 如果達(dá)到了梯度累積的步驟,或者是最后一步,會(huì)進(jìn)行以下操作:
      • 執(zhí)行梯度裁剪(如果需要)
      • 執(zhí)行優(yōu)化器步驟,并判斷優(yōu)化器是否真正執(zhí)行了步驟
      • 如果優(yōu)化器步驟執(zhí)行了,進(jìn)行學(xué)習(xí)率調(diào)度(除了在使用ReduceLROnPlateau學(xué)習(xí)率調(diào)度器的情況下,它需要在生成度量之后才執(zhí)行調(diào)度)
      • 模型的梯度清零
      • 更新全局步驟和epoch數(shù)
      • 調(diào)用回調(diào)函數(shù)處理步驟的結(jié)束
      • 有條件地記錄、保存和評(píng)估模型
    • 如果訓(xùn)練應(yīng)該停止,或者已經(jīng)完成了所有的步驟,則退出循環(huán)
  4. 在每個(gè)epoch結(jié)束時(shí),代碼處理epoch的結(jié)束,可能會(huì)記錄、保存和評(píng)估模型,檢查是否有配置的TPU,并決定是否應(yīng)該停止整個(gè)訓(xùn)練

4.1.8 對(duì)模型的加載、檢查、評(píng)估、保存

  • func_get_output_dir
  • func_load_from_checkpoint
  • func_load_best_model
  • func_issue_warnings_after_load
  • func_maybe_log_save_evaluate

這個(gè)函數(shù)主要執(zhí)行的是在訓(xùn)練過程中的日志記錄、模型評(píng)估和模型保存的操作。主要步驟包括:

  1. 記錄日志:如果控制標(biāo)志 should_log 為 True,那么就記錄日志。記錄的內(nèi)容包括訓(xùn)練損失、學(xué)習(xí)率等信息,并對(duì)這些信息進(jìn)行日志輸出。
  2. 評(píng)估模型:如果控制標(biāo)志 should_evaluate 為 True,那么就對(duì)模型進(jìn)行評(píng)估。評(píng)估的數(shù)據(jù)集可以是多個(gè),評(píng)估的結(jié)果將會(huì)用于更新學(xué)習(xí)率調(diào)度器或者報(bào)告給超參數(shù)搜索。
  3. 保存模型:如果控制標(biāo)志 should_save 為 True,那么就保存模型的檢查點(diǎn)。保存的內(nèi)容包括模型、評(píng)估指標(biāo)等信息
  • func_load_rng_state
  • func_save_checkpoint
  • func_load_optimizer_and_scheduler
    用于從給定的檢查點(diǎn)位置加載優(yōu)化器和學(xué)習(xí)率調(diào)度器的狀態(tài)

    這通常在訓(xùn)練的中斷后恢復(fù)訓(xùn)練時(shí)使用,以確保訓(xùn)練可以從上次停止的地方繼續(xù)。在加載狀態(tài)時(shí),需要考慮一些因素,例如是否啟用了DeepSpeed,是否啟用了SageMaker多處理,是否可用TPU,是否啟用了全尺寸數(shù)據(jù)并行(FSDP)等。各種情況需要采用不同的方式來加載狀態(tài)
  • func opt_load_hook
  • func opt_load_hook
  • func hyperparameter_search
    用于啟動(dòng)超參數(shù)搜索??梢允褂貌煌暮蠖诉M(jìn)行搜索,包括optuna、Ray Tune或SigOpt,默認(rèn)使用optuna

    該方法接收一個(gè)定義超參數(shù)搜索空間的函數(shù),一個(gè)計(jì)算目標(biāo)函數(shù)的函數(shù),試驗(yàn)次數(shù),優(yōu)化方向(最小化或最大化),使用的后端,定義試驗(yàn)名稱的函數(shù),以及其他參數(shù)。這個(gè)方法用于尋找最佳的超參數(shù)組合,以使模型的性能達(dá)到最優(yōu)
  • func log
  • func _prepare_input
  • func _prepare_inputs
  • func compute_loss_context_manager
  • func autocast_smart_context_manager

4.1.9 一個(gè)訓(xùn)練步驟的實(shí)現(xiàn):前向后向傳播、計(jì)算損失

  • training_step (第2661行-2660行)

一個(gè)訓(xùn)練步驟的實(shí)現(xiàn),它涵蓋了一個(gè)批量數(shù)據(jù)的前向和后向傳播

    # `training_step`函數(shù)表示訓(xùn)練過程中的一步操作,涵蓋了模型的前向和后向傳播
    def training_step(self, model: nn.Module, inputs: Dict[str, Union[torch.Tensor, Any]]) -> torch.Tensor:
        
        # 將模型設(shè)置為訓(xùn)練模式,這對(duì)于某些層(如Dropout或BatchNorm)的行為有影響,因?yàn)樗鼈冊(cè)谟?xùn)練和評(píng)估階段的行為是不同的
        model.train()  

        # 調(diào)用一個(gè)輔助方法準(zhǔn)備模型的輸入,具體的實(shí)現(xiàn)取決于模型的需求
        inputs = self._prepare_inputs(inputs)  

        # 如果啟用了 SageMaker Model Parallelism,則使用 `smp_forward_backward` 在多個(gè) GPU 上執(zhí)行前向和后向操作
        # 然后減小損失,并將其從計(jì)算圖中分離
        if is_sagemaker_mp_enabled():
            loss_mb = smp_forward_backward(model, inputs, self.args.gradient_accumulation_steps)
            return loss_mb.reduce_mean().detach().to(self.args.device)

        # 計(jì)算損失值
        with self.compute_loss_context_manager():
            loss = self.compute_loss(model, inputs)

        # 如果使用的 GPU 數(shù)量大于 1,則對(duì)損失值取平均,以處理多 GPU 并行訓(xùn)練
        if self.args.n_gpu > 1:
            loss = loss.mean()  # mean() to average on multi-gpu parallel training

        # 根據(jù)是否進(jìn)行梯度縮放,選擇不同的后向傳播方式
        if self.do_grad_scaling:
            self.scaler.scale(loss).backward()  # 使用梯度縮放進(jìn)行后向傳播,可以防止在混合精度訓(xùn)練中出現(xiàn)梯度下溢
        elif self.use_apex:
            with amp.scale_loss(loss, self.optimizer) as scaled_loss:  # 如果使用了APEX工具進(jìn)行混合精度訓(xùn)練,則需要對(duì)損失進(jìn)行縮放后再進(jìn)行后向傳播
                scaled_loss.backward()
        else:
            self.accelerator.backward(loss)  # 使用加速器進(jìn)行后向傳播,適用于沒有使用梯度縮放和APEX的情況

        # 返回?fù)p失值,如果設(shè)置了梯度累積步驟,則需要將損失值除以梯度累積步驟數(shù)
        return loss.detach() / self.args.gradient_accumulation_steps
  • compute_loss

計(jì)算損失

    # `compute_loss`函數(shù)用于計(jì)算模型的損失值
    def compute_loss(self, model, inputs, return_outputs=False):
        
        # 如果存在標(biāo)簽平滑處理器且輸入中有標(biāo)簽,則將標(biāo)簽從輸入中移除
        if self.label_smoother is not None and "labels" in inputs:
            labels = inputs.pop("labels")
        else:
            labels = None

        # 使用模型進(jìn)行前向傳播,得到輸出
        outputs = model(**inputs)

        # 如果存在之前的狀態(tài)信息,保存它
        # TODO: 這部分需要在未來進(jìn)行清理和優(yōu)化
        if self.args.past_index >= 0:
            self._past = outputs[self.args.past_index]

        # 如果標(biāo)簽存在,使用標(biāo)簽平滑處理器計(jì)算損失
        if labels is not None:
            if unwrap_model(model)._get_name() in MODEL_FOR_CAUSAL_LM_MAPPING_NAMES.values():
                loss = self.label_smoother(outputs, labels, shift_labels=True)
            else:
                loss = self.label_smoother(outputs, labels)
        else:
            # 如果輸出是一個(gè)字典,但并未包含損失,那么拋出錯(cuò)誤
            if isinstance(outputs, dict) and "loss" not in outputs:
                raise ValueError(
                    "The model did not return a loss from the inputs, only the following keys: "
                    f"{','.join(outputs.keys())}. For reference, the inputs it received are {','.join(inputs.keys())}."
                )
            # 我們并未直接使用.outputs,因?yàn)槟P涂赡芊祷氐氖窃M,而非ModelOutput
            loss = outputs["loss"] if isinstance(outputs, dict) else outputs[0]

        # 如果`return_outputs`為真,返回?fù)p失和輸出;否則只返回?fù)p失
        return (loss, outputs) if return_outputs else loss
  • func is_local_process_zero
  • func is_world_process_zero
  • func save_model
    此函數(shù)用于保存模型。如果給出了輸出目錄,則將在該目錄中保存模型,否則將在args.output_dir中保存模型。保存操作依賴于環(huán)境,例如,如果是在TPU上,將會(huì)調(diào)用`_save_tpu`。如果是在SageMaker多處理中,則會(huì)保存模型的狀態(tài)字典。另外,此函數(shù)也考慮了`ShardedDDPOption`的設(shè)置等。最后,如果設(shè)置了`args.push_to_hub`,那么在用戶調(diào)用`save_model`時(shí),模型會(huì)被推送到Hub
  • func _save_tpu
    在TPU上保存模型的專用函數(shù)
  • func _save
    保存模型的基本函數(shù)。這個(gè)函數(shù)不檢查進(jìn)程是否為零,因?yàn)橹挥性谶M(jìn)程為零的情況下才會(huì)調(diào)用此函數(shù)
  • func store_flos
    存儲(chǔ)進(jìn)入模型的浮點(diǎn)運(yùn)算數(shù)。如果模型在分布式模式下運(yùn)行,該函數(shù)會(huì)將當(dāng)前浮點(diǎn)運(yùn)算數(shù)的總數(shù)加到`state.total_flos`上,然后將當(dāng)前浮點(diǎn)運(yùn)算數(shù)歸零。在非分布式模式下,也執(zhí)行相同的操作,只是不需要分布式廣播浮點(diǎn)運(yùn)算數(shù)
  • func_sorted_checkpoints
    返回排序后的檢查點(diǎn)列表。使用修改時(shí)間或檢查點(diǎn)編號(hào)進(jìn)行排序,然后返回路徑列表。如果設(shè)置了最佳模型檢查點(diǎn),那么確保我們不會(huì)刪除最佳模型
  • func_rotate_checkpoints
  • func evaluate
    運(yùn)行評(píng)估并返回指標(biāo)。需要用戶提供計(jì)算指標(biāo)的方法,因?yàn)樗鼈兪侨蝿?wù)依賴的。你也可以重寫此方法以注入自定義行為。函數(shù)返回包含評(píng)估損失和可能從預(yù)測(cè)中計(jì)算出的指標(biāo)的字典。該字典也包含來自訓(xùn)練狀態(tài)的epoch編號(hào)
  • func predict
    def predict(
        self, test_dataset: Dataset, ignore_keys: Optional[List[str]] = None, metric_key_prefix: str = "test"
    ) -> PredictionOutput:
    
        # 設(shè)置內(nèi)存跟蹤器,盡早啟動(dòng)
        self._memory_tracker.start()
    
        # 獲取測(cè)試數(shù)據(jù)集的數(shù)據(jù)加載器
        test_dataloader = self.get_test_dataloader(test_dataset)
    
        # 記錄開始時(shí)間
        start_time = time.time()
    
        # 選擇預(yù)測(cè)循環(huán)或評(píng)估循環(huán),這取決于args中的use_legacy_prediction_loop參數(shù)
        eval_loop = self.prediction_loop if self.args.use_legacy_prediction_loop else self.evaluation_loop
    
        # 運(yùn)行選定的循環(huán),并獲得預(yù)測(cè)或評(píng)估輸出
        output = eval_loop(
            test_dataloader, description="Prediction", ignore_keys=ignore_keys, metric_key_prefix=metric_key_prefix
        )
    
        # 計(jì)算總批次大小,包括所有的并行處理單元
        total_batch_size = self.args.eval_batch_size * self.args.world_size
    
        # 如果度量指標(biāo)中包含jit編譯時(shí)間,那么將這段時(shí)間加到開始時(shí)間中
        if f"{metric_key_prefix}_jit_compilation_time" in output.metrics:
            start_time += output.metrics[f"{metric_key_prefix}_jit_compilation_time"]
    
        # 更新度量指標(biāo),包括預(yù)測(cè)速度相關(guān)的指標(biāo)
        output.metrics.update(
            speed_metrics(
                metric_key_prefix,
                start_time,
                num_samples=output.num_samples,
                num_steps=math.ceil(output.num_samples / total_batch_size),
            )
        )
    
        # 使用回調(diào)處理器進(jìn)行預(yù)測(cè)后的操作,并更新控制狀態(tài)
        self.control = self.callback_handler.on_predict(self.args, self.state, self.control, output.metrics)
    
        # 停止內(nèi)存跟蹤器,并更新相關(guān)度量指標(biāo)
        self._memory_tracker.stop_and_update_metrics(output.metrics)
    
        # 返回預(yù)測(cè)結(jié)果,包括預(yù)測(cè)值,標(biāo)簽(如果存在)和度量指標(biāo)
        return PredictionOutput(predictions=output.predictions, label_ids=output.label_ids, metrics=output.metrics)
  • func evaluation_loop
  • func_nested_gather
  • func_pad_across_processes
  • func prediction_step
  • func floating_point_ops
  • func init_git_repo
  • func create_model_card
  • func_push_from_checkpoint
  • func push_to_hub
  • func prediction_loop
  • func_gather_and_numpify
  • func_add_sm_patterns_to_gitignore
  • func create_accelerator_and_postp

// 待更文章來源地址http://www.zghlxwxcb.cn/news/detail-479568.html


參考文獻(xiàn)與推薦閱讀

  1. ??????Transformer通俗筆記:從Word2Vec、Seq2Seq逐步理解到GPT、BERT
  2. Transformer原始論文(值得反復(fù)讀幾遍):Attention Is All You Need
  3. Vision Transformer 超詳細(xì)解讀 (原理分析+代碼解讀) (一)
  4. Transformer模型詳解(圖解最完整版)
  5. The Annotated Transformer(翻譯之一),harvard對(duì)transformer的簡(jiǎn)單編碼實(shí)現(xiàn)
  6. transformer的細(xì)節(jié)到底是怎么樣的?
  7. 如何從淺入深理解transformer?
  8. Transformer 結(jié)構(gòu)詳解:位置編碼 | Transformer Architecture: The Positional Encoding
  9. Transformer學(xué)習(xí)筆記一:Positional Encoding(位置編碼)
  10. 保姆級(jí)講解Transformer
  11. Jay Alammar寫的圖解transformer
  12. 如何理解attention中的Q,K,V?

附錄:創(chuàng)作/修改記錄

  1. 4.12-4.14,基本完成第一部分 transformer編碼器部分的初稿
  2. 4.16,徹底完善關(guān)于transformer位置編碼的闡述,可能是網(wǎng)上對(duì)這點(diǎn)最一目了然的闡述了
  3. 4.17,完成transformer的解碼器部分
  4. 4.18,開始寫「第四部分 ChatGLM-6B的代碼架構(gòu)與逐一實(shí)現(xiàn)」
  5. 5.26,新增內(nèi)容
    分詞代碼的實(shí)現(xiàn):tokenization_chatglm.py
    quantization:模型量化——減小模型大小和推理時(shí)間
  6. 5.27,新增“第五部分 基于LangChain + ChatGLM-6B的本地知識(shí)庫的應(yīng)用實(shí)現(xiàn)”
  7. 6.8日,完善第五部分
  8. 7.5日,把原有的「第四部分 ChatGLM-6B的代碼架構(gòu)與逐一實(shí)現(xiàn)」放進(jìn)另一篇博客里:ChatGLM-6B的基座/部署/微調(diào)/實(shí)現(xiàn):從GLM到6B的LoRA/P-Tuning微調(diào)、及6B源碼解讀
    把原有的“第五部分 基于LangChain + ChatGLM-6B的本地知識(shí)庫的應(yīng)用實(shí)現(xiàn)”,獨(dú)立成文為:給LLM裝上知識(shí):從LangChain+LLM的本地知識(shí)庫問答到LLM與知識(shí)圖譜的結(jié)合
  9. 7.7-7.20日,寫本文新的「第四部分 Hugging face社區(qū)實(shí)現(xiàn)的Transformers庫的整體解讀」

到了這里,關(guān)于從零實(shí)現(xiàn)Transformer、ChatGLM-6B、LangChain+LLM的本地知識(shí)庫問答的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 開源大模型ChatGLM2-6B 2. 跟著LangChain參考文檔搭建LLM+知識(shí)庫問答系統(tǒng)

    開源大模型ChatGLM2-6B 2. 跟著LangChain參考文檔搭建LLM+知識(shí)庫問答系統(tǒng)

    租用了1臺(tái)GPU服務(wù)器,系統(tǒng) ubuntu20,Tesla V100-16GB (GPU服務(wù)器已經(jīng)關(guān)機(jī)結(jié)束租賃了) SSH地址:* 端口:17520 SSH賬戶:root 密碼:Jaere7pa 內(nèi)網(wǎng): 3389 , 外網(wǎng):17518 VNC地址:* 端口:17519 VNC用戶名:root 密碼:Jaere7pa 硬件需求,ChatGLM-6B和ChatGLM2-6B相當(dāng)。 量化等級(jí)?? ?最低 GPU 顯存 F

    2024年02月03日
    瀏覽(32)
  • AIGC:【LLM(四)】——LangChain+ChatGLM:本地知識(shí)庫問答方案

    AIGC:【LLM(四)】——LangChain+ChatGLM:本地知識(shí)庫問答方案

    LangChain+ChatGLM項(xiàng)目(https://github.com/chatchat-space/langchain-ChatGLM)實(shí)現(xiàn)原理如下圖所示 (與基于文檔的問答 大同小異,過程包括:1 加載文檔 - 2 讀取文檔 - 3/4文檔分割 - 5/6 文本向量化 - 8/9 問句向量化 - 10 在文檔向量中匹配出與問句向量最相似的top k個(gè) - 11/12/13 匹配出的文本作為上下

    2024年02月13日
    瀏覽(90)
  • AI-基于Langchain-Chatchat和chatglm3-6b部署私有本地知識(shí)庫

    AI-基于Langchain-Chatchat和chatglm3-6b部署私有本地知識(shí)庫

    手把手教你搭建本地知識(shí)庫問答AI機(jī)器人 LangChain-Chatchat:基于LangChain和ChatGLM2-6B構(gòu)建本地離線私有化知識(shí)庫 在家庭私有云上部署體驗(yàn)語言模型chatglm3-6b,打造私人助理 手把手教大家在本地運(yùn)行ChatGLM3-6B大模型(一) 自從去年GPT模型火爆以來,降低了很多個(gè)人和企業(yè)進(jìn)入人工智

    2024年02月20日
    瀏覽(30)
  • LLM本地知識(shí)庫問答系統(tǒng)(一):使用LangChain和LlamaIndex從零構(gòu)建PDF聊天機(jī)器人指南

    LLM本地知識(shí)庫問答系統(tǒng)(一):使用LangChain和LlamaIndex從零構(gòu)建PDF聊天機(jī)器人指南

    ? ? ? ?隨著大型語言模型(LLM)(如ChatGPT和GPT-4)的興起,現(xiàn)在比以往任何時(shí)候都更容易構(gòu)建比普通熊更智能的智能聊天機(jī)器人,并且可以瀏覽堆積如山的文檔,為您的輸入提供準(zhǔn)確的響應(yīng)。 ? ? ? ?在本系列中,我們將探索如何使用pre-trained的LLM創(chuàng)建一個(gè)聊天機(jī)器人,該聊

    2024年02月11日
    瀏覽(100)
  • LangChain + ChatGLM2-6B 搭建個(gè)人專屬知識(shí)庫

    LangChain + ChatGLM2-6B 搭建個(gè)人專屬知識(shí)庫

    之前教過大家利用 langchain + ChatGLM-6B 實(shí)現(xiàn)個(gè)人專屬知識(shí)庫,非常簡(jiǎn)單易上手。最近,智譜 AI 研發(fā)團(tuán)隊(duì)又推出了 ChatGLM 系列的新模型 ChatGLM2-6B,是開源中英雙語對(duì)話模型 ChatGLM-6B 的第二代版本,性能更強(qiáng)悍。 樹先生之所以現(xiàn)在才更新 ChatGLM2-6B 知識(shí)庫教程,是想等模型本身再多

    2024年02月16日
    瀏覽(24)
  • 使用LangChain與ChatGLM實(shí)現(xiàn)本地知識(shí)庫(一)

    使用LangChain與ChatGLM實(shí)現(xiàn)本地知識(shí)庫(一)

    ??本篇主要內(nèi)容為介紹ChatGLM3的安裝使用,后續(xù)才會(huì)涉及到使用LangChain實(shí)現(xiàn)本地知識(shí)庫的內(nèi)容; ??ChatGLM為智譜與清華大學(xué)開源的一個(gè)大語言模型,支持多輪對(duì)話、內(nèi)容創(chuàng)作等,ChatGLM3-6B為ChatGLM3系列中門檻相對(duì)較低的一個(gè),本地部署提供兼容OpenAI的API; ??LangChain用于快

    2024年02月05日
    瀏覽(27)
  • 使用Langchain與ChatGLM實(shí)現(xiàn)本地知識(shí)庫(二)

    使用Langchain與ChatGLM實(shí)現(xiàn)本地知識(shí)庫(二)

    ??大語言模型也只是將用戶提供的大規(guī)模數(shù)據(jù)集訓(xùn)練而來,也并非萬能的什么都知道,特別是一些小眾知識(shí)、內(nèi)部數(shù)據(jù)或私密的個(gè)人數(shù)據(jù)等,此時(shí)ChatGLM3肯定會(huì)胡亂回答就是ChatGPT4也不一定能給出滿意回答;不少公司、個(gè)人都有自己的知識(shí)庫或日志等此時(shí)如有可將這些數(shù)據(jù)以

    2024年02月05日
    瀏覽(31)
  • 【ChatGLM_02】LangChain知識(shí)庫+Lora微調(diào)chatglm2-6b模型+提示詞Prompt的使用原則

    【ChatGLM_02】LangChain知識(shí)庫+Lora微調(diào)chatglm2-6b模型+提示詞Prompt的使用原則

    運(yùn)行l(wèi)angchain-ChatGLM-master下面的webui.py文件 (1) 配置知識(shí)庫 新建知識(shí)庫 向知識(shí)庫當(dāng)中添加文件 支持上傳的數(shù)據(jù)格式:word、pdf、excel、csv、txt、文件夾等。但是此處我試了一下 (2) 文檔數(shù)據(jù)測(cè)試 word文檔測(cè)試: (3) 知識(shí)庫測(cè)試模式 知識(shí)庫測(cè)試只會(huì)返回輸入內(nèi)容在當(dāng)前知識(shí)庫當(dāng)中的

    2024年02月14日
    瀏覽(24)
  • 類ChatGPT逐行代碼解讀(1/2):從零實(shí)現(xiàn)Transformer、ChatGLM-6B

    類ChatGPT逐行代碼解讀(1/2):從零實(shí)現(xiàn)Transformer、ChatGLM-6B

    最近一直在做類ChatGPT項(xiàng)目的部署 微調(diào),關(guān)注比較多的是兩個(gè):一個(gè)LLaMA,一個(gè)ChatGLM,會(huì)發(fā)現(xiàn)有不少模型是基于這兩個(gè)模型去做微調(diào)的,說到微調(diào),那具體怎么微調(diào)呢,因此又詳細(xì)了解了一下微調(diào)代碼,發(fā)現(xiàn)微調(diào)LLM時(shí)一般都會(huì)用到Hugging face實(shí)現(xiàn)的Transformers庫的Trainer類 從而發(fā)現(xiàn)

    2024年02月03日
    瀏覽(21)
  • 類ChatGPT逐行代碼解讀(1/2):從零起步實(shí)現(xiàn)Transformer、ChatGLM-6B

    類ChatGPT逐行代碼解讀(1/2):從零起步實(shí)現(xiàn)Transformer、ChatGLM-6B

    最近一直在做類ChatGPT項(xiàng)目的部署 微調(diào),關(guān)注比較多的是兩個(gè):一個(gè)LLaMA,一個(gè)ChatGLM,會(huì)發(fā)現(xiàn)有不少模型是基于這兩個(gè)模型去做微調(diào)的,說到微調(diào),那具體怎么微調(diào)呢,因此又詳細(xì)了解了一下微調(diào)代碼,發(fā)現(xiàn)微調(diào)LLM時(shí)一般都會(huì)用到Hugging face實(shí)現(xiàn)的Transformers庫的Trainer類 從而發(fā)現(xiàn)

    2023年04月26日
    瀏覽(23)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包