当前位置: 首页 > 工具软件 > Steel Storm > 使用案例 >

kaggele-Unet-steel-defeat-detection

空正豪
2023-12-01

brief

 今天算是正式第一次打kaggle比赛,记录一下学习成果。这个比赛是用于检测钢铁是否存在缺陷,给出的训练数据集中有四类的缺陷情形,当然也存在没有划痕的情形。我一个打酱油的先是研读了一份开源的kernel代码,今天对同组的同学的Unet深入的学习的一下,说一下具体思路。训练是采取的两次训练的方法,第一次先是在所有的数据集上进行训练一次,第二次再在全是缺陷的数据集上进行测试;细节方面提一下最后的Loss那里采取的是4个channel的输出,然后对每一个的channel表示一个二分类,采用交叉熵为损失函数。

代码部分

model.py

from keras.layers import Conv2D, MaxPooling2D, Input, BatchNormalization, Deconv2D, Lambda
import keras.backend as K
from keras.models import Model
from keras.optimizers import Adam
import tensorflow as tf

def downsampling_conv_block(net, filters, kernel=3):#U-net下采样部分 
    net = Conv2D(filters, kernel, padding='same', activation='relu')(net)
    net = BatchNormalization()(net)
    net = Conv2D(filters, kernel, padding='same', activation='relu')(net)
    _net = BatchNormalization()(net)
    net = MaxPooling2D()(_net)
    return net, _net#_net是为了后面的跳跃连接

# keras 搭建网络时,只可以使用keras类型的变量,若要用一些tf的操作则需考虑使用Lambda层 如下所示,其中K为调用keras的底层,常用如tf、theano、mxnet等
def upsampling_conv_block(net1, net2, filters, kernel=3):
    net1 = Deconv2D(filters, kernel, strides=2, padding='same', activation='relu')(net1)
    net = Lambda(lambda x: K.concatenate(x, -1))([net1, net2])
    net = Conv2D(filters, kernel, padding='same', activation='relu')(net)
    net = BatchNormalization()(net)
    net = Conv2D(filters, kernel, padding='same', activation='relu')(net)
    net = BatchNormalization()(net)
    return net

# 在搭建工程时,网络常常用类搭建,本程序为自娱自乐,用函数节省时间
def unet(istraining=True):
    input = Input(shape=(256, 512, 1))
    net1, _net1 = downsampling_conv_block(input, 32)
    net2, _net2 = downsampling_conv_block(net1, 64)
    net3, _net3 = downsampling_conv_block(net2, 128)
    net4, _net4 = downsampling_conv_block(net3, 256)
    net5 = Conv2D(512, 3, padding='same', activation='relu')(net4)
    net5 = BatchNormalization()(net5)
    net5 = Conv2D(256, 1, activation='relu')(net5)
    net5 = BatchNormalization()(net5)
    net5 = Conv2D(256, 3, padding='same', activation='relu')(net5)
    net5 = BatchNormalization()(net5)
    net5 = Conv2D(512, 1, activation='relu')(net5)
    net5 = BatchNormalization()(net5)
    net6 = upsampling_conv_block(net5, _net4, 256)
    net7 = upsampling_conv_block(net6, _net3, 128)
    net8 = upsampling_conv_block(net7, _net2, 64)
    net9 = upsampling_conv_block(net8, _net1, 32)
    output = Conv2D(4, 1, padding='same', activation='sigmoid')(net9)
    model = Model(inputs=input, outputs=output)
    if istraining:
        compile_model(model)
    return model

def focal_loss(gamma=2.):
    def focal_loss_fixed(y_true, y_pred):
        eps = 0.0001
        pt_1 = tf.where(tf.equal(y_true, 1), y_pred, tf.ones_like(y_pred))#预测值和真实值都是1
        pt_0 = tf.where(tf.equal(y_true, 0), y_pred, tf.zeros_like(y_pred))#预测值和真实值都是0
        return -K.mean(K.pow(1 - pt_1, gamma) * K.log(pt_1 + eps)) - K.mean(K.pow(pt_0, gamma) * K.log(1 - pt_0 + eps))
    return focal_loss_fixed


def dice_coefficient(y_pre, y_true):
    eps = 0.0001
    y_pre = tf.where(y_pre > 0.5, K.ones_like(y_pre), K.zeros_like(y_pre))
    # int_sec = K.sum(y_pre * y_true, [1, 2, 3])
    # xy_sum = K.sum(y_true, [1, 2, 3]) + K.sum(y_pre, [1, 2, 3])
    int_sec = K.sum(y_pre * y_true)
    xy_sum = K.sum(y_true) + K.sum(y_pre)
    return (2 * int_sec + eps) / (xy_sum + eps)

# loss可以使用keras.losses里自带的loss,也可以如上自己定义,但是损失函数默认两个参数必须为y_true和y_pred,若欲使用其他参数
# 可以如上focal_loss函数嵌套的方法调用,metrics也同理
def compile_model(model):
    opt = Adam(lr=0.0005)
    model.compile(opt, focal_loss(), metrics=[dice_coefficient])



data.py

import cv2
import os
import numpy as np
import pandas as pd


def read_data(path):
    data = pd.read_csv(path)
    img_ids, img_masks = data['ImageId_ClassId'], data['EncodedPixels']
    idx = list(range(0, len(img_ids), 4))
    img_ids = img_ids[idx]#arry直接传入一个list可以直接取出这个List位置下的元素。
    img_ids = np.array([s[:-2] for s in img_ids])#获得每一张图片的名字
    img_masks = np.array(img_masks)#取得所有的mask,大小为img_ids的四倍,转化为array
    img_masks = np.reshape(img_masks, [-1, 4])#转成二维的,参数[-1,4]表示第二个维度为4,第一个维度用总的/4
    # print(img_masks)
    return img_ids, img_masks


def split_nan(id, mask):
    nor_id, nan_id, nor_mask, nan_mask = [], [], [], []
    for i in range(len(mask)):#mask 是一个二维的,len()取第一个维度
        zm = 0
        for m in mask[i]:
            if isinstance(m, float):
                zm += 1
        if zm == 4:
            nan_id.append(id[i])
            nan_mask.append(mask[i])# 一张图假如四中类型的缺陷不存在的话就存在nan_maks
        else:
            nor_id.append(id[i])
            nor_mask.append(mask[i]) #nor_mask用于储存那些一张图上大于或者等于1缺陷的
    nor_id = np.array(nor_id)
    nan_id = np.array(nan_id)
    nor_mask = np.array(nor_mask)
    nan_mask = np.array(nan_mask)
    return nor_id, nan_id, nor_mask, nan_mask


def split_all(id,mask):
    data_all, data_mask = [], []
    for i in range(len(mask)):
        data_mask.append(mask[i])
        data_all.append(id[i])
    data_mask = np.array(data_mask)
    data_all = np.array(data_all)
    return data_all, data_mask



def generator(img, mask, batchsize=16):
    batchnum = len(img) // batchsize  #// 表示整数除法,在batch_size为16的时候有多少个batch.
    idx = list(range(len(img)))#图片个数同size的List[0,1,2,...,size-1]
    while True:
        np.random.shuffle(idx) #对Train的数据顺序进行打乱
        for i in range(batchnum): 
            l = i * batchsize
            r = l + batchsize
            batchimg, batchmask = img[idx[l:r]], mask[idx[l:r]] #按照给出的batch_size取一个batch的图片
            batchimg = read_img(batchimg)#读取这个patch的照片
            batchmask = read_mask(batchmask)#读取对应的mask
            yield batchimg, batchmask #递归的输出值,每一次都会返回得到的值,用于在for,while中输出每一次迭代的值


def read_img(paths):#这个path是个array,表示一个batch的照片名
    imgs = []
    for s in paths:
        img = cv2.imread(os.path.join('../severstal-steel-defect-detection/train_images', s), cv2.IMREAD_GRAYSCALE)#以灰度的方式读取,不包括a通道
        img = cv2.resize(img, (512, 256))
        imgs.append(img) 
    imgs = np.array(imgs)# shape=(b,h,w,c)??
    imgs = imgs / 255
    imgs = np.expand_dims(imgs, -1)#在最后的一个维度加入一个维度??
    return imgs


def read_mask(masks):
    ms = []
    for m4 in masks:#m4表示每一行,表示一个照片的Mask个数,4个数据,可能为空
        mm = []
        for m in m4: #m是这一行的的数据,表示四个class
            if isinstance(m, float):#没有缺陷
                tmp = np.zeros((256, 512, 1))#生成一个whc直接为训练的数据大小,值为0的tensor,表示没有缺陷
            else:
                # tmp = pix2mask(m)
                tmp = rle2mask(m) #存在缺陷。返回一个mask
            # print(tmp.shape)
            mm.append(tmp)#储存4个class的Mask
        ms.append(np.concatenate(mm, -1))#在最后一个维度上组合成一个tensor
    ms = np.array(ms)#一张img此时就只对应一个ms了
    return ms


def rle2mask(mask_rle, shape=(1600, 256)):
    s = mask_rle.split()
    #np.asarray()和np.array()一样转化为np能处理的数据,但是除非必要,是不会cope对象的而np.array()是会cope对象的
    starts, lengths = [np.asarray(x, dtype=int) for x in (s[0:][::2], s[1:][::2])]
    #上式表示取mask中起点和每一个起点的步长
    starts -= 1
    ends = starts + lengths#对每一个start都存在一个end
    img = np.zeros(shape[0]*shape[1], dtype=np.uint8)
    for lo, hi in zip(starts, ends): #zip函数打包合成数据,一个start对应一个end
        img[lo:hi] = 1#该处的mask值为1
    img = img.reshape(shape).T#旋转shape变为(1600,512)
    img = cv2.resize(img, (512, 256))#resize()这一步默认采用双线性插值,会改变灰度值
    img = np.where(img >= 0.5, np.ones_like(img), np.zeros_like(img))#将上一步的操作的灰度值从新变成0,1
    img = np.reshape(img, (256, 512, 1))
    return img


def mask2rle(img):
    '''
    img: numpy array, 1 - mask, 0 - background
    Returns run length as string formated
    '''
    pixels = img.T.flatten()#旋转拉直 
    pixels = np.concatenate([[0], pixels, [0]])
    runs = np.where(pixels[1:] != pixels[:-1])[0] + 1
    runs[1::2] -= runs[::2]
    return ' '.join(str(x) for x in runs)


def pix2mask(pix):
    mask = np.zeros(256 * 1600)
    pix = np.reshape(pix.split(), [-1, 2])
    pix = pix.astype(np.int32)
    for s, l in pix:
        s = s - 1
        mask[s:(s + l)] = 255
    mask = np.reshape(mask, (256, 1600))
    mask = cv2.resize(mask, (512, 256))
    mask = np.reshape(mask, (256, 512, 1))
    mask[np.where(mask >= 127.5)] = 1
    mask[np.where(mask < 127.5)] = 0
    return mask




train.py

from model import *
from data import *
from sklearn.model_selection import train_test_split
from keras.callbacks import ModelCheckpoint

epochs = 30
batchsize = 16
is_mining_train = True

path = '../severstal-steel-defect-detection/train.csv'
data, label = read_data(path)
if is_mining_train:
    #data, _, label, _ = split_nan(data, label)#这里只取了有缺陷的做训练
    data,label=split_all(data,label)#取所有的数据进行第一次的测试
#train_test_split函数用于将矩阵随机划分为训练子集和测试子集,并返回划分好的训练集测试集样本和训练集测试集标签。
td, vd, tl, vl = train_test_split(data, label, test_size=0.2)#td,vd用于训练,20%的tl和vl用于测试
tg = generator(td, tl, batchsize)
vg = generator(vd, vl, batchsize)
net = unet()

#net.load_weights('./base_models/ver1.hdf5')

save_path = './models/{epoch:02d}-{val_loss:.2f}.hdf5'#参数什么意思?如何保存的name为ver2
ckpt = ModelCheckpoint(save_path)
# 网络训练常用fit或fit_genertaor,两者区别在于前者必须将数据全部读入内存,这在实际应用中很少能做到,所以常用后者,后者需要传入一个generator
# generator函数参照python的yield用法,另外,若不方便使用划分epoch,则可以用for循环和train_on_batch训练
# callbacks回调个人常用ModelCheckpoint和EarlyStopping或继承CallBack实现一些自己的功能
net.fit_generator(tg, len(td) // batchsize, epochs, callbacks=[ckpt], validation_data=vg,
                  validation_steps=len(vd) // batchsize)

我这里是只采用了第一次的训练的train代码。
作为新手,我还是有几个问题需要问:

  • save_path = ‘./models/{epoch:02d}-{val_loss:.2f}.hdf5’#参数什么意思?如何保存的name为ver2?
    参数就是文件名。。
  • metrics函数就是度量函数吗? 为啥不直接采用IOU
    这是赛题需要,分母上没有要求要减去交集部分,因此需要重载。

最后,今天七夕,大家玩的快乐!

 类似资料:

相关阅读

相关文章

相关问答