/
blog-202110.Rmd
347 lines (299 loc) · 17.6 KB
/
blog-202110.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
---
title: "2021, October Update"
author: "Francisco Bischoff"
date: "on October 12, 2021"
output:
bookdown::html_document2:
base_format: workflowr::wflow_html
toc: true
toc_float: true
number_sections: false
bibliography: [../papers/references.bib]
link-citations: true
csl: ../thesis/csl/ama.csl
css: style.css
editor_options:
chunk_output_type: console
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(
echo = FALSE, fig.align = "center", dev = "svg", autodep = TRUE,
fig.height = 5, fig.width = 10,
tidy = "styler",
tidy.opts = list(strict = TRUE)
)
library(here)
library(ggplot2)
library(glue)
library(dplyr)
library(tibble)
library(plotly)
library(kableExtra)
conflicted::conflict_prefer("filter", "dplyr")
options(ggplot2.discrete.fill = c("#1b9e77", "#d95f02"))
# library(visNetwork)
# library(kableExtra)
# library(targets)
my_graphics <- function(image_name, base_path = here::here("docs", "figure")) {
file_path <- glue::glue("{base_path}/{image_name}")
if (knitr::is_latex_output()) {
if (file.exists(glue::glue("{file_path}.pdf"))) {
file_path <- glue::glue("{file_path}.pdf")
} else if (file.exists(glue::glue("{file_path}.png"))) {
file_path <- glue::glue("{file_path}.png")
} else {
file_path <- glue::glue("{file_path}.jpg")
}
} else {
if (file.exists(glue::glue("{file_path}.svg"))) {
file_path <- glue::glue("{file_path}.svg")
} else if (file.exists(glue::glue("{file_path}.png"))) {
file_path <- glue::glue("{file_path}.png")
} else {
file_path <- glue::glue("{file_path}.jpg")
}
}
knitr::include_graphics(file_path)
}
my_kable <- function(title, label, content) {
res <- glue(r"(<br><table class="tg"><caption>)", "(\\#tab:{label}) {title}", r"(</caption>{content}</table>)")
out <- structure(res, format = "html", class = "knitr_kable")
attr(out, "format") <- "html"
out
}
```
## Regime Change Detection
While implementing the streaming-like pipeline, some declarations must be made. In 2017, the FLUSS
(Fast Low-cost Unipotent Semantic Segmentation) and the FLOSS (Fast Low-cost Online Semantic
Segmentation) algorithms were introduced by Gharghabi _et al._ [@gharghabi2017]. In 2018, the same
group published their findings using multi-dimensional time series [@gharghabi2018] using the same
algorithms.
Claims about the algorithm:
- **Domain Agnosticism:** the algorithm makes no assumptions about the data as opposed to most
available algorithms to date.
- **Streaming:** the algorithm can provide real-time information.
- **Real-World Data Suitability:** the objective is not to _explain_ all the data. Therefore, areas
marked as "don't know" areas are acceptable.
- **FLOSS is not:** a change point detection algorithm [@aminikhanghahi2016]. The interest here is
changes in the shapes of a sequence of measurements.
Briefly describing the regime detection algorithm, which can be explored in the original paper
[@gharghabi2018], it is based on the assumption that between two regimes, the most similar shape
(its nearest neighbor) is located on "the same side". This information is obtained from the Matrix
Profile computation. More precisely, using only the Profile Index.
Before talking about the Matrix Profile computation, some findings deserve to be mentioned:
In chapter 3.5 in the original paper, the authors of FLOSS wisely introduce the **temporal
constraint**. Nevertheless, some details are not mentioned. 1) As this algorithm only needs the
Profile Index, should we use the already computed Indexes or recompute the Matrix Profile using this
constraint (i.e., the constraint is on the Profile Index or the FLOSS algorithm?). That is not an
issue about the algorithm but a choice we need to make beforehand. One option is to apply the
constraint on the Profile Index, and we need to have this parameter set from the start. The second
option is to have the FLOSS algorithm not account for the indexes beyond the constraint, keeping the
original Profile Index. 2) The authors declare the correction curve typically used on FLUSS and
FLOSS as "simply a uniform distribution", but this is not an accurate statement. Empirically, there
is a helpful pattern to know about the distribution when using **temporal constraints** (at least
from the start, in the Matrix Profile stage). At first glance, we see that the distribution
resembles the skewed distribution used in FLOSS but is shorter, while the $constraint \ge
MatrixProfileSize/2$. For lower constraints, the maximum value of this distribution is equal to
$MatrixProfileSize/2$ between the indexes $constraint$ to the index $MatrixProfileSize - (constraint
\times 0.9)$. This is shown in Fig. \@ref(fig:distributions). That is important because the
output of the FLOSS algorithm should be normalized and constrained between 0 and 1, which allows us
to compare different trials using different parameters in the process. Finally, the last datapoints
are **not** irrelevant, opposed to what was stated by the authors, since an _Online_ algorithm needs
to return an answer as soon as the application domain requires. That is very much relevant to this
work's field, as, for example, for asystole detection, we have a window of 4 seconds to fire the
alarm. If the time constraint is 10 seconds, this would mean (by the original article) that the last
10 seconds of the incoming data would not be sufficient to detect the regime change.
As for the first point mentioned above, it seems more appropriate to set the temporal constraint in
the Matrix Profile algorithm, and indeed this is what the original paper did. That reduces the
computation time of the online Matrix Profile, and any post-processing done afterward will inherit
this constraint. The distribution for correcting the FLOSS algorithm is also simpler. On the other
hand, it is possible to apply the time constraint in the FLOSS algorithm, leaving the online Matrix
Profile in its original form. See Fig. \@ref(fig:floss-dist). The theoretical distribution
changes significantly according to the constraint value. The upside of this approach, at least
during the prospective phase, is to allow us to decide the time constraint value later in the
pipeline, avoiding the recomputation of the Matrix Profile. The results on detecting regime changes
are very similar to the first approach. See Fig. \@ref(fig:cac-regimes).
Concerning the second point mentioned above, the solution for evaluating the effect of using time
constraints in this work's setting was to generate the ideal distribution using the constrained
parameters beforehand. That gives us enough data to evaluate a regime change accurately utilizing a
minimum of $2 \times WindowSize$ datapoints. The best index is still to be determined, and current
tests are using 3 seconds limit.
```{r dist data, message=FALSE, warning=FALSE, include=FALSE}
source(here("scripts", "common", "compute_floss.R"))
get_dist <- function(mp_const = 1250, floss_const = 0) {
set.seed(2021)
iac <- list()
pro_size <- 5000
mp_time_constraint <- mp_const
floss_time_constraint <- floss_const
for (i in 1:5) {
# DEBUG
# for (i in 1:500) { # RELEASE
iac[[i]] <- get_asym(pro_size, mp_time_constraint, floss_time_constraint)
}
aic_avg <- rowMeans(as.data.frame(iac))
data.frame(index = 1:5000, counts = aic_avg)
}
data_5000 <- get_dist(5000)
data_4250 <- get_dist(4250)
data_2500 <- get_dist(2500)
data_1250 <- get_dist(1250)
floss_data_5000 <- get_dist(0, 5000)
floss_data_4250 <- get_dist(0, 4250)
floss_data_2500 <- get_dist(0, 2500)
floss_data_1250 <- get_dist(0, 1250)
```
```{r distributions, echo=FALSE, fig.cap="1D-IAC distributions for earlier temporal constraint (on Matrix Profile)", message=FALSE, warning=FALSE}
floss_dist <- ggplot(data_5000, aes(index, counts)) +
geom_line(size = 0.1) +
ggtitle("a) No constraint") +
theme_grey(base_size = 7)
floss_4250 <- ggplot(data_4250, aes(index, counts)) +
geom_line(size = 0.1) +
ggtitle("b) Constraint of 4250") +
theme_grey(base_size = 7)
floss_2500 <- ggplot(data_2500, aes(index, counts)) +
geom_line(size = 0.1) +
annotate("segment", y = 0, yend = max(data_2500$counts), x = 2500, xend = 2500, linetype = 2, size = 0.1) +
annotate("text", x = 2500 - 80, y = 40, label = "start", color = "black", size = 2, angle = 90, hjust = 0) +
annotate("segment", y = 0, yend = max(data_2500$counts), x = 5000 - 2500 * 0.9, xend = 5000 - 2500 * 0.9, linetype = 2, size = 0.1) +
annotate("text", x = 5000 - 2500 * 0.9 - 80, y = 40, label = "end", color = "black", size = 2, angle = 90, hjust = 0) +
ggtitle("c) Constraint of 2500") +
theme_grey(base_size = 7)
floss_1250 <- ggplot(data_1250, aes(index, counts)) +
geom_line(size = 0.1) +
annotate("segment", y = 0, yend = max(data_1250$counts), x = 1250, xend = 1250, linetype = 2, size = 0.1) +
annotate("text", x = 1250 - 80, y = 40, label = "start", color = "black", size = 2, angle = 90, hjust = 0) +
annotate("segment", y = 0, yend = max(data_1250$counts), x = 5000 - 1250 * 0.9, xend = 5000 - 1250 * 0.9, linetype = 2, size = 0.1) +
annotate("text", x = 5000 - 1250 * 0.9 - 80, y = 40, label = "end", color = "black", size = 2, angle = 90, hjust = 0) +
ggtitle("d) Constraint of 1250") +
theme_grey(base_size = 7)
gg <- gridExtra::grid.arrange(floss_dist, floss_4250, floss_2500, floss_1250,
nrow = 2, newpage = TRUE,
bottom = grid::textGrob(paste("The plot a) shows the distribution used for the arc count correction when there is no time constraint.", "\n", "b) Shows a constraint of 3/4 of the total. c) 1/2 of the total. d) 1/4 of the total; here we see clearly the flat line.", "\n", "The dashed line marks the start and the end of the uniform zone."), just = "center", gp = grid::gpar(fontsize = 7))
)
rm(gg)
```
```{r floss-dist, echo=FALSE, fig.cap="1D-IAC distributions for later temporal constraint (on FLOSS)", message=FALSE, warning=FALSE}
floss_dist <- ggplot(floss_data_5000, aes(index, counts)) +
geom_line(size = 0.1) +
ggtitle("a) No constraint") +
theme_grey(base_size = 7)
floss_4250 <- ggplot(floss_data_4250, aes(index, counts)) +
geom_line(size = 0.1) +
ggtitle("b) Constraint of 4250") +
theme_grey(base_size = 7)
floss_2500 <- ggplot(floss_data_2500, aes(index, counts)) +
geom_line(size = 0.1) +
annotate("segment", y = 0, yend = max(data_2500$counts), x = 2500, xend = 2500, linetype = 3, size = 0.1) +
annotate("text", x = 2500 - 80, y = 40, label = "start", color = "black", size = 2, angle = 90, hjust = 0) +
annotate("segment", y = 0, yend = max(data_2500$counts), x = 5000 - 2500 * 0.9, xend = 5000 - 2500 * 0.9, linetype = 3, size = 0.1) +
annotate("text", x = 5000 - 2500 * 0.9 - 80, y = 40, label = "end", color = "black", size = 2, angle = 90, hjust = 0) +
ggtitle("c) Constraint of 2500") +
theme_grey(base_size = 7)
floss_1250 <- ggplot(floss_data_1250, aes(index, counts)) +
geom_line(size = 0.1) +
annotate("segment", y = 0, yend = max(data_1250$counts), x = 1250, xend = 1250, linetype = 3, size = 0.1) +
annotate("text", x = 1250 - 80, y = 40, label = "start", color = "black", size = 2, angle = 90, hjust = 0) +
annotate("segment", y = 0, yend = max(data_1250$counts), x = 5000 - 1250 * 0.9, xend = 5000 - 1250 * 0.9, linetype = 3, size = 0.1) +
annotate("text", x = 5000 - 1250 * 0.9 - 80, y = 40, label = "end", color = "black", size = 2, angle = 90, hjust = 0) +
ggtitle("d) Constraint of 1250") +
theme_grey(base_size = 7)
gg <- gridExtra::grid.arrange(floss_dist, floss_4250, floss_2500, floss_1250,
nrow = 2, newpage = TRUE,
bottom = grid::textGrob(paste("The plot a) shows the distribution used for the arc count correction when there is no time constraint.", "\n", "b) Shows a constraint of 3/4 of the total. c) 1/2 of the total. d) 1/4 of the total; here we see clearly the flat line.", "\n", "The dotted line marks the start and the end of the uniform zone if using the constraint in the Matrix Profile."), just = "center", gp = grid::gpar(fontsize = 7))
)
rm(gg)
```
```{r output_data, include=FALSE}
if (file.exists(here("output/work_output_202110.rds"))) {
output <- readRDS(here("output/work_output_202110.rds"))
mp_data <- output$mp_constraint[[1]]$II[[76]]
mp_data <- tibble(time = seq_along(mp_data$cac), cac = mp_data$cac, iac = mp_data$iac, arcs = mp_data$arcs)
floss_data <- output$floss_constraint[[1]]$II[[76]]
floss_data <- tibble(time = seq_along(floss_data$cac), cac = floss_data$cac, iac = floss_data$iac, arcs = floss_data$arcs)
rm(output)
# invisible(gc())
}
```
```{r cac-regimes, echo=FALSE, fig.cap="CAC and Regime detection using early and later IAC", message=FALSE, warning=FALSE}
landmark <- 5000 - 3 * 250
mp_cac_landmark <- mp_data$cac[landmark]
floss_cac_landmark <- floss_data$cac[landmark]
mp_constraint <- ggplot(mp_data, aes(time, cac)) +
geom_line(size = 0.1) +
ylim(0, 1) +
ylab("CAC") +
xlab("") +
annotate("segment", y = 0, yend = 1, x = landmark, xend = landmark, linetype = 1, size = 0.1, color = "red") +
annotate("text", x = landmark, y = mp_cac_landmark, label = sprintf("%.2f", mp_cac_landmark), color = "red", size = 2, vjust = 0, hjust = -0.2) +
ggtitle("a) Constraint of 1250 on Matrix Profile") +
theme_grey(base_size = 7)
floss_constraint <- ggplot(floss_data, aes(time, cac)) +
geom_line(size = 0.1) +
ylim(0, 1) +
ylab("") +
xlab("") +
annotate("segment", y = 0, yend = 1, x = landmark, xend = landmark, linetype = 1, size = 0.1, color = "red") +
annotate("text", x = landmark, y = floss_cac_landmark, label = sprintf("%.2f", floss_cac_landmark), color = "red", size = 2, vjust = 0, hjust = -0.2) +
ggtitle("b) Constraint of 1250 on FLOSS") +
theme_grey(base_size = 7)
mp_arcs <- ggplot(mp_data, aes(time, arcs)) +
geom_line(size = 0.1) +
geom_line(aes(time, iac), size = 0.1, color = "red") +
ylab("Arcs and IAC") +
annotate("segment", y = 0, yend = 900, x = 1250, xend = 1250, linetype = 3, size = 0.1) +
annotate("segment", y = 0, yend = 900, x = 5000 - 1250 * 0.9, xend = 5000 - 1250 * 0.9, linetype = 3, size = 0.1) +
theme_grey(base_size = 7)
floss_arcs <- ggplot(floss_data, aes(time, arcs)) +
geom_line(size = 0.1) +
geom_line(aes(time, iac), size = 0.1, color = "red") +
ylab("") +
annotate("segment", y = 0, yend = 800, x = 1250, xend = 1250, linetype = 3, size = 0.1) +
annotate("segment", y = 0, yend = 800, x = 5000 - 1250 * 0.9, xend = 5000 - 1250 * 0.9, linetype = 3, size = 0.1) +
theme_grey(base_size = 7)
gg <- gridExtra::grid.arrange(mp_constraint, floss_constraint, mp_arcs, floss_arcs,
nrow = 2, newpage = TRUE,
bottom = grid::textGrob(paste(
"The plots on a) show above the Corrected Arc Count (CAC) and below the raw arc counts (black) and the ideal arc count (IAC) (red) using the temporal\n constraint earlier on the Matrix Profile.",
"The plots on b) show above the Corrected Arc Count (CAC) and below the raw arc counts (black) and\nthe ideal arc count (IAC) (red) using the temporal constraint later on the FLOSS algorithm.",
"The red vertical line marks the point\nwhere the current algorithm watches for regime changes."
),
just = "center", gp = grid::gpar(fontsize = 7)
)
)
rm(gg)
```
## The Matrix Profile Algorithm
Since the first Matrix Profile computation algorithm, the STAMP [@yeh2016], several improvements on
the algorithm were made [@zhu2016; @zhu2018]. Still, the ability to keep a growing Matrix Profile
(i.e., _Online_) relies on the STAMP algorithm. If the problem allows collecting several data points
(chunks), STOMP [@zhu2016] can speed up the computation. Curiously, the main bottleneck of all these
algorithms is the FFT (Fast Fourier Transform) algorithm that is the core of the MASS algorithm
published by Mueen _et al._ [@mueen2010] in 2010 and later in 2015, having its code released on
Professor Mueen's webpage [@mass2015]. The FFT libraries available are highly optimized and CPU (or
GPU) dependent what makes it at the same time fast but brittle and not suitable for MCU's
(Microcontroller Unit), for example. More interestingly yet, is the fact that several published
works using Matrix Profile, MPdist [@gharghabi2018a], for instance, uses an unpublished algorithm
called 'MPX' that computes the Matrix Profile using cross-correlation methods ending up faster and
is easily portable.
This work contributes to extending the MPX algorithm to allow the _Online_ computation of the Matrix
Profile. More precisely, we are interested in the Right Matrix Profile, whose updated indexes refer
only to the last incoming datapoint since we are looking for future regime changes, not backward.
This one-directional algorithm is already described in the FLOSS paper [@gharghabi2018].
Another contribution of this work is an unexplored constraint that we could apply on building the
Matrix Profile that we will call _Similarity Threshold_ (ST). The original work outputs the
similarity values in Euclidean Distance (ED) values, while MPX naturally outputs the values in
Pearson's correlation (CC) coefficients. Both ED and CC are interchangeable using the equation
\@ref(eq:correlation). However, we may argue that it is easier to compare values that do not depend on the
$WindowSize$ during an exploratory phase. MPX happens to naturally return values in CC, saving a few
more computation time.
$$
CC = 1 - \frac{ED}{(2 \times WindowSize)} (\#eq:correlation)
$$
The ST is an interesting factor that we can use, especially when detecting pattern changes during
time. The FLUSS/FLOSS algorithms rely on counting references between indexes in the time series. ST
can help remove "noise" from these references since only similar patterns above a certain threshold
are referenced, and changes have more impact on these counts. More information and visual content on
ST will be provided later. The best ST threshold is still to be determined.
## References {-}