手勢圖像識別實戰(LeNet模型) 原創
前言
上一章內容我們初步了解了卷積、卷積神經網絡、卷積神經網絡的搭建過程以及經典的LeNet網絡結構,本篇內容將基于LeNet網絡結構,實現手勢識別。
手勢識別
數據集介紹
在開展手勢識別之前,我們需要先下載并初步了解數據集的情況。
數據下載地址
下載地址:手勢識別數據集
├── train # 訓練集
├── G0 # 手勢0
├── IMG_1118.jpg # 手勢0的圖片
├──...
├── G1 # 手勢1
├── IMG_1119.jpg
├──...
├──...
├── G9 # 手勢9
├──test# 測試集
├── G0
├──...
├── G9
項目流程
在《【課程總結】Day8(上):深度學習基本流程》中,我們已了解到深度學習的基本流程為:
- 數據預處理 1.1 數據讀取 1.2 數據切分 1.3 數據規范化
- 批量化打包數據
- 模型搭建
- 籌備訓練
- 訓練模型 5.1 定義監控指標和方法 5.2 實現訓練過程 5.3 開始訓練
因此,本次項目也采用如上的基本流程。
數據預處理
由上述目錄結構可知,我們需要在訓練前使用DataLoader將數據集打包成適合訓練的格式,因此需要解決2個問題:
問題1:如何記錄標簽數據和圖片數據
解決方法:
- 獲取標簽:上述目錄中的G0、G1、G2...G9文件夾名稱即為手勢標簽,因此我們可以通過os.listdir()函數獲取文件夾名稱。
- 保存標簽:將上述遍歷的G0、G1、G2...G9文件夾名稱保存到列表?
?label_train?
?中,方便后續使用。 - 獲取圖片路徑:通過os.listdir()函數獲取文件夾中的圖片名稱,從而獲取圖片路徑。
- 保存圖片路徑:將上述遍歷的圖片路徑保存到列表?
?img_train?
?中,方便后續使用。
# 讀取gestures\train\G0目錄下的所有圖片路徑,添加至list中
import os
import random
import numpy as np
import cv2
defload_img_label(train_root):
img_train =[]
label_train =[]
for label in os.listdir(train_root):
label_path = os.path.join(train_root, label)
# 排除掉.開頭的文件
if label.startswith('.'):
continue
for img_name in os.listdir(label_path):
img_path = os.path.join(label_path, img_name)
img_train.append(img_path)
label_train.append(label)
return img_train, label_train
# 1,讀取基圖像的本信息
root ="gestures"
# 1,訓練集
train_root = os.path.join(root,'train')
train_img, train_label = load_img_label(train_root)
# 2,測試集
test_root = os.path.join(root,'test')
test_img, test_label = load_img_label(test_root)
lable_list =list(set(train_label))
lable_list.sort()
# 3,構建標簽字典
label2idx ={label: idx for idx, label inenumerate(lable_list)}
idx2label ={idx: label for idx, label inenumerate(lable_list)}
print(label2idx)
print(idx2label)
問題2:如何將圖片和標簽數據打包成適合訓練的格式
解決方法:
- 構建自定義數據集類GesturesDataset
- 重寫__getitem__(),len(),init()方法
- 在__getitem__()方法中:
- 使用cv2.imread()讀取圖片
- 使用cv2.resize()調整圖片大小
- 將圖像轉為numpy數組
- 對矩陣數組中的數據進行歸一化處理,規范化為[-1, 1]
- 使用torch將數據轉為張量
- 將數據從圖片數據的[H(高度), W(寬度), C(通道數)]轉維度為[N(批量個數), H(高度), W(寬度), C(通道數)]
- 將標簽轉為數字,例如:G0 -> 0, G1 -> 1, G2 -> 2, ..., G9 -> 9
- 將標簽轉為張量
import torch
from torch.utils.data importDataset
classGesturesDataset(Dataset):
"""
自定義數據集
"""
def__init__(self, X, y):
self.X = X
self.y = y
def__len__(self):
returnlen(self.X)
def__getitem__(self, idx):
img_path = self.X[idx]
img_label = self.y[idx]
# 1,讀取圖像
img = cv2.imread(img_path)
# 2,圖像轉為32*32
img = cv2.resize(img,(32,32))
# 3,圖像轉為numpy數組
img = np.array(img)
# 4,數據規范化到 [-1, 1]
img = img /255.0
img =(img -0.5)/0.5
# 5,數據轉為torch張量
img = torch.tensor(img, dtype=torch.float32)
# 6,數據轉維度 [H, W, C]
img = img.permute(2,0,1)
# 7,標簽轉為數字
label = label2idx[img_label]
label = torch.tensor(label, dtype=torch.long)
return img, label
模型搭建
本次模型使用LeNet網絡結構,相關結構已在《【課程總結】Day10:卷積網絡的基本組件》闡述,本次過程不再贅述。
import torch
from torch import nn
classConvBlock(nn.Module):
"""
一層卷積:
- 卷積層
- 批規范化層
- 激活層
"""
def__init__(self, in_channels, out_channels,
kernel_size=3, stride=1, padding=1):
super().__init__()
self.conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels,
kernel_size=kernel_size, stride=stride,padding=padding)
self.bn = nn.BatchNorm2d(num_features=out_channels)
self.relu = nn.ReLU()
defforward(self, x):
x = self.conv(x)
x = self.bn(x)
x = self.relu(x)
return x
classLeNet(nn.Module):
def__init__(self, num_classes=10):
super().__init__()
# 1, 特征抽取部分
self.feature_extractor = nn.Sequential(
# 卷積層1
ConvBlock(in_channels=3,
out_channels=6,
kernel_size=5,
stride=1,
padding=0),
# 亞采樣(池化)
nn.MaxPool2d(kernel_size=2, stride=2, padding=0),
# 卷積層2
ConvBlock(in_channels=6,
out_channels=16,
kernel_size=5,
stride=1,
padding=0),
# 亞采樣(池化)
nn.MaxPool2d(kernel_size=2, stride=2, padding=0),
)
# 2, 分類
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(in_features=400, out_features=120),
nn.ReLU(),
nn.Linear(in_features=120, out_features=84),
nn.ReLU(),
nn.Linear(in_features=84, out_features=num_classes)
)
defforward(self, x):
# 1, 提取特征
x = self.feature_extractor(x)
# 2, 分類輸出
x = self.classifier(x)
return x
- 將以上代碼單獨封裝為model.py文件,方便后續直接import。
- 在主程序中使用以下方式直接調用即可:?
?from models import LeNet
?
model = LeNet()?
籌備訓練
由于計算的數據量較大,所以我們需要借助torch以及GPU來提升訓練速度。
# 檢測是否有可用的CUDA設備,如果有則使用第一個可用的CUDA設備,否則使用CPU
device ="cuda:0"if torch.cuda.is_available()else"cpu"
# 將模型移動到指定的設備(CUDA或CPU)
model.to(device=device)
# 設置訓練的總輪數
epochs =80
# 設置學習率
lr =1e-3
# 定義損失函數為交叉熵損失
loss_fn = nn.CrossEntropyLoss()
# 定義優化器為隨機梯度下降(SGD),傳入模型的參數和學習率
optimizer = torch.optim.SGD(params=model.parameters(), lr=lr)
模型評估
為了觀察訓練過程情況,定義模型評估函數:
# 準確率計算
defget_acc(data_loader):
accs =[]
model.eval()
with torch.no_grad():
for X, y in data_loader:
X = X.to(device=device)
y = y.to(device=device)
y_pred = model(X)
y_pred = y_pred.argmax(dim=-1)
acc =(y_pred == y).to(torch.float32).mean().item()
accs.append(acc)
final_acc =round(number=sum(accs)/len(accs), ndigits=5)
return final_acc
實現訓練過程
# 訓練過程
deftrain():
train_accs =[]
test_accs =[]
cur_test_acc =0
# 1,訓練之前,檢測一下準確率
train_acc = get_acc(data_loader=train_dataloader)
test_acc = get_acc(data_loader=test_dataloader)
train_accs.append(train_acc)
test_accs.append(test_acc)
print(f"訓練之前:train_acc: {train_acc},test_acc: {test_acc}")
# 每一輪次
for epoch inrange(epochs):
# 模型設置為 train 模式
model.train()
# 計時
start_train = time.time()
# 每一批量
for X, y in train_dataloader:
# 數據搬家
X = X.to(device=device)
y = y.to(device=device)
# 1,正向傳播
y_pred = model(X)
# 2,計算損失
loss = loss_fn(y_pred, y)
# 3,反向傳播
loss.backward()
# 4,優化一步
optimizer.step()
# 5,清空梯度
optimizer.zero_grad()
# 計時結束
stop_train = time.time()
# 測試準確率
train_acc = get_acc(data_loader=train_dataloader)
test_acc = get_acc(data_loader=test_dataloader)
train_accs.append(train_acc)
test_accs.append(test_acc)
# 保存模型
if cur_test_acc < test_acc:
cur_test_acc = test_acc
# 保存最好模型
torch.save(obj=model.state_dict(), f="lenet_best.pt")
# 保存最后模型
torch.save(obj=model.state_dict(), f="lenet_last.pt")
# 格式化輸出日志
print(f"""
當前是第 {epoch + 1} 輪:
------------------------------------------------------------
| 訓練準確率 (train_acc) | 測試準確率 (test_acc) | 運行時間 (elapsed_time) |
------------------------------------------------------------
| {train_acc:<18} | {test_acc:<17} | {round(number=stop_train - start_train, ndigits=3)} 秒 |
------------------------------------------------------------
""")
return train_accs, test_accs
開始訓練
train_accs, test_accs = train()
圖形化監控數據
plt.plot(train_accs, label="train_acc")
plt.plot(test_accs, label="train_acc")
plt.legend()
plt.grid()
plt.xlabel(xlabel='epoch')
plt.ylabel(ylabel="acc")
plt.title(label="LeNet Training Process")
運行結果:
通過以上執行過程可以看到,經過80輪訓練后,LeNet模型在訓練集上的準確率達到99%,在測試集上的準確率達到94%。
模型預測
接下來,我們使用streamlit實現一個前端頁面,用戶在頁面上輸入圖片,模型會自動識別圖片中的手勢。
整體實現流程:
- 創建一個streamlit應用,并導入相關依賴。
- 顯示當前設備是GPU設備還是CPU
- 加載模型
- 使用streamlit.file_uploader顯示上傳圖片控件
- 使用streamlit.image顯示上傳的圖片
- 使用加載的模型進行預測 6.1 讀取圖像 6.2 圖像預處理 6.3 圖形轉為張量 6.4 轉換圖形的維度為[C, H, W] 6.5 新建一個批量維度[N, C, H, W] 6.6 數據搬家 6.7 模型設為評估模式 6.8 模型預測 6.9 預測結果轉為標簽 0 → G0, 1 → G1, 2 → G2, 3 → G3, 4 → G4, 5 → G5 6.10 返回標簽結果
import streamlit
import torch
import os
import numpy as np
from PIL importImage
from models importLeNet
# 生成idx2label字典,用于顯示預測結果
idx2label ={
0:'G0',
1:'G1',
2:'G2',
3:'G3',
4:'G4',
5:'G5',
6:'G6',
7:'G7',
8:'G8',
9:'G9'
}
definfer(img_path, model, device, idx2label):
"""
輸入:圖像地址
輸出:預測類別
"""
# 1,讀取圖像
ifnot os.path.exists(img_path):
raiseFileNotFoundError("文件沒找到")
# 2, 判斷當前局部變量中是否有model
# if "m1" not in globals() or not isinstance(globals()["m1"], LeNet):
# raise ValueError("m1模型不存在")
# 3,讀取圖像
img =Image.open(fp=img_path)
# 4,預處理
img = img.resize((32,32))
img = np.array(img)
img = img /255
img =(img -0.5)/0.5
# 5, 轉張量
img = torch.tensor(data=img, dtype=torch.float32)
# 6, 轉換維度
img = img.permute(dims=(2,0,1))
# 7, 新增一個批量維度
img = img.unsqueeze(dim=0)
# 8,數據搬家
img = img.to(device=device)
# 9,模型設為評估模式
model.eval()
# 10,無梯度環境
with torch.no_grad():
# 11,正向傳播
y_pred = m1(img)
# 12, 解析結果
y_pred = y_pred.argmax(dim=-1).item()
# 13,標簽轉換
label = idx2label.get(y_pred)
# 14, 返回結果
return label
if __name__ =="__main__":
# 1, 顯示當前設備是GPU設備還是CPU
# 檢測設備
device ="cuda"if torch.cuda.is_available()else"cpu"
streamlit.write(f"當前設備是{device}設備")
# 2, 加載模型
m1 =LeNet()
m1.to(device=device)
# 加載權重
m1.load_state_dict(state_dict=torch.load(f="lenet_best.pt", map_locatinotallow=device),
strict=False)
ifnotisinstance(m1,LeNet):
raiseValueError("模型加載失敗")
# 3, 上傳一張圖片
img_path = streamlit.file_uploader(label="上傳一張圖片",type=["png","jpg","jpeg"])
# 3.1, 將上傳的圖像文件保存到臨時文件
if img_path:
withopen(file="temp_img.jpg", mode="wb")as f:
f.write(img_path.getvalue())
img_path ="temp_img.jpg"
# 4, 顯示上傳的圖片
if img_path:
img =Image.open(fp=img_path)
streamlit.image(image=img, captinotallow="上傳的圖片", use_column_width=True)
# 5, 加載本地的lenet_best.pt模型
if img_path:
label = infer(img_path=img_path, model=m1, device=device, idx2label=idx2label)
streamlit.write(f"預測結果是{label}")
運行結果:
圖片
內容小結
- 回顧深度學習的整體流程,仍然是:數據預處理→批量化打包數據→模型搭建→訓練模型→模型評估→模型預測
- 圖片數據預處理時,批量化打包數據需要構造為[N, C, H, W]的格式
- 預處理的過程大致為:讀取圖片→調整圖片大小→轉為numpy數組→歸一化→轉為張量→調整維度→標簽轉為數字→轉為張量,該過程需要在自定義數據集的__getitem__函數中完成
- 模型構建使用的是LeNet模型,該模型定義可以單獨在models.py中實現,訓練代碼中直接import引用即可
- 訓練過程以及訓練時的監控過程,與前兩章學習的深度學習訓練過程是一樣的
本文轉載自公眾號一起AI技術 作者:熱情的Dongming
?著作權歸作者所有,如需轉載,請注明出處,否則將追究法律責任
已于2024-12-5 11:14:19修改
贊
收藏
回復
分享
微博
QQ
微信
舉報

回復
相關推薦