一、選題背景及動機
在現代社會中,圖像分類是計算機視覺領域的一個重要任務。動物圖像分類具有廣泛的應用,例如生態學研究、動物保護、農業監測等。通過對動物圖像進行自動分類,可以幫助人們更好地了解動物種類、數量和分布情況,從而支持相關領域的決策和研究。本研究的目標是使用卷積神經網絡(CNN)對動物圖像進行分類。通過對大量的貓、狗和野生動物圖像進行訓練,建立一個準確分類不同動物類別的模型。該模型可以用于自動識別和分類新的動物圖像,從而提供快速、準確的動物分類結果。
動機:
(1)對于寵物貓和狗的圖像分類,可以幫助飼養者或寵物主人快速準確地識別自己的寵物。這對于寵物尋找、寵物遺失的尋找以及寵物社交媒體的管理和組織都非常有用。
(2)通過在大規模的動物圖像數據庫中進行分類,可以構建一個便捷的圖像檢索系統。用戶可以根據感興趣的類別,如貓、狗或野生動物,快速搜索和瀏覽相關的圖像內容。
二、研究內容
1、本次訓練的數據來源
https://www.kaggle.com/datasets/andrewmvd/animal-faces/data
注冊登錄之后,下載即可。然后把下載好的數據集放在該項目路徑下的data文件中
2、使用PyTorch和scikit-learn框架進行機器學習任務的實現和評估
3、技術難點:
(1)如何準確評估模型的性能和分析分類結果
(2)每個類別的數據樣本不一致
解決方法:
(1)對模型進行評估,并計算準確率、精確度、召回率等指標,繪制混淆矩陣和學習曲線,以可視化模型的性能和錯誤情況
(2)通過對數據樣本多的數據集進行欠采樣,使所有類別的數據集統一樣本數量
三、步驟
1、導入必要的庫
import pandas as pd
from PIL import Image
import torch.nn as nn
import torch.optim as optim
from torch.utils.data.sampler import SubsetRandomSampler
from torch.utils.data import Dataset
import torchvision.transforms as transforms
import matplotlib.font_manager as fm
import torch
import torch.nn.functional as F
from sklearn.metrics import accuracy_score, precision_score, recall_score, confusion_matrix, roc_curve, auc
import matplotlib.pyplot as plt
import seaborn as sns
from collections import Counter
from sklearn.utils import resample
import numpy as np
2、加載數據集和對數據預處理
通過對數據的加載和數據預處理之后,打印出每個類別(貓、狗、野獸)的圖片總數,并繪制出直方圖,更直觀的表示出圖片每個類別的數量。
class InvalidDatasetException(Exception):def __init__(self, len_of_paths, len_of_labels):super().__init__(f"Number of paths ({len_of_paths}) is not compatible with number of labels ({len_of_labels})")
transform = transforms.Compose([transforms.ToTensor()])
class AnimalDataset(Dataset):def __init__(self, img_paths, img_labels, size_of_images):self.img_paths = img_pathsself.img_labels = img_labelsself.size_of_images = size_of_imagesif len(self.img_paths) != len(self.img_labels):raise InvalidDatasetException(self.img_paths, self.img_labels)def __len__(self):return len(self.img_paths)def __getitem__(self, index):PIL_IMAGE = Image.open(self.img_paths[index]).resize(self.size_of_images)TENSOR_IMAGE = transform(PIL_IMAGE)label = self.img_labels[index]return TENSOR_IMAGE, label
import glob
paths = []
labels = []
label_map = {0: "Cat",1: "Dog",2: "Wild"}
cat_paths = glob.glob("D:/test/pythonProject/data/afhq/train/cat/*") + glob.glob("D:/test/pythonProject/data/afhq/val/cat/*") #路徑需要改成自己存放項目數據的路徑
for cat_path in cat_paths:paths.append(cat_path)labels.append(0)
dog_paths = glob.glob("D:/test/pythonProject/data/afhq/train/dog/*") + glob.glob("D:/test/pythonProject/data/afhq/val/dog/*")
for dog_path in dog_paths:paths.append(dog_path)labels.append(1)
wild_paths = glob.glob("D:/test/pythonProject/data/afhq/train/wild/*") + glob.glob("D:/test/pythonProject/data/afhq/val/wild/*")
for wild_path in wild_paths:paths.append(wild_path)labels.append(2)
data = pd.DataFrame({'classes': labels})num_classes = len(label_map)
print('總類別數:', num_classes)
for class_label, class_name in label_map.items():count = data[data['classes'] == class_label].shape[0]print(f"類別 {class_name}: {count} 張照片")
font_path = "C:/Windows/Fonts/msyh.ttc"
font_prop = fm.FontProperties(fname=font_path)
sns.set_style("white")
plot = sns.countplot(x=data['classes'], color='#2596be')
plt.figure(figsize=(15, 12))
sns.despine()
plot.set_title('類別分布\n', x=0.1, y=1, font=font_prop, fontsize=18)
plot.set_ylabel("數量", x=0.02, font=font_prop, fontsize=12)
plot.set_xlabel("類別", font=font_prop, fontsize=15)
for p in plot.patches:plot.annotate(format(p.get_height(), '.0f'), (p.get_x() + p.get_width() / 2, p.get_height()),ha='center', va='center', xytext=(0, -20), font=font_prop, textcoords='offset points', size=15)
plt.show()
運行截圖:
通過對以上打印的數據以及可視化的圖片進行觀察,我們可以看到三個類別的數量存在一定的差異。雖然數量上的差距不是太大,但對于訓練學習結果可能會有一定的影響。為了克服類別不平衡的問題,我們可以采取欠采樣來平衡數據集,減少數量較多的類別的樣本數量。
#數據集欠采樣
labels = np.array(labels)
paths = np.array(paths)
counter = Counter(labels)
print("原始樣本數量:", counter)
cat_indices = np.where(labels == 0)[0]
dog_indices = np.where(labels == 1)[0]
wild_indices = np.where(labels == 2)[0]
min_samples = min([len(cat_indices), len(dog_indices), len(wild_indices)])
undersampled_cat_indices = resample(cat_indices, replace=False, n_samples=min_samples, random_state=42)
undersampled_dog_indices = resample(dog_indices, replace=False, n_samples=min_samples, random_state=42)
undersampled_wild_indices = resample(wild_indices, replace=False, n_samples=min_samples, random_state=42)
undersampled_indices = np.concatenate((undersampled_cat_indices, undersampled_dog_indices, undersampled_wild_indices))
undersampled_paths = paths[undersampled_indices]
undersampled_labels = labels[undersampled_indices]
counter_undersampled = Counter(undersampled_labels)
print("欠采樣后的樣本數量:", counter_undersampled)
counter_undersampled = Counter(undersampled_labels)
categories = [label_map[label] for label in counter_undersampled.keys()]
sample_counts = list(counter_undersampled.values())
#可視化
sns.set_style("white")
plt.figure(figsize=(6.4, 4.8))
plot = sns.countplot(x=undersampled_labels, color='#2596be')
sns.despine()
plot.set_title('類別分布\n', x=0.1, y=1, font=font_prop, fontsize=18)
plot.set_ylabel("數量", x=0.02, font=font_prop, fontsize=12)
plot.set_xlabel("類別", font=font_prop, fontsize=15)for p in plot.patches:plot.annotate(format(p.get_height(), '.0f'), (p.get_x() + p.get_width() / 2, p.get_height()),ha='center', va='center', xytext=(0, -20), font=font_prop, textcoords='offset points', size=15)plt.show()
運行結果圖:
在進行欠采樣后,每個類別的圖片數量已經被擴展為一致的數量,使得模型在訓練過程中更加公平地對待每個類別。
3、缺失值處理
對數據進行預處理完之后,需要查看是否有缺失值,要檢查路徑和標簽的數量是否匹配,并打印路徑和標簽數量,對缺失情況進行可視化
if len(undersampled_paths) != len(undersampled_labels):raise InvalidDatasetException(len(undersampled_paths), len(undersampled_labels))
#使用字符串格式化(f-string)來將整型值插入到字符串中。
print(f"打印paths列表的文件路徑數量: {len(undersampled_paths)}")
print(f"打印labels列表的圖片數量: {len(undersampled_labels)}")
#缺失情況數據可視化
df = pd.DataFrame({'Path': undersampled_paths, 'Label': undersampled_labels})
missing_values = df.isnull().sum()
#繪制條形圖
plt.bar(missing_values.index, missing_values.values)
plt.xlabel("特征", fontproperties=font_prop, fontsize=12)
plt.ylabel("缺失值數量", fontproperties=font_prop, fontsize=12)
plt.title("缺失情況數據可視化", fontproperties=font_prop, fontsize=18)
plt.grid(False)
plt.xticks(rotation=90)
plt.show()
運行截圖:
通過對打印的數據以及對條形圖的查看,我們可以確認數據沒有缺失。這意味著我們的數據集完整,并且可以進行進一步的分析和處理。
4、劃分數據集
對將數據集劃分為訓練集和測試集,并創建對應的數據加載器,并定義了每個批次的樣本數量。
dataset = AnimalDataset(undersampled_paths,undersampled_labels,(250,250))
from sklearn.model_selection import train_test_split
dataset_indices = list(range(0,len(dataset)))
#從數據集中劃分訓練集和測試集
train_indices,test_indices=train_test_split(dataset_indices,test_size=0.2,random_state=42)
print("訓練集樣本數量: ",len(train_indices))
print("測試集樣本數量: ",len(test_indices))
#創建訓練集和測試集的采樣器
train_sampler = SubsetRandomSampler(train_indices)
test_sampler = SubsetRandomSampler(test_indices)
BATCH_SIZE = 128
train_loader = torch.utils.data.DataLoader(dataset, batch_size=BATCH_SIZE,sampler=train_sampler)
validation_loader = torch.utils.data.DataLoader(dataset, batch_size=BATCH_SIZE,sampler=test_sampler)
dataset[1][0].shape
images,labels = next(iter(train_loader))
type(labels)
運行截圖:
5、獲取一個批次的訓練數據,并可視化
def add_subplot_label(ax, label):ax.text(0.5, -0.15, label, transform=ax.transAxes,ha='center', va='center', fontsize=12)
images, labels = next(iter(train_loader))
fig, axis = plt.subplots(3, 5, figsize=(15, 10))
for i, ax in enumerate(axis.flat):with torch.no_grad():npimg = images[i].numpy()npimg = np.transpose(npimg, (1, 2, 0))label = label_map[int(labels[i])]ax.imshow(npimg)ax.set(title = f"{label}")ax.grid(False)add_subplot_label(ax, f"({i // axis.shape[1]}, {i % axis.shape[1]})") # 添加編號
plt.tight_layout()
plt.show()
運行截圖:
6、模型設計
定義卷積神經網絡模型,并設定在哪個設備上運行,為后續的模型訓練做準備
class CNN(nn.Module):#定義了卷積神經網絡的各個層和全連接層。def __init__(self):super(CNN, self).__init__()# First we'll define our layersself.conv1 = nn.Conv2d(3, 32, kernel_size=3, stride=2, padding=1)self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1)self.batchnorm1 = nn.BatchNorm2d(64)self.conv3 = nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1)self.batchnorm2 = nn.BatchNorm2d(128)self.conv4 = nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1)self.batchnorm3 = nn.BatchNorm2d(256)self.maxpool = nn.MaxPool2d(2, 2)self.fc1 = nn.Linear(256 * 2 * 2, 512)self.fc2 = nn.Linear(512, 3)#定義數據在模型中的流動def forward(self, x):x = F.relu(self.conv1(x))x = F.relu(self.conv2(x))x = self.batchnorm1(x)x = self.maxpool(x)x = F.relu(self.conv3(x))x = self.batchnorm2(x)x = self.maxpool(x)x = F.relu(self.conv4(x))x = self.batchnorm3(x)x = self.maxpool(x)x = x.view(-1, 256 * 2 * 2)x = self.fc1(x)x = self.fc2(x)x = F.log_softmax(x, dim=1)return x
#選擇模型運行的設備
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
7、模型訓練
執行模型的訓練過程,使用交叉熵損失函數和RMSprop優化器來定義損失計算和參數優化的方法,設置了訓練的輪次數,并記錄每個訓練輪次的損失和準確率,對每個訓練輪次的損失和準確率進行可視化
model = CNN().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.RMSprop(model.parameters(),lr=1e-4)
EPOCH_NUMBER = 6
TRAIN_LOSS = []
TRAIN_ACCURACY = []
#訓練過程
for epoch in range(1, EPOCH_NUMBER + 1):epoch_loss = 0.0correct = 0total = 0#遍歷訓練數據加載器for data_, target_ in train_loader:target_ = target_.to(device).long()data_ = data_.to(device).float()#清零優化器中之前的梯度,準備計算當前輪次的梯度。optimizer.zero_grad()#將輸入數據傳遞給模型,獲取模型的預測輸出。outputs = model(data_)loss = criterion(outputs, target_)loss.backward()optimizer.step()epoch_loss = epoch_loss + loss.item()_, pred = torch.max(outputs, dim=1)#統計預測正確的樣本數量,將預測值與真實標簽進行比較,并累計正確預測的數量。correct = correct + torch.sum(pred == target_).item()total += target_.size(0)#記錄每個訓練輪次的損失和準確率,并輸出當前訓練輪次的準確率和損失。TRAIN_LOSS.append(epoch_loss)TRAIN_ACCURACY.append(100 * correct / total)print(f"Epoch {epoch}: Accuracy: {100 * correct / total}, Loss: {epoch_loss}")
#可視化訓練過程中的損失和準確率
plt.subplots(figsize=(6, 4))
plt.plot(range(EPOCH_NUMBER), TRAIN_LOSS, color="blue", label="Loss")
plt.legend()
plt.xlabel("輪次", fontproperties=font_prop)
plt.ylabel("損失值", fontproperties=font_prop)
plt.title("訓練損失", fontproperties=font_prop)
plt.show()
plt.subplots(figsize=(6, 4))
plt.plot(range(EPOCH_NUMBER), TRAIN_ACCURACY, color="green", label="Accuracy")
plt.legend()
plt.xlabel("輪次", fontproperties=font_prop)
plt.ylabel("準確率", fontproperties=font_prop)
plt.title("訓練準確率", fontproperties=font_prop)
plt.show()
運行截圖:
通過上面的數據以及圖形,我們可以觀察到,隨著訓練輪次的增加,訓練損失逐漸降低,訓練準確率逐漸提高。這表明模型在學習過程中逐漸減小了預測值與真實標簽之間的差異,提高了對訓練數據的擬合能力。每輪的訓練損失率都比上一輪的損失率低,說明模型的優化算法有效地調整了參數,使模型逐漸逼近最優解。也意味著模型在訓練數據上的分類性能不斷改善,更準確地預測了樣本的標簽。每輪的訓練準確率都比上一輪的高,說明模型逐漸學習到了更多的特征和模式,提高了對訓練數據的分類準確性。總體來說損失下降和準確率提高是我們期望在訓練過程中看到的趨勢,表明模型正在逐漸優化和提升性能。
8、性能評估
評估模型在每個類別上的性能,并繪制ROC曲線以衡量模型的分類準確性
def predict_labels(model, data_loader):model.eval()y_pred = []y_true = []with torch.no_grad():for images, labels in data_loader:images = images.to(device)labels = labels.to(device)outputs = model(images)_, predicted = torch.max(outputs.data, 1)y_pred.extend(predicted.cpu().numpy())y_true.extend(labels.cpu().numpy())return np.array(y_pred), np.array(y_true)
#獲取預測結果
y_pred, y_true = predict_labels(model, validation_loader)
#計算每個類別的ROC曲線
fpr = dict()
tpr = dict()
roc_auc = dict()
num_classes = len(label_map)
for i in range(num_classes):fpr[i], tpr[i], _ = roc_curve((np.array(y_true) == i).astype(int), (np.array(y_pred) == i).astype(int))roc_auc[i] = auc(fpr[i], tpr[i])
#繪制ROC曲線
plt.figure(figsize=(10, 8))
colors = ['b', 'g', 'r'] # 每個類別的曲線顏色
for i in range(num_classes):plt.plot(fpr[i], tpr[i], color=colors[i], lw=2, label='類別 {0} 的ROC曲線 (AUC = {1:.2f})'.format(i, roc_auc[i]))
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('假陽性率', fontproperties=font_prop)
plt.ylabel('真陽性率', fontproperties=font_prop)
plt.title('接收者操作特征曲線', fontproperties=font_prop)
plt.legend(loc="lower right", prop=font_prop)
plt.show()
運行截圖:
從圖片中可以看出來,cat類別的ROC曲線相對于其他類別的曲線更加接近左上角,而dog和wild類別的曲線則相對較低。這意味著在不同的閾值下,模型更容易將cat類別正確分類為正例,并且在cat類別上具有較高的真陽性率和較低的假陽性率。相比之下,dog和wild類別在模型分類能力方面相對較弱,表明模型更容易將它們錯誤地分類為其他類別。
9、測試
評估模型在驗證集上對模型進行測試,并計算評估指標(準確率、精確率、召回率)以及混淆矩陣,并使用可視化工具將混淆矩陣進行可視化。
model.eval() # 將模型設置為評估模式
predictions = [] # 存儲預測結果和真實標簽
true_labels = []
#使用測試集進行預測
with torch.no_grad():for images, labels in validation_loader:images = images.to(device)labels = labels.to(device)outputs = model(images) # 前向傳播_, predicted = torch.max(outputs.data, 1) # 獲取預測結果predictions.extend(predicted.tolist()) # 存儲預測結果和真實標簽true_labels.extend(labels.tolist())
#將預測結果和真實標簽轉換為NumPy數組
predictions = np.array(predictions)
true_labels = np.array(true_labels)
accuracy = accuracy_score(true_labels, predictions) # 計算準確率
precision = precision_score(true_labels, predictions, average='macro') # 計算精確率
recall = recall_score(true_labels, predictions, average='macro') # 計算召回率
confusion = confusion_matrix(true_labels, predictions) # 計算混淆矩陣
# 打印評估結果
print("準確率:", accuracy)
print("精確率:", precision)
print("召回率:", recall)
print("混淆矩陣:")
print(confusion)
# 可視化混淆矩陣
labels = ['Cat', 'Dog', 'Wild']
plt.rcParams['font.sans-serif'] = ['SimSun']
plt.figure(figsize=(8, 6))
sns.heatmap(confusion, annot=True, fmt="d", cmap="Blues", xticklabels=labels, yticklabels=labels)
plt.xlabel('預測標簽')
plt.ylabel('真實標簽')
plt.title('混淆矩陣')
plt.show()
運行截圖:
四 思考
1、換數據集行不行?
比如動物數據集換成植物數據集等,大家可以自行找公開數據集進行測試。
2、換模型行不行?
用其它卷積神經網絡模型試一試:
- LeNet-5?:由Yann LeCun等人于1998年提出,主要用于手寫數字識別。LeNet-5包含了卷積層、池化層和全連接層,是第一個成功應用于數字識別任務的卷積神經網絡模型。
- AlexNet?:由Alex Krizhevsky等人在2012年的ImageNet圖像分類競賽中提出。AlexNet采用了更深的網絡結構和更大的數據集,使用了ReLU激活函數和Dropout正則化技術,取得了突破性的性能提升。
- VGGNet?:由Karen Simonyan和Andrew Zisserman在2014年提出。VGGNet的特點是使用了非常小的卷積核(3x3),并通過堆疊多個卷積層來增加網絡的深度,提高了特征提取的效果。
- GoogLeNet (Inception)?:由Google團隊在2014年提出。GoogLeNet采用了Inception模塊結構,通過并行的多個卷積分支來提取不同尺度的特征,并使用1x1的卷積核來降低計算復雜度。?
- ResNet?:由Microsoft團隊在2015年提出。ResNet引入了殘差學習的思想,通過跨層連接解決了深度網絡訓練中的梯度消失和梯度爆炸問題,適用于大規模圖像識別任務。?
- MobileNet?:由Google團隊在2017年提出。MobileNet采用了深度可分離卷積的結構,減少了參數數量,適用于移動設備等資源受限的場景。
3、圖像出現以下情況怎么處理?
(1)模糊
(2)光照不均勻
(3)扭曲變形
(4)有雨有霧
(5)圖上除了動物外還有其它物體