1. ホーム
  2. Deep Learning

U-netのソースコード解説(Keras編)

2022-02-16 18:57:14

更新しました。

この企画は2018年8月に書かれたものです。時が経つのは早いもので、もう1年近く経っています。実際、この分野のディープラーニングはハードウェア、ソフトウェアの様々なアップデートが早いですね。

Unetを初めて使うなら、keras版を使うのが結構いいんだけど、結局は自分のニーズがあって、Kerasが面倒になってからその上に自分のモジュールを作っていく、テンソルアライメントとか色々考えないといけないし、Kerasはtensorflowベースだからデバッグも大変だし、今となっては ピトーチ シンプルで柔軟性があるため、広く使われており、発表された論文の多くがPytorchで実装されています、特に最近はtensorflowを追い越す傾向にあります、私もその流れに乗り、昨年Kerasを習得した直後にPytorchに切り替えました、今では私の指導で研究室は基本的にPytorchに切り替えましたのでPytorch習得もお勧めします。以下は私が書いたものです。 Pytorchベースの医用画像セグメンテーションテンプレート このテンプレートは多くのプロジェクトをサポートし、ポータブルで使いやすく、また、Pytorchの学習に関するドキュメントも少しあるので、参考にしてください。皆さん、ありがとうございました

Pytorch_Medical_Segmention_Template : https://github.com/FENGShuanglang/Pytorch_Medical_Segmention_Template

このプロジェクトで実装された機能

<テーブル このプロジェクトファイルでは、以下の機能を実装しています。 1. Unetベースの単一クラスセグメンテーション 2. Nフォールドクロスバリデーションの自動実装 3. 損失関数はDice+BCE 4. SGDの最適化、学習戦略の立案 5. nフォールドチェックポイントファイルの自動保存 6. n倍テンソルボードログの自動保存、複数実験の可視化比較の前後に対応 7. UNetを変更するたびに、比較するために複数の実験の前と後をサポートする UNet フォルダをコピーして、次のように名前を変更します: "UNet_Modify_Content"、そしてこれをベースに変更します。

----------------------------------------------------------- split line -------------------------------------------------------------------- ----

まず、論文での説明です。 住所

ソースコードのアドレスです。 https://github.com/FENGShuanglang/unet

環境:python3を使って実行してみてください。python2.7を使ったところ、2日間うまくいかず、テスト出力が常にグレー一色になってしまいました

ソースコードのフォルダディレクトリ。

ここでは、data.py, model.py, main.py(この3つのpythonファイルのみでも可)に注目します。

まずはmain.pyを見て、順番に各関数の意味を探っていきましょう。

from model import *
from data import *# import all functions in these two files

#os.environ["CUDA_VISIBLE_DEVICES"] = "0"


data_gen_args = dict(rotation_range=0.2,
                    width_shift_range=0.05,
                    height_shift_range=0.05,
                    shear_range=0.05,
                    zoom_range=0.05,
                    horizontal_flip=True,
                    fill_mode='nearest')#Dictionary of transformation modes for data enhancement
myGene = trainGenerator(2,'data/membrane/train','image','label',data_gen_args,save_to_dir = None)
# Get a generator that generates enhanced data infinitely at a rate of batch=2

model = unet()
model_checkpoint = ModelCheckpoint('unet_membrane.hdf5', monitor='loss',verbose=1, save_best_only=True)
# callback function, the first is to save the model path, the second is the value of the test, the test Loss is to make it the smallest, the third is to save only the model with the best performance on the validation set

model.fit_generator(myGene,steps_per_epoch=300,epochs=1,callbacks=[model_checkpoint])
#steps_per_epoch refers to how many batch_size per epoch, which is the total number of samples in the training set divided by the value of batch_size
#The above line is using the generator for training the number of batch_size, samples and labels are passed in through myGene
testGene = testGenerator("data/membrane/test")
results = model.predict_generator(testGene,30,verbose=1)
#30 is the step,steps: total number of steps (sample batches) from the generator before stopping. Optional parameter Sequence: if not specified, len(generator) will be used as the number of steps.
The return value of # above is: a Numpy array of predicted values.
saveResult("data/membrane/test",results)#Save results

data.py ファイルです。

from __future__ import print_function
from keras.preprocessing.image import ImageDataGenerator
import numpy as np 
import os
import glob
import skimage.io as io
import skimage.transform as trans

sky = [128,128,128]
Building = [128,0,0]
Pole = [192,192,128]
Road = [128,64,128]
Pavement = [60,40,222]
Tree = [128,128,0]
SignSymbol = [192,128,128]
Fence = [64,64,128]
Car = [64,0,128]
Pedestrian = [64,64,0]
Bicyclist = [0,128,192]
Unlabelled = [0,0,0]

COLOR_DICT = np.array([Sky, Building, Pole, Road, Pavement,
                          Tree, SignSymbol, Fence, Car, Pedestrian, Bicyclist, Unlabelled])


def adjustData(img,mask,flag_multi_class,num_class):
    if(flag_multi_class):# This is not a multi-class case in this program, so don't consider this
        img = img / 255
        mask = mask[:,:,:,0] if(len(mask.shape) == 4) else mask[:,:,0]
#If else is a concise way to write, a line of expressions, true when placed in front, do not understand what the case of mask.shape = 4, because there is a batch_size, so mask will have 3 dimensions [batch_size, wigth, high], I guess mask[:,::, 0] is written wrong, should be written as [0,:,::], so that you can get a piece of the image
        new_mask = np.zeros(mask.shape + (num_class,))
# np.zeros is inside the shape tuple, the purpose of this is to extend the data thickness to the num_class layer, in order to achieve one-hot structure in the direction of the layer

        for i in range(num_class):
            #for one pixel in the image, find the class in mask and convert it into one-hot vector
            #index = np.where(mask == i)
            #index_mask = (index[0],index[1],index[2],np.zeros(len(index[0]),dtype = np.int64) + i) if (len(mask.shape) == 4) else (index[0],index[1],np. zeros(len(index[0]),dtype = np.int64) + i)
            #new_mask[index_mask] = 1
            new_mask[mask == i,i] = 1# turn each class of the plane's mask, into a separate layer
        new_mask = np.reshape(new_mask,(new_mask.shape[0],new_mask.shape[1]*new_mask.shape[2],new_mask.shape[3])) if flag_multi_class else np. reshape(new_mask,(new_mask.shape[0]*new_mask.shape[1],new_mask.shape[2]))
        mask = new_mask
    elif(np.max(img) > 1):
        img = img / 255
        mask = mask /255
        mask[mask > 0.5] = 1
        mask[mask <= 0.5] = 0
    return (img,mask)
# The above function mainly normalizes the pixel values of the data and labels of the training set


def trainGenerator(batch_size,train_path,image_folder,mask_folder,aug_dict,image_color_mode = "grayscale",
                    mask_color_mode = "grayscale",image_save_prefix = "image",mask_save_prefix = "mask",
                    flag_multi_class = False,num_class = 2,save_to_dir = None,target_size = (256,256),seed = 1):
    '''
    can generate image and mask at the same time
    use the same seed for image_datagen and mask_datagen to ensure the transformation for image and mask is the same
    if you want to visualize the results of generator, set save_to_dir = "your path"
    '''
    image_datagen = ImageDataGenerator(**aug_dict)
    mask_datagen = ImageDataGenerator(**aug_dict)
    image_generator = image_datagen.flow_from_directory(#https://blog.csdn.net/nima1994/article/details/80626239
        train_path,#training_data_folder_path
        classes = [image_folder]

ここで注意すべきは、予測時に直接出力しているので、下のモデルの出力はシグモイド関数後の出力、つまり0と1の間の出力ですが、ここでは0と1の間の数値を直接画像として保存しており、ここで2つの疑問があります。

1. なぜ0と1の間の浮動小数点数を直接画像として保存できるのでしょうか?

なぜなら、skimageモジュールでは、画像データがfloatの場合、その値は0〜1または-1〜1の浮動小数点数であるべきだからです。

2. マスク2値画像を生成せず、直接保存する理由は?

これは、出力データの値がすでに非常に偏った値、つまり0に非常に近い値もあれば1に非常に近い値もあり、その中間の値は非常に少ないので、直接出力しても問題なく、グレースケールマップと同等になります。もし、2値画像を生成したくなったら、次のようにコードを修正すればよいのです。

def saveResult(save_path,npyfile,flag_multi_class = False,num_class = 2):
    for i,item in enumerate(npyfile):
        if flag_multi_class:
            img = labelVisualize(num_class,COLOR_DICT,item)
# multi class then the image is colored, non-multi class (two classes) then it is black and white
        else:
            img=item[:,:,0]
            print(np.max(img),np.min(img))
            img[img>0.5]=1#at this point 1 is a floating point number, the following 0 is also
            img[img<=0.5]=0
            print(np.max(img),np.min(img))
        io.imsave(os.path.join(save_path,"%d_predict.png"%i),img)

以下はmodel.pyです。

import numpy as np 
import os
import skimage.io as io
import skimage.transform as trans
import numpy as np
from keras.models import *
from keras.layers import *
from keras.optimizers import *
from keras.callbacks import ModelCheckpoint, LearningRateScheduler
from keras import backend as keras


def unet(pretrained_weights = None,input_size = (256,256,1)):
    inputs = Input(input_size)
    conv1 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(inputs)
    conv1 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv1)
    pool1 = MaxPooling2D(pool_size=(2, 2))(conv1)
    conv2 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool1)
    conv2 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv2)
    pool2 = MaxPooling2D(pool_size=(2, 2))(conv2)
    conv3 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool2)
    conv3 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv3)
    pool3 = MaxPooling2D(pool_size=(2, 2))(conv3)
    conv4 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool3)
    conv4 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv4)
    drop4 = Dropout(0.5)(conv4)
    pool4 = MaxPooling2D(pool_size=(2, 2))(drop4)

    conv5 = Conv2D(1024, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool4)
    conv5 = Conv2D(1024, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv5)
    drop5 = Dropout(0.5)(conv5)

    up6 = Conv2D(512, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(drop5))# upsampling followed by convolution, equivalent to transpose the convolution operation!
    merge6 = concatenate([drop4,up6],axis=3)
    conv6 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge6)
    conv6 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv6)

    up7 = Conv2D(256, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(conv6))
    merge7 = concatenate([conv3,up7],axis = 3)
    conv7 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge7)
    conv7 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv7)

    up8 = Conv2D(128, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(conv7))
    merge8 = concatenate([conv2,up8],axis = 3)
    conv8 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge8)
    conv8 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv8)

    up9 = Conv2D(64, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(conv8))
    merge9 = concatenate([conv1,up9],axis = 3)
    conv9 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge9)
    conv9 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv9)
    conv9 = Conv2D(2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv9)
    conv10 = Conv2D(1, 1, activation = 'sigmoid')(conv9) # I suspect that this sigmoid activation function is redundant, because it is the binary cross-entropy that is used in the later loss, which contains the sigmoid

    model = Model(input = inputs, output = conv10)

    model.compile(optimizer = Adam(lr = 1e-4), loss = 'binary_crossentropy', metrics = ['accuracy']) # model must be compiled before execution https://keras-cn.readthedocs.io/ en/latest/getting_started/sequential_model/
    #using binary cross-entropy, also known as sigmoid cross-entropy, metrics is generally chosen for accuracy, it will make the accuracy go higher
    #model.summary()

    if(pretrained_weigh

これで終了です。

テストの結果を見てください:。

1. テストの出力は256*256ですが、入力は512*512であることがわかると思います。これは、入力が一律に、256*256にリサイズされているためです。

2. もう一つの違いは、論文にあるモデル通りに作成されていないことです。具体的な違いとしては、ここでは各畳み込みにpadding=sameを使用していますが、論文ではpaddingされていないため、入力サイズと出力サイズが同じになるのに対し、論文では入力が出力より大きくなっています。具体的な をご覧ください。

ディープラーニングやAIに興味がある方は、私が作ったグループに参加してみてください 825524664 (ディープラーニング交流会)、学習と交流のためだけ、広告なし、よろしくお願いします

オールブラック、オールグレーの解決策。

1. python3 で実行してみる

2.img/255のところを全部img/255.0に変えてみる。