一,作业简介:杂草检测
1.、问题描述:
杂草是农业经营中不受欢迎的入侵者,它们通过窃取营养、水、土地和其他关键资源来破坏种植,这些入侵者会导致产量下降和资源部署效率低下。一种已知的方法是使用杀虫剂来清除杂草,但杀虫剂会给人类带来健康风险。我们的目标是利用计算机视觉技术可以自动检测杂草的存在,开发一种只在杂草上而不是在作物上喷洒农药的系统,并使用针对性的修复技术将其从田地中清除,从而最小化杂草对环境的负面影响。
2、预期解决方案:
我们期待您将其部署到模拟的生产环境中——这里推理时间和二分类准确度(F1分数)将作为评分的主要依据。
3、数据集:
https://filerepo.idzcn.com/hack2023/Weed_Detection5a431d7.zip
3.1.农作物图片展示:(标签(labels):0)
3.2.杂草图片展示:(标签(labels):1)
4、使用平台:
4.1.英特尔oneAPI Developer Cloud提供的免安装基于Jupyter Lab
4.2.谷歌的云端硬盘中的colaboratory
在此解释为什么我用两个平台做,谷歌的云端硬盘中可以使用T4 GPU方便训练模型,大幅降低模型训练时间;英特尔的平台可以提供oneAPI组件进行优化加速。两个平台中使用的数据集一致,代码大部分一致,在路径,和一些平台适应性上的代码略有不同。在最后我会分别给出两种完整代码。在分析解释时,重点使用英特尔的平台代码。
二:数据预处理
1.将data导入并解压。
data中的文件有txt和jpeg两种文件,成对出现。通过txt文件的第一个数字分辨是农作物还是杂草,获取第一个数字, 如果标签为0,即该图片是农作物;如果标签补位0,则是杂草。(最后val标签里面的数据我改为老师要求的测试集,即检测并清除杂草.zip)
2.探索性分析:
import cv2
train_dir = r'data' # 图片路径
crop_imgs = [] # 存放农作物的图像路径
weed_imgs = [] # 存放杂草的图像路径
Data = [] # 存放所有数据,用于后续分类
# 通过txt文件的第一个数字分辨是农作物还是杂草,获取第一个数字
for file in os.listdir(train_dir):
if file.endswith(".txt"):
file_path = os.path.join(train_dir, file)
with open(file_path, 'r') as f:
data = f.readline()
root, ext = os.path.splitext(file_path)
new_file_path = root + ".jpeg"
# 如果标签为0,即该图片是农作物
if(data[0][0] == '0'):
# 农作物的路径
crop_imgs.append(new_file_path)
else:
# 杂草的路径
weed_imgs.append(new_file_path)
# 图片和标签对应
Data.append((new_file_path, int(data[0][0])))
# 随机不重样的抽选3个农作物,3个杂草
select_CROP = np.random.choice(crop_imgs, 3, replace = False)
select_WEED = np.random.choice(weed_imgs, 3, replace = False)
# 使用pit打印出来,大小为20*10英寸
fig = plt.figure(figsize = (20,10))
for i in range(6):
if i < 3:
fp = f'{select_CROP[i]}'
label = '0'
else:
fp = f'{select_WEED[i-3]}'
label = '1'
ax = fig.add_subplot(2, 3, i+1)#两行三列
# to plot without rescaling, remove target_size
fn = cv2.imread(fp) # 加载图片
fn_gray = cv2.cvtColor(fn, cv2.COLOR_BGR2GRAY) # 将彩色图像转换为灰度图像
plt.imshow(fn, cmap='Greys_r') # 显示灰度反转后的图像
plt.title(label) # 设置label标题
plt.axis('off') # 关闭坐标轴
plt.show()
# 总的训练集样本数
print(f'农作物数量为: {len(crop_imgs)}')
print(f'杂草数量为: {len(weed_imgs)}')
3.将数据集分类:
这里我将数据集分成三类,分别包含test,train,val文件夹
每个文件夹下面有两个子文件夹分别是crop和weed子文件夹。
# 数据集路径
data_dir = 'data'
# 将数据集分成3部分, train, val, test
for divide in ['train', 'val', 'test']:
divide_dir = os.path.join(data_dir, divide)
# 每部分的数据集都包含两部分,农作物和杂草
for kinds in ['crop', 'weed']:
kinds_dir = os.path.join(divide_dir, kinds)
os.makedirs(kinds_dir, exist_ok = True) # exist_ok为true表示当目标目录已经存在时,函数不会引发错误
# 将train, val, test按比例分别划分为0.7, 0.2, 0.1
train_data, testVal_data = train_test_split(Data, test_size = 0.3, random_state = 42)
test_data, val_data = train_test_split(testVal_data, test_size = 0.2, random_state = 42)
# 将train_data中的文件分类,并复制到train/crop or weed下
for file in train_data:
if (file[1] == 0): # 农作物
shutil.copy(file[0], 'data/train/crop')
else:
shutil.copy(file[0], 'data/train/weed')
# 将val_data中的文件分类,并复制到val/crop or weed下
for file in val_data:
if (file[1] == 0):
shutil.copy(file[0], 'data/val/crop')
else:
shutil.copy(file[0], 'data/val/weed')
# 将test_data中的文件分类,并复制到test/crop or weed下
for file in test_data:
if (file[1] == 0):
shutil.copy(file[0], 'data/test/crop')
else:
shutil.copy(file[0], 'data/test/weed')
4.提取数据集
在本项目中,为了更好地提取出图像,我构建了一个函数,能够将每个主文件夹下的图片提取出来,并且打好了标签。
# 创建自定义数据集
class SelfDataset(Dataset):
def __init__(self, root_dir, transform=None):
self.root_dir = root_dir
self.transform = transform
self.classes = ['crop', 'weed']
self.data = self.load_data()
def load_data(self):
data = []
for class_idx, class_name in enumerate(self.classes): # 每次迭代返回标签和class_name
class_path = os.path.join(self.root_dir, class_name)
for file_name in os.listdir(class_path):
file_path = os.path.join(class_path, file_name)
if os.path.isfile(file_path) and file_name.lower().endswith('.jpeg'):
data.append((file_path, class_idx))
return data
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
img_path, label = self.data[idx]
img = Image.open(img_path).convert('RGB')
if self.transform:
img = self.transform(img)
return img, label
5.对train的数据增强,并构建数据集
在这里,我采用了transforms.RandomResizedCrop(64)首先对图像进行随机裁剪,并随机调整裁剪后的图像大小为 64x64 像素。这样的操作有助于模型学习对不同尺寸和位置的物体具有更好的鲁棒性,从而提高泛化能力。
其次,我使用了transforms.RandomHorizontalFlip进行随机水平翻转图像。这个操作通过一定的概率水平翻转图像,以扩充训练数据。这可以帮助模型学到物体在水平方向上的不变性。
# 数据集路径
train_dataset_path = './data/train'
test_dataset_path = './data/test'
val_dataset_path = './data/val'
# 数据增强
transform = transforms.Compose([
transforms.RandomResizedCrop(64), # 随机裁剪为64*64像素大小
transforms.RandomHorizontalFlip(), # 随机翻转转换
transforms.ToTensor(), # 转换为PyTorch Tensor,有助于模型的输入与PyTorch的期望格式匹配
])
# 创建数据集实例
train_dataset = SelfDataset(root_dir=train_dataset_path, transform=transform)
test_dataset = SelfDataset(root_dir=test_dataset_path, transform=transform)
val_dataset = SelfDataset(root_dir=val_dataset_path, transform=transform)
# 创建 DataLoader
batch_size = 64
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) # 启用随机打乱
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
三、使用卷积神经网络识别杂草图像
1.VGG-16架构
VGG16是由Karen Simonyan和Andrew Zisserman于2014年在论文“VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE SCALE IMAGE RECOGNITION”中提出的一种处理多分类、大范围图像识别问题的卷积神经网络架构,成功对ImageNet数据集的14万张图片进行了1000个类别的归类并有92.7%的准确率。
本项目将分类层的最后一层修改为(1x1x2)即可将分类结果从1000类 修改为二分类。
2.卷积神经网络
卷积神经网络(Convolutional Neural Network,CNN)是一种专门用于处理具有网格结构数据(如图像和视频)的深度学习模型。CNN 在计算机视觉任务中取得了巨大成功,因为它能够有效地捕获图像中的空间结构信息。
3深度神经网络
深度神经网络(Deep Neural Network,DNN)是一种神经网络结构,其具有多个隐藏层,使其成为深层次模型。深度神经网络是深度学习的核心组成部分,能够学习和表示更抽象、更复杂的数据特征,适用于各种机器学习任务。
4.更改VGG-16网络结构
传统的VGG-16网络的输出是1000的大小,为了适合本项目,我将网络改成了2分类问题并对一部分网络进行了优化。
vgg16_model = models.vgg16(pretrained=True)
# 如果需要微调,可以解冻最后几层
for param in vgg16_model.features.parameters():
param.requires_grad = False
# 修改分类层
num_features = vgg16_model.classifier[6].in_features
vgg16_model.classifier[6] = nn.Sequential(
nn.Linear(num_features, 512),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(512, 2)
)
四、在GPU上训练
1.参数设置
在这里我使用了以下几个部分来提高训练的精度:
(1):交叉熵损失函数 (nn.CrossEntropyLoss())。交叉熵损失对于分类任务是一种常见的损失函数,它在训练期间衡量模型的预测和真实标签之间的差异。
(2):Adam 优化器 (optim.Adam)。是一种基于梯度的优化算法,通常在深度学习中表现较好。
(3): ReduceLROnPlateau 学习率调度器(optim.lr_scheduler.ReduceLROnPlateau)。该调度器在验证集上监测模型性能,并在性能停滞时降低学习率。
criterion = nn.CrossEntropyLoss() # 定义了一个损失函数,用于衡量模型的预测与真实标签之间的差异
optimizer = optim.Adam(vgg16_model.parameters(), lr=0.001, weight_decay=1e-4) # 定义了一个优化器,用于更新模型的权重,设置权重衰减为0.0001
# 添加学习率调度器
# 使用 ReduceLROnPlateau 调度器,当验证损失在一定时间内不再下降时,它会降低学习率
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', factor=0.1, patience=3, verbose=True)
# 训练参数
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
vgg16_model.to(device)
2.在GPU上训练50次
在这里,我使用了三个数据集,分别为train,val,test。
2.1训练集(train):
用途: 用于训练机器学习模型。模型通过学习训练集中的样本来调整参数,使其能够捕捉输入数据的模式和特征。
特点: 训练集通常是最大的数据集,包含用于模型训练的大量样本。高质量、多样性的训练集有助于提高模型的泛化能力,使其在未见过的数据上表现良好。
2.2验证集(val):
用途: 用于调整模型超参数、选择模型架构和进行早停等操作。验证集上的性能评估有助于避免模型在训练集上过拟合,提高对未知数据的泛化能力。
特点: 验证集通常是从独立于训练集的数据中划分出来的,模型在训练过程中不使用验证集的信息。在训练过程中,通过监控验证集上的性能来调整模型的参数和架构。
2.3测试集(test):
用途: 用于评估训练好的模型的性能。测试集中的样本是模型在训练和验证过程中未曾见过的数据,因此测试集上的性能评估更接近模型在真实场景中的表现。
特点: 测试集应该是完全独立于训练集和验证集的,确保模型在测试集上的表现不受过拟合或过度调整的影响。测试集上的性能评估是对模型泛化能力的最终验证。
# 训练循环
num_epochs = 0
consecutive_f1_count = 0
while num_epochs < 50:
print(f'第{num_epochs+1}次训练开始了')
vgg16_model.train() # 设置模型为训练模式
train_loss = 0.0
for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.to(device)
# 将数据传递给模型
outputs = vgg16_model(inputs)
# 计算损失
loss = criterion(outputs, labels)
# 反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.item()
# 在每个 epoch 结束时进行验证
val_loss = 0.0
with torch.no_grad():
for inputs, labels in val_loader:
inputs, labels = inputs.to(device), labels.to(device)
# 在验证集上进行推理,可根据需要添加评估代码
val_outputs = vgg16_model(inputs)
val_loss += criterion(val_outputs, labels).item()
# 计算平均训练损失
avg_train_loss = train_loss / len(train_loader)
# 计算平均验证损失
avg_val_loss = val_loss / len(val_loader)
# 打印训练过程中的损失和验证损失
print(f'Epoch [{num_epochs+1}], 第{num_epochs+1}轮:训练集损失: {avg_train_loss:.4f}, 验证集损失: {avg_val_loss:.4f}')
# 在模型训练完后,使用测试集进行最终评估
vgg16_model.eval()
all_predictions = []
all_labels = []
start_time = time.time() # 记录开始时间
with torch.no_grad():
for inputs, labels in test_loader:
inputs, labels = inputs.to(device), labels.to(device)
# 在测试集上进行推理
outputs = vgg16_model(inputs)
# 将预测结果和真实标签保存
_, predicted = torch.max(outputs, 1)
all_predictions.extend(predicted.cpu().numpy())
all_labels.extend(labels.cpu().numpy())
end_time = time.time() # 记录结束时间
elapsed_time = end_time - start_time
print(f'测试集用的时间为: {elapsed_time:.2f} seconds')
# 计算F1分数
f1 = f1_score(all_labels, all_predictions, average='binary') # 适用于二分类问题
# 打印每轮的测试F1分数
print(f'第{num_epochs+1}轮的测试F1分数: {f1:.4f}')
# 调整学习率
scheduler.step(f1)
# 增加训练次数
num_epochs += 1
3.查看test数据集F1分数及时间
4.保存为VGG16模型并使用模型进行推理测试
这里使用test数据集里面的图像进行推理测试并打印出相应的图像。
# 保存模型
torch.save(vgg16_model.state_dict(), 'vgg16.pth')
# 打印保存成功的消息
print("模型已保存为 vgg16.pth")
import matplotlib.pyplot as plt
import numpy as np
# 选择一张 test_loader 中的图片
sample_image, true_label = next(iter(test_loader))
# 将图片传递给模型进行预测
sample_image = sample_image.to(device)
with torch.no_grad():
model_output = vgg16_model(sample_image)
# 获取预测结果
_, predicted_label = torch.max(model_output, 1)
# 转换为 NumPy 数组
sample_image = sample_image.cpu().numpy()[0] # 将数据从 GPU 移回 CPU 并取出第一张图片
predicted_label = predicted_label[0].item()
true_label = true_label[0].item() # 直接获取标量值
# 获取类别标签
class_labels = ['crop', 'weed']
# 显示图像
plt.imshow(np.transpose(sample_image, (1, 2, 0))) # 转置图片的维度顺序
plt.title(f'TRUE LABEL IS: {class_labels[true_label]}, PREDICT LABEL IS: {class_labels[predicted_label]}')
plt.axis('off')
plt.show()
五、转移到CPU上
1.创建VGG16模型
这里将GPU训练的模型保存到了vgg16.pth中,在CPU上进行加载。
class CustomVGG16(nn.Module):
def __init__(self):
super(CustomVGG16, self).__init__()
self.vgg16_model = models.vgg16(pretrained=True)
for param in self.vgg16_model.features.parameters():
param.requires_grad = False
num_features = self.vgg16_model.classifier[6].in_features
self.vgg16_model.classifier[6] = nn.Sequential(
nn.Linear(num_features, 512),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(512, 2)
)
def forward(self, x):
return self.vgg16_model(x)
# 创建 CustomVGG16 模型实例
vgg16_model = CustomVGG16()
# 加载权重
vgg16_model.load_state_dict(torch.load('vgg16.pth', map_location=torch.device('cpu')))
2.直接在CPU上面运行
这里测试集用的时间为: 4.66 seconds,F1分数为: 0.9642
vgg16_model.eval()
# Assuming you have a DataLoader for the test dataset (test_loader)
all_predictions = []
all_labels = []
start_time = time.time()
with torch.no_grad():
for inputs, labels in test_loader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = vgg16_model(inputs)
_, predicted = torch.max(outputs, 1)
all_predictions.extend(predicted.cpu().numpy())
all_labels.extend(labels.cpu().numpy())
end_time = time.time() # 记录结束时间
elapsed_time = end_time - start_time
print(f'测试集用的时间为: {elapsed_time:.2f} seconds')
f1 = f1_score(all_labels, all_predictions, average='binary') # 适用于二分类问题
print(f'F1分数为: {f1:.4f}')
六:OneAPI组件的使用
1.Transfer Learning with oneAPI AI Analytics Toolkit进行迁移学习
class CustomVGG16(nn.Module):
def __init__(self):
super(CustomVGG16, self).__init__()
self.vgg16_model = models.vgg16(pretrained=True)
for param in self.vgg16_model.features.parameters():
param.requires_grad = False
num_features = self.vgg16_model.classifier[6].in_features
self.vgg16_model.classifier[6] = nn.Sequential(
nn.Linear(num_features, 512),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(512, 2)
)
def forward(self, x):
return self.vgg16_model(x)
# 创建 CustomVGG16 模型实例
vgg16_model = CustomVGG16()
# 加载权重
vgg16_model.load_state_dict(torch.load('vgg16.pth', map_location=torch.device('cpu')))
2.使用Intel Extension for PyTorch进行优化
上面我发现使用CPU直接进行训练的话会相当慢,在这里使用Intel Extension for PyTorch大大提高了速度。大概缩短了时间,并且F1的值并没有太大改变。
# 将模型移动到CPU
device = torch.device('cpu')
vgg16_model.to(device)
# 重新构建优化器
optimizer = optim.Adam(vgg16_model.parameters(), lr=0.001, weight_decay=1e-4)
# 使用Intel Extension for PyTorch进行优化
vgg16_model, optimizer = ipex.optimize(model=vgg16_model, optimizer=optimizer, dtype=torch.float32)
3.保存使用Intel Extension for PyTorch进行优化的模型
# 保存模型参数
torch.save(vgg16_model.state_dict(), 'vgg16_optimized.pth')
# 加载模型参数
loaded_model = CustomVGG16()
loaded_model.load_state_dict(torch.load('vgg16_optimized.pth'))
4.使用 Intel® Neural Compressor 量化模型
这里对优化后的模型vgg16_optimized.pth进行加载
import os
import torch
# 检查文件是否存在
assert os.path.exists("./vgg16_optimized.pth"), "文件不存在"
# 尝试加载模型
model = torch.load("./vgg16_optimized.pth")
print("模型加载成功")
加载完成以后以准确度为评估函数进行量化
from neural_compressor.config import PostTrainingQuantConfig, AccuracyCriterion
from neural_compressor import quantization
import os
# 加载模型
model = CustomVGG16()
model.load_state_dict(torch.load('vgg16_optimized.pth'))
model.to('cpu') # 将模型移动到 CPU
model.eval()
# 定义评估函数
def eval_func(model):
with torch.no_grad():
y_true = []
y_pred = []
for inputs, labels in train_loader:
inputs = inputs.to('cpu')
labels = labels.to('cpu')
preds_probs = model(inputs)
preds_class = torch.argmax(preds_probs, dim=-1)
y_true.extend(labels.numpy())
y_pred.extend(preds_class.numpy())
return accuracy_score(y_true, y_pred)
# 配置量化参数
conf = PostTrainingQuantConfig(backend='ipex', # 使用 Intel PyTorch Extension
accuracy_criterion=AccuracyCriterion(higher_is_better=True,
criterion='relative',
tolerable_loss=0.01))
# 执行量化
q_model = quantization.fit(model,
conf,
calib_dataloader=train_loader,
eval_func=eval_func)
# 保存量化模型
quantized_model_path = './quantized_models'
if not os.path.exists(quantized_model_path):
os.makedirs(quantized_model_path)
q_model.save(quantized_model_path)
量化成功以后会出现如下代码
查看量化后的模型,分别为pt文件和json文件
5.使用量化后的模型在 CPU上进行推理
5.1.加载模型
import torch
import json
from neural_compressor import quantization
# 指定量化模型的路径
quantized_model_path = './quantized_models'
# 加载 Qt 模型和 JSON 配置
vgg16_model_path = f'{quantized_model_path}/best_model.pt'
json_config_path = f'{quantized_model_path}/best_configure.json'
# 加载 Qt 模型
vgg16_model = torch.jit.load(vgg16_model_path, map_location='cpu')
# 加载 JSON 配置
with open(json_config_path, 'r') as json_file:
json_config = json.load(json_file)
5.2.进行推理
import torch
from sklearn.metrics import f1_score
import time
# 假设 test_loader 是你的测试数据加载器
# 请确保它返回 (inputs, labels) 的形式
# 将模型设置为评估模式
vgg16_model.eval()
# 初始化变量用于存储真实标签和预测标签
y_true = []
y_pred = []
# 开始推理
start_time = time.time()
# 设置 batch_size
batch_size = 64
# 使用 DataLoader 时设置 batch_size
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
# 在推理时处理每个批次
with torch.no_grad():
for inputs, labels in test_loader:
# 将输入数据移动到 CPU(如果尚未在 CPU 上)
inputs = inputs.to('cpu')
labels = labels.to('cpu')
# 获取模型预测
preds_probs = vgg16_model(inputs)
preds_class = torch.argmax(preds_probs, dim=-1)
# 扩展真实标签和预测标签列表
y_true.extend(labels.numpy())
y_pred.extend(preds_class.numpy())
# 计算 F1 分数
f1 = f1_score(y_true, y_pred, average='weighted')
# 计算推理时间
inference_time = time.time() - start_time
# 打印结果
print(f"F1 Score: {f1}")
print(f"Inference Time: {inference_time} seconds")
5.3.推理结果:
5.4.F1分数及推理时间 :
七:总结
在使用oneAPI的优化组件以后,推理的时间大幅度下降,从原来的 4.66s到目前的2.06s,时间缩短了一倍!其次,在使用量化工具以后,在缩短推理时间的同时,整个过程中F1分数的值一直稳定在0.96左右,这是一个非常好的现象。证明了oneAPI优秀的模型压缩能力,在保证模型精确度,F1值的基础上还能够缩小模型的规模。