├── ModelEvaluator.scala ├── ModelSettings.scala ├── README.md └── plane.png /ModelEvaluator.scala: -------------------------------------------------------------------------------- 1 | package io.ticofab.piai.learning 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import org.datavec.api.io.filters.RandomPathFilter 6 | import org.deeplearning4j.earlystopping.EarlyStoppingConfiguration 7 | import org.deeplearning4j.earlystopping.saver.{InMemoryModelSaver, LocalFileModelSaver} 8 | import org.deeplearning4j.earlystopping.scorecalc.ClassificationScoreCalculator 9 | import org.deeplearning4j.earlystopping.termination.{MaxEpochsTerminationCondition, MaxTimeIterationTerminationCondition} 10 | import org.deeplearning4j.earlystopping.trainer.EarlyStoppingTrainer 11 | import org.deeplearning4j.nn.multilayer.MultiLayerNetwork 12 | import org.nd4j.evaluation.classification.Evaluation 13 | import org.nd4j.evaluation.classification.Evaluation._ 14 | 15 | import scala.util.Try 16 | 17 | object ModelEvaluator { 18 | 19 | def train(): Try[Unit] = Try { 20 | 21 | info("**************** starting ********************") 22 | 23 | // Load images 24 | info("Loading images....") 25 | 26 | val randomPathFilter = new RandomPathFilter(Settings.rng, Settings.allowedExtensions, 0L) 27 | val inputSplits = Settings.fileSplit.sample(randomPathFilter, Settings.trainSetPercentage, 100.0 - Settings.trainSetPercentage) 28 | 29 | val trainData = inputSplits(0) 30 | val testData = inputSplits(1) 31 | info(s"all files: ${Settings.fileSplit.length}, train set: ${trainData.length}, test set: ${testData.length}") 32 | 33 | // Prepare iterators for train and test 34 | info("Preparing train and test iterators....") 35 | val trainIterator = Settings.getInitializedIterator(trainData) 36 | val testIterator = Settings.getInitializedIterator(testData) 37 | 38 | // Construct the neural network 39 | info("Build trainer...") 40 | val saver = 41 | if (Settings.saveModel) new LocalFileModelSaver(STRING_POINTING_TO_THE_OUTPUT_FOLDER) 42 | else new InMemoryModelSaver[MultiLayerNetwork]() 43 | 44 | val esConf = new EarlyStoppingConfiguration.Builder[MultiLayerNetwork]() 45 | .epochTerminationConditions(new MaxEpochsTerminationCondition(30)) 46 | .iterationTerminationConditions(new MaxTimeIterationTerminationCondition(6, TimeUnit.MINUTES)) 47 | .scoreCalculator(new ClassificationScoreCalculator(Metric.ACCURACY, testIterator)) 48 | .evaluateEveryNEpochs(1) 49 | .modelSaver(saver) 50 | .build 51 | 52 | val trainer = new EarlyStoppingTrainer(esConf, Settings.getNetworkConfiguration, trainIterator) 53 | 54 | // Conduct early stopping training: 55 | info("Train network...") 56 | val result = trainer.fit 57 | 58 | // Print out the results: 59 | info("Done training!") 60 | info("Termination reason: " + result.getTerminationReason) 61 | info("Termination details: " + result.getTerminationDetails) 62 | info("Total epochs: " + result.getTotalEpochs) 63 | info("Best epoch number: " + result.getBestModelEpoch) 64 | info("Score at best epoch: " + result.getBestModelScore) 65 | 66 | // Get the best model: 67 | val model = result.getBestModel 68 | val eval: Evaluation = model.evaluate(testIterator) 69 | info(eval.stats()) 70 | 71 | info("try to classify images:\n") 72 | 73 | trainIterator.reset() 74 | Settings.predictLabels(model, trainIterator) 75 | 76 | info("**************** done ********************") 77 | 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /ModelSettings.scala: -------------------------------------------------------------------------------- 1 | package io.ticofab.piai.learning 2 | 3 | import java.io.File 4 | import java.util.Random 5 | 6 | import org.datavec.api.io.labels.ParentPathLabelGenerator 7 | import org.datavec.api.split.{FileSplit, InputSplit} 8 | import org.datavec.image.loader.BaseImageLoader 9 | import org.datavec.image.recordreader.ImageRecordReader 10 | import org.deeplearning4j.datasets.datavec.RecordReaderDataSetIterator 11 | import org.deeplearning4j.nn.conf.inputs.InputType 12 | import org.deeplearning4j.nn.conf.layers._ 13 | import org.deeplearning4j.nn.conf.{MultiLayerConfiguration, NeuralNetConfiguration} 14 | import org.deeplearning4j.nn.multilayer.MultiLayerNetwork 15 | import org.deeplearning4j.nn.weights.WeightInit 16 | import org.nd4j.linalg.activations.Activation 17 | import org.nd4j.linalg.learning.config.Adam 18 | import org.nd4j.linalg.lossfunctions.LossFunctions 19 | 20 | object Settings { 21 | 22 | // information of the non-flattened images 23 | val outputHeight = 25 // pixels 24 | val outputWidth = 171 // pixels 25 | val depth = 3 // Number of input channels, or depth - 3 because we're in RGB setting 26 | 27 | // training settings 28 | val numClasses = 2 // The number of possible outcomes 29 | val batchSize = 50 // Test batch size 30 | val nEpochs = 4 // Number of training epochs 31 | val trainSetPercentage = 80.01 32 | 33 | // data settings 34 | val seed = 456 // the random seed 35 | val rng = new Random(seed) 36 | val rootDir: File = FILE_POINTING_TO_THE_IMAGES_ROOT_DIRECTORY 37 | val allowedExtensions: Array[String] = BaseImageLoader.ALLOWED_FORMATS 38 | val labelMaker = new ParentPathLabelGenerator() 39 | val fileSplit = new FileSplit(rootDir, allowedExtensions, rng) 40 | 41 | // evaluation settings 42 | val scoreIterationListenerPrintIterations = 50 43 | val evalutativeListenerFrequency = 2 44 | val saveModel = true 45 | 46 | // creates the actual neural network 47 | def getNetworkConfiguration: MultiLayerConfiguration = { 48 | new NeuralNetConfiguration.Builder() 49 | .seed(seed) 50 | .l2(0.0005) 51 | .weightInit(WeightInit.XAVIER) 52 | .updater(new Adam(1e-3)) 53 | .list 54 | .layer(0, new ConvolutionLayer.Builder(5, 5) 55 | .nIn(depth) // nIn and nOut specify depth. nIn here is the nChannels and nOut is the number of filters to be applied 56 | .stride(1, 1) 57 | .nOut(20) 58 | .activation(Activation.IDENTITY) 59 | .build) 60 | .layer(1, new SubsamplingLayer.Builder(PoolingType.MAX) 61 | .kernelSize(2, 2) 62 | .stride(2, 2) 63 | .build) 64 | .layer(2, new ConvolutionLayer.Builder(5, 5) 65 | .stride(1, 1) // Note that nIn need not be specified in later layers 66 | .nOut(50) 67 | .activation(Activation.IDENTITY) 68 | .build) 69 | .layer(3, new SubsamplingLayer.Builder(PoolingType.MAX) 70 | .kernelSize(2, 2) 71 | .stride(2, 2) 72 | .build) 73 | .layer(4, new DenseLayer.Builder() 74 | .activation(Activation.RELU) 75 | .nOut(500) 76 | .build) 77 | .layer(5, new OutputLayer.Builder(LossFunctions.LossFunction.MCXENT) // to be used with Softmax 78 | .nOut(numClasses) 79 | .activation(Activation.SOFTMAX) // because I have two classes: plane or no-plane 80 | .build) 81 | .setInputType(InputType.convolutional(outputHeight, outputWidth, depth)) 82 | .build 83 | } 84 | 85 | // creates an image scanner with the accurate settings 86 | def getInitializedIterator(inputSplit: InputSplit): RecordReaderDataSetIterator = { 87 | val ir = new ImageRecordReader(outputHeight, outputWidth, depth, labelMaker) 88 | ir.initialize(inputSplit) 89 | new RecordReaderDataSetIterator(ir, batchSize, 1, numClasses) // always 1 for image record reader 90 | } 91 | 92 | // performs a quick prediction test over a few labels 93 | def predictLabels(model: MultiLayerNetwork, iterator: RecordReaderDataSetIterator): Unit = { 94 | var counter = 0 95 | val allClassLabels = iterator.getLabels 96 | while (iterator.hasNext && counter < 20) { 97 | val testDataSet = iterator.next 98 | val labels = testDataSet.getLabels 99 | val knownLabel = labels.argMax(1).getInt(0) // maybe 0 is always the place of the known class? 100 | val predictedLabel = model.predict(testDataSet.getFeatures)(0) // features have been automatically extracted 101 | val expectedResult = allClassLabels.get(knownLabel) 102 | val modelPrediction = allClassLabels.get(predictedLabel) 103 | info("For a single example that is labeled [" + expectedResult + "] the model predicted [" + modelPrediction + "]") 104 | counter = counter + 1 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # From Zero To Deep Learning With Scala 2 | 3 | Building blocks to train your own binary classifier using Scala. 4 | 5 | [`ModelSettings.scala`](ModelSettings.scala) contains the actual construction of the neural network (using [Deeplearning4j](https://deeplearning4j.org)), the necessary hyperparameters and a few other settings. 6 | 7 | [`ModelEvaluator.scala`](ModelEvaluator.scala) contains logic to load images, train and evaluate the model. 8 | 9 | The dataset to run your own experiments is avaiable [here](https://drive.google.com/drive/folders/1-O17UhjdJjtKJ1iAMDBJhaYMiTmhIIAi?usp=sharing). 10 | 11 | I gave a few talks about this project. Please have a look at the [slides](https://www.slideshare.net/FabioTiriticco/from-zero-to-deep-learning-with-scala-232229345) or to a [talk recording](https://www.youtube.com/watch?v=HQJgE1p2SG0). 12 | 13 | ![A plane on a bridge](plane.png) 14 | 15 | ## License 16 | 17 | Copyright 2020 Fabio Tiriticco - Fabway 18 | 19 | Licensed under the Apache License, Version 2.0 (the "License"); 20 | you may not use this file except in compliance with the License. 21 | You may obtain a copy of the License at 22 | 23 | http://www.apache.org/licenses/LICENSE-2.0 24 | 25 | Unless required by applicable law or agreed to in writing, software 26 | distributed under the License is distributed on an "AS IS" BASIS, 27 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | See the License for the specific language governing permissions and 29 | limitations under the License. 30 | -------------------------------------------------------------------------------- /plane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticofab/deep-learning-with-scala/5072c0e11a21e83cb96a3707455462eb0abb3bbe/plane.png --------------------------------------------------------------------------------