Auto Added by WPeMatico

KotlinDL 0.4 Is Out With Pose Detection API, EfficientDet for Object Detection, and EfficientNet for Image Recognition

Version 0.4 of our deep learning library, KotlinDL, is out!

KotlinDL 0.4 is now available on Maven Central with a variety of new features – check out all of the changes that are coming to the new release! We’re currently introducing new models in ModelHub (including the EfficientNet and EfficientDet model families), the experimental high-level Kotlin API for Pose Detection, new layers and preprocessors contributed by the community members, and many other changes.

KotlinDL on GitHub

In this post, we’ll walk you through the changes to the Kotlin Deep Learning library in the 0.4 release:

  1. Pose Detection
  2. NoTop models in the ModelHub
  3. New models: EfficientDet and EfficientNet
  4. Multiple callbacks
  5. Breaking changes in the Image Preprocessing DSL
  6. 4 new layers and 2 new activation functions
  7. Learn more and share your feedback


Pose Detection

Pose detection is using an ML model to detect the pose of a person from an image or a video by detecting the spatial locations of key body joints (keypoints).

We’re excited to launch the MoveNet family of pose detection modes with our new pose detection API in KotlinDL. MoveNet is a fast and accurate model that detects 17 keypoints on the body. The model is offered on ONNXModelHub with two variants, MoveNetSinglePoseLighting and MoveNetSinglePoseThunder. MoveNetSinglePoseLighting is intended for latency-critical applications, while MoveNetSinglePoseThunder is intended for applications that require high accuracy.

If you need to detect a few poses on a given image or video frame, try MoveNetMultiPoseLighting. This model is able to detect multiple people in the image frame at the same time, while still achieving real-time speed.

There are two ways to detect poses within the KotlinDL: parsing the model output manually or using our LightAPI for Pose Detection (the recommended way).

Just load the model:

val modelHub = ONNXModelHub(cacheDirectory = File("cache/pretrainedModels"))
val model = ONNXModels.PoseDetection.MoveNetSinglePoseLighting.pretrainedModel(modelHub)

Run the predictions and print out the pose landmarks and edges connecting the detected pose landmarks:

model.use { poseDetectionModel ->
       val imageFile = …
       val detectedPose = poseDetectionModel.detectPose(imageFile = imageFile)

       detectedPose.poseLandmarks.forEach {
           println("Found ${it.poseLandmarkLabel} with probability ${it.probability}")
       }

       detectedPose.edges.forEach {
           println("The ${it.poseEdgeLabel} starts at ${it.start.poseLandmarkLabel} and ends with ${it.end.poseLandmarkLabel}")
       }
}

Some visualization examples, where we drew landmarks and edges on the given images, are below.

The complete example can be found here.

If you want to run the MoveNet model to detect multiple poses on the given image, you need to make some minor changes to your code.

First, load the model:

val modelHub = ONNXModelHub(cacheDirectory = File("cache/pretrainedModels"))
val model = ONNXModels.PoseDetection.MoveNetSinglePoseLighting.pretrainedModel(modelHub)

Secondly, run the model and get the MultiPoseDetectionResult object, which contains the list of pairs <DetectedObject, DetectedPose>. As a result, we have access not only to the landmarks’ coordinates and labels, but also to the coordinates of the bounding box for the whole person.

model.use { poseDetectionModel ->
       val imageFile = …
       val detectedPoses = poseDetectionModel.detectPoses(imageFile = imageFile, confidence = 0.0f)

       detectedPoses.multiplePoses.forEach { detectedPose ->
           println("Found ${detectedPose.first.classLabel} with probability ${detectedPose.first.probability}")
           detectedPose.second.poseLandmarks.forEach {
               println("Found ${it.poseLandmarkLabel} with probability ${it.probability}")
           }

           detectedPose.second.edges.forEach {
               println("The ${it.poseEdgeLabel} starts at ${it.start.poseLandmarkLabel} and ends with ${it.end.poseLandmarkLabel}")
           }
       }
}

Some visualization examples, where we drew the bounding boxes, landmarks, and edges on the images are below.

The complete example can be found here.


NoTop models in the ModelHub

Running predictions on ready-made models is good, but what about fine-tuning them for your tasks?

The classic approach to Transfer Learning is to freeze all layers except the last few and then train the top few layers (the fully connected layers at the top of the network) on a new piece of data, often changing the number of model outputs.

Before the 0.4 release, KotlinDL users needed to remove the last layers manually, but with the 0.4 release, TensorFlowModelHub provides an option to download “noTop” models  – equivalent to earlier available models, but without weights and configurations for the last few layers.

The following “noTop” models are now available:

  • VGG’16
  • VGG’19
  • ResNet50
  • ResNet101
  • ResNet152
  • ResNet50V2
  • ResNet101V2
  • ResNet152V2
  • MobileNet
  • MobileNetV2
  • NasNetMobile
  • NasNetLarge
  • DenseNet121
  • DenseNet169
  • DenseNet201
  • Xception
  • Inception

In the example below, we load the ResNet50 model from our TensorFlowModelHub and fine-tune it to classify cats and dogs (using the embedded Dogs-vs-Cats dataset):

val modelHub = TFModelHub(cacheDirectory = File("cache/pretrainedModels"))

val modelType = TFModels.CV.ResNet50(noTop = true, inputShape = intArrayOf(IMAGE_SIZE, IMAGE_SIZE, NUM_CHANNELS))

val noTopModel = modelHub.loadModel(modelType)

The topModel is the simplest neural network and can be trained quickly, as it has few parameters.


val topModel = Sequential.of(
   GlobalAvgPool2D(
       name = "top_avg_pool",
   ),
   Dense(
       name = "top_dense",
       kernelInitializer = GlorotUniform(),
       biasInitializer = GlorotUniform(),
       outputSize = 200,
       activation = Activations.Relu
   ),
   Dense(
       name = "pred",
       kernelInitializer = GlorotUniform(),
       biasInitializer = GlorotUniform(),
       outputSize = NUM_CLASSES,
       activation = Activations.Linear
   ),
   noInput = true
)

The new helper function could join two models together: noTop and topModel: val model = Functional.of(pretrainedModel = noTopModel, topModel = topModel)

After that, load weights for the frozen layers from the noTop model, and the weights for the unfrozen layers from the topModel will be initialized during the fit method call.

model.use {
   it.compile(
       optimizer = Adam(),
       loss = Losses.SOFT_MAX_CROSS_ENTROPY_WITH_LOGITS,
       metric = Metrics.ACCURACY
   )

   it.loadWeightsForFrozenLayers(hdfFile)

   it.fit(
       dataset = train,
       batchSize = TRAINING_BATCH_SIZE,
       epochs = EPOCHS
   )

   val accuracy = it.evaluate(dataset = test, batchSize = TEST_BATCH_SIZE).metrics[Metrics.ACCURACY]

   println("Accuracy: $accuracy")
}

The complete example can be found here.


New models: EfficientDet and EfficientNet

Until v0.4, our ModelHub contained only one model (SSD) suitable for solving the Object Detection problem. Starting with this release, we’re gradually expanding the library’s capabilities for solving the Object Detection problem. We’d like to introduce to you a new family of object detectors, called EfficientDet, which consistently achieve much better efficiency than prior object detectors across a wide spectrum of resource constraints.

All models from this family have the same internal architecture which scales for different inputs (image resolution). The final user has a choice of models: from the smallest EfficientDet-D0, model with 3.9 million parameters and 10.2 ms latency on the V100 up to the EfficientDet-D7, with 52 million parameters and 122 ms latency on the V100.

Internally, EfficientDet models use another famous model, EfficientNet, as a backbone. It extracts features from input images and passes them to the next component of the Object Detection model).

EfficientDet Architecture

An example of EfficientDet-D2 usage can be found here.

The EfficientNet model family is also available in the ONNXModelHub. There are 8 different types of models and each model is presented in two variants: full and “noTop” for fine-tuning.

These models achieve better accuracy on the ImageNet dataset with 10x fewer parameters than ResNet or NasNet. If you need fast and accurate image recognition, EfficientNet is a good choice.

An example of EfficientNet0 usage can be found here.


Multiple callbacks

Earlier, Callback support for KotlinDL was pretty simple and not fully compatible with Keras. As a result, users faced difficulties in implementing their neural networks, building the custom validation process, and monitoring the neural network’s training.

The callback object was passed during compilation and was unique for each stage in the model’s lifecycle. However, model compilation can be located in very different places in the code than fit/predict/evaluate, meaning that users may need to create different callbacks for different purposes.

Let’s assume that we need to define EarlyStopping and  TerminateOnNaN for training to handle exceptional cases, and also add two custom callbacks for the prediction and evaluation phases:

val earlyStopping = EarlyStopping(
   monitor = EpochTrainingEvent::valLossValue,
   minDelta = 0.0,
   patience = 2,
   verbose = true,
   mode = EarlyStoppingMode.AUTO,
   baseline = 0.1,
   restoreBestWeights = false
)
val terminateOnNaN = TerminateOnNaN()


class EvaluateCallback : Callback() {
   override fun onTestBatchEnd(batch: Int, batchSize: Int, event: BatchEvent?, logs: History) {
       println("Test batch $batch ends with loss ${event!!.lossValue}..")
   }

   override fun onTestEnd(logs: History) {
       println("Train ends with last loss ${logs.lastBatchEvent().lossValue}")
   }
}

class PredictCallback : Callback() {
   override fun onPredictBatchBegin(batch: Int, batchSize: Int) {
       println("Prediction batch $batch begins.")
   }

   override fun onPredictBatchEnd(batch: Int, batchSize: Int) {
       println("Prediction batch $batch ends.")
   }
}

Let’s pass these callbacks to the model methods:

model.use {
   it.compile(
       optimizer = Adam(clipGradient = ClipGradientByValue(0.1f)),
       loss = Losses.SOFT_MAX_CROSS_ENTROPY_WITH_LOGITS,
       metric = Metrics.ACCURACY
   )

   it.logSummary()

   it.fit(
       dataset = train,
       epochs = EPOCHS,
       batchSize = TRAINING_BATCH_SIZE,
       callbacks = listOf(earlyStopping, terminateOnNaN)
   )

   val accuracy = it.evaluate(
       dataset = test,
       batchSize = TEST_BATCH_SIZE,
       callback = EvaluateCallback()
   ).metrics[Metrics.ACCURACY]


   val predictions = it.predict(
       dataset = test,
       batchSize = TEST_BATCH_SIZE,
       callback = PredictCallback()
   )
}

Found below in the logs:

The complete example can be found here.


4 new layers and 2 new activation functions

Many contributors to this release have added layers to Kotlin for performing non-trivial logic. With these added layers, you can start working with autoencoders and load the GAN models:

There are also two new activation functions:

These activation functions are not available in the TensorFlow core package, but we decided to add them after seeing how they’ve been widely used in recent papers.

We’d be delighted to look at your pull requests if you’d like to contribute a layer, activation function, callback, or initializer from a recent paper!


Breaking changes in the Image Preprocessing DSL

There are a few major changes in the Image Preprocessing DSL:

  • CustomPreprocessor was removed.
  • The loading section was moved from image preprocessing to the Dataset API
  • A few new Preprocessors were added:
    • Padding
    • CenterCrop
    • Convert
    • Grayscale
    • Normalizing

Here is an example of some of the new operations:

val preprocessing = preprocess {
 transformImage {
   centerCrop {
     size = 214
   }
   pad {
     top = 10
     bottom = 10
     left = 10
     right = 10
     mode = PaddingMode.Fill(Color.BLACK)
   }
   convert {
     colorMode = ColorMode.BGR
   }
 }
 transformTensor {
   normalize {
     mean = floatArrayOf(103.939f, 116.779f, 123.68f)
     std = floatArrayOf(57.375f, 57.12f, 58.395f)
   }
 }
}

Because of the removal of the loading section, the same preprocessing instance could now be used in several datasets:

val trainDataset = OnHeapDataset.create(File(datasetPath, "train"), labelGenerator, preprocessing)
val valDataset = OnHeapDataset.create(File(datasetPath, "val"), labelGenerator, preprocessing)


Standing on the shoulders of giants

We’d like to express our deep gratitude to Alexey Zinoviev for his great work developing the framework from minimum viable product to the current state, efforts towards creating a community, skillful release management, and competent marketing support.

His passion for democratizing AI and his continuous work to improve the ability of Kotlin and Java developers to use ML/DL models deserves great respect and inspires us to continue our work.

We’d also like to express our gratitude to Veniamin Viflyantsev, who’s invested a lot of time and effort into changing the architecture of the api module. Many of his changes are now part of this release.

Our team has expanded! Julia Beliaeva (author of the new version of Image Preprocessing DSL) and Nikita Ermolenko have joined us on a permanent basisWe wish them good luck and look forward to new releases!


Learn more and share your feedback

We hope you enjoyed this brief overview of the new features in KotlinDL 0.4! For more information, including the up-to-date Readme file, visit the project’s home on GitHub. Be sure to check out the KotlinDL guide, which contains detailed information about the library’s basic and advanced features and covers many of the topics mentioned in this blog post in more detail.

If you’ve previously used KotlinDL, use the changelog to find out what has changed and how to upgrade your projects to the stable release.

We’d be very thankful if you’d report any bugs you find to our issue tracker. We’ll try to fix all of the critical issues in the 0.4.1 release.

You’re also welcome to join the #kotlindl channel in Kotlin Slack (get an invite here). In this channel, you can ask questions, participate in discussions, and get notifications about the new preview releases and models in ModelHub.

Continue ReadingKotlinDL 0.4 Is Out With Pose Detection API, EfficientDet for Object Detection, and EfficientNet for Image Recognition

KotlinDL 0.4 Is Out With Pose Detection API, EfficientDet for Object Detection, and EfficientNet for Image Recognition

Version 0.4 of our deep learning library, KotlinDL, is out!

KotlinDL 0.4 is now available on Maven Central with a variety of new features – check out all of the changes that are coming to the new release! We’re currently introducing new models in ModelHub (including the EfficientNet and EfficientDet model families), the experimental high-level Kotlin API for Pose Detection, new layers and preprocessors contributed by the community members, and many other changes.

KotlinDL on GitHub

In this post, we’ll walk you through the changes to the Kotlin Deep Learning library in the 0.4 release:

  1. Pose Detection
  2. NoTop models in the ModelHub
  3. New models: EfficientDet and EfficientNet
  4. Multiple callbacks
  5. Breaking changes in the Image Preprocessing DSL
  6. 4 new layers and 2 new activation functions
  7. Learn more and share your feedback


Pose Detection

Pose detection is using an ML model to detect the pose of a person from an image or a video by detecting the spatial locations of key body joints (keypoints).

We’re excited to launch the MoveNet family of pose detection modes with our new pose detection API in KotlinDL. MoveNet is a fast and accurate model that detects 17 keypoints on the body. The model is offered on ONNXModelHub with two variants, MoveNetSinglePoseLighting and MoveNetSinglePoseThunder. MoveNetSinglePoseLighting is intended for latency-critical applications, while MoveNetSinglePoseThunder is intended for applications that require high accuracy.

If you need to detect a few poses on a given image or video frame, try MoveNetMultiPoseLighting. This model is able to detect multiple people in the image frame at the same time, while still achieving real-time speed.

There are two ways to detect poses within the KotlinDL: parsing the model output manually or using our LightAPI for Pose Detection (the recommended way).

Just load the model:

val modelHub = ONNXModelHub(cacheDirectory = File("cache/pretrainedModels"))
val model = ONNXModels.PoseDetection.MoveNetSinglePoseLighting.pretrainedModel(modelHub)

Run the predictions and print out the pose landmarks and edges connecting the detected pose landmarks:

model.use { poseDetectionModel ->
       val imageFile = …
       val detectedPose = poseDetectionModel.detectPose(imageFile = imageFile)

       detectedPose.poseLandmarks.forEach {
           println("Found ${it.poseLandmarkLabel} with probability ${it.probability}")
       }

       detectedPose.edges.forEach {
           println("The ${it.poseEdgeLabel} starts at ${it.start.poseLandmarkLabel} and ends with ${it.end.poseLandmarkLabel}")
       }
}

Some visualization examples, where we drew landmarks and edges on the given images, are below.

The complete example can be found here.

If you want to run the MoveNet model to detect multiple poses on the given image, you need to make some minor changes to your code.

First, load the model:

val modelHub = ONNXModelHub(cacheDirectory = File("cache/pretrainedModels"))
val model = ONNXModels.PoseDetection.MoveNetSinglePoseLighting.pretrainedModel(modelHub)

Secondly, run the model and get the MultiPoseDetectionResult object, which contains the list of pairs <DetectedObject, DetectedPose>. As a result, we have access not only to the landmarks’ coordinates and labels, but also to the coordinates of the bounding box for the whole person.

model.use { poseDetectionModel ->
       val imageFile = …
       val detectedPoses = poseDetectionModel.detectPoses(imageFile = imageFile, confidence = 0.0f)

       detectedPoses.multiplePoses.forEach { detectedPose ->
           println("Found ${detectedPose.first.classLabel} with probability ${detectedPose.first.probability}")
           detectedPose.second.poseLandmarks.forEach {
               println("Found ${it.poseLandmarkLabel} with probability ${it.probability}")
           }

           detectedPose.second.edges.forEach {
               println("The ${it.poseEdgeLabel} starts at ${it.start.poseLandmarkLabel} and ends with ${it.end.poseLandmarkLabel}")
           }
       }
}

Some visualization examples, where we drew the bounding boxes, landmarks, and edges on the images are below.

The complete example can be found here.


NoTop models in the ModelHub

Running predictions on ready-made models is good, but what about fine-tuning them for your tasks?

The classic approach to Transfer Learning is to freeze all layers except the last few and then train the top few layers (the fully connected layers at the top of the network) on a new piece of data, often changing the number of model outputs.

Before the 0.4 release, KotlinDL users needed to remove the last layers manually, but with the 0.4 release, TensorFlowModelHub provides an option to download “noTop” models  – equivalent to earlier available models, but without weights and configurations for the last few layers.

The following “noTop” models are now available:

  • VGG’16
  • VGG’19
  • ResNet50
  • ResNet101
  • ResNet152
  • ResNet50V2
  • ResNet101V2
  • ResNet152V2
  • MobileNet
  • MobileNetV2
  • NasNetMobile
  • NasNetLarge
  • DenseNet121
  • DenseNet169
  • DenseNet201
  • Xception
  • Inception

In the example below, we load the ResNet50 model from our TensorFlowModelHub and fine-tune it to classify cats and dogs (using the embedded Dogs-vs-Cats dataset):

val modelHub = TFModelHub(cacheDirectory = File("cache/pretrainedModels"))

val modelType = TFModels.CV.ResNet50(noTop = true, inputShape = intArrayOf(IMAGE_SIZE, IMAGE_SIZE, NUM_CHANNELS))

val noTopModel = modelHub.loadModel(modelType)

The topModel is the simplest neural network and can be trained quickly, as it has few parameters.


val topModel = Sequential.of(
   GlobalAvgPool2D(
       name = "top_avg_pool",
   ),
   Dense(
       name = "top_dense",
       kernelInitializer = GlorotUniform(),
       biasInitializer = GlorotUniform(),
       outputSize = 200,
       activation = Activations.Relu
   ),
   Dense(
       name = "pred",
       kernelInitializer = GlorotUniform(),
       biasInitializer = GlorotUniform(),
       outputSize = NUM_CLASSES,
       activation = Activations.Linear
   ),
   noInput = true
)

The new helper function could join two models together: noTop and topModel: val model = Functional.of(pretrainedModel = noTopModel, topModel = topModel)

After that, load weights for the frozen layers from the noTop model, and the weights for the unfrozen layers from the topModel will be initialized during the fit method call.

model.use {
   it.compile(
       optimizer = Adam(),
       loss = Losses.SOFT_MAX_CROSS_ENTROPY_WITH_LOGITS,
       metric = Metrics.ACCURACY
   )

   it.loadWeightsForFrozenLayers(hdfFile)

   it.fit(
       dataset = train,
       batchSize = TRAINING_BATCH_SIZE,
       epochs = EPOCHS
   )

   val accuracy = it.evaluate(dataset = test, batchSize = TEST_BATCH_SIZE).metrics[Metrics.ACCURACY]

   println("Accuracy: $accuracy")
}

The complete example can be found here.


New models: EfficientDet and EfficientNet

Until v0.4, our ModelHub contained only one model (SSD) suitable for solving the Object Detection problem. Starting with this release, we’re gradually expanding the library’s capabilities for solving the Object Detection problem. We’d like to introduce to you a new family of object detectors, called EfficientDet, which consistently achieve much better efficiency than prior object detectors across a wide spectrum of resource constraints.

All models from this family have the same internal architecture which scales for different inputs (image resolution). The final user has a choice of models: from the smallest EfficientDet-D0, model with 3.9 million parameters and 10.2 ms latency on the V100 up to the EfficientDet-D7, with 52 million parameters and 122 ms latency on the V100.

Internally, EfficientDet models use another famous model, EfficientNet, as a backbone. It extracts features from input images and passes them to the next component of the Object Detection model).

EfficientDet Architecture

An example of EfficientDet-D2 usage can be found here.

The EfficientNet model family is also available in the ONNXModelHub. There are 8 different types of models and each model is presented in two variants: full and “noTop” for fine-tuning.

These models achieve better accuracy on the ImageNet dataset with 10x fewer parameters than ResNet or NasNet. If you need fast and accurate image recognition, EfficientNet is a good choice.

An example of EfficientNet0 usage can be found here.


Multiple callbacks

Earlier, Callback support for KotlinDL was pretty simple and not fully compatible with Keras. As a result, users faced difficulties in implementing their neural networks, building the custom validation process, and monitoring the neural network’s training.

The callback object was passed during compilation and was unique for each stage in the model’s lifecycle. However, model compilation can be located in very different places in the code than fit/predict/evaluate, meaning that users may need to create different callbacks for different purposes.

Let’s assume that we need to define EarlyStopping and  TerminateOnNaN for training to handle exceptional cases, and also add two custom callbacks for the prediction and evaluation phases:

val earlyStopping = EarlyStopping(
   monitor = EpochTrainingEvent::valLossValue,
   minDelta = 0.0,
   patience = 2,
   verbose = true,
   mode = EarlyStoppingMode.AUTO,
   baseline = 0.1,
   restoreBestWeights = false
)
val terminateOnNaN = TerminateOnNaN()


class EvaluateCallback : Callback() {
   override fun onTestBatchEnd(batch: Int, batchSize: Int, event: BatchEvent?, logs: History) {
       println("Test batch $batch ends with loss ${event!!.lossValue}..")
   }

   override fun onTestEnd(logs: History) {
       println("Train ends with last loss ${logs.lastBatchEvent().lossValue}")
   }
}

class PredictCallback : Callback() {
   override fun onPredictBatchBegin(batch: Int, batchSize: Int) {
       println("Prediction batch $batch begins.")
   }

   override fun onPredictBatchEnd(batch: Int, batchSize: Int) {
       println("Prediction batch $batch ends.")
   }
}

Let’s pass these callbacks to the model methods:

model.use {
   it.compile(
       optimizer = Adam(clipGradient = ClipGradientByValue(0.1f)),
       loss = Losses.SOFT_MAX_CROSS_ENTROPY_WITH_LOGITS,
       metric = Metrics.ACCURACY
   )

   it.logSummary()

   it.fit(
       dataset = train,
       epochs = EPOCHS,
       batchSize = TRAINING_BATCH_SIZE,
       callbacks = listOf(earlyStopping, terminateOnNaN)
   )

   val accuracy = it.evaluate(
       dataset = test,
       batchSize = TEST_BATCH_SIZE,
       callback = EvaluateCallback()
   ).metrics[Metrics.ACCURACY]


   val predictions = it.predict(
       dataset = test,
       batchSize = TEST_BATCH_SIZE,
       callback = PredictCallback()
   )
}

Found below in the logs:

The complete example can be found here.


4 new layers and 2 new activation functions

Many contributors to this release have added layers to Kotlin for performing non-trivial logic. With these added layers, you can start working with autoencoders and load the GAN models:

There are also two new activation functions:

These activation functions are not available in the TensorFlow core package, but we decided to add them after seeing how they’ve been widely used in recent papers.

We’d be delighted to look at your pull requests if you’d like to contribute a layer, activation function, callback, or initializer from a recent paper!


Breaking changes in the Image Preprocessing DSL

There are a few major changes in the Image Preprocessing DSL:

  • CustomPreprocessor was removed.
  • The loading section was moved from image preprocessing to the Dataset API
  • A few new Preprocessors were added:
    • Padding
    • CenterCrop
    • Convert
    • Grayscale
    • Normalizing

Here is an example of some of the new operations:

val preprocessing = preprocess {
 transformImage {
   centerCrop {
     size = 214
   }
   pad {
     top = 10
     bottom = 10
     left = 10
     right = 10
     mode = PaddingMode.Fill(Color.BLACK)
   }
   convert {
     colorMode = ColorMode.BGR
   }
 }
 transformTensor {
   normalize {
     mean = floatArrayOf(103.939f, 116.779f, 123.68f)
     std = floatArrayOf(57.375f, 57.12f, 58.395f)
   }
 }
}

Because of the removal of the loading section, the same preprocessing instance could now be used in several datasets:

val trainDataset = OnHeapDataset.create(File(datasetPath, "train"), labelGenerator, preprocessing)
val valDataset = OnHeapDataset.create(File(datasetPath, "val"), labelGenerator, preprocessing)


Standing on the shoulders of giants

We’d like to express our deep gratitude to Alexey Zinoviev for his great work developing the framework from minimum viable product to the current state, efforts towards creating a community, skillful release management, and competent marketing support.

His passion for democratizing AI and his continuous work to improve the ability of Kotlin and Java developers to use ML/DL models deserves great respect and inspires us to continue our work.

We’d also like to express our gratitude to Veniamin Viflyantsev, who’s invested a lot of time and effort into changing the architecture of the api module. Many of his changes are now part of this release.

Our team has expanded! Julia Beliaeva (author of the new version of Image Preprocessing DSL) and Nikita Ermolenko have joined us on a permanent basisWe wish them good luck and look forward to new releases!


Learn more and share your feedback

We hope you enjoyed this brief overview of the new features in KotlinDL 0.4! For more information, including the up-to-date Readme file, visit the project’s home on GitHub. Be sure to check out the KotlinDL guide, which contains detailed information about the library’s basic and advanced features and covers many of the topics mentioned in this blog post in more detail.

If you’ve previously used KotlinDL, use the changelog to find out what has changed and how to upgrade your projects to the stable release.

We’d be very thankful if you’d report any bugs you find to our issue tracker. We’ll try to fix all of the critical issues in the 0.4.1 release.

You’re also welcome to join the #kotlindl channel in Kotlin Slack (get an invite here). In this channel, you can ask questions, participate in discussions, and get notifications about the new preview releases and models in ModelHub.

Continue ReadingKotlinDL 0.4 Is Out With Pose Detection API, EfficientDet for Object Detection, and EfficientNet for Image Recognition

Object Detection with KotlinDL and Ktor

I presented the webinar “Object Detection and Image Recognition with Kotlin,” where I explored a deep learning library written in Kotlin, described how to detect objects of different types in images, and explained how to create a Kotlin Web Application using Ktor and KotlinDL that recognizes cars and persons on photos. I have decided there is more that I would like to share with you on the subject, and so here is an extended article.

If you are new to Deep Learning, don’t worry about it. You don’t need any high-level calculus knowledge to start using the Object Detection Light API in the KotlinDL library.

However, when writing this article, I did assume you would be familiar with basic Kotlin web-development fundamentals, e.g., HTML, web-server, HTTP, and client-server communications.

This article will take you through how to detect objects in different images and create a Kotlin Web Application using Ktor and KotlinDL.

What is Object Detection?

It’s a pretty simple term from the Deep Learning world and just means the task of detecting instances of objects of a certain class within an image.

You are probably already familiar with Image Recognition, where the idea is to recognize the class or type of only one object within an image without having any coordinates for the recognized object.

Unlike the Image Recognition, during Object Detection, we are trying to detect a few objects (sometimes it could be a significant number, 100 or even 1,000, for example) and their locations, which are usually presented as four coordinates of a rectangle (x_min, x_max, y_min, y_max) containing the detected object.

For example, this screenshot of the example application shows how a few objects have been recognized, and their positions annotated:

OK – now for the fun stuff! It’s time to write some Kotlin code to detect objects within an image.

Object Detection Example

Let’s say we have the following image. We see a typical street: several cars, pedestrians crossing, traffic lights, and even someone using the pedestrian crossing on a bicycle.

With a few rows of code, we can obtain a list of the detected objects, sorted by score or probability (the degree of confidence of the model that a certain rectangle contains an object of a certain type).

val modelHub = ONNXModelHub(cacheDirectory = File("cache/pretrainedModels"))

val model = modelHub.loadPretrainedModel(ONNXModels.ObjectDetection.SSD)

model.use { detectionModel ->
   println(detectionModel)

   val imageFile = getFileFromResource("detection/image2.jpg")
   val detectedObjects = detectionModel.detectObjects(imageFile = imageFile, topK = 20)

   detectedObjects.forEach {
       println("Found ${it.classLabel} with probability ${it.probability}")
   }
}

This code prints the following:

Found car with probability 0.9872914
Found bicycle with probability 0.9547764
Found car with probability 0.93248314
Found person with probability 0.85994
Found person with probability 0.8397419
Found car with probability 0.7488473
Found person with probability 0.49446288
Found person with probability 0.48537987
Found person with probability 0.40268868
Found person with probability 0.3972058
Found person with probability 0.38047826
Found traffic light with probability 0.36501375
Found car with probability 0.30308443
Found traffic light with probability 0.30084336
Found person with probability 0.27078137
Found car with probability 0.26892117
Found person with probability 0.26232794
Found person with probability 0.23597576
Found person with probability 0.23156123
Found person with probability 0.21393918

OK, it looks like the model can detect objects, just like our eyes can do, but how do we go about marking the objects?

We can use the Swing framework to draw rectangles over the image. This also requires simple image preprocessing before visualization.

First, we need to add a simple visualization using JPanel, BufferedImage, and Graphics2D objects in the visualise function.

model.use { detectionModel ->
  …

   visualise(imageFile, detectedObjects)
}

Drawing rectangles on an image with the Graphics2D API may not be the best approach, but we can use it as a good starting point for our research.

private fun visualise(
   imageFile: File,
   detectedObjects: List<DetectedObject>
) {
   val frame = JFrame("Detected Objects")
   @Suppress("UNCHECKED_CAST")
   frame.contentPane.add(JPanel(imageFile, detectedObjects))
   frame.pack()
   frame.setLocationRelativeTo(null)
   frame.isVisible = true
   frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE
   frame.isResizable = false
}

class JPanel(
   val image: File,
   private val detectedObjects: List<DetectedObject>
) : JPanel() {
   private var bufferedImage = ImageIO.read(image)

   override fun paint(graphics: Graphics) {
       super.paint(graphics)
       graphics.drawImage(bufferedImage, 0, 0, null)

       detectedObjects.forEach {
           val top = it.yMin * bufferedImage.height
           val left = it.xMin * bufferedImage.width
           val bottom = it.yMax * bufferedImage.height
           val right = it.xMax * bufferedImage.width
           if (abs(top - bottom) > 300 || abs(right - left) > 300) return@forEach

           graphics.color = Color.ORANGE
           graphics.font = Font("Courier New", 1, 17)
           graphics.drawString(" ${it.classLabel} : ${it.probability}", left.toInt(), bottom.toInt() - 8)

           graphics as Graphics2D
           val stroke1: Stroke = BasicStroke(6f)
           graphics.setColor(Color.RED)
           graphics.stroke = stroke1
           graphics.drawRect(left.toInt(), bottom.toInt(), (right - left).toInt(), (top - bottom).toInt())
       }
   }

   override fun getPreferredSize(): Dimension {
       return Dimension(bufferedImage.width, bufferedImage.height)
   }

   override fun getMinimumSize(): Dimension {
       return Dimension(bufferedImage.width, bufferedImage.height)
   }
}

The result is the following image:

As you can see, the Object Detection Light API returns not only the class label and score but the relative image coordinates, which can be used for drawing rectangles or boxes around the detected objects.

Also, we could play a little bit with the paint palette and use different colors to differentiate people, bicycles, cars, and traffic lights.

when(it.classLabel) {
   "person" -> graphics.setColor(Color.WHITE)
   "car" -> graphics.setColor(Color.GREEN)
   "traffic light" -> graphics.setColor(Color.YELLOW)
   "bicycle" -> graphics.setColor(Color.MAGENTA)
   else -> graphics.setColor(Color.RED)
}

That looks significantly better!

You can continue experimenting with the visualization, but we need to move on!

Client-Server Application with Ktor

In this section, I will use Ktor to write two simple programs: client and server. The client application will send the image to the server application. If you have never used Ktor before, it’s an excellent time to see how easy it is to deal with classic web stuff like HTTP requests, headers, MIME types, and so on.

When the code below is run, the client application sends a POST request via the submitFormWithBinaryData method. You can read more about how this works in Ktor documentation. The result with the added boxes for the detected objects can be found in the clientFiles folder.

runBlocking {
   val client = HttpClient(CIO)

   val response: HttpResponse = client.submitFormWithBinaryData(
       url = "http://localhost:8001/detect",
       formData = formData {
           append("image", getFileFromResource("detection/image2.jpg").readBytes(), Headers.build {
               append(HttpHeaders.ContentType, "image/jpg")
               append(HttpHeaders.ContentDisposition, "filename=image2.jpg")
           })
       }
   )

   val imageFile = File("clientFiles/detectedObjects2.jpg")
   imageFile.writeBytes(response.readBytes())
}

Unfortunately, Ktor has no special API for receiving files from the server-side. But we’re programmers, right? Let’s just write the bytes obtained over the network to the File object.

The server part is a little more difficult. I’ll need to explain some parts of the code below.

val modelHub = ONNXModelHub(cacheDirectory = File("cache/pretrainedModels"))
val model = ONNXModels.ObjectDetection.SSD.pretrainedModel(modelHub)

Because model creation is a time-consuming step (due to loading and initializing), we need to create the model before we can run the server.

embeddedServer(Netty, 8001) {
   routing {
       post("/detect") {
           val multipartData = call.receiveMultipart()
           var newFileName = ""
           multipartData.forEachPart { part ->
               when (part) {
                   is PartData.FileItem -> {
                       val fileName = part.originalFileName as String
                       newFileName = fileName.replace("image", "detectedObjects")
                       val fileBytes = part.streamProvider().readBytes()
                       val imageFile = File("serverFiles/$fileName")
                       imageFile.writeBytes(fileBytes)

                       val detectedObjects =
                           model.detectObjects(imageFile = imageFile, topK = 20)

                       val filteredObjects =
                           detectedObjects.filter { it.classLabel == "car" || it.classLabel == "person" || it.classLabel == "bicycle" }

                       drawRectanglesForDetectedObjects(newFileName, imageFile, filteredObjects)

The intermediate result will be saved to the serverFiles folder. After that, the server application will send this file back to the client.

To send form data in a test POST/PUT request, you must set the Content-Type header and specify the request body. To do this, you can use the addHeader and setBody functions, respectively.

     
                  
                       call.response.header(
                           HttpHeaders.ContentDisposition,
                           ContentDisposition.Attachment.withParameter(ContentDisposition.Parameters.FileName, newFileName)
                               .toString()
                       )
                       call.respondFile(file)
                   }
               }
           }
       }
   }
}.start(wait = true)

At the end, we need to close our model to release all the resources.

   
model.close()

Run the server, and after that, try to make multiple runs of the client with the different images. Check clientFiles and serverFiles folders to find all the images that were sent with detected objects.

Yet another example of Object Detection

The complete example, including drawing and saving files to the serverFiles folder, can be found here in the GitHub repository.

Web Application

It’s time to write the whole Web Application with an HTML page rendered on the server, a few inputs, and a button. I’d like to upload an image, fill some input fields with the parameters, and press a button to download the image with the detected objects on my laptop.

The application will contain only the server part, but it has a few interesting aspects we will need to consider. It should handle two HTTP requests: the POST request, which handles multipart data with FileItem and FormItem handlers, and the GET request, which returns a simple HTML page.

From multipartData we can not only extract binary data like in the previous example but the values of the form parameters, too. These parameters, topK, and classLabelNames, will be explained later.

val modelHub = ONNXModelHub(cacheDirectory = File("cache/pretrainedModels"))
val model = ONNXModels.ObjectDetection.SSD.pretrainedModel(modelHub)

embeddedServer(Netty, 8002) {
   routing {
       post("/detect") {
           val multipartData = call.receiveMultipart()
           var imageFile: File? = null
           var newFileName = ""
           var topK = 20
           val classLabels = mutableListOf<String>()
           multipartData.forEachPart { part ->
               when (part) {
                   is PartData.FileItem -> {
                       val fileName = part.originalFileName as String
                       val fileBytes = part.streamProvider().readBytes()

                       newFileName = fileName.replace("image", "detectedObjects")
                       imageFile = File("serverFiles/$fileName")
                       imageFile!!.writeBytes(fileBytes)
                   }
                   is PartData.FormItem -> {
                       when (part.name) {
                           "topK" -> topK = if (part.value.isNotBlank()) part.value.toInt() else 20
                           "classLabelNames" -> part.value.split(",").forEach {
                               classLabels += it.trim()
                           }
                       }
                   }
                   is PartData.BinaryItem -> TODO()
               }
           }

           val detectedObjects =
               model.detectObjects(imageFile = imageFile!!, topK = topK)

           val filteredObjects = detectedObjects.filter {
                   if (classLabels.isNotEmpty()) {
                       it.classLabel in classLabels
                   } else {
                       it.classLabel == "car" || it.classLabel == "person" || it.classLabel == "bicycle"
                   }
               }

           drawRectanglesForDetectedObjects(newFileName, imageFile!!, filteredObjects)

           call.response.header(
               HttpHeaders.ContentDisposition,
               ContentDisposition.Attachment.withParameter(ContentDisposition.Parameters.FileName, newFileName)
                   .toString()
           )
           call.respondFile(File("serverFiles/$newFileName"))
       }

To describe the HTML page with this nice DSL, Ktor uses kotlinx.html as written in the documentation. This integration allows you to respond to a client with HTML blocks. With HTML DSL, you can write pure HTML in Kotlin, interpolate variables into views, and build complex HTML layouts using templates.

get("/") {
           call.respondHtml {
               body {
                   form(action = "/detect", encType = FormEncType.multipartFormData, method = FormMethod.post) {
                       p {
                           +"Your image: "
                           fileInput(name = "image")
                       }
                       p {
                           +"TopK: "
                           numberInput(name = "topK")
                       }
                       p {
                           +"Classes to detect: "
                           textInput(name = "classLabelNames")
                       }
                       p {
                           submitInput() { value = "Detect objects" }
                       }
                   }
               }
           }
       }
   }
}.start(wait = true)

model.close()

Run the server and open the page http://localhost:8002. Here you’ll find a form. Simply upload the image, fill inputs with the request parameters (or leave them empty), and press the button “Detect objects.” The new image will start downloading in a few seconds.

You also could play with the parameters topK, and classLabelNames to obtain different results. The topK parameter is used to determine how many detected objects (sorted by a score from highest to lowest) will be drawn on the image. The classLabelNames parameter takes as an input a list of labels (from the following list) separated by commas to filter categories of detected objects in the picture that will be enclosed in a rectangle.

The complete example can be found here in the GitHub repository. 

This represents only a small fraction of what you can do with the full power of Ktor. For example, you can also build a REST API for Object Detection, Image Recognition, or build a helpful microservice. It is your choice!

In conclusion

Release 0.3 was shipped with only one effective object detection model: SSD. The new release, 0.4, brings seven new object detection models with different characteristics of velocity and accuracy as well as the ability to detect complex objects.

We strongly recommend using Compose for Desktop, instead of Swing, for your visualization needs. The community is working on moving these examples to the new framework.

This is not the only improvement you can expect in the Object Detection Light API. In future releases, we will add some helpful methods for filtering and unioning different boxes in the YOLO style to avoid having places in the image where a single object has multiple rectangles drawn on it.

If you have any thoughts or user experience related to this use case, just make an issue on GitHub or ask in the Kotlin Slack (kotlindl channel).

Continue ReadingObject Detection with KotlinDL and Ktor

Object Detection with KotlinDL and Ktor

I presented the webinar “Object Detection and Image Recognition with Kotlin,” where I explored a deep learning library written in Kotlin, described how to detect objects of different types in images, and explained how to create a Kotlin Web Application using Ktor and KotlinDL that recognizes cars and persons on photos. I have decided there is more that I would like to share with you on the subject, and so here is an extended article.

If you are new to Deep Learning, don’t worry about it. You don’t need any high-level calculus knowledge to start using the Object Detection Light API in the KotlinDL library.

However, when writing this article, I did assume you would be familiar with basic Kotlin web-development fundamentals, e.g., HTML, web-server, HTTP, and client-server communications.

This article will take you through how to detect objects in different images and create a Kotlin Web Application using Ktor and KotlinDL.

What is Object Detection?

It’s a pretty simple term from the Deep Learning world and just means the task of detecting instances of objects of a certain class within an image.

You are probably already familiar with Image Recognition, where the idea is to recognize the class or type of only one object within an image without having any coordinates for the recognized object.

Unlike the Image Recognition, during Object Detection, we are trying to detect a few objects (sometimes it could be a significant number, 100 or even 1,000, for example) and their locations, which are usually presented as four coordinates of a rectangle (x_min, x_max, y_min, y_max) containing the detected object.

For example, this screenshot of the example application shows how a few objects have been recognized, and their positions annotated:

OK – now for the fun stuff! It’s time to write some Kotlin code to detect objects within an image.

Object Detection Example

Let’s say we have the following image. We see a typical street: several cars, pedestrians crossing, traffic lights, and even someone using the pedestrian crossing on a bicycle.

With a few rows of code, we can obtain a list of the detected objects, sorted by score or probability (the degree of confidence of the model that a certain rectangle contains an object of a certain type).

val modelHub = ONNXModelHub(cacheDirectory = File("cache/pretrainedModels"))

val model = modelHub.loadPretrainedModel(ONNXModels.ObjectDetection.SSD)

model.use { detectionModel ->
   println(detectionModel)

   val imageFile = getFileFromResource("detection/image2.jpg")
   val detectedObjects = detectionModel.detectObjects(imageFile = imageFile, topK = 20)

   detectedObjects.forEach {
       println("Found ${it.classLabel} with probability ${it.probability}")
   }
}

This code prints the following:

Found car with probability 0.9872914
Found bicycle with probability 0.9547764
Found car with probability 0.93248314
Found person with probability 0.85994
Found person with probability 0.8397419
Found car with probability 0.7488473
Found person with probability 0.49446288
Found person with probability 0.48537987
Found person with probability 0.40268868
Found person with probability 0.3972058
Found person with probability 0.38047826
Found traffic light with probability 0.36501375
Found car with probability 0.30308443
Found traffic light with probability 0.30084336
Found person with probability 0.27078137
Found car with probability 0.26892117
Found person with probability 0.26232794
Found person with probability 0.23597576
Found person with probability 0.23156123
Found person with probability 0.21393918

OK, it looks like the model can detect objects, just like our eyes can do, but how do we go about marking the objects?

We can use the Swing framework to draw rectangles over the image. This also requires simple image preprocessing before visualization.

First, we need to add a simple visualization using JPanel, BufferedImage, and Graphics2D objects in the visualise function.

model.use { detectionModel ->
  …

   visualise(imageFile, detectedObjects)
}

Drawing rectangles on an image with the Graphics2D API may not be the best approach, but we can use it as a good starting point for our research.

private fun visualise(
   imageFile: File,
   detectedObjects: List<DetectedObject>
) {
   val frame = JFrame("Detected Objects")
   @Suppress("UNCHECKED_CAST")
   frame.contentPane.add(JPanel(imageFile, detectedObjects))
   frame.pack()
   frame.setLocationRelativeTo(null)
   frame.isVisible = true
   frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE
   frame.isResizable = false
}

class JPanel(
   val image: File,
   private val detectedObjects: List<DetectedObject>
) : JPanel() {
   private var bufferedImage = ImageIO.read(image)

   override fun paint(graphics: Graphics) {
       super.paint(graphics)
       graphics.drawImage(bufferedImage, 0, 0, null)

       detectedObjects.forEach {
           val top = it.yMin * bufferedImage.height
           val left = it.xMin * bufferedImage.width
           val bottom = it.yMax * bufferedImage.height
           val right = it.xMax * bufferedImage.width
           if (abs(top - bottom) > 300 || abs(right - left) > 300) return@forEach

           graphics.color = Color.ORANGE
           graphics.font = Font("Courier New", 1, 17)
           graphics.drawString(" ${it.classLabel} : ${it.probability}", left.toInt(), bottom.toInt() - 8)

           graphics as Graphics2D
           val stroke1: Stroke = BasicStroke(6f)
           graphics.setColor(Color.RED)
           graphics.stroke = stroke1
           graphics.drawRect(left.toInt(), bottom.toInt(), (right - left).toInt(), (top - bottom).toInt())
       }
   }

   override fun getPreferredSize(): Dimension {
       return Dimension(bufferedImage.width, bufferedImage.height)
   }

   override fun getMinimumSize(): Dimension {
       return Dimension(bufferedImage.width, bufferedImage.height)
   }
}

The result is the following image:

As you can see, the Object Detection Light API returns not only the class label and score but the relative image coordinates, which can be used for drawing rectangles or boxes around the detected objects.

Also, we could play a little bit with the paint palette and use different colors to differentiate people, bicycles, cars, and traffic lights.

when(it.classLabel) {
   "person" -> graphics.setColor(Color.WHITE)
   "car" -> graphics.setColor(Color.GREEN)
   "traffic light" -> graphics.setColor(Color.YELLOW)
   "bicycle" -> graphics.setColor(Color.MAGENTA)
   else -> graphics.setColor(Color.RED)
}

That looks significantly better!

You can continue experimenting with the visualization, but we need to move on!

Client-Server Application with Ktor

In this section, I will use Ktor to write two simple programs: client and server. The client application will send the image to the server application. If you have never used Ktor before, it’s an excellent time to see how easy it is to deal with classic web stuff like HTTP requests, headers, MIME types, and so on.

When the code below is run, the client application sends a POST request via the submitFormWithBinaryData method. You can read more about how this works in Ktor documentation. The result with the added boxes for the detected objects can be found in the clientFiles folder.

runBlocking {
   val client = HttpClient(CIO)

   val response: HttpResponse = client.submitFormWithBinaryData(
       url = "http://localhost:8001/detect",
       formData = formData {
           append("image", getFileFromResource("detection/image2.jpg").readBytes(), Headers.build {
               append(HttpHeaders.ContentType, "image/jpg")
               append(HttpHeaders.ContentDisposition, "filename=image2.jpg")
           })
       }
   )

   val imageFile = File("clientFiles/detectedObjects2.jpg")
   imageFile.writeBytes(response.readBytes())
}

Unfortunately, Ktor has no special API for receiving files from the server-side. But we’re programmers, right? Let’s just write the bytes obtained over the network to the File object.

The server part is a little more difficult. I’ll need to explain some parts of the code below.

val modelHub = ONNXModelHub(cacheDirectory = File("cache/pretrainedModels"))
val model = ONNXModels.ObjectDetection.SSD.pretrainedModel(modelHub)

Because model creation is a time-consuming step (due to loading and initializing), we need to create the model before we can run the server.

embeddedServer(Netty, 8001) {
   routing {
       post("/detect") {
           val multipartData = call.receiveMultipart()
           var newFileName = ""
           multipartData.forEachPart { part ->
               when (part) {
                   is PartData.FileItem -> {
                       val fileName = part.originalFileName as String
                       newFileName = fileName.replace("image", "detectedObjects")
                       val fileBytes = part.streamProvider().readBytes()
                       val imageFile = File("serverFiles/$fileName")
                       imageFile.writeBytes(fileBytes)

                       val detectedObjects =
                           model.detectObjects(imageFile = imageFile, topK = 20)

                       val filteredObjects =
                           detectedObjects.filter { it.classLabel == "car" || it.classLabel == "person" || it.classLabel == "bicycle" }

                       drawRectanglesForDetectedObjects(newFileName, imageFile, filteredObjects)

The intermediate result will be saved to the serverFiles folder. After that, the server application will send this file back to the client.

To send form data in a test POST/PUT request, you must set the Content-Type header and specify the request body. To do this, you can use the addHeader and setBody functions, respectively.

     
                  
                       call.response.header(
                           HttpHeaders.ContentDisposition,
                           ContentDisposition.Attachment.withParameter(ContentDisposition.Parameters.FileName, newFileName)
                               .toString()
                       )
                       call.respondFile(file)
                   }
               }
           }
       }
   }
}.start(wait = true)

At the end, we need to close our model to release all the resources.

   
model.close()

Run the server, and after that, try to make multiple runs of the client with the different images. Check clientFiles and serverFiles folders to find all the images that were sent with detected objects.

Yet another example of Object Detection

The complete example, including drawing and saving files to the serverFiles folder, can be found here in the GitHub repository.

Web Application

It’s time to write the whole Web Application with an HTML page rendered on the server, a few inputs, and a button. I’d like to upload an image, fill some input fields with the parameters, and press a button to download the image with the detected objects on my laptop.

The application will contain only the server part, but it has a few interesting aspects we will need to consider. It should handle two HTTP requests: the POST request, which handles multipart data with FileItem and FormItem handlers, and the GET request, which returns a simple HTML page.

From multipartData we can not only extract binary data like in the previous example but the values of the form parameters, too. These parameters, topK, and classLabelNames, will be explained later.

val modelHub = ONNXModelHub(cacheDirectory = File("cache/pretrainedModels"))
val model = ONNXModels.ObjectDetection.SSD.pretrainedModel(modelHub)

embeddedServer(Netty, 8002) {
   routing {
       post("/detect") {
           val multipartData = call.receiveMultipart()
           var imageFile: File? = null
           var newFileName = ""
           var topK = 20
           val classLabels = mutableListOf<String>()
           multipartData.forEachPart { part ->
               when (part) {
                   is PartData.FileItem -> {
                       val fileName = part.originalFileName as String
                       val fileBytes = part.streamProvider().readBytes()

                       newFileName = fileName.replace("image", "detectedObjects")
                       imageFile = File("serverFiles/$fileName")
                       imageFile!!.writeBytes(fileBytes)
                   }
                   is PartData.FormItem -> {
                       when (part.name) {
                           "topK" -> topK = if (part.value.isNotBlank()) part.value.toInt() else 20
                           "classLabelNames" -> part.value.split(",").forEach {
                               classLabels += it.trim()
                           }
                       }
                   }
                   is PartData.BinaryItem -> TODO()
               }
           }

           val detectedObjects =
               model.detectObjects(imageFile = imageFile!!, topK = topK)

           val filteredObjects = detectedObjects.filter {
                   if (classLabels.isNotEmpty()) {
                       it.classLabel in classLabels
                   } else {
                       it.classLabel == "car" || it.classLabel == "person" || it.classLabel == "bicycle"
                   }
               }

           drawRectanglesForDetectedObjects(newFileName, imageFile!!, filteredObjects)

           call.response.header(
               HttpHeaders.ContentDisposition,
               ContentDisposition.Attachment.withParameter(ContentDisposition.Parameters.FileName, newFileName)
                   .toString()
           )
           call.respondFile(File("serverFiles/$newFileName"))
       }

To describe the HTML page with this nice DSL, Ktor uses kotlinx.html as written in the documentation. This integration allows you to respond to a client with HTML blocks. With HTML DSL, you can write pure HTML in Kotlin, interpolate variables into views, and build complex HTML layouts using templates.

get("/") {
           call.respondHtml {
               body {
                   form(action = "/detect", encType = FormEncType.multipartFormData, method = FormMethod.post) {
                       p {
                           +"Your image: "
                           fileInput(name = "image")
                       }
                       p {
                           +"TopK: "
                           numberInput(name = "topK")
                       }
                       p {
                           +"Classes to detect: "
                           textInput(name = "classLabelNames")
                       }
                       p {
                           submitInput() { value = "Detect objects" }
                       }
                   }
               }
           }
       }
   }
}.start(wait = true)

model.close()

Run the server and open the page http://localhost:8002. Here you’ll find a form. Simply upload the image, fill inputs with the request parameters (or leave them empty), and press the button “Detect objects.” The new image will start downloading in a few seconds.

You also could play with the parameters topK, and classLabelNames to obtain different results. The topK parameter is used to determine how many detected objects (sorted by a score from highest to lowest) will be drawn on the image. The classLabelNames parameter takes as an input a list of labels (from the following list) separated by commas to filter categories of detected objects in the picture that will be enclosed in a rectangle.

The complete example can be found here in the GitHub repository. 

This represents only a small fraction of what you can do with the full power of Ktor. For example, you can also build a REST API for Object Detection, Image Recognition, or build a helpful microservice. It is your choice!

In conclusion

Release 0.3 was shipped with only one effective object detection model: SSD. The new release, 0.4, brings seven new object detection models with different characteristics of velocity and accuracy as well as the ability to detect complex objects.

We strongly recommend using Compose for Desktop, instead of Swing, for your visualization needs. The community is working on moving these examples to the new framework.

This is not the only improvement you can expect in the Object Detection Light API. In future releases, we will add some helpful methods for filtering and unioning different boxes in the YOLO style to avoid having places in the image where a single object has multiple rectangles drawn on it.

If you have any thoughts or user experience related to this use case, just make an issue on GitHub or ask in the Kotlin Slack (kotlindl channel).

Continue ReadingObject Detection with KotlinDL and Ktor

Multik 0.1 Is Out

Introducing Multik 0.1 – a new, enhanced version of our multidimensional array library! You can check out the previous post to learn about the basic features and architecture of the library.

In the new release, we added new methods from linear algebra, supported complex numbers and reading/writing .csv files, improved the performance and stability of existing functions, and added many more features that will make it easier for you to work with multidimensional arrays.

Multik on GitHub

Let’s take a look at the new features this release brings to the API:

Reading and writing CSV files

CSV is a popular data recording format, and now Multik 0.1 allows you to read and write .csv files easily.

val a = mk.d2array(2, 2) { it }
mk.write("example.csv", a)
val b = mk.read<Int, D2>("example.csv")
println(b)
/*
[[0, 1],
[2, 3]]
*/

Complex numbers

Complex numbers are expressed as re + i ∙ im, where re and im are real numbers and i is the imaginary unit equal to the square root of -1; re is the real part of the complex number and i ∙ im is the imaginary part. Complex numbers are common in algebra and are now a part of the Multik API.

val cf = ComplexFloat(1f, 2f) // 1.0+(2.0)i
val cd = ComplexDouble.one // 1.0+(0.0)i

The ComplexFloat and ComplexDouble classes are created in full accordance with the Number classes, so there is no need to learn a new API.

val c1 = ComplexFloat(1f, 2f) // 1.0+(2.0)i
val c2 = ComplexFloat(3f, 4f) // 3.0+(4.0)i

println(c1 + c2) // 4.0+(6.0)i
println(c1 * c2) // -5.0+(10.0)i

val (re, im) = c1 // re = 1.0; im = 2.0
val re2 = c2.re // 3.0
val im2 = c2.im // 4.0

We’ve also added ComplexFloatArrays and ComplexDoubleArrays. They support all methods that primitive arrays do, except for methods that use comparison, since complex numbers are unordered. But you can use methods such as minBy, maxBy, sortedBy, and others to define the comparison logic yourself.

val array = ComplexDoubleArray(5) { ComplexDouble(it, it.toDouble() / 2) }

println(array)
// [0.0 + 0.0i, 1.0 + 0.0i, 2.0 + 1.0i, 3.0 + 1.0i, 4.0 + 2.0i]

println(array.filter { it.abs() > 2.0 })
// [2.0+(1.0)i, 3.0+(1.5)i, 4.0+(2.0)i]

println(array.sortedByDescending { it.im })
// [4.0+(2.0)i, 3.0+(1.5)i, 2.0+(1.0)i, 1.0+(0.5)i, 0.0+(0.0)i]

After supporting complex numbers and arrays of complex numbers, we added them to multidimensional arrays.

println(mk.d2arrayIndices(2, 2) { i, j -> ComplexFloat(i, j) })
/*
[[0.0+(0.0)i, 0.0+(1.0)i],
[1.0+(0.0)i, 1.0+(1.0)i]]
 */

println(mk.empty<ComplexDouble, D1>(5))
// [0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i]

println(mk.ndarrayOf(ComplexDouble.zero, ComplexDouble.one))
//[0.0+(0.0)i, 1.0+(0.0)i]

You can do everything with them that you did with integer and float multidimensional arrays before.

val ndarray = mk.d2arrayIndices(3, 3) { i, j -> ComplexDouble(i, j) }
println(ndarray.map { it.re * it.re + it.im * it.im })
/*
[[0.0, 1.0, 4.0],
[1.0, 2.0, 5.0],
[4.0, 5.0, 8.0]]
 */

 println(mk.math.sin(ndarray))
 /*
[[0.0+(0.0)i, 0.0+(1.1752011936438014)i, 0.0+(3.626860407847019)i],
[0.8414709848078965+(0.0)i, 1.2984575814159773+(0.6349639147847361)i, 3.165778513216168+(1.9596010414216063)i],
[0.9092974268256817+(-0.0)i, 1.4031192506220405+(-0.4890562590412937)i, 3.4209548611170133+(-1.5093064853236158)i]]
*/

We still have several open tasks for complex numbers. You can participate in the development of the library and become a contributor.

LU factorization

LU factorization, or lower-upper decomposition, represents a matrix as the product of a permutation matrix, a lower triangular matrix, and an upper triangular matrix: A = P ✕ L ✕ U.

We have Introduced this in Multik.

Figure 1. PLU factorization.
val a = mk.d2array(3, 3) { it.toDouble() }
val (p, l, u) = mk.linalg.plu(a)

abs((p dot l dot u) - a).all { 1e-13 > it } // true

Solving linear systems

Solving systems of linear equations is a common problem encountered in algebra. With PLU factorization, we can easily solve square linear systems. To do this, we have provided a corresponding method in linalg.

val a = mk.ndarray(mk[mk[2, 5, 1], mk[7, 3, 1], mk[8, 9, 4]])
val b = mk.ndarray(mk[3, 1, 2])
mk.linalg.solve(a, b)
// [-0.036363636363636265, 0.9090909090909091, -1.4727272727272729]

Inverse matrix

The inverse matrix of a square matrix A, denoted A-1, is such that the following equality holds: A ✕ A-1 = A-1 ✕ A = I, where I is the square identity matrix. If A-1 is nonsingular, then it can be calculated by solving the above expression. For convenience, we have added this method out of the box.

val a = mk.ndarray(mk[mk[2.0, 5.0, 1.0], mk[7.0, 3.0, 1.0], mk[8.0, 9.0, 4.0]])
val ainv = mk.linalg.inv(a)
abs((a dot ainv) - mk.identity<Double>(3)).all { 1e-13 > it } // true

QR factorization

Another important decomposition is QR decomposition. We can decompose any square matrix A as a product Q ✕ R, where Q is an orthogonal matrix and R is an upper triangular matrix.

val a = mk.d2array(3, 3) { (it * it).toDouble() }
val (q, r) = mk.linalg.qr(a)
abs(a - (q dot r)).all { 1e-13 > it } //  true

Eigenvalues and eigenvectors

Based on the QR decomposition, we’ve made it possible to calculate eigenvalues and eigenvectors with Multik.

val a = mk.ndarray(mk[mk[1, -1], mk[1, 1]])
val (w, v) = mk.linalg.eig(a)
/*
w = [0.9999999999999998+(0.9999999999999998)i, 1.0+(-0.9999999999999998)i]

v = 
[[0.7071067811865476+(0.0)i, 0.7071067811865474+(0.0)i],
[-0.0+(-0.7071067811865475)i, -8.326672684688674E-17+(0.7071067811865474)i]]
*/

Append, stack, and meshgrid

In this release, we have added functions to help you work with multidimensional arrays more easily.

For example, you can use the append function to concatenate an array with scalars and other arrays. We’re grateful to our contributor Ansh Tyagi for implementing this feature.

Figure 2. Appending two vectors.
val a = mk.d1array(5) { it }
a.append(10) // [0, 1, 2, 3, 4, 10]
a.append(mk.ndarrayOf(9, 8, 7)) // [0, 1, 2, 3, 4, 9, 8, 7]

Append can also be used along the ndarray axes, which then becomes equivalent to cat function.

Figure 3. Appending two matrices along the axis.
val a = mk.d2array(2, 3) { it }
println(a.append(a, axis=0))
/*
[[0, 1, 2],
[3, 4, 5],
[0, 1, 2],
[3, 4, 5]]
*/

println(a.append(a, axis=1))
/*
[[0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5]]
*/

stack allows arrays to be concatenated along a new axis.

Figure 4. Stacking two vectors.
val a = mk.ndarrayOf(1, 2, 3)
println(mk.stack(a, a))
/*
[[1, 2, 3],
[1, 2, 3]]
*/

println(mk.stack(a, a, axis=1))
/*
[[1, 1],
[2, 2],
[3, 3]]
*/

And meshgrid allows you to get a grid of coordinates from vectors.

val x = mk.linspace<Double>(0, 1, 3)
val y = mk.linspace<Double>(0, 1, 4)
val (xg, yg) = mk.meshgrid(x, y)

println(xg)
/*
[[0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0]]
 */

println(yg)
/*
[[0.0, 0.0, 0.0],
 [0.3333333333333333, 0.3333333333333333, 0.3333333333333333],
 [0.6666666666666666, 0.6666666666666666, 0.6666666666666666],
 [1.0, 1.0, 1.0]]
 */

Conclusion

For more details about this new release, please check out the changelog.

We want to thank our users and contributors – you help us improve the library and make it more reliable and convenient!

To use Multik in your project, add the following dependencies to your build.gradle file:

repositories {
    mavenCentral()
}

dependencies {
    implementation "org.jetbrains.kotlinx:multik-api:0.1.0"
    implementation "org.jetbrains.kotlinx:multik-default:0.1.0"
}

You can use different engines, not just the default. We have multik-default, multik-native, and multik-jvm. Please note that on Android we only support the JVM engine.

Alternatively, you can start using Multik in Jupyter or Datalore:

%use multik

If you don’t have the kotlin-kernel for Jupyter, you can read about installation here. In Datalore, everything works out of the box.

We’re looking for contributions from the community, so please don’t hesitate to join the effort! Current tasks can always be found in the issue tracker.

Try out Multik 0.1 and share your experience with us!

Let’s Kotlin!

Continue ReadingMultik 0.1 Is Out

Multik 0.1 Is Out

Introducing Multik 0.1 – a new, enhanced version of our multidimensional array library! You can check out the previous post to learn about the basic features and architecture of the library.

In the new release, we added new methods from linear algebra, supported complex numbers and reading/writing .csv files, improved the performance and stability of existing functions, and added many more features that will make it easier for you to work with multidimensional arrays.

Multik on GitHub

Let’s take a look at the new features this release brings to the API:

Reading and writing CSV files

CSV is a popular data recording format, and now Multik 0.1 allows you to read and write .csv files easily.

val a = mk.d2array(2, 2) { it }
mk.write("example.csv", a)
val b = mk.read<Int, D2>("example.csv")
println(b)
/*
[[0, 1],
[2, 3]]
*/

Complex numbers

Complex numbers are expressed as re + i ∙ im, where re and im are real numbers and i is the imaginary unit equal to the square root of -1; re is the real part of the complex number and i ∙ im is the imaginary part. Complex numbers are common in algebra and are now a part of the Multik API.

val cf = ComplexFloat(1f, 2f) // 1.0+(2.0)i
val cd = ComplexDouble.one // 1.0+(0.0)i

The ComplexFloat and ComplexDouble classes are created in full accordance with the Number classes, so there is no need to learn a new API.

val c1 = ComplexFloat(1f, 2f) // 1.0+(2.0)i
val c2 = ComplexFloat(3f, 4f) // 3.0+(4.0)i

println(c1 + c2) // 4.0+(6.0)i
println(c1 * c2) // -5.0+(10.0)i

val (re, im) = c1 // re = 1.0; im = 2.0
val re2 = c2.re // 3.0
val im2 = c2.im // 4.0

We’ve also added ComplexFloatArrays and ComplexDoubleArrays. They support all methods that primitive arrays do, except for methods that use comparison, since complex numbers are unordered. But you can use methods such as minBy, maxBy, sortedBy, and others to define the comparison logic yourself.

val array = ComplexDoubleArray(5) { ComplexDouble(it, it.toDouble() / 2) }

println(array)
// [0.0 + 0.0i, 1.0 + 0.0i, 2.0 + 1.0i, 3.0 + 1.0i, 4.0 + 2.0i]

println(array.filter { it.abs() > 2.0 })
// [2.0+(1.0)i, 3.0+(1.5)i, 4.0+(2.0)i]

println(array.sortedByDescending { it.im })
// [4.0+(2.0)i, 3.0+(1.5)i, 2.0+(1.0)i, 1.0+(0.5)i, 0.0+(0.0)i]

After supporting complex numbers and arrays of complex numbers, we added them to multidimensional arrays.

println(mk.d2arrayIndices(2, 2) { i, j -> ComplexFloat(i, j) })
/*
[[0.0+(0.0)i, 0.0+(1.0)i],
[1.0+(0.0)i, 1.0+(1.0)i]]
 */

println(mk.empty<ComplexDouble, D1>(5))
// [0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i]

println(mk.ndarrayOf(ComplexDouble.zero, ComplexDouble.one))
//[0.0+(0.0)i, 1.0+(0.0)i]

You can do everything with them that you did with integer and float multidimensional arrays before.

val ndarray = mk.d2arrayIndices(3, 3) { i, j -> ComplexDouble(i, j) }
println(ndarray.map { it.re * it.re + it.im * it.im })
/*
[[0.0, 1.0, 4.0],
[1.0, 2.0, 5.0],
[4.0, 5.0, 8.0]]
 */

 println(mk.math.sin(ndarray))
 /*
[[0.0+(0.0)i, 0.0+(1.1752011936438014)i, 0.0+(3.626860407847019)i],
[0.8414709848078965+(0.0)i, 1.2984575814159773+(0.6349639147847361)i, 3.165778513216168+(1.9596010414216063)i],
[0.9092974268256817+(-0.0)i, 1.4031192506220405+(-0.4890562590412937)i, 3.4209548611170133+(-1.5093064853236158)i]]
*/

We still have several open tasks for complex numbers. You can participate in the development of the library and become a contributor.

LU factorization

LU factorization, or lower-upper decomposition, represents a matrix as the product of a permutation matrix, a lower triangular matrix, and an upper triangular matrix: A = P ✕ L ✕ U.

We have Introduced this in Multik.

Figure 1. PLU factorization.
val a = mk.d2array(3, 3) { it.toDouble() }
val (p, l, u) = mk.linalg.plu(a)

abs((p dot l dot u) - a).all { 1e-13 > it } // true

Solving linear systems

Solving systems of linear equations is a common problem encountered in algebra. With PLU factorization, we can easily solve square linear systems. To do this, we have provided a corresponding method in linalg.

val a = mk.ndarray(mk[mk[2, 5, 1], mk[7, 3, 1], mk[8, 9, 4]])
val b = mk.ndarray(mk[3, 1, 2])
mk.linalg.solve(a, b)
// [-0.036363636363636265, 0.9090909090909091, -1.4727272727272729]

Inverse matrix

The inverse matrix of a square matrix A, denoted A-1, is such that the following equality holds: A ✕ A-1 = A-1 ✕ A = I, where I is the square identity matrix. If A-1 is nonsingular, then it can be calculated by solving the above expression. For convenience, we have added this method out of the box.

val a = mk.ndarray(mk[mk[2.0, 5.0, 1.0], mk[7.0, 3.0, 1.0], mk[8.0, 9.0, 4.0]])
val ainv = mk.linalg.inv(a)
abs((a dot ainv) - mk.identity<Double>(3)).all { 1e-13 > it } // true

QR factorization

Another important decomposition is QR decomposition. We can decompose any square matrix A as a product Q ✕ R, where Q is an orthogonal matrix and R is an upper triangular matrix.

val a = mk.d2array(3, 3) { (it * it).toDouble() }
val (q, r) = mk.linalg.qr(a)
abs(a - (q dot r)).all { 1e-13 > it } //  true

Eigenvalues and eigenvectors

Based on the QR decomposition, we’ve made it possible to calculate eigenvalues and eigenvectors with Multik.

val a = mk.ndarray(mk[mk[1, -1], mk[1, 1]])
val (w, v) = mk.linalg.eig(a)
/*
w = [0.9999999999999998+(0.9999999999999998)i, 1.0+(-0.9999999999999998)i]

v = 
[[0.7071067811865476+(0.0)i, 0.7071067811865474+(0.0)i],
[-0.0+(-0.7071067811865475)i, -8.326672684688674E-17+(0.7071067811865474)i]]
*/

Append, stack, and meshgrid

In this release, we have added functions to help you work with multidimensional arrays more easily.

For example, you can use the append function to concatenate an array with scalars and other arrays. We’re grateful to our contributor Ansh Tyagi for implementing this feature.

Figure 2. Appending two vectors.
val a = mk.d1array(5) { it }
a.append(10) // [0, 1, 2, 3, 4, 10]
a.append(mk.ndarrayOf(9, 8, 7)) // [0, 1, 2, 3, 4, 9, 8, 7]

Append can also be used along the ndarray axes, which then becomes equivalent to cat function.

Figure 3. Appending two matrices along the axis.
val a = mk.d2array(2, 3) { it }
println(a.append(a, axis=0))
/*
[[0, 1, 2],
[3, 4, 5],
[0, 1, 2],
[3, 4, 5]]
*/

println(a.append(a, axis=1))
/*
[[0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5]]
*/

stack allows arrays to be concatenated along a new axis.

Figure 4. Stacking two vectors.
val a = mk.ndarrayOf(1, 2, 3)
println(mk.stack(a, a))
/*
[[1, 2, 3],
[1, 2, 3]]
*/

println(mk.stack(a, a, axis=1))
/*
[[1, 1],
[2, 2],
[3, 3]]
*/

And meshgrid allows you to get a grid of coordinates from vectors.

val x = mk.linspace<Double>(0, 1, 3)
val y = mk.linspace<Double>(0, 1, 4)
val (xg, yg) = mk.meshgrid(x, y)

println(xg)
/*
[[0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0]]
 */

println(yg)
/*
[[0.0, 0.0, 0.0],
 [0.3333333333333333, 0.3333333333333333, 0.3333333333333333],
 [0.6666666666666666, 0.6666666666666666, 0.6666666666666666],
 [1.0, 1.0, 1.0]]
 */

Conclusion

For more details about this new release, please check out the changelog.

We want to thank our users and contributors – you help us improve the library and make it more reliable and convenient!

To use Multik in your project, add the following dependencies to your build.gradle file:

repositories {
    mavenCentral()
}

dependencies {
    implementation "org.jetbrains.kotlinx:multik-api:0.1.0"
    implementation "org.jetbrains.kotlinx:multik-default:0.1.0"
}

You can use different engines, not just the default. We have multik-default, multik-native, and multik-jvm. Please note that on Android we only support the JVM engine.

Alternatively, you can start using Multik in Jupyter or Datalore:

%use multik

If you don’t have the kotlin-kernel for Jupyter, you can read about installation here. In Datalore, everything works out of the box.

We’re looking for contributions from the community, so please don’t hesitate to join the effort! Current tasks can always be found in the issue tracker.

Try out Multik 0.1 and share your experience with us!

Let’s Kotlin!

Continue ReadingMultik 0.1 Is Out

Multik 0.1 Is Out

Introducing Multik 0.1 – a new, enhanced version of our multidimensional array library! You can check out the previous post to learn about the basic features and architecture of the library.

In the new release, we added new methods from linear algebra, supported complex numbers and reading/writing .csv files, improved the performance and stability of existing functions, and added many more features that will make it easier for you to work with multidimensional arrays.

Multik on GitHub

Let’s take a look at the new features this release brings to the API:

Reading and writing CSV files

CSV is a popular data recording format, and now Multik 0.1 allows you to read and write .csv files easily.

val a = mk.d2array(2, 2) { it }
mk.write("example.csv", a)
val b = mk.read<Int, D2>("example.csv")
println(b)
/*
[[0, 1],
[2, 3]]
*/

Complex numbers

Complex numbers are expressed as re + i ∙ im, where re and im are real numbers and i is the imaginary unit equal to the square root of -1; re is the real part of the complex number and i ∙ im is the imaginary part. Complex numbers are common in algebra and are now a part of the Multik API.

val cf = ComplexFloat(1f, 2f) // 1.0+(2.0)i
val cd = ComplexDouble.one // 1.0+(0.0)i

The ComplexFloat and ComplexDouble classes are created in full accordance with the Number classes, so there is no need to learn a new API.

val c1 = ComplexFloat(1f, 2f) // 1.0+(2.0)i
val c2 = ComplexFloat(3f, 4f) // 3.0+(4.0)i

println(c1 + c2) // 4.0+(6.0)i
println(c1 * c2) // -5.0+(10.0)i

val (re, im) = c1 // re = 1.0; im = 2.0
val re2 = c2.re // 3.0
val im2 = c2.im // 4.0

We’ve also added ComplexFloatArrays and ComplexDoubleArrays. They support all methods that primitive arrays do, except for methods that use comparison, since complex numbers are unordered. But you can use methods such as minBy, maxBy, sortedBy, and others to define the comparison logic yourself.

val array = ComplexDoubleArray(5) { ComplexDouble(it, it.toDouble() / 2) }

println(array)
// [0.0 + 0.0i, 1.0 + 0.0i, 2.0 + 1.0i, 3.0 + 1.0i, 4.0 + 2.0i]

println(array.filter { it.abs() > 2.0 })
// [2.0+(1.0)i, 3.0+(1.5)i, 4.0+(2.0)i]

println(array.sortedByDescending { it.im })
// [4.0+(2.0)i, 3.0+(1.5)i, 2.0+(1.0)i, 1.0+(0.5)i, 0.0+(0.0)i]

After supporting complex numbers and arrays of complex numbers, we added them to multidimensional arrays.

println(mk.d2arrayIndices(2, 2) { i, j -> ComplexFloat(i, j) })
/*
[[0.0+(0.0)i, 0.0+(1.0)i],
[1.0+(0.0)i, 1.0+(1.0)i]]
 */

println(mk.empty<ComplexDouble, D1>(5))
// [0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i]

println(mk.ndarrayOf(ComplexDouble.zero, ComplexDouble.one))
//[0.0+(0.0)i, 1.0+(0.0)i]

You can do everything with them that you did with integer and float multidimensional arrays before.

val ndarray = mk.d2arrayIndices(3, 3) { i, j -> ComplexDouble(i, j) }
println(ndarray.map { it.re * it.re + it.im * it.im })
/*
[[0.0, 1.0, 4.0],
[1.0, 2.0, 5.0],
[4.0, 5.0, 8.0]]
 */

 println(mk.math.sin(ndarray))
 /*
[[0.0+(0.0)i, 0.0+(1.1752011936438014)i, 0.0+(3.626860407847019)i],
[0.8414709848078965+(0.0)i, 1.2984575814159773+(0.6349639147847361)i, 3.165778513216168+(1.9596010414216063)i],
[0.9092974268256817+(-0.0)i, 1.4031192506220405+(-0.4890562590412937)i, 3.4209548611170133+(-1.5093064853236158)i]]
*/

We still have several open tasks for complex numbers. You can participate in the development of the library and become a contributor.

LU factorization

LU factorization, or lower-upper decomposition, represents a matrix as the product of a permutation matrix, a lower triangular matrix, and an upper triangular matrix: A = P ✕ L ✕ U.

We have Introduced this in Multik.

Figure 1. PLU factorization.
val a = mk.d2array(3, 3) { it.toDouble() }
val (p, l, u) = mk.linalg.plu(a)

abs((p dot l dot u) - a).all { 1e-13 > it } // true

Solving linear systems

Solving systems of linear equations is a common problem encountered in algebra. With PLU factorization, we can easily solve square linear systems. To do this, we have provided a corresponding method in linalg.

val a = mk.ndarray(mk[mk[2, 5, 1], mk[7, 3, 1], mk[8, 9, 4]])
val b = mk.ndarray(mk[3, 1, 2])
mk.linalg.solve(a, b)
// [-0.036363636363636265, 0.9090909090909091, -1.4727272727272729]

Inverse matrix

The inverse matrix of a square matrix A, denoted A-1, is such that the following equality holds: A ✕ A-1 = A-1 ✕ A = I, where I is the square identity matrix. If A-1 is nonsingular, then it can be calculated by solving the above expression. For convenience, we have added this method out of the box.

val a = mk.ndarray(mk[mk[2.0, 5.0, 1.0], mk[7.0, 3.0, 1.0], mk[8.0, 9.0, 4.0]])
val ainv = mk.linalg.inv(a)
abs((a dot ainv) - mk.identity<Double>(3)).all { 1e-13 > it } // true

QR factorization

Another important decomposition is QR decomposition. We can decompose any square matrix A as a product Q ✕ R, where Q is an orthogonal matrix and R is an upper triangular matrix.

val a = mk.d2array(3, 3) { (it * it).toDouble() }
val (q, r) = mk.linalg.qr(a)
abs(a - (q dot r)).all { 1e-13 > it } //  true

Eigenvalues and eigenvectors

Based on the QR decomposition, we’ve made it possible to calculate eigenvalues and eigenvectors with Multik.

val a = mk.ndarray(mk[mk[1, -1], mk[1, 1]])
val (w, v) = mk.linalg.eig(a)
/*
w = [0.9999999999999998+(0.9999999999999998)i, 1.0+(-0.9999999999999998)i]

v = 
[[0.7071067811865476+(0.0)i, 0.7071067811865474+(0.0)i],
[-0.0+(-0.7071067811865475)i, -8.326672684688674E-17+(0.7071067811865474)i]]
*/

Append, stack, and meshgrid

In this release, we have added functions to help you work with multidimensional arrays more easily.

For example, you can use the append function to concatenate an array with scalars and other arrays. We’re grateful to our contributor Ansh Tyagi for implementing this feature.

Figure 2. Appending two vectors.
val a = mk.d1array(5) { it }
a.append(10) // [0, 1, 2, 3, 4, 10]
a.append(mk.ndarrayOf(9, 8, 7)) // [0, 1, 2, 3, 4, 9, 8, 7]

Append can also be used along the ndarray axes, which then becomes equivalent to cat function.

Figure 3. Appending two matrices along the axis.
val a = mk.d2array(2, 3) { it }
println(a.append(a, axis=0))
/*
[[0, 1, 2],
[3, 4, 5],
[0, 1, 2],
[3, 4, 5]]
*/

println(a.append(a, axis=1))
/*
[[0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5]]
*/

stack allows arrays to be concatenated along a new axis.

Figure 4. Stacking two vectors.
val a = mk.ndarrayOf(1, 2, 3)
println(mk.stack(a, a))
/*
[[1, 2, 3],
[1, 2, 3]]
*/

println(mk.stack(a, a, axis=1))
/*
[[1, 1],
[2, 2],
[3, 3]]
*/

And meshgrid allows you to get a grid of coordinates from vectors.

val x = mk.linspace<Double>(0, 1, 3)
val y = mk.linspace<Double>(0, 1, 4)
val (xg, yg) = mk.meshgrid(x, y)

println(xg)
/*
[[0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0]]
 */

println(yg)
/*
[[0.0, 0.0, 0.0],
 [0.3333333333333333, 0.3333333333333333, 0.3333333333333333],
 [0.6666666666666666, 0.6666666666666666, 0.6666666666666666],
 [1.0, 1.0, 1.0]]
 */

Conclusion

For more details about this new release, please check out the changelog.

We want to thank our users and contributors – you help us improve the library and make it more reliable and convenient!

To use Multik in your project, add the following dependencies to your build.gradle file:

repositories {
    mavenCentral()
}

dependencies {
    implementation "org.jetbrains.kotlinx:multik-api:0.1.0"
    implementation "org.jetbrains.kotlinx:multik-default:0.1.0"
}

You can use different engines, not just the default. We have multik-default, multik-native, and multik-jvm. Please note that on Android we only support the JVM engine.

Alternatively, you can start using Multik in Jupyter or Datalore:

%use multik

If you don’t have the kotlin-kernel for Jupyter, you can read about installation here. In Datalore, everything works out of the box.

We’re looking for contributions from the community, so please don’t hesitate to join the effort! Current tasks can always be found in the issue tracker.

Try out Multik 0.1 and share your experience with us!

Let’s Kotlin!

Continue ReadingMultik 0.1 Is Out

Multik 0.1 Is Out

Introducing Multik 0.1 – a new, enhanced version of our multidimensional array library! You can check out the previous post to learn about the basic features and architecture of the library.

In the new release, we added new methods from linear algebra, supported complex numbers and reading/writing .csv files, improved the performance and stability of existing functions, and added many more features that will make it easier for you to work with multidimensional arrays.

Multik on GitHub

Let’s take a look at the new features this release brings to the API:

Reading and writing CSV files

CSV is a popular data recording format, and now Multik 0.1 allows you to read and write .csv files easily.

val a = mk.d2array(2, 2) { it }
mk.write("example.csv", a)
val b = mk.read<Int, D2>("example.csv")
println(b)
/*
[[0, 1],
[2, 3]]
*/

Complex numbers

Complex numbers are expressed as re + i ∙ im, where re and im are real numbers and i is the imaginary unit equal to the square root of -1; re is the real part of the complex number and i ∙ im is the imaginary part. Complex numbers are common in algebra and are now a part of the Multik API.

val cf = ComplexFloat(1f, 2f) // 1.0+(2.0)i
val cd = ComplexDouble.one // 1.0+(0.0)i

The ComplexFloat and ComplexDouble classes are created in full accordance with the Number classes, so there is no need to learn a new API.

val c1 = ComplexFloat(1f, 2f) // 1.0+(2.0)i
val c2 = ComplexFloat(3f, 4f) // 3.0+(4.0)i

println(c1 + c2) // 4.0+(6.0)i
println(c1 * c2) // -5.0+(10.0)i

val (re, im) = c1 // re = 1.0; im = 2.0
val re2 = c2.re // 3.0
val im2 = c2.im // 4.0

We’ve also added ComplexFloatArrays and ComplexDoubleArrays. They support all methods that primitive arrays do, except for methods that use comparison, since complex numbers are unordered. But you can use methods such as minBy, maxBy, sortedBy, and others to define the comparison logic yourself.

val array = ComplexDoubleArray(5) { ComplexDouble(it, it.toDouble() / 2) }

println(array)
// [0.0 + 0.0i, 1.0 + 0.0i, 2.0 + 1.0i, 3.0 + 1.0i, 4.0 + 2.0i]

println(array.filter { it.abs() > 2.0 })
// [2.0+(1.0)i, 3.0+(1.5)i, 4.0+(2.0)i]

println(array.sortedByDescending { it.im })
// [4.0+(2.0)i, 3.0+(1.5)i, 2.0+(1.0)i, 1.0+(0.5)i, 0.0+(0.0)i]

After supporting complex numbers and arrays of complex numbers, we added them to multidimensional arrays.

println(mk.d2arrayIndices(2, 2) { i, j -> ComplexFloat(i, j) })
/*
[[0.0+(0.0)i, 0.0+(1.0)i],
[1.0+(0.0)i, 1.0+(1.0)i]]
 */

println(mk.empty<ComplexDouble, D1>(5))
// [0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i]

println(mk.ndarrayOf(ComplexDouble.zero, ComplexDouble.one))
//[0.0+(0.0)i, 1.0+(0.0)i]

You can do everything with them that you did with integer and float multidimensional arrays before.

val ndarray = mk.d2arrayIndices(3, 3) { i, j -> ComplexDouble(i, j) }
println(ndarray.map { it.re * it.re + it.im * it.im })
/*
[[0.0, 1.0, 4.0],
[1.0, 2.0, 5.0],
[4.0, 5.0, 8.0]]
 */

 println(mk.math.sin(ndarray))
 /*
[[0.0+(0.0)i, 0.0+(1.1752011936438014)i, 0.0+(3.626860407847019)i],
[0.8414709848078965+(0.0)i, 1.2984575814159773+(0.6349639147847361)i, 3.165778513216168+(1.9596010414216063)i],
[0.9092974268256817+(-0.0)i, 1.4031192506220405+(-0.4890562590412937)i, 3.4209548611170133+(-1.5093064853236158)i]]
*/

We still have several open tasks for complex numbers. You can participate in the development of the library and become a contributor.

LU factorization

LU factorization, or lower-upper decomposition, represents a matrix as the product of a permutation matrix, a lower triangular matrix, and an upper triangular matrix: A = P ✕ L ✕ U.

We have Introduced this in Multik.

Figure 1. PLU factorization.
val a = mk.d2array(3, 3) { it.toDouble() }
val (p, l, u) = mk.linalg.plu(a)

abs((p dot l dot u) - a).all { 1e-13 > it } // true

Solving linear systems

Solving systems of linear equations is a common problem encountered in algebra. With PLU factorization, we can easily solve square linear systems. To do this, we have provided a corresponding method in linalg.

val a = mk.ndarray(mk[mk[2, 5, 1], mk[7, 3, 1], mk[8, 9, 4]])
val b = mk.ndarray(mk[3, 1, 2])
mk.linalg.solve(a, b)
// [-0.036363636363636265, 0.9090909090909091, -1.4727272727272729]

Inverse matrix

The inverse matrix of a square matrix A, denoted A-1, is such that the following equality holds: A ✕ A-1 = A-1 ✕ A = I, where I is the square identity matrix. If A-1 is nonsingular, then it can be calculated by solving the above expression. For convenience, we have added this method out of the box.

val a = mk.ndarray(mk[mk[2.0, 5.0, 1.0], mk[7.0, 3.0, 1.0], mk[8.0, 9.0, 4.0]])
val ainv = mk.linalg.inv(a)
abs((a dot ainv) - mk.identity<Double>(3)).all { 1e-13 > it } // true

QR factorization

Another important decomposition is QR decomposition. We can decompose any square matrix A as a product Q ✕ R, where Q is an orthogonal matrix and R is an upper triangular matrix.

val a = mk.d2array(3, 3) { (it * it).toDouble() }
val (q, r) = mk.linalg.qr(a)
abs(a - (q dot r)).all { 1e-13 > it } //  true

Eigenvalues and eigenvectors

Based on the QR decomposition, we’ve made it possible to calculate eigenvalues and eigenvectors with Multik.

val a = mk.ndarray(mk[mk[1, -1], mk[1, 1]])
val (w, v) = mk.linalg.eig(a)
/*
w = [0.9999999999999998+(0.9999999999999998)i, 1.0+(-0.9999999999999998)i]

v = 
[[0.7071067811865476+(0.0)i, 0.7071067811865474+(0.0)i],
[-0.0+(-0.7071067811865475)i, -8.326672684688674E-17+(0.7071067811865474)i]]
*/

Append, stack, and meshgrid

In this release, we have added functions to help you work with multidimensional arrays more easily.

For example, you can use the append function to concatenate an array with scalars and other arrays. We’re grateful to our contributor Ansh Tyagi for implementing this feature.

Figure 2. Appending two vectors.
val a = mk.d1array(5) { it }
a.append(10) // [0, 1, 2, 3, 4, 10]
a.append(mk.ndarrayOf(9, 8, 7)) // [0, 1, 2, 3, 4, 9, 8, 7]

Append can also be used along the ndarray axes, which then becomes equivalent to cat function.

Figure 3. Appending two matrices along the axis.
val a = mk.d2array(2, 3) { it }
println(a.append(a, axis=0))
/*
[[0, 1, 2],
[3, 4, 5],
[0, 1, 2],
[3, 4, 5]]
*/

println(a.append(a, axis=1))
/*
[[0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5]]
*/

stack allows arrays to be concatenated along a new axis.

Figure 4. Stacking two vectors.
val a = mk.ndarrayOf(1, 2, 3)
println(mk.stack(a, a))
/*
[[1, 2, 3],
[1, 2, 3]]
*/

println(mk.stack(a, a, axis=1))
/*
[[1, 1],
[2, 2],
[3, 3]]
*/

And meshgrid allows you to get a grid of coordinates from vectors.

val x = mk.linspace<Double>(0, 1, 3)
val y = mk.linspace<Double>(0, 1, 4)
val (xg, yg) = mk.meshgrid(x, y)

println(xg)
/*
[[0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0]]
 */

println(yg)
/*
[[0.0, 0.0, 0.0],
 [0.3333333333333333, 0.3333333333333333, 0.3333333333333333],
 [0.6666666666666666, 0.6666666666666666, 0.6666666666666666],
 [1.0, 1.0, 1.0]]
 */

Conclusion

For more details about this new release, please check out the changelog.

We want to thank our users and contributors – you help us improve the library and make it more reliable and convenient!

To use Multik in your project, add the following dependencies to your build.gradle file:

repositories {
    mavenCentral()
}

dependencies {
    implementation "org.jetbrains.kotlinx:multik-api:0.1.0"
    implementation "org.jetbrains.kotlinx:multik-default:0.1.0"
}

You can use different engines, not just the default. We have multik-default, multik-native, and multik-jvm. Please note that on Android we only support the JVM engine.

Alternatively, you can start using Multik in Jupyter or Datalore:

%use multik

If you don’t have the kotlin-kernel for Jupyter, you can read about installation here. In Datalore, everything works out of the box.

We’re looking for contributions from the community, so please don’t hesitate to join the effort! Current tasks can always be found in the issue tracker.

Try out Multik 0.1 and share your experience with us!

Let’s Kotlin!

Continue ReadingMultik 0.1 Is Out

Multik 0.1 Is Out

Introducing Multik 0.1 – a new, enhanced version of our multidimensional array library! You can check out the previous post to learn about the basic features and architecture of the library.

In the new release, we added new methods from linear algebra, supported complex numbers and reading/writing .csv files, improved the performance and stability of existing functions, and added many more features that will make it easier for you to work with multidimensional arrays.

Multik on GitHub

Let’s take a look at the new features this release brings to the API:

Reading and writing CSV files

CSV is a popular data recording format, and now Multik 0.1 allows you to read and write .csv files easily.

val a = mk.d2array(2, 2) { it }
mk.write("example.csv", a)
val b = mk.read<Int, D2>("example.csv")
println(b)
/*
[[0, 1],
[2, 3]]
*/

Complex numbers

Complex numbers are expressed as re + i ∙ im, where re and im are real numbers and i is the imaginary unit equal to the square root of -1; re is the real part of the complex number and i ∙ im is the imaginary part. Complex numbers are common in algebra and are now a part of the Multik API.

val cf = ComplexFloat(1f, 2f) // 1.0+(2.0)i
val cd = ComplexDouble.one // 1.0+(0.0)i

The ComplexFloat and ComplexDouble classes are created in full accordance with the Number classes, so there is no need to learn a new API.

val c1 = ComplexFloat(1f, 2f) // 1.0+(2.0)i
val c2 = ComplexFloat(3f, 4f) // 3.0+(4.0)i

println(c1 + c2) // 4.0+(6.0)i
println(c1 * c2) // -5.0+(10.0)i

val (re, im) = c1 // re = 1.0; im = 2.0
val re2 = c2.re // 3.0
val im2 = c2.im // 4.0

We’ve also added ComplexFloatArrays and ComplexDoubleArrays. They support all methods that primitive arrays do, except for methods that use comparison, since complex numbers are unordered. But you can use methods such as minBy, maxBy, sortedBy, and others to define the comparison logic yourself.

val array = ComplexDoubleArray(5) { ComplexDouble(it, it.toDouble() / 2) }

println(array)
// [0.0 + 0.0i, 1.0 + 0.0i, 2.0 + 1.0i, 3.0 + 1.0i, 4.0 + 2.0i]

println(array.filter { it.abs() > 2.0 })
// [2.0+(1.0)i, 3.0+(1.5)i, 4.0+(2.0)i]

println(array.sortedByDescending { it.im })
// [4.0+(2.0)i, 3.0+(1.5)i, 2.0+(1.0)i, 1.0+(0.5)i, 0.0+(0.0)i]

After supporting complex numbers and arrays of complex numbers, we added them to multidimensional arrays.

println(mk.d2arrayIndices(2, 2) { i, j -> ComplexFloat(i, j) })
/*
[[0.0+(0.0)i, 0.0+(1.0)i],
[1.0+(0.0)i, 1.0+(1.0)i]]
 */

println(mk.empty<ComplexDouble, D1>(5))
// [0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i]

println(mk.ndarrayOf(ComplexDouble.zero, ComplexDouble.one))
//[0.0+(0.0)i, 1.0+(0.0)i]

You can do everything with them that you did with integer and float multidimensional arrays before.

val ndarray = mk.d2arrayIndices(3, 3) { i, j -> ComplexDouble(i, j) }
println(ndarray.map { it.re * it.re + it.im * it.im })
/*
[[0.0, 1.0, 4.0],
[1.0, 2.0, 5.0],
[4.0, 5.0, 8.0]]
 */

 println(mk.math.sin(ndarray))
 /*
[[0.0+(0.0)i, 0.0+(1.1752011936438014)i, 0.0+(3.626860407847019)i],
[0.8414709848078965+(0.0)i, 1.2984575814159773+(0.6349639147847361)i, 3.165778513216168+(1.9596010414216063)i],
[0.9092974268256817+(-0.0)i, 1.4031192506220405+(-0.4890562590412937)i, 3.4209548611170133+(-1.5093064853236158)i]]
*/

We still have several open tasks for complex numbers. You can participate in the development of the library and become a contributor.

LU factorization

LU factorization, or lower-upper decomposition, represents a matrix as the product of a permutation matrix, a lower triangular matrix, and an upper triangular matrix: A = P ✕ L ✕ U.

We have Introduced this in Multik.

Figure 1. PLU factorization.
val a = mk.d2array(3, 3) { it.toDouble() }
val (p, l, u) = mk.linalg.plu(a)

abs((p dot l dot u) - a).all { 1e-13 > it } // true

Solving linear systems

Solving systems of linear equations is a common problem encountered in algebra. With PLU factorization, we can easily solve square linear systems. To do this, we have provided a corresponding method in linalg.

val a = mk.ndarray(mk[mk[2, 5, 1], mk[7, 3, 1], mk[8, 9, 4]])
val b = mk.ndarray(mk[3, 1, 2])
mk.linalg.solve(a, b)
// [-0.036363636363636265, 0.9090909090909091, -1.4727272727272729]

Inverse matrix

The inverse matrix of a square matrix A, denoted A-1, is such that the following equality holds: A ✕ A-1 = A-1 ✕ A = I, where I is the square identity matrix. If A-1 is nonsingular, then it can be calculated by solving the above expression. For convenience, we have added this method out of the box.

val a = mk.ndarray(mk[mk[2.0, 5.0, 1.0], mk[7.0, 3.0, 1.0], mk[8.0, 9.0, 4.0]])
val ainv = mk.linalg.inv(a)
abs((a dot ainv) - mk.identity<Double>(3)).all { 1e-13 > it } // true

QR factorization

Another important decomposition is QR decomposition. We can decompose any square matrix A as a product Q ✕ R, where Q is an orthogonal matrix and R is an upper triangular matrix.

val a = mk.d2array(3, 3) { (it * it).toDouble() }
val (q, r) = mk.linalg.qr(a)
abs(a - (q dot r)).all { 1e-13 > it } //  true

Eigenvalues and eigenvectors

Based on the QR decomposition, we’ve made it possible to calculate eigenvalues and eigenvectors with Multik.

val a = mk.ndarray(mk[mk[1, -1], mk[1, 1]])
val (w, v) = mk.linalg.eig(a)
/*
w = [0.9999999999999998+(0.9999999999999998)i, 1.0+(-0.9999999999999998)i]

v = 
[[0.7071067811865476+(0.0)i, 0.7071067811865474+(0.0)i],
[-0.0+(-0.7071067811865475)i, -8.326672684688674E-17+(0.7071067811865474)i]]
*/

Append, stack, and meshgrid

In this release, we have added functions to help you work with multidimensional arrays more easily.

For example, you can use the append function to concatenate an array with scalars and other arrays. We’re grateful to our contributor Ansh Tyagi for implementing this feature.

Figure 2. Appending two vectors.
val a = mk.d1array(5) { it }
a.append(10) // [0, 1, 2, 3, 4, 10]
a.append(mk.ndarrayOf(9, 8, 7)) // [0, 1, 2, 3, 4, 9, 8, 7]

Append can also be used along the ndarray axes, which then becomes equivalent to cat function.

Figure 3. Appending two matrices along the axis.
val a = mk.d2array(2, 3) { it }
println(a.append(a, axis=0))
/*
[[0, 1, 2],
[3, 4, 5],
[0, 1, 2],
[3, 4, 5]]
*/

println(a.append(a, axis=1))
/*
[[0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5]]
*/

stack allows arrays to be concatenated along a new axis.

Figure 4. Stacking two vectors.
val a = mk.ndarrayOf(1, 2, 3)
println(mk.stack(a, a))
/*
[[1, 2, 3],
[1, 2, 3]]
*/

println(mk.stack(a, a, axis=1))
/*
[[1, 1],
[2, 2],
[3, 3]]
*/

And meshgrid allows you to get a grid of coordinates from vectors.

val x = mk.linspace<Double>(0, 1, 3)
val y = mk.linspace<Double>(0, 1, 4)
val (xg, yg) = mk.meshgrid(x, y)

println(xg)
/*
[[0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0]]
 */

println(yg)
/*
[[0.0, 0.0, 0.0],
 [0.3333333333333333, 0.3333333333333333, 0.3333333333333333],
 [0.6666666666666666, 0.6666666666666666, 0.6666666666666666],
 [1.0, 1.0, 1.0]]
 */

Conclusion

For more details about this new release, please check out the changelog.

We want to thank our users and contributors – you help us improve the library and make it more reliable and convenient!

To use Multik in your project, add the following dependencies to your build.gradle file:

repositories {
    mavenCentral()
}

dependencies {
    implementation "org.jetbrains.kotlinx:multik-api:0.1.0"
    implementation "org.jetbrains.kotlinx:multik-default:0.1.0"
}

You can use different engines, not just the default. We have multik-default, multik-native, and multik-jvm. Please note that on Android we only support the JVM engine.

Alternatively, you can start using Multik in Jupyter or Datalore:

%use multik

If you don’t have the kotlin-kernel for Jupyter, you can read about installation here. In Datalore, everything works out of the box.

We’re looking for contributions from the community, so please don’t hesitate to join the effort! Current tasks can always be found in the issue tracker.

Try out Multik 0.1 and share your experience with us!

Let’s Kotlin!

Continue ReadingMultik 0.1 Is Out

Multik 0.1 Is Out

Introducing Multik 0.1 – a new, enhanced version of our multidimensional array library! You can check out the previous post to learn about the basic features and architecture of the library.

In the new release, we added new methods from linear algebra, supported complex numbers and reading/writing .csv files, improved the performance and stability of existing functions, and added many more features that will make it easier for you to work with multidimensional arrays.

Multik on GitHub

Let’s take a look at the new features this release brings to the API:

Reading and writing CSV files

CSV is a popular data recording format, and now Multik 0.1 allows you to read and write .csv files easily.

val a = mk.d2array(2, 2) { it }
mk.write("example.csv", a)
val b = mk.read<Int, D2>("example.csv")
println(b)
/*
[[0, 1],
[2, 3]]
*/

Complex numbers

Complex numbers are expressed as re + i ∙ im, where re and im are real numbers and i is the imaginary unit equal to the square root of -1; re is the real part of the complex number and i ∙ im is the imaginary part. Complex numbers are common in algebra and are now a part of the Multik API.

val cf = ComplexFloat(1f, 2f) // 1.0+(2.0)i
val cd = ComplexDouble.one // 1.0+(0.0)i

The ComplexFloat and ComplexDouble classes are created in full accordance with the Number classes, so there is no need to learn a new API.

val c1 = ComplexFloat(1f, 2f) // 1.0+(2.0)i
val c2 = ComplexFloat(3f, 4f) // 3.0+(4.0)i

println(c1 + c2) // 4.0+(6.0)i
println(c1 * c2) // -5.0+(10.0)i

val (re, im) = c1 // re = 1.0; im = 2.0
val re2 = c2.re // 3.0
val im2 = c2.im // 4.0

We’ve also added ComplexFloatArrays and ComplexDoubleArrays. They support all methods that primitive arrays do, except for methods that use comparison, since complex numbers are unordered. But you can use methods such as minBy, maxBy, sortedBy, and others to define the comparison logic yourself.

val array = ComplexDoubleArray(5) { ComplexDouble(it, it.toDouble() / 2) }

println(array)
// [0.0 + 0.0i, 1.0 + 0.0i, 2.0 + 1.0i, 3.0 + 1.0i, 4.0 + 2.0i]

println(array.filter { it.abs() > 2.0 })
// [2.0+(1.0)i, 3.0+(1.5)i, 4.0+(2.0)i]

println(array.sortedByDescending { it.im })
// [4.0+(2.0)i, 3.0+(1.5)i, 2.0+(1.0)i, 1.0+(0.5)i, 0.0+(0.0)i]

After supporting complex numbers and arrays of complex numbers, we added them to multidimensional arrays.

println(mk.d2arrayIndices(2, 2) { i, j -> ComplexFloat(i, j) })
/*
[[0.0+(0.0)i, 0.0+(1.0)i],
[1.0+(0.0)i, 1.0+(1.0)i]]
 */

println(mk.empty<ComplexDouble, D1>(5))
// [0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i]

println(mk.ndarrayOf(ComplexDouble.zero, ComplexDouble.one))
//[0.0+(0.0)i, 1.0+(0.0)i]

You can do everything with them that you did with integer and float multidimensional arrays before.

val ndarray = mk.d2arrayIndices(3, 3) { i, j -> ComplexDouble(i, j) }
println(ndarray.map { it.re * it.re + it.im * it.im })
/*
[[0.0, 1.0, 4.0],
[1.0, 2.0, 5.0],
[4.0, 5.0, 8.0]]
 */

 println(mk.math.sin(ndarray))
 /*
[[0.0+(0.0)i, 0.0+(1.1752011936438014)i, 0.0+(3.626860407847019)i],
[0.8414709848078965+(0.0)i, 1.2984575814159773+(0.6349639147847361)i, 3.165778513216168+(1.9596010414216063)i],
[0.9092974268256817+(-0.0)i, 1.4031192506220405+(-0.4890562590412937)i, 3.4209548611170133+(-1.5093064853236158)i]]
*/

We still have several open tasks for complex numbers. You can participate in the development of the library and become a contributor.

LU factorization

LU factorization, or lower-upper decomposition, represents a matrix as the product of a permutation matrix, a lower triangular matrix, and an upper triangular matrix: A = P ✕ L ✕ U.

We have Introduced this in Multik.

Figure 1. PLU factorization.
val a = mk.d2array(3, 3) { it.toDouble() }
val (p, l, u) = mk.linalg.plu(a)

abs((p dot l dot u) - a).all { 1e-13 > it } // true

Solving linear systems

Solving systems of linear equations is a common problem encountered in algebra. With PLU factorization, we can easily solve square linear systems. To do this, we have provided a corresponding method in linalg.

val a = mk.ndarray(mk[mk[2, 5, 1], mk[7, 3, 1], mk[8, 9, 4]])
val b = mk.ndarray(mk[3, 1, 2])
mk.linalg.solve(a, b)
// [-0.036363636363636265, 0.9090909090909091, -1.4727272727272729]

Inverse matrix

The inverse matrix of a square matrix A, denoted A-1, is such that the following equality holds: A ✕ A-1 = A-1 ✕ A = I, where I is the square identity matrix. If A-1 is nonsingular, then it can be calculated by solving the above expression. For convenience, we have added this method out of the box.

val a = mk.ndarray(mk[mk[2.0, 5.0, 1.0], mk[7.0, 3.0, 1.0], mk[8.0, 9.0, 4.0]])
val ainv = mk.linalg.inv(a)
abs((a dot ainv) - mk.identity<Double>(3)).all { 1e-13 > it } // true

QR factorization

Another important decomposition is QR decomposition. We can decompose any square matrix A as a product Q ✕ R, where Q is an orthogonal matrix and R is an upper triangular matrix.

val a = mk.d2array(3, 3) { (it * it).toDouble() }
val (q, r) = mk.linalg.qr(a)
abs(a - (q dot r)).all { 1e-13 > it } //  true

Eigenvalues and eigenvectors

Based on the QR decomposition, we’ve made it possible to calculate eigenvalues and eigenvectors with Multik.

val a = mk.ndarray(mk[mk[1, -1], mk[1, 1]])
val (w, v) = mk.linalg.eig(a)
/*
w = [0.9999999999999998+(0.9999999999999998)i, 1.0+(-0.9999999999999998)i]

v = 
[[0.7071067811865476+(0.0)i, 0.7071067811865474+(0.0)i],
[-0.0+(-0.7071067811865475)i, -8.326672684688674E-17+(0.7071067811865474)i]]
*/

Append, stack, and meshgrid

In this release, we have added functions to help you work with multidimensional arrays more easily.

For example, you can use the append function to concatenate an array with scalars and other arrays. We’re grateful to our contributor Ansh Tyagi for implementing this feature.

Figure 2. Appending two vectors.
val a = mk.d1array(5) { it }
a.append(10) // [0, 1, 2, 3, 4, 10]
a.append(mk.ndarrayOf(9, 8, 7)) // [0, 1, 2, 3, 4, 9, 8, 7]

Append can also be used along the ndarray axes, which then becomes equivalent to cat function.

Figure 3. Appending two matrices along the axis.
val a = mk.d2array(2, 3) { it }
println(a.append(a, axis=0))
/*
[[0, 1, 2],
[3, 4, 5],
[0, 1, 2],
[3, 4, 5]]
*/

println(a.append(a, axis=1))
/*
[[0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5]]
*/

stack allows arrays to be concatenated along a new axis.

Figure 4. Stacking two vectors.
val a = mk.ndarrayOf(1, 2, 3)
println(mk.stack(a, a))
/*
[[1, 2, 3],
[1, 2, 3]]
*/

println(mk.stack(a, a, axis=1))
/*
[[1, 1],
[2, 2],
[3, 3]]
*/

And meshgrid allows you to get a grid of coordinates from vectors.

val x = mk.linspace<Double>(0, 1, 3)
val y = mk.linspace<Double>(0, 1, 4)
val (xg, yg) = mk.meshgrid(x, y)

println(xg)
/*
[[0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0]]
 */

println(yg)
/*
[[0.0, 0.0, 0.0],
 [0.3333333333333333, 0.3333333333333333, 0.3333333333333333],
 [0.6666666666666666, 0.6666666666666666, 0.6666666666666666],
 [1.0, 1.0, 1.0]]
 */

Conclusion

For more details about this new release, please check out the changelog.

We want to thank our users and contributors – you help us improve the library and make it more reliable and convenient!

To use Multik in your project, add the following dependencies to your build.gradle file:

repositories {
    mavenCentral()
}

dependencies {
    implementation "org.jetbrains.kotlinx:multik-api:0.1.0"
    implementation "org.jetbrains.kotlinx:multik-default:0.1.0"
}

You can use different engines, not just the default. We have multik-default, multik-native, and multik-jvm. Please note that on Android we only support the JVM engine.

Alternatively, you can start using Multik in Jupyter or Datalore:

%use multik

If you don’t have the kotlin-kernel for Jupyter, you can read about installation here. In Datalore, everything works out of the box.

We’re looking for contributions from the community, so please don’t hesitate to join the effort! Current tasks can always be found in the issue tracker.

Try out Multik 0.1 and share your experience with us!

Let’s Kotlin!

Continue ReadingMultik 0.1 Is Out

Multik 0.1 Is Out

Introducing Multik 0.1 – a new, enhanced version of our multidimensional array library! You can check out the previous post to learn about the basic features and architecture of the library.

In the new release, we added new methods from linear algebra, supported complex numbers and reading/writing .csv files, improved the performance and stability of existing functions, and added many more features that will make it easier for you to work with multidimensional arrays.

Multik on GitHub

Let’s take a look at the new features this release brings to the API:

Reading and writing CSV files

CSV is a popular data recording format, and now Multik 0.1 allows you to read and write .csv files easily.

val a = mk.d2array(2, 2) { it }
mk.write("example.csv", a)
val b = mk.read<Int, D2>("example.csv")
println(b)
/*
[[0, 1],
[2, 3]]
*/

Complex numbers

Complex numbers are expressed as re + i ∙ im, where re and im are real numbers and i is the imaginary unit equal to the square root of -1; re is the real part of the complex number and i ∙ im is the imaginary part. Complex numbers are common in algebra and are now a part of the Multik API.

val cf = ComplexFloat(1f, 2f) // 1.0+(2.0)i
val cd = ComplexDouble.one // 1.0+(0.0)i

The ComplexFloat and ComplexDouble classes are created in full accordance with the Number classes, so there is no need to learn a new API.

val c1 = ComplexFloat(1f, 2f) // 1.0+(2.0)i
val c2 = ComplexFloat(3f, 4f) // 3.0+(4.0)i

println(c1 + c2) // 4.0+(6.0)i
println(c1 * c2) // -5.0+(10.0)i

val (re, im) = c1 // re = 1.0; im = 2.0
val re2 = c2.re // 3.0
val im2 = c2.im // 4.0

We’ve also added ComplexFloatArrays and ComplexDoubleArrays. They support all methods that primitive arrays do, except for methods that use comparison, since complex numbers are unordered. But you can use methods such as minBy, maxBy, sortedBy, and others to define the comparison logic yourself.

val array = ComplexDoubleArray(5) { ComplexDouble(it, it.toDouble() / 2) }

println(array)
// [0.0 + 0.0i, 1.0 + 0.0i, 2.0 + 1.0i, 3.0 + 1.0i, 4.0 + 2.0i]

println(array.filter { it.abs() > 2.0 })
// [2.0+(1.0)i, 3.0+(1.5)i, 4.0+(2.0)i]

println(array.sortedByDescending { it.im })
// [4.0+(2.0)i, 3.0+(1.5)i, 2.0+(1.0)i, 1.0+(0.5)i, 0.0+(0.0)i]

After supporting complex numbers and arrays of complex numbers, we added them to multidimensional arrays.

println(mk.d2arrayIndices(2, 2) { i, j -> ComplexFloat(i, j) })
/*
[[0.0+(0.0)i, 0.0+(1.0)i],
[1.0+(0.0)i, 1.0+(1.0)i]]
 */

println(mk.empty<ComplexDouble, D1>(5))
// [0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i]

println(mk.ndarrayOf(ComplexDouble.zero, ComplexDouble.one))
//[0.0+(0.0)i, 1.0+(0.0)i]

You can do everything with them that you did with integer and float multidimensional arrays before.

val ndarray = mk.d2arrayIndices(3, 3) { i, j -> ComplexDouble(i, j) }
println(ndarray.map { it.re * it.re + it.im * it.im })
/*
[[0.0, 1.0, 4.0],
[1.0, 2.0, 5.0],
[4.0, 5.0, 8.0]]
 */

 println(mk.math.sin(ndarray))
 /*
[[0.0+(0.0)i, 0.0+(1.1752011936438014)i, 0.0+(3.626860407847019)i],
[0.8414709848078965+(0.0)i, 1.2984575814159773+(0.6349639147847361)i, 3.165778513216168+(1.9596010414216063)i],
[0.9092974268256817+(-0.0)i, 1.4031192506220405+(-0.4890562590412937)i, 3.4209548611170133+(-1.5093064853236158)i]]
*/

We still have several open tasks for complex numbers. You can participate in the development of the library and become a contributor.

LU factorization

LU factorization, or lower-upper decomposition, represents a matrix as the product of a permutation matrix, a lower triangular matrix, and an upper triangular matrix: A = P ✕ L ✕ U.

We have Introduced this in Multik.

Figure 1. PLU factorization.
val a = mk.d2array(3, 3) { it.toDouble() }
val (p, l, u) = mk.linalg.plu(a)

abs((p dot l dot u) - a).all { 1e-13 > it } // true

Solving linear systems

Solving systems of linear equations is a common problem encountered in algebra. With PLU factorization, we can easily solve square linear systems. To do this, we have provided a corresponding method in linalg.

val a = mk.ndarray(mk[mk[2, 5, 1], mk[7, 3, 1], mk[8, 9, 4]])
val b = mk.ndarray(mk[3, 1, 2])
mk.linalg.solve(a, b)
// [-0.036363636363636265, 0.9090909090909091, -1.4727272727272729]

Inverse matrix

The inverse matrix of a square matrix A, denoted A-1, is such that the following equality holds: A ✕ A-1 = A-1 ✕ A = I, where I is the square identity matrix. If A-1 is nonsingular, then it can be calculated by solving the above expression. For convenience, we have added this method out of the box.

val a = mk.ndarray(mk[mk[2.0, 5.0, 1.0], mk[7.0, 3.0, 1.0], mk[8.0, 9.0, 4.0]])
val ainv = mk.linalg.inv(a)
abs((a dot ainv) - mk.identity<Double>(3)).all { 1e-13 > it } // true

QR factorization

Another important decomposition is QR decomposition. We can decompose any square matrix A as a product Q ✕ R, where Q is an orthogonal matrix and R is an upper triangular matrix.

val a = mk.d2array(3, 3) { (it * it).toDouble() }
val (q, r) = mk.linalg.qr(a)
abs(a - (q dot r)).all { 1e-13 > it } //  true

Eigenvalues and eigenvectors

Based on the QR decomposition, we’ve made it possible to calculate eigenvalues and eigenvectors with Multik.

val a = mk.ndarray(mk[mk[1, -1], mk[1, 1]])
val (w, v) = mk.linalg.eig(a)
/*
w = [0.9999999999999998+(0.9999999999999998)i, 1.0+(-0.9999999999999998)i]

v = 
[[0.7071067811865476+(0.0)i, 0.7071067811865474+(0.0)i],
[-0.0+(-0.7071067811865475)i, -8.326672684688674E-17+(0.7071067811865474)i]]
*/

Append, stack, and meshgrid

In this release, we have added functions to help you work with multidimensional arrays more easily.

For example, you can use the append function to concatenate an array with scalars and other arrays. We’re grateful to our contributor Ansh Tyagi for implementing this feature.

Figure 2. Appending two vectors.
val a = mk.d1array(5) { it }
a.append(10) // [0, 1, 2, 3, 4, 10]
a.append(mk.ndarrayOf(9, 8, 7)) // [0, 1, 2, 3, 4, 9, 8, 7]

Append can also be used along the ndarray axes, which then becomes equivalent to cat function.

Figure 3. Appending two matrices along the axis.
val a = mk.d2array(2, 3) { it }
println(a.append(a, axis=0))
/*
[[0, 1, 2],
[3, 4, 5],
[0, 1, 2],
[3, 4, 5]]
*/

println(a.append(a, axis=1))
/*
[[0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5]]
*/

stack allows arrays to be concatenated along a new axis.

Figure 4. Stacking two vectors.
val a = mk.ndarrayOf(1, 2, 3)
println(mk.stack(a, a))
/*
[[1, 2, 3],
[1, 2, 3]]
*/

println(mk.stack(a, a, axis=1))
/*
[[1, 1],
[2, 2],
[3, 3]]
*/

And meshgrid allows you to get a grid of coordinates from vectors.

val x = mk.linspace<Double>(0, 1, 3)
val y = mk.linspace<Double>(0, 1, 4)
val (xg, yg) = mk.meshgrid(x, y)

println(xg)
/*
[[0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0]]
 */

println(yg)
/*
[[0.0, 0.0, 0.0],
 [0.3333333333333333, 0.3333333333333333, 0.3333333333333333],
 [0.6666666666666666, 0.6666666666666666, 0.6666666666666666],
 [1.0, 1.0, 1.0]]
 */

Conclusion

For more details about this new release, please check out the changelog.

We want to thank our users and contributors – you help us improve the library and make it more reliable and convenient!

To use Multik in your project, add the following dependencies to your build.gradle file:

repositories {
    mavenCentral()
}

dependencies {
    implementation "org.jetbrains.kotlinx:multik-api:0.1.0"
    implementation "org.jetbrains.kotlinx:multik-default:0.1.0"
}

You can use different engines, not just the default. We have multik-default, multik-native, and multik-jvm. Please note that on Android we only support the JVM engine.

Alternatively, you can start using Multik in Jupyter or Datalore:

%use multik

If you don’t have the kotlin-kernel for Jupyter, you can read about installation here. In Datalore, everything works out of the box.

We’re looking for contributions from the community, so please don’t hesitate to join the effort! Current tasks can always be found in the issue tracker.

Try out Multik 0.1 and share your experience with us!

Let’s Kotlin!

Continue ReadingMultik 0.1 Is Out

Multik 0.1 Is Out

Introducing Multik 0.1 – a new, enhanced version of our multidimensional array library! You can check out the previous post to learn about the basic features and architecture of the library.

In the new release, we added new methods from linear algebra, supported complex numbers and reading/writing .csv files, improved the performance and stability of existing functions, and added many more features that will make it easier for you to work with multidimensional arrays.

Multik on GitHub

Let’s take a look at the new features this release brings to the API:

Reading and writing CSV files

CSV is a popular data recording format, and now Multik 0.1 allows you to read and write .csv files easily.

val a = mk.d2array(2, 2) { it }
mk.write("example.csv", a)
val b = mk.read<Int, D2>("example.csv")
println(b)
/*
[[0, 1],
[2, 3]]
*/

Complex numbers

Complex numbers are expressed as re + i ∙ im, where re and im are real numbers and i is the imaginary unit equal to the square root of -1; re is the real part of the complex number and i ∙ im is the imaginary part. Complex numbers are common in algebra and are now a part of the Multik API.

val cf = ComplexFloat(1f, 2f) // 1.0+(2.0)i
val cd = ComplexDouble.one // 1.0+(0.0)i

The ComplexFloat and ComplexDouble classes are created in full accordance with the Number classes, so there is no need to learn a new API.

val c1 = ComplexFloat(1f, 2f) // 1.0+(2.0)i
val c2 = ComplexFloat(3f, 4f) // 3.0+(4.0)i

println(c1 + c2) // 4.0+(6.0)i
println(c1 * c2) // -5.0+(10.0)i

val (re, im) = c1 // re = 1.0; im = 2.0
val re2 = c2.re // 3.0
val im2 = c2.im // 4.0

We’ve also added ComplexFloatArrays and ComplexDoubleArrays. They support all methods that primitive arrays do, except for methods that use comparison, since complex numbers are unordered. But you can use methods such as minBy, maxBy, sortedBy, and others to define the comparison logic yourself.

val array = ComplexDoubleArray(5) { ComplexDouble(it, it.toDouble() / 2) }

println(array)
// [0.0 + 0.0i, 1.0 + 0.0i, 2.0 + 1.0i, 3.0 + 1.0i, 4.0 + 2.0i]

println(array.filter { it.abs() > 2.0 })
// [2.0+(1.0)i, 3.0+(1.5)i, 4.0+(2.0)i]

println(array.sortedByDescending { it.im })
// [4.0+(2.0)i, 3.0+(1.5)i, 2.0+(1.0)i, 1.0+(0.5)i, 0.0+(0.0)i]

After supporting complex numbers and arrays of complex numbers, we added them to multidimensional arrays.

println(mk.d2arrayIndices(2, 2) { i, j -> ComplexFloat(i, j) })
/*
[[0.0+(0.0)i, 0.0+(1.0)i],
[1.0+(0.0)i, 1.0+(1.0)i]]
 */

println(mk.empty<ComplexDouble, D1>(5))
// [0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i, 0.0+(0.0)i]

println(mk.ndarrayOf(ComplexDouble.zero, ComplexDouble.one))
//[0.0+(0.0)i, 1.0+(0.0)i]

You can do everything with them that you did with integer and float multidimensional arrays before.

val ndarray = mk.d2arrayIndices(3, 3) { i, j -> ComplexDouble(i, j) }
println(ndarray.map { it.re * it.re + it.im * it.im })
/*
[[0.0, 1.0, 4.0],
[1.0, 2.0, 5.0],
[4.0, 5.0, 8.0]]
 */

 println(mk.math.sin(ndarray))
 /*
[[0.0+(0.0)i, 0.0+(1.1752011936438014)i, 0.0+(3.626860407847019)i],
[0.8414709848078965+(0.0)i, 1.2984575814159773+(0.6349639147847361)i, 3.165778513216168+(1.9596010414216063)i],
[0.9092974268256817+(-0.0)i, 1.4031192506220405+(-0.4890562590412937)i, 3.4209548611170133+(-1.5093064853236158)i]]
*/

We still have several open tasks for complex numbers. You can participate in the development of the library and become a contributor.

LU factorization

LU factorization, or lower-upper decomposition, represents a matrix as the product of a permutation matrix, a lower triangular matrix, and an upper triangular matrix: A = P ✕ L ✕ U.

We have Introduced this in Multik.

Figure 1. PLU factorization.
val a = mk.d2array(3, 3) { it.toDouble() }
val (p, l, u) = mk.linalg.plu(a)

abs((p dot l dot u) - a).all { 1e-13 > it } // true

Solving linear systems

Solving systems of linear equations is a common problem encountered in algebra. With PLU factorization, we can easily solve square linear systems. To do this, we have provided a corresponding method in linalg.

val a = mk.ndarray(mk[mk[2, 5, 1], mk[7, 3, 1], mk[8, 9, 4]])
val b = mk.ndarray(mk[3, 1, 2])
mk.linalg.solve(a, b)
// [-0.036363636363636265, 0.9090909090909091, -1.4727272727272729]

Inverse matrix

The inverse matrix of a square matrix A, denoted A-1, is such that the following equality holds: A ✕ A-1 = A-1 ✕ A = I, where I is the square identity matrix. If A-1 is nonsingular, then it can be calculated by solving the above expression. For convenience, we have added this method out of the box.

val a = mk.ndarray(mk[mk[2.0, 5.0, 1.0], mk[7.0, 3.0, 1.0], mk[8.0, 9.0, 4.0]])
val ainv = mk.linalg.inv(a)
abs((a dot ainv) - mk.identity<Double>(3)).all { 1e-13 > it } // true

QR factorization

Another important decomposition is QR decomposition. We can decompose any square matrix A as a product Q ✕ R, where Q is an orthogonal matrix and R is an upper triangular matrix.

val a = mk.d2array(3, 3) { (it * it).toDouble() }
val (q, r) = mk.linalg.qr(a)
abs(a - (q dot r)).all { 1e-13 > it } //  true

Eigenvalues and eigenvectors

Based on the QR decomposition, we’ve made it possible to calculate eigenvalues and eigenvectors with Multik.

val a = mk.ndarray(mk[mk[1, -1], mk[1, 1]])
val (w, v) = mk.linalg.eig(a)
/*
w = [0.9999999999999998+(0.9999999999999998)i, 1.0+(-0.9999999999999998)i]

v = 
[[0.7071067811865476+(0.0)i, 0.7071067811865474+(0.0)i],
[-0.0+(-0.7071067811865475)i, -8.326672684688674E-17+(0.7071067811865474)i]]
*/

Append, stack, and meshgrid

In this release, we have added functions to help you work with multidimensional arrays more easily.

For example, you can use the append function to concatenate an array with scalars and other arrays. We’re grateful to our contributor Ansh Tyagi for implementing this feature.

Figure 2. Appending two vectors.
val a = mk.d1array(5) { it }
a.append(10) // [0, 1, 2, 3, 4, 10]
a.append(mk.ndarrayOf(9, 8, 7)) // [0, 1, 2, 3, 4, 9, 8, 7]

Append can also be used along the ndarray axes, which then becomes equivalent to cat function.

Figure 3. Appending two matrices along the axis.
val a = mk.d2array(2, 3) { it }
println(a.append(a, axis=0))
/*
[[0, 1, 2],
[3, 4, 5],
[0, 1, 2],
[3, 4, 5]]
*/

println(a.append(a, axis=1))
/*
[[0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5]]
*/

stack allows arrays to be concatenated along a new axis.

Figure 4. Stacking two vectors.
val a = mk.ndarrayOf(1, 2, 3)
println(mk.stack(a, a))
/*
[[1, 2, 3],
[1, 2, 3]]
*/

println(mk.stack(a, a, axis=1))
/*
[[1, 1],
[2, 2],
[3, 3]]
*/

And meshgrid allows you to get a grid of coordinates from vectors.

val x = mk.linspace<Double>(0, 1, 3)
val y = mk.linspace<Double>(0, 1, 4)
val (xg, yg) = mk.meshgrid(x, y)

println(xg)
/*
[[0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0],
 [0.0, 0.5, 1.0]]
 */

println(yg)
/*
[[0.0, 0.0, 0.0],
 [0.3333333333333333, 0.3333333333333333, 0.3333333333333333],
 [0.6666666666666666, 0.6666666666666666, 0.6666666666666666],
 [1.0, 1.0, 1.0]]
 */

Conclusion

For more details about this new release, please check out the changelog.

We want to thank our users and contributors – you help us improve the library and make it more reliable and convenient!

To use Multik in your project, add the following dependencies to your build.gradle file:

repositories {
    mavenCentral()
}

dependencies {
    implementation "org.jetbrains.kotlinx:multik-api:0.1.0"
    implementation "org.jetbrains.kotlinx:multik-default:0.1.0"
}

You can use different engines, not just the default. We have multik-default, multik-native, and multik-jvm. Please note that on Android we only support the JVM engine.

Alternatively, you can start using Multik in Jupyter or Datalore:

%use multik

If you don’t have the kotlin-kernel for Jupyter, you can read about installation here. In Datalore, everything works out of the box.

We’re looking for contributions from the community, so please don’t hesitate to join the effort! Current tasks can always be found in the issue tracker.

Try out Multik 0.1 and share your experience with us!

Let’s Kotlin!

Continue ReadingMultik 0.1 Is Out

End of content

No more pages to load