1. 程式人生 > >kaggle 2018 data science bowl____一次失敗的 kaggle 專案參與經歷及反思總結

kaggle 2018 data science bowl____一次失敗的 kaggle 專案參與經歷及反思總結

____tz_zs

儘管,沒能得到好的結果,參與過程中的收穫和提高,也是很值得高興的。

這裡記錄下這次的失敗,以便下次吸取教訓、更進一步。

同時,也希望能夠幫到那些看到我這篇部落格的新人朋友。

一個專案的步驟分為:資料預處理、模型構造、模型訓練、模型評估

總體思路:訓練一個複雜的卷積神經網路需要非常多的標註資料和很長的訓練時間。而 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 組訓練資料。這樣,每組資料的圖片的尺寸不會很大,然後使用遷移學習。。。

參考: