【Intel校企合作课程】手动搭建ResNet网络实现检测并清除杂草

一、问题描述

        杂草是农业经营中不受欢迎的入侵者,它们通过窃取营养、水、土地和其他关键资源来破坏种植,这些入侵者会导致产量下降和资源部署效率低下。一种已知的方法是使用杀虫剂来清除杂草,但杀虫剂会给人类带来健康风险。

        我们的目标是利用计算机视觉技术可以自动检测杂草的存在,开发一种只在杂草上而不是在作物上喷洒农药的系统,并使用针对性的修复技术将其从田地中清除,从而最小化杂草对环境的负面影响。

二、数据集 

下载网址:https://filerepo.idzcn.com/hack2023/Weed_Detection5a431d7.zip

 数据介绍:

        该数据集中包含了1300个jpeg文件和1300个与jpeg文件同名的txt文件。其中jpeg文件是包含杂草和作物的图片,与其同名的txt文件包含了其类别信息。

        如图1,是数据集中一张杂草图片,其对应的txt文件的内容为:0 0.478516 0.560547 0.847656 0.625000,文本内容的第一个数字为0(后面的数字与类别无关),表示该图片中的植物为杂草。相反,如图2,是一张作物图片,其对应的txt文件的内容为:1 0.440430 0.531250 0.810547 0.921875,文本内容的第一个数字为1,表示该图片中的植物为作物。

图1 杂草图片
图2 作物图片

三、ResNet网络模型简介

        ResNet(Residual Network)是一种深度卷积神经网络架构,由微软亚洲研究院的何凯明等人于2015年提出。它的核心思想是通过建立残差映射来训练深层网络。

        ResNet的主要贡献在于解决了深度神经网络中的梯度消失和梯度爆炸问题,使得深层网络的训练变得更加容易。它引入了“跨层连接”的概念,即将输入信号直接加到网络中的某一层输出上,从而保留了前面层的信息,避免信息丢失。

        ResNet的基本结构是残差块,它由两个卷积层和一个跨层连接组成。每个残差块的输入和输出尺寸相同,这使得它们可以直接相加。ResNet还引入了“瓶颈结构”,将网络中的计算量减少了很多,同时还保持了较高的精度。

        ResNet的深度可以达到1000层以上,但为了避免过拟合和提高训练效率,通常只使用50层或者100层。在图像识别、目标检测、人脸识别等领域,ResNet已成为一种标准的网络结构。

四、项目代码

4.1导入需要的第三方包 

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset
import torchvision.models as models
 
from PIL import Image
import pandas as pd
 
import matplotlib.pyplot as plt
import numpy as np
import random
from PIL import ImageEnhance
import itertools
#import intel_extension_for_pytorch as ipex
 
from collections import Counter
import sys
import os
import random
from tqdm import tqdm

4.2加载数据

首先,读取数据集中所有图片文件的文件名,并保存在一个列表文件中;

其次,将列表中的数据依次写入到一个txt文件中,并查看数据大小;

 最后,随机输出三张图片(可选操作)。

import random
import matplotlib.image as mpimg
def get_file_name(images_dir):
    # 返回文件夹中后缀是.jpeg的文件名列表
    images_files = [f for f in os.listdir(images_dir) if f.endswith('.jpeg')]
    
    images_files.sort() #排序  
    
    with open(r'./data.txt','a') as f:
        for i in images_files:
            f.write(i+'\n')
    f.close()
    
    print(f"数据集的大小是{len(images_files)}")
    print('文件写入完成!!!')
    
    # 随机选择 n 张图片进行展示
    n = 3
    random_train_files = random.sample(images_files, n)
    random_train_paths = [os.path.join('./data', file) for file in random_train_files]

    # 显示随机选择的图片
    fig, axes = plt.subplots(1, n, figsize=(10, 5))
    for i, path in enumerate(random_train_paths):
        img = mpimg.imread(path)
        axes[i].imshow(img)
        axes[i].axis('off')
    plt.show()
get_file_name('./data')

4.3强化图像数据

        在训练和使用模型之前,需要对输入数据进行一个预处理,不论是训练数据还是测试数据,都需要进行预处理,来将输入的数据转变成可以供模型读取的。 

transformer = transforms.Compose([
    transforms.ToTensor(),
    transforms.ColorJitter(contrast=0.5),  # 增强对比度
    transforms.Normalize(mean=[0.5], std=[0.5])  # 归一化
])
 
train_images_tensor = []
with open(r'./data.txt','r') as f:
    file_name_url=[i.split('\n')[0] for i in f.readlines()]
for i in range(len(file_name_url)):
    image = Image.open('./data/'+file_name_url[i])
    #tensor = transformer(image.convert('L')).type(torch.float16)
    tensor = transformer(image.convert('L'))
    train_images_tensor.append(tensor)
image_train = []
image_test = []
for i in range(len(train_images_tensor)):
    if i <=len(train_images_tensor)*0.7:
        image_train.append(train_images_tensor[i])
    else:image_test.append(train_images_tensor[i])

4.4强化标签数据

        标签是数据集中txt文件中每一行的第一个数字,由于文本文件中还有其他数据,所以要提前将标签提取出来。 

transformerlab = transforms.Compose([
    transforms.ToTensor()
])
 
train_lables_tensor = []
with open(r'./data.txt','r') as f:
    file_name_url=[i.split('.')[0] for i in f.readlines()]
train_lables_tensor = []
 
for i in range(len(file_name_url)):
    image = open('./data/' + file_name_url[i] + '.txt')
    labels = image.readline()[0]
    labels = float(labels)
    #tensor = torch.tensor(labels, dtype=torch.float16)  # 使用float16数据类型
    tensor = torch.tensor(labels)  # 
    train_lables_tensor.append(tensor)
 
lables_train = []
lables_test = []
for i in range(len(train_lables_tensor)):
    if i <=len(train_lables_tensor)*0.7:
        lables_train.append(train_lables_tensor[i])
    else:lables_test.append(train_lables_tensor[i])

 4.5做成可直接输入网络的数据集

        通过PyTorch库中的函数torch.stack()将测试集和训练集的多个图像数据和标签数据堆叠在一起,形成一个大的PyTorch张量;再创建一个数据加载器,以便于在训练深度学习模型时按批次加载数据,并确保数据在每个训练时代开始时被随机打乱。

train_datas_tensor = torch.stack(image_train)
train_labels_tensor = torch.stack(lables_train)
test_datas_tensor = torch.stack(image_test)
test_labels_tensor = torch.stack(lables_test)

train_dataset = TensorDataset(train_labels_tensor, train_datas_tensor)
train_dataloader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_dataset = TensorDataset(test_labels_tensor, test_datas_tensor)
test_dataloader = DataLoader(test_dataset, batch_size=32, shuffle=True)

4.6手动搭建一个ResNet网络模型

# 搭建网络模型
class Residual(nn.Module):
    # 初始化
    def __init__(self, input_channels, num_channels, use_conv=False, strides=1):
        #input_channels:输入数据的通道数;num_channels:输出数据的通道数
        
        super().__init__()
        # 卷积层1:将输入数据从 input_channels 转换为 num_channels
        self.conv1 = nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1, stride=strides)
        # 卷积层2:再将数据从 num_channels 转换回 num_channels
        self.conv2 = nn.Conv2d(num_channels, num_channels, kernel_size=3, padding=1)
        
        # 决定了是否使用一个额外的卷积层self.conv3来调整输入数据的通道数
        if use_conv:
            self.conv3 = nn.Conv2d(input_channels, num_channels, kernel_size=1, stride=strides)
        else:
            self.conv3 = None # use_conv 为 False
        
        # 初始化归一化层并赋值
        self.bn1 = nn.BatchNorm2d(num_channels)
        self.bn2 = nn.BatchNorm2d(num_channels)
 
    # 向前传播
    def forward(self, X): # X:输入数据
        # 首先,通过卷积层 self.conv1 和批量归一化层 self.bn1 对输入数据进行处理,然后通过ReLU激活函数
        Y = F.relu(self.bn1(self.conv1(X)))
        
        # 通过卷积层 self.conv2 和批量归一化层 self.bn2 对数据进行进一步的处理
        Y = self.bn2(self.conv2(Y))
        
        if self.conv3:
            X = self.conv3(X)
        
        # 最后,将经过卷积和批量归一化处理后的数据与原始输入数据相加(这就是残差连接的部分),然后再次通过ReLU激活函数
        Y += X
        return F.relu(Y)


# 实现一个残差网络(ResNet)构建过程
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3), #输入具有3个通道的图像,输出64个通道,使用7x7的卷积核,步长为2,填充为3。
                   nn.BatchNorm2d(64), nn.ReLU(), #对64个通道的数据进行批量归一化,使用ReLU激活函数
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1)) #最大池化层,使用3x3的卷积核,步长为2,填充为1

# 定义残差块(Residual Block)的函数
def resnet_block(input_channels, num_channels, num_residuals, first_block=False):# 输入通道数、输出通道数、残差块的个数和是否为第一个块
    blk = []
    for i in range(num_residuals):
        if i == 0 and not first_block:
            blk.append(Residual(input_channels, num_channels, use_conv=True, strides=2))
        else:
            blk.append(Residual(num_channels, num_channels)) #每个残差块定义了一个Residual模型
    return blk

#通过resnet_block函数创建的残差块的序列模型 
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True)) #b2是第一个残差块的序列模型,它有64个输入通道和64个输出通道,并有两个残差块
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))


net = nn.Sequential(b1, b2, b3, b4, b5,
                    nn.AdaptiveAvgPool2d((1, 1)), # 一个自适应平均池化层(将特征图的大小变为1x1)
                    nn.Flatten(), # 然后是一个展平层(将数据展平为一维向量)
                    nn.Linear(512, 10)) # 最后是一个具有10个输出的线性层

4.7为网络模型定义损失和优化

         该网络是用于在CPU上进行训练,也可以根据自身设备情况将其放在GPU上进行训练。GPU训练会大大减少模型训练的时间。代码中的device = torch.device("cuda:0")就是表示将模型放在GPU上进行训练,并将数据设置为半精度浮点数,即16位浮点数。

device = torch.device("cpu")
net.to(device) 

#device = torch.device("cuda:0")
#net.to(device).half()
# 将模型转移到CPU并设置为半精度(fp16)
# .half()是一个快捷方式,用于将模型的所有参数从默认的32位浮点数转换为16位半精度浮点数(也称为fp16)

# 定义交叉熵损失函数nn.CrossEntropyLoss(),用于多分类任务
criterion = nn.CrossEntropyLoss()
# 定义一个一个随机梯度下降优化器torch.optim.SGD
optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

4.8训练模型 

        第一个for循环是控制训练的轮数,读者可根据自身设备的运算能力和精准度需求,适当增加训练轮数来提升模型的精准度。

for epoch in range(1, 10):
    # 存储累计的损失
    running_loss = 0.0
    # 记录处理的图像数量
    num_images = 0
    # 
    loop = tqdm(enumerate(train_dataloader, 0))
 
    
    for step, data in loop:
        # labels, inputs = data[0].to('cuda').float(), data[1].to('cuda').float()
        # 从数据中提取标签和输入数据,并将其转换为浮点数格式
        labels, inputs = data[0].float(), data[1].float()
        
        # 清除之前存在的梯度。这是在反向传播之前必须要做的,因为PyTorch会累积梯度。
        optimizer.zero_grad()
        
        # 将输入数据(inputs)的数据类型转换为半精度浮点数
        inputs = inputs.float()
        
        # 将处理过的输入数据传递给神经网络,得到输出。
        outputs = net(inputs)
        
        # 创建包含相同数量的目标值的示例目标张量
        target = labels  # 使用实际标签作为目标
 
        # 使用 MSE 损失函数,计算网络的输出和实际标签之间的差异
        loss = criterion(outputs, target.long())
 
        # 反向传播,计算梯度        
        loss.backward()
        
        # 使用优化器更新网络权重
        optimizer.step()
 
        # 增加处理的图像数量
        num_images += inputs.size(0)
        
        # 更新累计的损失
        running_loss += loss.item()
        
        # 设置进度条的描述信息,显示当前的周期数和总周期数
        loop.set_description(f'Epoch [{epoch}/9]')
        # 在进度条上显示当前批次的平均损失
        loop.set_postfix(loss=running_loss / (step + 1))
 
print('Finish!!!')

4.9保存训练好的模型

        在项目实验中,我们对应该模型构架有严格的控制,没有修改或转移到其他框架下运行,所以直接保存了整个模型。当然,读者可根据自身实际情况,选择只保存模型的部分参数和优化器状态,以便于在不同的环境和框架下加载。

torch.save(net , 'model.pth')

4.10计算准确度

        在进行推理预测时,需要使用torch.load()来调用我们保存好的模型,然后就可以直接使用我们训练好的模型,包括其参数和优化器状态。                

        在传入其他的训练数据时,一定更要注意,模型的输入数据需要经过预处理,可参照上文4.3-4.5模块中的代码处理过程,将需要预测的数据预处理之后再传入模型进行推理预测。

# 记录正确的样本数
correct = 0
# 记录总样本数
total = 0

net = torch.load('model.pth') 
with torch.no_grad():  #这是一个上下文管理器,用于指示接下来的操作不需要计算梯度。这在评估模型时很有用,因为评估不需要反向传播。
    for data in test_dataloader:  # 遍历测试数据加载器中的所有数据
        images, labels = data[1].to('cpu').float(), data[0].to('cpu').long()  # 将标签转换为整数类型
        net = net.float() # 确保神经网络模型也是用浮点数运算
        outputs = net(images) # 将图像输入到神经网络中,得到输出
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0) # 增加总样本数量,因为我们已经处理了一个样本
        correct += (predicted == labels).sum().item() # 统计预测正确的样本数量,.sum().item() 来将结果从张量转换为整数
 
accuracy = 100 * correct / total # 计算准确率
print(f'Accuracy on test set: {accuracy:.2f}%')

4.11模型优化 

        对于训练好的模型,我们也可以进行优化。这里,我们使用Pytorch框架的intel_extension_for_pytorch。但是,该优化只支持Linux系统,不支持Windows系统。不过,可以使用Intel公司提供的oneAPI软件开发工具,它支持跨多种硬件架构的高性能计算,提供了一套统一的编程模型和工具。

        我们可以完全使用oneAPI来编写代码,只需要将数据集上传到服务器上,就可以完全依赖oneAPI来编写和训练模型,并进行预测。当然,我们也可以在本地编写和训练模型,然后将训练好的模型保存在本地后上传至oneAPI来调用下方代码进行优化,然后保存优化后的模型,再下载到本地。

oneAPI链接:https://devcloud.intel.com/oneapi/home/

import intel_extension_for_pytorch as ipex
net,optimizer = ipex.optimize(new_net,optimizer=optimizer)
torch.save(net , 'new_model.pth')

五、测试数据

5.1下载测试集

        测试集中包含了50个图像数据和其对应的包含标签的txt文件。

链接:https://pan.baidu.com/s/1pFESHnfWRR5Dt795M2XMoQ (有效期30天)
提取码:2401

5.2 加载测试集数据

        此处直接调用了4.2模块的方法,注意的是4.2中该方法生成的txt文件名需要修改一下,两次调用不要出现重名        

 get_file_name('./检测并清除杂草_test')

 5.3对测试集进行数据预处理

        对测试的数据进行4.2-4.5的相同的处理方式,确保测试数据输入时能够与模型相匹配。

# 强化图片数据
transformer = transforms.Compose([
    transforms.ToTensor(),
    transforms.ColorJitter(contrast=0.5),  # 增强对比度
    transforms.Normalize(mean=[0.5], std=[0.5])  # 归一化
])

file_name_url = [] 
test_images_tensor = []
with open(r'./test.txt','r') as f:
    file_name_url=[i.split('\n')[0] for i in f.readlines()]
for i in range(len(file_name_url)):
    image = Image.open('./检测并清除杂草_test/'+file_name_url[i])
    #tensor = transformer(image.convert('L')).type(torch.float16)
    test_tensor = transformer(image.convert('L'))
    test_images_tensor.append(test_tensor)

test_images = []
for i in range(len(test_images_tensor)):
    test_images.append(test_images_tensor[i])

# 强化标签数据
transformerlab = transforms.Compose([
    transforms.ToTensor()
])
 
test_lables_tensor = []
with open(r'./test.txt','r') as f:
    file_name_url=[i.split('.')[0] for i in f.readlines()]

 
for i in range(len(file_name_url)):
    image = open('./检测并清除杂草_test/' + file_name_url[i] + '.txt')
    labels = image.readline()[0]
    labels = float(labels)
    #tensor = torch.tensor(labels, dtype=torch.float16)  # 使用float16数据类型
    test_tensor = torch.tensor(labels)  # 
    test_lables_tensor.append(test_tensor)
 

test_lables = []
for i in range(len(test_lables_tensor)):
    test_lables.append(test_lables_tensor[i])

# 做成数据集
new_test_image_tensor = torch.stack(test_images)
new_test_lables_tensor = torch.stack(test_lables)

new_test_dataset = TensorDataset(new_test_lables_tensor, new_test_image_tensor)
new_test_dataloader = DataLoader(new_test_dataset, batch_size=32, shuffle=True)

 5.4调用模型进行测试

        使用torch.load('model.pth') 直接调用之前训练好的模型,传入数据进行推理,并计算时间和准确度。通过多次运行结果来看,准确度在96%——98%,时间在3.5秒左右。

import time
from sklearn.metrics import f1_score

# 记录正确的样本数
correct = 0
# 记录总样本数
total = 0

y_true = []
y_pred = []
net = torch.load('model.pth') 

start_time = time.time()
with torch.no_grad():  #这是一个上下文管理器,用于指示接下来的操作不需要计算梯度。这在评估模型时很有用,因为评估不需要反向传播。
    for data in new_test_dataloader:  # 遍历测试数据加载器中的所有数据
        images, labels = data[1].to('cpu').float(), data[0].to('cpu').long()  # 将标签转换为整数类型
        net = net.float() # 确保神经网络模型也是用浮点数运算
        outputs = net(images) # 将图像输入到神经网络中,得到输出
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0) # 增加总样本数量,因为我们已经处理了一个样本
        correct += (predicted == labels).sum().item() # 统计预测正确的样本数量,.sum().item() 来将结果从张量转换为整数
        
        y_pred.append(predicted)
        y_true.append(labels)


end_time = time.time()
all_time = end_time - start_time # 计算时间
accuracy = 100 * correct / total # 计算准确率
y_pred =  [tensor.numpy() for tensor in y_pred]
y_true =  [tensor.numpy() for tensor in y_true]
print(y_pred[0],y_true[0])
f1 = f1_score(y_true[0], y_pred[0]) # 计算F1值
print(f'Accuracy on test set: {accuracy:.2f}%')
print(f'The F1 is:{f1:.2f}')
print(f'Total time is:{all_time:.2f}秒')

截取一次运行结果:

准确率为:98%

时间为:3.48秒

5.5调用优化后的模型进行测试

        使用torch.load('new_model.pth')直接调用优化后的模型对测试集进行预测。测试数据和5.4模块一样。通过运行结果来看,准确率已经达到98%——100%,且多次预测在100%;时间在2秒左右。与没有优化的模型相比,准确度上略有所提升,时间上提升要明显一点。由于该测试集数据较少,只有50个,所以优化后的效果也不是特别明显。当然,如果扩大测试集的数据规模,其提升效果也会不同。

test_net = torch.load('new_model.pth')
import time
from sklearn.metrics import f1_score

# 记录正确的样本数
correct = 0
# 记录总样本数
total = 0

y_true = []
y_pred = []


start_time = time.time()
with torch.no_grad():  #这是一个上下文管理器,用于指示接下来的操作不需要计算梯度。这在评估模型时很有用,因为评估不需要反向传播。
    for data in new_test_dataloader:  # 遍历测试数据加载器中的所有数据
        images, labels = data[1].to('cpu').float(), data[0].to('cpu').long()  # 将标签转换为整数类型
        net = test_net.float() # 确保神经网络模型也是用浮点数运算
        outputs = net(images) # 将图像输入到神经网络中,得到输出
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0) # 增加总样本数量,因为我们已经处理了一个样本
        correct += (predicted == labels).sum().item() # 统计预测正确的样本数量,.sum().item() 来将结果从张量转换为整数
        
        y_pred.append(predicted)
        y_true.append(labels)


end_time = time.time()
all_time = end_time - start_time # 计算时间
accuracy = 100 * correct / total # 计算准确率
y_pred =  [tensor.numpy() for tensor in y_pred]
y_true =  [tensor.numpy() for tensor in y_true]
print(f'Accuracy on test set: {accuracy:.2f}%')
print(f'Total time is:{all_time:.2f}秒')

截取一次运行结果:

准确率为:100%

时间为:2.04秒

 5.6计算F1值 

        由于模型直接输出的数据类型为 dtype=torch.int32,且输出的预测值为一个不等长的二维数组,所以在计算F1值之前,需要对预测值和标签数据进行处理,将其转换成一维的int型数组。F1值与模型准确度成正比,且我将代码单独放一个代码块,所以不同的模型都可以使用这个相同的代码块。这里也将附上两个模型预测的F1值,当预测结果为98%时,F1值就为0.98;当预测模型为100%时,预测结果就为1.00。

y_pred_np = []
print(y_pred)
for i in range(len(y_pred[0])):
    y_pred_np.append(y_pred[0][i])
for i in range(len(y_pred[1])):
    y_pred_np.append(y_pred[1][i])
print(y_pred_np)
y_true_np = []
print(y_true)
for i in range(len(y_true[0])):
    y_true_np.append(y_true[0][i])
for i in range(len(y_true[1])):
    y_true_np.append(y_true[1][i])
print(y_true_np)
f1 = f1_score(y_true_np, y_pred_np) # 计算F1值
print(f'The F1 is:{f1:.2f}')

 截取两次运行结果:F1值分别为0.98和1.00

 

  • 24
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值