您当前的位置:首页 > IT编程 > 深度学习
| C语言 | Java | VB | VC | python | Android | TensorFlow | C++ | oracle | 学术与代码 | cnn卷积神经网络 | gnn | 图像修复 | Keras | 数据集 | Neo4j | 自然语言处理 | 深度学习 | 医学CAD | 医学影像 | 超参数 | pointnet | pytorch |

自学教程:AI Challenger 2018 农作物病害细粒度分类-----Pytorch 深度学习实战

51自学网 2021-07-12 10:41:24
  深度学习
这篇教程AI Challenger 2018 农作物病害细粒度分类-----Pytorch 深度学习实战写得很实用,希望能帮到您。

AI Challenger 2018 农作物病害细粒度分类-----Pytorch 深度学习实战

AI Challenger 2018 农作物病害细粒度分类
1 前言
2 代码组织结构
3 完整流程解析
     3.1 EDA
     3.2 参数定义
     3.3 数据加载过程
     3.4 数据处理 DataAugmentation 和TTA
     3.5 模型定义
     3.6 训练过程定义
     3.7 模型融合
     3.8 测试与结果提交
     3.9 log记录,可视化训练过程及其他trick
4 收获与心得
前言

    本文以AI Challenger 2018 农作物病害细粒度分类为例,比赛详细信息和数据见文末,基于Pytorch 0.4.0 构建项目 其中模型训练部分是在jupyter中完成 因此没有将整个训练过程封装为可执行的py文件,做这个比赛的初衷是熟悉一下pytorch,还有就是了解一下打比赛的整个流程,在过程中排名一度还不错 让自己产生了可能能拿奖的错觉 果然还是年轻啊 第一次打比赛还想拿奖 最终acc 在0.883 如果有好心的大佬能够告诉我这个怎么调参调到0.89以上 十分感谢

    不过通过这次比赛 让自己学习到了很多编程的技巧 熟悉了流程 收获还是很大的 有想要一起打比赛的小伙伴可以组队呀。下面就将这次比赛的整个流程的收获做一下总结,方便日后参考,同时也能够作为一份真正的实战指导,虽然做的菜 但总归有可以借鉴的地方.

代码github地址
代码组织结构

    在使用pytroch过程中可以将整个流程分为如下部分:数据分析过程(EDA), 参数定义 ,数据加载过程,数据处理 Data Augmentation和TTA(Test Time Augmentation),模型定义,训练过程定义,验证过程定义,测试过程定义,log定义与训练过程可视化 ,模型融合。 大致可以分为上述部分,每一部分在下文中做具体展开。

整体代码结构如下:

• code
  ▫CropModel.py
  ▫CropDataSet.py
  ▫utils.py
   ......
• config
   ▫config.py
• data
  ▫trainData
  ▫validationData
  ▫testData
• model
  ▫ResNet50
    ▫2018-11-03_acc.pth
• feature
  ▫ ResNet50
      ▫ val_all_prediction.pth
      ▫ val_crop_prediction.pth
      ▫ test_all_prediction.pth
      ......
• log
    ▫ 2018-11-01
           ▫ ResNet50
                  ▫ tensorBoardX
                  ▫ logtxt
           ▫ ResNet 101
     ▫ 2018-11-02
    
• submision:
        ▫ 2018-11-02


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32

在这次比赛中我发现良好的代码组织以及模型组织是必不可少的,只有这样才能更好的实现源源不断的idea的修改,使得代码不至于不可控,上述代码组织结构是这次比赛摸索出来的,肯定还有不好的地方 需要之后实践中不断修改。
code : 存放项目代码其中CropModel .py 将项目使用到的所有模型进行封装 ,CropDataSet .py 存放数据加载类以及不同的transform的方法 ,utils存放各种工具方法
data:在data中下分三个文件夹 trainData ,testData,validationData 每个文件夹下面存放着对应的annotation.json以及img文件夹保存图像
model:model用来保存不同训练模型结果 以模型名称命名,在每个文件夹下以 日期+acc.pth 保留当日最好模型,日期+_loss.pth 保留当日最好loss模型。 在这里其实可以改进model的保存方式 可以每一轮(或者固定周期)都将模型保存下来 然后把最好的模型另外创建一个和model完全一致结构的checkpoint文件夹 专门保存最优模型
feature:feature文件夹存储TTA之后生成的结果(之所以称为feature 是在stacking的时候 第二层的算法是将第一次算法结果作为特征的 所以这里就使用feature来命名这些TTA的结果)
log:存储tensorboardX生成的训练过程图 以及自定义的训练过程中的log输出。在这次比赛中没有存储log输出而是使用jupyter 直接打印出来 这样做是有风险的 不利于log的回溯 同时如果jupyter断开与服务器的连接 那么log信息就会丢失
submission:存储提交结果
完整流程解析
EDA

对于该问题EDA相对而言较为简单 可以分为如下几个步骤
1.将annotation转化为pandas格式
2.查询trainData validateData testData中是否有缺失值存在
3.生成各类样本数量分布图 并按样本数量大小排序
4. 展示若干样本图像

首先通过使用matplotlib 和pandas 对数据进行简单的统计和可视化
注 matplotlib 可能会出现中文注解乱码的问题 可以通过下述代码解决

import matplotlib
matplotlib.rcParams[u'font.sans-serif'] = ['simhei']
matplotlib.rcParams['axes.unicode_minus'] = False

    1
    2
    3

将json文件转化为pandas

with open("../data/AgriculturalDisease_trainingset/AgriculturalDisease_train_annotations.json") as datafile1:
    trainDataFram=pd.read_json(datafile1,orient='records')
with open("../data/AgriculturalDisease_validationset/AgriculturalDisease_validation_annotations.json") as datafile2: #first check if it's a valid json file or not
    validateDataFram =pd.read_json(datafile2,orient='records')    

    1
    2
    3
    4
    5

查看数据中Null的分布情况:

total=trainDataFram.isnull().sum().sort_values(ascending=False)
percent=(trainDataFram.isnull().sum())/(trainDataFram.isnull().count()).sort_values(ascending = False)
missing_validation_data = pd.concat([total, percent], axis=1, keys=['Total', 'Percent'],sort=False)
missing_validation_data.head()

    1
    2
    3
    4

在这里插入图片描述

查看数据分布情况

dataDistribute=trainDataFram.groupby(by=['disease_class']).size()
plt.figure(figsize=(50,20),dpi=100)
plt.xticks(range(len(dataDistribute)),dataDistribution.index.tolist(),fontsize=40)
plt.yticks(fontsize=40)
bar=plt.bar(dataDistribution.index.tolist(), dataDistribute.tolist(),width=0.7)
 
for b in bar:
    h=b.get_height()
    plt.text(b.get_x()+b.get_width()/2,h,int(h),ha='center',fontsize=30)
plt.show()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

在这里插入图片描述

validate data
在这里插入图片描述

由此可见在训练过程中可以将44,45 label删除 提升正确率

根据数据量的大小排序

trainDataFram['disease_class'].value_counts().plot(kind='bar',figsize=(60,30),fontsize =60,title="Number of Training Examples Versus Class").title.set_size(80)

    1

在这里插入图片描述

按大小排列同时在柱状图上增加数据量大小

dataDistribute=trainDataFram['disease_class'].value_counts()
plt.figure(figsize=(50,20),dpi=100)
plt.xticks(range(len(dataDistribute)),dataDistribute.index.tolist(),fontsize=40) #第一个参数是在哪些位置需要放置坐标值  第二个参数是放置的坐标值大小
plt.yticks(fontsize=40)
bar=plt.bar(range(len(dataDistribute)),dataDistribute.tolist(),width=0.6)
for b in bar:
    h=b.get_height()
    plt.text(b.get_x()+b.get_width()/2,h,int(h),ha='center',fontsize=25)
plt.show()

    1
    2
    3
    4
    5
    6
    7
    8
    9

在这里插入图片描述

查看trainDataSet和Validate DataSet数据分布

f,(ax0,ax1)=plt.subplots(1,2,sharey=True,figsize=(15,5))
ax0.hist(trainDataFram['disease_class'].value_counts())
ax0.set_xlabel("# images per class")
ax0.set_ylabel("# classes")
ax0.set_title('Class distribution for Train')

ax1.hist(validateDataFram['disease_class'].value_counts())
ax1.set_xlabel("# images per class")
ax1.set_title('Class distribution for Validation')

    1
    2
    3
    4
    5
    6
    7
    8
    9

在这里插入图片描述
参数定义

1.对于参数定义可以在每一个jupyter中进行定义
2.也可以将所有的参数在一个Config类中进行定义
要注意在使用jupyter训练多个模型的时候不要通过一个函数 不同模型修改不同参数来进行 要创建多个jupyter 每一个jupyter训练一个模型 这样能够保存训练时候的代码清晰 不然想要通过封装一个函数 不用模型进行不同参数的调用会使得代码越来越乱 (毕竟不同模型要修改的参数还是很多的)
如果不想把这些参数都写到模型的jupyter中 可以通过定义一个Config 基类 不同模型继承这个基类 并重写部分参数 保证每个模型有一个独立的Config。
另一点要注意的是 对于随机种子的设定 为了能够复现出训练结果 需要保存随机种子的值

同时由于config文件夹和code文件夹不在同一个目录下 为了能够在code文件夹下的py文件中引用config文件夹下的py文件 需要使用如下命令 将…/config/ 添加到系统路径中去

import sys
sys.path.append('../config/')

    1
    2

import datetime
class BaseConfig():
    def __init__(self,modelName):
        #模型名称
        self.modelName=modelName
        
        #各个存储路径名称
        self.img_train_pre='../data/AgriculturalDisease_trainingset/images/'
        self.img_val_pre='../data/AgriculturalDisease_validationset/images/'
        self.img_test_pre='../data/AgriculturalDisease_testA/images/'
        self.annotation_train='../data/AgriculturalDisease_trainingset/AgriculturalDisease_train_annotations.json'
        self.annotation_val='../data/AgriculturalDisease_validationset/AgriculturalDisease_validation_annotations.json'

        #当前时间
        self.date=str(datetime.date.today())
        
        self.tensorBoard_path='../log/'+self.date+'/'+self.modelName+'/'+'tensorBoardX/'
        self.txtLog_path='../log/'+self.date+'/'+self.modelName+'/'+'txtLog/'+self.date+'_train.txt'
        self.best_acc_path='../model/'+self.date+'/'+self.modelName+'/'+self.date+'_acc.pth'
        self.best_loss_path='../model/'+self.date+'/'+self.modelName+'/'+self.date+'_loss.pth'
        self.submit_path='../submit/'+self.date+'/result.json'
        self.SEED=666
        self.img_size=229
        self.batch_size=64
        self.num_class=61
        
class ResNet50Config(BaseConfig):
    def __init__(self):
        super().__init__('Resnet50')
        self.img_size=224
        
resnet50Config=ResNet50Config()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32

数据加载过程

具体可参考pytorch数据加载过程详解
pytorch 通用的数据加载是通过定义自己的数据加载类 并继承torch.utils.data.Dataset类 并重写其中的 init len 和getitem方法

def default_loader(path):
    return Image.open(path).convert('RGB')
    
class MyDataSet(Dataset):
    def __init__(self,json_Description,transform=None,target_transform=None,loader=default_loader,path_pre=None):
        description=open(json_Description,'r')
        imgs=json.load(description)
        image_path=[element['image_id'] for element in imgs]
        image_label=[element['disease_class'] for element in imgs]
        imgs_Norm=list(zip(image_path,image_label))
        self.imgs=imgs_Norm
        self.transform=transform
        self.target_transform=target_transform
        self.loader=loader
        self.path_pre=path_pre
    def __getitem__(self,index):
        path,label=self.imgs[index]
        img=self.loader(self.path_pre+path)
        if self.transform is not None:
            img=self.transform(img)
        if self.target_transform is not None:
            label=self.target_transform(label)
        return img,label
    def __len__(self):
        return len(self.imgs)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

重写init方法是将不同表现形式的数据如json数据读取并解析成(数据,label)对的形式方便getitem函数使用
重写getitem函数是将init中(数据,label)对取出索引为index的数据 此时的数据可能只是存储路径,因此经过PIL或者Opencv等等 (注意两者channel顺序不同 PIL是 rgb 顺序 opencv为gbr顺序使用,cv2.cvtColor(img,cv2.COLOR_BGR2RGB) 对opencv读取数据进行修改)读取图像 并经过transforms处理返回该index 的(图像,label)对
重写len方法得到数据大小
除此之外 我还习惯将不同类型的transforms 也和Dataset读取定义在同一个py文件中

normalize_torch = transforms.Normalize(
    mean=[0.485, 0.456, 0.406],
    std=[0.229, 0.224, 0.225]
)

normalize_05 = transforms.Normalize(
    mean=[0.5, 0.5, 0.5],
    std=[0.5, 0.5, 0.5]
)

normalize_dataset=transforms.Normalize(
    mean=[0.463,0.400, 0.486],
    std= [0.191,0.212, 0.170]
)

def preprocesswithoutNorm(image_size):
    return transforms.Compose([
       transforms.Resize((image_size, image_size)),
       transforms.ToTensor()
    ])

def preprocess(normalize, image_size):
    return transforms.Compose([
        transforms.Resize((image_size, image_size)),
        transforms.ToTensor(),
        normalize
    ])


def preprocess_hflip(normalize, image_size):
    return transforms.Compose([
        transforms.Resize((image_size, image_size)),
        HorizontalFlip(),
        transforms.ToTensor(),
        normalize
    ])

def preprocess_with_augmentation(normalize, image_size):
    return transforms.Compose([
        transforms.Resize((image_size + 20, image_size + 20)),
        transforms.RandomRotation(15, expand=True),
        transforms.RandomCrop((image_size, image_size)),
        transforms.RandomHorizontalFlip(),
        transforms.ColorJitter(brightness=0.4,
                               contrast=0.4,
                               saturation=0.4,
                               hue=0.2),
        transforms.ToTensor(),
        normalize
    ])

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50

一般会定义三个normalize,分别为mean std为0.5 ,ImageNet的mean和std 以及 使用的数据集的std和mean
下面的代码对于数据集不是特别大的时候可以使用求数据集上的mean和std 如果特别大可以分块使用

import tqdm
import cv2
import pandas as pd
import numpy as np
# 训练样本和训练样本对应的label路径
trainImgPre='../data/AgriculturalDisease_trainingset/images/'
trainAnnotation='../data/AgriculturalDisease_trainingset/AgriculturalDisease_train_annotations.json'

RESIZE_SIZE=256
CROP_SIZE=224
if __name__=='__main__':
    data=[]
    with open(trainAnnotation) as datafile1:
        trainDataFram=pd.read_json(datafile1,orient='records')
    i=0
#     for fileName in tqdm_notebook(trainDataFram['image_id'],miniters=256):
    for fileName in trainDataFram['image_id']:        
        img=cv2.imread(trainImgPre+fileName)
        data.append(cv2.resize(img,(CROP_SIZE,CROP_SIZE)))
        i=i+1
        print(i)
    data = np.array(data, np.float32) / 255 # Must use float32 at least otherwise we get over float16 limits
    print("Shape: ", data.shape)
    means = []
    stdevs = []
    for i in range(3):
        pixels = data[:,:,:,i].ravel()
        means.append(np.mean(pixels))
        stdevs.append(np.std(pixels))
    print("means: {}".format(means))
    print("stdevs: {}".format(stdevs))
    print('transforms.Normalize(mean = {}, std = {})'.format(means, stdevs))

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32

同时在batch size相对较大的时候可以使用在输入层增加一个BN层 代替全局的mean和std 可能会有不错的效果
数据处理 Data Augmentation 和TTA

在本次比赛中对于数据的处理其实没有太多的设计,主要就是在训练时的data augmentation ,对类别不均衡样本的处理 以及在测试的时候的TTA

离线数据增强的方法:
imgaug
augmentor
Augmentation 方法
在线数据增强:
在pytorch中transforms封装了在线数据增强的方法

对于不均衡样本的处理:

处理不均衡的样本

1.LabelShuffling
首先对原始的图像label文件按照label顺序进行排序 在这里为AgriculturalDisease_train_annotations.json 按照disease_class 的顺序进行排序,
(1)计算各个label数量,并得到样本数量最多的label对应的样本数目
(2)根据最大类别数目对每一类产生一个随机排列的列表
(3)用列表中的数目对每一个类别数目取余得到索引值 并根据该索引值
(4)然后把所有类别的随机列表连在一起,做个Random Shuffling,得到最后的图像列表,用这个列表进行训练

在这里插入图片描述

labels=dataFrame.groupby(by=[groupByName]).siez() #获取每个类别groupby之后的数据数目 Series类型 index为类别名称 values为每一个类别的数目
maxNum=max(labels)
tmpLabels=np.array(range(maxNum)) #生成maxNum数目的数组
randomTmpLabels=np.random.permutation(tmpLabels)#将上述数组打乱顺序
targetGroup=trainDataFram.groupby(by=['disease_class']).get_group(groupByName) #根据groupBy的名称获取一个group
targetGroup.iloc[randomTmpLabels%labels[i]] # pandas中读取行分为通过行索引进行读取以及通过行号索引进行读取 通过行索引进行读取使用.loc[] 函数 通过行号索引进行读取使用 .iloc[]

    1
    2
    3
    4
    5
    6

def  labelShuffling(dataFrame,outputPaht="../data/AgriculturalDisease_trainingset/",outputName="AgriculturalDisease_train_Shuffling_annotations.json",groupByName='disease_class'):
    groupDataFrame=dataFrame.groupby(by=[groupByName])
    labels=groupDataFrame.size()
    print("length of label is ",len(labels))
    maxNum=max(labels)
    lst=pd.DataFrame(columns=["disease_class","image_id"])
    for i in range(len(labels)):
        print("Processing label  :",i)
        tmpGroupBy=groupDataFrame.get_group(i)
        createdShuffleLabels=np.random.permutation(np.array(range(maxNum)))%labels[i]
        print("Num of the label is : ",labels[i])
        lst=lst.append(tmpGroupBy.iloc[createdShuffleLabels],ignore_index=True)
        print("Done")
    lst.to_json(outputPaht+outputName,orient="records",force_ascii=False)   # 在这里因为路径中存在中文 所以需要将force_ascii 设置为false

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

2.在这次比赛中存在一个问题就是没有做K折交叉验证 这样使得做第二层stacking的时候会出现数据量不足的问题 同时对于得到的结果也会出现不稳定的问题 最后正确率差距都在千分位上 在本地测试结果较好不一定在线上结果要好 如果做交叉验证 可以使得结果更加稳定

3.使用带权重的惩罚

from sklearn.utils import class_weight
class_weight = class_weight.compute_class_weight('balanced',
                                             np.unique(y_train),
                                             y_train)

    1
    2
    3
    4

4.使用训练集各个label概率与测试集各个label概率进行结果微调 对label进行微调 (该方法适用于训练样本和测试样本分布不同的时候 因为这次比赛训练样本和测试样本的分布基本相近 所有该方法效果不是特别稳定)
首先对于二分类而言 由贝叶斯公式可知
在这里插入图片描述
其中 P(y0 | X )代表对于X的预测结果 Pr(y0)代表y0的先验概率 L(X|y0)代表X和y0之间的似然函数 这里就是我们训练所得的预测函数
对于使用训练集合验证集 似然函数是相同的,但是由于不同数据的先验不同 那么我们得到的P(y0 | X )也不同
在这里插入图片描述
这里的 P(y0 | X )'是我们要求得的测试集(或者在线下测试时的验证集)的概率,
又因为L一定,所以:
在这里插入图片描述
在这里插入图片描述

对于多类也是一样的 我们可把当前处理的类看做y0 ,其他类看做y1
在这里插入图片描述

def calibrate_probs(train_df,val_df,prob,NB_CLASS):
    calibrated_prob = np.zeros_like(prob)
    nb_train = train_df.shape[0]
    for class_ in range(NB_CLASS): # enumerate all classes 这里有61类 其他
        prior_y0_train = (train_df['disease_class'] == class_).mean() #类别为class_的先验
        prior_y1_train = 1 - prior_y0_train
        prior_y0_test=(val_df['disease_class'] ==class_).mean()
        prior_y1_test=1-prior_y0_test
        for i in range(prob.shape[0]): # enumerate every probability for a class
            predicted_prob_y0 = prob[i, class_]
            calibrated_prob_y0 = calibrate(
                prior_y0_train, prior_y0_test,
                prior_y1_train, prior_y1_test,                
                predicted_prob_y0)
            calibrated_prob[i, class_] = calibrated_prob_y0
    return calibrated_prob

def calibrate(prior_y0_train, prior_y0_test,
              prior_y1_train, prior_y1_test,
              predicted_prob_y0):
    predicted_prob_y1 = (1 - predicted_prob_y0)
    
    p_y0 = prior_y0_test * (predicted_prob_y0 / prior_y0_train)
    p_y1 = prior_y1_test * (predicted_prob_y1 / prior_y1_train)
    return p_y0 / (p_y0 + p_y1)  # normalization

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

通过融合测试先验提升准确率 1
通过融合测试先验提升准确率 2

TTA

Test Time Augmentation,可以通过多次尝试不同的TTA方法 因为不能够保证每一次的TTA都是对结构有正作用,需要根据数据特性和模型选择不同TTA(其实我也不同清楚选择TTA的标准化流程 只能通过不断试验 希望大佬可以告知TTA 一般化选择的方法 )
这里做TTA的时候主要是使用了翻转和5个位置切割。注: 在写深度学习代码的时候 一定要多用assert断言来验证维度数量 ,每个维度的大小 等等是否和预期的相同 ,尤其是在forward函数中 很关键

import numbers

import torchvision.transforms.functional as F
from torchvision.transforms import transforms


class HorizontalFlip(object):
    """Horizontally flip the given PIL Image."""

    def __call__(self, img):
        """
        Args:
            img (PIL Image): Image to be flipped.
        Returns:
            PIL Image: Flipped image.
        """
        return F.hflip(img)


def five_crop(img, size, crop_pos):
    if isinstance(size, numbers.Number):
        size = (int(size), int(size))
    else:
        assert len(size) == 2, "Please provide only two dimensions (h, w) for size."

    w, h = img.size
    crop_h, crop_w = size
    if crop_w > w or crop_h > h:
        raise ValueError("Requested crop size {} is bigger than input size {}".format(size,
                                                                                      (h, w)))
    if crop_pos == 0:
        return img.crop((0, 0, crop_w, crop_h))
    elif crop_pos == 1:
        return img.crop((w - crop_w, 0, w, crop_h))
    elif crop_pos == 2:
        return img.crop((0, h - crop_h, crop_w, h))
    elif crop_pos == 3:
        return img.crop((w - crop_w, h - crop_h, w, h))
    else:
        return F.center_crop(img, (crop_h, crop_w))


class FiveCropParametrized(object):
    def __init__(self, size, crop_pos):
        self.size = size
        self.crop_pos = crop_pos
        if isinstance(size, numbers.Number):
            self.size = (int(size), int(size))
        else:
            assert len(size) == 2, "Please provide only two dimensions (h, w) for size."
            self.size = size

    def __call__(self, img):
        return five_crop(img, self.size, self.crop_pos)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54

模型定义

在模型定义的时候习惯将多个模型写在一个py文件中 方便调用 这里使用的模型主要是来自 torchvision.models和pretrained models将这些model做一个封装

class ResNetFinetune(nn.Module):
    finetune = True

    def __init__(self, num_classes, net_cls=M.resnet50, dropout=False):
        super().__init__()
        self.net = net_cls(pretrained=True)
        if dropout:
            self.net.fc = nn.Sequential(
                nn.Dropout(),
                nn.Linear(self.net.fc.in_features, num_classes),
            )
        else:
            self.net.avgpool=nn.AdaptiveAvgPool2d(2)   ##如果不想限制输入的大小 可以修改网络的池化层
            self.net.fc = nn.Linear(self.net.fc.in_features*4, num_classes) # 修改全连接层 输出目标数量的结果

    def fresh_params(self):   #可以在这里增加其他函数 获取不同层级的参数等 方便之后设定不同的学习率
        return self.net.fc.parameters()

    def forward(self, x):
        
             return self.net(x)
 
class FinetunePretrainedmodels(nn.Module):
    finetune = True

    def __init__(self, num_classes: int, net_cls, net_kwards):
        super().__init__()
        self.net = net_cls(**net_kwards)
        self.net.last_linear = nn.Linear(self.net.last_linear.in_features, num_classes)
    def fresh_params(self):
        return self.net.last_linear.parameters()
    def forward(self, x):
        return self.net(x)            
resnet18_finetune = partial(ResNetFinetune, net_cls=M.resnet18) #通过partial固定部分参数 同时保留部分参数
resnet34_finetune = partial(ResNetFinetune, net_cls=M.resnet34)
nasnet_finetune = partial(FinetunePretrainedmodels,
                          net_cls=pretrainedmodels.nasnetalarge,
                          net_kwards={'pretrained': 'imagenet', 'num_classes': 1000})

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38

其中对于修改网络结构可参见 ResNet网络结构解析
对于上述**这种参数传递的方式可参见 * 和 ** 解析

在加载model的时候 可以通过如下的方式加载:

def getmodel():
    print('[+] loading model... ', end='', flush=True)
    model=CropModels.resnet50_finetune(NB_CLASS)
    model.cuda()  
    print('Done')
    return model

    1
    2
    3
    4
    5
    6

训练过程定义
为每一个模型构建以ipynb 用来保存训练过程 每个模型中有3个用以训练的函数 train ,reuseTrain,TrainWithRawData 。具体代码见github 首先设置好随机种子 方便之后的复现 并设定GPU编号

random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.cuda.manual_seed_all(SEED)
os.environ["CUDA_VISIBLE_DEVICES"] = '0'
torch.backends.cudnn.benchmark = True

    1
    2
    3
    4
    5
    6

训练策略:

    Train FC adam (LR 0.001-> 0.00001)
    Polishing, only FC SGD (LR 0.001 -> 0.00001)
    Train full net Adam(LR 0.0001->收敛) 以loss是否下降为标准 连续3个epoch loss不下降 则lr减小十倍
    Train full net SGD (LR 1e-4 ---->收敛)以loss是否下降为标准 连续3个epoch loss不下降 则lr减小十倍
    Train without augs SGD LR 1e-4
    可以尝试在训练过程中FC层的lr设置为其他层的10倍

使用带权重的交叉熵作为损失函数,在整个训练过程中 每300个batch 做一次记录 并跑一遍验证集
除了上述基本模型 有很多为细粒度分类专门设计的模型,在这次比赛中没有尝试这些模型 有时间可以跑一下试一试

Bilinear CNN (B-CNN) for Fine-grained recognition
Look Closer to See Better
NTS-Net 论文
NTS-Net实现
Pairwise Confusion 论文
Pairwise Confusion 实现
孪生网络的pytorch实现
模型融合

在这次比赛中做的不好的地方就是模型融合 使用stacking的方法进行模型融合后结果不增反降,使用gmeans 融合结果也只是稍有增加,可能是我对于stacking的做法有些问题
Stacking 介绍1
Stacking 介绍2
针对CNN不同集成策略
CNN with xgboost
使用LightGBM或xgboost (其实也可以试试 LR和Random Forest )对上述神经网络结果进行stacking
这里不同的地方是可以借鉴 stacking with cnn的模型融合方法 在这里实验没有什么提升 可能是代码的问题
测试与结果提交
因为在结果预测的时候使用了TTA ,所以生成的最终预测结果是一个三维的矩阵 每一行是一个样本 每一列代表预测的类型 而channel代表不同的TTA生成的对于不同样本每一类的预测得分 比如使用5 crop 作为TTA那么就会得到5个channel

def predictAll(model_name, model_class, weight_pth, image_size, normalize):
    print(f'[+] predict {model_name}')
    model = get_model(model_class)
    model.load_state_dict(torch.load(weight_pth)['state_dict'])
    model.eval()
    print('load state dict done')

    tta_preprocess = [preprocess(normalize, image_size), preprocess_hflip(normalize, image_size)]
    tta_preprocess += make_transforms([transforms.Resize((image_size + 20, image_size + 20))],
                                      [transforms.ToTensor(), normalize],
                                      five_crops(image_size))
    tta_preprocess += make_transforms([transforms.Resize((image_size + 20, image_size + 20))],
                                      [HorizontalFlip(), transforms.ToTensor(), normalize],
                                      five_crops(image_size))
    print(f'[+] tta size: {len(tta_preprocess)}')


    data_loaders = []
    for transform in tta_preprocess:

        test_dataset = MyDataSet(json_Description=ANNOTATION_VAL,transform=transform,path_pre=IMAGE_VAL_PRE)
        data_loader = DataLoader(dataset=test_dataset, num_workers=16,
                                 batch_size=BATCH_SIZE,
                                 shuffle=False)
        data_loaders.append(data_loader)
        print('add transforms')

    lx, px = utils.predict_tta(model, data_loaders)
    data = {
        'lx': lx.cpu(),
        'px': px.cpu(),
    }
    if not os.path.exists('../feature/'+model_name):
        os.makedirs('../feature/'+model_name)
    torch.save(data, '../feature/'+model_name+'/val_all_prediction.pth')
    print('Done'

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36

其中使用到的predict,predicttta等都在utils中做了封装

def predict(model, dataloader):
    all_labels = []
    all_outputs = []
    model.eval()
    with torch.no_grad():        
        for batch_idx, (inputs, labels) in enumerate(dataloader):
            all_labels.append(labels)
            inputs = Variable(inputs).cuda()
            outputs = model(inputs)
            all_outputs.append(outputs.data.cpu())
        all_outputs = torch.cat(all_outputs)
        all_labels = torch.cat(all_labels)
        all_labels = all_labels.cuda()
        all_outputs = all_outputs.cuda()
    return all_labels, all_outputs

def safe_stack_2array(acc, a):
    a = a.unsqueeze(-1) # 在最后一维扩充 unsqueeze用来扩充一维  squeeze用来压缩维度为1的
    if acc is None:
        return a
    return torch.cat((acc, a), dim=acc.dim() - 1)


def predict_tta(model, dataloaders):
    prediction = None
    lx = None
    for dataloader in dataloaders:
        lx, px = predict(model, dataloader)
        print('predict finish')
        prediction = safe_stack_2array(prediction, px)
    return lx, prediction

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31

log记录,可视化训练过程及其他trick

1 使用tensorBoardX可视化训练过程

writer=SummaryWriter(Config.log) # 创建   /日期/模型名称/TensorBoard的组织形式
writer.add_scalar('Val/Acc',accuracy,niter) #每次想要添加记录点的时候调用 add_scalar  参数分别为 图表名称  记录值   记录点坐标
writer.add_scalar('Val/Loss',log_loss,niter)

    1
    2
    3

2.保存log

class Logger(object):
    def __init__(self):
        self.terminal = sys.stdout  #stdout
        self.file = None

    def open(self, file, mode=None):
        if mode is None: mode ='w'
        self.file = open(file, mode)

    def write(self, message, is_terminal=1, is_file=1 ):
        if '\r' in message: is_file=0

        if is_terminal == 1:
            self.terminal.write(message)
            self.terminal.flush()
            #time.sleep(1)

        if is_file == 1:
            self.file.write(message)
            self.file.flush()

    def flush(self):
        # this flush method is needed for python 3 compatibility.
        # this handles the flush command by doing nothing.
        # you might want to specify some extra behavior here.
        pass
#Log 调用
    log = Logger()
    log.open(config.logs + "log_train.txt",mode="a")
    log.write("\n------------------------------------ [START %s] %s\n\n" % (datetime.now().strftime('%Y-%m-%d %H:%M:%S'), '-' * 40))
    
     log.write('** start training here! **\n')
    log.write('                               |------------ VALID -------------|----------- TRAIN -------------|         \n')  #可以定制想要的log保存内容
    log.write('lr           iter     epoch    | loss   top-1  top-2            | loss   top-1  top-2           |  time   \n')
    log.write('----------------------------------------------------------------------------------------------------\n')

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35

3.save check point

def snapshot(dir_path, run_name, state,key='loss'):
    
    if key=='loss':
        filePath=os.path.join(dir_path,run_name)
        if not os.path.exists(filePath):
            os.makedirs(filePath)
        snapshot_file = os.path.join(dir_path,
                        run_name , date+'_loss_best.pth')
        torch.save(state, snapshot_file)
    else:
        filePath=os.path.join(dir_path,run_name)
        if not os.path.exists(filePath):
            os.makedirs(filePath)
        snapshot_file = os.path.join(dir_path,
                        run_name ,date+ '_acc_best.pth')
        torch.save(state, snapshot_file)
#上述代码其实写的很不好 可以稍加修改
    
utils.snapshot('../model/', 'ResNet50', {
                   'epoch': epoch + 1,
                   'state_dict': model.state_dict(),
                   'optimizer': optimizer.state_dict(),
                   'val_loss': log_loss,
                   'val_correct':accuracy })    

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

4.定制运行时存储内容

class RunningMean:
    def __init__(self, value=0, count=0):
        self.total_value = value
        self.count = count

    def update(self, value, count=1):
        self.total_value += value
        self.count += count

    @property
    def value(self):
        if self.count:
            return float(self.total_value)/ self.count
        else:
            return float("inf")

    def __str__(self):
        return str(self.value)
 
 

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

这些东西其实也可以不自己写 下述代码库模块化了这些内容 下次可以尝试使用

TNT is a library providing powerful dataloading, logging and visualization utilities for Python.
心得与体会

打比赛最重要的还是要去尝试 然后查询之前是否有类似的比赛 从别人的代码中学习 不断总结才能够有所提高。
希望下次比赛能够有一个好成绩,这次比赛的地址和数据集下载地址如下:
比赛地址
数据集地址 密码: 4ac2

看了前几名的答辩之后总的来说大家的水平其实差距并不大 也可能是因为这道赛题本身的缘故
总结一下前几名的主要思路
1.loss函数的 选取
前几名中基本都使用了focal loss 代替交叉熵损失 从而减轻因为类别分布不均匀带来的误差
2. 数据增强
使用mixup做线下的增强
使用翻转 小角度旋转 随机裁剪 放射变换等等 在线随机增强方式

3.修改全连接之前的pooling层 使用自适应pooling 这样可以使用 不同分辨率的数据 在训练过程中 首先使用224224 接着使用 331331 之后448*448 等在不同的epoch使用多种 分辨率进行训练
不同层级lr设置不同 底层Conv设置最低lr 高层Conv设置较高lr FC设置最高lr
尝试新增加一个全连接层

4.使用Resnet和Densnet联合训练 在最后的Conv层将 两个feature map 加权平均 送入全连接层

5.层次化分类
————————————————
版权声明:本文为CSDN博主「1只小包子」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a940902940902/article/details/83993234
自适应小数据集乳腺癌病理组织分类研究
anaconda安装及配置深度学习环境
51自学网,即我要自学网,自学EXCEL、自学PS、自学CAD、自学C语言、自学css3实例,是一个通过网络自主学习工作技能的自学平台,网友喜欢的软件自学网站。
京ICP备13026421号-1