바운딩 박스(bounding box) 데이터셋을 전처리할 수 있습니다.
Object detection 모델을 학습할 수 있습니다.
Detection 모델을 활용한 시스템을 만들 수 있습니다.
자율주행 보조장치
RetinaNet
keras-retinanet
이미지와 코드 출처: AIFFEL
자율주행 보조장치는 카메라에 사람이 가까워졌을 때, 그리고 차가 가까워져서 탐지된 크기가 일정 이상일 때 멈춰야 합니다.
자율주행 보조장치 object detection 조건
- 1) 사람이 카메라에 감지되면 정지
- 2) 차량이 일정 크기 이상으로 감지되면 정지
KITTI 데이터셋은 자율주행을 위한 데이터셋으로 2D objecte detection 뿐만 아니라 깊이까지 포함한 3D object detection 라벨 등을 제공하고 있습니다.
import urllib3
urllib3.disable_warnings()
(ds_train, ds_test), ds_info = tfds.load(
'kitti',
split=['train', 'test'],
shuffle_files=True,
with_info=True,
)
fig = tfds.show_examples(ds_train, ds_info)
*첫 실습 때 하지 않은 부분
이번에는 데이터셋을 직접 확인하는 시간을 갖도록 하겠습니다. ds_train.take(1)
을 통해서 데이터셋을 하나씩 뽑아볼 수 있는 TakeDataset
을 얻을 수 있습니다.
이렇게 뽑은 데이터에는 image
등의 정보가 포함되어 있습니다.
눈으로 확인해서 학습에 사용할 데이터를 직접 이해할 수 있게 됩니다.
TakeDataset = ds_train.take(1)
for example in TakeDataset:
print('------Example------')
print(list(example.keys())) # example is `{'image': tf.Tensor, 'label': tf.Tensor}`
image = example["image"]
filename = example["image/file_name"].numpy().decode('utf-8')
objects = example["objects"]
print('------objects------')
print(objects)
img = Image.fromarray(image.numpy())
img
이미지 위에 바운딩 박스 그려서 시각화
RetinaNet은 Focal Loss for Dense Object Detection 논문을 통해 공개된 detection 모델입니다.
Detection 모델을 직접 만들기에는 많은 시간이 소요되므로, 미리 모델을 구현한 라이브러리를 가져와 커스텀 데이터셋에 학습시키고 빠르게 사용하겠습니다.
1-stage detector 모델인 YOLO와 SSD는 2-stage detector인 Faster-RCNN 등보다 속도는 빠르지만 성능이 낮은 문제를 가지고 있었습니다.
이를 해결하기 위해서 RetinaNet에서는 focal loss와 FPN(Feature Pyramid Network) 를 적용한 네트워크를 사용합니다.
Focal loss는 기존의 1-stage detection 모델들(YOLO, SSD)이 물체 전경과 배경을 담고 있는 모든 그리드(grid)에 대해 한 번에 학습됨으로 인해서 생기는 클래스 간의 불균형을 해결하고자 도입되었습니다.
위 그림 왼쪽 7x7 feature level에서는 한 픽셀이고, 오른쪽의 image level(자동차 사진)에서 보이는 그리드는 각 픽셀의 receptive field입니다.
그림에서 보이는 것처럼 우리가 사용하는 이미지는 물체보다는 많은 배경을 학습하게 됩니다.
논문에서는 이를 해결하기 위해서 Loss를 개선하여 정확도를 높였습니다.
Focal loss는 우리가 많이 사용해왔던 교차 엔트로피를 기반으로 만들어졌습니다.
위 그림을 보면 Focal loss는 그저 교차 엔트로피 CE()의 앞단에 간단히 라는 modulating factor를 붙여주었습니다.
교차 엔트로피의 개형을 보면 ground truth class에 대한 확률이 높으면 잘 분류된 것으로 판단되므로 손실이 줄어드는 것을 볼 수 있습니다.
하지만 확률이 1에 매우 가깝지 않은 이상 상당히 큰 손실로 이어지게 돱니다. 그러면 모델 학습 시키는 과정에서 문제가 발생이 됩니다.
따라서 이미지는 극단적으로 배경의 class가 많은 class imbalanced data라고 할 수 있습니다.
이렇게 너무 많은 배경 class에 압도되지 않도록 modulating factor로 손실을 조절해줍니다.
람다를 0으로 설정하면 modulating factor가 0이 되어 일반적인 교차 엔트로피가 되고 람다가 커질수록 modulating이 강하게 적용됩니다.
FPN은 특성을 피라미드처럼 쌓아서 사용하는 방식입니다.
CNN 백본 네트워크에서는 다양한 레이어의 결과값을 특성 맵(feature map)으로 사용할 수 있습니다.
이때 컨볼루션 연산은 커널을 통해 일정한 영역을 보고 몇 개의 숫자로 요약해 내기 때문에, 입력 이미지를 기준으로 생각하면 입력 이미지와 먼 모델의 뒷쪽의 특성 맵일수록 하나의 "셀(cell)"이 넓은 이미지 영역의 정보를 담고 있고, 입력 이미지와 가까운 앞쪽 레이어의 특성 맵일수록 좁은 범위의 정보를 담고 있습니다.
이를 receptive field라고 합니다. 레이어가 깊어질 수록 pooling을 거쳐 넓은 범위의 정보(receptive field)를 갖게 되는 것입니다.
SSD가 각 레이어의 특성 맵에서 다양한 크기에 대한 결과를 얻는 방식을 취했다면 RetinaNet에서는 receptive field가 넓은 뒷쪽의 특성 맵을 upsampling(확대)하여 앞단의 특성 맵과 더해서 사용했습니다.
레이어가 깊어질수록 feature map의 방향의 receptive field가 넓어지는 것인데, 넓게 보는 것과 좁게 보는 것을 같이 쓰겠다는 목적입니다.
사실 tensorflow_dataset
의 KITTI 데이터셋을 그대로 사용해서 Keras RetinaNet을 학습시키기 위해서는 라이브러리를 수정해야 합니다.
하지만 이보다 더 쉬운 방법은 해당 모델을 훈련할 수 있는 공통된 데이터셋 포맷인 CSV 형태로 모델을 변경해주는 방법입니다.
tensorflow_dataset
의 API를 사용해 이미지와 각 이미지에 해당하는 바운딩 박스 라벨의 정보를 얻을 수 있었습니다. 그렇다면 API를 활용하여 데이터를 추출, 이를 포맷팅 하여 CSV 형태로 한 줄씩 저장해 봅시다.
한 라인에 이미지 파일의 위치, 바운딩 박스 위치, 그리고 클래스 정보를 가지는 CSV 파일을 작성하도록 코드를 작성하고, 이를 사용해 CSV 파일을 생성합니다.
import os
data_dir = os.getenv('HOME')+'/aiffel/object_detection/data'
img_dir = os.getenv('HOME')+'/kitti_images'
train_csv_path = data_dir + '/kitti_train.csv'
# parse_dataset 함수를 구현해 주세요.
def parse_dataset(dataset, img_dir="kitti_images", total=0):
if not os.path.exists(img_dir):
os.mkdir(img_dir)
# Dataset의 claas를 확인하여 class에 따른 index를 확인해둡니다.
# 저는 기존의 class를 차와 사람으로 나누었습니다.
type_class_map = {
0: "car",
1: "car",
2: "car",
3: "person",
4: "person",
5: "person",
}
# Keras retinanet을 학습하기 위한 dataset을 csv로 parsing하기 위해서 필요한 column을 가진 pandas.DataFrame을 생성합니다.
df = pd.DataFrame(columns=["img_path", "x1", "y1", "x2", "y2", "class_name"])
for item in tqdm(dataset, total=total):
filename = item['image/file_name'].numpy().decode('utf-8')
img_path = os.path.join(img_dir, filename)
img = Image.fromarray(item['image'].numpy())
img.save(img_path)
object_bbox = item['objects']['bbox']
object_type = item['objects']['type'].numpy()
width, height = img.size
# tf.dataset의 bbox좌표가 0과 1사이로 normalize된 좌표이므로 이를 pixel좌표로 변환합니다.
x_min = object_bbox[:,1] * width
x_max = object_bbox[:,3] * width
y_min = height - object_bbox[:,2] * height
y_max = height - object_bbox[:,0] * height
# 한 이미지에 있는 여러 Object들을 한 줄씩 pandas.DataFrame에 append합니다.
rects = np.stack([x_min, y_min, x_max, y_max], axis=1).astype(np.int)
for i, _rect in enumerate(rects):
_type = object_type[i]
if _type not in type_class_map.keys():
continue
df = df.append({
"img_path": img_path,
"x1": _rect[0],
"y1": _rect[1],
"x2": _rect[2],
"y2": _rect[3],
"class_name": type_class_map[_type]
}, ignore_index=True)
break
return df
df_train = parse_dataset(ds_train, img_dir, total=ds_info.splits['train'].num_examples)
df_train.to_csv(train_csv_path, sep=',',index = False, header=False)
## dataframe생성
test_csv_path = data_dir + '/kitti_test.csv'
df_test = parse_dataset(ds_test, img_dir, total=ds_info.splits['test'].num_examples)
df_test.to_csv(test_csv_path, sep=',',index = False, header=False)
데이터셋에서 클래스는 문자열(string)으로 표시되지만, 모델에게 데이터를 알려줄 때에는 숫자를 사용해 클래스를 표시해야 합니다.
이때 모두 어떤 클래스가 있고 각 클래스가 어떤 인덱스(index)에 맵핑(mapping)될지 미리 정하고 저장해 두어야
학습을 한 후 추론(inference)을 할 때에도 숫자 인덱스로 나온 정보를 클래스 이름으로 바꾸어 해석할 수 있습니다.
아래 형식을 참고하여, 자동차와 사람을 구별하기 위한 클래스 맵핑 함수를 만들어 주세요.
class_txt_path = data_dir + '/classes.txt'
def save_class_format(path="./classes.txt"):
class_type_map = {
"car" : 0,
"person": 1
}
with open(path, mode='w', encoding='utf-8') as f:
for k, v in class_type_map.items():
f.write(f"{k},{v}\n")
save_class_format(class_txt_path)
학습이 잘 되기 위해서는 환경에 따라 batch_size
나 worker
, epoch
를 조절해야 합니다.
훈련 이미지 크기 또는 batch_size
가 너무 크면 GPU에서 out-of-memory 에러가 날 수 있으니 적절히 조정해야 합니다.
이제 위에서 변환한 모델을 load하고 추론 및 시각화를 해보세요!
아래에 load된 모델을 통해 추론을 하고 시각화를 하는 함수를 작성해 주세요. 일정 점수 이하는 경우를 제거해야 함을 기억해야 합니다.