当前位置: 首页 > 科技观察

深度学习-计算机视觉8个常见错误总结及防坑指南

时间:2023-03-18 00:29:51 科技观察

本文转载自雷锋网。人不是完美的,我们在编写软件时经常会犯错误。有时这些错误很容易找到:您的代码根本无法运行,您的应用程序崩溃了。但是有些错误是隐藏的并且很难发现,这使得它们更加危险。在处理深度学习问题时,由于一些不确定性,很容易犯这样的错误:很容易看出Web应用的端点是否正确路由了请求,但很难检查梯度下降步骤是否正确.但是,在深度学习的实践套路中,有很多BUG是可以避免的。我想和大家分享一下我在过去两年的计算机视觉工作中发现或犯过的一些错误。我在会议上谈到了这个话题,会议结束后很多人告诉我:“是的,伙计,我也有很多这样的错误。”我希望我的文章能帮助您避免其中的一些问题。1.翻转图像和关键点假设有人正在研究关键点检测的问题。他们的数据看起来像一对图像和一系列关键点元组,例如[(0,1),(2,2)],其中每个关键点是一对x和y坐标。让我们对这些数据集进行基本的增强:w-x)fory,xinkpts]returnimg,kpts上面的代码看起来很对吧?接下来,让我们想象一下。图片=np.ones((10,10),dtype=np.float32)kpts=[(0,1),(2,2)]image_flipped,kpts_flipped=flip_img_and_keypoints(image,kpts)img1=image.copy()fory,xinkpts:img1[y,x]=0img2=image_flipped.copy()fory,xinkpts_flipped:img2[y,x]=0_=plt.imshow(np.hstack((img1,img2)))这个图片不是对称的,看起来很奇怪!如果我们检查极值怎么办?图片=np.ones((10,10),dtype=np.float32)kpts=[(0,0),(1,1)]image_flipped,kpts_flipped=flip_img_and_keypoints(image,kpts)img1=image.copy()fory,xinkpts:img1[y,x]=0img2=image_flipped.copy()fory,xinkpts_flipped:img2[y,x]=0------------------------------------------------------------------------IndexErrorTraceback(最近的调用)in8img2=image_flipped.copy()9fory,xinkpts_flipped:--->10img2[y,x]=0IndexError:index10isoutofboundsforaxis1withsize10isnot好的!这是一个典型的错误。正确的代码如下:defflip_img_and_keypoints(img:np.ndarray,kpts:Sequence[Sequence[int]]):img=np.fliplr(img)h,w,*_=img.shapekpts=[(y,w-x-1)fory,xinkpts]returnimg,kpts我们已经通过可视化检测到这个问题,但是,x=0点的单元测试也会有所帮助。有趣的事实:我团队中的三个人(包括我自己)独立地犯了几乎相同的错误。2.继续说重点,上面的功能就算固定了,也有危险。接下来的内容更多是关于语义的,而不仅仅是一段代码。假设一个人需要用双掌增强图像。看起来很安全-左右翻转后手仍然是手。可是等等!我们对关键点语义一无所知。如果关键点真的是这样的:kpts=[(20,20),#leftpinky(20,200),#rightpinky...]这意味着增强实际上改变了语义:左变为右,右变为左,但我们不知道'交换数组中的关键点索引。它给训练带来了巨大的噪音和更糟糕的指标。应该在这里吸取教训:在应用增强功能或其他功能之前了解并考虑数据结构和语义;保持你的实验独立:添加一个小的变化(例如,一个新的转换),检查它是如何工作的,如果分数提高然后合并。3.自定义损失函数熟悉语义分割问题的人可能知道IoU(intersectionoverunion)度量。不幸的是,我们不能直接用SGD优化它,所以一个常见的技巧是用可微的损失函数来近似它。我们来写相关的代码吧!def_continuous_loss(y_pred,y_true):eps=1e-6def_sum(x):returnx.sum(-1).sum(-1)numerator=(_sum(y_true*y_pred)+eps)denominator=(_sum(y_true**2)+_sum(y_pred**2)-_sum(y_true*y_pred)+eps)return(numerator/denominator).mean()看起来不错,让我们做一下检查:In[3]:ones=np.ones((1,3,10,10))...:x1=iou_continuous_loss(ones*0.01,ones)...:x2=iou_continuous_loss(ones*0.99,ones)In[4]:x1,x2Out[4]:(0.010099999897990103,0.9998990001020204)在x1中,我们计算了一个与标准答案完全不同的损失,而x2是一个非常接近标准答案的函数的结果。我们期望x1很大,因为预测不好,x2应该接近于零。发生了什么?上面的函数是度量的一个很好的近似值。指标不是损失:它通常越高越好。既然我们想使用SGD来最小化损失,那么我们真的应该使用相反的方法:v>defiou_continuous(y_pred,y_true):eps=1e-6def_sum(x):returnx.sum(-1).sum(-1)分子=(_sum(y_true*y_pred)+eps)分母=(_sum(y_true**2)+_sum(y_pred**2)-_sum(y_true*y_pred)+eps)return(分子/分母).mean()defiou_continuous_loss(y_pred,y_true):return1-iou_continuous(y_pred,y_true)这些问题可以通过两种方式确定:做一个彻底的检查,尝试适应你的模型的批次。4.使用Pytorch假设有一个预训练模型,它是一个时间序列模型。我们基于ceeveeapi编写预测类。fromceevee.baseimportAbstractPredictorclassMySuperPredictor(AbstractPredictor):def__init__(self,weights_path:str,):super().__init__()self.model=self._load_model(weights_path=weights_path)defprocess(self,x,*kw):withtorch.no_grad():res=self.model(x)returnres@staticmethoddef_load_model(weights_path):model=ModelClass()weights=torch.load(weights_path,map_location='cpu')model.load_state_dict(weights)returnmodel密码是否正确?或许!对于某些模型确实如此。例如当模型没有范数层时,如torch.nn.BatchNorm2d;或者当模型需要对每个图像使用实际的规范统计时(例如,许多基于pix2pix的体系结构需要它)。但是对于大多数计算机视觉应用程序,代码缺少一些重要的东西:切换到评估模式。如果您尝试将动态pytorch图转换为静态pytorch图,则此问题很容易识别。这个转换有一个torch.jit模块。一个简单的修复:In[4]:model=nn.Sequential(...:nn.Linear(10,10),..:nn.Dropout(.5)...:)...:..。:traced_model=torch.jit.trace(model.eval(),torch.rand(10))#Nomorewarnings!此时,torch.jit.trace多次运行模型并比较结果。这里似乎没有区别。然而,torch.jit.trace在这里并不是万能的。这是一个应该知道和记住的细微差别。5.复制粘贴问题许多东西都是成对存在的:训练和验证、宽度和高度、纬度和经度……如果你仔细阅读,你可以很容易地发现成对成员之间复制粘贴导致的错误:v>defmake_dataloaders(train_cfg,val_cfg,batch_size):train=Dataset.from_config(train_cfg)val=Dataset.from_config(val_cfg)shared_pa??rams={'batch_size':batch_size,'shuffle':True,'num_workers':cpu_count()}火车=DataLoader(train,**shared_pa??rams)val=DataLoader(train,**shared_pa??rams)returntrain,val不只是我在犯愚蠢的错误。流行的库中也有类似的错误。#https://github.com/albu/albumentations/blob/0.3.0/albumentations/augmentations/transforms.pydefapply_to_keypoint(self,keypoint,crop_height=0,crop_width=0,h_start=0,w_start=0,rows=0,cols=0,**params):keypoint=F.keypoint_random_crop(keypoint,crop_height,crop_width,h_start,w_start,rows,cols)scale_x=self.width/crop_heightscale_y=self.height/crop_heightkeypoint=F.keypoint_scale(关键点,scale_x,scale_y)returnkeypoint别担心,这个错误已经被修复了。如何避免?不要复制粘贴代码,尽量不要复制粘贴代码。datasets=[]data_a=get_dataset(MyDataset(config['dataset_a']),config['shared_pa??ram'],param_a)datasets.append(data_a)data_b=get_dataset(MyDataset(config['dataset_b']),config['shared_pa??ram'],param_b)datasets.append(data_b)datasets=[]forname,paraminzip(('dataset_a','dataset_b'),(param_a,param_b)):datasets.append(get_dataset(MyDataset(配置[名称]),config['shared_pa??ram'],param))6.合适的数据类型让我们再做一个增强:defadd_noise(img:np.ndarray)->np.ndarray:mask=np.random.rand(*img.shape)+.5img=img.astype('float32')*maskreturnimg.astype('uint8')图片变了。这是我们期待的吗?嗯,也许改变太多了。这里有一个危险的操作:将float32转成uint8。这会导致溢出:defadd_noise(img:np.ndarray)->np.ndarray:mask=np.random.rand(*img.shape)+.5img=img.astype('float32')*maskreturnnp.clip(img,0,255).astype('uint8')img=add_noise(cv2.imread('two_hands.jpg')[:,:,::-1])_=plt.imshow(img)看起来好多了,对吧??对了,还有一个方法可以避免这个问题:不要重新发明轮子,可以在前人的基础上修改代码。例如:albumments.augmentations.transforms.GaussNoise。我又遇到了同样的错误。这里出了什么问题?首先,使用三次插值调整掩码大小不是一个好主意。将float32转换为uint8存在同样的问题:三次插值可以输出大于输入的值,并导致溢出。我发现了这个问题。在循环中使用断言也是一个好主意。7.错别字假设全卷积网络(如语义分割问题)需要用巨大的图像进行处理。该图像太大,您无法将其放入您的GPU中-例如,它可能是医学图像或卫星图像。在这种情况下,可以将图像划分为网格,独立推理每个块,最后合并。此外,一些预测交点可用于平滑边界附近的伪影。让我们编码吧!fromtqdmimporttqdmclassGridPredictor:"""当你有GPU内存限制"""def__init__(self,predictor:AbstractPredictor,size:int,stride:Optional[int]=None):self.predictor=predictorself.size=sizeself.stride=strideifstrideisnotNoneelsize__(self,2def__x:np.ndarray):h,w,_=x.shapemask=np.zeros((h,w,1),dtype='float32')weights=mask.copy()foriintqdm(range(0,h-1,self.stride)):forjinrange(0,w-1,self.stride):a,b,c,d=i,min(h,i+self.size),j,min(w,j+self.size)patch=x[a:b,c:d,:]mask[a:b,c:d,:]+=np.expand_dims(self.predictor(patch),-1)权重[a:b,c:d,:]=1returnmask/weights有一个符号错字,代码片段足够大,很容易找到。我怀疑它可以通过代码快速识别,很容易检查代码是否正确:classModel(nn.Module):defforward(self,x):returnx.mean(axis=-1)model=Model()grid_predictor=GridPredictor(模型,size=128,stride=64)simple_pred=np.expand_dims(model(img),-1)grid_pred=grid_predictor(img)np.testing.assert_allclose(simple_pred,grid_pred,atol=.001)调用正确版本的方法如下:def__call__(self,x:np.ndarray):h,w,_=x.shapemask=np.zeros((h,w,1),dtype='float32')weights=mask.copy()foriintqdm(范围(0,h-1,self.stride)):forjin范围(0,w-1,self.stride):a,b,c,d=i,min(h,i+self.size),j,min(w,j+self.size)patch=x[a:b,c:d,:]mask[a:b,c:d,:]+=np.expand_dims(self.predictor(patch),-1)weights[a:b,c:d,:]+=1returnmask/weights如果还是没看出问题,请注意线宽[a:b,c:d,:]+=1。8.ImageNetNormalization当需要进行迁移学习时,通常最好像训练ImageNet时那样对图像进行归一化。让我们使用我们已经熟悉的相册库。fromalbumentationsimportNormalizenorm=Normalize()img=cv2.imread('img_small.jpg')mask=cv2.imread('mask_small.png',cv2.IMREAD_GRAYSCALE)mask=np.expand_dims(mask,-1)#shape(64,64))->shape(64,64,1)normed=norm(image=img,mask=mask)img,mask=[normed[x]forxin['image','mask']]defimg_to_batch(x):x=np.transpose(x,(2,0,1)).astype('float32')returntorch.from_numpy(np.expand_dims(x,0))img,mask=map(img_to_batch,(img,mask))标准=F.binary_cross_entropy现在是训练网络并将其过拟合到图像的时候了——正如我提到的,这是一个很好的调试技术:model_a=UNet(3,1)optimizer=torch.optim.Adam(model_a.parameters(),lr=1e-3)losses=[]fortintqdm(range(20)):loss=criterion(model_a(img),mask)losses.append(loss.item())optimizer.zero_grad()loss.backward()优化器.step()_=plt.plot(losses)曲率看起来不错,但是交叉熵损失值估计不是-300。这里发生了什么?图像的归一化效果很好,需要手动缩放到[0,1]。model_b=UNet(3,1)优化器=torch.optim.Adam(model_b.parameters(),lr=1e-3)losses=[]fortintqdm(range(20)):loss=criterion(model_b(img),mask/255.)losses.append(loss.item())optimizer.zero_grad()loss.backward()optimizer.step()_=plt.plot(losses)训练循环中的简单断言(例如断言mask.max()<=1)会很快检测到问题。同样,单元测试也可以检测问题。总结:测试很重要;运行断言可用于训练管道;可视化是一个很好的工具;剽窃是一种诅咒;没有什么是万能的,机器学习工程师必须时刻保持谨慎。