This is an old revision of the document!
Time Series Clustering Using Self-Organizing Maps
Similarity Distances
library(zoo)
library(kohonen)
library(ggplot2)
library(dtw)
# Two time series
X <- c(1,1,1,4,4,4,4,4,1,1)
Y <- c(1,1,4,4,4,4,4,1,1,1)
# Euclidean distance
TSdist::DTWDistance(X, Y)
[1] 0
# Euclidean distance
TSdist::EuclideanDistance(X, Y)
[1] 4.242641
# DTW distance
alignment<-dtw::dtw(X, Y, keep.internals = TRUE, step=symmetric1, k=T)
plot(alignment)
plot(alignment,type="threeway")
alignment$localCostMatrix
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
[1,] 0 0 3 3 3 3 3 0 0 0
[2,] 0 0 3 3 3 3 3 0 0 0
[3,] 0 0 3 3 3 3 3 0 0 0
[4,] 3 3 0 0 0 0 0 3 3 3
[5,] 3 3 0 0 0 0 0 3 3 3
[6,] 3 3 0 0 0 0 0 3 3 3
[7,] 3 3 0 0 0 0 0 3 3 3
[8,] 3 3 0 0 0 0 0 3 3 3
[9,] 0 0 3 3 3 3 3 0 0 0
[10,] 0 0 3 3 3 3 3 0 0 0
alignment$distance
[1] 0
alignment$stepsTaken
[1] 3 1 1 1 1 1 1 1 2 1
plot(alignment$index1,alignment$index2,main="Warping function");
plot(dtw::dtw(X, Y, keep.internals = TRUE, step.pattern =rabinerJuangStepPattern(6,"c")),
type="twoway",offset=-2)
dtwPlotTwoWay(dtw(X, Y, step = asymmetricP1, keep = T))
rabinerJuangStepPattern(6,"c")
Step pattern recursion:
g[i,j] = min(
g[i-3,j-2] + d[i-2,j-1] + d[i-1,j ] + d[i ,j ] ,
g[i-1,j-1] + d[i ,j ] ,
g[i-2,j-3] + d[i-1,j-2] + d[i ,j-1] + 0 * d[i ,j ] ,
)
Normalization hint: N
plot(rabinerJuangStepPattern(6,"c"))
# Plot
zoo::plot.zoo(cbind(X, Y), plot.type = "multiple", col = c("red", "blue"), lwd = 2)
#Creates time series
ts1_x <- c(1,2,3,4,5,6,7,8,9,10)
ts1_y <- c(1,1,1,4,4,4,4,4,1,1)
ts1 <- zoo::zoo(ts1_y, ts1_x);
#Creates time series
ts2_x <- c(1,2,3,4,5,6,7,8,9,10)
ts2_y <- c(1,1,4,4,4,4,4,1,1,1)
ts2 <- zoo::zoo(ts1_x, ts1_y);
some methods for <U+393C><U+3E33>zoo<U+393C><U+3E34> objects do not work if the index entries in <U+393C><U+3E31>order.by<U+393C><U+3E32> are not unique
Plot time series
zoo::plot.zoo(cbind(ts2_y, ts1_y), plot.type = "multiple", col = c("red", "blue"), lwd = 2)
TSdist::EuclideanDistance(ts1_y, ts2_y)
[1] 4.242641
TSdist::DTWDistance(ts1_y, ts2_y)
[1] 0
Read the time series from csv file and plot all
#read time series from file
timeseries.zoo <- read.zoo("timeseries.csv", header = TRUE, sep = ",")
#zoo::plot.zoo(z, main = "Set of time series")
#Define a color for each group (4 groups)
colors<-c("#FF0000FF","#FF0000FF","#FF0000FF","#FF0000FF",
"#00FF9FFF","#00FF9FFF","#00FF9FFF","#00FF9FFF",
"#009FFFFF","#009FFFFF","#009FFFFF","#009FFFFF",
"#DFFF00FF","#DFFF00FF","#DFFF00FF","#DFFF00FF")
#Plot all time series
zoo::plot.zoo(timeseries.zoo, plot.type = "multiple", col = colors, lwd = 2, main= "Set of time series")
Self-organizing maps using Kohonen package
# SOM
somgrid=kohonen::somgrid(xdim=4, ydim=4, topo="rectangular", neighbourhood.fct = "gaussian")
som<-kohonen::supersom(t(as.matrix(timeseries.zoo)),somgrid,rlen = 100, alpha=1)
plot(som,"codes",codeRendering="lines")
The time series were allocated in these neurons. Each number represents the number of neuron.
Example: the position 1 of vector represents the time series 1 in a dataset, this time series was allocated in neuron 9.
neuron_timeseries<-som$unit.classif
info_timeSeries<- data.frame(time_series=(rownames(t(timeseries.zoo))),
neuron=as.integer(som$unit.classif))
codes<-zoo::zoo(t(som$codes[[1]]))
zoo::plot.zoo(codes, plot.type = "multiple", col = "black", lwd = 2, main= "Weight of neurons")
#plot all time series (neurons)
ggplot2::autoplot((codes), facet = NULL) + ylab("y") +xlab("time") + geom_line(size = 1) + ggtitle("Weight of neurons")
library(proxy)
library(stats)
## use hierarchical clustering to cluster the codebook vectors.
# Cut dendrogram in 4 groups
hc <- stats::hclust(dist(t(codes)),method = "ward.D2")
#Cuts a tree, e.g., as resulting from hclust, into several groups either
#by specifying the desired number(s) of groups or the cut height(s).
som_cluster <- stats::cutree(hc, 4)
library(dendextend)
#plot dendrogram
dend <- hc%>% as.dendrogram %>%
set("branches_k_color", k = 4) %>% set("branches_lwd", 1.2) %>%
set("labels_cex", 0.8) %>% set("labels_colors", k = 4) %>%
set("leaves_pch", 19) %>% set("leaves_cex", 0.5)
ggd1 <- as.ggdend(dend)
ggplot(ggd1, horiz = FALSE) + ggtitle("Dendrogram")
#Cluster is created by a set of neurons.
cluster<-data.frame(cluster=som_cluster,neuron= 1:dim(codes)[2])
#Timeseries, neuron and cluster
cluster_info<-merge(cluster,info_timeSeries, by="neuron")
cluster_info[order(cluster_info$time_series),]
neuron cluster time_series
5 2 1 t1_1
7 2 1 t1_10
20 5 1 t1_2
2 1 1 t1_3
3 1 1 t1_4
6 2 1 t1_5
1 1 1 t1_6
4 1 1 t1_7
8 2 1 t1_8
9 2 1 t1_9
15 4 2 t2_1
14 3 2 t2_10
11 3 2 t2_2
16 4 2 t2_3
10 3 2 t2_4
19 4 2 t2_5
13 3 2 t2_6
18 4 2 t2_7
12 3 2 t2_8
17 4 2 t2_9
25 12 4 t3_1
29 12 4 t3_10
30 12 4 t3_2
38 16 4 t3_3
28 12 4 t3_4
26 12 4 t3_5
27 12 4 t3_6
40 16 4 t3_7
31 12 4 t3_8
39 16 4 t3_9
24 10 3 t4_1
37 14 3 t4_10
22 9 3 t4_2
32 13 3 t4_3
35 14 3 t4_4
23 10 3 t4_5
21 9 3 t4_6
33 13 3 t4_7
36 14 3 t4_8
34 13 3 t4_9
#get the neurons that corresponds each group
neuron_group1<-which(som_cluster==1)
neuron_group2<-which(som_cluster==2)
neuron_group3<-which(som_cluster==3)
neuron_group4<-which(som_cluster==4)
#Paint neurons and plot som with groups separated by hierarchical clustering.
plot(som, type="codes",codeRendering="lines", bgcol = terrain.colors(4)[som_cluster], main = "Clusters")
plot(som, type="mapping",codeRendering="lines", bgcol = terrain.colors(4)[som_cluster], main = "Clusters")
kohonen::add.cluster.boundaries(som, som_cluster)
#Plot the weight vector of neurons.
#Are they similar with the time series set?
v1<-ggplot2::autoplot(codes[,neuron_group1], facet = NULL) + ylab("y") + geom_line(size = 1)
v2<-ggplot2::autoplot(codes[,neuron_group2], facet = NULL) + ylab("y") + geom_line(size = 1)
v3<-ggplot2::autoplot(codes[,neuron_group3], facet = NULL) + ylab("y") + geom_line(size = 1)
v4<-ggplot2::autoplot(codes[,neuron_group4], facet = NULL) + ylab("y") + geom_line(size = 1)
gridExtra::grid.arrange(v1, v2,v3, v4, ncol=2,nrow=2, top="Weight of Neurons")
#plot the time series that are grouping in same cluster
LS0tDQp0aXRsZTogIlRpbWUgU2VyaWVzIENsdXN0ZXJpbmcgVXNpbmcgU2VsZi1Pcmdhbml6aW5nIE1hcHMiDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KU2ltaWxhcml0eSBEaXN0YW5jZXMNCg0KDQpgYGB7cn0NCmxpYnJhcnkoem9vKQ0KbGlicmFyeShrb2hvbmVuKQ0KbGlicmFyeShnZ3Bsb3QyKQ0KbGlicmFyeShkdHcpDQpgYGANCg0KYGBge3J9DQoNCiMgVHdvIHRpbWUgc2VyaWVzDQpYIDwtIGMoMSwxLDEsNCw0LDQsNCw0LDEsMSkNClkgPC0gYygxLDEsNCw0LDQsNCw0LDEsMSwxKQ0KDQojIEV1Y2xpZGVhbiBkaXN0YW5jZQ0KVFNkaXN0OjpEVFdEaXN0YW5jZShYLCBZKQ0KDQoNCiMgRXVjbGlkZWFuIGRpc3RhbmNlDQpUU2Rpc3Q6OkV1Y2xpZGVhbkRpc3RhbmNlKFgsIFkpDQoNCiMgRFRXIGRpc3RhbmNlDQphbGlnbm1lbnQ8LWR0dzo6ZHR3KFgsIFksIGtlZXAuaW50ZXJuYWxzID0gVFJVRSwgc3RlcD1zeW1tZXRyaWMxLCBrPVQpDQoNCnBsb3QoYWxpZ25tZW50KQ0KDQpwbG90KGFsaWdubWVudCx0eXBlPSJ0aHJlZXdheSIpDQoNCmFsaWdubWVudCRsb2NhbENvc3RNYXRyaXgNCmFsaWdubWVudCRkaXN0YW5jZQ0KYWxpZ25tZW50JHN0ZXBzVGFrZW4NCg0KcGxvdChhbGlnbm1lbnQkaW5kZXgxLGFsaWdubWVudCRpbmRleDIsbWFpbj0iV2FycGluZyBmdW5jdGlvbiIpOw0KDQpwbG90KGR0dzo6ZHR3KFgsIFksIGtlZXAuaW50ZXJuYWxzID0gVFJVRSwgc3RlcC5wYXR0ZXJuID1yYWJpbmVySnVhbmdTdGVwUGF0dGVybig2LCJjIikpLA0KICAgICB0eXBlPSJ0d293YXkiLG9mZnNldD0tMikNCg0KZHR3UGxvdFR3b1dheShkdHcoWCwgWSwgc3RlcCA9IGFzeW1tZXRyaWNQMSwga2VlcCA9IFQpKQ0KDQpyYWJpbmVySnVhbmdTdGVwUGF0dGVybig2LCJjIikNCnBsb3QocmFiaW5lckp1YW5nU3RlcFBhdHRlcm4oNiwiYyIpKQ0KDQojIFBsb3QNCnpvbzo6cGxvdC56b28oY2JpbmQoWCwgWSksIHBsb3QudHlwZSA9ICJtdWx0aXBsZSIsIGNvbCA9IGMoInJlZCIsICJibHVlIiksIGx3ZCA9IDIpDQoNCg0KI0NyZWF0ZXMgdGltZSBzZXJpZXMNCnRzMV94IDwtIGMoMSwyLDMsNCw1LDYsNyw4LDksMTApDQp0czFfeSA8LSBjKDEsMSwxLDQsNCw0LDQsNCwxLDEpDQp0czEgPC0gem9vOjp6b28odHMxX3ksIHRzMV94KTsNCg0KI0NyZWF0ZXMgdGltZSBzZXJpZXMNCnRzMl94IDwtIGMoMSwyLDMsNCw1LDYsNyw4LDksMTApDQp0czJfeSA8LSBjKDEsMSw0LDQsNCw0LDQsMSwxLDEpDQp0czIgPC0gem9vOjp6b28odHMxX3gsIHRzMV95KTsNCg0KYGBgDQpQbG90IHRpbWUgc2VyaWVzDQpgYGB7cn0NCnpvbzo6cGxvdC56b28oY2JpbmQodHMyX3ksIHRzMV95KSwgcGxvdC50eXBlID0gIm11bHRpcGxlIiwgY29sID0gYygicmVkIiwgImJsdWUiKSwgbHdkID0gMikNCg0KVFNkaXN0OjpFdWNsaWRlYW5EaXN0YW5jZSh0czFfeSwgdHMyX3kpDQoNClRTZGlzdDo6RFRXRGlzdGFuY2UodHMxX3ksIHRzMl95KQ0KYGBgDQoNClJlYWQgdGhlIHRpbWUgc2VyaWVzIGZyb20gY3N2IGZpbGUgYW5kIHBsb3QgYWxsDQoNCmBgYHtyfQ0KI3JlYWQgdGltZSBzZXJpZXMgZnJvbSBmaWxlDQp0aW1lc2VyaWVzLnpvbyA8LSByZWFkLnpvbygidGltZXNlcmllcy5jc3YiLCBoZWFkZXIgPSBUUlVFLCBzZXAgPSAiLCIpDQoNCiN6b286OnBsb3Quem9vKHosIG1haW4gPSAiU2V0IG9mIHRpbWUgc2VyaWVzIikNCg0KI0RlZmluZSBhIGNvbG9yIGZvciBlYWNoIGdyb3VwICg0IGdyb3VwcykNCmNvbG9yczwtYygiI0ZGMDAwMEZGIiwiI0ZGMDAwMEZGIiwiI0ZGMDAwMEZGIiwiI0ZGMDAwMEZGIiwNCiAgICAgICAgICAiIzAwRkY5RkZGIiwiIzAwRkY5RkZGIiwiIzAwRkY5RkZGIiwiIzAwRkY5RkZGIiwNCiAgICAgICAgICAiIzAwOUZGRkZGIiwiIzAwOUZGRkZGIiwiIzAwOUZGRkZGIiwiIzAwOUZGRkZGIiwNCiAgICAgICAgICAiI0RGRkYwMEZGIiwiI0RGRkYwMEZGIiwiI0RGRkYwMEZGIiwiI0RGRkYwMEZGIikNCmBgYA0KDQoNCmBgYHtyfQ0KDQojUGxvdCBhbGwgdGltZSBzZXJpZXMNCnpvbzo6cGxvdC56b28odGltZXNlcmllcy56b28sIHBsb3QudHlwZSA9ICJtdWx0aXBsZSIsIGNvbCA9IGNvbG9ycywgbHdkID0gMiwgbWFpbj0gIlNldCBvZiB0aW1lIHNlcmllcyIpDQoNCmBgYA0KDQpTZWxmLW9yZ2FuaXppbmcgbWFwcyB1c2luZyBLb2hvbmVuIHBhY2thZ2UNCmBgYHtyfQ0KIyBTT00NCnNvbWdyaWQ9a29ob25lbjo6c29tZ3JpZCh4ZGltPTQsIHlkaW09NCwgdG9wbz0icmVjdGFuZ3VsYXIiLCBuZWlnaGJvdXJob29kLmZjdCA9ICJnYXVzc2lhbiIpDQpzb208LWtvaG9uZW46OnN1cGVyc29tKHQoYXMubWF0cml4KHRpbWVzZXJpZXMuem9vKSksc29tZ3JpZCxybGVuID0gMTAwLCAgYWxwaGE9MSkNCg0KDQpgYGANCg0KDQpgYGB7cn0NCnBsb3Qoc29tLCJjb2RlcyIsY29kZVJlbmRlcmluZz0ibGluZXMiKQ0KYGBgDQpUaGUgdGltZSBzZXJpZXMgd2VyZSBhbGxvY2F0ZWQgaW4gdGhlc2UgbmV1cm9ucy4NCkVhY2ggbnVtYmVyIHJlcHJlc2VudHMgdGhlIG51bWJlciBvZiBuZXVyb24uDQoNCkV4YW1wbGU6IHRoZSBwb3NpdGlvbiAxIG9mIHZlY3RvciByZXByZXNlbnRzIHRoZSB0aW1lIHNlcmllcyAxIGluIGEgZGF0YXNldCwgdGhpcyB0aW1lIHNlcmllcw0Kd2FzIGFsbG9jYXRlZCBpbiBuZXVyb24gOS4NCg0KYGBge3J9DQpuZXVyb25fdGltZXNlcmllczwtc29tJHVuaXQuY2xhc3NpZg0KDQppbmZvX3RpbWVTZXJpZXM8LSBkYXRhLmZyYW1lKHRpbWVfc2VyaWVzPShyb3duYW1lcyh0KHRpbWVzZXJpZXMuem9vKSkpLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbmV1cm9uPWFzLmludGVnZXIoc29tJHVuaXQuY2xhc3NpZikpDQogICAgICAgICAgICAgICAgICAgICAgICANCmNvZGVzPC16b286Onpvbyh0KHNvbSRjb2Rlc1tbMV1dKSkNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgDQpgYGANCg0KDQpgYGB7cn0NCg0Kem9vOjpwbG90Lnpvbyhjb2RlcywgcGxvdC50eXBlID0gIm11bHRpcGxlIiwgY29sID0gImJsYWNrIiwgbHdkID0gMiwgbWFpbj0gIldlaWdodCBvZiBuZXVyb25zIikNCiNwbG90IGFsbCB0aW1lIHNlcmllcyAobmV1cm9ucykNCmdncGxvdDI6OmF1dG9wbG90KChjb2RlcyksIGZhY2V0ID0gTlVMTCkgKyB5bGFiKCJ5IikgK3hsYWIoInRpbWUiKSArIGdlb21fbGluZShzaXplID0gMSkgKyBnZ3RpdGxlKCJXZWlnaHQgb2YgbmV1cm9ucyIpDQoNCg0KDQpsaWJyYXJ5KHByb3h5KQ0KbGlicmFyeShzdGF0cykNCg0KIyMgdXNlIGhpZXJhcmNoaWNhbCBjbHVzdGVyaW5nIHRvIGNsdXN0ZXIgdGhlIGNvZGVib29rIHZlY3RvcnMuDQojIEN1dCBkZW5kcm9ncmFtIGluIDQgZ3JvdXBzDQpoYyA8LSBzdGF0czo6aGNsdXN0KGRpc3QodChjb2RlcykpLG1ldGhvZCA9ICJ3YXJkLkQyIikNCg0KI0N1dHMgYSB0cmVlLCBlLmcuLCBhcyByZXN1bHRpbmcgZnJvbSBoY2x1c3QsIGludG8gc2V2ZXJhbCBncm91cHMgZWl0aGVyIA0KI2J5IHNwZWNpZnlpbmcgdGhlIGRlc2lyZWQgbnVtYmVyKHMpIG9mIGdyb3VwcyBvciB0aGUgY3V0IGhlaWdodChzKS4NCnNvbV9jbHVzdGVyIDwtIHN0YXRzOjpjdXRyZWUoaGMsIDQpDQoNCmxpYnJhcnkoZGVuZGV4dGVuZCkNCiNwbG90IGRlbmRyb2dyYW0NCmRlbmQgPC0gaGMlPiUgYXMuZGVuZHJvZ3JhbSAlPiUNCiAgc2V0KCJicmFuY2hlc19rX2NvbG9yIiwgayA9IDQpICU+JSBzZXQoImJyYW5jaGVzX2x3ZCIsIDEuMikgJT4lDQogIHNldCgibGFiZWxzX2NleCIsIDAuOCkgJT4lIHNldCgibGFiZWxzX2NvbG9ycyIsIGsgPSA0KSAlPiUNCiAgc2V0KCJsZWF2ZXNfcGNoIiwgMTkpICU+JSBzZXQoImxlYXZlc19jZXgiLCAwLjUpIA0KZ2dkMSA8LSBhcy5nZ2RlbmQoZGVuZCkNCmdncGxvdChnZ2QxLCBob3JpeiA9IEZBTFNFKSArIGdndGl0bGUoIkRlbmRyb2dyYW0iKQ0KDQojQ2x1c3RlciBpcyBjcmVhdGVkIGJ5IGEgc2V0IG9mIG5ldXJvbnMuIA0KY2x1c3RlcjwtZGF0YS5mcmFtZShjbHVzdGVyPXNvbV9jbHVzdGVyLG5ldXJvbj0gMTpkaW0oY29kZXMpWzJdKQ0KDQojVGltZXNlcmllcywgbmV1cm9uIGFuZCBjbHVzdGVyDQpjbHVzdGVyX2luZm88LW1lcmdlKGNsdXN0ZXIsaW5mb190aW1lU2VyaWVzLCBieT0ibmV1cm9uIikNCmNsdXN0ZXJfaW5mb1tvcmRlcihjbHVzdGVyX2luZm8kdGltZV9zZXJpZXMpLF0NCg0KDQojZ2V0IHRoZSBuZXVyb25zIHRoYXQgY29ycmVzcG9uZHMgZWFjaCBncm91cA0KbmV1cm9uX2dyb3VwMTwtd2hpY2goc29tX2NsdXN0ZXI9PTEpDQpuZXVyb25fZ3JvdXAyPC13aGljaChzb21fY2x1c3Rlcj09MikNCm5ldXJvbl9ncm91cDM8LXdoaWNoKHNvbV9jbHVzdGVyPT0zKQ0KbmV1cm9uX2dyb3VwNDwtd2hpY2goc29tX2NsdXN0ZXI9PTQpDQoNCiNQYWludCBuZXVyb25zIGFuZCBwbG90IHNvbSB3aXRoIGdyb3VwcyBzZXBhcmF0ZWQgYnkgaGllcmFyY2hpY2FsIGNsdXN0ZXJpbmcuDQpwbG90KHNvbSwgdHlwZT0iY29kZXMiLGNvZGVSZW5kZXJpbmc9ImxpbmVzIiwgYmdjb2wgPSB0ZXJyYWluLmNvbG9ycyg0KVtzb21fY2x1c3Rlcl0sIG1haW4gPSAiQ2x1c3RlcnMiKQ0KcGxvdChzb20sIHR5cGU9Im1hcHBpbmciLGNvZGVSZW5kZXJpbmc9ImxpbmVzIiwgYmdjb2wgPSB0ZXJyYWluLmNvbG9ycyg0KVtzb21fY2x1c3Rlcl0sIG1haW4gPSAiQ2x1c3RlcnMiKSANCmtvaG9uZW46OmFkZC5jbHVzdGVyLmJvdW5kYXJpZXMoc29tLCBzb21fY2x1c3RlcikNCg0KI1Bsb3QgdGhlIHdlaWdodCB2ZWN0b3Igb2YgbmV1cm9ucy4gDQojQXJlIHRoZXkgc2ltaWxhciB3aXRoIHRoZSB0aW1lIHNlcmllcyBzZXQ/DQp2MTwtZ2dwbG90Mjo6YXV0b3Bsb3QoY29kZXNbLG5ldXJvbl9ncm91cDFdLCBmYWNldCA9IE5VTEwpICsgeWxhYigieSIpICsgZ2VvbV9saW5lKHNpemUgPSAxKSANCnYyPC1nZ3Bsb3QyOjphdXRvcGxvdChjb2Rlc1ssbmV1cm9uX2dyb3VwMl0sIGZhY2V0ID0gTlVMTCkgKyB5bGFiKCJ5IikgKyBnZW9tX2xpbmUoc2l6ZSA9IDEpIA0KdjM8LWdncGxvdDI6OmF1dG9wbG90KGNvZGVzWyxuZXVyb25fZ3JvdXAzXSwgZmFjZXQgPSBOVUxMKSArIHlsYWIoInkiKSArIGdlb21fbGluZShzaXplID0gMSkgDQp2NDwtZ2dwbG90Mjo6YXV0b3Bsb3QoY29kZXNbLG5ldXJvbl9ncm91cDRdLCBmYWNldCA9IE5VTEwpICsgeWxhYigieSIpICsgZ2VvbV9saW5lKHNpemUgPSAxKSANCmdyaWRFeHRyYTo6Z3JpZC5hcnJhbmdlKHYxLCB2Mix2MywgdjQsIG5jb2w9Mixucm93PTIsIHRvcD0iV2VpZ2h0IG9mIE5ldXJvbnMiKQ0KDQoNCiNwbG90IHRoZSB0aW1lIHNlcmllcyB0aGF0IGFyZSBncm91cGluZyBpbiBzYW1lIGNsdXN0ZXINCg0KDQoNCmBgYA0KDQoNCg==