导盲赛事第二弹: 数据集与训练策略

来源:游家吧
欢视tv最新版app v1.0.1

/ 36.7m

查看

本文详细探讨了智能导盲机器狗比赛的数据集与训练策略。首先,我们介绍了train和val数据融合的方法及代码实现,并分享了伪标签训练(半监督学习)的基础、进阶以及创新应用方式。接着,在训练策略部分,针对PaddleDetection系统,提供了显卡数量选择、batchsize大小及优化器选用的建议,并展示了含droppath的ConvNeXt代码实例。

0、简介

一只导盲犬能够给盲人带来许多生活上的便利,但是导盲犬的培训周期长,费用高昂,因此,不是所有盲人能够拥有导盲犬,如果有机器狗代替导盲犬,将极大的造福盲人,此项比赛为智能导盲机器狗比赛,通过比赛来考评智能导盲机器狗的智能感知能力及综合运动性能,要求智能四足仿生机器人沿布置好的城市人行道场景走完全程并完成指定任务。

实际上,高大上是指当我们在面对具体的任务时会发现,它实际上就是一个非常简单的目标检测问题。这种任务通常涉及到识别图片或视频中特定的物体类型,并进行分类或者计数。现在赛事组为我们提供了一种解决方案,需要我们去识别五种不同的目标。为了适应现实场景的需求,这些模型不仅在大小上有所限制(要求不超过),而且在处理速度方面也有着严格的规范。例如,检测的速度需要达到每秒至少,以确保实时性。通过这样的设计,赛事组旨在提供一个既实用又高效的解决方案,同时也能促进我们学习和理解目标检测领域中的各种技术和挑战。

目前赛事即将接近尾声,大家肯定已经确定了自己的参赛路线。同时,网络优化也进入了最后阶段。接下来,我们需要在此期间探索如何提升数据集和训练策略的表现。现在正是时候,让我们在这两个领域上进行深入研究和改进。

上集指路:导盲赛道思路分享

1、数据集

1.1 train val 数据融合

在某些比赛或项目中,主办方会直接提供大量数据给你,要求你在没有预先训练模型的情况下自行划分train和val两个子集进行初步的训练与验证。这种情况下,选手通常首先会依据这些提供的数据来构建train和val的数据分布。他们可能会采用一种逐步优化网络结构的方法:先在特定的框架下训练一个基础的模型,然后根据val集的表现不断微调参数直到val分数达到最高。当这个过程结束时,网络架构已经基本确定,选手通常会选择在两者之间的交叉点,即将train和val数据结合起来再次进行训练,以最终优化表现。然而,在本赛事中,比赛组织者已经为你准备好了train和val的数据集,并且使用了COCO(Common Objects in Context)这一国际知名的计算机视觉数据集格式。这样处理的好处在于,你无需再从头开始划分数据分布,从而提高了效率并节省了时间和资源。同时,通过已有的coco数据集格式,选手能够快速、准确地确定网络架构,并且可以立即开始模型的训练和验证过程。然而,对于那些在数据分析方面能力不足的选手来说,将train和val合并可能会遇到一些问题,尤其是数据处理不够熟练时,可能会影响到训练效果。但庆幸的是,我们已经为你解决了这个问题,确保了你能够顺利地进行比赛准备并提交最佳结果。

你只需要运行下面的代码就可以得到一个包含train与val所有数据的文件夹了 In []

## 解压文件夹!tar -zxvf data/data137625/WisdomGuide.tar.gz## 安装所需第三方库!pip install lxml !pip install pycocotools#coco2vocfrom pycocotools.coco import COCO import os, cv2, shutilfrom lxml import etree, objectifyfrom tqdm import tqdmfrom PIL import Imageimport numpy as npimport timeimport jsondef cover_copy(src,dst): ''' src和dst都必须是文件,该函数是执行覆盖操作 ''' if os.path.exists(dst): os.remove(dst) shutil.copy(src,dst) else: shutil.copy(src,dst)def coco2voc(basedir='VOCdevkit/COCO_VOC',sourcedir='WisdomGuide'): """ basedir:用来存放转换后数据和标注文件 sourcedir:用来指定原始COCO数据集的存放位置 """ img_savepath= os.path.join(basedir,'JPEGImages') ann_savepath=os.path.join(basedir,'Annotations') main_path = os.path.join(basedir,"annotations") for p in [basedir,img_savepath,ann_savepath,main_path]: if os.path.exists(p): shutil.rmtree(p) os.makedirs(p) else: os.makedirs(p) datasets = ['train','val'] # datasets = ['val2017'] for dataset in datasets: start = time.time() print(f"start {dataset}") no_ann=[] #用来存放没有标注数据的图片的id,并将这些图片复制到results文件夹中 not_rgb=[] #是灰度图,同样将其保存 annfile = 'instance_{}.json'.format(dataset) annpath=os.path.join(sourcedir,'annotations',annfile) print('loading annotations into memory...') tic = time.time() with open(annpath, 'r') as f: dataset_ann = json.load(f) assert type( dataset_ann ) == dict, 'annotation file format {} not supported'.format( type(dataset)) print('Done (t={:0.2f}s)'.format(time.time() - tic)) coco = COCO(annpath) classes = dict() for cat in coco.dataset['categories']: classes[cat['id']] = cat['name'] imgIds = coco.getImgIds() # imgIds=imgIds[0:1000]#测试用,抽取10张图片,看下存储效果 for imgId in tqdm(imgIds): img = coco.loadImgs(imgId)[0] filename = img['file_name'] filepath=os.path.join(sourcedir,dataset,filename) annIds = coco.getAnnIds(imgIds=img['id'], iscrowd=None) anns = coco.loadAnns(annIds) if not len(anns): # print(f"{dataset}:{imgId}该文件没有标注信息,将其复制到{dataset}_noann_result中,以使查看") no_ann.append(imgId) result_path = os.path.join(sourcedir,dataset+"_noann_result") dest_path = os.path.join(result_path,filename) if not os.path.exists(result_path): os.makedirs(result_path) cover_copy(filepath,dest_path) continue #如果没有标注信息,则把没有标注信息的图片移动到相关结果文件 noann_result中,来进行查看 ,然后返回做下一张图 #有标注信息,接着往下走,获取标注信息 objs = [] for ann in anns: name = classes[ann['category_id']] if 'bbox' in ann: # print('bbox in ann',imgId) bbox = ann['bbox'] xmin = (int)(bbox[0]) ymin = (int)(bbox[1]) xmax = (int)(bbox[2] + bbox[0]) ymax = (int)(bbox[3] + bbox[1]) obj = [name, 1.0, xmin, ymin, xmax, ymax] #标错框在这里 if not(xmin-xmax==0 or ymin-ymax==0): objs.append(obj) else: print(f"{dataset}:{imgId}bbox在标注文件中不存在")# 单张图有多个标注框,某个类别没有框 annopath = os.path.join(ann_savepath,filename[:-3] + "xml") #生成的xml文件保存路径 dst_path = os.path.join(img_savepath,filename) im = Image.open(filepath) image = np.array(im).astype(np.uint8) if im.mode != "RGB": # if img.shape[-1] != 3: # print(f"{dataset}:{imgId}该文件非rgb图,其复制到{dataset}_notrgb_result中,以使查看") # print(f"img.shape{image.shape} and img.mode{im.mode}") not_rgb.append(imgId) result_path = os.path.join(sourcedir,dataset+"_notrgb_result") dest_path = os.path.join(result_path,filename) if not os.path.exists(result_path): os.makedirs(result_path) cover_copy(filepath,dest_path) #复制到notrgb_result来方便查看 im=im.convert('RGB') image = np.array(im).astype(np.uint8) im.save(dst_path,quality=95)#图片经过转换后,放到我们需要的位置片 im.close() else: cover_copy(filepath, dst_path)#把原始图像复制到目标文件夹 E = objectify.ElementMaker(annotate=False) anno_tree = E.annotation( E.folder('VOC'), E.filename(filename), E.source( E.database('COCO'), E.annotation('VOC'), E.image('COCO') ), E.size( E.width(image.shape[1]), E.height(image.shape[0]), E.depth(image.shape[2]) ), E.segmented(0) ) for obj in objs: E2 = objectify.ElementMaker(annotate=False) anno_tree2 = E2.object( E.name(obj[0]), E.pose(), E.truncated("0"), E.difficult(0), E.bndbox( E.xmin(obj[2]), E.ymin(obj[3]), E.xmax(obj[4]), E.ymax(obj[5]) ) ) anno_tree.append(anno_tree2) etree.ElementTree(anno_tree).write(annopath, pretty_print=True) print(f"{dataset}该数据集有{len(no_ann)}/{len(imgIds)}张图片没有instance标注信息,已经这些图片复制到{dataset}_noann_result中以使进行查看") print(f"{dataset}该数据集有{len(not_rgb)}/{len(imgIds)}张图片是非RGB图像,已经这些图片复制到{dataset}_notrgb_result中以使进行查看") duriation = time.time()-start print(f"数据集{dataset}处理完成用时{round(duriation/60,2)}分")#run coco2voccoco2voc()#voc2coco!cd voc2coco && python voc2coco.py ../VOCdevkit/COCO_VOC/Annotations ../VOCdevkit/COCO_VOC/COCO.json !mkdir COCO_all !mv VOCdevkit/COCO_VOC/JPEGImages COCO_all/ !mv VOCdevkit/COCO_VOC/COCO.json COCO_all/登录后复制

1.2 伪标签训练

一些比赛不会提供像导盲赛那样让你提交测试集的网络结果,而是直接给你照片供你预测。这种情况下,你就可以使用伪标签来提升模型效果了。简单来说,你可以将模型生成的结果作为标注信息和原数据一起训练,这样通常可以增加左右的效果。但是有的赛事并不允许使用这样的方法。幸运的是,这个比赛并没有提供测试照片,所以我不用担心这个问题。

当然,我相信有不少实力雄厚的学校早已积累了大量相关数据。但很遗憾的是,其中很大一部分并没有经过细致标注,每个样本都需要人为注释确实有些不现实。在这种情况下,你可以尝试利用“伪标签”进行训练,这种做法在机器学习领域也被称为半监督学习。

这里引用知乎的文章简单介绍一下半监督学习的方式

来源伪标签(Pseudo-Labelling)锋利的匕首

入门版: 利用标记数据训练有监督的模型 M。 对于无标签数据,使用有监督模型 M 进行预测,并计算其概率 P。 选取高置信度的样本进行进一步处理。 结合有标记数据和伪标签数据,重新训练一个新模型 M'。

升级版 利用标注数据训练监督性模型M 应用监督性模型M对无标注信息进行预测,产生预测概率P 依据此预测概率筛选出高可信样本 同时结合已有标记数据及伪标签数据重新训练新模型M' 将原先的M替换为M',再重复上述步骤直至效果未见改善

创新版 利用标注数据训练监督学习模型M 采用有监督模型M对无标签数据进行预测,生成预测概率P 将模型的损失函数调整为:Loss = loss(labeled_data) + α*loss(unlabeled_data) 使用标注数据和伪标签数据共同训练新模型M’登录后复制

2、 训练策略

训练策略:PaddleDetection的优化指南相信大多数人使用的都是PaddleDetection框架。然而,在实际应用中,训练策略的确需要一些注意事项。首先,我们需要了解在配置文件中卡数的影响,以及调整学习率和batchsize的策略。# 卡数对训练结果的影响 首先,看看你的设备有多少个GPU(显卡)?如果只有显卡,建议将学习率设置为原来的同时将batsize也相应地减小。这是因为在多个GPU配置下,网络处理和计算效率大幅提升,从而能够更快地完成训练。# 网络数据量的影响 现在来看一下你正在训练的COCO数据集的情况。通常情况下,使用v卡起步是合理的。因为这是一个非常庞大的数据集,而显卡配置为v话,每张卡的batchsize可以设置为这样每个epoch下的迭代次数就只有了。# 优化器的选择 在PaddleDetection中,目前两个主流优化器是Momentum和AdamW。Momentum适用于普通的卷积网络,而AdamW则更适合那些训练困难的网络或者你有特定目标且寻求稳定性的场景。如果你相信奇迹并希望取得最佳效果,那么Momentum是一个不错的选择;如果你更倾向于稳妥性和稳定性,可以选择使用AdamW。# 梯度裁剪 为了防止过拟合(overfitting),在训练策略中经常采用一些措施,比如droppath等。然而,这些方法需要在网络代码中实现相应的逻辑。在PaddleDetection的配置文件中,通常会使用L式来规范Momentum优化器。如果你选择了AdamW,那么就会使用clip_grad_by_norm来进行梯度裁剪。# 总结 卡数:对于训练COCO数据集,建议v步。 学习率和batchsize:如果只有一块显卡,将学习率设置为原来的并相应地调整batsize。 优化器选择:Momentum适用于卷积网络,AdamW则更适合训练困难的网络或需要稳定性的场景。如果你是一个追求奇迹或者有特定目标的人,Momentum可能更适合;否则,AdamW是更好的选择。 梯度裁剪:在PaddleDetection中通常使用L式规范Momentum优化器,在使用AdamW时会使用clip_grad_by_norm进行梯度裁剪。总之,通过合理的训练策略和配置文件设置,可以有效提升模型的训练效率和结果质量。

下面我就把我的一个训练策略开放给大家做一个参考

epoch: 400LearningRate: base_lr: 0.00025 schedulers: - !CosineDecay max_epochs: 500 - !LinearWarmup start_factor: 0. epochs: 5OptimizerBuilder: clip_grad_by_norm: 0.1 regularizer: false optimizer: type: AdamW weight_decay: 0.0001登录后复制

2.1 droppath实例 使用ConvNeXt进行droppath操作

In []

# copyright (c) 2022 PaddlePaddle Authors. All Rights Reserve.## Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at## http://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.# Code was based on https://github.com/facebookresearch/ConvNeXt# import math# from numbers import Integral# import paddle# import paddle.nn as nn# import paddle.nn.functional as F# from ppdet.core.workspace import register, serializable# from paddle.regularizer import L2Decay# from paddle.nn.initializer import Uniform# from paddle import ParamAttr# from paddle.nn.initializer import Constant# from paddle.vision.ops import DeformConv2D# from .name_adapter import NameAdapter# from ppdet.modeling.shape_spec import ShapeSpecimport paddleimport paddle.nn as nnimport paddle.nn.functional as F# __all__ = ['ConvNeXt']trunc_normal_ = nn.initializer.TruncatedNormal(std=0.02) zeros_ = nn.initializer.Constant(value=0.0) ones_ = nn.initializer.Constant(value=1.0)class Identity(nn.Layer): def __init__(self): super().__init__() def forward(self, x): return xdef drop_path(x, drop_prob=0.0, training=False): if drop_prob == 0.0 or not training: return x keep_prob = paddle.to_tensor(1 - drop_prob) shape = (paddle.shape(x)[0], ) + (1, ) * (x.ndim - 1) random_tensor = keep_prob + paddle.rand(shape, dtype=x.dtype) random_tensor = paddle.floor(random_tensor) # binarize output = x.divide(keep_prob) * random_tensor return outputclass DropPath(nn.Layer): def __init__(self, drop_prob=None): super(DropPath, self).__init__() self.drop_prob = drop_prob def forward(self, x): return drop_path(x, self.drop_prob, self.training)class Block(nn.Layer): """ ConvNeXt Block. There are two equivalent implementations: (1) DwConv -> LayerNorm (channels_first) -> 1x1 Conv -> GELU -> 1x1 Conv; all in (N, C, H, W) (2) DwConv -> Permute to (N, H, W, C); LayerNorm (channels_last) -> Linear -> GELU -> Linear; Permute back Args: dim (int): Number of input channels. drop_path (float): Stochastic depth rate. Default: 0.0 layer_scale_init_value (float): Init value for Layer Scale. Default: 1e-6. """ def __init__(self, dim, drop_path=0., layer_scale_init_value=1e-6): super().__init__() self.dwconv = nn.Conv2D(dim, dim, kernel_size=7, padding=3, groups=dim) # depthwise conv self.norm = LayerNorm(dim, epsilon=1e-6) self.pwconv1 = nn.Linear( dim, 4 * dim) # pointwise/1x1 convs, implemented with linear layers self.act = nn.GELU() self.pwconv2 = nn.Linear(4 * dim, dim) self.gamma = paddle.create_parameter( shape=[dim], dtype='float32', default_initializer=nn.initializer.Constant( value=layer_scale_init_value) ) if layer_scale_init_value > 0 else None self.drop_path = DropPath(drop_path) if drop_path > 0. else Identity() def forward(self, x): input = x x = self.dwconv(x) x = x.transpose([0, 2, 3, 1]) # (N, C, H, W) -> (N, H, W, C) x = self.norm(x) x = self.pwconv1(x) x = self.act(x) x = self.pwconv2(x) if self.gamma is not None: x = self.gamma * x x = x.transpose([0, 3, 1, 2]) # (N, H, W, C) -> (N, C, H, W) x = input + self.drop_path(x) return xclass LayerNorm(nn.Layer): """ LayerNorm that supports two data formats: channels_last (default) or channels_first. The ordering of the dimensions in the inputs. channels_last corresponds to inputs with shape (batch_size, height, width, channels) while channels_first corresponds to inputs with shape (batch_size, channels, height, width). """ def __init__(self, normalized_shape, epsilon=1e-6, data_format="channels_last"): super().__init__() self.weight = paddle.create_parameter(shape=[normalized_shape], dtype='float32', default_initializer=ones_) self.bias = paddle.create_parameter(shape=[normalized_shape], dtype='float32', default_initializer=zeros_) self.epsilon = epsilon self.data_format = data_format if self.data_format not in ["channels_last", "channels_first"]: raise NotImplementedError self.normalized_shape = (normalized_shape, ) def forward(self, x): if self.data_format == "channels_last": return F.layer_norm(x, self.normalized_shape, self.weight, self.bias, self.epsilon) elif self.data_format == "channels_first": u = x.mean(1, keepdim=True) s = (x - u).pow(2).mean(1, keepdim=True) x = (x - u) / paddle.sqrt(s + self.epsilon) x = self.weight[:, None, None] * x + self.bias[:, None, None] return x# @register# @serializableclass ConvNeXt(nn.Layer): """ ConvNeXt A Paddle impl of : `A ConvNet for the 2020s` - https://arxiv.org/pdf/2201.03545.pdf Args: in_chans (int): Number of input image channels. Default: 3 depths (tuple(int)): Number of blocks at each stage. Default: [3, 3, 9, 3] dims (int): Feature dimension at each stage. Default: [96, 192, 384, 768] drop_path_rate (float): Stochastic depth rate. Default: 0. layer_scale_init_value (float): Init value for Layer Scale. Default: 1e-6. head_init_scale (float): Init scaling value for classifier weights and biases. Default: 1. """ def __init__( self, in_chans=3, out_channals=[1, 2, 3], depths=[3, 3, 9, 3], dims=[96, 192, 384, 768], drop_path_rate=0., layer_scale_init_value=1e-6, ): super().__init__() self._out_strides = [4, 8, 16, 32] self.dims = dims self.out_channals = out_channals self.downsample_layers = nn.LayerList( ) # stem and 3 intermediate downsampling conv layers stem = nn.Sequential( nn.Conv2D(in_chans, dims[0], kernel_size=4, stride=4), LayerNorm(dims[0], epsilon=1e-6, data_format="channels_first")) self.downsample_layers.append(stem) for i in range(3): downsample_layer = nn.Sequential( LayerNorm(dims[i], epsilon=1e-6, data_format="channels_first"), nn.Conv2D(dims[i], dims[i + 1], kernel_size=2, stride=2), ) self.downsample_layers.append(downsample_layer) self.stages = nn.LayerList( ) # 4 feature resolution stages, each consisting of multiple residual blocks dp_rates = [ x.item() for x in paddle.linspace(0, drop_path_rate, sum(depths)) ] cur = 0 for i in range(4): stage = nn.Sequential(*[ Block(dim=dims[i], drop_path=dp_rates[cur + j], layer_scale_init_value=layer_scale_init_value) for j in range(depths[i]) ]) self.stages.append(stage) cur += depths[i] self.norm = nn.LayerNorm(dims[-1], epsilon=1e-6) # final norm layer self.apply(self._init_weights) def _init_weights(self, m): if isinstance(m, (nn.Conv2D, nn.Linear)): trunc_normal_(m.weight) zeros_(m.bias) # @property def out_shape(self): return [ ShapeSpec( channels=self.dims[i], stride=self._out_strides[i]) for i in self.out_channals ] def forward(self, x): x = x outs = [] for i in range(4): x = self.downsample_layers[i](x) x = self.stages[i](x) if i in self.out_channals: outs.append(x) return outsif __name__ == "__main__": model = ConvNeXt() paddle.summary(model, (1, 3, 640, 640))登录后复制

以上就是导盲赛事第二弹: 数据集与训练策略的详细内容,更多请关注其它相关文章!

精品推荐