Skip to content

Commit a226133

Browse files
committed
Multilayer Perceptron regressor and classifier
ANN test
1 parent d38c502 commit a226133

File tree

7 files changed

+1444
-0
lines changed

7 files changed

+1444
-0
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.spark.ml.classification
19+
20+
import breeze.linalg.{argmax => Bargmax}
21+
22+
import org.apache.spark.annotation.Experimental
23+
import org.apache.spark.ml.{PredictionModel, Predictor}
24+
import org.apache.spark.ml.param.ParamMap
25+
import org.apache.spark.ml.util.Identifiable
26+
import org.apache.spark.ml.regression.MultilayerPerceptronParams
27+
import org.apache.spark.mllib.ann.{FeedForwardTrainer, FeedForwardTopology}
28+
import org.apache.spark.mllib.linalg.{Vectors, Vector}
29+
import org.apache.spark.mllib.regression.LabeledPoint
30+
import org.apache.spark.sql.DataFrame
31+
32+
/**
33+
* :: Experimental ::
34+
* Label to vector converter.
35+
*/
36+
@Experimental
37+
private object LabelConverter {
38+
39+
/**
40+
* Encodes a label as a vector.
41+
* Returns a vector of given length with zeroes at all positions
42+
* and value 1.0 at the position that corresponds to the label.
43+
*
44+
* @param labeledPoint labeled point
45+
* @param labelCount total number of labels
46+
* @return vector encoding of a label
47+
*/
48+
def apply(labeledPoint: LabeledPoint, labelCount: Int): (Vector, Vector) = {
49+
val output = Array.fill(labelCount){0.0}
50+
output(labeledPoint.label.toInt) = 1.0
51+
(labeledPoint.features, Vectors.dense(output))
52+
}
53+
54+
/**
55+
* Converts a vector to a label.
56+
* Returns the position of the maximal element of a vector.
57+
*
58+
* @param output label encoded with a vector
59+
* @return label
60+
*/
61+
def apply(output: Vector): Double = {
62+
Bargmax(output.toBreeze.toDenseVector).toDouble
63+
}
64+
}
65+
66+
/**
67+
* :: Experimental ::
68+
* Classifier trainer based on the Multilayer Perceptron.
69+
* Each layer has sigmoid activation function, output layer has softmax.
70+
* Number of inputs has to be equal to the size of feature vectors.
71+
* Number of outputs has to be equal to the total number of labels.
72+
*
73+
*/
74+
@Experimental
75+
class MultilayerPerceptronClassifier (override val uid: String)
76+
extends Predictor[Vector, MultilayerPerceptronClassifier, MultilayerPerceptronClassifierModel]
77+
with MultilayerPerceptronParams {
78+
79+
override def copy(extra: ParamMap): MultilayerPerceptronClassifier = defaultCopy(extra)
80+
81+
def this() = this(Identifiable.randomUID("mlpc"))
82+
83+
/**
84+
* Train a model using the given dataset and parameters.
85+
* Developers can implement this instead of [[fit()]] to avoid dealing with schema validation
86+
* and copying parameters into the model.
87+
*
88+
* @param dataset Training dataset
89+
* @return Fitted model
90+
*/
91+
override protected def train(dataset: DataFrame): MultilayerPerceptronClassifierModel = {
92+
val labels = getLayers.last.toInt
93+
val lpData = extractLabeledPoints(dataset)
94+
val data = lpData.map(lp => LabelConverter(lp, labels))
95+
val myLayers = getLayers.map(_.toInt)
96+
val topology = FeedForwardTopology.multiLayerPerceptron(myLayers, true)
97+
val FeedForwardTrainer = new FeedForwardTrainer(topology, myLayers(0), myLayers.last)
98+
FeedForwardTrainer.LBFGSOptimizer.setConvergenceTol(getTol).setNumIterations(getMaxIter)
99+
FeedForwardTrainer.setStackSize(getBlockSize)
100+
val mlpModel = FeedForwardTrainer.train(data)
101+
new MultilayerPerceptronClassifierModel(uid, myLayers, mlpModel.weights())
102+
}
103+
}
104+
105+
/**
106+
* :: Experimental ::
107+
* Classifier model based on the Multilayer Perceptron.
108+
* Each layer has sigmoid activation function, output layer has softmax.
109+
*/
110+
@Experimental
111+
class MultilayerPerceptronClassifierModel private[ml] (override val uid: String,
112+
layers: Array[Int],
113+
weights: Vector)
114+
extends PredictionModel[Vector, MultilayerPerceptronClassifierModel]
115+
with Serializable {
116+
117+
private val mlpModel = FeedForwardTopology.multiLayerPerceptron(layers, true).getInstance(weights)
118+
119+
/**
120+
* Predict label for the given features.
121+
* This internal method is used to implement [[transform()]] and output [[predictionCol]].
122+
*/
123+
override protected def predict(features: Vector): Double = {
124+
LabelConverter(mlpModel.predict(features))
125+
}
126+
127+
override def copy(extra: ParamMap): MultilayerPerceptronClassifierModel = {
128+
copyValues(new MultilayerPerceptronClassifierModel(uid, layers, weights), extra)
129+
}
130+
}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.spark.ml.regression
19+
20+
import breeze.linalg.{argmax => Bargmax}
21+
22+
import org.apache.spark.Logging
23+
import org.apache.spark.annotation.Experimental
24+
import org.apache.spark.ml.{Model, Transformer, Estimator, PredictorParams}
25+
import org.apache.spark.ml.param._
26+
import org.apache.spark.ml.param.shared._
27+
import org.apache.spark.ml.util.Identifiable
28+
import org.apache.spark.mllib.ann.{FeedForwardTopology, FeedForwardTrainer}
29+
import org.apache.spark.mllib.linalg.{VectorUDT, Vector}
30+
import org.apache.spark.sql.{Row, DataFrame}
31+
import org.apache.spark.sql.functions._
32+
import org.apache.spark.sql.types.{StructField, StructType}
33+
34+
/**
35+
* Params for Multilayer Perceptron.
36+
*/
37+
private[ml] trait MultilayerPerceptronParams extends PredictorParams
38+
with HasSeed with HasMaxIter with HasTol {
39+
/**
40+
* Layer sizes including input size and output size.
41+
* @group param
42+
*/
43+
final val layers: IntArrayParam =
44+
// TODO: we need IntegerArrayParam!
45+
new IntArrayParam(this, "layers",
46+
"Sizes of layers including input and output from bottom to the top." +
47+
" E.g., Array(780, 100, 10) means 780 inputs, " +
48+
"hidden layer with 100 neurons and output layer of 10 neurons."
49+
// TODO: how to check that array is not empty?
50+
)
51+
52+
/**
53+
* Block size for stacking input data in matrices. Speeds up the computations.
54+
* Cannot be more than the size of the dataset.
55+
* @group expertParam
56+
*/
57+
final val blockSize: IntParam = new IntParam(this, "blockSize",
58+
"Block size for stacking input data in matrices.",
59+
ParamValidators.gt(0))
60+
61+
/** @group setParam */
62+
def setLayers(value: Array[Int]): this.type = set(layers, value)
63+
64+
/** @group getParam */
65+
final def getLayers: Array[Int] = $(layers)
66+
67+
/** @group setParam */
68+
def setBlockSize(value: Int): this.type = set(blockSize, value)
69+
70+
/** @group getParam */
71+
final def getBlockSize: Int = $(blockSize)
72+
73+
/**
74+
* Set the maximum number of iterations.
75+
* Default is 100.
76+
* @group setParam
77+
*/
78+
def setMaxIter(value: Int): this.type = set(maxIter, value)
79+
80+
/**
81+
* Set the convergence tolerance of iterations.
82+
* Smaller value will lead to higher accuracy with the cost of more iterations.
83+
* Default is 1E-4.
84+
* @group setParam
85+
*/
86+
def setTol(value: Double): this.type = set(tol, value)
87+
88+
/**
89+
* Set the seed for weights initialization.
90+
* Default is 11L.
91+
* @group setParam
92+
*/
93+
def setSeed(value: Long): this.type = set(seed, value)
94+
95+
setDefault(seed -> 11L, maxIter -> 100, tol -> 1e-4, layers -> Array(1, 1), blockSize -> 1)
96+
}
97+
98+
/**
99+
* :: Experimental ::
100+
* Multi-layer perceptron regression. Contains sigmoid activation function on all layers.
101+
* See https://en.wikipedia.org/wiki/Multilayer_perceptron for details.
102+
*
103+
*/
104+
@Experimental
105+
class MultilayerPerceptronRegressor (override val uid: String)
106+
extends Estimator[MultilayerPerceptronRegressorModel]
107+
with MultilayerPerceptronParams with HasInputCol with HasOutputCol with HasRawPredictionCol
108+
with Logging {
109+
110+
/** @group setParam */
111+
def setInputCol(value: String): this.type = set(inputCol, value)
112+
113+
/** @group setParam */
114+
def setOutputCol(value: String): this.type = set(outputCol, value)
115+
116+
/**
117+
* Fits a model to the input and output data.
118+
* InputCol has to contain input vectors.
119+
* OutputCol has to contain output vectors.
120+
*/
121+
override def fit(dataset: DataFrame): MultilayerPerceptronRegressorModel = {
122+
val data = dataset.select($(inputCol), $(outputCol)).map {
123+
case Row(x: Vector, y: Vector) => (x, y)
124+
}
125+
val myLayers = getLayers
126+
val topology = FeedForwardTopology.multiLayerPerceptron(myLayers, false)
127+
val FeedForwardTrainer = new FeedForwardTrainer(topology, myLayers(0), myLayers.last)
128+
FeedForwardTrainer.LBFGSOptimizer.setConvergenceTol(getTol).setNumIterations(getMaxIter)
129+
FeedForwardTrainer.setStackSize(getBlockSize)
130+
val mlpModel = FeedForwardTrainer.train(data)
131+
new MultilayerPerceptronRegressorModel(uid, myLayers, mlpModel.weights())
132+
}
133+
134+
/**
135+
* :: DeveloperApi ::
136+
*
137+
* Derives the output schema from the input schema.
138+
*/
139+
override def transformSchema(schema: StructType): StructType = {
140+
val inputType = schema($(inputCol)).dataType
141+
require(inputType.isInstanceOf[VectorUDT],
142+
s"Input column ${$(inputCol)} must be a vector column")
143+
val outputType = schema($(outputCol)).dataType
144+
require(outputType.isInstanceOf[VectorUDT],
145+
s"Input column ${$(outputCol)} must be a vector column")
146+
require(!schema.fieldNames.contains($(rawPredictionCol)),
147+
s"Output column ${$(rawPredictionCol)} already exists.")
148+
val outputFields = schema.fields :+ StructField($(rawPredictionCol), new VectorUDT, false)
149+
StructType(outputFields)
150+
}
151+
152+
def this() = this(Identifiable.randomUID("mlpr"))
153+
154+
override def copy(extra: ParamMap): MultilayerPerceptronRegressor = defaultCopy(extra)
155+
}
156+
157+
/**
158+
* :: Experimental ::
159+
* Multi-layer perceptron regression model.
160+
*
161+
* @param layers array of layer sizes including input and output
162+
* @param weights weights (or parameters) of the model
163+
*/
164+
@Experimental
165+
class MultilayerPerceptronRegressorModel private[ml] (override val uid: String,
166+
layers: Array[Int],
167+
weights: Vector)
168+
extends Model[MultilayerPerceptronRegressorModel]
169+
with HasInputCol with HasRawPredictionCol {
170+
171+
private val mlpModel =
172+
FeedForwardTopology.multiLayerPerceptron(layers, false).getInstance(weights)
173+
174+
/** @group setParam */
175+
def setInputCol(value: String): this.type = set(inputCol, value)
176+
177+
/**
178+
* Transforms the input dataset.
179+
* InputCol has to contain input vectors.
180+
* RawPrediction column will contain predictions (outputs of the regressor).
181+
*/
182+
override def transform(dataset: DataFrame): DataFrame = {
183+
transformSchema(dataset.schema, logging = true)
184+
val pcaOp = udf { mlpModel.predict _ }
185+
dataset.withColumn($(rawPredictionCol), pcaOp(col($(inputCol))))
186+
}
187+
188+
/**
189+
* :: DeveloperApi ::
190+
*
191+
* Derives the output schema from the input schema.
192+
*/
193+
override def transformSchema(schema: StructType): StructType = {
194+
val inputType = schema($(inputCol)).dataType
195+
require(inputType.isInstanceOf[VectorUDT],
196+
s"Input column ${$(inputCol)} must be a vector column")
197+
require(!schema.fieldNames.contains($(rawPredictionCol)),
198+
s"Output column ${$(rawPredictionCol)} already exists.")
199+
val outputFields = schema.fields :+ StructField($(rawPredictionCol), new VectorUDT, false)
200+
StructType(outputFields)
201+
}
202+
203+
override def copy(extra: ParamMap): MultilayerPerceptronRegressorModel = {
204+
copyValues(new MultilayerPerceptronRegressorModel(uid, layers, weights), extra)
205+
}
206+
}

0 commit comments

Comments
 (0)