「RetinaNetとKerasCVを使用した物体検出」

「RetinaNetとKerasCVを活用した物体検出術」(RetinaNetとKerasCVをかつようしたぶったいけんしゅつじゅつ)

KerasCVライブラリのパワーとシンプリシティを活用したオブジェクト検出

植物の葉の画像。DALL·E 2で作成。

目次

  1. 待ってください、KerasCVとは何ですか?
  2. データの検査
  3. 画像の前処理
  4. RetinaNetモデルの背景
  5. RetinaNetのトレーニング
  6. 予測の作成
  7. 結論
  8. 参考文献

関連リンク

  • Kaggleノートブックの利用:ノートブックをコピーし、コードを試し、無料のGPUを使用してください。
  • PlantDocデータセット:このノートブックで使用されたデータセットで、Roboflowでホストされています。このデータセットはCC BY 4.0 DEEDライセンスの元で公開されており、VoAGIの目的のために、商業利用を含め、どの形式でも材料をコピーまたは再配布することができます。

待ってください、KerasCVとは何ですか?

画像セグメンテーションに基づいたミニプロジェクトを終えて(こちらを参照)、コンピュータビジョンの一環として、別の一般的なタスクであるオブジェクト検出に取り掛かりました。オブジェクト検出とは、画像を取り、興味のあるオブジェクトの周りにボックスを生成し、そのボックスの中に含まれるオブジェクトを分類することを指します。簡単な例として、以下の画像をご覧ください:

オブジェクト検出の例。バウンディングボックスとクラスラベルに注意してください。画像は著者によるものです。

青色のボックスはバウンディングボックスと呼ばれ、その上部にクラス名が表示されています。オブジェクト検出は、次の2つの小さな問題に分けることができます:

  1. モデルがボックスの左上隅と右下隅のxおよびy座標を予測する回帰問題。
  2. モデルがボックスが観測しているオブジェクトのクラスを予測する分類問題。

この例では、バウンディングボックスは人間によって作成およびラベル付けされました。このプロセスを自動化したいと考えており、十分にトレーニングされたオブジェクト検出モデルはその役割を果たすことができます。

オブジェクト検出に関する学習教材を見直そうとして、私はすぐに失望しました。残念ながら、ほとんどの初級教材にはオブジェクト検出についてほとんど触れられていません。フランソワ・ショレーは『Deep Learning with Python』[1]で次のように述べています:

注意してください。この入門書では、オブジェクト検出については取り上げません。それは専門的すぎて複雑すぎるからです。

Aurélion Géron氏[2]は、オブジェクト検出の背後にあるアイデアについて多くのテキストコンテンツを提供していますが、ダミーのバウンディングボックスを持つオブジェクト検出タスクをカバーするコードは、私が探しているエンドツーエンドのパイプラインから遠く離れています。 Andrew Ng氏の有名なDeep Learning Specializationコースは、オブジェクト検出について最も詳しく解説していますが、コーディングラボでは事前に訓練されたオブジェクト検出モデルをロードして推論するだけで終わってしまいます。

もっと深く掘り下げるために、私はオブジェクト検出パイプラインの概要をスケッチし始めました。 RetinaNetモデルの前処理だけでも、以下の手順が必要です(注:YOLOなどの他のオブジェクト検出モデルには異なる手順が必要です):

  • 入力画像をすべて同じサイズにリサイズし、アスペクト比が狂わないようにパディングします。ああ、バウンディングボックスを忘れないでください。これらも適切に再形成する必要があります。さもないとデータが壊れます。
  • トレーニングセットの正解バウンディングボックスに基づいて、さまざまなスケールとアスペクト比のアンカーボックスを生成します。これらのアンカーボックスはトレーニング中におけるモデルの参照点として機能します。
  • アンカーボックスと正解ボックスのオーバーラップに基づいて、アンカーボックスにラベルを割り当てます。高いオーバーラップを持つアンカーボックスは正例としてラベル付けされ、低いオーバーラップを持つアンカーボックスは負例としてラベル付けされます。
  • 同じバウンディングボックスを説明するための複数の方法があります。これらの異なる形式間の変換に関する関数を実装する必要があります。これについては後で詳しく説明します。
  • データ拡張を実装し、画像だけでなくボックスも拡張する必要があります。理論的にはこれを省略することもできますが、モデルがよく一般化するためにはこれが必要です。

このKerasのウェブサイトの例をご覧ください。 ひえっ。モデルの予測のポストプロセッシングにはさらに多くの作業が必要です。Kerasチームの言葉を借りると、これは技術的に複雑な問題です。

私が絶望し始めたとき、必死にインターネットを検索していると、これまで聞いたことのないライブラリに偶然出くわしました:KerasCV。ドキュメントを読みながら、これがTensorFlow/Kerasでのコンピュータビジョンの未来であることがわかってきました。彼らの紹介文によると:

KerasCVは、Keras APIの水平拡張として理解することができます。それらは、コアKerasに追加するには特化しすぎた第一党のKerasオブジェクトです。これらは、コアKeras APIと同じレベルの洗練度と後方互換性の保証を受け、Kerasチームによってメンテナンスがされています。

「なぜ私の学習教材のどれもこれを言及していないのか?」と私は思いました。その答えは簡単です:これはかなり新しいライブラリなのです。GitHubでの最初のコミットは2022年4月13日であり、私の教科書の最新版にさえ表示されるには新しすぎます。実際、ライブラリの1.0バージョンはまだリリースされていません(2023年11月10日時点ではバージョン0.6.4です)。私は次の教科書やオンラインコースでKerasCVについて詳細に説明されることを期待しています(公正を期すために、ジェロンは「新しいKeras NLPプロジェクト」とKeras CVプロジェクトについて一言触れています)。

KerasCVは非常に新しいため、Kerasチーム自身による公開チュートリアル以外にはまだ多くのチュートリアルがありません(こちらを参照してください)。このチュートリアルでは、公式のKerasガイドとは異なる手法を用いて、健康な葉と病気の葉を認識するためのエンドツーエンドのオブジェクト検出パイプラインをデモンストレーションします。 KerasCVを使えば、初心者でもラベル付きのデータセットを使用して効果的なオブジェクト検出パイプラインを構築することができます。

始める前の注意点数点あります。KerasCVは頻繁にコードベースとドキュメンテーションが更新される、変動の激しいライブラリです。ここで示した実装はKerasCVバージョン0.6.4で動作します。Kerasチームは「KerasCVがv1.0.0に達するまで後方互換性の契約はない」と述べています。これは、このチュートリアルで使用されるメソッドがKerasCVの更新に伴って引き続き動作する保証がないことを意味します。このリンクされたKaggleノートブックでKerasCVバージョン番号をハードコーディングし、この種の問題を防ぎます。

KerasCVにはGitHubのIssuesタブで既にいくつかのバグが報告されています。また、ドキュメンテーションにはいくつかの欠点があります(MultiClassNonMaxSuppression、あなたを見ています)。KerasCVを試しているときにこれらの問題に挫折しないでください。実際、これはKerasCVのコードベースへの貢献者になる絶好の機会です!このチュートリアルでは、KerasCVの実装の詳細に焦点を当てます。オブジェクト検出の高レベルな概念を簡単に説明しますが、読者にはRetinaNetアーキテクチャなどの概念についての背景知識があることを前提としています。ここに示されているコードは、わかりやすさのために編集され、再配置されたものです。完全なコードについては、上記のKaggleノートブックを参照してください。

最後に、安全に関する注意事項です。ここで作成されたモデルは最先端のものではありません。これは高レベルのチュートリアルとして扱ってください。この植物病害検出モデルを実際のプロダクションに実装する前に、さらなる微調整とデータのクリーニングが必要となるでしょう。モデルが行う予測を、診断を確認するために人間の専門家に審査してもらうことは良いアイデアです。

データの検査

PlantDocデータセットには、13の植物種と30のクラスを含む2,569枚の画像が含まれています。このデータセットの目標は、Singh et. al[4]の論文「PlantDoc: A Dataset for Visual Plant Disease Detection」の要約に記載されています。

インドでは、植物病害による年間作物収量の35%が失われています。植物病害の早期発見は、実験室のインフラストラクチャと専門知識の不足により困難となっています。本論文では、スケーラブルで早期の植物病害検出のためのコンピュータビジョン手法の可能性を探索します。

これは立派な目標であり、コンピュータビジョンが農民にとって多くの利益をもたらす可能性のある領域です。

Roboflowでは、さまざまな形式でデータセットをダウンロードすることができます。今回はTensorFlowを使用しているので、データセットをTFRecord形式でダウンロードしましょう。TFRecordは、大量のデータを効率的に保存するために設計されたTensorFlowの特定の形式です。データはレコードのシーケンスで表され、各レコードはキーと値のペアです。各キーは機能と呼ばれます。ダウンロードされたZIPファイルには、トレーニング用とバリデーション用の2つのファイルが含まれています:

  • leaves_label_map.pbtxt:これはプロトコルバッファテキスト形式のファイルで、データの構造を記述するために使用されます。テキストエディタでファイルを開くと、30のクラスがあることがわかります。 Apple leafなどの健康な葉と、 Apple Scab Leafなどの病気の葉などが混在しています。
  • leaves.tfrecord:これはすべてのデータが含まれるTFRecordファイルです。

最初のステップは、leaves.tfrecordを検査することです。レコードにどのようなフィーチャーが含まれていますか?残念ながら、Roboflowではこれが指定されていません。

train_tfrecord_file = '/kaggle/input/plants-dataset/leaves.tfrecord'val_tfrecord_file = '/kaggle/input/plants-dataset/test_leaves.tfrecord'# Create a TFRecordDatasettrain_dataset = tf.data.TFRecordDataset([train_tfrecord_file])val_dataset = tf.data.TFRecordDataset([val_tfrecord_file])# Iterate over a few entries and print their content. Uncomment this to look at the raw datafor record in train_dataset.take(1):  example = tf.train.Example()  example.ParseFromString(record.numpy())  print(example)

次のフィーチャーが表示されていることに気づきます:

  • image/encoded:これは画像のエンコードされたバイナリ表現です。このデータセットの場合、画像はJPEG形式でエンコードされています。
  • image/height:各画像の高さです。
  • image/width:各画像の幅です。
  • image/object/bbox/xmin:バウンディングボックスの左上のx座標です。
  • image/object/bbox/xmax:バウンディングボックスの右下のx座標です。
  • image/object/bbox/ymin:バウンディングボックスの左上のy座標です。
  • image/object/bbox/ymax:バウンディングボックスの右下のy座標です。
  • image/object/class/label:各バウンディングボックスに関連付けられたラベルです。

これで、すべての画像と関連するバウンディングボックスをTensorFlowのDatasetオブジェクトにまとめることができます。データセットオブジェクトを使用すると、システムのメモリを圧倒することなく大量のデータを保存することができます。これは、遅延読み込みバッチ処理などの機能によって実現されます。遅延読み込みは、データが明示的に要求されるまでメモリに読み込まれないことを意味します(例えば、変換やトレーニング中に要求される場合)。バッチ処理では、一度に選択された数の画像(通常は8, 16, 32など)のみがメモリに読み込まれます。要するに、特に大量のデータ(通常はオブジェクト検出で一般的)を扱う場合は、常にデータをDatasetオブジェクトに変換することをお勧めします。

TFRecordをTensorFlowのデータセットオブジェクトに変換するには、tf.data.TFRecordDatasetクラスを使用して、TFRecordファイルからデータセットを作成し、mapメソッドを使用して解析関数を適用して特徴量を抽出および前処理します。解析コードは以下の通りです。

def parse_tfrecord_fn(example):    feature_description = {        'image/encoded': tf.io.FixedLenFeature([], tf.string),        'image/height': tf.io.FixedLenFeature([], tf.int64),        'image/width': tf.io.FixedLenFeature([], tf.int64),        'image/object/bbox/xmin': tf.io.VarLenFeature(tf.float32),        'image/object/bbox/xmax': tf.io.VarLenFeature(tf.float32),        'image/object/bbox/ymin': tf.io.VarLenFeature(tf.float32),        'image/object/bbox/ymax': tf.io.VarLenFeature(tf.float32),        'image/object/class/label': tf.io.VarLenFeature(tf.int64),    }        parsed_example = tf.io.parse_single_example(example, feature_description)    # JPEGイメージをデコードし、ピクセル値を[0, 1]の範囲に正規化します。    img = tf.image.decode_jpeg(parsed_example['image/encoded'], channels=3) # uint8で返されます    # ピクセル値を[0, 256]に正規化します    img = tf.image.convert_image_dtype(img, tf.uint8)    # バウンディングボックスの座標とクラスラベルを取得します。    xmin = tf.sparse.to_dense(parsed_example['image/object/bbox/xmin'])    xmax = tf.sparse.to_dense(parsed_example['image/object/bbox/xmax'])    ymin = tf.sparse.to_dense(parsed_example['image/object/bbox/ymin'])    ymax = tf.sparse.to_dense(parsed_example['image/object/bbox/ymax'])    labels = tf.sparse.to_dense(parsed_example['image/object/class/label'])    # バウンディングボックスの座標をスタックして[num_boxes、4]テンソルを作成します。    rel_boxes = tf.stack([xmin, ymin, xmax, ymax], axis=-1)    boxes = keras_cv.bounding_box.convert_format(rel_boxes, source='rel_xyxy', target='xyxy', images=img)    # 最終的な辞書を作成します。    image_dataset = {        'images': img,        'bounding_boxes': {            'classes': labels,            'boxes': boxes        }    }    return image_dataset

以下に詳細を説明します。

  • feature_description:各特徴量の期待される形式を記述する辞書です。データセットのすべての例で特徴量の長さが一定の場合はtf.io.FixedLenFeature、長さの変動が予想される場合はtf.io.VarLenFeatureを使用します。バウンディングボックスの数はデータセット全体で一定ではないため(一部の画像にはボックスが多く、他の画像には少ない)、バウンディングボックスに関連するものにはtf.io.VarLenFeatureを使用します。
  • 画像ファイルをtf.image.decode_jpegを使用してデコードします。画像はJPEG形式でエンコードされているためです。
  • バウンディングボックスの座標とラベルにtf.sparse.to_denseを使用することに注意してください。tf.io.VarLenFeatureを使用すると、情報は疎行列として返されます。疎行列は、ほとんどの要素がゼロである行列であり、非ゼロの値とそのインデックスだけを効率的に格納するデータ構造です。残念ながら、TensorFlowの多くの前処理関数では、密行列が必要です。これには、複数のバウンディングボックスからの情報を水平にスタックするために使用するtf.stackも含まれます。この問題を修正するために、疎行列を密行列に変換するためにtf.sparse.to_denseを使用します。
  • ボックスをスタックした後、KerasCVのkeras_cv.bounding_box.convert_format関数を使用します。データを調べると、バウンディングボックスの座標が0から1の間で正規化されていることがわかりました。つまり、数字は画像の総幅/高さのパーセンテージを表します。例えば、値が0.5の場合、50% * image_widthです。これはKerasがREL_XYXYと呼ぶ相対フォーマットであり、絶対フォーマットであるXYXYではありません。理論的には、絶対フォーマットに変換する必要はありませんが、私は相対座標でモデルをトレーニングするときにバグに遭遇しました。サポートされている他のバウンディングボックスのフォーマットについては、「KerasCVのドキュメント」を参照してください。
  • 最後に、画像とバウンディングボックスをKerasCVが要求するフォーマットの辞書に変換します。Pythonの辞書はキーと値のペアを含むデータ型です。具体的には、KerasCVは次のフォーマットを期待しています:
image_dataset = {  "images": [width, height, channels],  bounding_boxes = {    "classes": [num_boxes],    "boxes": [num_boxes, 4]  }}

これは実際には「辞書の中の辞書」です。なぜなら、bounding_boxesも辞書だからです。

最後に、.map関数を使用してTFRecordに解析関数を適用します。その後、Datasetオブジェクトを検査することができます。すべて正しく動作しています。

train_dataset = train_dataset.map(parse_tfrecord_fn)val_dataset = val_dataset.map(parse_tfrecord_fn)# データの検査for data in train_dataset.take(1):    print(data)

おめでとうございます、これで最も困難な部分は終わりました。 KerasCVが必要とする「辞書の中の辞書」を作成することは、私の意見では最も難しい作業です。残りの部分は比較的簡単です。

画像の前処理

データは既にトレーニングセットと検証セットに分割されていますので、まずはデータセットをバッチ化します。

# バッチサイズBATCH_SIZE = 32# プリフェッチのためのautotuneの追加AUTOTUNE = tf.data.experimental.AUTOTUNEtrain_dataset = train_dataset.ragged_batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)val_dataset = val_dataset.ragged_batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)NUM_ROWS = 4NUM_COLS = 8IMG_SIZE = 416BBOX_FORMAT = "xyxy"

いくつかの注意点:

  • 同じ数のバウンディングボックスがあると分かっている場合は、batchを使用する代わりに、ragged_batchを使用しています。なぜなら、VarLenFeatureと同じ理由です。
  • BBOX_FORMAT=“xyxy”に設定しています。前にデータをロードするとき、相対XYXY形式から絶対XYXY形式にバウンディングボックスの形式を変換しました。

それではデータ拡張を実装できます。データ拡張はコンピュータビジョンの問題でよく使われる技術です。トレーニング画像をわずかに変更します。たとえば、わずかな回転、画像の水平反転などです。これによりデータが不足している問題を解決し、正則化にも役立ちます。ここでは以下の拡張を導入します:

  • KerasCVのJitteredResize関数。この関数はオブジェクト検出パイプライン向けに設計されており、ランダムにスケーリング、リサイズ、クロップ、およびパディングされた画像と対応するバウンディングボックスを実装する画像拡張手法を提供します。このプロセスにより、スケールとローカルな特徴の変動性が導入され、改善された一般化のためのトレーニングデータの多様性が向上します。
  • 次に、水平および垂直のRandomFlips、およびRandomRotationを追加します。ここでのfactorは2πの割合を表す浮動小数点数です。ここでは.25を使用しており、オーグメンターは画像を-25%の2πから25%の2πまでの数値で回転させます。これは度数で言うと-45°から45°の回転を意味します。
  • 最後に、RandomSaturationRandomHueを追加します。彩度が0.0の場合はグレースケールの画像になり、1.0の場合は完全に彩色された画像になります。0.5のファクターは変更を残さないため、0.4-0.6の範囲を選ぶことで微妙な変化が生じます。色相のファクターが0.0の場合は変化がありません。0.2を指定することで0.0-0.2の範囲が得られます。
augmenter = keras.Sequential(    [        keras_cv.layers.JitteredResize(            target_size=(IMG_SIZE, IMG_SIZE), scale_factor=(0.8, 1.25), bounding_box_format=BBOX_FORMAT        ),        keras_cv.layers.RandomFlip(mode="horizontal_and_vertical", bounding_box_format=BBOX_FORMAT),        keras_cv.layers.RandomRotation(factor=0.25, bounding_box_format=BBOX_FORMAT),        keras_cv.layers.RandomSaturation(factor=(0.4, 0.6)),        keras_cv.layers.RandomHue(factor=0.2, value_range=[0,255])    ])train_dataset = train_dataset.map(augmenter, num_parallel_calls=tf.data.AUTOTUNE)

通常、トレーニングセットの拡張のみを行います。なぜなら、モデルが「パターンを記憶」するのではなく、実世界で見つかる一般的なパターンを学習することが重要だからです。これにより、トレーニング中にモデルが見るものの多様性が増します。

また、バリデーション画像のサイズを同じサイズにリサイズし(パディング付き)、歪みなくリサイズします。バウンディングボックスも応じて再形成する必要があります。KerasCVはこの困難な作業を簡単に処理できます:

# 画像のリサイズとパディングinference_resizing = keras_cv.layers.Resizing(    IMG_SIZE, IMG_SIZE, pad_to_aspect_ratio=True, bounding_box_format=BBOX_FORMAT)val_dataset = val_dataset.map(inference_resizing, num_parallel_calls=tf.data.AUTOTUNE)

最後に、前処理を含めた画像とバウンディングボックスを可視化することができます:

class_mapping = {    1: 'Apple Scab Leaf',    2: 'Apple leaf',    3: 'Apple rust leaf',    4: 'Bell_pepper leaf',    5: 'Bell_pepper leaf spot',    6: 'Blueberry leaf',    7: 'Cherry leaf',    8: 'Corn Gray leaf spot',    9: 'Corn leaf blight',    10: 'Corn rust leaf',    11: 'Peach leaf',    12: 'Potato leaf',    13: 'Potato leaf early blight',    14: 'Potato leaf late blight',    15: 'Raspberry leaf',    16: 'Soyabean leaf',    17: 'Soybean leaf',    18: 'Squash Powdery mildew leaf',    19: 'Strawberry leaf',    20: 'Tomato Early blight leaf',    21: 'Tomato Septoria leaf spot',    22: 'Tomato leaf',    23: 'Tomato leaf bacterial spot',    24: 'Tomato leaf late blight',    25: 'Tomato leaf mosaic virus',    26: 'Tomato leaf yellow virus',    27: 'Tomato mold leaf',    28: 'Tomato two spotted spider mites leaf',    29: 'grape leaf',    30: 'grape leaf black rot'}def visualize_dataset(inputs, value_range, rows, cols, bounding_box_format):    inputs = next(iter(inputs.take(1)))    images, bounding_boxes = inputs["images"], inputs["bounding_boxes"]    visualization.plot_bounding_box_gallery(        images,        value_range=value_range,        rows=rows,        cols=cols,        y_true=bounding_boxes,        scale=5,        font_scale=0.7,        bounding_box_format=bounding_box_format,        class_mapping=class_mapping,    )# トレーニングセットの可視化visualize_dataset(    train_dataset, bounding_box_format=BBOX_FORMAT, value_range=(0, 255), rows=NUM_ROWS, cols=NUM_COLS)# バリデーションセットの可視化visualize_dataset(    val_dataset, bounding_box_format=BBOX_FORMAT, value_range=(0, 255), rows=NUM_ROWS, cols=NUM_COLS)

このような可視化関数は、KerasCVでは一般的です。引数で指定した行と列のグリッドが表示されます。私たちのトレーニング画像はわずかに回転していること、一部は水平または垂直に反転されていること、ズームインまたはズームアウトされていること、そして微妙な色相/彩度の変化が見られることがわかります。ただし、KerasCVのすべての拡張レイヤーでは、必要に応じてバウンディングボックスも拡張されます。

左側が元の画像の例(バリデーションセット)、右側が増強された画像の例(トレーニングセット)。画像は筆者によるものです。

RetinaNetモデルを見る前に最後の事前処理として、データをKerasCVの前処理と互換性のある形式の辞書内の辞書から数値のタプルに変換する必要があります。これは比較的簡単に行えます:

def dict_to_tuple(inputs):    return inputs["images"], bounding_box.to_dense(        inputs["bounding_boxes"], max_boxes=32    )train_dataset = train_dataset.map(dict_to_tuple, num_parallel_calls=tf.data.AUTOTUNE)validation_dataset = val_dataset.map(dict_to_tuple, num_parallel_calls=tf.data.AUTOTUNE)

RetinaNetモデルの背景

オブジェクト検出を行うための人気のあるモデルの1つはRetinaNetと呼ばれています。このモデルの詳細な説明はこの記事の範囲を超えています。簡単に言うと、RetinaNetはシングルステージの検出器であり、画像を1回しか見ずにバウンディングボックスとクラスを予測します。これは有名なYOLO(You Only Look Once)モデルと似ていますが、いくつかの重要な違いがあります。ここで強調したいのは、使用される新しい分類損失関数であるフォーカルロスです。これは画像内のクラスの不均衡の問題を解決します。

なぜこれが重要なのかを理解するために、次の例え話を考えてみましょう:100人の生徒がいる教室の先生だと想像してください。95人の生徒はうるさいほどに騒がしく、いつも叫んで手を挙げています。5人の生徒は静かであまり物を言いません。先生としては全員に平等に注意を払う必要がありますが、騒々しい生徒が静かな生徒を圧倒しています。ここで、クラスの不均衡の問題が発生します。この問題を解決するために、静かな生徒を増強し、うるさい生徒を軽視する特別な補聴器を開発します。この例え話では、うるさい生徒は画像内のほとんどの背景ピクセルで、葉が含まれていない領域であり、静かな生徒は葉が含まれている小さな領域です。補聴器とは、葉を含むピクセルにモデルの焦点を当てることができるフォーカルロスです。葉を含まない領域にあまり注意を払いすぎないで済むようにするのです。

RetinaNetモデルには3つの重要なコンポーネントがあります:

  • バックボーン。これがモデルのベースを形成します。これを特徴抽出器とも呼びます。名前の通り、画像をスキャンして特徴を抽出します。低レベルの層は低レベルの特徴(例:線や曲線)を抽出し、高レベルの層は高レベルの特徴(例:口唇や目)を抽出します。このプロジェクトでは、バックボーンにはCOCOデータセットで事前学習されたYOLOv8モデルを使用します。YOLOは単なる特徴抽出器として使用し、オブジェクト検出器としてではありません。
  • フィーチャーピラミッドネットワーク(FPN)。これは、異なるスケールのオブジェクトを検出するために、低解像度の意味のある特徴と高解像度の意味の薄い特徴をトップダウンの経路と横方向の接続を介して組み合わせて「ピラミッド」の特徴マップを生成するモデルアーキテクチャです。詳しい説明はこのビデオをご覧いただくか、この論文 [5]をご覧ください。
  • 2つのタスク特化のサブネットワーク。これらのサブネットワークはピラミッドの各レベルを取り込み、それぞれのレベルでオブジェクトを検出します。1つのサブネットワークはクラスを識別(分類)し、もう1つのサブネットワークはバウンディングボックスを識別(回帰)します。これらのサブネットワークは未学習です。
Simplified RetinaNet architecture. Image by author.

以前に画像のサイズを416×416に変更しました。これは多少恣意的な選択ですが、選択するオブジェクト検出モデルによっては、望ましい最小サイズが指定されることもよくあります。使用しているYOLOv8のバックボーンでは、画像サイズは32で割り切れる必要があります。これは、バックボーンの最大ストライドが32であり、完全畳み込みネットワークであるためです。自分のプロジェクトではこの因子を調べるために使用するモデルについての詳細な情報を調べましょう。

RetinaNetのトレーニング

基本的なパラメータを設定することから始めましょう。オプティマイザと使用するメトリックスです。ここでは、オプティマイザとしてAdamを使用します。 global_clip_norm 引数に注意してください。 KerasCVの物体検出ガイドによると:

物体検出モデルの学習時には、常に global_clipnorm を含める必要があります。これは、物体検出モデルの学習時に頻繁に発生する勾配爆発の問題を修正するためです。

base_lr = 0.0001# 物体検出タスクにおいて global_clipnorm を含めることは非常に重要です。optimizer_Adam = tf.keras.optimizers.Adam(learning_rate=base_lr, global_clipnorm=10.0)

彼らのアドバイスに従いましょう。メトリックスには BoxCOCOMetricsを使用します。このメトリックスは物体検出において人気のあるものです。これらは基本的に 平均精度 (mAP)平均再現率 (mAR) で構成されています。要するに、mAP はモデルが正確にオブジェクトを検出し特定する効果を測定し、モデルの予測の総面積に対する正しいオブジェクト検出の平均面積を計算します。mAR は異なるスコアであり、モデルがオブジェクトの完全な範囲をキャプチャする能力を評価し、正しく識別されたオブジェクト領域を実際のオブジェクト領域の平均比率を計算します。メトリックスの詳細については、この記事を参照してください。 このビデオは精度と再現率の基礎について素晴らしい説明をしています。

coco_metrics = keras_cv.metrics.BoxCOCOMetrics(bounding_box_format=BBOX_FORMAT, evaluate_freq=5)

ボックスメトリックスは計算コストが高いため、トレーニング中にすべてのバッチではなく、5つのバッチごとにメトリックスを計算するように evaluate_freq=5 引数を渡します。高すぎる数では、検証メトリックスがまったく表示されないことに気づきました。

次は使用するコールバックを見てみましょう:

class VisualizeDetections(keras.callbacks.Callback):    def on_epoch_end(self, epoch, logs):        if (epoch+1)%5==0:            visualize_detections(                self.model, bounding_box_format=BBOX_FORMAT, dataset=val_dataset, rows=NUM_ROWS, cols=NUM_COLS            )checkpoint_path="best-custom-model"callbacks_list = [    # 検証損失が改善されない場合、6エポック後にトレーニングを停止する    keras.callbacks.EarlyStopping(        monitor="val_loss",        patience=6,    ),        # 最良のモデルを保存    keras.callbacks.ModelCheckpoint(        filepath=checkpoint_path,        monitor="val_loss",        save_best_only=True,        save_weights_only=True    ),        # エポックの終了後にカスタムメトリックスを表示    tf.keras.callbacks.LambdaCallback(    on_epoch_end=lambda epoch, logs:         print(f"\nEpoch #{epoch+1} \n" +              f"Loss: {logs['loss']:.4f} \n" +               f"mAP: {logs['MaP']:.4f} \n" +               f"Validation Loss: {logs['val_loss']:.4f} \n" +               f"Validation mAP: {logs['val_MaP']:.4f} \n")     ),        # 5エポックごとに結果を視覚化    VisualizeDetections()]
  • 早期停止。 検証損失が6エポック後に改善されていない場合、トレーニングを停止します。
  • モデルチェックポイント。 各エポック後に val_loss を確認し、以前のエポックよりも良い場合はモデルの重みを保存します。
  • ラムダコールバック。 ラムダコールバックは、各エポックのトレーニング中に異なる時点で任意のPython関数を定義して実行するカスタムコールバックです。この場合、カスタムメトリックスを各エポック後に表示するために使用しています。COCOMetricsをそのまま表示すると、数値の乱雑な表示になります。簡単にするために、トレーニングおよび検証の損失とmAPのみを表示します。
  • 検出結果の可視化。 これにより、5エポックごとに画像の4×8のグリッドが表示され、予測された境界ボックスが表示されます。これにより、モデルの優れた(またはひどい)性能を把握することができます。うまくいけば、トレーニングが進むにつれてこれらの可視化結果は改善されるはずです。

最後にモデルを作成します。バックボーンはYOLOv8モデルです。使用するnum_classesbounding_box_formatを渡す必要があります。

# cocoデータセットでトレーニングされたバックボーンを用いてRetinaNetモデルを構築するdef create_model():  
    model = keras_cv.models.RetinaNet.from_preset(      
        "yolo_v8_m_backbone_coco",      
        num_classes=len(class_mapping),      
        bounding_box_format=BBOX_FORMAT  
    )  
    return modelmodel = create_model()

また、モデルのnon-max suppressionパラメーターをカスタマイズする必要があります。非最大抑制は、オブジェクト検出において同じオブジェクトに対応する複数の重なり合う予測境界ボックスをフィルタリングするために使用されます。最も信頼性の高いスコアを持つボックスを保持し、冗長なボックスを削除し、各オブジェクトを一度だけ検出します。次の2つのパラメーターを組み込んでいます:iou_thresholdconfidence_threshold

  1. IoU(intersection over union)は、1つの予測ボックスと別の予測ボックスの間に重なりがどれだけあるかを示す、0から1の数値です。重なりがiou_thresholdよりも高ければ、信頼性スコアの低い予測ボックスは破棄されます。
  2. 信頼性スコアは、モデルが予測した境界ボックスに対する信頼度を表します。予測ボックスの信頼性スコアがconfidence_thresholdよりも低い場合、ボックスは破棄されます。

これらのパラメーターはトレーニングには影響しませんが、予測目的で特定のアプリケーションに合わせて調整する必要があります。iou_threshold=0.5confidence_threshold=0.5を設定することは、良い出発点です。

トレーニングを開始する前に、分類損失が焦点損失であることの利点については説明しましたが、予測された境界ボックスの座標のエラーを定義する適切な回帰損失についてはまだ説明していませんでした。人気のある回帰損失(またはbox_loss)は、スムーズL1損失です。スムーズL1は「最良の両方の世界」の損失と考えます。L1損失(絶対誤差)とL2損失(平均二乗誤差)の両方を組み込んでいます。誤差が小さい場合には二次方程式の値に、大きい場合には線形になります(このリンクを参照) 。KerasCVには、私たちの便宜のために組み込まれたスムーズL1損失があります。トレーニング中に表示される損失は、box_lossclassification_lossの合計になります。

# focal分類損失とsmoothl1ボックス損失を使用してcocoメトリックスを持つモデルをコンパイルするmodel.compile(    
    classification_loss="focal",    
    box_loss="smoothl1",    
    optimizer=optimizer_Adam,    
    metrics=[coco_metrics])history = model.fit(    
    train_dataset,    
    validation_data=validation_dataset,    
    epochs=40,    
    callbacks=callbacks_list,    
    verbose=0,)

NVIDIA Tesla P100 GPUでのトレーニングには約1時間12分かかります。

予測する

# 最適なモデルの重みを持つモデルを作成するmodel = create_model()model.load_weights(checkpoint_path)# モデル予測の非最大抑制をカスタマイズする。これらの数値はかなりうまく機能するとわかりましたmodel.prediction_decoder = keras_cv.layers.MultiClassNonMaxSuppression(    
    bounding_box_format=BBOX_FORMAT,    
    from_logits=True,    
    iou_threshold=0.2,    
    confidence_threshold=0.6,)# 検証セットでの可視化visualize_detections(model, dataset=val_dataset, bounding_box_format=BBOX_FORMAT, rows=NUM_ROWS, cols=NUM_COLS)

さあ、トレーニング中に見た最適なモデルをロードし、検証セットで予測を行うことができます:

Sample visual of validation set predictions. Image by author.

最適なモデルに対するメトリックスは次のとおりです:

  • 損失:0.4185
  • mAP:0.2182
  • 検証の損失:0.4584
  • 検証のmAP:0.2916

尊敬なる皆様、これは改善の余地があります。詳細は結論で説明します。(注意: MultiClassNonMaxSuppression が正しく機能していないことに気付きました。上記に表示される左下の画像は、明らかに20%以上の面積を重なるボックスを持っていますが、信頼度の低いボックスが抑制されていません。これについては、より詳しく調査する必要があります。)

こちらはトレーニングと検証の損失のエポックごとのプロットです。過学習の兆候が見られます。また、学習率スケジュールを追加して学習率を時間とともに減らすことが賢明かもしれません。これにより、トレーニングの終わり近くで大きなジャンプが行われる問題を解決するのに役立つかもしれません。

トレーニングと検証の損失のエポックごとのプロット。過学習の兆候が見られます。著者による画像。

結論

ここまで来られた方には、自分自身を褒めてください!物体検出はコンピュータビジョンの中でもより困難なタスクの一つです。幸いなことに、私たちの生活をより簡単にしてくれる新しいKerasCVライブラリがあります。オブジェクト検出パイプラインを作成するためのワークフローをまとめると次のようになります:

  • データセットの可視化から始めましょう。自分自身に質問を投げかけて、「バウンディングボックスの形式は何ですか?xyxyですか?Relxyxyですか?扱うクラスの数はいくつですか?」といった質問をしてください。画像とバウンディングボックスを表示するために visualize_dataset のような関数を作成することを忘れないでください。
  • 持っているデータの形式をKerasCVが望む「辞書内の辞書」の形式に変換します。TensorFlowのデータセットオブジェクトを使用してデータを保持することが特に役立ちます。
  • 画像のリサイズやデータ拡張などの基本的な前処理を行いましょう。KerasCVを使用すれば、これはかなり簡単になります。選んだモデルの文献を注意深く読んで、画像サイズが適切であることを確認してください。
  • 辞書をトレーニング用にタプルに戻します。
  • オプティマイザ(Adamは簡単な選択肢です)、2つの損失関数(クラスの損失にはfocal、ボックスの損失にはL1スムースが簡単な選択肢です)、およびメトリクス(COCOメトリクスが簡単な選択肢です)を選択します。
  • トレーニング中に検出結果を可視化することは、モデルが見逃しているオブジェクトの種類を確認するために役立ちます。
データセット内の問題のあるラベルの例。著者による画像。

次の主なステップの一つは、データセットを整理することです。例えば、上記の画像を見てみましょう。ラベラーは正しくジャガイモの葉の早期枯れ病を識別しましたが、他の健康なジャガイモの葉はどうなっているのでしょうか?なぜこれらはジャガイモの葉としてラベル付けされていないのでしょうか?Roboflowのウェブサイトのヘルスチェックタブを見ると、データセット内の一部のクラスが非常に少ないことがわかります:

クラスの不均衡を示すグラフ。Roboflowのウェブサイトより。

ハイパーパラメータを調整する前に、これらの問題を修正してみてください。オブジェクト検出の課題で頑張ってください!

参考文献

[1] F. Chollet, Pythonによるディープラーニング(2021年)、Manning Publications Co.

[2] A. Géron, Scikit-Learn、Keras、およびTensorFlowによるハンズオン機械学習(2022年)、O’Reily Media Inc.

[3] A. Ng, ディープラーニングスペシャリゼーション、DeepLearning.AI

[4] D. Singh, N. Jain, P. Jain, P. Kayal, S. Kumawat, N. Batra, PlantDoc:視覚的な植物病気検出のためのデータセット(2019)、CoDS COMAD 2020

[5] T. Lin、P. Dollár、R. Girshick、K. He、B. Hariharan、S. Belongie、オブジェクト検出のための特徴ピラミッドネットワーク(2017)、CVPR 2017

[6] T. Lin、P. Goyal、R. Girshick、K. He、P. Dollar、オブジェクト検出のためのフォーカルロス(2020)、IEEE Transactions on Pattern Analysis and Machine Intelligence

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more