如何客製化企業 RAG 知識庫?— 從資料庫到知識整合的實戰技術
|

利用Intel平台提升LLM微調與訓練效率:ResNet50+LoRA範例

   

作者:高煥堂、鄭仲平

本文將說明如何善用英特爾(Intel)所研發的新產品和相關技術,來提升AI模型的訓練效率,以便提供更優質的推論功能。其中,英特爾推出了兩項核心產品:

  • 核心軟體是:Intel-extension-for-pytorch。
  • 核心硬體是:Intel(R) Data Center GPU Flex 170。

而與以上核心產品息息相關的重要技術是:19381

  • BF16浮點數精度。
  • 資料格式設為 Channel_last資料格式(即是NHWC格式)。

為了循序漸進介紹上述的新產品及其相關的使用技術,本文先從大家熟悉ResNet50圖像分類模型做為起步範例,並搭配LoRA來進行外掛訓練,以便從這簡單範例中充分理解如何活用上述的創新產品及相關技術。

<ResNet50+LoRA>訓練範例

簡介ResNet50

ResNet50是很通用的AI模型,擅長於圖像的特徵提取(Feature extraction),然後依據特徵來進行分類(Classification)。所以,它能幫您瞬間探索任何一張圖像的特徵,然後幫您識別出圖片裡的人或物的種類。目前的ResNet50可以準確地識別出1,000種人或物,如日常生活中常遇到的狗、貓、食物、汽車和各種家居物品等。例如,您可以從百度圖片上截取一張224x224大小的圖片(圖1):

圖1:輸入給ResNet50的圖像

圖1:輸入給ResNet50的圖像

當您把這圖片提交給ResNet50,它會瞬間探索並進行分類,然後告訴您:我預測這是大熊貓(Giant panda)。

簡介LoRA

隨著大語言模型(LLM)等大模型日益繁榮發展,基於這些大模型的遷移學習(Transfer learning),將其預訓練好的模型加以微調(Fine tune),來適應到下游的各項新任務,已經成為熱門的議題。關於微調技術,其中LoRA是一種資源消耗較小的訓練方法,它能在較少訓練參數時就得到比較穩定的效果。

LoRA的全名是:Low-Rank Adaptation of Large Language Models (及大語言模型的低階適應)。使用這種LoRA微調方法進行訓練時,並不需要調整原(大)模型的參數值(圖2裡的藍色部分),而只需要訓練LoRA模型的參數(圖2裡的棕色部分)。

圖2:LoRA的架構圖

圖2:LoRA的架構圖 (來源:https://deci.ai/blog/fine-tune-llama-2-with-lora-for-question-answering/)

典型的LoRA微調途徑是,使用下游任務的資料來對<原模型 + LoRA>進行重新訓練,讓該協同模型的性能在該下游任務上表現出最佳效果。

在一般CPU環境裡訓練ResNet50

先來觀察一般CPU環境的ResNet50訓練流程。

Step-1:準備訓練資料(Training data)

首先,準備了/oopc/m_data/train/圖像集,包含三個類別—斑馬、貓頭鷹和蘑菇:

 

各類別都有80張圖像,例如斑馬圖像:


總共有240張圖像。

Step-2: 準備ResNet50預訓練模型

本範例從torchvision.models裡載入resnet50預訓練模型。程式碼如下:


# RESNET_basic_001.py
import numpy as np
import torch
import torch.nn as nn
from torchvision import transforms
from torchvision.datasets import ImageFolder
from torch.utils.data import Dataset, DataLoader
import torchvision.models as models
data_path = 'c:/oopc/m_data/train/'

#------------------------------------
# Make torch deterministic
# 設定:每次訓練的W&amp;B初始化都是一樣的
_ = torch.manual_seed(0)

#-------------------------------------
# 載入ResNet50預訓練模型
resnet_model = models.resnet50(
        weights=models.ResNet50_Weights.IMAGENET1K_V1)

# 遷移學習不需要梯度(不更改權重)
for param in resnet_model.parameters():
       param.requires_grad = False

#------------------------------------------
resnet_model.eval()
#------------------------------------------
# 準備Training data
# 把圖片轉換成Tensor
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor()
])

data_set = ImageFolder(data_path, transform=transform)
length = len(data_set)
print('\n', length, "張圖片")
print('本地分類標籤:斑馬--&gt;0,  貓頭鷹--&gt;1, 蘑菇--&gt;2')

bz = 10
train_loader = DataLoader(data_set, batch_size=bz, shuffle=True)
for idx, (images, labels) in enumerate(train_loader):
     break
print('\n本地讀取的類別Labels:')
print(labels)

#--------------------------------------
def process_lx(labels):
    lx = labels.clone()
    for i in range(bz):
        if(labels[i]==0):
           lx[i]=340
        elif(labels[i]==1):
           lx[i]=24
        elif(labels[i]==2):
           lx[i]=947
    return lx

print('\n轉換為ResNet50的類別Labels:')
res_labels = process_lx(labels)
print(res_labels)
#------------------------------------------------------
#END

在本範例裡,其圖像分為3個類別:斑馬、貓頭鷹、蘑菇。所以在此程式裡,其<斑馬、貓頭鷹、蘑菇>的類別標籤(Label)分別為:[0, 1, 2]。而在ResNet50預訓練模型裡,其<斑馬、貓頭鷹、蘑菇>類別標籤分別為:[340, 24, 947]。於是,使用process_lx()函數,來把此程式裡的類別標籤,轉換為ResNet50的類別標籤值。 當我們在Windows環境裡執行這個程式時,會輸出如下:

Step-3: 使用ResNet50預訓練模型進行推論

這ResNet50屬於大模型,其泛化能力很好。然而,對於本範例的較少類別的預測(推論)準確度就常顯得不足。現在,就拿本範例的訓練圖像集來檢測一下。程式碼如下:


import numpy as np
import torch
import torch.nn as nn
from torchvision import transforms
from torchvision.datasets import ImageFolder
from torch.utils.data import Dataset, DataLoader
import torchvision.models as models
data_path = 'c:/oopc/m_data/train/'

#------------------------------------
# Make torch deterministic
# 設定:每次訓練的W&amp;B初始化都是一樣的
_ = torch.manual_seed(0)

#-------------------------------------
# 載入ResNet50預訓練模型
resnet_model = models.resnet50(
        weights=models.ResNet50_Weights.IMAGENET1K_V1)

# 遷移學習不需要梯度(不更改權重)
for param in resnet_model.parameters():
       param.requires_grad = False

#------------------------------------------
resnet_model.eval()
#------------------------------------------
# 準備Training data
# 把圖片轉換成Tensor
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor()
])

data_set = ImageFolder(data_path, transform=transform)
length = len(data_set)
bz = 10
train_loader = DataLoader(data_set, batch_size=bz, shuffle=True)

#--------------------------------------
def process_lx(labels):
    lx = labels.clone()
    for i in range(bz):
        if(labels[i]==0):
           lx[i]=340
        elif(labels[i]==1):
           lx[i]=24
        elif(labels[i]==2):
           lx[i]=947
    return lx

#=======================================
def test():
    correct = 0
    total = 0
    wrong_counts = [0 for i in range(3)] # 本範例有3類別

    with torch.no_grad():
        for idx, (images, la) in enumerate(train_loader):              
            labels = process_lx(la)
            prediction = resnet_model(images)
            for idx, zv in enumerate(prediction):
                if torch.argmax(zv) == labels[idx]:
                    correct +=1
                else:
                    #print(idx)
                    wrong_counts[la[idx]] +=1
                total +=1
               
    #print(correct)            
    print('\n', f'Accuracy: {round(correct/total, 3)}')
    for i in range(len(wrong_counts)):
        print(f'wrong counts for the class {i}: {wrong_counts[i]}')

#------------------------------------------
test()
#------------------------------------------
#END

在此程式裡,我們拿訓練資料集裡的<斑馬、貓頭鷹、蘑菇>共240張圖像來給ResNet50進行分類預測。當我們在Windows環境裡執行這個程式時,會輸出如下:

這顯示出:預測準確度為:0.358。其中,有24張斑馬(class_0)圖像預測錯誤。也有64張斑馬(class_1)圖像預測錯誤。還有66張蘑菇(class_2)圖像預測錯誤。

可以觀察到了,大模型ResNet50在這範例裡的下游任務上,其預測的準確度並不美好。於是,LoRA微調方法就派上用場了。

Step-4: 定義LoRA模型,並展開協同訓練

茲回顧LoRA的架構(圖3):

圖3:更詳細的LoRA的架構圖

圖3:更詳細的LoRA的架構圖 (來源:https://huggingface.co/docs/peft/conceptual_guides/lora/)

在剛才的範例裡,我們載入的ResNet50模型,就是上圖裡的Pretrained Weights(即藍色)部分。現在,就準備添加LoRA模型,也就是上圖裡的A和B(即棕色)部分。


OpenVINO更多的新功能陸續問世!新版本將新增對新硬體以及更多生成式AI模型的支援,在2024年的新開始,讓我們一起透過免費線上講座「掌握OpenVINO在生成式AI最新功能, 並實現Model Server創新應用」來探索這套越來越強大的工具,讓您的專案開發更如虎添翼,名額有限快來報名

當我們把A&B部分添加上去了,就能展開協同訓練了。在協同訓練時,我們會先凍結Pretrained Weights部分的參數,不去更改它;而只更新LoRA的A&B參數。

一旦協同訓練完成了,就會把LoRA與ResNet50的參數合併起來(即上圖右方的橘色部分。請來看看程式碼:


# lora_RESNET_basic_003_train.py
import numpy as np
import torch
import torch.nn as nn
from torchvision import transforms
from torchvision.datasets import ImageFolder
from torch.utils.data import Dataset, DataLoader
import torchvision.models as models
data_path = 'c:/oopc/m_data/train/'
base_path = 'c:/oopc/m_data/'

#------------------------------------
# Make torch deterministic
# 設定:每次訓練的W&amp;B初始化都是一樣的
_ = torch.manual_seed(0)

#-------------------------------------
# 載入ResNet50預訓練模型
resnet_model = models.resnet50(
        weights=models.ResNet50_Weights.IMAGENET1K_V1)

# 遷移學習不需要梯度(不更改權重)
for param in resnet_model.parameters():
       param.requires_grad = False

#-----------------------------------------
resnet_model.eval()

#------------------------------------------
# 準備Training data
# 把圖片轉換成Tensor
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor()
])

data_set = ImageFolder(data_path, transform=transform)
length = len(data_set)

bz = 60
train_loader = DataLoader(data_set, batch_size=bz, shuffle=True)

#--------------------------------------
def process_lx(labels):
    lx = labels.clone()
    for i in range(bz):
        if(labels[i]==0):
           lx[i]=340
        elif(labels[i]==1):
           lx[i]=24
        elif(labels[i]==2):
           lx[i]=947
    return lx

#=======================================
def test():
    correct = 0
    total = 0
    wrong_counts = [0 for i in range(3)] # 本範例有3類別
    with torch.no_grad():
        for idx, (images, la) in enumerate(train_loader):              
            labels = process_lx(la)
            prediction = resnet_model(images)
            for idx, zv in enumerate(prediction):
                if torch.argmax(zv) == labels[idx]:
                    correct +=1
                else:
                    wrong_counts[la[idx]] +=1
                total +=1
               
    #print(correct)            
    print('\n', f'Accuracy: {round(correct/total, 3)}')
    for i in range(len(wrong_counts)):
        print(f'wrong counts for the class {i}: {wrong_counts[i]}')

#------------------------------------------
test()

#====== 定義LoRA模型 ===============================
class Lora(nn.Module):  
    def __init__(self, m, n, rank=10):  
        super().__init__()  
        self.m = m  
        self.A = nn.Parameter(torch.randn(m, rank))  
        self.B = nn.Parameter(torch.zeros(rank, n))  
 
    def forward(self, inputs):  
        inputs = inputs.view(-1, self.m)  
        return torch.mm(torch.mm(inputs, self.A), self.B)

lora = Lora(224 * 224 * 3, 1000)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(lora.parameters(), lr=1e-4)  

#----------------------------------------------------
print('\n------ 外掛LoRA模型,協同訓練: ------')
base = 0
epochs = 100
for ep in range(epochs+1):
    total_loss = 0
    for idx, (images, la) in enumerate(train_loader):
        labels = process_lx(la)
        pred = resnet_model(images) + lora(images)  
        loss = loss_fn(pred, labels)  
        loss.backward()  
        optimizer.step()  
        optimizer.zero_grad()
        total_loss += loss.item() * bz

    if((base+ep)%5 == 0):
        loss_np = total_loss / 240
        print('ep=', base+ep,'/',base+epochs, ', loss=', loss_np)

#------ Saved to *.CKPT ---------------------------------------
FILE = base_path + 'LORA_for_RESNET_ep100.ckpt'
torch.save(lora.state_dict(), FILE)
print('\nsaved to ' + FILE)

#-------------------------------------------------------------------
def test_lora_resnet():
    correct = 0
    total = 0
    wrong_counts = [0 for i in range(3)]
    with torch.no_grad():
        for idx, (images, la) in enumerate(train_loader):              
            labels = process_lx(la)
            prediction = resnet_model(images)+ lora(images)
            for idx, zv in enumerate(prediction):
                if torch.argmax(zv) == labels[idx]:
                    correct +=1
                else:
                    wrong_counts[la[idx]] +=1
                total +=1
               
    print('\n', f'Accuracy: {round(correct/total, 3)}')
    for i in range(len(wrong_counts)):
        print(f'wrong counts for the class {i}: {wrong_counts[i]}')

#----------------------------------------------------------------
print('\n------LoRA 協同測試: ------')
test_lora_resnet()
#-----------------------------------------------------------------
#END

請仔細觀察這程式的執行情境:單純使用CPU環境(沒有使用GPU)搭配一般Pytorch框架,並在Windows或Ubuntu裡展開協同訓練。

例如,在Ubuntu環境裡,使用命令(Command):

就展開訓練100回合,執行結果顯示如下圖:

圖4:訓練環境是:CPU+Pytorch 3.10搭配Ubuntu

圖4:訓練環境是:CPU+Pytorch 3.10搭配Ubuntu

 

從上述的輸出結果,於是我們可以觀察到:

【協同訓練之前的預測準確率】 當ResNet50在未加掛 LoRA時,其預測的準確率是:0.358。且斑馬圖像的誤辨次數為:24;而貓頭鷹的誤判次數則是:64,至於蘑菇的誤判次數為: 66。

【協同訓練花費時間】 這是單純使用CPU環境下進行訓練(沒有使用GPU),我們把LoRA外掛到ResNet50,然後進行協同訓練100回合,其訓練的耗時為:405 秒。

【協同訓練之後的預測準確率】 當我們完成協同訓練100回合之後,再拿訓練資料集的<斑馬、貓頭鷹、蘑菇>圖像(一樣240張)來檢驗看看<ResNet50 + LoRA>整合模型的預測能力。結果發現了,這整合模型的預測準確度高達100%了。亦即,誤判次數皆是:0。

結語

在這篇文章中,我們先以單純CPU環境來訓練<ResNet50 + LoRA>的整合模型;並記錄其訓練100回合的花費時間長度(即405秒)。

接下來的文章我們將使用更高等級的Intel GPU環境來進行<ResNet50 + LoRA>的協同訓練,並展示相關的軟硬體技術,也呈現各種等級GPU環境的訓練效能提升的幅度,成為促進AI生態未來發展的強大力量。

 

高煥堂

訂閱MakerPRO知識充電報

與40000位開發者一同掌握科技創新的技術資訊!

Author: 高煥堂

擁有40多年軟硬體整合設計經驗,專精Android終端平台、AI、Docker容器、VR、AI(人工智慧)、IC軟硬整合技術與大數據應用。 近5年來,從事於AI普及化教育工作,目前擔任銘傳大學AI課程、長庚智慧醫療研究所AI課程授課老師。也擔任永春國小、東園國小、立志中學、君毅中學、永春高中等學校的AI師資培育工作。

Share This Post On
468 ad

Submit a Comment

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *