作者:高煥堂、鄭仲平
本文將說明如何善用英特爾(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的圖像
當您把這圖片提交給ResNet50,它會瞬間探索並進行分類,然後告訴您:我預測這是大熊貓(Giant panda)。
簡介LoRA
隨著大語言模型(LLM)等大模型日益繁榮發展,基於這些大模型的遷移學習(Transfer learning),將其預訓練好的模型加以微調(Fine tune),來適應到下游的各項新任務,已經成為熱門的議題。關於微調技術,其中LoRA是一種資源消耗較小的訓練方法,它能在較少訓練參數時就得到比較穩定的效果。
LoRA的全名是:Low-Rank Adaptation of Large Language Models (及大語言模型的低階適應)。使用這種LoRA微調方法進行訓練時,並不需要調整原(大)模型的參數值(圖2裡的藍色部分),而只需要訓練LoRA模型的參數(圖2裡的棕色部分)。

圖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預訓練模型。程式碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63 # 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&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('本地分類標籤:斑馬-->0, 貓頭鷹-->1, 蘑菇-->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屬於大模型,其泛化能力很好。然而,對於本範例的較少類別的預測(推論)準確度就常顯得不足。現在,就拿本範例的訓練圖像集來檢測一下。程式碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77 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&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的架構圖 (來源: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的參數合併起來(即上圖右方的橘色部分。請來看看程式碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143 # 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&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
從上述的輸出結果,於是我們可以觀察到:
【協同訓練之前的預測準確率】 當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生態未來發展的強大力量。
- LoRA微調三步驟:以大語言模型MT5為例 - 2024/05/02
- 為什麼Gemma採取Decoder-Only Transformer架構呢? - 2024/04/08
- 如何從0訓練企業自用Gemma模型 - 2024/04/03
訂閱MakerPRO知識充電報
與40000位開發者一同掌握科技創新的技術資訊!