基于AlexNet的猫狗识别

2,738 阅读8分钟

基于AlexNet的猫狗识别

前言

机器视觉的课程设计,数据集来自网站Kaggle:www.kaggle.com/datasets/to…

源代码

一、加载数据集

1、数据处理

设置每批数据中的图片数batch_size,每次训练256张图片;设置图片大小为resize,将每张图片的大小重塑为224224224 * 224,但实际似乎会变为225225225 * 225

以上操作由函数from torchvision import transform完成。Compose(组成),指由Resize和ToTensro两个操作组成数据转换transform。

batch_size = 256  # 每批里面的样本数
resize = 224  # 重塑图片大小
transform = transforms.Compose([
    transforms.Resize((resize, resize)), #重塑
    transforms.ToTensor(), # 设置为Tensor数据格式
])

2、加载数据集

图片数据加载依靠函数from torchvision.datasets import ImageFolder。

ImageFolder假设所有的文件按文件夹保存,每个文件夹下存储同一个类别的图片,文件夹名为类名,其构造函数如下:

ImageFolder(root, transform=None, target_transform=None, loader=default_loader)

它主要有四个参数:
root:在root指定的路径下寻找图片,
transform:对PIL Image进行的转换操作, transform的输入是使用loader读取图片的返回对象,
target_transform:对label的转换,
loader:给定路径后如何读取图片,默认读取为RGB格式的PIL Image对象。
参考博客原文链接

本次课设实际加载数据集代码:

train_data = ImageFolder(r"训练数据集路径", transform=transform)

test_data = ImageFolder(r"测试数据集路径",transform=transform)

3、数据分批

数据集不能一次就训练完,需要分批训练,GPU性能不佳那么batch_size就设置得小一些。 到此数据加载完成。

# torch.utils.data.DataLoader 小批量读取数据

train_iter = torch.utils.data.DataLoader(
    train_data,  # 数据(特征,标签)
    batch_size=batch_size,  # 批量样本数
    shuffle=True,  # 打乱顺序,一般为True
    num_workers=num_workers)  # 额外进程,一般用于加速加载数据,windows系统设为0即可

test_iter = torch.utils.data.DataLoader(
    test_data,
    batch_size=batch_size,
    shuffle=False,
    num_workers=num_workers)

二、建立网络模型

网络结构不介绍,参考书籍《动手学深度学习》第四章第6节。

如数据为灰度图,则输入通道in_channels为1,
nn.Conv2d(3, 96, 11, 4) 改为 nn.Conv2d(1, 96, 11, 4)。
彩图则代码不变。

课设为二分类问题,因此全连接层最后一层为 nn.Linear(4096, 2)
n分类问题则改为nn.Linear(4096, n)

# %%建立模型

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

class AlexNet(nn.Module):
    def __init__(self):
        super(AlexNet, self).__init__()
        self.conv = nn.Sequential(
            # in_channels, out_channels, kernel_size, stride
            nn.Conv2d(3, 96, 11, 4),
            nn.ReLU(),
            # kernel_size, stride
            nn.MaxPool2d(3, 2),
            # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,
            # 且增大输出通道数
            nn.Conv2d(96, 256, 5, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(3, 2),
            # 连续3个卷积层,且使用更小的卷积窗口。除了最后的卷积层外,
            # 进一步增大了输出通道数。
            # 前两个卷积层后不使用池化层来减小输入的高和宽
            nn.Conv2d(256, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 256, 3, 1, 1),
            nn.ReLU(),
            nn.MaxPool2d(3, 2)
        )
        # 这里全连接层的输出个数比LeNet中的大数倍。使用丢弃层来缓解过拟合
        self.fc = nn.Sequential(
            nn.Linear(256 * 5 * 5, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            # 输出层。由于这里使用Fashion-MNIST
            # 所以用类别数为10,而非论文中的1000
            nn.Linear(4096, 2)
        )

    def forward(self, img):
        feature = self.conv(img)
        output = self.fc(feature.view(img.shape[0], -1))
        return output

net = AlexNet()

# 初始化模型参数,使之更容易收敛
for m in net.modules():
    if isinstance(m,torch.nn.Linear):   #判断是否同类型
        torch.nn.init.xavier_normal_(m.weight)

三、训练

训练时优化算法:SGD,
损失函数:交叉熵函数(适用于一般分类问题)
模型准确率计算:正确分类次数/总分类次数正确分类次数/总分类次数
训练过程由Tensorboard记录。

# %%训练
# 超参数
lr, num_epochs = 0.1, 20  # lr为收敛步长,num_epochs为训练周期
optimizer = torch.optim.SGD(net.parameters(), lr=lr)  # 选择优化函数
loss = torch.nn.CrossEntropyLoss()  # 选择损失函数
writer = SummaryWriter('./log')  # 设置路径

# 将模型加载到指定运算器中
net = net.to(device)
n_train = len(train_data)
n_test = len(test_data)
print("training on ", device)
print("训练样本数:{} , 测试样本数:{} ".format(n_train,n_test))

for epoch in range(num_epochs):
    print(15 * '*' + '第{}轮训练'.format(epoch) + 15 * '*')
    train_l_sum, train_acc_sum, n, batch_count = 0.0, 0.0, 0, 0
    start = time.time()

    for X, y in train_iter:
        X = X.to(device)
        y = y.to(device)
        y_pre = net(X)
        l = loss(y_pre, y)  # y_hat预测target概率,y真实target,l此批数据的loss
        # 梯度清零
        optimizer.zero_grad()
        l.backward()
        optimizer.step()  # 优化网络参数

        # 输出每批训练结果
        train_l_sum += l.cpu().item()
        train_acc_sum += (y_pre.argmax(dim=1) == y).sum().cpu().item()  # 计算正确样本数
        n += y.shape[0]  # n:累和每批样本数量,也就是训练样本数量
        batch_count += 1  # 批次
        if batch_count % 20 == 0:
            print('训练次数{} , 准确率:{} , loos:{} , time:{} '.format(batch_count, train_acc_sum/n_train, train_l_sum,
                                                                time.time() - start))
        writer.add_scalar('Train_loss', train_l_sum, batch_count+len(train_iter)*epoch)
        writer.add_scalar('Train_acc', train_acc_sum, batch_count+len(train_iter)*epoch)

    # 输出每轮测试结果
    test_acc = evaluate_accuracy(test_iter, net)
    print('测试acc:{} , time:{}'.format(test_acc,time.time()-start))
    writer.add_scalar('Test_acc', test_acc, epoch)
    torch.save(net.state_dict(), "model{}.pt".format(epoch))

# writer.add_graph(net, torch.rand(4, 3, resize, resize))
writer.close()

训练后感:训练时常常遇到欠拟合或过拟合的情况,此时可以尝试优化以下几个方面。

优化算法
学习率(先大后小)
参数初始化
数据标准化

四、应用

1、加载模型

首先,将训练时得网络结构移过来,不然会加载失败。

class AlexNet(nn.Module):
    def __init__(self):
        super(AlexNet, self).__init__()
        self.conv = nn.Sequential(
            # in_channels, out_channels, kernel_size, stride
            nn.Conv2d(3, 96, 11, 4),
            nn.ReLU(),
            # kernel_size, stride
            nn.MaxPool2d(3, 2),
            # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,
            # 且增大输出通道数
            nn.Conv2d(96, 256, 5, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(3, 2),
            # 连续3个卷积层,且使用更小的卷积窗口。除了最后的卷积层外,
            # 进一步增大了输出通道数。
            # 前两个卷积层后不使用池化层来减小输入的高和宽
            nn.Conv2d(256, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 256, 3, 1, 1),
            nn.ReLU(),
            nn.MaxPool2d(3, 2)
        )
        # 这里全连接层的输出个数比LeNet中的大数倍。使用丢弃层来缓解过拟合
        self.fc = nn.Sequential(
            nn.Linear(256 * 5 * 5, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            # 输出层。由于这里使用Fashion-MNIST
            # 所以用类别数为10,而非论文中的1000
            nn.Linear(4096, 2)
        )

    def forward(self, img):
        feature = self.conv(img)
        output = self.fc(feature.view(img.shape[0], -1))
        return output

再加载训练好的模型参数

net = AlexNet()
net.load_state_dict(load(('model .pt文件所在路径')))

2、应用模型

这部分仅本次课设适用。 传入图片路径path进行猫狗识别,并输出结果。

小细节:需要将图片格式先转为PIL , 再转为Tensor, 因为transforms.Resize((resize, resize))仅用于PIL图片。

def classfiey(path):
    resize = 224
    path = os.path.abspath(path)
    transform = transforms.Compose([
        transforms.ToPILImage(),
        transforms.Resize((resize, resize)),
        transforms.ToTensor(),
    ])

    img = cv2.imread(path)
    img_1 = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)    #opencv读取图片会把图片格式变为BGR
    img_2 = transform(img_1)
    image = reshape(img_2, (1, 3, resize, resize))  # 转为合适大小

    net.eval()
    result = net(image)
    selection = ['猫', '狗']
    return selection[argmax(result)]

最后设计简单UI界面进行调用。

五、模型评价

模型评价的大部分内容(包括图片)参考自知乎文章,图片上的水印是该社区自动打的。

1、评价指标

对于分类问题,一般有评价指标:准确率,精确率,召回率,F1,ROC曲线,AUC曲线。

5.1.1、准确率

精确率和准确率是比较容易混淆的两个评估指标,两者是有区别的。精确率是一个二分类指标,而准确率能应用于多分类,其常见计算公式为:

5.1.2、精确率、召回率、F1值

精确率(Precision)=True PositiveTrue Positive+False Positive\Large 精确率(Precision) = \frac{True \ Positive}{True \ Positive + False \ Positive}
召回率(Recall)=True PositiveTrue Positive+False Negative\Large 召回率(Recall) = \frac{True \ Positive}{True \ Positive + False \ Negative}

上述计算公式中的Positive与Negative是预测标签里的正例和反例,True与false代表预测正误;要注意,精确率和召回率是二分类指标,不适用多分类,由此得到P-R曲线以及ROC曲线均是二分类评估指标(因为其横纵轴指标均为二分类混淆矩阵计算得到),而准确率适用于多分类评估。(可以将多分类问题转换为二分类问题进行求解,将关注的类化为一类,其他所有类化为一类)参考自知乎文章

==个人分析:== 精确率:预测为正例的那些数据里预测正确的数据个数。适用于罪犯判死刑的情况,宁可漏判也不可误判。
召回率:正例中预测准确的。适用于地震预测情况,宁可误判也不可漏判。

显然二者偏重点不同,难以只用某一个指标评价模型,因此有了精确率和召回率的调和均值F1值。

2F1=1P+1R\frac{2}{F_1} = \frac{1}{P} + \frac{1}{R}
F1=2PRP+RF_1 = \frac{2PR}{P+R}

如果你对二者有侧重点,可以进行加权。

Fα=(1+α2)PRα2P+RF_{\alpha} = \frac{(1+\alpha^2) PR}{\alpha^2 P+R}

a>1a>1时,RRPP重要,反之同理。

5.1.3、PR曲线

为更好观测精确率和召回率的关系,可画出PR曲线观测,曲线越靠近右上角越好,但一般不太好用,一般使用F1值或AUC值。

5.1.4、ROC曲线与AUC ROC曲线纵坐标是真正率,横坐标是假正率,如下图,去对应的计算公式为:

真正率(True Positive Rate)=Recall=TPTP+FN真正率(True \ Positive \ Rate) =Recall= \frac{TP}{TP+FN}
假正率(False Positive Rate)=FPFP+TN假正率(False \ Positive \ Rate) = \frac{FP}{FP+TN}

其特点为不受不平衡数据(反例>>正例)的影响,因此优先级高于PR曲线。

ROC曲线就是以true positive rate 和 false positive rate为轴,取不同的threshold点画点成线。有人问了threshold是什么。这么说吧,每个分类器作出的预测,都是基于一个probability score。一般默认的threshold都是0.5,如果probability>0.5,那么这个sample被模型分成正例了,反之则是反例。搬运自知乎文章

AUC是ROC曲线的积分面积,用一个值代替ROC曲线去衡量模型,ROC曲线越靠近左上角越好,所围面积也越大。

使用ROC曲线和AUC可以不设定threshold,减少一个超参数的影响。

2、本次设计模型评价结果

混淆矩阵:

Alex:

Vgg16:

综合评价:

Alex:

          precision    recall  f1-score   support

       0       0.88      0.72      0.79      1011
       1       0.76      0.90      0.82      1012

accuracy                           0.81      2023
macro avg       0.82      0.81     0.81      2023
weighted avg    0.82      0.81     0.81      2023

Vgg16:

          precision    recall  f1-score   support

       0       0.98      0.90      0.94      1011
       1       0.91      0.98      0.94      1012

accuracy                           0.94      2023
macro avg      0.94      0.94      0.94      2023
weighted avg   0.94      0.94      0.94      2023

其中0:猫,1:狗。
从上述结果可以看出,模型对于狗的识别优于猫的识别。