import os
import numpy as np
import tensorflow as tf
import shutil
import json
import time
import cv2 as cv
from py2vision.recognition.selector import NeuralNetwork
from py2vision.models.models_manager import ModelManager
from py2vision.models.blocks.backbone_block import BackboneBlock
from py2vision.models.blocks.backbone_block import darknet53, darknet19_tiny
from py2vision.compute.yolov3_calculus import YoloV3Calculus
from py2vision.utils.label_utils import read_class_names
from py2vision.image_process.frame_decorator import Frame
from py2vision.image_process.resize_with_bbox import ResizeWithBBox
from py2vision.datasets_loader.yolov3_dataset_generator import YoloV3DatasetGenerator
[docs]class ObjectDetectorYoloV3(NeuralNetwork):
"""Made of an Yolo network model and a dataset generator.
Args:
mode_name: an string to naming the model.
num_class: an integer with the numbers of classes in the model.
input_shape: A tuple with dims shape (height, weight, channels).
version: it can be 'yolov3' or 'yolov3_tiny'.
training: a boolean that change depending if you want to train the model
gpu_name: a gpu name if it is None this class search automatically a gpu compatible.
Attributes:
model: A model instance.
num_class: an integer with the numbers of classes in the model.
version: it can be 'yolov3' or 'yolov3_tiny'.
model_name: an string to naming the model.
input_shape: A tuple with dims shape (height, weight, channels).
gpus: a list with all allowed gpus.
conv_tensors: these are the ouput of build yolov3 without prediction layer.
"""
def __init__(self, model_name, num_class, input_shape=[416, 416, 3], version="yolov3", training=False, gpu_name=None):
super().__init__()
self.model = None
self.num_class = num_class
self.version = version
self.model_name = model_name
self.input_shape = input_shape
if gpu_name == None:
self.gpus = tf.config.experimental.list_physical_devices('GPU')
if len(self.gpus) > 0:
print(f'GPUs {self.gpus}')
try: tf.config.experimental.set_memory_growth(self.gpus[0], True)
except RuntimeError: pass
else:
try: tf.config.experimental.set_memory_growth(gpu_name, True)
except RuntimeError: pass
if self.version == "yolov3":
backbone_net = BackboneBlock(darknet53())
model_manager = ModelManager()
self.conv_tensors = model_manager.build_yolov3(backbone_net, self.num_class)(np.asarray(self.input_shape))
elif self.version == "yolov3_tiny":
backbone_net = BackboneBlock(darknet19_tiny())
model_manager = ModelManager()
self.conv_tensors = model_manager.build_yolov3_tiny(backbone_net, self.num_class)(np.asarray(self.input_shape))
else:
versions = ["yolov3", "yolov3_tiny"]
raise ValueError("yolo_version just can be: {}".format(", ".join(versions)))
self.model = self.build_model(self.conv_tensors, training)
[docs] def build_model(self, conv_tensors, training):
"""Build the complete yolo model and return model instance.
Args:
conv_tensors: a tensor with convolutional layers of a yolo network without output layers or prediction layers.
training: a boolean that change network structure, if is true the last layers will be predict tensors otherwise it will be output tensors.
Returns:
A yolo model.
"""
output_tensors = []
input_layer = conv_tensors[-1]
self.training = training
compute = YoloV3Calculus()
for i, conv_tensor in enumerate(conv_tensors[:-1]):
pred_tensor = compute.decode(conv_tensor, self.num_class, i)
if self.training: output_tensors.append(conv_tensor)
output_tensors.append(pred_tensor)
return tf.keras.Model(input_layer, output_tensors, name=self.model_name)
[docs] def train(self, train_annotations_path, test_annotations_path, class_file_name,
checkpoint_path="checkpoints", use_checkpoint=False, warmup_epochs=2,
epochs=100, log_dir="logs", save_only_best_model=True, save_all_checkpoints=False, batch_size=4, lr_init=1e-4, lr_end=1e-6,
strides=[8, 16, 32],
anchors=[[[10, 13], [16, 30], [33, 23]],
[[30, 61], [62, 45], [59, 119]],
[[116, 90], [156, 198], [373, 326]]],
anchor_per_scale=3, max_bbox_per_scale=100):
"""Train an yolov3 network or yolov3 tiny.
Args:
train_annotations_path: a string corresponding to the folder where train annotations are located.
test_annotations_path: a string corresponding to the folder where test annotations are located.
class_file_name: a string corresponding to the classes file (a .txt file with a list of classes) is located.
checkpoint_path: a string corresponding to the checkpoint file that is inside of a checkpoints folder.
use_checkpoint: a boolean that controls if use chepoint before train
warmup_epochs: an hiperparameter that update learning rate like this paper https://arxiv.org/pdf/1812.01187.pdf&usg=ALkJrhglKOPDjNt6SHGbphTHyMcT0cuMJg
epochs: Number of epochs to train.
log_dir: a folder to save logs.
save_only_best_model: if is true the model will be saved when best validation loss > total validation loss/total test elements, but if it isn't true model will be saved always.
save_all_checkpoints: it is a boolean, if is true model will be saved in each epoch.
batch_size: an integer with the size of batches in test and train datasets.
lr_init: a float which is initial learning rate
lr_end: a float which is final learning rate
strides: a list with the strides in a yolo model.
anchors: these are the yolo anchors sizes.
anchor_per_scale: an integer with the number of anchor boxes per scale.
max_bbox_per_scale: nan integer with the number of bounding boxes per scale.
"""
training = True
if self.training == False:
self.model = self.build_model(self.conv_tensors, training)
if os.path.exists(log_dir): shutil.rmtree(log_dir)
self.writer = tf.summary.create_file_writer(log_dir)
train_set = YoloV3DatasetGenerator(train_annotations_path, class_file_name, batch_size=batch_size, strides=strides, anchors=anchors, anchor_per_scale=anchor_per_scale, max_bbox_per_scale=max_bbox_per_scale)
test_set = YoloV3DatasetGenerator(test_annotations_path, class_file_name, batch_size=batch_size, strides=strides, anchors=anchors, anchor_per_scale=anchor_per_scale, max_bbox_per_scale=max_bbox_per_scale)
steps_per_epoch = len(train_set)
self.global_steps = tf.Variable(1, trainable=False, dtype=tf.int64)
self.warmup_steps = warmup_epochs * steps_per_epoch
self.total_steps = epochs * steps_per_epoch
if use_checkpoint:
try:
self.model.load_weights(checkpoint_path)
except ValueError:
assert Exception("Shapes are incompatible between model and weights")
checkpoint_path_splited = os.path.split(checkpoint_path)
if len(checkpoint_path_splited[0]) == 0:
try:
os.mkdir("checkpoints")
except OSError as error:
print(error)
checkpoint_folder = "checkpoints"
else:
checkpoint_folder = checkpoint_path_splited[0]
optimizer = tf.keras.optimizers.Adam()
validate_writer = tf.summary.create_file_writer(log_dir)
mAP_model = self.build_model(self.conv_tensors, training) # create second model to measure mAP
best_val_loss = 1000 # should be large at start
for epoch in range(epochs):
for image_data, target in train_set:
results = self.train_step(image_data, target, optimizer, lr_init, lr_end)
cur_step = results[0]%steps_per_epoch
print("epoch:{:2.0f} step:{:5.0f}/{}, lr:{:.6f}, giou_loss:{:7.2f}, conf_loss:{:7.2f}, prob_loss:{:7.2f}, total_loss:{:7.2f}"
.format(epoch, cur_step, steps_per_epoch, results[1], results[2], results[3], results[4], results[5]))
if len(test_set) == 0:
print("configure TEST options to validate model")
self.model.save_weights(os.path.join(checkpoint_folder, self.model._name))
continue
count, giou_val, conf_val, prob_val, total_val = 0., 0, 0, 0, 0
for image_data, target in test_set:
results = self.validate_step(image_data, target)
count += 1
giou_val += results[0]
conf_val += results[1]
prob_val += results[2]
total_val += results[3]
# writing validate summary data
with validate_writer.as_default():
tf.summary.scalar("validate_loss/total_val", total_val/count, step=epoch)
tf.summary.scalar("validate_loss/giou_val", giou_val/count, step=epoch)
tf.summary.scalar("validate_loss/conf_val", conf_val/count, step=epoch)
tf.summary.scalar("validate_loss/prob_val", prob_val/count, step=epoch)
validate_writer.flush()
print("\n\ngiou_val_loss:{:7.2f}, conf_val_loss:{:7.2f}, prob_val_loss:{:7.2f}, total_val_loss:{:7.2f}\n\n".
format(giou_val/count, conf_val/count, prob_val/count, total_val/count))
if save_all_checkpoints and not save_only_best_model:
save_directory = os.path.join(checkpoint_folder, self.model._name+"_val_loss_{:7.2f}_epoch_{}.ckpt".format(total_val/count, epoch))
self.model.save_weights(save_directory)
if save_only_best_model and best_val_loss>total_val/count:
save_directory = os.path.join(checkpoint_folder, self.model._name+"best_val_loss_{:7.2f}_epoch_{}.ckpt".format(total_val/count, epoch))
self.model.save_weights(save_directory)
best_val_loss = total_val/count
if not save_only_best_model and not save_all_checkpoints:
save_directory = os.path.join(checkpoint_folder, self.model._name+"_val_loss_{:7.2f}_epoch_{}.ckpt".format(total_val/count, epoch))
self.model.save_weights(save_directory)
# measure mAP of trained custom model
if 'save_directory' in locals():
mAP_model.load_weights(save_directory) # use keras weights
self.evaluate(mAP_model, test_set, class_file_name, 0.3, 0.45)
[docs] def train_step(self, image_data, target, optimizer, lr_init=1e-4, lr_end=1e-6):
""" training step.
Args:
image_data: an image.
target: labels
optimizer: an tensorflow optimizer like Adams optimizer.
lr_init: initial leraning rate hiperparameter.
lr_end: final learning rate hiperparameter.
Returns:
(global_steps, optimizer.lr, giou_loss, conf_loss, prob_loss, total_loss)
"""
with tf.GradientTape() as tape:
pred_result = self.model(image_data, training=True)
giou_loss=conf_loss=prob_loss=0
compute = YoloV3Calculus()
# optimizing process
grid = 3 if not (self.version == "yolov3_tiny") else 2
for i in range(grid):
conv, pred = pred_result[i*2], pred_result[i*2+1]
loss_items = compute.loss(pred, conv, *target[i], self.num_class, i)
giou_loss += loss_items[0]
conf_loss += loss_items[1]
prob_loss += loss_items[2]
total_loss = giou_loss + conf_loss + prob_loss
gradients = tape.gradient(total_loss, self.model.trainable_variables)
optimizer.apply_gradients(zip(gradients, self.model.trainable_variables))
# update learning rate
# about warmup: https://arxiv.org/pdf/1812.01187.pdf&usg=ALkJrhglKOPDjNt6SHGbphTHyMcT0cuMJg
self.global_steps.assign_add(1)
if self.global_steps < self.warmup_steps:# and not TRAIN_TRANSFER:
lr = self.global_steps / self.warmup_steps * lr_init
else:
lr = lr_end + 0.5 * (lr_init - lr_end)*(
(1 + tf.cos((self.global_steps - self.warmup_steps) / (self.total_steps - self.warmup_steps) * np.pi)))
optimizer.lr.assign(lr.numpy())
# writing summary data
with self.writer.as_default():
tf.summary.scalar("lr", optimizer.lr, step=self.global_steps)
tf.summary.scalar("loss/total_loss", total_loss, step=self.global_steps)
tf.summary.scalar("loss/giou_loss", giou_loss, step=self.global_steps)
tf.summary.scalar("loss/conf_loss", conf_loss, step=self.global_steps)
tf.summary.scalar("loss/prob_loss", prob_loss, step=self.global_steps)
self.writer.flush()
return self.global_steps.numpy(), optimizer.lr.numpy(), giou_loss.numpy(), conf_loss.numpy(), prob_loss.numpy(), total_loss.numpy()
[docs] def validate_step(self, image_data, target):
""" Validation step.
Args:
image_data: an image.
target: labels.
Returns:
(giou_loss, conf_loss, prob_loss, total_loss)
"""
with tf.GradientTape() as tape:
pred_result = self.model(image_data, training=False)
giou_loss=conf_loss=prob_loss=0
compute = YoloV3Calculus()
# optimizing process
grid = 3 if not (self.version == "yolov3_tiny") else 2
for i in range(grid):
conv, pred = pred_result[i*2], pred_result[i*2+1]
loss_items = compute.loss(pred, conv, *target[i], self.num_class, i)
giou_loss += loss_items[0]
conf_loss += loss_items[1]
prob_loss += loss_items[2]
total_loss = giou_loss + conf_loss + prob_loss
return giou_loss.numpy(), conf_loss.numpy(), prob_loss.numpy(), total_loss.numpy()
[docs] def restore_weights(self, weights_file, use_checkpoint=False):
"""Load previously trained model weights.
Args:
weights_file: beginning by project root this is the path where is save your weights; example: "weights/weights_01.h5".
use_checkpoint: if you wanna use a .ckpt file this variable should be True.
"""
tf.keras.backend.clear_session() # used to reset layer names
# load Darknet original weights to TensorFlow model
if use_checkpoint:
try:
self.model.load_weights(weights_file)
except ValueError:
assert Exception("Shapes are incompatible between model and weights")
else:
range1 = 75 if self.version == "yolov3" else 13
range2 = [58, 66, 74] if self.version == "yolov3" else [9, 12]
with open(weights_file, 'rb') as wf:
major, minor, revision, seen, _ = np.fromfile(wf, dtype=np.int32, count=5)
j = 0
for i in range(range1):
if i > 0:
conv_layer_name = 'conv2d_%d' %i
else:
conv_layer_name = 'conv2d'
if j > 0:
bn_layer_name = 'batch_normalization_%d' %j
else:
bn_layer_name = 'batch_normalization'
conv_layer = self.model.get_layer(conv_layer_name)
filters = conv_layer.filters
k_size = conv_layer.kernel_size[0]
in_dim = conv_layer.input_shape[-1]
if i not in range2:
# darknet weights: [beta, gamma, mean, variance]
bn_weights = np.fromfile(wf, dtype=np.float32, count=4 * filters)
# tf weights: [gamma, beta, mean, variance]
bn_weights = bn_weights.reshape((4, filters))[[1, 0, 2, 3]]
bn_layer = self.model.get_layer(bn_layer_name)
j += 1
else:
conv_bias = np.fromfile(wf, dtype=np.float32, count=filters)
# darknet shape (out_dim, in_dim, height, width)
conv_shape = (filters, in_dim, k_size, k_size)
conv_weights = np.fromfile(wf, dtype=np.float32, count=np.product(conv_shape))
# tf shape (height, width, in_dim, out_dim)
conv_weights = conv_weights.reshape(conv_shape).transpose([2, 3, 1, 0])
if i not in range2:
conv_layer.set_weights([conv_weights])
bn_layer.set_weights(bn_weights)
else:
conv_layer.set_weights([conv_weights, conv_bias])
assert len(wf.read()) == 0, 'failed to read all data'
[docs] def inference(self, image_path, input_size=416, score_threshold=0.3, iou_threshold=0.45, nms_method="nms"):
""" Apply inference with trained model.
Args:
image_path: a path to an image.
input_size: integer to resize an input image from their original dimensions to an square image.
score_threshold: if the score of a bounding boxes is less than score_threshold, it will be discard.
iou_threshold: a parameter between (0, 1) which is used for nms algorithm
nms_method: a string that can be 'nms' or 'soft-nms'.
Returns:
An array with bounding boxes
"""
original_image = cv.imread(image_path)
original_image = cv.cvtColor(original_image, cv.COLOR_BGR2RGB)
original_image = cv.cvtColor(original_image, cv.COLOR_BGR2RGB)
frame = Frame(np.copy(original_image))
image_data = ResizeWithBBox(frame).apply([input_size, input_size])
image_data = image_data[np.newaxis, ...].astype(np.float32)
pred_bbox = self.model.predict(image_data)
pred_bbox = [tf.reshape(x, (-1, tf.shape(x)[-1])) for x in pred_bbox]
pred_bbox = tf.concat(pred_bbox, axis=0)
compute = YoloV3Calculus()
bboxes = compute.postprocess_boxes(pred_bbox, original_image, input_size, score_threshold)
bboxes = compute.nms(bboxes, iou_threshold, method=nms_method)
return bboxes
[docs] def evaluate(self, model, dataset, classes_file, score_threshold=0.05, iou_threshold=0.50, test_input_size=416):
"""Apply evaluation using mAP.
Args:
model: a tesorflow detection model.
dataset: an YoloV3DatasetGenerator instance with test dataset.
classes_file: a string corresponding to the classes file (a .txt file with a list of classes) is located.
score_threshold: if the score of a bounding boxes is less than score_threshold, it will be discard.
iou_threshold: a parameter between (0, 1) which is used for nms algorithm.
test_input_size: integer to resize an input image from their original dimensions to an square image.
Returns:
mAP score
"""
min_overlap = 0.5 # default value (defined in the PASCAL VOC2012 challenge)
num_class = read_class_names(classes_file)
ground_truth_dir_path = 'mAP/ground-truth'
if os.path.exists(ground_truth_dir_path): shutil.rmtree(ground_truth_dir_path)
if not os.path.exists('mAP'): os.mkdir('mAP')
os.mkdir(ground_truth_dir_path)
print(f'\ncalculating mAP{int(iou_threshold*100)}...\n')
gt_counter_per_class = {}
for index in range(dataset.num_samples):
ann_dataset = dataset.annotations[index]
original_image, bbox_data_gt = dataset.parse_annotation(ann_dataset, True)
if len(bbox_data_gt) == 0:
bboxes_gt = []
classes_gt = []
else:
bboxes_gt, classes_gt = bbox_data_gt[:, :4], bbox_data_gt[:, 4]
ground_truth_path = os.path.join(ground_truth_dir_path, str(index) + '.txt')
num_bbox_gt = len(bboxes_gt)
bounding_boxes = []
for i in range(num_bbox_gt):
class_name = num_class[classes_gt[i]]
xmin, ymin, xmax, ymax = list(map(str, bboxes_gt[i]))
bbox = xmin + " " + ymin + " " + xmax + " " +ymax
bounding_boxes.append({"class_name":class_name, "bbox":bbox, "used":False})
# count that object
if class_name in gt_counter_per_class:
gt_counter_per_class[class_name] += 1
else:
# if class didn't exist yet
gt_counter_per_class[class_name] = 1
bbox_mess = ' '.join([class_name, xmin, ymin, xmax, ymax]) + '\n'
with open(f'{ground_truth_dir_path}/{str(index)}_ground_truth.json', 'w') as outfile:
json.dump(bounding_boxes, outfile)
gt_classes = list(gt_counter_per_class.keys())
# sort the classes alphabetically
gt_classes = sorted(gt_classes)
n_classes = len(gt_classes)
compute = YoloV3Calculus()
times = []
json_pred = [[] for i in range(n_classes)]
for index in range(dataset.num_samples):
ann_dataset = dataset.annotations[index]
image_name = ann_dataset[0].split('/')[-1]
original_image, bbox_data_gt = dataset.parse_annotation(ann_dataset, True)
frame = Frame(original_image)
image = ResizeWithBBox(frame).apply([test_input_size, test_input_size])
image_data = image[np.newaxis, ...].astype(np.float32)
t1 = time.time()
pred_bbox = model.predict(image_data)
t2 = time.time()
times.append(t2-t1)
pred_bbox = [tf.reshape(x, (-1, tf.shape(x)[-1])) for x in pred_bbox]
pred_bbox = tf.concat(pred_bbox, axis=0)
bboxes = compute.postprocess_boxes(pred_bbox, original_image, test_input_size, score_threshold)
bboxes = compute.nms(bboxes, iou_threshold, method='nms')
for bbox in bboxes:
coor = np.array(bbox[:4], dtype=np.int32)
score = bbox[4]
class_ind = int(bbox[5])
class_name = num_class[class_ind]
score = '%.4f' % score
xmin, ymin, xmax, ymax = list(map(str, coor))
bbox = xmin + " " + ymin + " " + xmax + " " +ymax
json_pred[gt_classes.index(class_name)].append({"confidence": str(score), "file_id": str(index), "bbox": str(bbox)})
ms = sum(times)/len(times)*1000
fps = 1000 / ms
for class_name in gt_classes:
json_pred[gt_classes.index(class_name)].sort(key=lambda x:float(x['confidence']), reverse=True)
with open(f'{ground_truth_dir_path}/{class_name}_predictions.json', 'w') as outfile:
json.dump(json_pred[gt_classes.index(class_name)], outfile)
# Calculate the AP for each class
sum_AP = 0.0
ap_dictionary = {}
# open file to store the results
with open("mAP/results.txt", 'w') as results_file:
results_file.write("# AP and precision/recall per class\n")
count_true_positives = {}
for class_index, class_name in enumerate(gt_classes):
count_true_positives[class_name] = 0
# Load predictions of that class
predictions_file = f'{ground_truth_dir_path}/{class_name}_predictions.json'
predictions_data = json.load(open(predictions_file))
# Assign predictions to ground truth objects
nd = len(predictions_data)
tp = [0] * nd # creates an array of zeros of size nd
fp = [0] * nd
for idx, prediction in enumerate(predictions_data):
file_id = prediction["file_id"]
# assign prediction to ground truth object if any
# open ground-truth with that file_id
gt_file = f'{ground_truth_dir_path}/{str(file_id)}_ground_truth.json'
ground_truth_data = json.load(open(gt_file))
ovmax = -1
gt_match = -1
# load prediction bounding-box
bb = [ float(x) for x in prediction["bbox"].split() ] # bounding box of prediction
for obj in ground_truth_data:
# look for a class_name match
if obj["class_name"] == class_name:
bbgt = [ float(x) for x in obj["bbox"].split() ] # bounding box of ground truth
bi = [max(bb[0],bbgt[0]), max(bb[1],bbgt[1]), min(bb[2],bbgt[2]), min(bb[3],bbgt[3])]
iw = bi[2] - bi[0] + 1
ih = bi[3] - bi[1] + 1
if iw > 0 and ih > 0:
# compute overlap (IoU) = area of intersection / area of union
ua = (bb[2] - bb[0] + 1) * (bb[3] - bb[1] + 1) + (bbgt[2] - bbgt[0]
+ 1) * (bbgt[3] - bbgt[1] + 1) - iw * ih
ov = iw * ih / ua
if ov > ovmax:
ovmax = ov
gt_match = obj
# assign prediction as true positive/don't care/false positive
if ovmax >= min_overlap:# if ovmax > minimum overlap
if not bool(gt_match["used"]):
# true positive
tp[idx] = 1
gt_match["used"] = True
count_true_positives[class_name] += 1
# update the ".json" file
with open(gt_file, 'w') as f:
f.write(json.dumps(ground_truth_data))
else:
# false positive (multiple detection)
fp[idx] = 1
else:
# false positive
fp[idx] = 1
# compute precision/recall
cumsum = 0
for idx, val in enumerate(fp):
fp[idx] += cumsum
cumsum += val
cumsum = 0
for idx, val in enumerate(tp):
tp[idx] += cumsum
cumsum += val
#print(tp)
rec = tp[:]
for idx, val in enumerate(tp):
rec[idx] = float(tp[idx]) / gt_counter_per_class[class_name]
#print(rec)
prec = tp[:]
for idx, val in enumerate(tp):
prec[idx] = float(tp[idx]) / (fp[idx] + tp[idx])
#print(prec)
ap, mrec, mprec = self.__voc_ap(rec, prec)
sum_AP += ap
text = "{0:.3f}%".format(ap*100) + " = " + class_name + " AP " #class_name + " AP = {0:.2f}%".format(ap*100)
rounded_prec = [ '%.3f' % elem for elem in prec ]
rounded_rec = [ '%.3f' % elem for elem in rec ]
# Write to results.txt
results_file.write(text + "\n Precision: " + str(rounded_prec) + "\n Recall :" + str(rounded_rec) + "\n\n")
print(text)
ap_dictionary[class_name] = ap
results_file.write("\n# mAP of all classes\n")
mAP = sum_AP / n_classes
text = "mAP = {:.3f}%, {:.2f} FPS".format(mAP*100, fps)
results_file.write(text + "\n")
print(text)
return mAP*100
def __voc_ap(self, rec, prec):
"""
--- Official matlab code VOC2012---
mrec=[0 ; rec ; 1];
mpre=[0 ; prec ; 0];
for i=numel(mpre)-1:-1:1
mpre(i)=max(mpre(i),mpre(i+1));
end
i=find(mrec(2:end)~=mrec(1:end-1))+1;
ap=sum((mrec(i)-mrec(i-1)).*mpre(i));
"""
rec.insert(0, 0.0) # insert 0.0 at begining of list
rec.append(1.0) # insert 1.0 at end of list
mrec = rec[:]
prec.insert(0, 0.0) # insert 0.0 at begining of list
prec.append(0.0) # insert 0.0 at end of list
mpre = prec[:]
"""
This part makes the precision monotonically decreasing
(goes from the end to the beginning)
matlab: for i=numel(mpre)-1:-1:1
mpre(i)=max(mpre(i),mpre(i+1));
"""
# matlab indexes start in 1 but python in 0, so I have to do:
# range(start=(len(mpre) - 2), end=0, step=-1)
# also the python function range excludes the end, resulting in:
# range(start=(len(mpre) - 2), end=-1, step=-1)
for i in range(len(mpre)-2, -1, -1):
mpre[i] = max(mpre[i], mpre[i+1])
"""
This part creates a list of indexes where the recall changes
matlab: i=find(mrec(2:end)~=mrec(1:end-1))+1;
"""
i_list = []
for i in range(1, len(mrec)):
if mrec[i] != mrec[i-1]:
i_list.append(i) # if it was matlab would be i + 1
"""
The Average Precision (AP) is the area under the curve
(numerical integration)
matlab: ap=sum((mrec(i)-mrec(i-1)).*mpre(i));
"""
ap = 0.0
for i in i_list:
ap += ((mrec[i]-mrec[i-1])*mpre[i])
return ap, mrec, mpre
[docs] def print_summary(self):
"""Print network summary for debugging purposes."""
self.model.summary()