Diff of /R/metrics.R [000000] .. [409433]

Switch to unified view

a b/R/metrics.R
1
#' F1 metric
2
#' 
3
#' Compute F1 metric. If loss is `"categorical_crossentropy"`, number of targets must be 2. If
4
#' loss is `"binary_crossentropy"` and number of targets > 1, will flatten `y_true` and `y_pred` matrices 
5
#' to a single vector (rather than computing separate F1 scores for each class).
6
#'
7
#' @param num_targets Size of model output.
8
#' @param loss Loss function of model.
9
#' @examplesIf reticulate::py_module_available("tensorflow")
10
#' 
11
#' y_true <- c(1,0,0,1,1,0,1,0,0)  
12
#' y_pred <-  c(0.9,0.05,0.05,0.9,0.05,0.05,0.9,0.05,0.05) 
13
#' \donttest{
14
#' library(keras)
15
#' f1_metric <- f1_wrapper(3L, "binary_crossentropy")
16
#' f1_metric$update_state(y_true, y_pred)
17
#' f1_metric$result()  
18
#' 
19
#' 
20
#' # add metric to a model
21
#' 
22
#' num_targets <- 1
23
#' model <- create_model_lstm_cnn(maxlen = 20,
24
#'                                layer_lstm = 8,
25
#'                                bal_acc = FALSE,
26
#'                                last_layer_activation = "sigmoid",
27
#'                                loss_fn = "binary_crossentropy",
28
#'                                layer_dense = c(8, num_targets))
29
#' 
30
#' f1_metric <- f1_wrapper(num_targets, loss = model$loss)
31
#' model %>% keras::compile(loss = model$loss, 
32
#'                          optimizer = model$optimizer,
33
#'                          metrics = c(model$metrics, f1_metric))
34
#' }                    
35
#' @returns A keras metric.                          
36
#' @export
37
f1_wrapper <- function(num_targets = 2, loss = "binary_crossentropy") {
38
  
39
  stopifnot(loss %in% c("binary_crossentropy", "categorical_crossentropy"))
40
  
41
  if (loss == "binary_crossentropy" & num_targets > 1) {
42
    message("Will flatten y_true and y_pred matrices to a single vector for evaluation
43
            rather than computing separate F1 scores for each class and taking the mean.")
44
  }
45
  
46
  if (loss == "categorical_crossentropy" & num_targets != 2) {
47
    stop("Output size must be two, when loss is categorical_crossentropy")
48
  }
49
  
50
  f1_stateful <- reticulate::PyClass("f1",
51
                                     inherit = tensorflow::tf$keras$metrics$Metric,
52
                                     list(
53
                                       
54
                                       `__init__` = function(self, num_targets, loss) {
55
                                         super()$`__init__`(name = "f1")
56
                                         self$num_targets <- num_targets
57
                                         self$f1_score <- 0
58
                                         self$loss <- loss
59
                                         self$rc <- tensorflow::tf$keras$metrics$Recall()
60
                                         self$pr <- tensorflow::tf$keras$metrics$Precision()
61
                                         NULL
62
                                       },
63
                                       
64
                                       update_state = function(self, y_true, y_pred, sample_weight = NULL) {
65
                                         if (self$loss == "binary_crossentropy") {
66
                                           self$rc$update_state(y_true, y_pred)
67
                                           self$pr$update_state(y_true, y_pred)
68
                                         } else {
69
                                           self$rc$update_state(y_true[ , 1], y_pred[ , 1])
70
                                           self$pr$update_state(y_true[ , 1], y_pred[ , 1])
71
                                         }
72
                                         NULL
73
                                       },
74
                                       
75
                                       result = function(self) {
76
                                         self$f1_score <- self$compute_f1()
77
                                         return(self$f1_score)
78
                                       },
79
                                       
80
                                       compute_f1 = function(self) {
81
                                         f1 <- (2 * self$pr$result() * self$rc$result())/(self$pr$result() + self$rc$result() + tensorflow::tf$constant(1e-15))
82
                                         return(f1)
83
                                       },
84
                                       
85
                                       reset_state = function(self) {
86
                                         self$rc$reset_state()
87
                                         self$pr$reset_state()
88
                                         #self$f1_score$assign(0)
89
                                         NULL
90
                                       }
91
                                       
92
                                     ))
93
  
94
  return(f1_stateful(num_targets = num_targets, loss = loss))
95
}
96
97
98
#' Balanced accuracy metric
99
#'
100
#' Compute balanced accuracy as additional score. Useful for imbalanced data. Only implemented for 
101
#' model with mutually exclusive targets.
102
#'
103
#' @param num_targets Number of targets.
104
#' @param cm_dir Directory of confusion matrix used to compute balanced accuracy.
105
#' @examplesIf reticulate::py_module_available("tensorflow")
106
#' 
107
#' y_true <- c(1,0,0,1,
108
#'             0,1,0,0,
109
#'             0,0,1,0) %>% matrix(ncol = 3)
110
#' y_pred <- c(0.9,0.1,0.2,0.1,
111
#'             0.05,0.7,0.2,0.0,
112
#'             0.05,0.2,0.6,0.9) %>% matrix(ncol = 3)
113
#' 
114
#' cm_dir <- tempfile() 
115
#' dir.create(cm_dir)
116
#' \donttest{
117
#' bal_acc_metric <- balanced_acc_wrapper(num_targets = 3L, cm_dir = cm_dir)
118
#' bal_acc_metric$update_state(y_true, y_pred)
119
#' bal_acc_metric$result()
120
#' as.array(bal_acc_metric$cm)
121
#' }
122
#' 
123
#' @returns A keras metric.                          
124
#' @export
125
balanced_acc_wrapper <- function(num_targets, cm_dir) {
126
  balanced_acc_stateful <- reticulate::PyClass("balanced_acc",
127
                                               inherit = tensorflow::tf$keras$metrics$Metric,
128
                                               list(
129
                                                 
130
                                                 `__init__` = function(self, num_targets, cm_dir) {
131
                                                   super()$`__init__`(name = "balanced_acc")
132
                                                   self$num_targets <- num_targets
133
                                                   self$cm_dir <- cm_dir
134
                                                   self$count <- 0
135
                                                   self$cm <- self$add_weight(name = "cm_matrix", shape = c(num_targets, num_targets), initializer="zeros")
136
                                                   NULL
137
                                                 },
138
                                                 
139
                                                 update_state = function(self, y_true, y_pred, sample_weight = NULL) {
140
                                                   self$cm$assign_add(self$compute_cm(y_true, y_pred))
141
                                                   NULL
142
                                                 },
143
                                                 
144
                                                 result = function(self) {
145
                                                   balanced_acc <- self$compute_balanced_acc()
146
                                                   #self$store_cm()
147
                                                   return(balanced_acc)
148
                                                 },
149
                                                 
150
                                                 compute_cm = function(self, y_true, y_pred) {
151
                                                   labels <- tensorflow::tf$math$argmax(y_true, axis = 1L)
152
                                                   predictions <- tensorflow::tf$math$argmax(y_pred, axis = 1L)
153
                                                   current_cm <- tensorflow::tf$math$confusion_matrix(
154
                                                     labels = labels, predictions = predictions,
155
                                                     dtype = "float32", num_classes = self$num_targets)
156
                                                   current_cm <- tensorflow::tf$transpose(current_cm)
157
                                                   return(current_cm)
158
                                                 },
159
                                                 
160
                                                 compute_balanced_acc = function(self) {
161
                                                   diag <- tensorflow::tf$linalg$diag_part(self$cm)
162
                                                   col_sums <- tensorflow::tf$math$reduce_sum(self$cm, axis=0L)
163
                                                   average_per_class <- tensorflow::tf$math$divide(diag, col_sums)
164
                                                   nan_index <- tensorflow::tf$math$logical_not(tensorflow::tf$math$is_nan(average_per_class))
165
                                                   average_per_class <- tensorflow::tf$boolean_mask(average_per_class, nan_index)
166
                                                   acc_sum <- tensorflow::tf$math$reduce_sum(average_per_class)
167
                                                   balanced_acc <- tensorflow::tf$math$divide(acc_sum, tensorflow::tf$math$count_nonzero(col_sums, dtype= acc_sum$dtype))
168
                                                   return(balanced_acc)
169
                                                 },
170
                                                 
171
                                                 reset_state = function(self) {
172
                                                   self$store_cm()
173
                                                   self$count <- self$count + 1
174
                                                   self$cm$assign_sub(self$cm)
175
                                                   NULL
176
                                                 },
177
                                                 
178
                                                 store_cm = function(self) {
179
                                                   #if (self$count > 0) {
180
                                                   if (self$count %% 2 != 0) {
181
                                                     file_name <- file.path(self$cm_dir, paste0("cm_val_", floor(self$count/2), ".rds"))
182
                                                   } else {
183
                                                     file_name <- file.path(self$cm_dir, paste0("cm_train_", floor(self$count/2), ".rds"))
184
                                                   }
185
                                                   saveRDS(keras::k_eval(self$cm), file_name)
186
                                                   NULL
187
                                                   #}
188
                                                 }
189
                                                 
190
                                               ))
191
  return(balanced_acc_stateful(num_targets = num_targets, cm_dir = cm_dir))
192
}
193
194
195
#' Mean AUC score
196
#'
197
#' Compute AUC score as additional metric. If model has several output neurons with binary crossentropy loss, will use the average score.
198
#'
199
#' @param model_output_size Number of neurons in model output layer.
200
#' @param loss Loss function of model, for which metric will be applied to; must be `"binary_crossentropy"`
201
#' or `"categorical_crossentropy"`.
202
#' @examplesIf reticulate::py_module_available("tensorflow")
203
#' 
204
#' y_true <- c(1,0,0,1,1,0,1,0,0) %>% matrix(ncol = 3)
205
#' y_pred <- c(0.9,0.05,0.05,0.9,0.05,0.05,0.9,0.05,0.05) %>% matrix(ncol = 3)
206
#' 
207
#' \donttest{
208
#' library(keras)
209
#' auc_metric <- auc_wrapper(3L, "binary_crossentropy")
210
#' 
211
#' auc_metric$update_state(y_true, y_pred)
212
#' auc_metric$result()  
213
#' 
214
#' # add metric to a model
215
#' num_targets <- 4
216
#' model <- create_model_lstm_cnn(maxlen = 20,
217
#'                                layer_lstm = 8,
218
#'                                bal_acc = FALSE,
219
#'                                last_layer_activation = "sigmoid",
220
#'                                loss_fn = "binary_crossentropy",
221
#'                                layer_dense = c(8, num_targets))
222
#' 
223
#' auc_metric <- auc_wrapper(num_targets, loss = model$loss)
224
#' model %>% keras::compile(loss = model$loss, 
225
#'                          optimizer = model$optimizer,
226
#'                          metrics = c(model$metrics, auc_metric))
227
#' }
228
#' @returns A keras metric.                          
229
#' @export
230
auc_wrapper <- function(model_output_size,
231
                        loss = "binary_crossentropy") {
232
  
233
  multi_label <- FALSE
234
  stopifnot(loss %in% c("binary_crossentropy", "categorical_crossentropy"))
235
  
236
  if (loss == "categorical_crossentropy" & model_output_size != 2) {
237
    stop("Output size must be two, when loss is categorical_crossentropy")
238
  }
239
  
240
  if (loss == "categorical_crossentropy") {
241
    label_weights <- c(1L, 0L)
242
  } else {
243
    label_weights <- NULL
244
  }
245
  
246
  if (loss == "binary_crossentropy" & model_output_size > 1) {
247
    multi_label <- TRUE
248
  }
249
  
250
  metric_name <- ifelse(loss == "binary_crossentropy" & model_output_size > 1,
251
                        "mean_AUC", "AUC")
252
  
253
  auc_metric <- tensorflow::tf$keras$metrics$AUC(label_weights = label_weights,
254
                                                 multi_label = multi_label)
255
  
256
  return(auc_metric)
257
}
258
259
#' Loss function for label noise
260
#' 
261
#' Implements approach from this [paper](https://arxiv.org/abs/1609.03683) and code from 
262
#' [here](https://github.com/giorgiop/loss-correction/blob/15a79de3c67c31907733392085c333547c2f2b16/loss.py#L16-L21). 
263
#' Can be used if labeled data contains noise, i.e. some of the data is labeled wrong.
264
#'
265
#' @param noise_matrix Matrix of noise distribution.
266
#' @importFrom magrittr "%>%"
267
#' @examplesIf reticulate::py_module_available("tensorflow")
268
#' # If first label contains 5% wrong labels and second label no noise
269
#' noise_matrix <- matrix(c(0.95, 0.05, 0, 1), nrow = 2, byrow = TRUE)
270
#' noisy_loss <- noisy_loss_wrapper(noise_matrix)
271
#' 
272
#' @returns A function implementing noisy loss.                          
273
#' @export
274
noisy_loss_wrapper <- function(noise_matrix) {
275
  inverted_noise_matrix <- solve(noise_matrix)
276
  inverted_noise_matrix <- tensorflow::tf$cast(inverted_noise_matrix, dtype = "float32")
277
  noisy_loss <- function(y_true, y_pred) {
278
    y_pred <- y_pred / keras::k_sum(y_pred, axis = -1, keepdims = TRUE)
279
    y_pred <- keras::k_clip(y_pred, tensorflow::tf$keras$backend$epsilon(), 1.0 - tensorflow::tf$keras$backend$epsilon())
280
    loss <- -1 * keras::k_sum(keras::k_dot(y_true, inverted_noise_matrix) * keras::k_log(y_pred), axis=-1)
281
    return(loss)
282
  }
283
  noisy_loss
284
}
285
286
cpcloss <- function(latents,
287
                    context,
288
                    target_dim = 64,
289
                    emb_scale = 0.1 ,
290
                    steps_to_ignore = 2,
291
                    steps_to_predict = 3,
292
                    steps_skip = 1,
293
                    batch_size = 32,
294
                    k = 5,
295
                    train_type = "cpc") {
296
  # define empty lists for metrics
297
  loss <- list()
298
  acc <- list()
299
  # create context tensor
300
  ctx <- context(latents)
301
  c_dim <- latents$shape[[2]]
302
  
303
  # loop for different distances of predicted patches
304
  for (i in seq(steps_to_ignore, (steps_to_predict - 1), steps_skip)) {
305
    # define patches to be deleted
306
    c_dim_i <- c_dim - i - 1
307
    if (train_type == "Self-GenomeNet") {
308
      steps_to_ignore <- 1
309
      steps_to_predict <- 2
310
      steps_skip <- 1
311
      target_dim <- ctx$shape[[3]]
312
      ctx_conv <-
313
        ctx %>% keras::layer_conv_1d(kernel_size = 1, filters = target_dim)
314
      logits <- tensorflow::tf$zeros(list(0L,as.integer(batch_size*2)))
315
      for (j in seq_len(c_dim - (i + 1))) {
316
        basepos <- ctx_conv[, j,] %>% keras::k_reshape(c(-1, target_dim))
317
        targetpos <-
318
          ctx[, (c_dim - j - i), ] %>% keras::k_reshape(c(-1, target_dim))
319
        logits_j <- tensorflow::tf$matmul(basepos, tensorflow::tf$transpose(targetpos))
320
        logits <- tensorflow::tf$concat(list(logits, logits_j), axis = 0L)
321
      }
322
      # define labels
323
      labels <-
324
        rep(c(seq(batch_size, (
325
          2 * batch_size - 1
326
        )), (seq(
327
          0, (batch_size - 1)
328
        ))), (c_dim - (i + 1))) %>% as.integer()
329
    } else {
330
      c_dim_i <- c_dim - i - 1
331
      # define total number of elements in context tensor
332
      total_elements <- batch_size * c_dim_i
333
      # add conv layer and reshape tensor for matrix multiplication
334
      targets <-
335
        latents %>% keras::layer_conv_1d(kernel_size = 1, filters = target_dim) %>% keras::k_reshape(c(-1, target_dim))
336
      # add conv layer and reshape for matrix multiplication
337
      preds_i <-
338
        ctx %>% keras::layer_conv_1d(kernel_size = 1, filters = target_dim)
339
      preds_i <- preds_i[, (1:(c_dim - i - 1)),]
340
      preds_i <- keras::k_reshape(preds_i, c(-1, target_dim)) * emb_scale
341
      
342
      # define logits normally
343
      logits <- tensorflow::tf$matmul(preds_i, tensorflow::tf$transpose(targets))
344
      
345
      # get position of labels
346
      b <- floor(seq(0, total_elements - 1) / c_dim_i)
347
      col <- seq(0, total_elements - 1) %% c_dim_i
348
      
349
      # define labels
350
      labels <- b * c_dim + col + (i + 1)
351
      labels <- as.integer(labels)
352
      
353
    }
354
    # calculate loss and accuracy for each step
355
    loss[[length(loss) + 1]] <-
356
      tensorflow::tf$nn$sparse_softmax_cross_entropy_with_logits(labels, logits) %>%
357
      tensorflow::tf$stack(axis = 0) %>% tensorflow::tf$reduce_mean()
358
    acc[[length(acc) + 1]] <-
359
      tensorflow::tf$keras$metrics$sparse_top_k_categorical_accuracy(tensorflow::tf$cast(labels, dtype = "int64"), logits, as.integer(k)) %>%
360
      tensorflow::tf$stack(axis = 0) %>% tensorflow::tf$reduce_mean()
361
  }
362
  # convert to tensor for output
363
  loss <- loss %>% tensorflow::tf$stack(axis = 0) %>% tensorflow::tf$reduce_mean()
364
  acc <- acc %>% tensorflow::tf$stack(axis = 0) %>% tensorflow::tf$reduce_mean()
365
  return(tensorflow::tf$stack(list(loss, acc)))
366
}
367
368
#' Stochastic Gradient Descent with Warm Restarts
369
#' 
370
#' Compute the learning Rate for a given epoch using Stochastic Gradient Descent with Warm Restarts. Implements approach from this [paper](https://arxiv.org/abs/1608.03983).
371
#'
372
#' @param lrmin Lower limit of the range for the learning rate.
373
#' @param lrmax Upper limit of the range for the learning rate.
374
#' @param restart Number of epochs until a restart is conducted.
375
#' @param mult Factor, by which the number of epochs until a restart is increased at every restart.
376
#' @param epoch Epoch, for which the learning rate shall be calculated.
377
#' @examples 
378
#' sgdr(lrmin = 5e-10, lrmax = 5e-2, restart = 50,
379
#' mult = 1, epoch = 5)
380
#' 
381
#' @returns A numeric value.
382
#' @export
383
sgdr <- function(lrmin = 5e-10,
384
                 lrmax = 5e-2,
385
                 restart = 50,
386
                 mult = 1,
387
                 epoch = NULL) {
388
  iter <- c()
389
  position <- c()
390
  i <- 0
391
  while (length(iter) < epoch) {
392
    iter <- c(iter, rep(i, restart * mult ^ i))
393
    position <- c(position, c(1:(restart * mult ^ i)))
394
    i <- i + 1
395
  }
396
  restart2 <- (restart * mult ^ iter[epoch])
397
  epoch <- position[epoch]
398
  return(lrmin + 1 / 2 * (lrmax - lrmin) * (1 + cos((epoch / restart2) * pi)))
399
}
400
401
#' Step Decay
402
#' 
403
#' Compute the learning Rate for a given epoch using Step Decay.
404
#'
405
#' @param lrmax Upper limit of the range for the learning rate.
406
#' @param newstep Number of epochs until the learning rate is reduced.
407
#' @param mult Factor, by which the number of epochs until a restart is decreased after a new step.
408
#' @param epoch Epoch, for which the learning rate shall be calculated.
409
#' @examples
410
#' stepdecay(lrmax = 0.005, newstep = 50,
411
#' mult = 0.7, epoch = 3)
412
#' 
413
#' @returns A numeric value.
414
#' @export
415
stepdecay <- function(lrmax = 0.005,
416
                      newstep = 50,
417
                      mult = 0.7,
418
                      epoch = NULL) {
419
  return(lrmax * (mult ^ (floor((
420
    epoch
421
  ) / newstep))))
422
}
423
424
#' Exponential Decay
425
#' 
426
#' Compute the learning Rate for a given epoch using Exponential Decay.
427
#'
428
#' @param lrmax Upper limit of the range for the learning rate.
429
#' @param mult Factor, by which the number of epochs until a restart is decreased after a new step.
430
#' @param epoch Epoch, for which the learning rate shall be calculated.
431
#' @examples
432
#' exp_decay(lrmax = 0.005, mult = 0.1, epoch = 8) 
433
#' 
434
#' @returns A numeric value.
435
#' @export
436
exp_decay <- function(lrmax = 0.005,
437
                      mult = 0.1,
438
                      epoch = NULL) {
439
  return(lrmax * exp(-mult * epoch))
440
}
441
442
443
euclidean_distance <- function(vects) {
444
  x <- vects[[1]]
445
  y <- vects[[2]]
446
  sum_square <- tensorflow::tf$math$reduce_sum(tensorflow::tf$math$square(x - y), axis=1L, keepdims=TRUE)
447
  return(tensorflow::tf$math$sqrt(tensorflow::tf$math$maximum(sum_square, tensorflow::tf$keras$backend$epsilon())))
448
}
449
450
cosine_similarity <- function(vects) {
451
  x <- vects[[1]]
452
  y <- vects[[2]]
453
  xy_dot <- tensorflow::tf$math$reduce_sum(x*y, axis=1L, keepdims=TRUE)
454
  x_norm <- tensorflow::tf$math$sqrt(tensorflow::tf$math$reduce_sum(tensorflow::tf$math$square(x), axis=1L, keepdims=TRUE))
455
  y_norm <- tensorflow::tf$math$sqrt(tensorflow::tf$math$reduce_sum(tensorflow::tf$math$square(y), axis=1L, keepdims=TRUE))
456
  return(xy_dot/(x_norm*y_norm))
457
}
458
459
#' Contrastive loss 
460
#' 
461
#' Contrastive loss as used here: https://keras.io/examples/vision/siamese_contrastive/.
462
#'
463
#' @param margin Integer, baseline for distance for which pairs should be classified as dissimilar.
464
#' @examplesIf reticulate::py_module_available("tensorflow")
465
#' cl <- loss_cl(margin=1)
466
#' 
467
#' @returns A function implementing contrastive loss.
468
#' @export
469
loss_cl <- function(margin=1) {
470
  
471
  contrastive_loss <- function(y_true, y_pred) {
472
    
473
    square_pred <- tensorflow::tf$math$square(y_pred)
474
    margin_square <- tensorflow::tf$math$square(tensorflow::tf$math$maximum(margin - (y_pred), 0))
475
    l <- tensorflow::tf$math$reduce_mean(
476
      (1 - y_true) * square_pred + (y_true) * margin_square
477
    )
478
    return(l)
479
  }
480
  
481
  return(contrastive_loss)
482
  
483
}
484
485
#' Focal loss for two or more labels
486
#' 
487
#' @param y_true Vector of true values.
488
#' @param y_pred Vector of predicted values.
489
#' @param gamma Focusing parameter.
490
#' @param alpha Vector of weighting factors.
491
#' @examplesIf reticulate::py_module_available("tensorflow")
492
#' y_true <- matrix(c(0, 1, 0, 0, 0, 1), nrow = 2, byrow = TRUE)
493
#' y_pred <- matrix(c(0.15, 0.8, 0.05,
494
#'                    0.08, 0.02, 0.9), nrow = 2, byrow = TRUE) 
495
#' fl <- focal_loss_multiclass(y_true, y_pred)
496
#' fl$numpy()
497
#' 
498
#' @returns A function implementing focal loss.
499
#' @export
500
focal_loss_multiclass <- function(y_true, y_pred, gamma = 2.5, alpha = c(1)) {
501
  y_pred <- keras::k_clip(y_pred, keras::k_epsilon(), 1.0 - keras::k_epsilon())
502
  cd_loss <- -y_true * keras::k_log(y_pred) # categorical cross entropy
503
  fl_loss <- alpha * keras::k_pow(1. - y_pred, gamma) * cd_loss
504
  return(keras::k_sum(fl_loss, axis = -1))
505
}