В главе 3 мы увидели, как сделать дополнение к существующим данным и как создать класс набора данных. В этой главе мы построим модель обнаружения медицинских масок, используя RetinaNet, одноэтапную (однокаскадную) модель, предоставленную torchvision.
В разделах с 4.1 по 4.3 мы определим класс набора данных после загрузки данных на основе содержимого, подтвержденного в главах 2 и 3, и разделив их на обучающие и тестовые данные. В разделе 4.4 мы будем использовать API torchvision для загрузки предварительно обученной модели. В разделе 4.5 мы обучим модель с помощью трансферного обучения, а затем проверим расчет прогнозируемого значения и производительность модели в разделе 4.6.
4.1 Загрузка данных
В упражнении по моделированию мы будем использовать код из раздела 2.1 для загрузки данных. Тот же набор данных что мы использовали и в предыдущих главах.
4.2 Разделение данных
Мы будем использовать метод разделения данных, описанный в разделе 3.3, для разделения данных.
4.3 Определение класса набора данных
Чтобы обучить модель PyTorch, вам необходимо определить класс набора данных. Метод __getitem__ класса набора данных для обучения модели обнаружения объектов, предоставляемой torchvision, возвращает файл изображения и координаты ограничивающей рамки. Мы определим класс набора данных следующим образом, применив код, использованный в главе 3.
import os import glob import matplotlib.pyplot as plt import matplotlib.image as mpimg import matplotlib.patches as patches from bs4 import BeautifulSoup from PIL import Image import cv2 import numpy as np import time import torch import torchvision from torch.utils.data import Dataset from torchvision import transforms from matplotlib import pyplot as plt import os def generate_box(obj): xmin = float(obj.find('xmin').text) ymin = float(obj.find('ymin').text) xmax = float(obj.find('xmax').text) ymax = float(obj.find('ymax').text) return [xmin, ymin, xmax, ymax] def generate_label(obj): if obj.find('name').text == "with_mask": return 1 elif obj.find('name').text == "mask_weared_incorrect": return 2 return 0 def generate_target(file): with open(file) as f: data = f.read() soup = BeautifulSoup(data, "html.parser") objects = soup.find_all("object") num_objs = len(objects) boxes = [] labels = [] for i in objects: boxes.append(generate_box(i)) labels.append(generate_label(i)) boxes = torch.as_tensor(boxes, dtype=torch.float32) labels = torch.as_tensor(labels, dtype=torch.int64) target = {} target["boxes"] = boxes target["labels"] = labels return target def plot_image_from_output(img, annotation): img = img.cpu().permute(1,2,0) rects = [] for idx in range(len(annotation["boxes"])): xmin, ymin, xmax, ymax = annotation["boxes"][idx] if annotation['labels'][idx] == 0 : rect = patches.Rectangle((xmin,ymin),(xmax-xmin),(ymax-ymin),linewidth=1,edgecolor='r',facecolor='none') elif annotation['labels'][idx] == 1 : rect = patches.Rectangle((xmin,ymin),(xmax-xmin),(ymax-ymin),linewidth=1,edgecolor='g',facecolor='none') else : rect = patches.Rectangle((xmin,ymin),(xmax-xmin),(ymax-ymin),linewidth=1,edgecolor='orange',facecolor='none') rects.append(rect) return img, rects class MaskDataset(Dataset): def __init__(self, path, transform=None): self.path = path self.imgs = list(sorted(os.listdir(self.path))) self.transform = transform def __len__(self): return len(self.imgs) def __getitem__(self, idx): file_image = self.imgs[idx] file_label = self.imgs[idx][:-3] + 'xml' img_path = os.path.join(self.path, file_image) if 'test' in self.path: label_path = os.path.join("test_annotations/", file_label) else: label_path = os.path.join("annotations/", file_label) img = Image.open(img_path).convert("RGB") target = generate_target(label_path) to_tensor = torchvision.transforms.ToTensor() if self.transform: img, transform_target = self.transform(np.array(img), np.array(target['boxes'])) target['boxes'] = torch.as_tensor(transform_target) # tensor로 변경 img = to_tensor(img) return img, target def collate_fn(batch): return tuple(zip(*batch)) dataset = MaskDataset('images/') test_dataset = MaskDataset('test_images/') data_loader = torch.utils.data.DataLoader(dataset, batch_size=4, collate_fn=collate_fn) test_data_loader = torch.utils.data.DataLoader(test_dataset, batch_size=2, collate_fn=collate_fn)
4.4 Импорт модели
Torchvision предоставляет API для простого вызова моделей глубокого обучения для решения различных задач компьютерного зрения. Мы загрузим модель RetinaNet с помощью модуля torchvision.models. RetinaNet поставляется с torchvision 0.8.0 и выше, поэтому используйте приведенный ниже код, чтобы соответствовать версии torchvision.
!pip install torch==1.7.0+cu101 torchvision==0.8.1+cu101 torchaudio==0.7.0 -f https://download.pytorch.org/whl/torch_stable.html
Проверим установленную версию torchvision
import torchvision import torch
torchvision.__version__
'0.8.1+cu101'
Вы можете проверить, установлена ли версия torchvision 0.8.1, которая работает с текущей версией cuda 10.1, с помощью команды torchvision.__version__. Затем запустите приведенный ниже код, чтобы загрузить модель RetinaNet. Поскольку в наборе данных Face Mask Detection есть 3 класса, параметр num_classes определяется как 3, и, поскольку будет выполняться трансферное обучение, магистральная структура импортирует предварительно обученные веса, а другие веса инициализируются. Магистраль была предварительно обучена на наборе данных COCO, который известен своими наборами данных обнаружения объектов.
retina = torchvision.models.detection.retinanet_resnet50_fpn(num_classes = 3, pretrained=False, pretrained_backbone = True)
4.5 Перенос обучения
После загрузки модели выполняется перенос обучения с использованием приведенного ниже кода.
device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu') num_epochs = 30 retina.to(device) # parameters params = [p for p in retina.parameters() if p.requires_grad] # Извлекайте только те params, для которых требуется gradient calculation optimizer = torch.optim.SGD(params, lr=0.005, momentum=0.9, weight_decay=0.0005) len_dataloader = len(data_loader) # epoch Около 4 минут for epoch in range(num_epochs): start = time.time() retina.train() i = 0 epoch_loss = 0 for images, targets in data_loader: images = list(image.to(device) for image in images) targets = [{k: v.to(device) for k, v in t.items()} for t in targets] loss_dict = retina(images, targets) losses = sum(loss for loss in loss_dict.values()) i += 1 optimizer.zero_grad() losses.backward() optimizer.step() epoch_loss += losses print(epoch_loss, f'time: {time.time() - start}')
Выполните приведенный ниже код, чтобы сохранить изученные веса для повторного использования модели. Вы можете использовать функцию torch.save для сохранения изученных весов в указанном месте.
torch.save(retina.state_dict(),f'retina_{num_epochs}.pt')
retina.load_state_dict(torch.load(f'retina_{num_epochs}.pt'))
Чтобы загрузить изученные веса, вы можете использовать функции load_state_dict и torch.load. Если переменная retina указана заново, модель должна быть загружена в память графического процессора, чтобы включить работу графического процессора.
device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu') retina.to(device)
4.6 Прогноз
Когда обучение закончится, мы проверим результаты прогноза. После загрузки данных из test_data_loader и помещения их в модель мы визуализируем прогнозируемый результат и фактическое значение соответственно. Во-первых, давайте определим функции, необходимые для предсказания.
def make_prediction(model, img, threshold): model.eval() preds = model(img) for id in range(len(preds)) : idx_list = [] for idx, score in enumerate(preds[id]['scores']) : if score > threshold : #threshold 넘는 idx 구함 idx_list.append(idx) preds[id]['boxes'] = preds[id]['boxes'][idx_list] preds[id]['labels'] = preds[id]['labels'][idx_list] preds[id]['scores'] = preds[id]['scores'][idx_list] return preds
Функция make_prediction хранит алгоритм, который прогнозирует с использованием обученной модели глубокого обучения. Настройте пороговый параметр, чтобы выбирать только ограничивающие рамки с определенным уровнем достоверности. Обычно используется значение больше 0,5. Далее мы будем использовать оператор for для прогнозирования всех данных в test_data_loader.
from tqdm import tqdm labels = [] preds_adj_all = [] annot_all = [] for im, annot in tqdm(test_data_loader, position = 0, leave = True): im = list(img.to(device) for img in im) #annot = [{k: v.to(device) for k, v in t.items()} for t in annot] for t in annot: labels += t['labels'] with torch.no_grad(): preds_adj = make_prediction(retina, im, 0.5) preds_adj = [{k: v.to(torch.device('cpu')) for k, v in t.items()} for t in preds_adj] preds_adj_all.append(preds_adj) annot_all.append(annot)
Я использую функцию tqdm для отображения прогресса. Все предсказанные значения хранятся в переменной preds_adj_all. Далее приступим к визуализации фактической ограничивающей рамки и прогнозируемой ограничивающей рамки.
nrows = 8 ncols = 2 fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=(ncols*4, nrows*4)) batch_i = 0 for im, annot in test_data_loader: pos = batch_i * 4 + 1 for sample_i in range(len(im)) : img, rects = plot_image_from_output(im[sample_i], annot[sample_i]) axes[(pos)//2, 1-((pos)%2)].imshow(img) for rect in rects: axes[(pos)//2, 1-((pos)%2)].add_patch(rect) img, rects = plot_image_from_output(im[sample_i], preds_adj_all[batch_i][sample_i]) axes[(pos)//2, 1-((pos+1)%2)].imshow(img) for rect in rects: axes[(pos)//2, 1-((pos+1)%2)].add_patch(rect) pos += 2 batch_i += 1 if batch_i == 4: break # xtick, ytick 제거 for idx, ax in enumerate(axes.flat): ax.set_xticks([]) ax.set_yticks([]) colnames = ['True', 'Pred'] for idx, ax in enumerate(axes[0]): ax.set_title(colnames[idx]) plt.tight_layout() plt.show()
Используя оператор for, я попытался визуализировать фактические и прогнозируемые значения для 4 пакетов, всего 8 изображений. Левый столбец — это метка и расположение фактической ограничивающей рамки, а правый столбец — это прогнозируемые значения модели. Носители масок (зеленые) хорошо наблюдают за обнаружением, в то время как носящие маски (красные) могут видеть случайные обнаружения как неправильное ношение маски (оранжевые). Чтобы оценить общую производительность модели, давайте рассчитаем среднюю среднюю точность (mAP). mAP — это метрика, используемая при оценке моделей обнаружения объектов.
В папке Tutorial-Book-Utils есть файл utils_ObjectDetection.py, который был загружен при загрузке данных. Давайте рассчитаем mAP, используя функцию в модуле. Сначала загрузите модуль utils_ObjectDetection.py.
import utils_ObjectDetection as utils
sample_metrics = [] for batch_i in range(len(preds_adj_all)): sample_metrics += utils.get_batch_statistics(preds_adj_all[batch_i], annot_all[batch_i], iou_threshold=0.5)
После сохранения информации, необходимой для расчета mAP для каждой партии в sample_metrics, mAP рассчитывается с использованием функции ap_per_class.
true_positives, pred_scores, pred_labels = [torch.cat(x, 0) for x in list(zip(*sample_metrics))] # 배치가 전부 합쳐짐 precision, recall, AP, f1, ap_class = utils.ap_per_class(true_positives, pred_scores, pred_labels, torch.tensor(labels)) mAP = torch.mean(AP) print(f'mAP : {mAP}') print(f'AP : {AP}')
mAP : 0.5824690281035101 AP : tensor([0.7684, 0.9188, 0.0603], dtype=torch.float64)
Интерпретируя результаты, он показывает 0,7684 AP для класса 0, объект без маски, 0,9188 AP для класса 1, объект в маске и 0,06 AP для класса 2, объект без маски.
На данный момент мы создали модель обнаружения медицинских масок, проведя трансферное обучение в RetinaNet. В следующей главе мы будем использовать Faster R-CNN, двухэтапный детектор, для повышения эффективности обнаружения.