Baseline of CAM

14442 ワード

学習目標
CAMは分類モデルから得ることができる.
CAMで物体の位置を見つけることができます.
CAMを視覚的に比較できます.
学習内容
CAM、Grad-CAMのモデルを準備する
CAM
Grad-CAM
Detection with CAM
! mkdir -p ~/aiffel/class_activation_map
CAM、Grad-CAMのモデルを準備する
CAMは、フィーチャー抽出CNNネットワークの後ろに、GAPとsoftmax layerが接続されている必要がありますが、Grad-CAMはありません.
このセクションでは、CAM、Grad-CAMを実装し、まずCAMモデルを実装し、このモデルを使用してGrad-CAMから可視化結果を抽出します.
CAMはクラスの活性化度合いを表し、デフォルトモデルは分類を実行します.
ただし、最終的な目標は、位置を特定し、正解と比較し、位置情報を記録したデータを含むことである.
Tensorflow Datasets

  • Standford dogsデータセットの使用
  • 120種写真判別
  • 分類問題データセット
  • 金庫:ラベルこの位置情報
  • 必要なライブラリの読み込み
    # TensorFlow and tf.keras
    import tensorflow as tf
    from tensorflow import keras
    
    # Helper libraries
    import numpy as np
    import matplotlib.pyplot as plt
    
    import tensorflow_datasets as tfds
    
    import copy
    import cv2
    from PIL import Image
    
    print('슝=3')
    GPUの検証|
    tf.config.list_physical_devices('GPU')
    Stanford dogsデータセット

  • ラーニングデータセット:12000ページ

  • 評価用データセット:8580
  • データのダウンロード
    # 최초 수행시에는 다운로드가 진행됩니다. 오래 걸릴 수 있으니 유의해 주세요.  
    (ds_train, ds_test), ds_info = tfds.load(
        'stanford_dogs',
        split=['train', 'test'],
        shuffle_files=True,
        with_info=True,
    )
    print('슝=3')
    
    ラーニングデータの確認
    tfds.show_examples(ds_train, ds_info)
    評価データの確認
    tfds.show_examples(ds_test, ds_info)
    オブジェクトの位置について
    フィーチャーのチェック

  • image:画像の入力

  • Label:画像の正しいクラスインデックス

  • bbox:バインドボックス、物体の位置を矩形領域とマークする

  • 属性のチェック
    ds_info.features
    バインドボックスの表示方法
    xywh
    固定枠の中心をx,y、小幅w,hで表す.
    ex:(x_center, y_center, width, height)
    x,yは、中心点ではなく左上隅の点を指す可能性がある.
    minmax
    バインドボックスを構成する座標の最上位値と最上位値をマークします.
    ex: (x_min, x_max, y_min, y_max)
    これは座標の屈折値ではなく、画像全体の幅と高さを基準に正規化された相対値タグです.
    BBoxFeature

    このバインドボックスはminmaxを表し、tfdsは高さを最初の軸([ymin、xmin、ymx、xmax)として表します.
    CAMのモデルの作成

  • イメージデータ:ImageNet

  • トレーニングモデル:ResNet 50

  • Grad-CAMモデルは、プール層+Softmax層(アクティブなアクティブ関数、fc層)から作成されます.
  • 実装を試みる
    CAMモデル構造

  • 分類モードに類似

  • プール="avg"FC層ではなく(GAP計算可能)
  • num_classes = ds_info.features["label"].num_classes
    base_model = keras.applications.resnet50.ResNet50(
        include_top=False,     # Imagenet 분류기  fully connected layer 제거
        weights='imagenet',
        input_shape=(224, 224,3),
        pooling='avg',      # GAP를 적용  
    )
    x = base_model.output
    preds = keras.layers.Dense(num_classes, activation='softmax')(x)
    cam_model = keras.Model(inputs=base_model.input, outputs=preds)
    
    cam_model.summary()
    
    CAMモデルの学習
  • normalize_and_resize_img()
  • シーク湿潤データと検証データの簡単な前処理(正規化、スケーリング)
  • apply_normalize_on_dataset()

  • 配置の設定

  • input
  • bbox情報を含む

  • りようりつ
  • ダイレクトラベル(バインドボックス)を無効にする
  • 略図により間接学習物体領域を学習する.
  • def normalize_and_resize_img(input):
        # Normalizes images: `uint8` -> `float32`
        image = tf.image.resize(input['image'], [224, 224])
        input['image'] = tf.cast(image, tf.float32) / 255.
        return input['image'], input['label']
    
    def apply_normalize_on_dataset(ds, is_test=False, batch_size=16):
        ds = ds.map(
            normalize_and_resize_img, 
            num_parallel_calls=2
        )
        ds = ds.batch(batch_size)
        if not is_test:
            ds = ds.repeat()
            ds = ds.shuffle(200)
        ds = ds.prefetch(tf.data.experimental.AUTOTUNE)
        return ds
    
    print('슝=3')
    # 데이터셋에 전처리와 배치처리를 적용합니다. 
    ds_train_norm = apply_normalize_on_dataset(ds_train)
    ds_test_norm = apply_normalize_on_dataset(ds_test)
    
    # 구성된 배치의 모양을 확인해 봅니다. 
    for input in ds_train_norm.take(1):
        image, label = input
        print(image.shape)
        print(label.shape)
    tf.random.set_seed(2021)
    cam_model.compile(
        loss='sparse_categorical_crossentropy',
        optimizer=tf.keras.optimizers.SGD(lr=0.01),
        metrics=['accuracy'],
    )
    
    print('슝=3')
    history_cam_model = cam_model.fit(
        ds_train_norm,
        steps_per_epoch=int(ds_info.splits['train'].num_examples/16),
        validation_steps=int(ds_info.splits['test'].num_examples/16),
        epochs=3,
        validation_data=ds_test_norm,
        verbose=1,
        use_multiprocessing=True,
    )
    学習済みウェイトの保存
    import os
    
    cam_model_path = os.getenv('HOME')+'/aiffel/class_activation_map/cam_model.h5'
    cam_model.save(cam_model_path)
    print("저장 완료!")
    CAM
    メモリが不足する場合がありますので、カーネルを再起動してから試してください.
    # 커널 재시작 이후 실습을 위해, 이전 스텝의 코드를 모아서 한꺼번에 실행합니다.
    import tensorflow as tf
    from tensorflow import keras
    import numpy as np
    import matplotlib.pyplot as plt
    import tensorflow_datasets as tfds
    import copy
    import cv2
    from PIL import Image
    
    (ds_train, ds_test), ds_info = tfds.load(
        'stanford_dogs',
        split=['train', 'test'],
        shuffle_files=True,
        with_info=True,
    )
    
    def normalize_and_resize_img(input):
        # Normalizes images: `uint8` -> `float32`
        image = tf.image.resize(input['image'], [224, 224])
        input['image'] = tf.cast(image, tf.float32) / 255.
        return input['image'], input['label']
    
    def apply_normalize_on_dataset(ds, is_test=False, batch_size=16):
        ds = ds.map(
            normalize_and_resize_img, 
            num_parallel_calls=2
        )
        ds = ds.batch(batch_size)
        if not is_test:
            ds = ds.repeat()
            ds = ds.shuffle(200)
        ds = ds.prefetch(tf.data.experimental.AUTOTUNE)
        return ds
    
    print('슝=3')
    CAMの作成はバッチ単位ではなく、単一の画像データ単位で行われるため、get one()関数で個々に抽出される.
    def get_one(ds):
        ds = ds.take(1)
        sample_data = list(ds.as_numpy_iterator())
        bbox = sample_data[0]['objects']['bbox']
        image = sample_data[0]['image']
        label = sample_data[0]['label']
        return sample_data[0]
    
    print('슝=3')
    データの準備が整いました.前のステップで学習したモデルにロードしてCAMを作成します.
    item = get_one(ds_test)
    print(item['label'])
    plt.imshow(item['image'])
    plt.show()
    import os
    cam_model_path = os.getenv('HOME')+'/aiffel/class_activation_map/cam_model.h5'
    cam_model = tf.keras.models.load_model(cam_model_path)
    print('슝=3')
    CAM作成条件

  • プロパティマッピング

  • ソフトMax層の重み(クラス確率を得る)

  • 必要なクラスの出力値
  • モデルがどのように入力画像を表示するかを直感的に理解するために,CAMを入力サイズと同じにすることで可視化する.
    したがって、モデルとアイテムを受け取ると、入力画像と同じサイズのCAMを返す関数を作成する必要があります.
    generate cam()を実装するために,レイヤの結果値を出力として新しいモデルを定義し,フィードフォワード後にCAMを計算することを実装した.
    最後に,CAMを入力画像の大きさに調整した.
    def generate_cam(model, item):
        item = copy.deepcopy(item)
        width = item['image'].shape[1]
        height = item['image'].shape[0]
        
        img_tensor, class_idx = normalize_and_resize_img(item)
        
        # 학습한 모델에서 원하는 Layer의 output을 얻기 위해서 모델의 input과 output을 새롭게 정의해줍니다.
        # model.layers[-3].output에서는 우리가 필요로 하는 GAP 이전 Convolution layer의 output을 얻을 수 있습니다.
        cam_model = tf.keras.models.Model([model.inputs], [model.layers[-3].output, model.output])
        conv_outputs, predictions = cam_model(tf.expand_dims(img_tensor, 0))
        
        conv_outputs = conv_outputs[0, :, :, :]
        class_weights = model.layers[-1].get_weights()[0] #마지막 모델의 weight activation을 가져옵니다.
        
        cam_image = np.zeros(dtype=np.float32, shape=conv_outputs.shape[0:2])
        for i, w in enumerate(class_weights[:, class_idx]):
            # W * f 를 통해 class별 activation map을 계산합니다.
            cam_image += w * conv_outputs[:, :, i]
    
        cam_image /= np.max(cam_image) # activation score를 normalize합니다.
        cam_image = cam_image.numpy()
        cam_image = cv2.resize(cam_image, (width, height)) # 원래 이미지의 크기로 resize합니다.
        return cam_image
    
    print('슝=3')
    正確な位置表示を実現できます.
    cam_image = generate_cam(cam_model, item)
    plt.imshow(cam_image)
    plt.show()
    CAMイメージを元のイメージに合わせてみましょう.
    def visualize_cam_on_image(src1, src2, alpha=0.5):
        beta = (1.0 - alpha)
        merged_image = cv2.addWeighted(src1, alpha, src2, beta, 0.0)
        return merged_image
    
    print('슝=3')
    
    origin_image = item['image'].astype(np.uint8)
    cam_image_3channel = np.stack([cam_image*255]*3, axis=-1).astype(np.uint8)
    
    blended_image = visualize_cam_on_image(cam_image_3channel, origin_image)
    plt.imshow(blended_image)
    plt.show()
    Grad-CAM
    Grad-CAMでは、適用モデルを柔軟に選択し、作成したモデルを再利用できるため、モデルを観察してもcam modelを使用できます.
    新しいイメージを選びます
    item = get_one(ds_test)
    print(item['label'])
    plt.imshow(item['image'])
    plt.show()
    Grad-CAMを使用したCAMの作成
    grad camは,観察が必要な層と正解クラスの予測値との間でグラデーションを求め,GAP演算を適用して各層の重み付けを求めるために用いられる.
    そして、各チャンネルの重みと特性マッピングを重み付けてcamイメージを得る.これが最終CAMの画像である.
    CAMとは異なり、Grad-CAMは、グラデーション計算を行う観察層activation layerを抽出することによってCAM画像を書き込むためにactivation layerの名前を受け入れて使用します.
    次にgenerate grad cam()は、所望のレイヤの出力と特定のクラスの予測との間のグラデーションを取得し、重みとして使用する.
    def generate_grad_cam(model, activation_layer, item):
        item = copy.deepcopy(item)
        width = item['image'].shape[1]
        height = item['image'].shape[0]
        img_tensor, class_idx = normalize_and_resize_img(item)
        
        # Grad cam에서도 cam과 같이 특정 레이어의 output을 필요로 하므로 모델의 input과 output을 새롭게 정의합니다.
        # 이때 원하는 레이어가 다를 수 있으니 해당 레이어의 이름으로 찾은 후 output으로 추가합니다.
        grad_model = tf.keras.models.Model([model.inputs], [model.get_layer(activation_layer).output, model.output])
        
        # Gradient를 얻기 위해 tape를 사용합니다.
        with tf.GradientTape() as tape:
            conv_output, pred = grad_model(tf.expand_dims(img_tensor, 0))
        
            loss = pred[:, class_idx] # 원하는 class(여기서는 정답으로 활용) 예측값을 얻습니다.
            output = conv_output[0] # 원하는 layer의 output을 얻습니다.
            grad_val = tape.gradient(loss, conv_output)[0] # 예측값에 따른 Layer의 gradient를 얻습니다.
    
        weights = np.mean(grad_val, axis=(0, 1)) # gradient의 GAP으로 class별 weight를 구합니다.
        grad_cam_image = np.zeros(dtype=np.float32, shape=conv_output.shape[0:2])
        for k, w in enumerate(weights):
            # 각 class별 weight와 해당 layer의 output을 곱해 class activation map을 얻습니다.
            grad_cam_image += w * output[:, :, k]
            
        grad_cam_image /= np.max(grad_cam_image)
        grad_cam_image = grad_cam_image.numpy()
        grad_cam_image = cv2.resize(grad_cam_image, (width, height))
        return grad_cam_image
    
    print('슝=3')
    
    
    複数層のCAM画像を抽出します.
    grad_cam_image = generate_grad_cam(cam_model, 'conv5_block3_out', item)
    plt.imshow(grad_cam_image)
    plt.show()
    
    
    grad_cam_image = generate_grad_cam(cam_model, 'conv4_block3_out', item)
    plt.imshow(grad_cam_image)
    plt.show()
    
    grad_cam_image = generate_grad_cam(cam_model, 'conv3_block3_out', item)
    plt.imshow(grad_cam_image)
    plt.show()
    
    
    Detection with CAM
    セーフティボックス
    CAMで物体位置の検出をします.
    新しいイメージを選びます
    item = get_one(ds_test)
    print(item['label'])
    plt.imshow(item['image'])
    plt.show()
    
    cam_image = generate_cam(cam_model, item)
    plt.imshow(cam_image)
    plt.show()
    
    
    

  • generate cam():CAM画像を抽出

  • get bbox():バインドボックスを作成する関数
  • score threshold:しきい値以下のバインドボックス
  • をクリアする.

  • OpenCV利用
  • ConTours()とminAreaReac()の検索:長方形
  • の検索

  • 回転rect():回転のバインドボックスを取得する

  • boxpoints():頂点に切り替えます.

  • intデータ型に変換します.
  • def get_bbox(cam_image, score_thresh=0.05):
        low_indicies = cam_image <= score_thresh
        cam_image[low_indicies] = 0
        cam_image = (cam_image*255).astype(np.uint8)
        
        contours,_ = cv2.findContours(cam_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
        cnt = contours[0]
        rotated_rect = cv2.minAreaRect(cnt)
        rect = cv2.boxPoints(rotated_rect)
        rect = np.int0(rect)
        return rect
    
    print('슝=3')
    bboxはcamイメージで取得され、画像上で可視化される.
    rect = get_bbox(cam_image)
    rect
    
    image = copy.deepcopy(item['image'])
    image = cv2.drawContours(image, [rect], 0, (0,0,255), 2)
    plt.imshow(image)
    plt.show()
    
    
    
    Intersection Over Union(IOU)
    バインドボックスは、CAMとGrad-CAMの2つの方法で取得できます.

    IOUは、2つの領域の交差領域の幅を合計する.これにより、面積の大きさにかかわらず、最適なゼロが見つかったかどうかと記録の相対パーセントを得ることができます.
     rect의 좌표는 (x, y) 형태로, bbox는 (y_min, x_min, y_max, x_max)의 normalized 형태로 주어집니다. 
    def rect_to_minmax(rect, image):
        bbox = [
            rect[:,1].min()/float(image.shape[0]),  #bounding box의 y_min
            rect[:,0].min()/float(image.shape[1]),  #bounding box의 x_min
            rect[:,1].max()/float(image.shape[0]), #bounding box의 y_max
            rect[:,0].max()/float(image.shape[1]) #bounding box의 x_max
        ]
        return bbox
    
    print('슝=3')
    
    
    rectをminmax bboxに変換します.
    pred_bbox = rect_to_minmax(rect, item['image'])
    pred_bbox
    データのground truth bboxを確認します.
    item['objects']['bbox']
    
    IOUを計算して両者の類似度を決定する.
    def get_iou(boxA, boxB):
        y_min = max(boxA[0], boxB[0])
        x_min= max(boxA[1], boxB[1])
        y_max = min(boxA[2], boxB[2])
        x_max = min(boxA[3], boxB[3])
        
        interArea = max(0, x_max - x_min) * max(0, y_max - y_min)
        boxAArea = (boxA[2] - boxA[0]) * (boxA[3] - boxA[1])
        boxBArea = (boxB[2] - boxB[0]) * (boxB[3] - boxB[1])
        iou = interArea / float(boxAArea + boxBArea - interArea)
    
        return iou
    
    print('슝=3')
    
    get_iou(pred_bbox, item['objects']['bbox'][0])