一、项目简介
- 1、问题描述
- 2、预期解决方案
- 3、数据集
- 4、背景知识
- 4.1、Intel oneAPI
- 4.2、ResNet50
- 二、数据预处理
- 1、自定义数据集类
- 2、图像展示
- 3、数据增强
- 4、划分训练集与测试集
- 5、构建数据集
- 三、在GPU上训练
- 1、自写ResNet网络
- 2、使用ResNet50
- 3、训练模型
- 4、保存模型
- 5、推理测试
- 四、转移到 CPU 上
- 1、构造测试集
- 2、创建模型
- 3、推理测试
- 4、OneAPI 组件的使用
- 五、总结
一、项目简介
1、问题描述
杂草是农业经营中不受欢迎的入侵者,它们通过窃取营养、水、土地和其他关键资源来破坏种植,这些入侵者会导致产量下降和资源部署效率低下。一种已知的方法是使用杀虫剂来清除杂草,但杀虫剂会给人类带来健康风险。
我们的目标是利用计算机视觉技术可以自动检测杂草的存在,开发一种只在杂草上而不是在作物上喷洒农药的系统,并使用针对性的修复技术将其从田地中清除,从而最小化杂草对环境的负面影响。
2、预期解决方案
我们期待您将其部署到模拟的生产环境中——这里推理时间和二分类准确度(F1分数)将作为评分的主要依据。
3、数据集
数据集下载地址:https://filerepo.idzcn.com/hack2023/Weed_Detection5a431d7.zip
原始数据集展示如下图所示:
原始数据集中的文件名都以 argi_0_xxx 开头,后缀名为 jpeg 的为图像文件,后缀名为 txt 的为文本文档。共有 1300 个 jpeg 文件和 1300 个 txt 文件,且每一个 jpeg 文件对应与其同名的一个 txt 文件。jpeg 文件是杂草或作物的图片,与其同名的 txt 文件包含了农作物或杂草的类别信息和坐标。本项目中无需关心坐标信息,只需要文本文档内容中的开头第一个数字。该数字值为 0 代表对应的图像中为农作物,为 1 代表对应的图像中为杂草。
4、背景知识
4.1、Intel oneAPI
Intel oneAPI 是一个跨平台、可移植的开发工具集,它支持多种处理器架构,包括英特尔 CPU、GPU、FPGA 和其他加速器。这套工具集旨在为开发者提供编写高性能并行应用程序的能力,并简化跨平台开发的过程,使开发者能够更轻松地利用不同处理器架构的优势。
同时,oneAPI 还提供了一套统一的编程模型和工具,使开发人员能够轻松地利用不同类型的处理器和加速器来加速应用程序的执行。其目标是实现代码的可移植性和可扩展性,使开发人员能够更高效地利用现代硬件。
我们将使用的主要组件是英特尔 PyTorch 扩展 。Intel Extension for PyTorch 通过最新的功能优化扩展了 PyTorch,从而进一步提升了相关硬件的性能。这个 PyTorch 拓展有以下优势:
- 易于使用的 Python API:用户只需进行少量代码更改即可获得性能优化,例如图形优化和运算符优化。每部分优化通常只需要修改 2 到 3 行代码。
- Channels Last:在 Intel Extension for PyTorch 中,大多数关键 CPU 运算符已启用 NHWC 内存格式。与默认的 NCHW 内存格式相比,channels_last(NHWC) 内存格式可以进一步加速卷积神经网络。
- 自动混合精度(AMP):针对 CPU 的自动混合精度(AMP) 和 BFloat 16 的支持以及运算符的 BFloat 16 优化已在该扩展中大量启用。
- 图形优化:为了利用 torchscript 进一步优化性能,该扩展支持常用算子模式的融合,例如 Conv2D+ReLU、Linear+ReLU 等。
- 运算符优化:该扩展还优化了运算符并实现了多个定制运算符(例如 Mask R-CNN 中定义了 ROIAlign 和 NMS)以提高性能。
4.2、ResNet50
ResNet50 是一种深度残差网络,其结构复杂且功能强大,主要用于图像分类和目标检测的任务。它的核心思想是通过建立残差映射来训练深层网络。它的名称 “50” 来源于网络中总共有 50 层,包括 49 个卷积层和一个全连接层。
ResNet50 的网络结构可以被分为七个部分,每个部分都包含了残差块。这些残差块有助于提高网络的学习能力和性能。具体来说,ResNet50 的结构可以看作是 5 个 stage,每个 stage 包含 5 个 Bottleneck,而每个 Bottleneck 又包含三个卷积层。这种设计使得网络能够有效地学习和提取图像的特征。
在 ResNet50 网络的输入为 224×224×3 的情况下,经过前五部分的卷积计算后,输出为 7×7×2048 的特征图。这个特征图随后会被池化层转化为一个特征向量。最后,全连接层会将这个特征向量转化为最终的分类结果。
与传统的网络结构相比,ResNet 的主要贡献是发现了神经网络的退化现象,并针对退化现象提出了短路连接 shortcut connection,极大地消除了深度过大的神经网络的训练困难问题。
总的来说,ResNet50 的设计巧妙地解决了深度 CNN 模型难以训练的问题,通过利用残差学习来提高网络的性能。
二、数据预处理
1、自定义数据集类
构建自定义数据集
# 数据集类
class CustomDataset(Dataset):
def __init__(self, root_dir, transform=None):
self.root_dir = root_dir
self.transform = transform
self.file_list = [file for file in os.listdir(root_dir) if file.endswith(".jpeg")]
def __len__(self):
return len(self.file_list)
def __getitem__(self, idx):
img_name = os.path.join(self.root_dir, self.file_list[idx])
image = Image.open(img_name).convert("RGB")
# 获取对应的标签文件名
label_name = img_name.replace(".jpeg", ".txt")
with open(label_name, "r") as f:
label_content = f.readline().split()
label = int(label_content[0]) # 第一个数字是标签
if self.transform:
image = self.transform(image)
return image, label
2、图像展示
# 数据目录
data_dir = '/content/gdrive/MyDrive/data'
# 图像和标签的转换
transformer = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
])
# 创建数据集实例
custom_dataset = CustomDataset(root_dir=data_dir, transform=transformer)
# 分别随机选取三张 crop 和三张 weed 的图片并展示
crop_indices = [i for i, (_, label) in enumerate(custom_dataset) if label == 0]
weed_indices = [i for i, (_, label) in enumerate(custom_dataset) if label == 1]
crop_sample_indices = random.sample(crop_indices, 3)
weed_sample_indices = random.sample(weed_indices, 3)
plt.figure(figsize=(15, 5))
# 显示 crop 图片
for i, index in enumerate(crop_sample_indices, 1):
image, label = custom_dataset[index]
plt.subplot(2, 3, i)
plt.imshow((image.numpy().transpose((1, 2, 0)) + 1) / 2) # 将归一化还原
plt.title(f'CROP - Label: {label}')
plt.axis('off')
# 显示 weed 图片
for i, index in enumerate(weed_sample_indices, 1):
image, label = custom_dataset[index]
plt.subplot(2, 3, i + 3)
plt.imshow((image.numpy().transpose((1, 2, 0)) + 1) / 2) # 将归一化还原
plt.title(f'WEED - Label: {label}')
plt.axis('off')
plt.show()
3、数据增强
利用数据转换可以有效的提取想要的数据,并且恰当的数据转换方式还有利于缩短训练模型的时间
transformer = transforms.Compose([
transforms.ToTensor(),
transforms.ColorJitter(contrast=0.5), # 增强对比度
transforms.Normalize(mean=[0.5], std=[0.5]) # 归一化
])
4、划分训练集与测试集
读取图像数据
按照7:3的比例划分训练集和测试集并转化为tensor的float16
train_images_tensor = []
with open(r'/content/gdrive/MyDrive/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('/content/gdrive/MyDrive/data/'+file_name_url[i])
tensor = transformer(image.convert('L')).type(torch.float16)
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])
读取标签数据
按照7:3的比例划分训练集和测试集并转化为tensor的float16
transformerlab = transforms.Compose([
transforms.ToTensor()
])
train_lables_tensor = []
with open(r'/content/gdrive/MyDrive/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('/content/gdrive/MyDrive/data/' + file_name_url[i] + '.txt')
labels = image.readline()[0]
labels = float(labels)
tensor = torch.tensor(labels, dtype=torch.float16) # 使用float16数据类型
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])
5、构建数据集
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)
三、在GPU上训练
1、自写ResNet网络
class Residual(nn.Module):
def __init__(self, input_channels, num_channels, use_conv=False, strides=1):
super().__init__()
self.conv1 = nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1, stride=strides)
self.conv2 = nn.Conv2d(num_channels, num_channels, kernel_size=3, padding=1)
if use_conv:
self.conv3 = nn.Conv2d(input_channels, num_channels, kernel_size=1, stride=strides)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm2d(num_channels)
self.bn2 = nn.BatchNorm2d(num_channels)
def forward(self, X):
Y = F.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
Y += X
return F.relu(Y)
b1 = nn.Sequential(nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
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))
return blk
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
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)),
nn.Flatten(), nn.Linear(512, 10))
2、使用ResNet50
通过导包的方式直接使用 ResNet50
net = torchvision.models.resnet50(pretrained=True)
net.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)
num_features = net.fc.in_features
net.fc = nn.Linear(num_features, 2)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
net.to(device).half()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
3、训练模型
以均方误差作为损失函数(迭代 18 次)
# 定义一个函数,用于计算F1分数
def calculate_f1_score(predictions, labels):
predictions = predictions.argmax(dim=1).cpu().numpy()
labels = labels.cpu().numpy()
f1 = f1_score(labels, predictions, average='macro')
return f1
# 迭代每个epoch
for epoch in range(1, 19):
running_loss = 0.0
num_images = 0
net.train() # 设置模型为训练模式
# 训练阶段
with tqdm(enumerate(train_dataloader, 0)) as loop:
for step, data in loop:
labels, inputs = data[0].to('cuda').float(), data[1].to('cuda').float()
optimizer.zero_grad()
inputs = inputs.half()
outputs = net(inputs)
target = labels
loss = criterion(outputs, target.long())
loss.backward()
optimizer.step()
num_images += inputs.size(0)
running_loss += loss.item()
loop.set_description(f'Epoch [{epoch}/18]')
loop.set_postfix(loss=running_loss / (step + 1))
# 计算平均训练损失并打印
average_train_loss = running_loss / len(train_dataloader)
print(f'\tAverage Training Loss: {average_train_loss:.4f}')
# 测试阶段
net.eval() # 设置模型为评估模式
test_start_time = time.time()
all_predictions = []
all_labels = []
with torch.no_grad():
for data in test_dataloader:
labels, inputs = data[0].to('cuda').float(), data[1].to('cuda').float()
inputs = inputs.half() # 保持和训练阶段一致
outputs = net(inputs)
all_predictions.append(outputs)
all_labels.append(labels)
test_end_time = time.time()
test_time = test_end_time - test_start_time
print(f'\tTest Time: {test_time:.4f} seconds')
# 计算并打印测试F1分数
all_predictions = torch.cat(all_predictions, dim=0)
all_labels = torch.cat(all_labels, dim=0)
f1 = calculate_f1_score(all_predictions, all_labels)
print(f'\tF1 Score: {f1:.4f}\n')
print('\nFinish!!!')
训练过程展示如下:
4、保存模型
# 保存模型
save_path = '/content/gdrive/MyDrive/resnet50_model.pth'
torch.save(net.state_dict(), save_path)
# 打印保存成功的消息
print("模型已保存为",save_path)
5、推理测试
从 test 数据集中随机选择一张图片,并用训练的模型进行预测,并展示预测结果和图像
# 从 test_dataset 中随机选择一张图片
random_index = random.randint(0, len(test_dataset) - 1)
sample_data = test_dataset[random_index]
sample_image, true_label = sample_data[1], sample_data[0]
# 将图片传递给模型进行预测
sample_image = sample_image.unsqueeze(0).to(device).float()
# 将模型的权重类型转换为与输入数据一致的类型
net = net.to(device).float()
with torch.no_grad():
model_output = net(sample_image)
# 获取预测结果
_, predicted_label = torch.max(model_output, 1)
class_labels = ['crop', 'weed']
predicted_label = int(predicted_label[0].item())
true_label = int(true_label.item())
# 打印真实标签和预测标签
print(f'\t\tTrue Label: {class_labels[true_label]}')
print(f'\t\tPredicted Label: {class_labels[predicted_label]}')
# 转换为 NumPy 数组
sample_image = sample_image.squeeze().cpu().detach().numpy()
# 显示图像
plt.imshow(sample_image)
plt.axis('off')
plt.show()
四、转移到 CPU 上
1、构造测试集
由于我的 GPU 与 CPU 不在同一设备,且所用测试集与 GPU 平台上不同,因此要重新构建测试集,然后再使用已经训练好的模型进行推理测试
测试集中包含了 50 个图像数据和其对应的包含标签的 txt 文件
测试集网盘链接:https://pan.baidu.com/s/1tDUpQJu6MVtA2Zh-P7uJHA?pwd=hp8z 提取码:hp8z
在 CPU 设备上用和 GPU 设备上相同的方式,预处理数据,并载入 data loader。
transformer = transforms.Compose([
transforms.ToTensor(),
transforms.ColorJitter(contrast=0.5), # 增强对比度
transforms.Normalize(mean=[0.5], std=[0.5]) # 归一化
])
# 构造文件名列表
with open(r'./test.txt','r') as f:
file_name_url=[i.split('.')[0] for i in f.readlines()]
image_test = []
for i in range(len(file_name_url)):
image = Image.open('./test/' + file_name_url[i] + '.jpeg')
tensor = transformer(image.convert('L')).type(torch.float16)
image_test.append(tensor)
transformerlab = transforms.Compose([
transforms.ToTensor()
])
lables_test = []
for i in range(len(file_name_url)):
txt = open('./test/' + file_name_url[i] + '.txt')
label = txt.readline()[0]
label = float(label)
tensor = torch.tensor(label, dtype=torch.float16) # 使用float16数据类型
lables_test.append(tensor)
test_datas_tensor = torch.stack(image_test)
test_labels_tensor = torch.stack(lables_test)
test_dataset = TensorDataset(test_labels_tensor, test_datas_tensor)
test_dataloader = DataLoader(test_dataset, batch_size=32, shuffle=True)
2、创建模型
这里将 GPU 上训练的模型保存到了 resnet50_model.pth 中,在 CPU 上进行加载。
# 设备选择
device = torch.device("cpu")
# 加载模型
net = torchvision.models.resnet50(pretrained=False)
net.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)
num_features = net.fc.in_features
net.fc = nn.Linear(num_features, 2)
# 加载之前保存的模型权重
net.load_state_dict(torch.load('./resnet50_model.pth', map_location=device))
net.to(device).eval() # 将模型移动到CPU并设置为评估模式
# 重新构建优化器
optimizer = optim.Adam(net.parameters(), lr=0.001, weight_decay=1e-4)
输出的网络信息:
3、推理测试
在 CPU 上直接使用模型对测试集进行推理,查看推理时间和 F1 分数。
import time
from sklearn.metrics import f1_score
# 推理测试集
start_time = time.time()
predictions = []
true_labels = []
with torch.no_grad():
for data in test_dataloader:
images, labels = data[1].to(device).float(), data[0].to(device).long()
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
predictions.extend(predicted.cpu().numpy())
true_labels.extend(labels.cpu().numpy())
inference_time = time.time() - start_time
# 计算F1分数
f1 = f1_score(true_labels, predictions)
# 打印结果
print(f'Testing time: {inference_time:.2f} seconds')
print(f'F1 Score on test set: {f1:.4f}')
运行结果:
4、OneAPI 组件的使用
使用 Intel Extension for PyTorch 进行模型优化
只需添加一行代码:
# 加载模型
net = torchvision.models.resnet50(pretrained=False)
net.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)
num_features = net.fc.in_features
net.fc = nn.Linear(num_features, 2)
# 加载之前保存的模型权重
net.load_state_dict(torch.load('./resnet50_model.pth', map_location=device))
net.to(device).eval() # 将模型移动到CPU并设置为评估模式
# 重新构建优化器
optimizer = optim.Adam(net.parameters(), lr=0.001, weight_decay=1e-4)
# 使用Intel Extension for PyTorch进行优化
net = ipex.optimize(model=net, dtype=torch.float32)
再次进行推理测试,运行结果:
可以发现预测时间缩短了一半,F1 分数几乎没有变化,确保了模型性能的稳定性
最后保存优化后的模型即可
由于优化后的模型预测时间已经很短且 F1 分数较高,所以不再使用 Intel Neural Compressor 量化模型,感兴趣的伙伴可以自行进行量化,参考博客:
五、总结
通过本次校企合作课程,我了解到了 Intel OneAPI 在深度学习中的应用。在使用 OneAPI 的优化组件以后,推理的时间大幅度下降,从原来的 9s 到 4s。并且 F1 分数的值一直稳定在 0.9583 左右,这是一个非常好的现象。证明了 OneAPI 优秀的模型压缩能力,在保证模型精确度和 F1 指数的基础上还能够缩小模型的规模。
可以说 OneAPI 完成了人工智能领域的一个巨大贡献,兼顾机器学习和深度学习,为开发者和应用者提供了极大的便利。最后感谢英特尔公司提供的云服务环境!
完整源代码后续有时间了再添加上来