library('knitr')
setwd('~/Data_Science/R/Tasks/SwiftKey')
opts_chunk$set(echo = TRUE, warning = FALSE, message = FALSE)

Project Swiftkey: German Corpora

The main goal of this project is to build a predictive text model. That means, I will use some data science, or to be more specific, NLP (Natural Language Processing) to predict the next words a user intends to type. This reduces the time a person needs to write a message. To start with this project the data exploration that you will see in this documents is performed.

Packages

library('tm')
library('stringi')
library('wordcloud')
library('ggplot2')

Exploratory data analysis

In this section, we will understand the distribution of words and relationship between the words in the corpora, then we will be able to answer these questions.

  1. Some words are more frequent than others - what are the distributions of word frequencies?
  2. What are the frequencies of 2-grams and 3-grams in the dataset?
  3. How many unique words do you need in a frequency sorted dictionary to cover 50% of all word instances in the language? 90%?
  4. How do you evaluate how many of the words come from foreign languages?
  5. Can you think of a way to increase the coverage – identifying words that may not be in the corpora or using a smaller number of words in the dictionary to cover the same number of phrases?

Loading the data

The corpora is charged to calculate basic statistics in each type of source: twitter, news and blogs.

f <- file.path(getwd(), "Coursera-SwiftKey.zip")
# Reading the files
de_twitter <- read.table(unz(f,"final/de_DE/de_DE.twitter.txt"), header=F, sep = "\n",stringsAsFactors = F)
de_news <-  read.table(unz(f,"final/de_DE/de_DE.news.txt"), header=F, sep = "\n",stringsAsFactors = F)
de_blogs <- read.table(unz(f,"final/de_DE/de_DE.blogs.txt"), header=F, sep = "\n",stringsAsFactors = F)
words_blogs <- stri_count_words(de_blogs$V1)
words_news <- stri_count_words(de_news$V1)
words_twitter <- stri_count_words(de_twitter$V1)
size_blogs <- file.info("final/de_DE/de_DE.blogs.txt")$size/1024^2
size_news <- file.info("final/de_DE/de_DE.news.txt")$size/1024^2
size_twitter <- file.info("final/de_DE/de_DE.twitter.txt")$size/1024^2
basic_stats <- data.frame(filename = c("de_blogs","de_news","de_twitter"),
                            file_size_MB = c(size_blogs, size_news, size_twitter),
                            lines = c(length(de_blogs$V1),length(de_news$V1),length(de_twitter$V1)),
                            num_words = c(sum(words_blogs),sum(words_news),sum(words_twitter)),
                            mean_num_words = c(mean(words_blogs),mean(words_news),mean(words_twitter)))
basic_stats

Now, we need to take a sample to perform some more complex calculations.

# Taking a sample.
set.seed(85)
de_sample = rbind(de_twitter$V1[sample(length(de_twitter$V1), 1000)],
                                   de_news$V1[sample(length(de_news$V1), 1000)],
                                    de_blogs$V1[sample(length(de_blogs$V1), 1000)])
remove(de_twitter,de_news,de_blogs)

With the sample a Corpus is created, then we will remove profanity (numbers, punctations and multiple whitespace characters). After, the sparse terms are removed, the maximal allowed sparsity is the 0.999. This helps to remove words from other languages or very uncommon ones.

# Creting Corpus.
de_corpus <- VCorpus(VectorSource(de_sample))
# Removing profanity.
de_corpus <- tm_map(de_corpus, function(x) iconv(x, from='UTF-8', to="latin1"))
de_corpus <- tm_map(de_corpus, removeNumbers)
de_corpus <- tm_map(de_corpus, removePunctuation)
de_corpus <- tm_map(de_corpus, stripWhitespace)
de_corpus <- tm_map(de_corpus, PlainTextDocument)
# Creating a document-term matrix.
de_tdm <- TermDocumentMatrix(de_corpus)
nTerms(de_tdm)
[1] 35317
de_tdm <- removeSparseTerms(de_tdm, 0.999)
nTerms(de_tdm)
[1] 4732

Analyzing frequent words

We need to see the frequent words in our corpora. They might appear in our 2-grams, 3-grams and 4-grams. I’m not removing stop words, as the intention of the SwiftKey is to predict the next word to be typed.

# Finding frequent terms
de_freq <- sort(rowSums(as.matrix(de_tdm)),decreasing = T)
de_wc = data.frame(term=names(de_freq),frequency=de_freq)
de_wc[, 'cum_freq'] <- cumsum(de_wc[, 2])
# Number of words with more than 50% of instances
words_50 <- sum(de_wc$cum_freq < tail(de_wc$cum_fre,n=1)*0.5)
words_50
[1] 82
# Number of words with more than 90% of instances
words_90 <- sum(de_wc$cum_freq < tail(de_wc$cum_fre,n=1)*0.9)
words_90
[1] 2177
# Hitogram of frequent terms
p <- ggplot(subset(de_wc, frequency>1000), aes(x=reorder(term, frequency),y=frequency))
p <- p + geom_bar(aes(fill = frequency),stat="identity") + coord_flip() +xlab('words')
p

# Wordcloud
wordcloud(names(de_freq),de_freq, min.freq=300, colors=brewer.pal(6,"Accent"))

Analyzing frequent n-grams

So far, we have explored the behaviour of individual words in the corpora. Time to see 2-grams and 3-grams.

# Creating tokenizers.
BigramTokenizer <- function(x) unlist(lapply(ngrams(words(x), 2), paste, collapse = " "), use.names = FALSE)
TrigramTokenizer <- function(x) unlist(lapply(ngrams(words(x), 3), paste, collapse = " "), use.names = FALSE)
# 2-grams
de_tdm_2g <- TermDocumentMatrix(de_corpus,  control=list(tokenize=BigramTokenizer))
de_tdm_2g <- removeSparseTerms(de_tdm_2g, 0.999)
# Finding frequent terms
de_freq_2g <- sort(rowSums(as.matrix(de_tdm_2g)),decreasing = T)
findFreqTerms(de_tdm_2g,lowfreq=100)
 [1] "an der"     "auch die"   "auf dem"    "auf den"    "auf der"    "auf die"    "aus dem"   
 [8] "bei der"    "das ist"    "dass die"   "für den"    "für die"    "in den"     "in der"    
[15] "in die"     "mehr als"   "mit dem"    "mit der"    "mit einem"  "nicht mehr" "sich die"  
[22] "über die"   "um die"     "und der"    "und die"    "von der"   
de_wc_g = data.frame(term=names(de_freq_2g),occurrences=de_freq_2g)
# Wordcloud
wordcloud(names(de_freq_2g),de_freq_2g, min.freq=75, colors=brewer.pal(6,"Accent"))

# 3-grams
de_tdm_3g <- TermDocumentMatrix(de_corpus,  control=list(tokenize=TrigramTokenizer))
de_tdm_3g <- removeSparseTerms(de_tdm_3g, 0.999)
# Finding frequent terms
de_freq_3g <- sort(rowSums(as.matrix(de_tdm_3g)),decreasing = T)
findFreqTerms(de_tdm_3g,lowfreq=12)
 [1] "auf jeden fall"         "das ist ein"            "den vergangenen jahren"
 [4] "die zahl der"           "im vergangenen jahr"    "in den letzten"        
 [7] "in den nächsten"        "in den vergangenen"     "in diesem jahr"        
[10] "nach wie vor"           "sich in den"            "sich in der"           
de_wc_g = data.frame(term=names(de_freq_3g),occurrences=de_freq_3g)
# Wordcloud
wordcloud(names(de_freq_3g),de_freq_3g, min.freq=50, scale = c(2,.25) , max.words=10,colors=brewer.pal(3,"Accent"))

Plan for further steps

Goals of the prediction model:

To build a prediction model, I have to find an answer to these questions.

Plans for creating a prediction algorithm and Shiny app.

  1. Define a metric to evaluate the performance of the model.
  2. Design the Shiny application.
  3. Clean and transform the daa following the steps done tin this data exploration.
  4. Set a simple baseline to build a model by improving over the baseline.
  5. Explore different parameters (like n in the n-gram model) to balance simplisity vs. accuracy.
  6. Explore cluster analysis or K-means to find close words in ‘unobserverd’ words or n-grams.
  7. Build and validate different models to find an adequate solution.
  8. Evaluate the final model (which should be better than the baseline defined in step 4) using the metric in step 1.
  9. Develop a Shiny application according to its design.
LS0tDQp0aXRsZTogIk1pbGVzdG9uZSBSZXBvcnQiDQphdXRob3I6IFNhbmRyYSBNZW5lc2VzDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQpgYGB7ciB3YXJuaW5nPUZBTFNFLCBlcnJvcj1GQUxTRX0NCmxpYnJhcnkoJ2tuaXRyJykNCnNldHdkKCd+L0RhdGFfU2NpZW5jZS9SL1Rhc2tzL1N3aWZ0S2V5JykNCm9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFLCB3YXJuaW5nID0gRkFMU0UsIG1lc3NhZ2UgPSBGQUxTRSkNCmBgYA0KDQoNCiMgUHJvamVjdCBTd2lmdGtleTogR2VybWFuIENvcnBvcmENCg0KVGhlIG1haW4gZ29hbCBvZiB0aGlzIHByb2plY3QgaXMgdG8gYnVpbGQgYSBwcmVkaWN0aXZlIHRleHQgbW9kZWwuIFRoYXQgbWVhbnMsIEkgd2lsbCB1c2Ugc29tZSBkYXRhIHNjaWVuY2UsIG9yIHRvIGJlIG1vcmUgc3BlY2lmaWMsIE5MUCAoTmF0dXJhbCBMYW5ndWFnZSBQcm9jZXNzaW5nKSB0byBwcmVkaWN0IHRoZSBuZXh0IHdvcmRzIGEgdXNlciBpbnRlbmRzIHRvIHR5cGUuIFRoaXMgcmVkdWNlcyB0aGUgdGltZSBhIHBlcnNvbiBuZWVkcyB0byB3cml0ZSBhIG1lc3NhZ2UuIFRvIHN0YXJ0IHdpdGggdGhpcyBwcm9qZWN0IHRoZSBkYXRhIGV4cGxvcmF0aW9uIHRoYXQgeW91IHdpbGwgc2VlIGluIHRoaXMgZG9jdW1lbnRzIGlzIHBlcmZvcm1lZC4NCg0KKlBhY2thZ2VzKg0KDQpgYGB7ciB3YXJuaW5nID0gRkFMU0UsIG1lc3NhZ2UgPSBGQUxTRX0NCmxpYnJhcnkoJ3RtJykNCmxpYnJhcnkoJ3N0cmluZ2knKQ0KbGlicmFyeSgnd29yZGNsb3VkJykNCmxpYnJhcnkoJ2dncGxvdDInKQ0KDQpgYGANCg0KLSB0bTogQSBmcmFtZXdvcmsgZm9yIHRleHQgbWluaW5nIGFwcGxpY2F0aW9ucyB3aXRoaW4gUi4NCi0gc3RyaW5naTogQWxsb3dzIGZvciBmYXN0LCBjb3JyZWN0LCBjb25zaXN0ZW50LCBwb3J0YWJsZSwgYXMgd2VsbCBhcyBjb252ZW5pZW50IGNoYXJhY3RlciBzdHJpbmcvdGV4dCBwcm9jZXNzaW5nIGluIGV2ZXJ5IGxvY2FsZSBhbmQgYW55IG5hdGl2ZSBlbmNvZGluZy4NCi0gd29yZGNsb3VkOiBQcmV0dHkgd29yZCBjbG91ZHMuDQotIGdncGxvdDI6IEEgc3lzdGVtIGZvciAnZGVjbGFyYXRpdmVseScgY3JlYXRpbmcgZ3JhcGhpY3MsIGJhc2VkIG9uICJUaGUgR3JhbW1hciBvZiBHcmFwaGljcyINCg0KDQojIEV4cGxvcmF0b3J5IGRhdGEgYW5hbHlzaXMNCg0KSW4gdGhpcyBzZWN0aW9uLCB3ZSB3aWxsIHVuZGVyc3RhbmQgdGhlIGRpc3RyaWJ1dGlvbiBvZiB3b3JkcyBhbmQgcmVsYXRpb25zaGlwIGJldHdlZW4gdGhlIHdvcmRzIGluIHRoZSBjb3Jwb3JhLCB0aGVuIHdlIHdpbGwgYmUgYWJsZSB0byBhbnN3ZXIgdGhlc2UgcXVlc3Rpb25zLg0KDQoxLiBTb21lIHdvcmRzIGFyZSBtb3JlIGZyZXF1ZW50IHRoYW4gb3RoZXJzIC0gd2hhdCBhcmUgdGhlIGRpc3RyaWJ1dGlvbnMgb2Ygd29yZCBmcmVxdWVuY2llcz8NCjIuIFdoYXQgYXJlIHRoZSBmcmVxdWVuY2llcyBvZiAyLWdyYW1zIGFuZCAzLWdyYW1zIGluIHRoZSBkYXRhc2V0Pw0KMy4gSG93IG1hbnkgdW5pcXVlIHdvcmRzIGRvIHlvdSBuZWVkIGluIGEgZnJlcXVlbmN5IHNvcnRlZCBkaWN0aW9uYXJ5IHRvIGNvdmVyIDUwJSBvZiBhbGwgd29yZCBpbnN0YW5jZXMgaW4gdGhlIGxhbmd1YWdlPyA5MCU/DQo0LiBIb3cgZG8geW91IGV2YWx1YXRlIGhvdyBtYW55IG9mIHRoZSB3b3JkcyBjb21lIGZyb20gZm9yZWlnbiBsYW5ndWFnZXM/DQo1LiBDYW4geW91IHRoaW5rIG9mIGEgd2F5IHRvIGluY3JlYXNlIHRoZSBjb3ZlcmFnZSAtLSBpZGVudGlmeWluZyB3b3JkcyB0aGF0IG1heSBub3QgYmUgaW4gdGhlIGNvcnBvcmEgb3IgdXNpbmcgYSBzbWFsbGVyIG51bWJlciBvZiB3b3JkcyBpbiB0aGUgZGljdGlvbmFyeSB0byBjb3ZlciB0aGUgc2FtZSBudW1iZXIgb2YgcGhyYXNlcz8NCg0KIyMgTG9hZGluZyB0aGUgZGF0YQ0KDQpUaGUgY29ycG9yYSBpcyBjaGFyZ2VkIHRvIGNhbGN1bGF0ZSBiYXNpYyBzdGF0aXN0aWNzIGluIGVhY2ggdHlwZSBvZiBzb3VyY2U6IHR3aXR0ZXIsIG5ld3MgYW5kIGJsb2dzLg0KDQpgYGB7ciBjYWNoZT1UUlVFLCB3YXJuaW5nID0gRkFMU0UsIG1lc3NhZ2UgPSBGQUxTRX0NCmYgPC0gZmlsZS5wYXRoKGdldHdkKCksICJDb3Vyc2VyYS1Td2lmdEtleS56aXAiKQ0KDQojIFJlYWRpbmcgdGhlIGZpbGVzDQpkZV90d2l0dGVyIDwtIHJlYWQudGFibGUodW56KGYsImZpbmFsL2RlX0RFL2RlX0RFLnR3aXR0ZXIudHh0IiksIGhlYWRlcj1GLCBzZXAgPSAiXG4iLHN0cmluZ3NBc0ZhY3RvcnMgPSBGKQ0KZGVfbmV3cyA8LSAgcmVhZC50YWJsZSh1bnooZiwiZmluYWwvZGVfREUvZGVfREUubmV3cy50eHQiKSwgaGVhZGVyPUYsIHNlcCA9ICJcbiIsc3RyaW5nc0FzRmFjdG9ycyA9IEYpDQpkZV9ibG9ncyA8LSByZWFkLnRhYmxlKHVueihmLCJmaW5hbC9kZV9ERS9kZV9ERS5ibG9ncy50eHQiKSwgaGVhZGVyPUYsIHNlcCA9ICJcbiIsc3RyaW5nc0FzRmFjdG9ycyA9IEYpDQoNCndvcmRzX2Jsb2dzIDwtIHN0cmlfY291bnRfd29yZHMoZGVfYmxvZ3MkVjEpDQp3b3Jkc19uZXdzIDwtIHN0cmlfY291bnRfd29yZHMoZGVfbmV3cyRWMSkNCndvcmRzX3R3aXR0ZXIgPC0gc3RyaV9jb3VudF93b3JkcyhkZV90d2l0dGVyJFYxKQ0Kc2l6ZV9ibG9ncyA8LSBmaWxlLmluZm8oImZpbmFsL2RlX0RFL2RlX0RFLmJsb2dzLnR4dCIpJHNpemUvMTAyNF4yDQpzaXplX25ld3MgPC0gZmlsZS5pbmZvKCJmaW5hbC9kZV9ERS9kZV9ERS5uZXdzLnR4dCIpJHNpemUvMTAyNF4yDQpzaXplX3R3aXR0ZXIgPC0gZmlsZS5pbmZvKCJmaW5hbC9kZV9ERS9kZV9ERS50d2l0dGVyLnR4dCIpJHNpemUvMTAyNF4yDQpiYXNpY19zdGF0cyA8LSBkYXRhLmZyYW1lKGZpbGVuYW1lID0gYygiZGVfYmxvZ3MiLCJkZV9uZXdzIiwiZGVfdHdpdHRlciIpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIGZpbGVfc2l6ZV9NQiA9IGMoc2l6ZV9ibG9ncywgc2l6ZV9uZXdzLCBzaXplX3R3aXR0ZXIpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxpbmVzID0gYyhsZW5ndGgoZGVfYmxvZ3MkVjEpLGxlbmd0aChkZV9uZXdzJFYxKSxsZW5ndGgoZGVfdHdpdHRlciRWMSkpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIG51bV93b3JkcyA9IGMoc3VtKHdvcmRzX2Jsb2dzKSxzdW0od29yZHNfbmV3cyksc3VtKHdvcmRzX3R3aXR0ZXIpKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZWFuX251bV93b3JkcyA9IGMobWVhbih3b3Jkc19ibG9ncyksbWVhbih3b3Jkc19uZXdzKSxtZWFuKHdvcmRzX3R3aXR0ZXIpKSkNCmJhc2ljX3N0YXRzDQoNCmBgYA0KDQpOb3csIHdlIG5lZWQgdG8gdGFrZSBhIHNhbXBsZSB0byBwZXJmb3JtIHNvbWUgbW9yZSBjb21wbGV4IGNhbGN1bGF0aW9ucy4gDQoNCmBgYHtyfQ0KIyBUYWtpbmcgYSBzYW1wbGUuDQoNCnNldC5zZWVkKDg1KQ0KZGVfc2FtcGxlID0gcmJpbmQoZGVfdHdpdHRlciRWMVtzYW1wbGUobGVuZ3RoKGRlX3R3aXR0ZXIkVjEpLCAxMDAwKV0sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRlX25ld3MkVjFbc2FtcGxlKGxlbmd0aChkZV9uZXdzJFYxKSwgMTAwMCldLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZGVfYmxvZ3MkVjFbc2FtcGxlKGxlbmd0aChkZV9ibG9ncyRWMSksIDEwMDApXSkNCnJlbW92ZShkZV90d2l0dGVyLGRlX25ld3MsZGVfYmxvZ3MpDQoNCmBgYA0KDQpXaXRoIHRoZSBzYW1wbGUgYSBDb3JwdXMgaXMgY3JlYXRlZCwgdGhlbiB3ZSB3aWxsIHJlbW92ZSBwcm9mYW5pdHkgKG51bWJlcnMsIHB1bmN0YXRpb25zIGFuZCBtdWx0aXBsZSB3aGl0ZXNwYWNlIGNoYXJhY3RlcnMpLiBBZnRlciwgdGhlIHNwYXJzZSB0ZXJtcyBhcmUgcmVtb3ZlZCwgdGhlIG1heGltYWwgYWxsb3dlZCBzcGFyc2l0eSBpcyB0aGUgMC45OTkuIFRoaXMgaGVscHMgdG8gcmVtb3ZlIHdvcmRzIGZyb20gb3RoZXIgbGFuZ3VhZ2VzIG9yIHZlcnkgdW5jb21tb24gb25lcy4NCg0KYGBge3J9DQoNCiMgQ3JlYXRpbmcgQ29ycHVzLg0KZGVfY29ycHVzIDwtIFZDb3JwdXMoVmVjdG9yU291cmNlKGRlX3NhbXBsZSkpDQoNCiMgUmVtb3ZpbmcgcHJvZmFuaXR5Lg0KDQpkZV9jb3JwdXMgPC0gdG1fbWFwKGRlX2NvcnB1cywgZnVuY3Rpb24oeCkgaWNvbnYoeCwgZnJvbT0nVVRGLTgnLCB0bz0ibGF0aW4xIikpDQpkZV9jb3JwdXMgPC0gdG1fbWFwKGRlX2NvcnB1cywgcmVtb3ZlTnVtYmVycykNCmRlX2NvcnB1cyA8LSB0bV9tYXAoZGVfY29ycHVzLCByZW1vdmVQdW5jdHVhdGlvbikNCmRlX2NvcnB1cyA8LSB0bV9tYXAoZGVfY29ycHVzLCBzdHJpcFdoaXRlc3BhY2UpDQpkZV9jb3JwdXMgPC0gdG1fbWFwKGRlX2NvcnB1cywgUGxhaW5UZXh0RG9jdW1lbnQpDQoNCiMgQ3JlYXRpbmcgYSBkb2N1bWVudC10ZXJtIG1hdHJpeC4NCg0KZGVfdGRtIDwtIFRlcm1Eb2N1bWVudE1hdHJpeChkZV9jb3JwdXMpDQpuVGVybXMoZGVfdGRtKQ0KZGVfdGRtIDwtIHJlbW92ZVNwYXJzZVRlcm1zKGRlX3RkbSwgMC45OTkpDQpuVGVybXMoZGVfdGRtKQ0KDQpgYGANCg0KDQojIyBBbmFseXppbmcgZnJlcXVlbnQgd29yZHMNCg0KV2UgbmVlZCB0byBzZWUgdGhlIGZyZXF1ZW50IHdvcmRzIGluIG91ciBjb3Jwb3JhLiBUaGV5IG1pZ2h0IGFwcGVhciBpbiBvdXIgMi1ncmFtcywgMy1ncmFtcyBhbmQgNC1ncmFtcy4gSSdtIG5vdCByZW1vdmluZyBzdG9wIHdvcmRzLCBhcyB0aGUgaW50ZW50aW9uIG9mIHRoZSBTd2lmdEtleSBpcyB0byBwcmVkaWN0IHRoZSBuZXh0IHdvcmQgdG8gYmUgdHlwZWQuDQoNCmBgYHtyfQ0KDQojIEZpbmRpbmcgZnJlcXVlbnQgdGVybXMNCmRlX2ZyZXEgPC0gc29ydChyb3dTdW1zKGFzLm1hdHJpeChkZV90ZG0pKSxkZWNyZWFzaW5nID0gVCkNCmRlX3djID0gZGF0YS5mcmFtZSh0ZXJtPW5hbWVzKGRlX2ZyZXEpLGZyZXF1ZW5jeT1kZV9mcmVxKQ0KZGVfd2NbLCAnY3VtX2ZyZXEnXSA8LSBjdW1zdW0oZGVfd2NbLCAyXSkNCg0KIyBOdW1iZXIgb2Ygd29yZHMgd2l0aCBtb3JlIHRoYW4gNTAlIG9mIGluc3RhbmNlcw0Kd29yZHNfNTAgPC0gc3VtKGRlX3djJGN1bV9mcmVxIDwgdGFpbChkZV93YyRjdW1fZnJlLG49MSkqMC41KQ0Kd29yZHNfNTANCg0KIyBOdW1iZXIgb2Ygd29yZHMgd2l0aCBtb3JlIHRoYW4gOTAlIG9mIGluc3RhbmNlcw0Kd29yZHNfOTAgPC0gc3VtKGRlX3djJGN1bV9mcmVxIDwgdGFpbChkZV93YyRjdW1fZnJlLG49MSkqMC45KQ0Kd29yZHNfOTANCg0KIyBIaXRvZ3JhbSBvZiBmcmVxdWVudCB0ZXJtcw0KcCA8LSBnZ3Bsb3Qoc3Vic2V0KGRlX3djLCBmcmVxdWVuY3k+MTAwMCksIGFlcyh4PXJlb3JkZXIodGVybSwgZnJlcXVlbmN5KSx5PWZyZXF1ZW5jeSkpDQpwIDwtIHAgKyBnZW9tX2JhcihhZXMoZmlsbCA9IGZyZXF1ZW5jeSksc3RhdD0iaWRlbnRpdHkiKSArIGNvb3JkX2ZsaXAoKSAreGxhYignd29yZHMnKQ0KcA0KDQojIFdvcmRjbG91ZA0Kd29yZGNsb3VkKG5hbWVzKGRlX2ZyZXEpLGRlX2ZyZXEsIG1pbi5mcmVxPTMwMCwgY29sb3JzPWJyZXdlci5wYWwoNiwiQWNjZW50IikpDQoNCmBgYA0KDQoNCiMjIEFuYWx5emluZyBmcmVxdWVudCBuLWdyYW1zDQoNClNvIGZhciwgd2UgaGF2ZSBleHBsb3JlZCB0aGUgYmVoYXZpb3VyIG9mIGluZGl2aWR1YWwgd29yZHMgaW4gdGhlIGNvcnBvcmEuIFRpbWUgdG8gc2VlIDItZ3JhbXMgYW5kIDMtZ3JhbXMuIA0KDQpgYGB7cn0NCiMgQ3JlYXRpbmcgdG9rZW5pemVycy4NCkJpZ3JhbVRva2VuaXplciA8LSBmdW5jdGlvbih4KSB1bmxpc3QobGFwcGx5KG5ncmFtcyh3b3Jkcyh4KSwgMiksIHBhc3RlLCBjb2xsYXBzZSA9ICIgIiksIHVzZS5uYW1lcyA9IEZBTFNFKQ0KVHJpZ3JhbVRva2VuaXplciA8LSBmdW5jdGlvbih4KSB1bmxpc3QobGFwcGx5KG5ncmFtcyh3b3Jkcyh4KSwgMyksIHBhc3RlLCBjb2xsYXBzZSA9ICIgIiksIHVzZS5uYW1lcyA9IEZBTFNFKQ0KDQojIDItZ3JhbXMNCmRlX3RkbV8yZyA8LSBUZXJtRG9jdW1lbnRNYXRyaXgoZGVfY29ycHVzLCAgY29udHJvbD1saXN0KHRva2VuaXplPUJpZ3JhbVRva2VuaXplcikpDQpkZV90ZG1fMmcgPC0gcmVtb3ZlU3BhcnNlVGVybXMoZGVfdGRtXzJnLCAwLjk5OSkNCg0KIyBGaW5kaW5nIGZyZXF1ZW50IHRlcm1zDQpkZV9mcmVxXzJnIDwtIHNvcnQocm93U3Vtcyhhcy5tYXRyaXgoZGVfdGRtXzJnKSksZGVjcmVhc2luZyA9IFQpDQpmaW5kRnJlcVRlcm1zKGRlX3RkbV8yZyxsb3dmcmVxPTEwMCkNCmRlX3djX2cgPSBkYXRhLmZyYW1lKHRlcm09bmFtZXMoZGVfZnJlcV8yZyksb2NjdXJyZW5jZXM9ZGVfZnJlcV8yZykNCg0KIyBXb3JkY2xvdWQNCndvcmRjbG91ZChuYW1lcyhkZV9mcmVxXzJnKSxkZV9mcmVxXzJnLCBtaW4uZnJlcT03NSwgY29sb3JzPWJyZXdlci5wYWwoNiwiQWNjZW50IikpDQoNCiMgMy1ncmFtcw0KZGVfdGRtXzNnIDwtIFRlcm1Eb2N1bWVudE1hdHJpeChkZV9jb3JwdXMsICBjb250cm9sPWxpc3QodG9rZW5pemU9VHJpZ3JhbVRva2VuaXplcikpDQpkZV90ZG1fM2cgPC0gcmVtb3ZlU3BhcnNlVGVybXMoZGVfdGRtXzNnLCAwLjk5OSkNCg0KIyBGaW5kaW5nIGZyZXF1ZW50IHRlcm1zDQpkZV9mcmVxXzNnIDwtIHNvcnQocm93U3Vtcyhhcy5tYXRyaXgoZGVfdGRtXzNnKSksZGVjcmVhc2luZyA9IFQpDQpmaW5kRnJlcVRlcm1zKGRlX3RkbV8zZyxsb3dmcmVxPTEyKQ0KZGVfd2NfZyA9IGRhdGEuZnJhbWUodGVybT1uYW1lcyhkZV9mcmVxXzNnKSxvY2N1cnJlbmNlcz1kZV9mcmVxXzNnKQ0KDQojIFdvcmRjbG91ZA0Kd29yZGNsb3VkKG5hbWVzKGRlX2ZyZXFfM2cpLGRlX2ZyZXFfM2csIG1pbi5mcmVxPTUwLCBzY2FsZSA9IGMoMiwuMjUpICwgbWF4LndvcmRzPTEwLGNvbG9ycz1icmV3ZXIucGFsKDMsIkFjY2VudCIpKQ0KDQpgYGANCg0KIyBQbGFuIGZvciBmdXJ0aGVyIHN0ZXBzDQoNCkdvYWxzIG9mIHRoZSBwcmVkaWN0aW9uIG1vZGVsOg0KDQogLSBFeHBsb3JlIGRpZmZlcmVudCBtZXRob2RzIHRvIHByZWRpY3QgdGV4dCBpbiBHZXJtYW4gbGFuZ3VhZ2UuDQogLSBWYWxpZGF0ZSBkaWZmZXJlbnQgbW9kZWxzIHVzaW5nIGEgbWV0cmljIG9yIGFjY3VyYWN5IG1lYXN1cmUuDQogLSBCdWlsZCBhIGZpbmFsIHRleHQgcHJlZGljdGlvbiBtb2RlbCB1c2luZyB0aGUgbW9zdCBmcmVxdWVudCBuLWdyYW1zLg0KDQoNClRvIGJ1aWxkIGEgcHJlZGljdGlvbiBtb2RlbCwgSSBoYXZlIHRvIGZpbmQgYW4gYW5zd2VyIHRvIHRoZXNlIHF1ZXN0aW9ucy4NCg0KIC0gSG93IGNhbiB5b3UgZWZmaWNpZW50bHkgc3RvcmUgYW4gbi1ncmFtIG1vZGVsPw0KIC0gSG93IGNhbiB5b3UgdXNlIHRoZSBrbm93bGVkZ2UgYWJvdXQgd29yZCBmcmVxdWVuY2llcyB0byBtYWtlIHlvdXIgbW9kZWwgc21hbGxlciBhbmQgbW9yZSBlZmZpY2llbnQ/DQogLSBIb3cgbWFueSBwYXJhbWV0ZXJzIGRvIHlvdSBuZWVkIChpLmUuIGhvdyBiaWcgaXMgbiBpbiB5b3VyIG4tZ3JhbSBtb2RlbCk/DQogLSBDYW4geW91IHRoaW5rIG9mIHNpbXBsZSB3YXlzIHRvICJzbW9vdGgiIHRoZSBwcm9iYWJpbGl0aWVzPw0KIC0gSG93IGRvIHlvdSBldmFsdWF0ZSB3aGV0aGVyIHlvdXIgbW9kZWwgaXMgYW55IGdvb2Q/DQogLSBIb3cgY2FuIHlvdSB1c2UgYmFja29mZiBtb2RlbHMgdG8gZXN0aW1hdGUgdGhlIHByb2JhYmlsaXR5IG9mIHVub2JzZXJ2ZWQgbi1ncmFtcz8NCg0KUGxhbnMgZm9yIGNyZWF0aW5nIGEgcHJlZGljdGlvbiBhbGdvcml0aG0gYW5kIFNoaW55IGFwcC4NCg0KIDEuIERlZmluZSBhIG1ldHJpYyB0byBldmFsdWF0ZSB0aGUgcGVyZm9ybWFuY2Ugb2YgdGhlIG1vZGVsLg0KIDIuIERlc2lnbiB0aGUgU2hpbnkgYXBwbGljYXRpb24uDQogMy4gQ2xlYW4gYW5kIHRyYW5zZm9ybSB0aGUgZGFhIGZvbGxvd2luZyB0aGUgc3RlcHMgZG9uZSB0aW4gdGhpcyBkYXRhIGV4cGxvcmF0aW9uLg0KIDQuIFNldCBhIHNpbXBsZSBiYXNlbGluZSB0byBidWlsZCBhIG1vZGVsIGJ5IGltcHJvdmluZyBvdmVyIHRoZSBiYXNlbGluZS4NCiA1LiBFeHBsb3JlIGRpZmZlcmVudCBwYXJhbWV0ZXJzIChsaWtlIG4gaW4gdGhlIG4tZ3JhbSBtb2RlbCkgdG8gYmFsYW5jZSBzaW1wbGlzaXR5IHZzLiBhY2N1cmFjeS4NCiA2LiBFeHBsb3JlIGNsdXN0ZXIgYW5hbHlzaXMgb3IgSy1tZWFucyB0byBmaW5kIGNsb3NlIHdvcmRzIGluICd1bm9ic2VydmVyZCcgd29yZHMgb3Igbi1ncmFtcy4NCiA3LiBCdWlsZCBhbmQgdmFsaWRhdGUgZGlmZmVyZW50IG1vZGVscyB0byBmaW5kIGFuIGFkZXF1YXRlIHNvbHV0aW9uLg0KIDguIEV2YWx1YXRlIHRoZSBmaW5hbCBtb2RlbCAod2hpY2ggc2hvdWxkIGJlIGJldHRlciB0aGFuIHRoZSBiYXNlbGluZSBkZWZpbmVkIGluIHN0ZXAgNCkgdXNpbmcgdGhlICBtZXRyaWMgaW4gc3RlcCAxLg0KIDkuIERldmVsb3AgYSBTaGlueSBhcHBsaWNhdGlvbiBhY2NvcmRpbmcgdG8gaXRzIGRlc2lnbi4NCg0K