イメージセグメンテーション:詳細ガイド
イメージセグメンテーションの詳細ガイド' (Imēji segumentēshon no shōsai gaido)
画像中の異なる種類のオブジェクトをコンピュータが識別する方法は?ステップバイステップのガイド。
目次
関連リンク
- Kaggleノートブックの使用例(おすすめ;Kaggleの競技に登録し、ノートブックをコピーして無料のGPUを使ってください)
- Colabノートブック(データセットへのアクセスを得るためにKaggleの競技に登録が必要です)
- Carvana Image Masking Challenge Kaggle Competition
はじめに、モチベーション
画像セグメンテーションとは、コンピュータ(正確にはコンピュータに保存されたモデル)が画像を取り込み、画像の各ピクセルを対応するカテゴリに割り当てる能力を指します。例えば、上記に示された白いフェンスの前の猫の画像を画像セグメンターションにかけることができ、セグメンテーションされた画像が出力されます:
この例では、私が手作業で画像をセグメンテーションしました。これは手間のかかる操作であり、自動化したいと考えています。このガイドでは、画像セグメンテーションのアルゴリズムを訓練するプロセスを詳しく説明します。インターネットや教科書の多くのガイドはある程度役立ちますが、実装の詳細なディテールには触れていません。ここでは、実装する際に時間を節約するため、可能な限り詳細に説明します。
まず、私たちは機械学習の広いコンテキストに私たちのタスクを置いてみましょう。機械学習の定義は自明です:私たちは機械に問題を解決する方法を学ぶように教えています。自動化したい多くの問題がありますが、この記事ではコンピュータビジョンの一部の問題に焦点を当てます。コンピュータビジョンは、コンピュータに見る方法を教えることを目指しています。6歳の子供に白い柵の前に座っている猫の画像を与え、その画像を「猫」のピクセルと「背景」のピクセルにセグメント化するように頼むのは簡単です(もちろん、困惑した子供に「セグメント」という言葉の意味を説明した後で)。しかし、数十年にわたって、コンピュータはこの問題に苦しんできました。
なぜコンピュータは6歳の子供ができることができないのでしょうか?私たちは、視覚障害者が点字を通じて読む方法を考えることによって、コンピュータと共感することができます。点字で書かれたエッセイを手渡され、それを読む方法を全く知識がないと仮定してみましょう。どのように進めますか?点字を英語に解読するためには何が必要ですか?
必要なのは、この入力をあなたにとって理解可能な出力に変換する方法です。数学では、これをマッピングと呼びます。私たちは、私たちの理解ができない入力xから理解できる出力yに変換する関数f(x)を学びたいと言います。
長期の練習と良いチューターを持てば、誰でも点字から英語への必要なマッピングを学ぶことができます。類推によると、画像を処理するコンピュータは、初めて点字に出会うようなものです。まるで無駄なもののように見えます。コンピュータは、ピクセルに対応する数値の束を画像をセグメント化するのに使用できる何かに変換するための必要なマッピングf(x)を学ばなければなりません。そして残念ながら、コンピュータモデルは数千年の進化、生物学、そして世界を見るための経験を持っていません;プログラムを起動するときに実質的に「生まれる」のです。 これが私たちがコンピュータビジョンのモデルに教えたいことです。
まず、なぜ最初に画像セグメンテーションを行いたいのでしょうか?その一つはZoomです。多くの人々は、ビデオ会議中に同僚にリビングルームで犬が手立てを行っている様子を見せたくないため、仮想的な背景を使用することを好みます。画像セグメンテーションは、このタスクには欠かせません。もう一つの強力なユースケースは医療画像です。患者の臓器のCTスキャンを撮影する際、アルゴリズムが画像で臓器を自動的にセグメント化することは、医療専門家が怪我や腫瘍の存在などを判断するのに役立つかもしれません。 こちらはこのタスクに重点を置いたKaggleのコンペティションの素晴らしい例です。
画像セグメンテーションには、単純なものから複雑なものまで、さまざまな種類があります。この記事では、最も簡単な画像セグメンテーションであるバイナリセグメンテーションに焦点を当てます。つまり、オブジェクトのクラスが2つだけ存在します。「猫」と「背景」などです。
ここで提示するコードは、明瞭さのために若干再配置および編集されています。動作するコードを実行するには、記事の先頭にあるコードリンクを参照してください。KaggleのCarvana Image Masking Challengeデータセットを使用します。このチャレンジにサインアップしてデータセットにアクセスし、ColabノートブックにKaggle APIキーを入力して動作させる必要があります(Kaggleのノートブックを使用しない場合)。 詳細については、このディスカッション投稿をご覧ください。
もう一つ。このコードのすべてのアイデアの詳細について詳しく説明したいと思っても、畳み込みニューラルネットワーク、最大プーリング層、密結合層、ドロップアウト層、残余コネクタに関する作業知識があると想定します。これらの概念について詳しく議論するには、新しい記事が必要であり、この記事の範囲外です。私たちは実装の詳細に焦点を当てています。
データの抽出
この記事に関連するデータは、以下のフォルダに格納されます:
- train_hq.zip:車の高品質トレーニング画像が含まれるフォルダ
- test_hq.zip:車の高品質テスト画像が含まれるフォルダ
- train_masks.zip:トレーニングセットのマスクが含まれるフォルダ
画像セグメンテーションの文脈では、マスクはセグメンテーションされた画像です。入力画像を出力セグメンテーションマスクにマッピングする方法をモデルに学習させようとしています。通常、真のマスク(またはグラウンドトゥルース)は、人間の専門家によって手書きされたものとされます。
最初のステップは、/kaggle/inputソースからフォルダを解凍することです:
def getZippedFilePaths(): zip_file_names = [] for dirname, _, filenames in os.walk('/kaggle/input'): for filename in filenames: if filename.split('.')[-1] == 'zip': zip_file_names.append((os.path.join(dirname, filename))) return zip_file_nameszip_file_names = getZippedFilePaths()items_to_remove = ['/kaggle/input/carvana-image-masking-challenge/train.zip', '/kaggle/input/carvana-image-masking-challenge/test.zip'] zip_file_names = [item for item in zip_file_names if item not in items_to_remove]for zip_file_path in zip_file_names: with zipfile.ZipFile(zip_file_path, 'r') as zip_ref: zip_ref.extractall()
このコードは、入力中のすべての.zipファイルのファイルパスを取得し、それらを/kaggle/outputディレクトリに展開します。非高品質の写真は意図的に展開しないことに注意してください。Kaggleのリポジトリは、20 GBのデータを保持できるだけであり、このステップはこの制限を超えないために必要です。
画像の可視化
ほとんどのコンピュータビジョンの問題では、データセットを観察することが最初のステップです。具体的には何を扱っているのでしょうか?まず、画像を表示するためにデータセットを整理する必要があります。(このガイドではTensorFlowを使用しますが、PyTorchへの変換はそれほど難しくありません。)
# すべてのパス名をソートされたリストに追加train_hq_dir = '/kaggle/working/train_hq/'train_masks_dir = '/kaggle/working/train_masks/'test_hq_dir = '/kaggle/working/test_hq/'X_train_id = sorted([os.path.join(train_hq_dir, filename) for filename in os.listdir(train_hq_dir)], key=lambda x: x.split('/')[-1].split('.')[0])y_train = sorted([os.path.join(train_masks_dir, filename) for filename in os.listdir(train_masks_dir)], key=lambda x: x.split('/')[-1].split('.')[0])X_test_id = sorted([os.path.join(test_hq_dir, filename) for filename in os.listdir(test_hq_dir)], key=lambda x: x.split('/')[-1].split('.')[0])X_train_id = X_train_id[:1000]y_train = y_train[:1000]X_train, X_val, y_train, y_val = train_test_split(X_train_id, y_train, test_size=0.2, random_state=42)# ファイルパスのリストからDatasetオブジェクトを作成X_train = tf.data.Dataset.from_tensor_slices(X_train)y_train = tf.data.Dataset.from_tensor_slices(y_train)X_val = tf.data.Dataset.from_tensor_slices(X_val)y_val = tf.data.Dataset.from_tensor_slices(y_val)X_test = tf.data.Dataset.from_tensor_slices(X_test_id)img_height = 96img_width = 128num_channels = 3img_size = (img_height, img_width)# 前処理の適用X_train = X_train.map(preprocess_image)y_train = y_train.map(preprocess_target)X_val = X_val.map(preprocess_image)y_val = y_val.map(preprocess_target)X_test = X_test.map(preprocess_image)# データフレームオブジェクトにラベルを追加(ワンホットエンコード)train_dataset = tf.data.Dataset.zip((X_train, y_train))val_dataset = tf.data.Dataset.zip((X_val, y_val))# バッチサイズをデータセットに適用BATCH_SIZE = 32batched_train_dataset = train_dataset.batch(BATCH_SIZE)batched_val_dataset = val_dataset.batch(BATCH_SIZE)batched_test_dataset = X_test.batch(BATCH_SIZE)# プリフェッチのためのオートチューンの追加AUTOTUNE = tf.data.experimental.AUTOTUNEbatched_train_dataset = batched_train_dataset.prefetch(buffer_size=AUTOTUNE)batched_val_dataset = batched_val_dataset.prefetch(buffer_size=AUTOTUNE)batched_test_dataset = batched_test_dataset.prefetch(buffer_size=AUTOTUNE)
これを詳しく見ていきましょう:
- まず、トレーニングセット、テストセット、およびグラウンドトゥルースマスクの全ての画像のファイルパスのソートされたリストを作成します。これまでの段階では、これらはまだ画像ではありません。画像へのファイルパスのみを見ています。
- 次に、Carvanaデータセットの最初の1000の画像/マスクのファイルパスのみを取得します。これは、計算量を減らし、トレーニングを高速化するために行われます。複数の強力なGPUにアクセスできる場合は、すべての画像を使用してさらに優れたパフォーマンスを発揮することができます。また、トレーニング/検証の分割を80/20に作成します。含めるデータ(画像)が多ければ多いほど、この分割はトレーニングセットに傾斜するべきです。非常に大きなデータセットを取り扱う場合、トレーニング/検証/テストの分割で98/1/1のような分割を見ることは珍しくありません。トレーニングセットのデータが多ければ多いほど、モデルの性能は一般的に向上します。
- 次に、tf.data.Dataset.from_tensor_slices()メソッドを使用してTensorFlow(TF)のDatasetオブジェクトを作成します。Datasetオブジェクトを使用することは、トレーニング、検証、テストセットを処理する一般的な方法であり、それらをNumpy配列として保持する方法とは異なります。私の経験では、Datasetオブジェクトを使用すると、データの前処理がはるかに高速かつ簡単に行えます。こちらのリンクを参照してください。
- 次に、入力画像の画像の高さ、幅、チャンネルの数を指定します。実際の高品質の画像は96ピクセル×128ピクセルよりも大きいですが、この画像の縮小は計算量を減らすために行われます(大きな画像はトレーニングにより多くの時間を要します)。必要な容量(GPU)がある場合、縮小は推奨しません。
- 次に、Datasetオブジェクトの.map()関数を使用して画像の前処理を行います。これにより、ファイルパスが画像に変換され、適切な前処理が行われます。詳細については、後で説明します。
- トレーニングセットの生の画像とグラウンドトゥルースマスクを前処理した後、画像とマスクをペアにする方法が必要です。これを実現するために、Datasetオブジェクトの.zip()関数を使用します。これは、2つのデータリストを取り、各リストの最初の要素をタプルにまとめます。2番目の要素、3番目の要素などについても同様の処理を行います。結果は、(画像、マスク)の形式のタプルで満たされた単一のリストになります。
- 次に、.batch()関数を使用して1000枚の画像から32枚のバッチを作成します。バッチ処理は、1つずつではなく、複数の画像を処理することができるため、機械学習パイプラインの重要な部分です。これによりトレーニングが高速化されます。
- 最後に、.prefetch()関数を使用します。これもトレーニングを高速化するためのステップです。データの読み込みと前処理は、トレーニングパイプラインでボトルネックになります。これにより、アイドル状態のGPUやCPU時間が発生することがあります。モデルが順方向と逆方向の伝播を行っている間、.prefetch()関数は次のバッチを準備します。TensorFlowのAUTOTUNE変数は、システムリソースに基づいて事前取得するバッチの数を動的に計算します。これは一般的に推奨されます。
次に、前処理ステップを詳しく見てみましょう:
def preprocess_image(file_path): # 画像を読み込んでデコードする img = tf.io.read_file(file_path) # 画像のチャンネル数を調整できます(RGBの場合は3) img = tf.image.decode_jpeg(img, channels=3) # uint8で返されます # ピクセル値を[0, 1]に正規化する img = tf.image.convert_image_dtype(img, tf.float32) # 画像を所望のサイズにリサイズする img = tf.image.resize(img, [96, 128], method='nearest') return imgdef preprocess_target(file_path): # 画像を読み込んでデコードする mask = tf.io.read_file(file_path) # 0から1の範囲に正規化する(2つのクラスのみ) mask = tf.image.decode_image(mask, expand_animations=False, dtype=tf.float32) # 3番目のチャンネルの値のみを取得する mask = tf.math.reduce_max(mask, axis=-1, keepdims=True) # 画像を所望のサイズにリサイズする mask = tf.image.resize(mask, [96, 128], method='nearest') return mask
これらの関数は以下のようなことを行います:
- まず、ファイルパスをtf.io.read_file()を使用して’string’のデータ型のテンソルに変換します。テンソルはTensorFlowの特殊なデータ構造であり、他の数学ライブラリの多次元配列に似ていますが、ディープラーニングに役立つ特性とメソッドを持っています。TensorFlowのドキュメントによれば、tf.io.read_file()は「パースを行わず、そのままの内容を返す」とされています。これは、画像の情報を含む文字列型のバイナリファイル(1と0)を返すことを意味します。
- 次に、バイナリデータをデコードする必要があります。これには、TensorFlowで適切なメソッドを使用する必要があります。生の画像データが.jpeg形式であるため、tf.image.decode_jpeg()メソッドを使用します。マスクはGIF形式であるため、tf.io.decode_gif()を使用するか、一般的なtf.image.decode_image()を使用することができます。どちらを選ぶかは重要ではありません。これらは実際にはアニメーションではなく、単なる画像ですので、expand_animations=Falseを設定します。
- そして、convert_image_dtype()を使用して画像データをfloat32に変換します。これは画像に対してのみ行われます(マスクは既にfloat32にデコードされています)。画像処理では2つの一般的なデータ型が使用されます:float32とuint8です。float32はコンピュータメモリ上で32ビットを占める浮動小数点数(10進数)を表します。これらは符号付き(つまり、数値が負になる可能性がある)で、値の範囲は0から2³² =
データセットを整理した後、画像を表示することができます。
# 画像と関連するラベルを表示しますfor images, masks in batched_val_dataset.take(1): car_number = 0 for image_slot in range(16): ax = plt.subplot(4, 4, image_slot + 1) if image_slot % 2 == 0: plt.imshow((images[car_number])) class_name = '画像' else: plt.imshow(masks[car_number], cmap = 'gray') plt.colorbar() class_name = 'マスク' car_number += 1 plt.title(class_name) plt.axis("off")
ここでは、.take()メソッドを使用して、バッチ化されたvalデータセットの最初のバッチを表示しています。バイナリセグメンテーションを行っているため、マスクには0と1の2つの値しか含まれません。マスクimshow()に引数cmap = ‘gray’を追加して、これらの画像をグレースケールで表示するようにpltに伝えています。
シンプルなU-Netモデルの構築
1675年2月5日付の手紙で、アイザック・ニュートンはライバルのロバート・フックに対して次のように述べています:
「もし私が遠くを見ることができたのなら、それは巨人たちの肩の上に立っているからです。」
同じように、我々も画像セグメンテーションのタスクに最適なアーキテクチャを発見した過去の機械学習研究者の肩に乗ることになります。独自のアーキテクチャを試すことも悪い考えではありませんが、我々の先人たちは試行錯誤の末に有効なモデルを見つけるために多くの経験を積み重ねてきました。これらのアーキテクチャは決して完全なものではなく、研究はまだ進行中でより良いアーキテクチャが見つかるかもしれません。
よりよく知られたアーキテクチャの一つはU-Netと呼ばれ、ネットワークのダウンサンプリングとアップサンプリングの部分をU字型として視覚化することができます(下記を参照)。Ronneberger、Fisher、およびBroxの論文「U-Net:バイオメディカル画像セグメンテーション用の畳み込みネットワーク」[1]では、画像セグメンテーションに効果的な完全畳み込みネットワーク(FCN)の作成方法が説明されています。完全畳み込みとは、密結合層はなく、すべての層が畳み込み層であることを意味します。
次のことに注意してください:
- ネットワークは、2つの畳み込み層で構成される一連の繰り返しブロックで構成されており、パディング=’same’およびストライド=1でブロック内の畳み込みの出力が縮小されないようにしています。
- 各ブロックの後には、フィーチャーマップの幅と高さを半分にカットするmaxプーリング層が続きます。
- 次のブロックでは、フィルタの数が倍になります。このパターンは繰り返されます。この特徴空間をカットし、フィルタの数を増やすパターンは、CNNを学習したことがある場合には馴染み深いものです。これが著者が「収縮パス」と呼ぶものです。
- ボトルネック層は‘U’の底部にあります。この層は高度に抽象化された特徴(線、曲線、窓、ドアなど)を捉えますが、空間的な解像度は大幅に低下しています。
- 次に、著者が「拡張パス」と呼ぶものが始まります。これは収縮を逆にしたもので、各ブロックは再び2つの畳み込み層で構成されます。各ブロックの後には、TensorFlowではConv2DTransposeレイヤーと呼ぶアップサンプリングレイヤーが続きます。これにより、小さなフィーチャーマップが高さと幅が2倍になります。
- 次のブロックでは、フィルタの数を半分に減らします。このプロセスを繰り返し、元の画像と同じ高さと幅になるまで続けます。最後に、1×1の畳み込み層でチャンネル数を1に減らします。バイナリセグメンテーションなので、2つのクラスに対応するピクセル値がある1つのフィルタで終わるようにしたいのです。ピクセル値を0から1の間に収めるためにシグモイド活性化関数を使用します。
- U-Netアーキテクチャにはスキップ接続もあり、ダウンサンプリングとアップサンプリングの後も細かい空間情報を保持することができます。通常、このプロセスでは多くの情報が失われます。収縮ブロックから情報を拡張ブロックに渡すことで、この空間情報を保持することができます。アーキテクチャには対称性があります。
簡単なバージョンのU-Netから始めましょう。これはFCNですが、残余接続や最大プーリング層はありません。
data_augmentation = tf.keras.Sequential([ tfl.RandomFlip(mode="horizontal", seed=42), tfl.RandomRotation(factor=0.01, seed=42), tfl.RandomContrast(factor=0.2, seed=42)])def get_model(img_size): inputs = Input(shape=img_size + (3,)) x = data_augmentation(inputs) # コントラクトパス x = tfl.Conv2D(64, 3, strides=2, activation="relu", padding="same", kernel_initializer='he_normal')(x) x = tfl.Conv2D(64, 3, activation="relu", padding="same", kernel_initializer='he_normal')(x) x = tfl.Conv2D(128, 3, strides=2, activation="relu", padding="same", kernel_initializer='he_normal')(x) x = tfl.Conv2D(128, 3, activation="relu", padding="same", kernel_initializer='he_normal')(x) x = tfl.Conv2D(256, 3, strides=2, padding="same", activation="relu", kernel_initializer='he_normal')(x) x = tfl.Conv2D(256, 3, activation="relu", padding="same", kernel_initializer='he_normal')(x) # エキスパンドパス x = tfl.Conv2DTranspose(256, 3, activation="relu", padding="same", kernel_initializer='he_normal')(x) x = tfl.Conv2DTranspose(256, 3, activation="relu", padding="same", kernel_initializer='he_normal', strides=2)(x) x = tfl.Conv2DTranspose(128, 3, activation="relu", padding="same", kernel_initializer='he_normal')(x) x = tfl.Conv2DTranspose(128, 3, activation="relu", padding="same", kernel_initializer='he_normal', strides=2)(x) x = tfl.Conv2DTranspose(64, 3, activation="relu", padding="same", kernel_initializer='he_normal')(x) x = tfl.Conv2DTranspose(64, 3, activation="relu", padding="same", kernel_initializer='he_normal', strides=2)(x) outputs = tfl.Conv2D(1, 3, activation="sigmoid", padding="same")(x) model = keras.Model(inputs, outputs) return modelcustom_model = get_model(img_size=img_size)
ここでは、U-Netと同じ基本的な構造を持っています。コントラクトパスとエキスパンドパスがあります。興味深いことに、フィーチャースペースを半分にするために、最大プーリング層ではなくstrides=2で畳み込み層を使用しています。Cholletによると、これは最大プーリング層よりも空間情報をより保存してフィーチャースペースを半分にすることができます。彼は、場所情報が重要な場合(画像セグメンテーションなど)、破壊的な最大プーリング層を避け、strided convolutionsを使用することが良いアイデアであると述べています(有名なU-Netアーキテクチャ自体では最大プーリングを使用しているため、これは興味深いです)。また、モデルの汎化性を向上させるために、データ拡張も行っています。
いくつかの重要な詳細: ReLUアクティベーションのためのカーネルイニシャライザを’he_normal’に設定することは、トレーニングの安定性の観点から驚くほど大きな違いを生み出します。私は最初、カーネルの初期化の力を過小評価していました。ランダムに重みを初期化する代わりに、he_normalizationでは重みが平均0、標準偏差が(2 / 層への入力ユニットの数の平方根)になるように初期化されます。CNNの場合、入力ユニットの数は前の層のフィーチャーマップのチャンネル数を指します。これにより収束が速くなり、消失勾配が緩和され、学習が改善されることがわかっています。詳細については参考文献[3]を参照してください。
メトリクスと損失関数
バイナリセグメンテーションに使用できる一般的なメトリクスと損失関数はいくつかあります。ここでは、競技で要求されているように、dice coefficientをメトリクスとして使用し、対応するdice lossをトレーニングに使用します。
まず、dice係数の背後にある数学を見てみましょう:
ダイス係数は、2つの集合(XとY)の共通部分を、各集合の合計で割ったものに2を乗じることで定義されます。ダイス係数は、集合が交差しない場合は0(集合が完全に重なる場合は1)の間にある値を持ちます。これが画像セグメンテーションにおいて優れた指標になる理由がわかります。
上記の式は、ダイス係数の一般的な定義です。ベクトル量(集合ではなく)に適用する場合、より具体的な定義を使用します。
ここでは、各マスクの各要素(ピクセル)を反復処理しています。xは予測マスクのithピクセルを表し、yは対応するピクセルを表します。上部では要素ごとの積を行い、下部では各マスクのすべての要素を合計しています。Nはピクセルの総数を表します(予測マスクと対象マスクの両方に対して同じである必要があります)。マスクでのすべての数値は0または1のいずれかであることを覚えておいてください。したがって、対象マスクの値が1であり、予測マスクの対応するピクセルの値が0であるピクセルは、ダイススコアに寄与しません(1 x 0 = 0)。
ダイス損失は、簡単に1からダイススコアを引いたものと定義されます。ダイススコアは0から1の間にあるため、ダイス損失も0から1の間にあります。実際、ダイススコアとダイス損失の合計は1になります。それらは逆の関係にあります。
コードでの実装を見てみましょう。
from tensorflow.keras import backend as Kdef dice_coef(y_true, y_pred, smooth=10e-6): y_true_f = K.flatten(y_true) y_pred_f = K.flatten(y_pred) intersection = K.sum(y_true_f * y_pred_f) dice = (2. * intersection + smooth) / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth) return dicedef dice_loss(y_true, y_pred): return 1 - dice_coef(y_true, y_pred)
ここでは、2つの4次元マスク(バッチ、高さ、幅、チャンネル=1)を1次元ベクトルに変換し、バッチ内のすべての画像に対してダイススコアを計算しています。2つのマスクが重ならない場合に0/0の問題を防ぐため、分子と分母の両方に平滑化値を追加しています。
最後に、トレーニングを開始します。過学習を防ぐために早期停止を行い、最良のモデルを保存しています。
custom_model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001, epsilon=1e-06), loss=[dice_loss], metrics=[dice_coef])callbacks_list = [ keras.callbacks.EarlyStopping( monitor="val_loss", patience=2, ), keras.callbacks.ModelCheckpoint( filepath="best-custom-model", monitor="val_loss", save_best_only=True, )]history = custom_model.fit(batched_train_dataset, epochs=20, callbacks=callbacks_list, validation_data=batched_val_dataset)
以下のコードでトレーニングの結果を確認できます。
def display(display_list): plt.figure(figsize=(15, 15)) title = ['入力画像', '正解マスク', '予測マスク'] for i in range(len(display_list)): plt.subplot(1, len(display_list), i+1) plt.title(title[i]) plt.imshow(tf.keras.preprocessing.image.array_to_img(display_list[i])) plt.axis('off') plt.show() def create_mask(pred_mask): mask = pred_mask[..., -1] >= 0.5 pred_mask[..., -1] = tf.where(mask, 1, 0) # バッチの最初のマスクのみを返す return pred_mask[0]def show_predictions(model, dataset=None, num=1): """ 最初のnumバッチの各イメージを表示 """ if dataset: for image, mask in dataset.take(num): pred_mask = model.predict(image) display([image[0], mask[0], create_mask(pred_mask)]) else: display([sample_image, sample_mask, create_mask(model.predict(sample_image[tf.newaxis, ...]))])custom_model = keras.models.load_model("/kaggle/working/best-custom-model", custom_objects={'dice_coef': dice_coef, 'dice_loss': dice_loss})show_predictions(model = custom_model, dataset = batched_train_dataset, num = 6)
10エポック後、最高の検証ダイススコアは0.8788に達しました。それほど悪くはありませんが、素晴らしいわけでもありません。P100 GPUで約20分かかりました。以下にレビュー用のサンプルマスクを掲載します:
興味深いポイントをいくつか強調しておきます:
- create_maskは、ピクセル値を0または1に変換する関数です。0.5未満のピクセル値は0にカットされ、「背景」カテゴリのピクセルに割り当てられます。0.5以上の値は1に増加し、「車」カテゴリのピクセルに割り当てられます。
- なぜマスクが黄色と紫になってしまったのでしょうか?我々は、tf.keras.preprocessing.image.array_to_img()を使用して、マスクの出力をテンソルからPILイメージに変換しました。次に、そのイメージをplt.imshow()に渡しました。ドキュメンテーションによると、単一チャンネルのイメージのデフォルトのカラーマップは「viridis」です(3チャンネルのRGBイメージはそのまま出力されます)。viridisカラーマップは、低い値を濃い紫に、高い値を黄色に変換します。このカラーマップは、色覚異常のある人々が画像の色を正確に見るのに役立つとされています。cmap=”grayscale”を引数として追加することで、この問題を修正できましたが、入力画像が狂ってしまう可能性があります。詳細はこちらをご覧ください。
完全なU-Netの構築
ここでは、完全なU-Netアーキテクチャを使用して、残差接続、最大プーリング層を含み、正則化のためにドロップアウト層も含めます。コントラクティングパス、ボトルネック層、エクスパンディングパスに注目してください。ドロップアウト層は、各ブロックの末尾に追加することができます。
def conv_block(inputs=None, n_filters=64, dropout_prob=0, max_pooling=True): conv = Conv2D(n_filters, 3, activation='relu', padding='same', kernel_initializer='he_normal')(inputs) conv = Conv2D(n_filters, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv) if dropout_prob > 0: conv = Dropout(dropout_prob)(conv) if max_pooling: next_layer = MaxPooling2D(pool_size=(2, 2))(conv) else: next_layer = conv skip_connection = conv return next_layer, skip_connectiondef upsampling_block(expansive_input, contractive_input, n_filters=64): up = Conv2DTranspose( n_filters, 3, strides=(2, 2), padding='same', kernel_initializer='he_normal')(expansive_input) # The previous output and the contractive_input are merged merge = concatenate([up, contractive_input], axis=3) conv = Conv2D(n_filters, 3, activation='relu', padding='same', kernel_initializer='he_normal')(merge) conv = Conv2D(n_filters, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv) return convdef unet_model(input_size=(96, 128, 3), n_filters=64, n_classes=1): inputs = Input(input_size) inputs = data_augmentation(inputs) # Contracting Path (encoding) cblock1 = conv_block(inputs, n_filters) cblock2 = conv_block(cblock1[0], n_filters*2) cblock3 = conv_block(cblock2[0], n_filters*4) cblock4 = conv_block(cblock3[0], n_filters*8, dropout_prob=0.3) # Bottleneck Layer cblock5 = conv_block(cblock4[0], n_filters*16, dropout_prob=0.3, max_pooling=False) # Expanding Path (decoding) ublock6 = upsampling_block(cblock5[0], cblock4[1], n_filters*8) ublock7 = upsampling_block(ublock6, cblock3[1], n_filters*4) ublock8 = upsampling_block(ublock7, cblock2[1], n_filters*2) ublock9 = upsampling_block(ublock8, cblock1[1], n_filters) conv9 = Conv2D(n_filters, 3, activation='relu', padding='same', kernel_initializer='he_normal')(ublock9) conv10 = Conv2D(n_classes, 1, padding='same', activation="sigmoid")(conv9) model = tf.keras.Model(inputs=inputs, outputs=conv10) return model
次に、U-Netをコンパイルします。最初の畳み込みブロックには64のフィルタを使用しています。これは、最適な結果を得るために調整したいハイパーパラメータです。
unet = unet_model(input_size=(img_height, img_width, num_channels), n_filters=64, n_classes=1)unet.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001, epsilon=1e-06), loss=[dice_loss], metrics=[dice_coef])callbacks_list = [ keras.callbacks.EarlyStopping( monitor="val_loss", patience=2, ), keras.callbacks.ModelCheckpoint( filepath="best-u_net-model", monitor="val_loss", save_best_only=True, )]history = unet.fit(batched_train_dataset, epochs=20, callbacks=callbacks_list, validation_data=batched_val_dataset)
16エポック後、バリデーションダイススコアは0.9416になりました。これは、シンプルなU-Netよりもはるかに良い結果です。これはあまり驚くことではありません。パラメータ数を見ると、シンプルなU-Netから完全なU-Netへの桁数の増加があります。P100 GPUでは約32分かかりました。それでは、予測をのぞいてみましょう:
unet = keras.models.load_model("/kaggle/working/best-u_net-model", custom_objects={'dice_coef': dice_coef, 'dice_loss': dice_loss})show_predictions(model = unet, dataset = batched_train_dataset, num = 6)
これらの予測はかなり良くなりました。複数の予測を見ると、車から突き出ているアンテナはネットワークにとって難しいものです。画像が非常にピクセル化されているため、この点を見落とすことはネットワークのせいではありません。
パフォーマンスを向上させるためには、以下のようなハイパーパラメータを調整することになります:
- ダウンサンプリングとアップサンプリングブロックの数
- フィルタの数
- 画像の解像度
- トレーニングセットのサイズ
- 損失関数(ダイス損失をバイナリ交差エントロピー損失と組み合わせるなど)
- オプティマイザのパラメータを調整する。トレーニングの安定性は両方のモデルにとって問題のようです。Adamオプティマイザのドキュメントによると、「epsilon」のデフォルト値である1e-7は一般的には適切なデフォルトではないかもしれません。「epsilon」を桁数以上大きくすることで、トレーニングの安定性が向上するかもしれません。
Carvanaのチャレンジで優れたスコアを達成するための道筋が既に見えています。残念なことに、チャレンジは既に終了しています!
まとめ
この記事では、画像セグメンテーション、特に二値セグメンテーションについて深く掘り下げました。何かを学ぶなら、次のことを覚えてください:
- 画像セグメンテーションの目標は、画像の入力ピクセル値からモデルが各ピクセルにクラスを割り当てるために使用できる出力数にマッピングすることです。
- 最初のステップの一つは、画像をTensorFlowのDatasetオブジェクトに整理し、画像と対応するマスクを確認することです。
- モデルのアーキテクチャを再発明する必要はありません。U-Netがうまく機能することは経験から知られています。
- ダイススコアは、モデルの予測の成功を監視するために使用される一般的なメトリックです。私たちの損失関数もこれから取得できます。
将来の作業では、カノニカルなU-Netアーキテクチャのマックスプーリングレイヤーをストライド畳み込みレイヤーに変換することができます。
画像セグメンテーションの問題で成功を収めるために、幸運を祈ります!
参考文献
[1] O. Ronneberger, P. Fischer, and T. Brox, U-Net: Convolutional Networks for Biomedical Image Segmentation (2015), MICCAI 2015 International Conference
[2] F. Chollet, Deep Learning with Python (2021), Manning Publications Co.
[3] K. He、X. Zhang、S. Ren、J. Sun、Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification(2015)、国際コンピュータビジョンカンファレンス(ICCV)
We will continue to update VoAGI; if you have any questions or suggestions, please contact us!
Was this article helpful?
93 out of 132 found this helpful
Related articles