新版的 RStudio 可以在單一介面下使用多種語言,這裡我演示一下同時用 Python 和 R,透過「卷積神經網絡(CNN,Convolutional Neural Network)」來做臉孔辨識。即使在相當低的解析度(28*28)之下,模型還是可以有不錯的辨識率(97.5%),CNN的威力可見一般。程式的解說請參考 原PO


Installation

因為這些Packages比較新,你可能需要用以下的方法做安裝和設定:

# install EBImage from Bioconductor
source("http://bioconductor.org/biocLite.R")
biocLite("EBImage")

# install mxnet its own repo
install.packages("drat", repos="https://cran.rstudio.com")
drat:::addRepo("dmlc")
install.packages("mxnet")

# You may also need to re-install DiagrammeR for a compatability issue.
# Hopefully this step won't be needed in the near future.
remove.packages("DiagrammeR")
require(devtools)
packageurl <- "http://cran.us.r-project.org/src/contrib/Archive/DiagrammeR/DiagrammeR_0.8.1.tar.gz"
install.packages(packageurl, repos=NULL, type="source")


Loading & Pre-Processing

# Imports
from sklearn.datasets import fetch_olivetti_faces
import numpy as np

# Download Olivetti faces dataset
olivetti = fetch_olivetti_faces('data')
x = olivetti.images
y = olivetti.target

# Print info on shapes and reshape where necessary
print("Original x shape:", x.shape)
X = x.reshape((400, 4096))
print("New x shape:", X.shape)
print("y shape", y.shape)

# Save the numpy arrays
np.savetxt("olivetti_X.csv", X, delimiter = ",")
np.savetxt("olivetti_y.csv", y, delimiter = ",", fmt = '%d')

print("\nDownloading and reshaping done!")
Original x shape: (400, 64, 64)
New x shape: (400, 4096)
y shape (400,)

Downloading and reshaping done!


Reshaping

# This script is used to resize images from 64x64 to 28x28 pixels

# Clear workspace
rm(list=ls())

# Load EBImage library
require(EBImage)

# Load data
X <- read.csv("olivetti_X.csv", header = F)
labels <- read.csv("olivetti_y.csv", header = F)

# Dataframe of resized images
rs_df <- data.frame()

# Main loop: for each image, resize and set it to greyscale
for(i in 1:nrow(X))
{
    # Try-catch
    result <- tryCatch({
    # Image (as 1d vector)
    img <- as.numeric(X[i,])
    # Reshape as a 64x64 image (EBImage object)
    img <- Image(img, dim=c(64, 64), colormode = "Grayscale")
    # Resize image to 28x28 pixels
    img_resized <- resize(img, w = 28, h = 28)
    # Get image matrix (there should be another function 
    # to do this faster and more neatly!)
    img_matrix <- img_resized@.Data
    # Coerce to a vector
    img_vector <- as.vector(t(img_matrix))
    # Add label
    label <- labels[i,]
    vec <- c(label, img_vector)
    # Stack in rs_df using rbind
    rs_df <- rbind(rs_df, vec)
    # Print status
    print(paste("Done",i,sep = " "))},
    # Error function (just prints the error). Btw you should get no errors!
    error = function(e){print(e)})
}


# Set names. The first columns are the labels, the other columns are the pixels.
names(rs_df) <- c("label", paste("pixel", c(1:784)))

Output omitted.

Original Image

par(mfrow=c(2,5),mar=c(1,1,1,1))
for(i in 1:10) {
  img <- as.numeric(X[i,])
  img <- Image(img, dim=c(64, 64), colormode = "Grayscale")
  m = t(apply(img@.Data, 1, rev))
  image(m, axes = FALSE, col = grey(seq(0, 1, length = 256)))
}

Resuce the resolution

par(mfrow=c(2,5),mar=c(1,1,1,1))
for(i in 1:10) {
  m = matrix(as.numeric(rs_df[i,2:ncol(rs_df)]),28,28,byrow=T)
  m = t(apply(m, 1, rev))
  image(m, axes = FALSE, col = grey(seq(0, 1, length = 256))) }


# Train-test split
#-------------------------------------------------------------------------------
# Simple train-test split. No crossvalidation is done in this tutorial.
# Set seed for reproducibility purposes
set.seed(100)
# Shuffled df
shuffled <- rs_df[sample(1:400),]
# Train-test split
train_28 <- shuffled[1:360, ]
test_28 <- shuffled[361:400, ]
# Save train-test datasets
write.csv(train_28, "train_28.csv", row.names = FALSE)
write.csv(test_28, "test_28.csv", row.names = FALSE)
# Done!
print("Done!")
[1] "Done!"


Train a CNN Model

Setup

# Clean workspace
rm(list=ls())
# Load MXNet
require(mxnet)
Loading required package: mxnet
Init Rcpp
# Loading data and set up
#-------------------------------------------------------------------------------
# Load train and test datasets
train <- read.csv("train_28.csv")
test <- read.csv("test_28.csv")
# Set up train and test datasets
train <- data.matrix(train)
train_x <- t(train[, -1])
train_y <- train[, 1]
train_array <- train_x
dim(train_array) <- c(28, 28, 1, ncol(train_x))
test_x <- t(test[, -1])
test_y <- test[, 1]
test_array <- test_x
dim(test_array) <- c(28, 28, 1, ncol(test_x))
# Set up the symbolic model
#-------------------------------------------------------------------------------
data <- mx.symbol.Variable('data')
# 1st convolutional layer
conv_1 <- mx.symbol.Convolution(data = data, kernel = c(5, 5), num_filter = 20)
tanh_1 <- mx.symbol.Activation(data = conv_1, act_type = "tanh")
pool_1 <- mx.symbol.Pooling(data = tanh_1, pool_type = "max", kernel = c(2, 2), stride = c(2, 2))
# 2nd convolutional layer
conv_2 <- mx.symbol.Convolution(data = pool_1, kernel = c(5, 5), num_filter = 50)
tanh_2 <- mx.symbol.Activation(data = conv_2, act_type = "tanh")
pool_2 <- mx.symbol.Pooling(data=tanh_2, pool_type = "max", kernel = c(2, 2), stride = c(2, 2))
# 1st fully connected layer
flatten <- mx.symbol.Flatten(data = pool_2)
fc_1 <- mx.symbol.FullyConnected(data = flatten, num_hidden = 500)
tanh_3 <- mx.symbol.Activation(data = fc_1, act_type = "tanh")
# 2nd fully connected layer
fc_2 <- mx.symbol.FullyConnected(data = tanh_3, num_hidden = 40)
# Output. Softmax output since we'd like to get some probabilities.
NN_model <- mx.symbol.SoftmaxOutput(data = fc_2)
# Set seed for reproducibility
mx.set.seed(100)
# Device used. CPU in my case.
devices <- mx.cpu()


Training

# Training
#-------------------------------------------------------------------------------
# Train the model
model <- mx.model.FeedForward.create(NN_model,
                                     X = train_array,
                                     y = train_y,
                                     ctx = devices,
                                     num.round = 400,
                                     array.batch.size = 40,
                                     learning.rate = 0.01,
                                     momentum = 0.9,
                                     eval.metric = mx.metric.accuracy,
                                     epoch.end.callback = mx.callback.log.train.metric(100))

Output omitted.

Predicting & Testing

# Predict labels
predicted <- predict(model, test_array)
# Assign labels
predicted_labels <- max.col(t(predicted)) - 1
# Get accuracy
sum(diag(table(test[, 1], predicted_labels)))/40
[1] 0.975
LS0tDQp0aXRsZTogIlB5dGhvbiBhbmQgQ292b2x1dGlvbmFsIE5ldXJhbCBOZXR3b3JrIg0KYXV0aG9yOiAiVG9ueSBDaHVvLCB0b255Y2h1b0BnbWFpbC5jb20iDQpkYXRlOiAiYHIgU3lzLkRhdGUoKWAiDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQrmlrDniYjnmoQgUlN0dWRpbyDlj6/ku6XlnKjllq7kuIDku4vpnaLkuIvkvb/nlKjlpJrnqK7oqp7oqIDvvIzpgJnoo6HmiJHmvJTnpLrkuIDkuIvlkIzmmYLnlKggUHl0aG9uIOWSjCBS77yM6YCP6YGO44CM5Y2356mN56We57aT57ay57WhKENOTu+8jENvbnZvbHV0aW9uYWwgTmV1cmFsIE5ldHdvcmsp44CN5L6G5YGa6IeJ5a2U6L6o6K2Y44CC5Y2z5L2/5Zyo55u455W25L2O55qE6Kej5p6Q5bqmKDI4KjI4KeS5i+S4i++8jOaooeWei+mChOaYr+WPr+S7peacieS4jemMr+eahOi+qOitmOeOhyg5Ny41JSnvvIxDTk7nmoTlqIHlipvlj6/opovkuIDoiKzjgILnqIvlvI/nmoTop6Poqqroq4vlj4PogIMgW+WOn1BPXShodHRwczovL2ZpcnN0dGltZXByb2dyYW1tZXIuYmxvZ3Nwb3QudHcvMjAxNi8wOC9pbWFnZS1yZWNvZ25pdGlvbi10dXRvcmlhbC1pbi1yLXVzaW5nLmh0bWwp44CCDQoNCjwvYnI+DQoNCiMjIyBJbnN0YWxsYXRpb24NCuWboOeCuumAmeS6m1BhY2thZ2Vz5q+U6LyD5paw77yM5L2g5Y+v6IO96ZyA6KaB55So5Lul5LiL55qE5pa55rOV5YGa5a6J6KOd5ZKM6Kit5a6a77yaDQoNCmBgYHtyIGV2YWw9RkFMU0V9DQojIGluc3RhbGwgRUJJbWFnZSBmcm9tIEJpb2NvbmR1Y3Rvcg0Kc291cmNlKCJodHRwOi8vYmlvY29uZHVjdG9yLm9yZy9iaW9jTGl0ZS5SIikNCmJpb2NMaXRlKCJFQkltYWdlIikNCg0KIyBpbnN0YWxsIG14bmV0IGl0cyBvd24gcmVwbw0KaW5zdGFsbC5wYWNrYWdlcygiZHJhdCIsIHJlcG9zPSJodHRwczovL2NyYW4ucnN0dWRpby5jb20iKQ0KZHJhdDo6OmFkZFJlcG8oImRtbGMiKQ0KaW5zdGFsbC5wYWNrYWdlcygibXhuZXQiKQ0KDQojIFlvdSBtYXkgYWxzbyBuZWVkIHRvIHJlLWluc3RhbGwgRGlhZ3JhbW1lUiBmb3IgYSBjb21wYXRhYmlsaXR5IGlzc3VlLg0KIyBIb3BlZnVsbHkgdGhpcyBzdGVwIHdvbid0IGJlIG5lZWRlZCBpbiB0aGUgbmVhciBmdXR1cmUuDQpyZW1vdmUucGFja2FnZXMoIkRpYWdyYW1tZVIiKQ0KcmVxdWlyZShkZXZ0b29scykNCnBhY2thZ2V1cmwgPC0gImh0dHA6Ly9jcmFuLnVzLnItcHJvamVjdC5vcmcvc3JjL2NvbnRyaWIvQXJjaGl2ZS9EaWFncmFtbWVSL0RpYWdyYW1tZVJfMC44LjEudGFyLmd6Ig0KaW5zdGFsbC5wYWNrYWdlcyhwYWNrYWdldXJsLCByZXBvcz1OVUxMLCB0eXBlPSJzb3VyY2UiKQ0KYGBgDQo8L2JyPg0KDQoNCiMjIyBMb2FkaW5nICYgUHJlLVByb2Nlc3NpbmcNCg0KYGBge3B5dGhvbn0NCiMgSW1wb3J0cw0KZnJvbSBza2xlYXJuLmRhdGFzZXRzIGltcG9ydCBmZXRjaF9vbGl2ZXR0aV9mYWNlcw0KaW1wb3J0IG51bXB5IGFzIG5wDQoNCiMgRG93bmxvYWQgT2xpdmV0dGkgZmFjZXMgZGF0YXNldA0Kb2xpdmV0dGkgPSBmZXRjaF9vbGl2ZXR0aV9mYWNlcygnZGF0YScpDQp4ID0gb2xpdmV0dGkuaW1hZ2VzDQp5ID0gb2xpdmV0dGkudGFyZ2V0DQoNCiMgUHJpbnQgaW5mbyBvbiBzaGFwZXMgYW5kIHJlc2hhcGUgd2hlcmUgbmVjZXNzYXJ5DQpwcmludCgiT3JpZ2luYWwgeCBzaGFwZToiLCB4LnNoYXBlKQ0KWCA9IHgucmVzaGFwZSgoNDAwLCA0MDk2KSkNCnByaW50KCJOZXcgeCBzaGFwZToiLCBYLnNoYXBlKQ0KcHJpbnQoInkgc2hhcGUiLCB5LnNoYXBlKQ0KDQojIFNhdmUgdGhlIG51bXB5IGFycmF5cw0KbnAuc2F2ZXR4dCgib2xpdmV0dGlfWC5jc3YiLCBYLCBkZWxpbWl0ZXIgPSAiLCIpDQpucC5zYXZldHh0KCJvbGl2ZXR0aV95LmNzdiIsIHksIGRlbGltaXRlciA9ICIsIiwgZm10ID0gJyVkJykNCg0KcHJpbnQoIlxuRG93bmxvYWRpbmcgYW5kIHJlc2hhcGluZyBkb25lISIpDQoNCmBgYA0KDQo8L2JyPg0KDQojIyMgUmVzaGFwaW5nDQpgYGB7cn0NCiMgVGhpcyBzY3JpcHQgaXMgdXNlZCB0byByZXNpemUgaW1hZ2VzIGZyb20gNjR4NjQgdG8gMjh4MjggcGl4ZWxzDQoNCiMgQ2xlYXIgd29ya3NwYWNlDQpybShsaXN0PWxzKCkpDQoNCiMgTG9hZCBFQkltYWdlIGxpYnJhcnkNCnJlcXVpcmUoRUJJbWFnZSkNCg0KIyBMb2FkIGRhdGENClggPC0gcmVhZC5jc3YoIm9saXZldHRpX1guY3N2IiwgaGVhZGVyID0gRikNCmxhYmVscyA8LSByZWFkLmNzdigib2xpdmV0dGlfeS5jc3YiLCBoZWFkZXIgPSBGKQ0KDQojIERhdGFmcmFtZSBvZiByZXNpemVkIGltYWdlcw0KcnNfZGYgPC0gZGF0YS5mcmFtZSgpDQoNCiMgTWFpbiBsb29wOiBmb3IgZWFjaCBpbWFnZSwgcmVzaXplIGFuZCBzZXQgaXQgdG8gZ3JleXNjYWxlDQpmb3IoaSBpbiAxOm5yb3coWCkpDQp7DQogICAgIyBUcnktY2F0Y2gNCiAgICByZXN1bHQgPC0gdHJ5Q2F0Y2goew0KICAgICMgSW1hZ2UgKGFzIDFkIHZlY3RvcikNCiAgICBpbWcgPC0gYXMubnVtZXJpYyhYW2ksXSkNCiAgICAjIFJlc2hhcGUgYXMgYSA2NHg2NCBpbWFnZSAoRUJJbWFnZSBvYmplY3QpDQogICAgaW1nIDwtIEltYWdlKGltZywgZGltPWMoNjQsIDY0KSwgY29sb3Jtb2RlID0gIkdyYXlzY2FsZSIpDQogICAgIyBSZXNpemUgaW1hZ2UgdG8gMjh4MjggcGl4ZWxzDQogICAgaW1nX3Jlc2l6ZWQgPC0gcmVzaXplKGltZywgdyA9IDI4LCBoID0gMjgpDQogICAgIyBHZXQgaW1hZ2UgbWF0cml4ICh0aGVyZSBzaG91bGQgYmUgYW5vdGhlciBmdW5jdGlvbiANCiAgICAjIHRvIGRvIHRoaXMgZmFzdGVyIGFuZCBtb3JlIG5lYXRseSEpDQogICAgaW1nX21hdHJpeCA8LSBpbWdfcmVzaXplZEAuRGF0YQ0KICAgICMgQ29lcmNlIHRvIGEgdmVjdG9yDQogICAgaW1nX3ZlY3RvciA8LSBhcy52ZWN0b3IodChpbWdfbWF0cml4KSkNCiAgICAjIEFkZCBsYWJlbA0KICAgIGxhYmVsIDwtIGxhYmVsc1tpLF0NCiAgICB2ZWMgPC0gYyhsYWJlbCwgaW1nX3ZlY3RvcikNCiAgICAjIFN0YWNrIGluIHJzX2RmIHVzaW5nIHJiaW5kDQogICAgcnNfZGYgPC0gcmJpbmQocnNfZGYsIHZlYykNCiAgICAjIFByaW50IHN0YXR1cw0KICAgIHByaW50KHBhc3RlKCJEb25lIixpLHNlcCA9ICIgIikpfSwNCiAgICAjIEVycm9yIGZ1bmN0aW9uIChqdXN0IHByaW50cyB0aGUgZXJyb3IpLiBCdHcgeW91IHNob3VsZCBnZXQgbm8gZXJyb3JzIQ0KICAgIGVycm9yID0gZnVuY3Rpb24oZSl7cHJpbnQoZSl9KQ0KfQ0KDQoNCiMgU2V0IG5hbWVzLiBUaGUgZmlyc3QgY29sdW1ucyBhcmUgdGhlIGxhYmVscywgdGhlIG90aGVyIGNvbHVtbnMgYXJlIHRoZSBwaXhlbHMuDQpuYW1lcyhyc19kZikgPC0gYygibGFiZWwiLCBwYXN0ZSgicGl4ZWwiLCBjKDE6Nzg0KSkpDQpgYGANCk91dHB1dCBvbWl0dGVkLjwvYnI+IA0KPC9icj4NCg0KT3JpZ2luYWwgSW1hZ2UNCmBgYHtyIGZpZy53aWR0aD04LCBmaWcuaGVpZ2h0PTMsIGZpZy5hbGlnbj0nY2VudHJlcid9DQpwYXIobWZyb3c9YygyLDUpLG1hcj1jKDEsMSwxLDEpKQ0KZm9yKGkgaW4gMToxMCkgew0KICBpbWcgPC0gYXMubnVtZXJpYyhYW2ksXSkNCiAgaW1nIDwtIEltYWdlKGltZywgZGltPWMoNjQsIDY0KSwgY29sb3Jtb2RlID0gIkdyYXlzY2FsZSIpDQogIG0gPSB0KGFwcGx5KGltZ0AuRGF0YSwgMSwgcmV2KSkNCiAgaW1hZ2UobSwgYXhlcyA9IEZBTFNFLCBjb2wgPSBncmV5KHNlcSgwLCAxLCBsZW5ndGggPSAyNTYpKSkNCn0NCmBgYA0KDQpSZXN1Y2UgdGhlIHJlc29sdXRpb24NCmBgYHtyIGZpZy53aWR0aD04LCBmaWcuaGVpZ2h0PTMsIGZpZy5hbGlnbj0nY2VudHJlcid9DQpwYXIobWZyb3c9YygyLDUpLG1hcj1jKDEsMSwxLDEpKQ0KZm9yKGkgaW4gMToxMCkgew0KICBtID0gbWF0cml4KGFzLm51bWVyaWMocnNfZGZbaSwyOm5jb2wocnNfZGYpXSksMjgsMjgsYnlyb3c9VCkNCiAgbSA9IHQoYXBwbHkobSwgMSwgcmV2KSkNCiAgaW1hZ2UobSwgYXhlcyA9IEZBTFNFLCBjb2wgPSBncmV5KHNlcSgwLCAxLCBsZW5ndGggPSAyNTYpKSkgfQ0KYGBgDQo8L2JyPg0KDQoNCmBgYHtyfQ0KIyBUcmFpbi10ZXN0IHNwbGl0DQojLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQ0KIyBTaW1wbGUgdHJhaW4tdGVzdCBzcGxpdC4gTm8gY3Jvc3N2YWxpZGF0aW9uIGlzIGRvbmUgaW4gdGhpcyB0dXRvcmlhbC4NCg0KIyBTZXQgc2VlZCBmb3IgcmVwcm9kdWNpYmlsaXR5IHB1cnBvc2VzDQpzZXQuc2VlZCgxMDApDQoNCiMgU2h1ZmZsZWQgZGYNCnNodWZmbGVkIDwtIHJzX2RmW3NhbXBsZSgxOjQwMCksXQ0KDQojIFRyYWluLXRlc3Qgc3BsaXQNCnRyYWluXzI4IDwtIHNodWZmbGVkWzE6MzYwLCBdDQp0ZXN0XzI4IDwtIHNodWZmbGVkWzM2MTo0MDAsIF0NCg0KIyBTYXZlIHRyYWluLXRlc3QgZGF0YXNldHMNCndyaXRlLmNzdih0cmFpbl8yOCwgInRyYWluXzI4LmNzdiIsIHJvdy5uYW1lcyA9IEZBTFNFKQ0Kd3JpdGUuY3N2KHRlc3RfMjgsICJ0ZXN0XzI4LmNzdiIsIHJvdy5uYW1lcyA9IEZBTFNFKQ0KDQojIERvbmUhDQpwcmludCgiRG9uZSEiKQ0KYGBgDQo8L2JyPg0KDQojIyMgVHJhaW4gYSBDTk4gTW9kZWwNCg0KIyMjIyBTZXR1cA0KDQpgYGB7cn0NCiMgQ2xlYW4gd29ya3NwYWNlDQpybShsaXN0PWxzKCkpDQoNCiMgTG9hZCBNWE5ldA0KcmVxdWlyZShteG5ldCkNCg0KIyBMb2FkaW5nIGRhdGEgYW5kIHNldCB1cA0KIy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCg0KIyBMb2FkIHRyYWluIGFuZCB0ZXN0IGRhdGFzZXRzDQp0cmFpbiA8LSByZWFkLmNzdigidHJhaW5fMjguY3N2IikNCnRlc3QgPC0gcmVhZC5jc3YoInRlc3RfMjguY3N2IikNCg0KIyBTZXQgdXAgdHJhaW4gYW5kIHRlc3QgZGF0YXNldHMNCnRyYWluIDwtIGRhdGEubWF0cml4KHRyYWluKQ0KdHJhaW5feCA8LSB0KHRyYWluWywgLTFdKQ0KdHJhaW5feSA8LSB0cmFpblssIDFdDQp0cmFpbl9hcnJheSA8LSB0cmFpbl94DQpkaW0odHJhaW5fYXJyYXkpIDwtIGMoMjgsIDI4LCAxLCBuY29sKHRyYWluX3gpKQ0KDQp0ZXN0X3ggPC0gdCh0ZXN0WywgLTFdKQ0KdGVzdF95IDwtIHRlc3RbLCAxXQ0KdGVzdF9hcnJheSA8LSB0ZXN0X3gNCmRpbSh0ZXN0X2FycmF5KSA8LSBjKDI4LCAyOCwgMSwgbmNvbCh0ZXN0X3gpKQ0KDQojIFNldCB1cCB0aGUgc3ltYm9saWMgbW9kZWwNCiMtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQoNCmRhdGEgPC0gbXguc3ltYm9sLlZhcmlhYmxlKCdkYXRhJykNCiMgMXN0IGNvbnZvbHV0aW9uYWwgbGF5ZXINCmNvbnZfMSA8LSBteC5zeW1ib2wuQ29udm9sdXRpb24oZGF0YSA9IGRhdGEsIGtlcm5lbCA9IGMoNSwgNSksIG51bV9maWx0ZXIgPSAyMCkNCnRhbmhfMSA8LSBteC5zeW1ib2wuQWN0aXZhdGlvbihkYXRhID0gY29udl8xLCBhY3RfdHlwZSA9ICJ0YW5oIikNCnBvb2xfMSA8LSBteC5zeW1ib2wuUG9vbGluZyhkYXRhID0gdGFuaF8xLCBwb29sX3R5cGUgPSAibWF4Iiwga2VybmVsID0gYygyLCAyKSwgc3RyaWRlID0gYygyLCAyKSkNCiMgMm5kIGNvbnZvbHV0aW9uYWwgbGF5ZXINCmNvbnZfMiA8LSBteC5zeW1ib2wuQ29udm9sdXRpb24oZGF0YSA9IHBvb2xfMSwga2VybmVsID0gYyg1LCA1KSwgbnVtX2ZpbHRlciA9IDUwKQ0KdGFuaF8yIDwtIG14LnN5bWJvbC5BY3RpdmF0aW9uKGRhdGEgPSBjb252XzIsIGFjdF90eXBlID0gInRhbmgiKQ0KcG9vbF8yIDwtIG14LnN5bWJvbC5Qb29saW5nKGRhdGE9dGFuaF8yLCBwb29sX3R5cGUgPSAibWF4Iiwga2VybmVsID0gYygyLCAyKSwgc3RyaWRlID0gYygyLCAyKSkNCiMgMXN0IGZ1bGx5IGNvbm5lY3RlZCBsYXllcg0KZmxhdHRlbiA8LSBteC5zeW1ib2wuRmxhdHRlbihkYXRhID0gcG9vbF8yKQ0KZmNfMSA8LSBteC5zeW1ib2wuRnVsbHlDb25uZWN0ZWQoZGF0YSA9IGZsYXR0ZW4sIG51bV9oaWRkZW4gPSA1MDApDQp0YW5oXzMgPC0gbXguc3ltYm9sLkFjdGl2YXRpb24oZGF0YSA9IGZjXzEsIGFjdF90eXBlID0gInRhbmgiKQ0KIyAybmQgZnVsbHkgY29ubmVjdGVkIGxheWVyDQpmY18yIDwtIG14LnN5bWJvbC5GdWxseUNvbm5lY3RlZChkYXRhID0gdGFuaF8zLCBudW1faGlkZGVuID0gNDApDQojIE91dHB1dC4gU29mdG1heCBvdXRwdXQgc2luY2Ugd2UnZCBsaWtlIHRvIGdldCBzb21lIHByb2JhYmlsaXRpZXMuDQpOTl9tb2RlbCA8LSBteC5zeW1ib2wuU29mdG1heE91dHB1dChkYXRhID0gZmNfMikNCg0KIyBTZXQgc2VlZCBmb3IgcmVwcm9kdWNpYmlsaXR5DQpteC5zZXQuc2VlZCgxMDApDQoNCiMgRGV2aWNlIHVzZWQuIENQVSBpbiBteSBjYXNlLg0KZGV2aWNlcyA8LSBteC5jcHUoKQ0KYGBgDQo8L2JyPg0KDQojIyMjIFRyYWluaW5nDQoNCmBgYHtyfQ0KIyBUcmFpbmluZw0KIy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCiMgVHJhaW4gdGhlIG1vZGVsDQptb2RlbCA8LSBteC5tb2RlbC5GZWVkRm9yd2FyZC5jcmVhdGUoTk5fbW9kZWwsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgWCA9IHRyYWluX2FycmF5LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHkgPSB0cmFpbl95LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGN0eCA9IGRldmljZXMsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbnVtLnJvdW5kID0gNDAwLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFycmF5LmJhdGNoLnNpemUgPSA0MCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsZWFybmluZy5yYXRlID0gMC4wMSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtb21lbnR1bSA9IDAuOSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBldmFsLm1ldHJpYyA9IG14Lm1ldHJpYy5hY2N1cmFjeSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBlcG9jaC5lbmQuY2FsbGJhY2sgPSBteC5jYWxsYmFjay5sb2cudHJhaW4ubWV0cmljKDEwMCkpDQpgYGANCk91dHB1dCBvbWl0dGVkLiA8YnI+DQo8YnI+DQoNCiMjIyMgUHJlZGljdGluZyAmIFRlc3RpbmcNCg0KYGBge3J9DQojIFByZWRpY3QgbGFiZWxzDQpwcmVkaWN0ZWQgPC0gcHJlZGljdChtb2RlbCwgdGVzdF9hcnJheSkNCiMgQXNzaWduIGxhYmVscw0KcHJlZGljdGVkX2xhYmVscyA8LSBtYXguY29sKHQocHJlZGljdGVkKSkgLSAxDQojIEdldCBhY2N1cmFjeQ0Kc3VtKGRpYWcodGFibGUodGVzdFssIDFdLCBwcmVkaWN0ZWRfbGFiZWxzKSkpLzQwDQpgYGANCg0KDQo=