Neural Network with Apache Spark Machine Learning Multilayer Perceptron Classifier

Biology Neuron vs Digital Perceptron:

Neuron

The perceptron is a mathematical replica of a biological neuron. While in actual neurons the dendrite receives electrical signals from the axons of other neurons.
This is also modeled in the perceptron by multiplying each input value by a coefficient called weight, sometime, plus another value called bias. An actual neuron fires an output signal only when the total strength of the input signals exceed a certain threshold.

Perceptron

Imitating, in a perceptron, weighted sum of the inputs to represent the total strength of the input signals is calculated the, and then is applied a step function (or called activate function) on the sum to determine its output.

Multilayer perceptron classifier from Apache Spark Machine Learning

Multilayer perceptron classifier (MLPC) from Apache Spark ML is a classifier based on the feedforward artificial neural network. MLPC consists of multiple layers of nodes. Each layer is fully connected to the next layer in the network.
Note:
Each circle in above represents a perceptron:
Each layer is fully connected means neurons (perceptron) are all neurons on the next layer
Feedforward artificial neural network means signals move forward, there is no loop back.

Math representation of the neural network

Neurons in the input layer represent the input data. All other neurons map inputs to outputs by a linear combination of the inputs with the neuron’s weights w and bias b and applying an activation function or step function. This can be written in matrix form for MLPC with K+1 layers as follows:

Activate/Step function:

Neurons in intermediate layers (hidden layers) use sigmoid (logistic) function:
Neurons in the output layer use SoftMax function:

Notes on number neurons/perceptron

The number of neurons N in the output layer corresponds to the number of classes to be classified, The number of neurons in the first layer needs to be equal to number of features (columns)

Build a neural network with Apache Spark Multilayer perceptron classifier (MPLC)

As part of the effort, I need a dataset to train and test the MPLC. The obvious one to go to would classify images of hand written digits data set called MNIST and I am going to code in Scala.
Download MNIST dataset
Down the 4 data files from
However, they are binary files, in ubyte format, and not readily be loaded into Apache Spark dataframe and must be preprocessed.

Data Preprocessing

Ubyte to CSV to libsvm

Ubyte file format is formally known as IDX format.
The IDX file format is a simple format for vectors and multidimensional matrices of various numerical types.
The basic format according to http://yann.lecun.com/exdb/mnist/ is:
magic number
size in dimension 1
size in dimension 2
size in dimension 3
….
size in dimension N
data
The magic number is four bytes long.
The first 2 bytes are always 0.
The third byte codes the type of the data:
0x08: unsigned byte
0x09: signed byte
0x0B: short (2 bytes)
0x0C: int (4 bytes)
0x0D: float (4 bytes)
0x0E: double (8 bytes)
The fourth byte codes the number of dimensions of the vector/matrix: 1 for vectors, 2 for matrices….
The sizes in each dimension are 4-byte integers (big endian, like in most non-Intel processors).
The data is stored like in a C array, i.e. the index in the last dimension changes the fastest.
Convert the 4 MNIST files
These 2 files are for training (1 is feature data, 1 is label data)
train-images-idx3-ubyte
train-labels-idx1-ubyte
These 2 files are for testing (1 is feature data, 1 is label)
t10k-images-idx3-ubyte
t10k-labels-idx1-ubyte

File exploration

To understand the data definition of the feature data, hex dump the file and show first 20 lines
1
(spark) [email protected]:~/libsvm$ xxd train-images-idx3-ubyte | head -n 20
2
00000000: 0000 0803 0000 ea60 0000 001c 0000 001c .......`........
3
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
4
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
5
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
6
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
7
00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................
8
00000060: 0000 0000 0000 0000 0000 0000 0000 0000 ................
9
00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
10
00000080: 0000 0000 0000 0000 0000 0000 0000 0000 ................
11
00000090: 0000 0000 0000 0000 0000 0000 0000 0000 ................
12
000000a0: 0000 0000 0000 0000 0312 1212 7e88 af1a ............~...
13
000000b0: a6ff f77f 0000 0000 0000 0000 0000 0000 ................
14
000000c0: 1e24 5e9a aafd fdfd fdfd e1ac fdf2 c340 .$^[email protected]
15
000000d0: 0000 0000 0000 0000 0000 0031 eefd fdfd ...........1....
16
000000e0: fdfd fdfd fdfb 5d52 5238 2700 0000 0000 ......]RR8'.....
17
000000f0: 0000 0000 0000 0012 dbfd fdfd fdfd c6b6 ................
18
00000100: f7f1 0000 0000 0000 0000 0000 0000 0000 ................
19
00000110: 0000 0000 509c 6bfd fdcd 0b00 2b9a 0000 ....P.k.....+...
20
00000120: 0000 0000 0000 0000 0000 0000 0000 0000 ................
21
00000130: 000e 019a fd5a 0000 0000 0000 0000 0000 .....Z..........
Copied!
The first 2 bytes are always 0.
First 2 bytes are 00 00
Third byte is 08 (0x08: unsigned byte)
Fourth byte is 03 (The fourth byte codes the number of dimensions of the vector/matrix: 1 for vectors, 2 for matrices….), so this is 3 dimension.
Since this is 3-dimension dataset:
Next 4-byte integer is 0000ea60, which is the size of 1st dimension. 0x 0000ea60 = 60000
Next 4-byte integer is 0000001c, which is the size of 2nd dimension. 0x 0000001c = 28
Next 4-byte integer is 0000001c, which is the size of 3rd dimension. 0x 0000001c = 28
Actual feature data follows after that.
This means the training feature data is 60000 rows, each row is a flattened matrix of 28*28 = 784 (feature) columns. This also means, 1st 16 bytes of the training image data are not actual data, but metadata. These 16 bytes will be thrown away when extract actual data from file in the code later on.
Now do the same on the label data file:
1
(spark) [email protected]:~/libsvm$ xxd train-labels-idx1-ubyte | head -n 20
2
00000000: 0000 0801 0000 ea60 0500 0401 0902 0103 .......`........
3
00000010: 0104 0305 0306 0107 0208 0609 0400 0901 ................
4
00000020: 0102 0403 0207 0308 0609 0005 0600 0706 ................
5
00000030: 0108 0709 0309 0805 0903 0300 0704 0908 ................
6
00000040: 0009 0401 0404 0600 0405 0601 0000 0107 ................
7
00000050: 0106 0300 0201 0107 0900 0206 0708 0309 ................
8
00000060: 0004 0607 0406 0800 0708 0301 0507 0107 ................
9
00000070: 0101 0603 0002 0903 0101 0004 0902 0000 ................
10
00000080: 0200 0207 0108 0604 0106 0304 0509 0103 ................
11
00000090: 0308 0504 0707 0402 0805 0806 0703 0406 ................
12
000000a0: 0109 0906 0003 0702 0802 0904 0406 0409 ................
13
000000b0: 0700 0902 0905 0105 0901 0203 0203 0509 ................
14
000000c0: 0107 0602 0802 0205 0007 0409 0708 0302 ................
15
000000d0: 0101 0803 0601 0003 0100 0001 0702 0703 ................
16
000000e0: 0004 0605 0206 0407 0108 0909 0300 0701 ................
17
000000f0: 0002 0003 0504 0605 0806 0307 0508 0009 ................
18
00000100: 0100 0301 0202 0303 0604 0705 0006 0207 ................
19
00000110: 0908 0509 0201 0104 0405 0604 0102 0503 ................
20
00000120: 0903 0900 0509 0605 0704 0103 0400 0408 ................
21
00000130: 0004 0306 0807 0600 0907 0507 0201 0106 ................
Copied!
First 2 bytes are 00 00
Third byte is 08 (0x08: unsigned byte)
Fourth byte is 01 (The fourth byte codes the number of dimensions of the vector/matrix: 1 for vectors, 2 for matrices….), this is 1 dimension.
Since this is 1-dimension dataset:
Next 4-byte integer is 0000ea60, which is the size of 1st dimension. 0x 0000ea60 = 60000
Actual feature data follows after that.
This means the training label data is 60000 rows and 1 column. This also means, 1st 8 bytes of the training image label data are not actual data, but metadata. These 8 bytes will be thrown away when extract actual data from file in the code later on.
Test data files are the same format, no need to analyze. Except testing data is 10000 rows while training data is 60000 rows.

Helper Scala utility function to convert MNIST ubyte files to CSV file

Following Scala code is going to read the image and label data files in ubyte/IDX format and convert into text file delimited by β€œ,”, called CSV file.
1
import java.io._
2
/*
3
define helper function to perform MNIST ubyte files to csv conversion. Read ubyte files, and transform and save as single CSV file, 1 column is label, next 784 columns are features
4
*/
5
def mnistFileConvertUByteToCSV(imageFileName: String, labelFileName: String, recNum: Int, csvFileName: String): Unit={
6
val rows = recNum
7
val cols = 28*28
8
var data = None: Option[FileInputStream]
9
var label = None: Option[FileInputStream]
10
var out = None: Option[FileOutputStream]
11
val features = Array.ofDim[Int](rows, cols)
12
val file = new File(csvFileName)
13
val output = new BufferedWriter(new FileWriter(file))
14
val target=Array.ofDim[Int](rows,1)
15
try {
16
data = Some(new FileInputStream(imageFileName))
17
label = Some(new FileInputStream(labelFileName))
18
var c = 0
19
//throw away 1st 16 bytes as they are not data for feature file
20
for (_<-0 until 16)
21
data.get.read
22
//throw away 1st 8 bytes as they are not data for label file
23
for (_<-0 until 8)
24
label.get.read
25
c = 0
26
for (i<-0 until rows)
27
{
28
c=label.get.read
29
target(i)(0)=c
30
output.write(c.toString)
31
output.write(",")
32
for (j<-0 until cols)
33
{
34
c = data.get.read
35
features(i)(j)=c
36
output.write(c.toString)
37
if (j<cols-1)
38
output.write(",")
39
else
40
output.write("\n")
41
}
42
if (i%1000==0)
43
println(s"Line number: $i")
44
} } catch {
45
case e: IOException => e.printStackTrace
46
} finally {
47
println("entered finally ...")
48
if (data.isDefined) data.get.close
49
if (label.isDefined)
50
{
51
label.get.close
52
}
53
output.close
54
}
55
}
56
/*
57
X is path to the mnist feature file (existing)
58
Y is path to the mnist label file (existing)
59
Z is path to the csv file after conversion (to be created)
60
*/
61
var x="/home/bigdata2/libsvm/train-images-idx3-ubyte"
62
var y="/home/bigdata2/libsvm/train-labels-idx1-ubyte"
63
var z="/home/bigdata2/libsvm/mnist_train.csv"
64
// To convert training MNIST files to a single CSV training file
65
mnistFileConvertUByteToCSV(x,y,60000,z)
66
/*
67
I place some print out to make sure it is not hanging
68
Line number: 0
69
Line number: 1000
70
Line number: 2000
71
Line number: 3000
72
Line number: 4000
73
...
74
Line number: 58000
75
Line number: 59000
76
entered finally ...
77
*/
78
x="/home/bigdata2/libsvm/t10k-images-idx3-ubyte"
79
y="/home/bigdata2/libsvm/t10k-labels-idx1-ubyte"
80
z="/home/bigdata2/libsvm/t10k.csv"
81
// To convert testing MNIST files to a single CSV testing file
82
mnistFileConvertUByteToCSV(x,y,10000,z)
83
/*
84
Line number: 0
85
Line number: 1000
86
Line number: 2000
87
Line number: 3000
88
...
89
Line number: 9000
90
entered finally ...
91
*/
Copied!

About libsvm format file

It is very common in practice to have sparse training data. MLlib supports reading training examples stored in LIBSVM format, which is the default format used by LIBSVM and LIBLINEAR. It is a text format in which each line represents a labeled sparse feature vector using the following format:
label index1:value1 index2:value2 …
where the indices are one-based and in ascending order. After loading, the feature indices are converted to zero-based.
By the way, LIBSVM format does not have to have sparse data storage, data storage can be dense. Sparse data storage means it will not store fields with zero value, it only stores fields with non-zero value. Dense data storage means it stores fields with all values including zero values. Consequently, file size with dense storage will be larger than the one with sparse storage, but conceivably, it takes less work to process a libsvm file with dense storage than sparse storage.
I also wanted to create a utility to convert csv file into libsvm file, for simplicity, it only convert into libsvm file in dense storage.
For illustrative purpose, convert the following CSV file:
Label, value1, value2, value 3, … value N
Into libsvm file:
Label index1:value1 index2:value2 … indexN:valueN

Scala Helper utility function to convert Spark ML CSV file to libsvm file

Following is the Scala code:
1
def makeLibsvm(a:Array[String]):String ={
2
var result=a(0)+" "
3
for(i<-1 to a.size.toInt-1)
4
result=result+i+":"+a(i)(0)+" "
5
return result
6
}
7
//convert the training csv to training libsvm files
8
var csvFile=sc.textFile("file:///home/bigdata2/libsvm/mnist_train.csv")
9
var libsvm=csvFile.map(line => line.split(',')).map(i=>makeLibsvm(i))
10
libsvm.saveAsTextFile("file:///home/bigdata2/libsvm/mnist_train")
11
//convert the testing csv to testing libsvm files
12
csvFile=sc.textFile("file:///home/bigdata2/libsvm/t10k.csv")
13
libsvm=csvFile.map(line => line.split(',')).map(i=>makeLibsvm(i))
14
libsvm.saveAsTextFile("file:///home/bigdata2/libsvm/mnist_test")
Copied!
RDD class method saveAsTextFile() is likely to create multiple parts of the files, you will need to come up a way to automatically merge these parts into one file, or you can do it manually. That is the nature of Spark application that runs on cluster of multiple worker nodes.
I have merged (outside this writing resultant parts files) into mnist_train.libsvm and mnist_test.libsvm.

Caveats

If you really want the Scala code to save RDD into a single file, you will have to copy RDD that spreads on multiple worker nodes into driver worker node that you launch your Spark application on by following code (not recommended, very slow, do not run it)
1
import java.io._
2
csvFile=sc.textFile("file:///home/bigdata2/libsvm/mnist_train.csv")
3
var libsvm=csvFile.map(line => line.split(',')).map(i=>makeLibsvm(i))
4
val libsvmFile = new File("/home/bigdata2/libsvm/mnist_train.libsvm")
5
val svmout = new BufferedWriter(new FileWriter(libsvmFile))
6
//.collect will take a long time to copy RDD contents to driver node
7
val libsvmArray=libsvm.collect
8
for (i<-0 until libsvmArray.size)
9
{
10
svmout.write(libsvmArray(i))
11
svmout.write("\n")
12
if (i%1000==0)
13
print(s"line $i")
14
}
15
svmout.close
Copied!

Create a neural network to train and test MNIST image of hand written digit

Create a neural network to train and test MNIST image of hand written digit, that has already been converted to libsvm file format from original ubyte raw format that Scala is not able to comprehend.
1
import org.apache.spark._
2
import org.apache.spark.SparkContext._
3
import org.apache.spark.rdd._
4
import org.apache.spark.util.LongAccumulator
5
import org.apache.log4j._
6
import scala.collection.mutable.ArrayBuffer
7
import org.apache.spark.sql._
8
import spark.implicits._
9
/*
10
Load the train and test data from mnist_train.libsvm and mnist_test.libsvm respectively
11
*/
12
val train = spark.read.format("libsvm")
13
.load("file:///home/bigdata2/libsvm/mnist_train.libsvm")
14
val test = spark.read.format("libsvm")
15
.load("file:///home/bigdata2/libsvm/mnist_test.libsvm")
16
/*
17
This is important to check number of features (columns), here is 784, that needs to defines number of neurons (perceptrons) in the input layer.
18
*/
19
train.show(3)
20
​
21
/*
22
+-----+--------------------+
23
|label| features|
24
+-----+--------------------+
25
| 5.0|(784,[152,153,154...|
26
| 0.0|(784,[127,128,129...|
27
| 4.0|(784,[160,161,162...|
28
+-----+--------------------+
29
only showing top 3 rows
30
​
31
Define the neural network that has 4 layers, 1st layer (input) has 784 perceptrons, so are 2nd and 3rd hidden layes, 4th layer is output that has 10 perceptrons matching 10 classes, 0, 1, 2, … 9
32
*/
33
​
34
val layers = Array[Int](784, 784, 784, 10)
35
​
36
// create the trainer and set its parameters
37
​
38
import org.apache.spark.ml.classification.MultilayerPerceptronClassifier
39
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
40
val trainer = new MultilayerPerceptronClassifier()
41
.setLayers(layers)
42
.setBlockSize(128)
43
.setSeed(1234L)
44
.setMaxIter(100)
45
// train the model
46
val model = trainer.fit(train)
47
// compute accuracy on the test set
48
val result = model.transform(test)
49
val predictionAndLabels = result.select("prediction", "label")
50
val evaluator = new MulticlassClassificationEvaluator()
51
.setMetricName("accuracy")
52
println(s"Test set accuracy = ${evaluator.evaluate(predictionAndLabels)}")
53
/*
54
Test set accuracy = 0.9193
55
​
56
Training accuracy is 0.9193, about 92%, meaning out of every 100 hand written digit image, model (machine) recognizes about 92 correctly, about 8 wrong, meaning 92 prediction = label
57
​
58
​
59
In fact, you can use SQL query to come up the accuracy metrics, because result variable is a Spark SQL dataframe
60
​
61
​
62
To use Spark SQL, create a temp view from the dataframe variable result
63
*/
64
​
65
result.toDF.createOrReplaceTempView("deep_learning")
66
​
67
/*
68
Run testing query
69
select * from deep_learning where prediction = label and label = 2.0 limit 2"
70
*/
71
​
72
spark.sql("select * from deep_learning where prediction = label and label = 2.0 limit 2").show()
73
​
74
/*
75
+-----+--------------------+--------------------+--------------------+----------+
76
|label| features| rawPrediction| probability|prediction|
77
+-----+--------------------+--------------------+--------------------+----------+
78
| 2.0|(784,[94,95,96,97...|[-7.3486569212363...|[1.88751449816836...| 2.0|
79
| 2.0|(784,[124,125,126...|[-4.9435463442057...|[1.24313405935197...| 2.0|
80
+-----+--------------------+--------------------+--------------------+----------+
81
​
82
Following query will show the accuracy, to divide the count of rows where prediction=label by the total count:
83
with x as (select count(*) as count_x from deep learning
84
where prediction = label),
85
y as (select count(*) as total from deep_learning)
86
select count_x/total as accuracy from x,y
87
*/
88
​
89
spark.sql("with x as (select count(*) as count_x from deep_learning where prediction = label),y as (select count(*) as total from deep_learning) select count_x/total as accuracy from x,y").show()
90
​
91
/*
92
+--------+
93
|accuracy|
94
+--------+
95
| 0.9193|
96
+--------+
97
​
98
Following sub-query in the select list produces the same result:
99
select (select count(*) from deep_learning where label=prediction)/count(*) as accuracy from deep_learning
100
*/
101
​
102
spark.sql("select (select count(*) from deep_learning where label=prediction)/count(*) as accuracy from deep_learning").show()
103
​
104
/*
105
+--------+
106
|accuracy|
107
+--------+
108
| 0.9193|
109
+--------+
110
​
111
*/
Copied!

Summary

While Apache Spark Multilayer perceptron classifier is no replacement of TensorFlow, in fact, Apache has its own deep learning library MXNet that is more comparable to TensorFlow, building neural network with Multilayer perceptron classifier under specific use case make good sense especially on the data that is already with Spark distributed computing cluster and in concert with Spark SQL and Spark Streaming.
Last modified 1yr ago