kaggle 2018 data science bowl____一次失败的 kaggle 项目参与经历及反思总结

霍书
2023-12-01

____tz_zs

 

前段时间参加了 kaggle 2018 data science bowl ,初生牛犊不怕虎,于是我撸起袖子就开始干了。

尽管,没能得到好的结果,参与过程中的收获和提高,也是很值得高兴的。

这里记录下这次的失败,以便下次吸取教训、更进一步。

同时,也希望能够帮到那些看到我这篇博客的新人朋友。

 

一个项目的步骤分为:数据预处理、模型构造、模型训练、模型评估

总体思路:训练一个复杂的卷积神经网络需要非常多的标注数据和很长的训练时间。而 kaggle 的比赛项目所提供的数据量比较小,总共只有 670 张不同的原始图片,数据集相对较小。所以,我决定使用迁移学习(迁移学习能将一个问题上训练好的模型通过调整使其适用于一个新的问题,能够在数据量不大及训练时间不足的情况下,训练出令人满意的神经网络模型)。所用模型为 Google 训练好的 Inception-v3 模型。我将本次 kaggle 项目看成是一个二值图像的生成问题(一个二维数组),将卷积神经网络部分作为原始图片特征向量的提取过程。

 

·此代码仅作为反面例子,这是最初版,问题很大。

# -*- coding: utf-8 -*-
"""
@author: tz_zs

"""
import pathlib
import numpy as np
from skimage import io, data
from skimage.color import rgb2gray
from skimage.filters import threshold_otsu
import matplotlib.pyplot as plt
from scipy import ndimage
import tensorflow as tf
from tensorflow.python.platform import gfile
import os
from PIL import Image

# inception-v3 模型瓶颈层的节点个数
BOTTLENECK_TENSOR_SIZE = 2048
# 下载的谷歌训练好的inception-v3模型文件目录
MODEL_DIR = 'D:/kaggle/inception_dec_2015'
# 下载的谷歌训练好的inception-v3模型文件名
MODEL_FILE = 'tensorflow_inception_graph.pb'
# inception-v3 模型中代表瓶颈层结果的张量名称
BOTTLENECK_TENSOR_NAME = 'pool_3/_reshape:0'
# 图像输入张量所对应的名称
JPEG_DATA_TENSOR_NAME = 'DecodeJpeg/contents:0'
# 学习率
LEARNING_RATE = 0.01
STEPS = 400
BATCH = 1

# 缩放的尺寸
P = 100


# np.set_printoptions(threshold=1e6)  # 设置打印数量的阈值

def get_train_data_path():
    # 用Path类可以创建path路径对象, 属于比os.path更高抽象级别的对象。
    training_paths = pathlib.Path('D:/kaggle/stage1_train').glob('*')
    #  每一个文件夹,就是一个样本,获取其路径并保存
    train_data_path = {}
    for paths in training_paths:
        # masks文件夹
        masks_list = []
        masks_dir = paths.joinpath('masks')
        masks_dir_iterdir = masks_dir.iterdir()
        for masks_paths in masks_dir_iterdir:
            masks_list.append(masks_paths)

        # images文件夹
        images_dir = paths.joinpath('images')
        images_dir_iterdir = images_dir.iterdir()
        for images_paths in images_dir_iterdir:
            images_id = images_paths.stem
            train_data_path[images_id] = [images_paths, masks_list]
    return train_data_path


if __name__ == '__main__':
    # 数据准备
    train_data_path = get_train_data_path()  # 得到路径
    train_data_masks = []
    train_data_im = []
    i = 0
    for k, v in train_data_path.items():

        # 将masks解码并合并
        masks = 0
        for mask in v[1]:
            im_mask = Image.open(mask)
            im_mask.thumbnail((P, P))
            # im_mask = io.imread(mask)
            masks += np.array(im_mask)
        print(masks.shape)
        train_data_masks.append(masks)

        # for mask in v[1]:
        #     image_data = gfile.FastGFile(mask, 'r').read()
        #     print(image_data)

        # 解码 image,并清理数据
        # im = io.imread(v[0])
        im = Image.open(mask)
        im.thumbnail((P, P))
        im = np.array(im)
        im_gray = rgb2gray(im)  # 使用scikit-image中的rgb2gray,将图像强制转换为灰度格式
        # train_data_im.append(im_gray) #加入list

        io.imsave("D:/tmp/im_tmp/{}.png".format(k), im_gray)
        # 获取图片内容
        image_data = gfile.FastGFile("D:/tmp/im_tmp/{}.png".format(k), 'rb').read()
        train_data_im.append(image_data)

        '''
        #清理数据(废弃)
        # 去除背景:Otsu方法将图像建模为双峰分布,并找到最优的分离值。(暂时先这样处理)
        thresh_val = threshold_otsu(im_gray)
        mask = np.where(im_gray > thresh_val, 1, 0)
        if np.sum(mask == 0) < np.sum(mask == 1):  # 比较0和1的区域的大小,保正是背景占多数
            mask = np.where(mask, 0, 1)  # 0和1交换

        # 使用ndimage.label函数,查找mask中的所有对象,并标记(ndimage.label会将输入中的任何非零值都被视为特性,零值视为背景)
        labels, nlabels = ndimage.label(mask)

        label_arrays = []
        for label_num in range(1, nlabels + 1):  # 遍历并用list分门别类的装好
            label_mask = np.where(labels == label_num, 1, 0)
            label_arrays.append(label_mask)

        # 使用ndimage.find_objects函数遍历mask,返回图像中每个标签对象的坐标范围列表,去除那些较小的像素点(噪音),得到新的mask
        for label_ind, label_coords in enumerate(ndimage.find_objects(labels)):
            cell = im_gray[label_coords]
            if np.product(cell.shape) < 10:
                mask = np.where(labels == label_ind + 1, 0, mask)

        # 重新生成labels
        labels, nlabels = ndimage.label(mask)
        '''
    
    print("数据准备完成")

    # 读取模型
    with gfile.FastGFile(os.path.join(MODEL_DIR, MODEL_FILE), 'rb') as f:
        graph_def = tf.GraphDef()
        graph_def.ParseFromString(f.read())
    # 加载模型,返回对应名称的张量
    bottleneck_tensor, image_data_tensor = tf.import_graph_def(graph_def, return_elements=[BOTTLENECK_TENSOR_NAME,
                                                                                           JPEG_DATA_TENSOR_NAME])
    # 输入
    bottleneck_input = tf.placeholder(tf.float32, [None, BOTTLENECK_TENSOR_SIZE], name='BottleneckInputPlaceholder')
    ground_truth_input = tf.placeholder(tf.float32, [None, P * P], name='GroundTruthInput')

    # 全连接层
    with tf.name_scope('final_training_ops'):
        weights = tf.Variable(tf.truncated_normal([BOTTLENECK_TENSOR_SIZE, P * P], stddev=0.001))
        biases = tf.Variable(tf.zeros([P * P]))
        logits = tf.matmul(bottleneck_input, weights) + biases
        final_tensor = tf.nn.softmax(logits)

    # 损失
    # cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=ground_truth_input)
    # cross_entropy_mean = tf.reduce_mean(cross_entropy)
    mse = tf.reduce_mean(tf.square(ground_truth_input - final_tensor))
    # cross_entropy = -tf.reduce_sum(ground_truth_input * tf.log(final_tensor))
    
    # 优化
    train_step = tf.train.GradientDescentOptimizer(LEARNING_RATE).minimize(mse)

    # 正确率
    with tf.name_scope('evaluation'):
        # correct_prediction = tf.equal(tf.argmax(final_tensor, 1), tf.argmax(ground_truth_input, 1))
        # evaluation_step = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        evaluation_step = tf.reduce_mean(tf.square(ground_truth_input - final_tensor))

    with tf.Session() as sess:
        # 初始化参数
        init = tf.global_variables_initializer()
        sess.run(init)
        for i in range(STEPS):
            # 每次获取一个batch的训练数据
            # temp = i % BATCH
            # print("temp:", temp)
            # masks = train_data_masks[temp:temp + BATCH]
            # im = train_data_im[temp:temp + BATCH]
            masks = train_data_masks[i]
            im = train_data_im[i]

            # 调整矩阵
            reshaped_masks = np.reshape(masks, [1, P * P])

            # 迁移
            bottleneck_values = sess.run(bottleneck_tensor, feed_dict={image_data_tensor: im})  # (1, 2048)

            # 训练
            sess.run(train_step, feed_dict={bottleneck_input: bottleneck_values, ground_truth_input: reshaped_masks})

            # 检测
            validation_accuracy = sess.run(evaluation_step, feed_dict={bottleneck_input: bottleneck_values,
                                                                       ground_truth_input: reshaped_masks})
            print('Step %d ———— %.1f%%' % (i, validation_accuracy * 100))


# RLE方法,用于kaggle提交
def rle_encoding(x):
    '''
    x: numpy array of shape (height, width), 1 - mask, 0 - background
    Returns run length as list
    '''
    dots = np.where(x.T.flatten() == 1)[0]  # .T sets Fortran order down-then-right
    run_lengths = []
    prev = -2
    for b in dots:
        if (b > prev + 1): run_lengths.extend((b + 1, 0))
        run_lengths[-1] += 1
        prev = b
    return " ".join([str(i) for i in run_lengths])

·

 

总结反思:

 

  • 没有清晰的解决问题的思路和方案,有些地方都是临时去找解决方法,导致各种框架的使用过程混乱,没能很好的配合使用。如:图片的清洗过程使用的是 skimage,而后为了配合 tensorflow 迁移学习的输入,只好将清洗好的图片保存到临时文件夹,然后再使用 tensorflow 读取图片,又耗资源,又繁琐。
  • 没有论证想法能否实现。比如:初期想法中就是拍脑袋决定的把项目看作是一个二值图像的生成问题,却没有考虑到通过 全连接神经网络生成 一个如此大的数组,比如图片大小为256*256,全连接层的参数就有2048*65536+65536个,事实上,这个项目没完成的一个原因就是一运行就直接内存溢出了。
    • 解决方法一:我想用缩放图片的方法来解决却发现需要缩放到很小很小,损失了精度。(不可行)。
    • 解决方法二:将原始图像和叠加后的mask图像做同样的随机截取,这样截取出来的image和mask都是对应的部分,而且图像大小可控,且增加了训练集数据。(还在尝试)
  • 对于自己进行的操作可能造成的一些不利后果没有清晰的认识,比如:对于细胞核相邻的问题(通过神经网络生成的mask之间也可能是相邻接的),我计划是使用ndimage.binary_opening 打开邻接,却没考虑到形态学操作对于精度的影响等问题,甚至我本身对于形态学操作的理解就不够。https://blog.csdn.net/tz_zs/article/details/79765189
  • 对于卷积神经网络的理解不够。卷积神经网络的存在,本身弱化了图片预处理的作用,我做的大量的图像处理或许可能反而 导致丢失了精度?比如将原始图像对应的mask叠加为了一张图像。比如前面的导致出现的形态学问题,或许通过mask图像标签让神经网络学会断开链接是更好的选择。

 

此次项目总耗时约一个星期,因为思路和方案的缺陷等,失败了╥﹏╥。

 

后续:

新方案:对于每一个 mask 图像,用一个最小标注框包裹其非零区域,然后在 image 图像上用相同的标注框截取图像。如此,得到一个完整的细胞核图像与其对应的 mask 图像,作为一组数据。所以,在训练集中,如果一张原始图像有 n 个 mask 图像,则可用此方法生成 n 组训练数据。这样,每组数据的图片的尺寸不会很大,然后使用迁移学习。。。

 

补充本次 kaggle 的数据(2019-11-19):

链接:https://pan.baidu.com/s/1WWb5peDt_2ZStpaHUrzRDg 
提取码:35xl

 

参考:

从神经网络视角看均方误差与交叉熵作为损失函数时的共同点

https://www.kaggle.com/rakhlin/fast-run-length-encoding-python

https://www.kaggle.com/stkbailey/teaching-notebook-for-total-imaging-newbies

 类似资料: