第一章:計算機視覺中圖像的基礎認知
第二章:計算機視覺:卷積神經網絡(CNN)基本概念(一)
第三章:計算機視覺:卷積神經網絡(CNN)基本概念(二)
第四章:搭建一個經典的LeNet5神經網絡(附代碼)
第五章:計算機視覺:神經網絡實戰之手勢識別(附代碼)
第六章:計算機視覺:目標檢測從簡單到容易(一)(附代碼)
一、簡介
手勢識別作為計算機視覺領域的一個重要應用,通過分析圖像或視頻序列來識別特定的手勢動作。下面以0-9手勢數字識別為案例,從數據加載、預處理到模型訓練的手勢識別全流程,基于PyTorch框架構建經典卷積神經網絡。
數字圖片的存放的目錄結構:
gesture是圖片的根目錄,二級子目錄分為訓練集 train和測試集test。三級目錄就是每個數字的英文單詞作為目錄名字,這也是每張圖片的歸屬標簽,三級目錄里面存放的是具體的數字圖片,比如 one 目錄下,存放166 張不同的數字 1 手勢圖片
![]() | ![]() |
二、數據讀取與預處理
2.1 數據加載策略
首先定義一個函數 load_dataset,用于動態加載一個圖像數據集的路徑和對應的標簽。適用于以文件夾結構組織的數據集,其中每個子文件夾的名字代表一個類別(或標簽),并且該子文件夾內包含了屬于這個類別的所有圖像文件。
import osdef load_dataset(root_path):"""動態加載數據集路徑:param root_path: 數據集根目錄(需包含以標簽命名的子目錄):return: 路徑列表, 標簽列表"""paths, labels = [], []for label in os.listdir(root_path):# 忽略隱藏文件或目錄if label.startswith('.'):continue# 獲取每個標簽對應的子目錄路徑label_path = os.path.join(root_path, label)# 遍歷子目錄下的所有文件for file in os.listdir(label_path):# 忽略隱藏文件或目錄if file.startswith('.'):continue# 構造完整的文件路徑file_path = os.path.join(label_path, file)# 將文件路徑添加到路徑列表中paths.append(file_path)# 將對應的標簽添加到標簽列表中labels.append(label)return paths, labels
加載數據整體思路:
-
初始化兩個空列表 paths 和 labels,分別用于存儲圖像文件的完整路徑和對應的類別標簽。
-
使用 os.listdir(root_path) 列出根目錄下的所有項目(文件或文件夾)。
-
對于每一個子目錄,即類別標簽,構造其完整路徑 label_path。
-
遍歷子目錄中的所有文件,對于每個文件構造其完整路徑,并將這些路徑添加到 paths 列表中;同時,將對應的類別標簽添加到 labels 列表中。
-
最后返回 paths 和 labels 列表。
實際調用示例
train_root = "gesture/train"
test_root = "gesture/test"
train_paths, train_labels = load_dataset(train_root)
test_paths, test_labels = load_dataset(test_root)
- 指定訓練集(train)和測試集(test)的根目錄分別為
gesture/train
和gesture/test
。 - 調用
load_dataset
函數,分別對訓練集和測試集進行處理,獲取它們的文件路徑和標簽列表。 - train_paths 和 train_labels 分別保存了訓練集中所有圖像的路徑及其對應的類別標簽;同樣地,test_paths 和 test_labels 保存了測試集的相關信息。
2.2 標簽編碼映射
建立標簽與數字ID的雙向映射:
labels = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]
label2idx = {label:idx for idx, label in enumerate(labels)}
idx2label = {idx:label for label, idx in label2idx.items()}# 轉換示例
print(label2idx["three"]) # 輸出: 3
print(idx2label[5]) # 輸出: "five"
三、數據加載
3.1 自定義數據集類
定義一個名為 GestureDataset
的自定義數據集類,該類繼承自 PyTorch 的 torch.utils.data.Dataset
類。這個類主要用于手勢識別任務中處理圖像數據集,包括圖像的讀取、轉換以及標簽的處理。
# 引入必要的工具類
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
from PIL import Image
from torchvision import transforms
import torchclass GestureDataset(Dataset):"""自定義手勢識別數據集"""def __init__(self, X, y):"""初始化"""# 圖像文件路徑列表self.X = X# 對應的標簽列表self.y = ydef __getitem__(self, idx):"""實現:- 按下標來索引一個樣本"""# 獲取圖像路徑img_path = self.X[idx]# 讀取圖像img = Image.open(fp=img_path)# 統一大小img = img.resize((32, 32))# 轉張量 [C, H, W]# 轉換為PyTorch張量(范圍[0,1])img = transforms.ToTensor()(img)# 標準化到[-1,1]范圍img = transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])(img)# 讀取標簽img_label = self.y[idx]# 標簽轉 idimg_idx = label2idx.get(img_label)# 轉張量label = torch.tensor(data=img_idx, dtype=torch.long)return img, labeldef __len__(self):"""返回該數據集的樣本個數"""return len(self.X)
- 引入必要的工具和庫,用于創建自定義的數據集 (Dataset) 和數據加載器 (DataLoader),處理圖像 (PIL.Image),應用圖像變換 (transforms) 以及張量操作 (torch)。
GestureDataset
類接受兩個參數:X 和 y。- X 是一個包含所有圖像文件路徑的列表。
- y 是與這些圖像對應的標簽列表。
- 實現
__getitem__
方法:- 根據給定的索引 idx,返回一個樣本(即一張圖像及其對應標簽)。
- 首先,通過提供的索引從 X 列表中獲取圖像的路徑,并使用 Image.open() 打開圖像。
- 使用 .resize((32, 32)) 將圖像調整為統一大小。
- 使用 transforms.ToTensor() 將圖像轉換為PyTorch張量,并將像素值縮放到[0, 1]范圍內。
- 使用 transforms.Normalize() 對張量進行標準化處理,使每個通道的像素值均值為0.5,標準差也為0.5,從而將像素值映射到[-1, 1]范圍內。
- 獲取該圖像的標簽 img_label,并將其轉換為整數索引 img_idx。
- 最后,將標簽轉換為PyTorch張量類型,并返回圖像張量和標簽張量。
- 實現
__len__
方法:此方法返回數據集中樣本的數量,即圖像路徑列表 X 的長度。
為什么要將圖像轉換為PyTorch張量?
從圖像到張量
:深度學習模型通常接受張量作為輸入,而不是原始的圖像格式(如PIL Image或NumPy數組)。因此,需要將讀取的圖像轉換為張量格式。
為什么要將像素值縮放到[0, 1]范圍內?
數值范圍調整
:原始圖像的像素值通常位于[0, 255]范圍內(對于8位圖像),這意味著每個像素的值是一個介于0到255之間的整數。然而,大多數現代神經網絡期望輸入數據的數值范圍在[0, 1]或[-1, 1]之間。通過使用ToTensor(),每個像素值除以255,從而將其縮放到[0, 1]范圍。
為什么要對張量進行標準化處理?
transforms.Normalize(mean, std)歸一化是另一個重要的預處理步驟,它涉及到對數據進行均值和標準差的標準化處理。這一步驟有助于加速模型的收斂過程,并可能提高模型的性能。
- 加快收斂速度:通過對輸入數據進行標準化處理,可以使得損失函數表面更加平滑,從而幫助梯度下降算法更快地找到最優解。這是因為當輸入特征具有相似的尺度時,優化算法能夠更有效地更新權重。
- 提升模型表現:標準化后的輸入可以使不同特征(在這個場景中指的是圖像的不同通道)具有相同的尺度,避免某些特征因為尺度過大或過小而對模型的學習產生不成比例的影響。這對于深層網絡尤為重要,因為它有助于防止梯度消失或爆炸的問題。
- 具體參數說明:
mean=[0.5, 0.5, 0.5] 和 std=[0.5, 0.5, 0.5] 分別表示三個通道(通常是RGB圖像的紅、綠、藍通道)的平均值和標準差。這些值用于將輸入張量的每個元素標準化到接近正態分布的形式,即均值為0,方差為1。具體來說,這里的設置會將像素值從[0, 1]范圍映射到[-1, 1]范圍。 - 計算公式:對于每個像素值 x,經過標準化后的新值為 (x - mean) / std。根據上面的例子,如果一個像素值原來是0.75(已經通過
ToTensor()
轉換到了[0, 1]范圍),那么標準化后的值將是 (0.75 - 0.5) / 0.5 = 0.5。
transforms.ToTensor()
和 transforms.Normalize()
這兩個操作是為了確保輸入到神經網絡的數據具有一致的尺度和分布,這樣不僅有助于提高模型訓練的效率,還可能改善最終模型的表現。這是因為在良好的初始化條件下,模型能夠更穩定和高效地學習輸入數據中的模式。
3.2 數據加載器配置
使用自定義的數據集類 GestureDataset 來創建訓練集和測試集的數據加載器 DataLoader,以便于后續的模型訓練和評估。
# 訓練集加載器
train_dataset = GestureDataset(X=train_paths, y=train_labels)
train_dataloader = DataLoader(dataset=train_dataset, shuffle=True, batch_size=16)
# 測試集加載器
test_dataset = GestureDataset(X=test_paths, y=test_labels)
test_dataloader = DataLoader(dataset=test_dataset, shuffle=False, batch_size=32)# 測試
for X, y in test_dataloader:print(X.shape)print(y.shape)break
GestureDataset(X=train_paths, y=train_labels)
:- 這里實例化了 GestureDataset 類,傳入兩個參數:X=train_paths 和 y=train_labels。
- train_paths 是一個包含所有訓練圖像路徑的列表。
- train_labels 是與這些圖像對應的標簽列表。
- 通過這種方式,train_dataset 對象可以訪問每個樣本(圖像及其標簽),并對其進行必要的預處理(如調整大小、轉換為張量等)。
DataLoader(dataset=train_dataset, shuffle=True, batch_size=16)
:- 使用 PyTorch 的 DataLoader 創建了一個數據加載器 train_dataloader。
- dataset=train_dataset: 指定了要加載的數據集對象。
- shuffle=True: 表示在每個epoch開始時,將數據集中的樣本順序打亂。這對于訓練集來說非常重要,因為它可以幫助模型更好地泛化,避免模型學習到數據順序的相關性。對于測試集,通常不需要打亂順序,因為我們更關注模型在未見過的數據上的表現,而不是訓練過程中的隨機性。
- batch_size=16: 設置了每個批次的大小為16,即每次迭代時從數據集中取出16個樣本進行訓練。選擇合適的批量大小對于訓練效率和模型性能都很關鍵,但也需要考慮內存限制。
四、卷積神經網絡構建
4.1 網絡架構設計
基于改進版LeNet-5卷積神經網絡實現,LeNet-5網絡在上一篇《搭建一個經典的LeNet5神經網絡》中有詳細介紹。
定義一個名為 GestureNet 的神經網絡類,該類繼承自 PyTorch 的 nn.Module。這個網絡設計用于手勢識別任務。
import torch.nn as nnclass GestureNet(nn.Module):# 定義了 GestureNet 類并初始化了父類 nn.Module。# 參數 in_channels 默認設置為3,通常代表輸入圖像的通道數(如RGB圖像)。# num_classes 參數指定了模型最終輸出的類別數量,默認設置為10,適用于一個包含10個類別的分類任務。def __init__(self, in_channels=3, num_classes=10):super().__init__()# 特征提取器self.features = nn.Sequential(nn.Conv2d(in_channels, 6, kernel_size=5),nn.ReLU(inplace=True),nn.MaxPool2d(2, 2),nn.Conv2d(6, 16, kernel_size=5),nn.ReLU(inplace=True),nn.MaxPool2d(2, 2))# 分類器self.classifier = nn.Sequential(nn.Flatten(),nn.Linear(16*5*5, 120),nn.ReLU(inplace=True),nn.Dropout(0.5),nn.Linear(120, 84),nn.ReLU(inplace=True),nn.Dropout(0.5),nn.Linear(84, num_classes))def forward(self, x):x = self.features(x)x = self.classifier(x)return x
代碼解釋:
self.features = nn.Sequential(...)
: 功能:這部分負責從輸入圖像中提取有用的特征。- 第一層是一個卷積層 (nn.Conv2d),它將輸入通道數轉換為6個輸出通道,使用5x5大小的卷積核。
- 接著是一個ReLU激活函數 (nn.ReLU),這里使用了 inplace=True 來節省內存,直接在原地修改輸入張量而不是創建新的張量。
- 然后是最大池化層 (nn.MaxPool2d),它使用2x2的窗口大小進行下采樣,這有助于減少數據維度同時保留主要特征。
- 接下來的第二層卷積層 (nn.Conv2d) 將通道數從6增加到16,同樣使用5x5的卷積核。
- 最后再經過ReLU激活函數和最大池化層處理。
self.classifier = nn.Sequential(...)
:功能:這部分將特征提取器提取的特征映射到具體的類別上,完成分類任務。- 首先通過 nn.Flatten() 層將多維的特征圖展平成一維向量。假設輸入圖像大小為32x32,經過兩次池化操作后,特征圖大小變為8x8(考慮邊緣丟失),再乘以16個通道,因此這里的 16 x 5 x 5 可能需要根據實際的輸入尺寸調整為正確的值(可能是 16 x 8 x 8 或者其他值,取決于你的輸入尺寸和網絡的具體配置)。
- 然后是一個全連接層 (nn.Linear),將展平后的特征映射到120維的空間。
- 使用ReLU激活函數和Dropout (nn.Dropout),后者用于防止過擬合,此處的丟棄概率為0.5。
- 接下來是另一個全連接層,將特征維度減少到84。
- 再次應用ReLU激活函數和Dropout。
- 最終,最后一個全連接層將特征映射到指定的類別數量 (num_classes)。
forward(self, x)
:這個方法定義了數據如何從前向后流經網絡。- 輸入 x 首先通過特征提取器 self.features 提取特征。
- 然后這些特征被送入分類器 self.classifier 進行分類處理。
- 最終返回分類結果。
上面是構建了一個簡單的卷積神經網絡用于手勢識別,包括特征提取和分類兩個階段,并且在分類階段引入了Dropout來提高模型的泛化能力。
4.2 網絡維度變化
輸入圖像32x32的完整計算過程:
層 | 輸出尺寸 | 參數計算 |
---|---|---|
Input | 3×32×32 | - |
Conv1(5x5) | 6×28×28 | (5×5×3+1)×6 = 456 |
MaxPool1 | 6×14×14 | - |
Conv2(5x5) | 16×10×10 | (5×5×6+1)×16 = 2416 |
MaxPool2 | 16×5×5 | - |
Flatten | 400 | - |
FC1 | 120 | (400+1)×120 = 48120 |
FC2 | 84 | (120+1)×84 = 10164 |
Output | 10 | (84+1)×10 = 850 |
總參數量: 456 + 2416 + 48120 + 10164 + 850 = 61,706
五、模型訓練與優化
5.1 訓練配置
設置一個用于訓練神經網絡的基本環境,包括設備選擇、模型實例化、優化器配置、損失函數定義以及學習率調度器的配置。
# 這行代碼檢查系統是否支持CUDA(即是否有可用的NVIDIA GPU),如果有,則將device變量設置為 "cuda",否則設置為 "cpu"。
# 確保模型的參數和運算都在選定的設備上進行,以充分利用硬件資源。
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = GestureNet().to(device)
# 使用Adam優化算法來更新模型參數。Adam是一種常用的自適應學習率優化算法,適用于大多數深度學習任務。
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)
# 定義交叉熵損失函數,這是分類問題中最常用的損失函數之一,特別適合于多分類任務。
loss_fn = nn.CrossEntropyLoss()
# 配置了一個學習率調度器 ReduceLROnPlateau,它可以根據某個監控指標的表現動態調整學習率。
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=3)
參數解釋:
model.parameters()
:提供模型的所有可學習參數給優化器。lr=0.001
:設定初始學習率為0.001。weight_decay=1e-4
:加入L2正則化,防止過擬合。這里的權重衰減系數設為0.0001。optimizer
:要調整的學習率所屬的優化器。'min'
:模式設定為最小化監控指標(如驗證損失)。如果監控指標停止下降,就會觸發學習率的降低。patience=3
:容忍度設為3,意味著如果連續3個epoch監控指標都沒有改善,就會降低學習率。
利用Adam優化器和交叉熵損失函數進行有效的訓練,并通過學習率調度器根據訓練情況自動調整學習率,從而可能提高模型的最終性能。
5.2 訓練循環實現
定義一個名為 train_epoch 的函數,用于執行一個epoch的模型訓練。通過遍歷提供的數據加載器(loader),對模型進行前向傳播、計算損失、反向傳播以及參數更新。
def train_epoch(model, loader, optimizer, loss_fn):# 設置模型為訓練模式model.train()# 初始化變量 total_loss 用于累積整個epoch中的所有批次損失。這有助于最后計算平均損失。total_loss = 0# 使用 for 循環遍歷數據加載器 loader 提供的所有批次數據 (inputs, labels)。for inputs, labels in loader:# 將輸入數據和標簽都移動到指定的設備(CPU或GPU)上,以便與模型保持一致。inputs, labels = inputs.to(device), labels.to(device)"""清空梯度:optimizer.zero_grad() 清除之前的梯度信息,因為PyTorch默認會累加梯度。前向傳播:outputs = model(inputs) 執行一次前向傳播,獲取模型的預測輸出。計算損失:loss = loss_fn(outputs, labels) 根據預測輸出和真實標簽計算損失。反向傳播:loss.backward() 執行反向傳播,計算每個參數的梯度。更新權重:optimizer.step() 使用計算出的梯度來更新模型的權重。"""optimizer.zero_grad()outputs = model(inputs)loss = loss_fn(outputs, labels)loss.backward()optimizer.step()# loss.item() 獲取當前批次的標量損失值。# inputs.size(0) 獲取當前批次的樣本數量。# 將當前批次的損失乘以批次大小并累加到 total_loss 中,這樣做是為了計算整個數據集上的平均損失。total_loss += loss.item() * inputs.size(0)# 返回整個epoch的平均損失,即總損失除以數據集中樣本的數量。return total_loss / len(loader.dataset)
該函數接受四個參數:
model
: 要訓練的神經網絡模型。loader
: 數據加載器,提供批次的數據和標簽。optimizer
: 優化器,用于更新模型的權重。loss_fn
: 損失函數,用于評估模型預測值與真實標簽之間的誤差。
定義一個名為 validate 的函數,用于在驗證集或測試集上評估模型的性能。通過計算平均損失和分類準確率來衡量模型的表現。
def validate(model, loader, loss_fn):# 設置模型為推理模式model.eval()# total_loss: 用于累積整個數據集上的所有批次損失。total_loss = 0# correct: 記錄預測正確的樣本數,用于計算準確率。correct = 0# 使用 torch.no_grad() 上下文管理器,在這個塊內的所有操作都不會被追蹤用于自動求導。# 這對于推理階段非常有用,因為它減少了內存消耗并加快了計算速度,因為不需要計算梯度。with torch.no_grad():# 遍歷數據加載器提供的所有批次數據 (inputs, labels)。for inputs, labels in loader:# 將輸入數據和標簽都移動到指定的設備(CPU或GPU)上,以便與模型保持一致。inputs, labels = inputs.to(device), labels.to(device)# 執行一次前向傳播,獲取模型的預測輸出 outputs。outputs = model(inputs)# 根據預測輸出和真實標簽計算損失 loss。loss = loss_fn(outputs, labels)# 將當前批次的損失乘以批次大小并累加到 total_loss 中,這樣做是為了最后計算整個數據集上的平均損失。total_loss += loss.item() * inputs.size(0)# 獲取每個輸入的最大得分對應的類別索引(即模型的預測類別)。_, predicted = torch.max(outputs, 1)# 比較預測類別 predicted 和真實標簽 labels,統計預測正確的樣本數,并將其累加到 correct 變量中。correct += (predicted == labels).sum().item()# 計算整個數據集上的平均損失:total_loss 除以數據集中的樣本總數 len(loader.dataset)。# 計算分類準確率:正確預測的樣本數 correct 除以數據集中的樣本總數 len(loader.dataset)。# 最終返回這兩個指標作為函數的結果,分別是平均損失和準確率。return total_loss / len(loader.dataset), correct / len(loader.dataset)
該函數接受三個參數:
- model: 要評估的神經網絡模型。
- loader: 數據加載器,提供批次的數據和標簽(通常為驗證集或測試集)。
- loss_fn: 損失函數,用于評估模型預測值與真實標簽之間的誤差。
validate方法評估模型在驗證集或測試集上的表現,通過計算平均損失和準確率來量化模型的性能。這種評估對于監控模型的學習進度、調優超參數以及決定何時停止訓練非常重要。使用 torch.no_grad() 和 model.eval() 確保了在推理過程中不會進行不必要的梯度計算和參數更新,從而提高了效率和準確性。
5.3 完整訓練流程
這段代碼實現一個簡單的訓練循環,用于在一個數據集上訓練神經網絡模型,并在每個epoch后使用驗證集評估模型性能。同時保存最佳模型。
# 初始化一個變量 best_acc,用于記錄目前為止在驗證集上獲得的最佳準確率。初始值設為0。
best_acc = 0
# 循環執行總共50個epoch的訓練。你可以根據需要調整這個數字。
for epoch in range(50):# 執行一個epoch的訓練和驗證train_loss = train_epoch(model, train_dataloader, optimizer, loss_fn)val_loss, val_acc = validate(model, test_dataloader, loss_fn)# 學習率調度scheduler.step(val_loss)print(f"Epoch {epoch+1:02d}: "f"Train Loss: {train_loss:.4f} | "f"Val Loss: {val_loss:.4f} | "f"Accuracy: {val_acc:.2%}")# 保存最佳模型參數if val_acc > best_acc:best_acc = val_acctorch.save(model.state_dict(), "best_model.pth")
輸出內容:
Epoch 01: Train Loss: 2.2930 | Val Loss: 2.1288 | Accuracy: 36.00%
Epoch 02: Train Loss: 1.6239 | Val Loss: 0.9157 | Accuracy: 73.75%
Epoch 03: Train Loss: 1.0185 | Val Loss: 0.5172 | Accuracy: 86.00%
Epoch 04: Train Loss: 0.7497 | Val Loss: 0.3426 | Accuracy: 88.25%
Epoch 05: Train Loss: 0.5869 | Val Loss: 0.2387 | Accuracy: 93.00%
Epoch 06: Train Loss: 0.4685 | Val Loss: 0.2408 | Accuracy: 91.75%
Epoch 07: Train Loss: 0.3958 | Val Loss: 0.1606 | Accuracy: 94.75%
Epoch 08: Train Loss: 0.3526 | Val Loss: 0.1398 | Accuracy: 96.25%
Epoch 09: Train Loss: 0.2660 | Val Loss: 0.1457 | Accuracy: 95.00%
Epoch 10: Train Loss: 0.2468 | Val Loss: 0.1102 | Accuracy: 96.25%
Epoch 11: Train Loss: 0.2453 | Val Loss: 0.1406 | Accuracy: 94.75%
Epoch 12: Train Loss: 0.2144 | Val Loss: 0.1584 | Accuracy: 93.75%
Epoch 13: Train Loss: 0.2132 | Val Loss: 0.1025 | Accuracy: 96.00%
Epoch 14: Train Loss: 0.1924 | Val Loss: 0.0857 | Accuracy: 97.75%
Epoch 15: Train Loss: 0.1701 | Val Loss: 0.1847 | Accuracy: 93.50%
Epoch 16: Train Loss: 0.1622 | Val Loss: 0.0780 | Accuracy: 97.25%
Epoch 17: Train Loss: 0.1204 | Val Loss: 0.1193 | Accuracy: 95.00%
Epoch 18: Train Loss: 0.1181 | Val Loss: 0.0955 | Accuracy: 96.75%
Epoch 19: Train Loss: 0.1312 | Val Loss: 0.0799 | Accuracy: 97.75%
Epoch 20: Train Loss: 0.1139 | Val Loss: 0.1064 | Accuracy: 96.50%
Epoch 21: Train Loss: 0.0857 | Val Loss: 0.0972 | Accuracy: 96.75%
Epoch 22: Train Loss: 0.0808 | Val Loss: 0.0980 | Accuracy: 97.00%
Epoch 23: Train Loss: 0.0966 | Val Loss: 0.0870 | Accuracy: 96.75%
Epoch 24: Train Loss: 0.0744 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 25: Train Loss: 0.0722 | Val Loss: 0.0854 | Accuracy: 96.50%
Epoch 26: Train Loss: 0.0947 | Val Loss: 0.0844 | Accuracy: 96.50%
Epoch 27: Train Loss: 0.0698 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 28: Train Loss: 0.0873 | Val Loss: 0.0858 | Accuracy: 96.50%
Epoch 29: Train Loss: 0.0797 | Val Loss: 0.0857 | Accuracy: 96.50%
Epoch 30: Train Loss: 0.0622 | Val Loss: 0.0857 | Accuracy: 96.50%
Epoch 31: Train Loss: 0.0728 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 32: Train Loss: 0.0841 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 33: Train Loss: 0.0644 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 34: Train Loss: 0.0755 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 35: Train Loss: 0.0797 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 36: Train Loss: 0.0677 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 37: Train Loss: 0.0667 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 38: Train Loss: 0.0902 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 39: Train Loss: 0.0715 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 40: Train Loss: 0.0727 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 41: Train Loss: 0.0750 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 42: Train Loss: 0.0813 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 43: Train Loss: 0.0780 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 44: Train Loss: 0.0847 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 45: Train Loss: 0.0767 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 46: Train Loss: 0.0732 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 47: Train Loss: 0.0634 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 48: Train Loss: 0.0750 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 49: Train Loss: 0.0665 | Val Loss: 0.0856 | Accuracy: 96.50%
Epoch 50: Train Loss: 0.0736 | Val Loss: 0.0856 | Accuracy: 96.50%
訓練階段:調用 train_epoch 函數對模型進行一個epoch的訓練,并返回該epoch的平均訓練損失 train_loss。
驗證階段:調用 validate 函數評估模型在驗證集上的表現,返回驗證損失 val_loss 和分類準確率 val_acc。注意這里使用的是 test_loader,通常應使用 val_loader 來代表驗證集加載器,以避免直接在測試集上進行驗證。
scheduler.step(val_loss)
根據驗證集上的損失 val_loss 調整學習率。這里使用的是 ReduceLROnPlateau 調度器,當監測的指標(這里是驗證損失)停止改善時,會自動降低學習率,幫助模型跳出局部最優解或加速收斂。
打印出當前epoch的信息,包括:
- 當前是第幾個epoch ({epoch+1:02d} 確保輸出兩位數,例如01, 02…50)。
- 訓練損失 train_loss,保留四位小數。
- 驗證損失 val_loss,保留四位小數。
- 分類準確率 val_acc,以百分比形式顯示,并保留兩位小數。
六、圖像預測
定義一個名為 predict 的函數,用于對單個圖像進行預測。
加載一個預訓練的模型,對輸入圖像進行必要的預處理,然后使用該模型預測圖像的類別,并返回預測的標簽。
predict函數接受兩個參數:
- image_path: 輸入圖像的路徑。
- model_path: 預訓練模型的路徑,默認為 “best_model.pth”。
def predict(image_path, model_path="best_model.pth"):# 加載模型model = GestureNet()# 加載保存的最佳模型參數(權重)到模型中。model.load_state_dict(torch.load(model_path))# 將模型設置為評估模式,確保在推理時正確處理如 Dropout 和 Batch Normalization 等層的行為。model.eval()# 打開指定路徑的圖像,并通過 .convert('RGB') 確保其格式為 RGB。image = Image.open(image_path).convert('RGB')# 創建一系列圖像變換操作,包括調整大小、轉換為張量以及標準化處理。transform = transforms.Compose([transforms.Resize((32, 32)), # 將圖像調整為 32x32 像素大小。transforms.ToTensor(), # 將圖像轉換為 PyTorch 張量,并將像素值從 [0, 255] 縮放到 [0, 1]。transforms.Normalize([0.5]*3, [0.5]*3) # 對每個通道應用均值和標準差為 0.5 的標準化處理,使數據分布接近正態分布。])# 通過 transform(image) 應用上述變換,# 并使用 .unsqueeze(0) 在維度0處增加一個新的維度,以適應模型輸入的要求(即模擬一個批次的輸入)。# 這一步是因為大多數深度學習模型期望輸入是一個四維張量(批次大小,通道數,高度,寬度),即使只有一個樣本也需要指定批次大小。tensor_img = transform(image).unsqueeze(0)# 禁用梯度計算:使用 torch.no_grad() 上下文管理器,在推理過程中禁用梯度計算,減少內存占用并加快計算速度。with torch.no_grad():# 前向傳播:執行一次前向傳播,獲取模型輸出 output。output = model(tensor_img)# 獲取預測結果:使用 torch.max(output, 1) 獲取輸出中最大值的索引(即預測的類別),_ 用來忽略最大值本身,只保留索引。_, predicted = torch.max(output, 1)# 返回預測標簽:通過 idx2label[predicted.item()] 將預測的類別索引轉換為對應的標簽名稱。return idx2label[predicted.item()]# 使用示例
print(predict("../gesture/train/four/IMG_1122.JPG")) # 輸出: "four"
七、 訓練模式和推理模式
模型為訓練模式和推理模式,有什么區別?
在深度學習中,模型通常有兩種運行模式:訓練模式(Training Mode) 和 推理模式(Inference Mode 或 Evaluation Mode)。這兩種模式在行為上有一些關鍵的區別,主要是因為某些層或操作在訓練和推理時需要不同的處理方式。以下是它們之間的主要區別:
7.1 訓練模式
-
Dropout 層:
- 在訓練模式下,Dropout 層會隨機“丟棄”一些神經元(即設置其輸出為零),以防止過擬合并增強模型的泛化能力。
- 這種機制有助于避免模型對特定特征過度依賴。
-
Batch Normalization:
- 在訓練過程中,Batch Normalization 會使用當前批次的數據來計算均值和方差,并對輸入進行歸一化。
- 同時,它還會更新運行時的平均均值和方差,這些值會在推理階段使用。
-
梯度計算與參數更新:
- 在訓練模式下,模型不僅執行前向傳播,還會通過反向傳播計算損失函數關于模型參數的梯度,并根據優化算法更新這些參數。
-
數據增強:
- 在訓練模式下,通常會對輸入數據應用數據增強技術(如隨機裁剪、翻轉等),以增加數據集的多樣性,從而提高模型的泛化能力。
7.2 推理模式
-
Dropout 層:
- 在推理模式下,Dropout 層不會丟棄任何神經元;相反,所有神經元都會參與計算,但它們的輸出會被縮放(通常是乘以保留概率)。這是因為我們希望模型能夠利用全部的信息來做預測。
-
Batch Normalization:
- 在推理模式下,Batch Normalization 使用的是在整個訓練過程中累積得到的運行時均值和方差來進行歸一化,而不是當前批次的統計數據。
- 這樣做的目的是為了保證推理時的一致性,因為推理時可能每次只處理少量樣本甚至單個樣本。
-
梯度計算與參數更新:
- 在推理模式下,不涉及梯度計算和參數更新。模型只是簡單地執行前向傳播以生成預測結果。
-
數據增強:
- 在推理模式下,通常不會應用數據增強技術,因為我們希望基于原始輸入數據做出最準確的預測。
7.3 如何切換模式
-
在 PyTorch 中,可以通過調用
model.train()
將模型設置為訓練模式,通過調用model.eval()
將模型設置為推理模式(也稱為評估模式)。 -
切換到相應模式非常重要,特別是在驗證集或測試集上評估模型性能時,確保模型處于
eval
模式可以避免不必要的 Dropout 或 Batch Normalization 行為影響結果。
理解這兩種模式的區別對于正確地訓練和評估深度學習模型至關重要,尤其是在涉及到 Dropout、Batch Normalization 等技術的應用場景中。正確設置模型的工作模式可以顯著影響最終模型的性能和穩定性。
八、結語
以上展現手勢識別系統的構建過程,涵蓋數據管理、模型設計、訓練優化等關鍵環節。可根據實際需求調整網絡深度、數據增強策略等參數。
計算機視覺:計算機視覺之手勢識別圖片