1.1问题描述
杂草是农业经营中不受欢迎的入侵者,它们通过窃取营养、水、土地和其他关键资源来破坏种植,这些入侵者会导致产量下降和资源部署效率低下。一种已知的方法是使用杀虫剂来清除杂草,但杀虫剂会给人类带来健康风险。我们的目标是利用计算机视觉技术可以自动检测杂草的存在,开发一种只在杂草上而不是在作物上喷洒农药的系统,并使用针对性的修复技术将其从田地中清除,从而最小化杂草对环境的负面影响。
1.2预期解决方案
我们期待您将其部署到模拟的生产环境中——这里推理时间和二分类准确度(F1分数)将作为评分的主要依据。
1.3原始数据集
figure 1 原始数据集
如图1所示,原始数据集以agri_0_n.jpeg, agri_0_n.txt命名,n从0到1399,一共2600项,1300对数据。
对于每组数据,jpeg文件存储其图像信息,txt文件第一个int型数据代表该组数据标签,该值为1代表该图像数据为杂草;
该值为0代表该图像数据标签是农作物。
1.4数据展示
figure 2 图像数据展示
Crop与Weed图像展示如上图,均为分辨率=512*512,位深度=22bits的jpeg文件。
figure 3 文本数据展示
以agri_0_3.txt为例,首位数字代表其标签,标志该同名图像是否杂草。
2数据预处理
2.1探索性分析
随机抽取3个Weed与Crop,统计其数量信息。
import cv2
import os
import numpy as np
import matplotlib.pyplot as plt
train_dir = 'data/'
# 获取图片和对应的标签
image_files = [fn for fn in os.listdir(train_dir) if fn.endswith('.jpeg')]
data = []
for image_file in image_files:
# 获取对应的txt文件路径
txt_file = image_file.replace('.jpeg', '.txt')
txt_path = os.path.join(train_dir, txt_file)
# 统计作物和杂草的数量
crop_count = sum(1 for _, label in data if label == 0)
weed_count = sum(1 for _, label in data if label == 1)
print(f'作物数量为: {crop_count}')
print(f'杂草数量为: {weed_count}')
# 随机选择3个作物和3个杂草图片
select_crop = np.random.choice([img for img, label in data if label == 0], 3, replace=False)
select_weed = np.random.choice([img for img, label in data if label == 1], 3, replace=False)
# 使用subplot打印出来,只显示彩色图像
fig = plt.figure(figsize=(20, 10))
for i in range(6):
if i < 3:
fp = f'{select_crop[i]}'
label = 'Crop'
else:
fp = f'{select_weed[i-3]}'
label = 'Weed'
ax = fig.add_subplot(2, 3, i + 1)
#print(fp)
img = cv2.imread(fp)
#print(img)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)
plt.title(label)
plt.axis('off')
plt.show()
figure 4 Crop与Weed样本量对比
杂草与作物数据量基本一致。
2.2提取数据集
这里首先对混在一起的数据集进行处理,目标是将其分配为train、test、val三种数据集,依照txt文件的标签分别在每种数据集下分为Weed与Crop,所以先进行相应文件目录的创建,方便后续处理。
import os
import shutil
from sklearn.model_selection import train_test_split
data_dir = 'data/' # 数据集目录
output_dir = 'data/' # 输出目录(train, test, val)
# 创建输出文件夹
for split in ['train', 'test', 'val']:
split_dir = os.path.join(output_dir, split)
for category in ['Crop', 'Weed']:
category_dir = os.path.join(split_dir, category)
os.makedirs(category_dir, exist_ok=True)
这里对原始数据集进行处理,按照0.8,0.1,0.1的比例分别分配给train,test,val,根据图像标签分类。
训练集(train set)、验证集(validation set )、测试集(test set),每个数据集都有不同的用途和特点:
训练集(Train set):
用途:训练模型的最大的数据集。用来拟合模型的参数,使其能够学习到数据的模式和特征。
特点:通常是最大的数据集,用于模型的训练和参数优化。模型通过与训练集中的数据进行交互来学习。
验证集(Validation set):
用途:评估模型在训练过程中的性能和调整超参数。用于模型选择、调整和验证。
特点:用于调整模型的超参数(例如学习率、正则化参数等),以避免过拟合。通常不参与模型参数的训练,而是用来选择最佳模型或确定模型的泛化能力。
测试集(Test set):
用途:评估模型的泛化能力和性能,检验模型对未见过数据的泛化情况。
特点:完全独立于训练过程。模型在测试集上的表现能够反映其在实际应用场景中的效果。
# 获取图像和标签的对应关系
data = []
for file in os.listdir(data_dir):
if file.endswith('.txt'):
txt_path = os.path.join(data_dir, file)
with open(txt_path, 'r') as txt_file:
label = int(txt_file.readline().strip().split()[0])
img_path = os.path.join(data_dir, file.replace('.txt', '.jpeg'))
data.append((img_path, label))
# 按比例划分数据集
train_data, test_val_data = train_test_split(data, test_size=0.2, random_state=42)
test_data, val_data = train_test_split(test_val_data, test_size=0.5, random_state=42)
分配后当前目录如上图所示,分为三级结构,方便后续提取。
接着通过自定义函数提取数据,配合所在目录打好标签。
# 创建自定义数据集
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_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
2.4数据增强
# 数据增强
transform = transforms.Compose([
transforms.RandomResizedCrop(64),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
])
针对图像数据进行数据增强的操作,用于在训练深度学习模型时扩充训练数据集,以提高模型的泛化能力和鲁棒性。
这里定义了一个数据增强的操作序列,使用了以下几种变换:
- transforms.RandomResizedCrop(64):
在图像上进行随机裁剪和缩放。它会随机地从图像中裁剪出一个区域,并将其缩放为指定的大小(64x64像素)。这个操作有助于模型对不同大小和位置的物体具有更好的识别能力。
- transforms.RandomHorizontalFlip():
进行随机水平翻转图像。
这种操作通过随机翻转图像,增加了训练数据的多样性。物体在水平翻转后仍然具有相同的类别标签,因此这种变换不会改变图像的标签,但可以为模型提供更多变化的样本。
- transforms.ToTensor():
将图像数据转换为 PyTorch 的张量格式。
模型通常需要输入张量形式的数据进行训练,因此这个操作将图像数据转换为模型可接受的格式。
2.5构建数据集
# 创建数据集实例
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)
使用 DataLoader
来加载自定义数据集,并为训练、测试和验证集创建数据加载器。
3.深度神经网络结构分析和修改
3.1传统CNN
传统 CNN 是最早应用于计算机视觉任务的神经网络类型之一。它包括卷积层、池化层和全连接层。卷积层用于提取图像的特征,池化层则用于减少特征图的大小。全连接层在提取了特征之后进行分类或回归等任务。
3.2 深度神经网络
深度神经网络是指具有多个隐藏层的神经网络,允许学习非常复杂的函数映射。这种结构的网络通常包含多个隐藏层,如深度卷积神经网络(DCNNs)或深度残差网络(ResNets),它们通过堆叠多个层来提高模型的表达能力。
3.1VGG16架构
VGG(Visual Geometry Group)是一个视觉几何组在2014年提出的深度卷积神经网络架构。
VGG在2014年ImageNet图像分类竞赛亚军,定位竞赛冠军;VGG网络采用连续的小卷积核(3x3)和池化层构建深度神经网络,网络深度可以达到16层或19层,其中VGG16和VGG19最为著名。
VGG网络被广泛应用于图像分类、目标检测、语义分割等计算机视觉任务中,并且其网络结构的简单性和易实现性使得VGG成为了深度学习领域的经典模型之一。
VGG的参数数量非常大,使用3*3的卷积核,优点如下:
- 参数少: 一个3x3卷积核拥有9个权重参数,而一个5x5卷积核则需要25个权重参数,因此采用3x3卷积核可以大幅度减少网络的参数数量,从而减少过拟合的风险;
- 提高非线性能力: 多个3x3卷积核串联起来可以形成一个感受野更大的卷积核,而且这个组合具有更强的非线性能力。在VGG中,多次使用3x3卷积核相当于采用了更大的卷积核,可以提高网络的特征提取能力;
- 减少计算量: 一个3x3的卷积核可以通过步长为1的卷积操作,得到与一个5x5卷积核步长为2相同的感受野,但计算量更小(即2个3x3代替一个5x5);3个3x3代替一个7x7的卷积;因此,VGG网络采用多个3x3的卷积核,可以在不增加计算量的情况下增加感受野,提高网络的性能。
在图像去雾、超分辨率、风格迁移等领域,感知损失被广泛使用。而感知损失采用较多的正是VGG16网路,后续虽然出现了残差网络ResNet、密集连接网络DenseNet、快速推理的MobileNet等,VGG16网络(及其变种VGG19等)仍然被研究人员普遍使用。

如图可知,传统VGG16网络的全连接层经过激活层后有1000个输出,这里需要针对本文的二分类问题进行全连接层的修改。
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)
)
4.进行训练
4.1定损、决定优化
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(vgg16_model.parameters(), lr=0.001, weight_decay=1e-4)
# 添加学习率调度器
# 使用 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)
这里提供了训练神经网络所需的核心组件:损失函数、优化器、学习率调度器以及设备的选择,以便进行高效的模型训练和参数优化。
下面是修改后的vgg结构:
4.2进行50轮次训练,输出每轮测试结果
# 训练循环
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
训练部分结果如下:
5.使用intel one API优化
5.1使用Intel Extension for PyTorch进行优化
# 使用Intel Extension for PyTorch进行优化
vgg16_model, optimizer = ipex.optimize(model=vgg16_model, optimizer=optimizer, dtype=torch.float32)
对比训练集,用时没有明显差异,这可能是本任务训练集和测试集过小的原因。
5.2使用 Intel® Neural Compressor 量化模型
以准确度为评估函数进行量化。
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)
量化正常进行的输出结果:
保存量化后的模型:
对比初始接近520M的模型,量化后的文件只有120M左右,空间得到了极大的压缩。
5.3测试量化模型
加载量化:
#使用量化的模型
##加载
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)
# 打印 JSON 配置(可选)
#print(json_config)
进行推理测试:
#推理
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"测试集用的时间为: {inference_time} seconds")
print(f"F1分数: {f1}")
测试结果:
5.4总结
在多次测试下,使用Intel Extension for PyTorch进行优化的模型参数耗时稳定在1.03s左右,f1稳定在0.94左右,效率略好于普通模型,针对本任务中100多项的测试集,该优化工具的效果已经有所体现。后续我将继续测试更复杂的数据增强,相信在更大的测试集下使用Intel Extension for PyTorch的优化效果会更突出。
对于使用Intel® Neural Compressor的量化模型,参数的空间占用得到了极大的减少,针对100个所有的测试样本,测试时间在0.4s到1s浮动,耗时得到了极大的优化。且f1稳定在了0.93左右,说明该compressor在压缩模型中很好的保证模型精度和效果。