基于OpenVINO的钢板表面缺陷检测

一、项目简介

本项目来源于Kaggle上一个钢材表面缺陷检测竞赛,这也是一个非常好的将深度学习应用于传统工业材料检测的案例。本项目将使用百度飞桨PaddleSeg(https://github.com/PaddlePaddle/PaddleSeg)深度学习算法套件进行模型训练,并通过OpenVINO实现在Intel平台上的部署和性能优化。

钢材是现代最重要的建筑材料之一。刚材结构建筑能够抵抗自然和人为磨损,这使得这种材料在世界各地随处可见。在所有钢材加工环节中,平板钢的生产工艺特别精细。从加热、轧制,再到干燥和切割,需要几台机器协同操作,其中一个重要环节就是利用高清摄像头捕获的图像对加工环节中的钢材进行缺陷自动检测。Kaggle上举办的这场钢材缺陷竞赛希望各位参赛者利用机器学习来改进钢板表面缺陷检测算法精度,提高钢铁生产自动化程度。


如需更多技术交流与合作,欢迎扫码加入群聊。



二、 数据集分析

这个竞赛主要目的是定位和分类钢板表面缺陷,属于语义分割问题。对应的数据集可以从Kaggle竞赛官网下载,也可以从ai studio进行下载。下载下来后包含两个图像文件夹和两个标注文件:

分别对应如下:

  • train_images:该文件夹中存储训练图像
  • test_images:该文件夹中存储测试图像
  • train.csv:该文件中存储训练图像的缺陷标注,有4类缺陷,ClassId = [1, 2, 3, 4]
  • sample_submission.csv:该文件是一个上传文件的样例,每个ImageID要有4行,每一行对应一类缺陷

对于竞赛项目来说,拿到数据后我们首先要做的就是先对数据集进行分析。下面我们按照文本和图像逐步来分析这个数据集。

解压数据集:

!unzip -oq data/data12731/severstal-steel-de -d /home/aistudio/severstal
!chmod -R 777 /home/aistudio/severstal
!unzip -oq /home/aistudio/severstal/test_images.zip -d /home/aistudio/severstal/test_images
!unzip -oq /home/aistudio/severstal/train_images.zip -d /home/aistudio/severstal/train_images
!chmod -R 777 /home/aistudio/severstal

2.1 读取和分析文本数据

假设下载下来的数据集放在名为severstal的文件夹中,首先读取csv文本数据

import pandas as pd
from collections import defaultdict

train_df = pd.read_csv("severstal/train.csv")
sample_df = pd.read_csv("severstal/sample_submission.csv")

初步查看下里面的数据

print(train_df.head())
print('\r\n')
print(sample_df.head())
   ImageId_ClassId                                      EncodedPixels
0  0002cc93b.jpg_1  29102 12 29346 24 29602 24 29858 24 30114 24 3...
1  0002cc93b.jpg_2                                                NaN
2  0002cc93b.jpg_3                                                NaN
3  0002cc93b.jpg_4                                                NaN
4  00031f466.jpg_1                                                NaN


   ImageId_ClassId EncodedPixels
0  004f40c73.jpg_1           1 1
1  004f40c73.jpg_2           1 1
2  004f40c73.jpg_3           1 1
3  004f40c73.jpg_4           1 1
4  006f39c41.jpg_1           1 1

结果如下:

   ImageId_ClassId                                      EncodedPixels
0  0002cc93b.jpg_1  29102 12 29346 24 29602 24 29858 24 30114 24 3...
1  0002cc93b.jpg_2                                                NaN
2  0002cc93b.jpg_3                                                NaN
3  0002cc93b.jpg_4                                                NaN
4  00031f466.jpg_1                                                NaN

   ImageId_ClassId EncodedPixels
0  004f40c73.jpg_1           1 1
1  004f40c73.jpg_2           1 1
2  004f40c73.jpg_3           1 1
3  004f40c73.jpg_4           1 1
4  006f39c41.jpg_1           1 1

注意数据的标注形式,一共2列:第1列:ImageId_ClassId(图片编号+类编号);第2例EncodedPixels(图像标签)。注意这个图像标签和我们平常遇到的不一样,平常的是一个mask灰度图像,里面有许多数字填充,背景为0,但是这里,为了缩小数据,它使用的是像素“列位置-长度”格式。举个例子:

我们把一个图像(h,w)flatten(注意不是按行而是按列),29102 12 29346 24 29602 24表示从29102像素位置开始的12长度均为非背景,后面以此类推。这就相当于在每个图像上画一条竖线。

接下来统计有无缺陷及每类缺陷的图像数量:

class_dict = defaultdict(int)
kind_class_dict = defaultdict(int)
no_defects_num = 0
defects_num = 0

for col in range(0, len(train_df), 4):
    img_names = [str(i).split("_")[0] for i in train_df.iloc[col:col+4, 0].values]
    if not (img_names[0] == img_names[1] == img_names[2] == img_names[3]):
        raise ValueError

    labels = train_df.iloc[col:col+4, 1]
    if labels.isna().all():
        no_defects_num += 1
    else:
        defects_num += 1

    kind_class_dict[sum(labels.isna().values == False)] += 1

    for idx, label in enumerate(labels.isna().values.tolist()):
        if label == False:
            class_dict[idx+1] += 1

输出有、无缺陷的图像数量

print("无缺陷钢板数量: {}".format(no_defects_num))
print("有缺陷钢板数量: {}".format(defects_num))
无缺陷钢板数量: 5902
有缺陷钢板数量: 6666

输出结果为:

无缺陷钢板数量: 5902
有缺陷钢板数量: 6666

接下来对有缺陷的图像进行分类统计:

import seaborn as sns
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
sns.barplot(x=list(class_dict.keys()), y=list(class_dict.values()), ax=ax)
ax.set_title("the number of images for each class")
ax.set_xlabel("class")
plt.show()
print(class_dict)

在这里插入图片描述

defaultdict(<class 'int'>, {1: 897, 3: 5150, 4: 801, 2: 247})

输出结果如下:

defaultdict(<class 'int'>, {1: 897, 3: 5150, 4: 801, 2: 247})

到这里得出的结论有两个:

(1)有缺陷和无缺陷的图像数量大致相当;

(2)缺陷的类别是不平衡的。

接下来统计一张图像中可能包含的缺陷种类数:

fig, ax = plt.subplots()
sns.barplot(x=list(kind_class_dict.keys()), y=list(kind_class_dict.values()), ax=ax)
ax.set_title("Number of classes included in each image");
ax.set_xlabel("number of classes in the image")
plt.show()
print(kind_class_dict)

在这里插入图片描述

defaultdict(<class 'int'>, {1: 6239, 0: 5902, 2: 425, 3: 2})

输出结果如下:

defaultdict(<class 'int'>, {1: 6239, 0: 5902, 2: 425, 3: 2})

这一步得到的结论是:
大多数图像没有缺陷或仅含一种缺陷,并且在训练集中没有1张图像会同时包含4中缺陷。

2.2 读取和分析图像数据

读取训练集图像数据

from collections import defaultdict
from pathlib import Path
from PIL import Image

train_size_dict = defaultdict(int)
train_path = Path("severstal/train_images/")
for img_name in train_path.iterdir():
    img = Image.open(img_name)
    train_size_dict[img.size] += 1

上面的代码一方面是检查训练集所有图像的尺寸是否一致,同时也检查每张图像格式是否正常,是否可以正常打开。

检查下训练集中图像的尺寸和数目

print(train_size_dict)
defaultdict(<class 'int'>, {(1600, 256): 12568})

输出结果:

defaultdict(<class 'int'>, {(1600, 256): 12568})

可以看到训练集中图像大小为1600乘以256大小,一共有12568张。所有图像尺寸是一样的。

接下来读取测试集图像数据:

test_size_dict = defaultdict(int)
test_path = Path("severstal/test_images/")

for img_name in test_path.iterdir():
    img = Image.open(img_name)
    test_size_dict[img.size] += 1

print(test_size_dict)
defaultdict(<class 'int'>, {(1600, 256): 1801})

输出结果:

defaultdict(<class 'int'>, {(1600, 256): 1801})

测试集中的图像也是1600乘以256,共1801张。

2.3 可视化数据集

首先读取csv文本数据

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pathlib import Path
import cv2

train_df = pd.read_csv("severstal/train.csv")

下面为了方便可视化,我们为不同的缺陷类别设置颜色显示:

palet = [(249, 192, 12), (0, 185, 241), (114, 0, 218), (249,50,12)]
fig, ax = plt.subplots(1, 4, figsize=(15, 5))
for i in range(4):
    ax[i].axis('off')
    ax[i].imshow(np.ones((50, 50, 3), dtype=np.uint8) * palet[i])
    ax[i].set_title("class color: {}".format(i+1))

fig.suptitle("each class colors")
plt.show()

在这里插入图片描述

输出如下:

接下来将不同的缺陷标识归类:

idx_no_defect = []
idx_class_1 = []
idx_class_2 = []
idx_class_3 = []
idx_class_4 = []
idx_class_multi = []
idx_class_triple = []

for col in range(0, len(train_df), 4):
    img_names = [str(i).split("_")[0] for i in train_df.iloc[col:col+4, 0].values]
    if not (img_names[0] == img_names[1] == img_names[2] == img_names[3]):
        raise ValueError

    labels = train_df.iloc[col:col+4, 1]
    if labels.isna().all():
        idx_no_defect.append(col)
    elif (labels.isna() == [False, True, True, True]).all():
        idx_class_1.append(col)
    elif (labels.isna() == [True, False, True, True]).all():
        idx_class_2.append(col)
    elif (labels.isna() == [True, True, False, True]).all():
        idx_class_3.append(col)
    elif (labels.isna() == [True, True, True, False]).all():
        idx_class_4.append(col)
    elif labels.isna().sum() == 1:
        idx_class_triple.append(col)
    else:
        idx_class_multi.append(col)

train_path = Path("severstal/train_images/")

接下来创建可视化标注函数:

def name_and_mask(start_idx):
    col = start_idx
    img_names = [str(i).split("_")[0] for i in train_df.iloc[col:col+4, 0].values]
    if not (img_names[0] == img_names[1] == img_names[2] == img_names[3]):
        raise ValueError

    labels = train_df.iloc[col:col+4, 1]
    mask = np.zeros((256, 1600, 4), dtype=np.uint8)

    for idx, label in enumerate(labels.values):
        if label is not np.nan:
            mask_label = np.zeros(1600*256, dtype=np.uint8)
            label = label.split(" ")
            positions = map(int, label[0::2])
            length = map(int, label[1::2])
            for pos, le in zip(positions, length):
                mask_label[pos-1:pos+le-1] = 1
            mask[:, :, idx] = mask_label.reshape(256, 1600, order='F')  #按列取值reshape

    return img_names[0], mask

def show_mask_image(col):
    name, mask = name_and_mask(col)
    img = cv2.imread(str(train_path / name))
    fig, ax = plt.subplots(figsize=(15, 15))
    for ch in range(4):
        contours, _ = cv2.findContours(mask[:, :, ch], cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
        for i in range(0, len(contours)):
            cv2.polylines(img, contours[i], True, palet[ch], 2)

    ax.set_title(name)
    ax.imshow(img)
    plt.show()

接下来先展示5张无缺陷图像:

for idx in idx_no_defect[:5]:
    show_mask_image(idx)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

输出结果如下:

仅含第1类缺陷的图像展示:

for idx in idx_class_1[:5]:
    show_mask_image(idx)

在这里插入图片描述

在这里插入图片描述

输出结果如下:

从图像上分析第1类缺陷主要是局部的白色噪点和黑色噪点混合区。

仅含第2类缺陷的图像展示:

for idx in idx_class_2[:5]:
    show_mask_image(idx)

在这里插入图片描述

在这里插入图片描述

输出结果如下:

从图像上分析第2类缺陷主要是竖状黑色划痕类

仅含第3类缺陷的图像展示:

for idx in idx_class_3[:5]:
    show_mask_image(idx)

在这里插入图片描述

在这里插入图片描述

输出结果如下:

从图像上分析第3类缺陷类型比较多样,这也可以从前面分析得到的第3类缺陷占比看出。总结来看,第3类缺陷主要是白色噪点和白色划痕的成片区域。

仅含第4类缺陷的图像展示:

for idx in idx_class_4[:5]:
    show_mask_image(idx)

在这里插入图片描述

在这里插入图片描述

输出结果如下:

从图像上分析第4类缺陷主要是大面积的黑色凸起。

最后再看下同时具有3种缺陷的图片:

for idx in idx_class_triple:
    show_mask_image(idx)

在这里插入图片描述

以上分析可以帮助我们快速掌握数据基本属性,初步了解项目难度。在实际比赛时这些数据分析是非常重要的,通过这些数据分析再挖掘整理成重要的“线索”,然后改进算法模型,往往可以取得出其不意的好效果。

三、制作标准数据集

3.1 PaddleSeg数据集格式说明

从前面的数据分析中我们看到钢板图像的每个像素只属于1种缺陷类别(或者没缺陷),由于我们需要定位出钢板缺陷的精细区域,因此可以把这个任务看作是一个语义分割任务,即按照像素级别精度判断每个像素所属的缺陷类别。

接下来我们就可以使用语义分割算法进行训练和验证。本项目采用百度飞桨PaddleSeg算法套件来快速训练。PaddleSeg是基于飞桨PaddlePaddle开发的端到端图像分割开发套件,涵盖了高精度和轻量级等不同方向的大量高质量分割模型。通过模块化的设计,提供了配置化驱动和API调用两种应用方式,帮助开发者更便捷地完成从训练到部署的全流程图像分割应用。

  • 特性如下:
    • 高精度模型:基于百度自研的半监督标签知识蒸馏方案(SSLD)训练得到高精度骨干网络,结合前沿的分割技术,提供了50+的高质量预训练模型,效果优于其他开源实现。
    • 模块化设计:支持15+主流 分割网络 ,结合模块化设计的 数据增强策略 、骨干网络、损失函数 等不同组件,开发者可以基于实际应用场景出发,组装多样化的训练配置,满足不同性能和精度的要求。
    • 高性能:支持多进程异步I/O、多卡并行训练、评估等加速策略,结合飞桨核心框架的显存优化功能,可大幅度减少分割模型的训练开销,让开发者更低成本、更高效地完成图像分割训练。

PaddleSeg安装方法参考官网

为了能够使用PaddleSeg进行语义分割训练,我们首先要对数据集格式进行转化。PaddleSeg语义分割算法要求数据集按照如下方式进行组织:

原图均放在同一目录,如JPEGImages,标注的同名png文件均放在同一目录,如Annotations。示例如下:

MyDataset/ # 语义分割数据集根目录
|--JPEGImages/ # 原图文件所在目录
|  |--1.jpg
|  |--2.jpg
|  |--...
|  |--...
|
|--Annotations/ # 标注文件所在目录
|  |--1.png
|  |--2.png
|  |--...
|  |--...

JPEGImages目录存放的是原文件图像(jpg格式),Annotations存放对应的标注文件图像(png格式) 。这里的标注图像,如1.png,为单通道图像,像素标注类别需要从0开始递增, 例如0, 1, 2, 3表示4种类别(一般0表示background背景),标注类别最多255个类别(其中像素值255不参与训练和评估)。

3.2 数据转换

数据转换本身并不难,在前面可视化数据的时候已经将缺陷区域显示了出来,因此,数据转化时只需要将缺陷对应的灰度值填入mask即可。

将下载下来的数据集解压后放置在当前项目根目录下,然后执行下面的代码即可:

!python prepare_dataset.py
全部完成

执行完后在当前目录下会生成PaddleSeg标准的数据集,名为steel。

3.3 数据切分

在开始数据切分之前,需要提前完成PaddleSeg安装工具套件下载:

# 或者在AIsutio上通过gitee进行下载
!git clone https://gitee.com/paddlepaddle/PaddleSeg.git
Cloning into 'PaddleSeg'...
remote: Enumerating objects: 18055, done.[K
remote: Counting objects: 100% (3018/3018), done.[K
remote: Compressing objects: 100% (1534/1534), done.[K
remote: Total 18055 (delta 1698), reused 2505 (delta 1435), pack-reused 15037[K
Receiving objects: 100% (18055/18055), 341.84 MiB | 21.84 MiB/s, done.
Resolving deltas: 100% (11562/11562), done.
Checking connectivity... done.

在模型进行训练时,我们需要划分训练集,验证集和测试集,可直接使用PaddleSeg命令将数据集随机划分。本项目将训练集、验证集和测试集按照8.5:1:0.5的比例划分。 PaddleSeg中提供了简单易用的API,方便用户直接使用进行数据划分,参考自定义数据集说明。进入PaddleSeg目录,执行以下命令:

!python PaddleSeg/tools/split_dataset_list.py steel JPEGImages Annotations --split 0.85 0.1 0.05 --format jpg png --label_class 0 1 2 3 4

其中steel为数据集根目录路径, 数据文件夹切分前后的状态如下:

  steel/                            steel/
  ├── Annotations/      -->         ├── Annotations/
  ├── JPEGImages/                   ├── JPEGImages/
                                    ├── labels.txt
                                    ├── testt.txt
                                    ├── train.txt
                                    ├── val.txt

执行上面命令行,会在steel下生成labels.txt, train.txt, val.txt, test.txt,分别存储训练样本标签,训练样本列表,验证样本列表,测试样本列表。

四、训练和验证

4.1 模型选择

在实际产业落地过程中对算法的要求是苛刻的。在保障高识别精度的情况下,往往会牺牲算法运行速度;反之追求速度,则会带来精度的大幅度损失。因此,产业及学术界翘楚都竭力探索能同时实现速度和精度平衡、在当前云、边、端多场景实现高效协同的优秀算法。而PP-LiteSeg凭着mIoU 72.0、273.6 FPS(Cityscapes数据集,1080ti)的超优秀性能,超越当前CVPR SOTA模型STDC,在众多优秀算法中脱颖而出,真正实现了精度和速度的最佳均衡。

PP-LiteSeg介绍

PP-LiteSeg提出三个创新模块:灵活的解码模块(FLD)、注意力融合模块(UAFM)、简易金字塔池化模块(SPPM)。FLD灵活调整解码模块中通道数,平衡编码模块和解码模块的计算量,使得整个模型更加高效;UAFM模块效地加强特征表示,更好地提升了模型的精度;SPPM模块减小了中间特征图的通道数、移除了跳跃连接,使得模型性能进一步提升。

更多关于PP-LiteSeg的内容,请参考:

https://github.com/PaddlePaddle/PaddleSeg/tree/release/2.5/configs/pp_liteseg

4.2 模型配置

在完成数据准备后,我要对模型训练的参数进行配置,对PaddleSeg/configs/pp_liteseg/pp_liteseg_stdc1_camvid_960x720_10k.yml进行如下修改

更换配置文件中type类型、并按照上文提示修改dataset_root和train_path所对应的路径、补充num_classes,以及模型类别等信息。

batch_size: 6
iters: 200000

train_dataset:
  type: Dataset
  dataset_root: steel
  train_path: steel/train.txt
  num_classes: 5
  transforms:
    - type: Resize
      target_size: [800, 128]
      interp: 'LINEAR'
    - type: RandomHorizontalFlip
    - type: Normalize
  mode: train

val_dataset:
  type: Dataset
  dataset_root: steel
  val_path: steel/val.txt
  num_classes: 5
  transforms:
    - type: Resize
      target_size: [800, 128]
      interp: 'LINEAR'
    - type: Normalize
  mode: val

optimizer:
  type: sgd
  momentum: 0.9
  weight_decay: 5.0e-4

lr_scheduler:
  type: PolynomialDecay
  learning_rate: 0.0025
  end_lr: 0
  power: 0.9
  warmup_iters: 200
  warmup_start_lr: 1.0e-5

loss:
  types:
    - type: OhemCrossEntropyLoss
      min_kept: 200000   
    - type: OhemCrossEntropyLoss
      min_kept: 200000
    - type: OhemCrossEntropyLoss
      min_kept: 200000
  coef: [1, 1, 1]

model:
  type: PPLiteSeg
  backbone:
    type: STDC1
    pretrained: https://bj.bcebos.com/paddleseg/dygraph/PP_STDCNet1.tar.gz
  arm_out_chs: [32, 64, 128]
  seg_head_inter_chs: [32, 64, 64]

4.3 模型训练

当准备好数据集以及配置文件后,我们可以通过PaddleSeg提供的脚本对模型进行训练。 请确保已经完成了PaddleSeg的安装工作,并且位于PaddleSeg目录下,执行以下脚本:

#!export CUDA_VISIBLE_DEVICES=0 # 设置1张可用的卡
# windows下请执行以下命令
# set CUDA_VISIBLE_DEVICES=0
!python3 -m paddle.distributed.launch PaddleSeg/train.py \
       --config PaddleSeg/configs/pp_liteseg/pp_liteseg_stdc1_camvid_960x720_10k.yml \
       --do_eval \
       --use_vdl \
       --save_interval 500 \
       --save_dir PaddleSeg/output
-----------  Configuration Arguments -----------
gpus: None
heter_worker_num: None
heter_workers: 
http_port: None
ips: 127.0.0.1
log_dir: log
nproc_per_node: None
run_mode: None
server_num: None
servers: 
training_script: PaddleSeg/train.py
training_script_args: ['--config', 'PaddleSeg/configs/pp_liteseg/pp_liteseg_stdc1_camvid_960x720_10k.yml', '--do_eval', '--use_vdl', '--save_interval', '500', '--save_dir', 'PaddleSeg/output']
worker_num: None
workers: 
------------------------------------------------
WARNING 2022-06-25 22:16:54,183 launch.py:359] Not found distinct arguments and compiled with cuda or xpu. Default use collective mode
launch train in GPU mode!
INFO 2022-06-25 22:16:54,185 launch_utils.py:510] Local start 1 processes. First process distributed environment info (Only For Debug): 
    +=======================================================================================+
    |                        Distributed Envs                      Value                    |
    +---------------------------------------------------------------------------------------+
    |                       PADDLE_TRAINER_ID                        0                      |
    |                 PADDLE_CURRENT_ENDPOINT                 127.0.0.1:33465               |
    |                     PADDLE_TRAINERS_NUM                        1                      |
    |                PADDLE_TRAINER_ENDPOINTS                 127.0.0.1:33465               |
    |                     PADDLE_RANK_IN_NODE                        0                      |
    |                 PADDLE_LOCAL_DEVICE_IDS                        0                      |
    |                 PADDLE_WORLD_DEVICE_IDS                        0                      |
    |                     FLAGS_selected_gpus                        0                      |
    |             FLAGS_selected_accelerators                        0                      |
    +=======================================================================================+

INFO 2022-06-25 22:16:54,185 launch_utils.py:514] details abouts PADDLE_TRAINER_ENDPOINTS can be found in log/endpoints.log, and detail running logs maybe found in log/workerlog.0
launch proc_id:251 idx:0
/home/aistudio/PaddleSeg/paddleseg/models/losses/rmi_loss.py:78: DeprecationWarning: invalid escape sequence \i
  """
2022-06-25 22:16:55 [INFO]	
------------Environment Information-------------
platform: Linux-4.15.0-140-generic-x86_64-with-debian-stretch-sid
Python: 3.7.4 (default, Aug 13 2019, 20:35:49) [GCC 7.3.0]
Paddle compiled with cuda: True
NVCC: Cuda compilation tools, release 10.1, V10.1.243
cudnn: 7.6
GPUs used: 1
CUDA_VISIBLE_DEVICES: None
GPU: ['GPU 0: Tesla V100-SXM2-32GB']
GCC: gcc (Ubuntu 7.5.0-3ubuntu1~16.04) 7.5.0
PaddleSeg: 2.5.0
PaddlePaddle: 2.1.2
OpenCV: 4.1.1
------------------------------------------------
2022-06-25 22:16:56 [INFO]	
---------------Config Information---------------
batch_size: 6
iters: 200000
loss:
  coef:
  - 1
  - 1
  - 1
  types:
  - ignore_index: 255
    min_kept: 200000
    type: OhemCrossEntropyLoss
  - ignore_index: 255
    min_kept: 200000
    type: OhemCrossEntropyLoss
  - ignore_index: 255
    min_kept: 200000
    type: OhemCrossEntropyLoss
lr_scheduler:
  end_lr: 0
  learning_rate: 0.0025
  power: 0.9
  type: PolynomialDecay
  warmup_iters: 200
  warmup_start_lr: 1.0e-05
model:
  arm_out_chs:
  - 32
  - 64
  - 128
  backbone:
    pretrained: https://bj.bcebos.com/paddleseg/dygraph/PP_STDCNet1.tar.gz
    type: STDC1
  seg_head_inter_chs:
  - 32
  - 64
  - 64
  type: PPLiteSeg
optimizer:
  momentum: 0.9
  type: sgd
  weight_decay: 0.0005
train_dataset:
  dataset_root: steel
  mode: train
  num_classes: 5
  train_path: steel/train.txt
  transforms:
  - interp: LINEAR
    target_size:
    - 800
    - 128
    type: Resize
  - type: RandomHorizontalFlip
  - type: Normalize
  type: Dataset
val_dataset:
  dataset_root: steel
  mode: val
  num_classes: 5
  transforms:
  - interp: LINEAR
    target_size:
    - 800
    - 128
    type: Resize
  - type: Normalize
  type: Dataset
  val_path: steel/val.txt
------------------------------------------------
W0625 22:16:56.168403   251 device_context.cc:404] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 11.2, Runtime API Version: 10.1
W0625 22:16:56.168457   251 device_context.cc:422] device: 0, cuDNN Version: 7.6.
2022-06-25 22:16:59 [INFO]	Loading pretrained model from https://bj.bcebos.com/paddleseg/dygraph/PP_STDCNet1.tar.gz
Connecting to https://bj.bcebos.com/paddleseg/dygraph/PP_STDCNet1.tar.gz
Downloading PP_STDCNet1.tar.gz

[                                                  ] 0.01%
[                                                  ] 0.52%
[                                                  ] 1.38%
[=                                                 ] 2.79%
[==                                                ] 5.08%
[===                                               ] 7.11%
[====                                              ] 9.13%
[=====                                             ] 11.46%
[======                                            ] 13.54%
[=======                                           ] 15.72%
[========                                          ] 17.76%
[=========                                         ] 19.84%
[===========                                       ] 22.24%
[============                                      ] 24.55%
[=============                                     ] 27.14%
[==============                                    ] 29.79%
[================                                  ] 32.55%
[=================                                 ] 35.30%
[===================                               ] 38.34%
[====================                              ] 41.25%
[======================                            ] 44.48%
[=======================                           ] 47.65%
[=========================                         ] 51.39%
[===========================                       ] 55.23%
[=============================                     ] 58.78%
[===============================                   ] 62.62%
[=================================                 ] 66.46%
[===================================               ] 70.57%
[=====================================             ] 74.97%
[=======================================           ] 79.42%
[=========================================         ] 83.61%
[===========================================       ] 87.99%
[==============================================    ] 92.31%
[================================================  ] 96.85%
[==================================================] 100.00%
Uncompress PP_STDCNet1.tar.gz

[                                                  ] 0.00%
[=========================                         ] 50.00%
[==================================================] 100.00%
2022-06-25 22:17:04 [INFO]	There are 295/295 variables loaded into STDCNet.
/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/fluid/dygraph/math_op_patch.py:239: UserWarning: The dtype of left and right variables are not the same, left dtype is paddle.float32, but right dtype is paddle.int64, the right dtype will convert to paddle.float32
  format(lhs_dtype, rhs_dtype, lhs_dtype))
/home/aistudio/PaddleSeg/paddleseg/models/losses/ohem_cross_entropy_loss.py:93: DeprecationWarning: [93m
Warning:
API "paddle.nn.functional.loss.softmax_with_cross_entropy" is deprecated since 2.0.0, and will be removed in future versions. Please use "paddle.nn.functional.cross_entropy" instead.
reason: Please notice that behavior of "paddle.nn.functional.softmax_with_cross_entropy" and "paddle.nn.functional.cross_entropy" is different. [0m
  logit, label, ignore_index=self.ignore_index, axis=1)
2022-06-25 22:17:05 [INFO]	[TRAIN] epoch: 1, iter: 10/200000, loss: 7.1089, lr: 0.000122, batch_cost: 0.1023, reader_cost: 0.02876, ips: 58.6568 samples/sec | ETA 05:40:56
2022-06-25 22:17:06 [INFO]	[TRAIN] epoch: 1, iter: 20/200000, loss: 6.6937, lr: 0.000247, batch_cost: 0.0836, reader_cost: 0.01534, ips: 71.7996 samples/sec | ETA 04:38:31
2022-06-25 22:17:07 [INFO]	[TRAIN] epoch: 1, iter: 30/200000, loss: 5.8359, lr: 0.000371, batch_cost: 0.0824, reader_cost: 0.01454, ips: 72.8191 samples/sec | ETA 04:34:36
2022-06-25 22:17:08 [INFO]	[TRAIN] epoch: 1, iter: 40/200000, loss: 5.0107, lr: 0.000496, batch_cost: 0.0840, reader_cost: 0.01549, ips: 71.4231 samples/sec | ETA 04:39:57
2022-06-25 22:17:09 [INFO]	[TRAIN] epoch: 1, iter: 50/200000, loss: 4.1553, lr: 0.000620, batch_cost: 0.1030, reader_cost: 0.03387, ips: 58.2529 samples/sec | ETA 05:43:14
2022-06-25 22:17:09 [INFO]	[TRAIN] epoch: 1, iter: 60/200000, loss: 3.4296, lr: 0.000745, batch_cost: 0.0830, reader_cost: 0.01505, ips: 72.2777 samples/sec | ETA 04:36:37
2022-06-25 22:17:10 [INFO]	[TRAIN] epoch: 1, iter: 70/200000, loss: 2.7271, lr: 0.000869, batch_cost: 0.0837, reader_cost: 0.01431, ips: 71.6455 samples/sec | ETA 04:39:03
2022-06-25 22:17:11 [INFO]	[TRAIN] epoch: 1, iter: 80/200000, loss: 2.2952, lr: 0.000994, batch_cost: 0.0818, reader_cost: 0.01262, ips: 73.3857 samples/sec | ETA 04:32:25
2022-06-25 22:17:12 [INFO]	[TRAIN] epoch: 1, iter: 90/200000, loss: 2.0616, lr: 0.001118, batch_cost: 0.0828, reader_cost: 0.01437, ips: 72.4981 samples/sec | ETA 04:35:44
2022-06-25 22:17:13 [INFO]	[TRAIN] epoch: 1, iter: 100/200000, loss: 2.0095, lr: 0.001243, batch_cost: 0.0825, reader_cost: 0.01409, ips: 72.7404 samples/sec | ETA 04:34:48
2022-06-25 22:17:14 [INFO]	[TRAIN] epoch: 1, iter: 110/200000, loss: 1.9415, lr: 0.001367, batch_cost: 0.0999, reader_cost: 0.03201, ips: 60.0443 samples/sec | ETA 05:32:54
2022-06-25 22:17:15 [INFO]	[TRAIN] epoch: 1, iter: 120/200000, loss: 1.5710, lr: 0.001492, batch_cost: 0.0816, reader_cost: 0.01405, ips: 73.5683 samples/sec | ETA 04:31:41
2022-06-25 22:17:15 [INFO]	[TRAIN] epoch: 1, iter: 130/200000, loss: 1.5168, lr: 0.001616, batch_cost: 0.0834, reader_cost: 0.01501, ips: 71.9087 samples/sec | ETA 04:37:56
2022-06-25 22:17:16 [INFO]	[TRAIN] epoch: 1, iter: 140/200000, loss: 1.4862, lr: 0.001741, batch_cost: 0.0843, reader_cost: 0.01181, ips: 71.1564 samples/sec | ETA 04:40:52
2022-06-25 22:17:17 [INFO]	[TRAIN] epoch: 1, iter: 150/200000, loss: 1.5042, lr: 0.001865, batch_cost: 0.0820, reader_cost: 0.01134, ips: 73.1428 samples/sec | ETA 04:33:13
^C
Traceback (most recent call last):
  File "/opt/conda/envs/python35-paddle120-env/lib/python3.7/runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "/opt/conda/envs/python35-paddle120-env/lib/python3.7/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/distributed/launch.py", line 16, in <module>
    launch.launch()
  File "/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/distributed/fleet/launch.py", line 371, in launch
    launch_collective(args)
  File "/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/distributed/fleet/launch.py", line 272, in launch_collective
    time.sleep(3)
KeyboardInterrupt

4.4 恢复训练:

!python -m paddle.distributed.launch PaddleSeg/train.py \
       --config PaddleSeg/configs/pp_liteseg/pp_liteseg_stdc1_camvid_960x720_10k.yml \
       --resume_model PaddleSeg/output/iter_500 \
       --do_eval \
       --use_vdl \
       --save_interval 500 \
       --save_dir PaddleSeg/output

4.5 训练可视化

PaddleSeg会将训练过程中的数据写入VisualDL文件,并实时的查看训练过程中的日志,记录的数据包括:

  1. loss变化趋势
  2. 学习率变化趋势
  3. 训练时间
  4. 数据读取时间
  5. mean IoU变化趋势(当打开了do_eval开关后生效)
  6. mean pixel Accuracy变化趋势(当打开了do_eval开关后生效)

使用如下命令启动VisualDL查看日志

# 下述命令会在127.0.0.1上启动一个服务,支持通过前端web页面查看,可以通过--host这个参数指定实际ip地址
!visualdl --logdir PaddleSeg/output/

在浏览器输入提示的网址,效果如下:

在浏览器输入提示的网址,效果如下:

4.6 训练结果

以下为验证集上的实际表现(忘记截图了,用照片代替吧 lol)

相较同样主打轻量化的Unet,PP-LiteSeg网络无论在推理精度或是性能都有显著提升(实测CPU上可以实现23+FPS)

archepochresolutionBatch sizeLearning rateAugmentMiou
PP-LiteSeg100128x80060.0025RandomHorizontalFlip60.11%
Unet100128x80060.0025RandomHorizontalFlip54.65%

五、基于OpenVINO进行模型部署

5.1 模型导出

模型训练后保存在output文件夹,如果要使用OpenVINO进行部署需要导出成静态图的模型,运行如下命令,会自动在output文件夹下创建一个inference_model的文件夹,用来存放导出后的模型。

!python PaddleSeg/export.py \
       --config PaddleSeg/configs/pp_liteseg/pp_liteseg_stdc1_camvid_960x720_10k.yml \
       --model_path PaddleSeg/output/iter_26000/model.pdparams \
       --save_dir PaddleSeg/output

如果您不想自己重新训练一个模型,我们也在本项目的model目录里为大家准备好了预训练的Paddle静态格式模型,供本次部署验证

5.2 OpenVINO安装

OpenVINO是是Intel在2018发布的深度学习推理框架,可以提升神经网络模型在Intel硬件平台下的推理性能。近期OpenVINO的最新版本2022.1已经发布,作为版本迭代至今最大的一次升级,今天我们就来可看看新版OpenVINO可以为Paddle模型带来哪些部署和性能优化方案。

OpenVINO的Python runtime和工具包可以通过pipy一键完成安装。OpenVINO的其他安装方式可以参考

pip install openvino-dev

目前aistudio还无法支持OpenVINO 2022.1版本的直接安装,需要提前将相关推理代码迁移到本地后进行运行。

5.3 OpenVINO部署

该实例代码将演示如何在通过OpenVINO完成Paddle模型在Intel平台上部署。使用训练好的UNET模型对测试用的钢板图片进行批量检测,实现表面缺陷的识别。以下是本次演示中新版OpenVINO的特性介绍:

  • 直接支持PaddlePaddle模型:目前OpenVINO 2022.1发行版中已完成对PaddlePaddle模型的直接支持,作为国内最受欢迎的深度学习框架之一,之前OpenVINO在对Paddle模型做适配的时候,需要将Paddle模型转化为ONNX格式,再通过MO工具对ONNX模型进行优化和加速部署。现在MO工具已经可以直接完成对Paddle模型的离线转化,同事runtime api接口也可以直接读取加载Paddle模型到指定的硬件设备,省去了离线转换的过程,大大提升了Paddle开发者在Intel平台上部署的效率。经过性能和准确性验证,在OpenVINO™ 2022.1发行版中,会有 13个模型涵盖5大应用场景的Paddle模型将被直接支持,其中不乏像PPYolo和PPOCR这样非常受开发者欢迎的网络。

  • Auto Device plugin:Intel CPU平台上往往都会配备一个iGPU, 以及VPU这样的外置协处理器,如何充分发挥Intel平台下不同算力单元的能力,成为OpenVINO开发者们必修的功课之一。在2022.1版本之前,小伙伴们必须自己手动选择相应的部署设备,并配置设备中的相关资源。目前在新版本中,当开发者将定义的设备设置为“Auto”后,该接口会搜索系统中所有的算力单元以及其规格属性,根据模型的精度、网络结构以及用户指定性能要求等信息,选取并配置合适的算力单元进行部署,降低不同设备平台间的移植难度与部署难度
    除了自动选择部署硬件以外,Auto Device plugin还将进一步优化GPU/VPU的第一次推理延迟。由于CPU在延迟上有着先天的优势,AUTO Device plugin的策略是默认将第一次推理任务加载到CPU上运行,同时在GPU/VPU等性能更强的协处理器上编译加载执行网络,待CPU完成第一次推理请求后,再将任务无感地迁移到GPU/VPU等硬件设备中完成后续的推理任务。

  • PERFORMANCE_HINT:由于不同的硬件平台间存在配置和性能差异,想要让应用程序能够全面地适配不同的硬件平台,就需要事先根据硬件特性为你的推理任务配置不同的参数,例如绑定线程数,streams,推理请求数,batch size等,这个过程包含大量的验证和调试工作,非常耗时耗力。为了让开发者尽可能用一套代码来适配不同的Intel硬件平台。OpenVINO 2022.1发行版中引入了全新的PERFORMANCE_HINT功能,用户只需指定他的推理任务需求:延迟优先还是推理优先。compile_model便可以自动进行相应的硬件参数配置,达到相对较优的性能目标。此外针对GPU这样并行计算能力较强的设备,新的Auto Batching功能还可以通过将PERFORMANCE_HINT指定为吞吐量模式,来隐式地开启该功能,并自动配置batch size大小,充分激活GPU硬件性能,优化内存资源占用。

OpenVINO runtime api的调用流程可以参考以下链接及示意图:


import os
import cv2
from openvino.runtime import Core, AsyncInferQueue
import numpy as np
import time

# 获取测试文件名
test_path = 'steel/test.txt'
f = open(test_path)
lines = f.readlines()
f.close()

test_image = []
image_path = []
queue = 64
total_time = 0
# 批量完成测试图片的预处理任务
for i in range(queue):
    imgname = os.path.basename(lines[i].split(' ')[0])
    image_path.append(os.path.join('steel/JPEGImages', imgname))
    img = cv2.imread(image_path[i])
    input_image = np.expand_dims(img.transpose(2, 0, 1) / 255, 0)
    img_mean = np.array([0.5, 0.5, 0.5]).reshape((3, 1, 1))
    img_std = np.array([0.5, 0.5, 0.5]).reshape((3, 1, 1))
    input_image -= img_mean
    input_image /= img_std
    test_image.append(input_image)

def get_color_map_list(num_classes):
    color_map = num_classes * [0, 0, 0]
    for i in range(0, num_classes):
        j = 0
        lab = i
        while lab:
            color_map[i * 3] |= (((lab >> 0) & 1) << (7 - j))
            color_map[i * 3 + 1] |= (((lab >> 1) & 1) << (7 - j))
            color_map[i * 3 + 2] |= (((lab >> 2) & 1) << (7 - j))
            j += 1
            lab >>= 3
    color_map = [color_map[i:i + 3] for i in range(0, len(color_map), 3)]
    return color_map

def visualize_segmentation(image,
                           result,
                           weight=0.6,
                           save_dir='./',
                           color=None):
    label_map = result['label_map'].astype("uint8")
    color_map = get_color_map_list(256)
    if color is not None:
        for i in range(len(color) // 3):
            color_map[i] = color[i * 3:(i + 1) * 3]
    color_map = np.array(color_map).astype("uint8")

    # Use OpenCV LUT for color mapping
    c1 = cv2.LUT(label_map, color_map[:, 0])
    c2 = cv2.LUT(label_map, color_map[:, 1])
    c3 = cv2.LUT(label_map, color_map[:, 2])
    pseudo_img = np.dstack((c1, c2, c3))

    if isinstance(image, np.ndarray):
        im = image
        image_name = str(int(time.time() * 1000)) + '.jpg'
        if image.shape[2] != 3:
            print(
                "The image is not 3-channel array, so predicted label map is shown as a pseudo color image."
            )
            weight = 0.
    else:
        image_name = os.path.split(image)[-1]
        im = cv2.imread(image)

    if abs(weight) < 1e-5:
        vis_result = pseudo_img
    else:
        vis_result = cv2.addWeighted(im, weight,
                                     pseudo_img.astype(im.dtype), 1 - weight,
                                     0)

    if save_dir is not None:
        if not os.path.exists(save_dir):
            os.makedirs(save_dir)
        out_path = os.path.join(save_dir, 'visualize_{}'.format(image_name))
        cv2.imwrite(out_path, vis_result)
        print('The visualized result is saved as {}'.format(out_path))
    else:
        return vis_result

# 定义异步队列函数AsyncInferQueue的回调函数,实现每次推理后的数据可视化展示和存储
def callback(infer_request, i) -> None:
    global total_time
    #从每一个队列中每一个infer_request中获取结果数据
    res= np.squeeze(infer_request.get_output_tensor(0).data, 0)
    result={"label_map": res}
    visualize_segmentation(image_path[i], result, weight=0.4, save_dir='output/predict')
    latency = infer_request.latency
    print("inference latency: {:.2f} ms".format(latency))
    total_time = total_time + latency

# 初始化Inference Engine对象
ie = Core()       
# 通过Inference Engine直接读取pdmodel格式模型
model = ie.read_model(model="model/model.pdmodel")
input_layer = model.input(0)

# 固化网络的input shape以提升推理性能
model.reshape({input_layer.any_name: [1, 3, 256, 1600]})

# 编译模型,使用在“AUTO”自动选择部署的硬件设备(CPU, iGPU, VPU),并使用Performance hint功能自动对硬件平台进行配置
compiled_model = ie.compile_model(model=model, device_name="AUTO:CPU", config={"PERFORMANCE_HINT": "THROUGHPUT"})
#compiled_model = ie.compile_model(model=model, device_name="AUTO:CPU", config={"PERFORMANCE_HINT": "LATENCY"})

# 定义异步队列
infer_queue = AsyncInferQueue(compiled_model)
infer_queue.set_callback(callback)
start = time.time()
for i in range(queue):
    # 开始推理任务
    infer_queue.start_async({input_layer.any_name: test_image[i]}, i)
infer_queue.wait_all()
end = time.time()
# Calculate the average FPS
fps = queue / (end - start)
print("throughput: {:.2f} fps".format(fps))
back)
start = time.time()
for i in range(queue):
    # 开始推理任务
    infer_queue.start_async({input_layer.any_name: test_image[i]}, i)
infer_queue.wait_all()
end = time.time()
# Calculate the average FPS
fps = queue / (end - start)
print("throughput: {:.2f} fps".format(fps))
print("average latency: {:.2f} ms".format(total_time / queue))

5.4 实现效果

实测在开启Throguhput模式,并且仅用CPU推理的情况下,Intel 11代i7可以达到23+FPS的性能表现。




更多基于OpenVINO的示例代码可以查阅:OpenVINO notebook

此文章为搬运
原项目链接

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐