diff --git a/samples/settings.gradle b/samples/settings.gradle
index ad365fb16bc..cdb2f7b5ef6 100644
--- a/samples/settings.gradle
+++ b/samples/settings.gradle
@@ -10,6 +10,7 @@ include ':opengl'
include ':socket'
include ':tetris'
include ':tensorflow'
+include ':torch'
// Android native activity build requires Android SDK.
// So temporary switching off for now, as it breaks the build
// of other samples if SDK is not present.
diff --git a/samples/torch/README.md b/samples/torch/README.md
new file mode 100644
index 00000000000..7e465f69502
--- /dev/null
+++ b/samples/torch/README.md
@@ -0,0 +1,47 @@
+# Torch demo
+
+Trains a handwritten digit classifier using the [Torch](http://torch.ch) C backend.
+Like other Torch clients, most prominently [PyTorch](http://pytorch.org),
+this example is built on top of the
+[ATen C API](https://github.com/pytorch/pytorch/tree/master/aten),
+showing how a Torch client for Kotlin/Native could look like.
+
+## Installation
+
+To build [ATen (Torch for C)](https://github.com/pytorch/pytorch/tree/master/aten),
+make sure you have Python 2.X and pyyaml installed:
+
+ # macOS: if you don't have pip
+ sudo easy_install pip
+ # Linux: if you don't have pip
+ apt-get -y install python-pip
+
+ # if you don't have pyyaml
+ sudo pip install pyyaml
+
+Now
+
+ ./downloadTorch.sh
+
+will install it into `$HOME/.konan/third-party/torch` (if not yet done).
+
+To build use `../gradlew build` or `./build.sh`.
+
+ ./downloadMNIST.sh
+
+will download and unzip the [MNIST dataset](https://en.wikipedia.org/wiki/MNIST_database) of
+[70000 labeled handwritten digits](http://yann.lecun.com/exdb/mnist/) for training and testing a classifier.
+
+Then run
+
+ ../gradlew run
+
+Alternatively you can run the artifact directly through
+
+ ./build/konan/bin/macbook/HelloTorch.kexe
+
+You may need to specify `LD_LIBRARY_PATH` or `DYLD_LIBRARY_PATH` to `$HOME/.konan/third-party/torch/lib`
+if the ATen dynamic library cannot be found.
+
+Even on a CPU, training should only take some minutes,
+and you should observe a classification accuracy of about 95% on the test dataset.
\ No newline at end of file
diff --git a/samples/torch/build.gradle b/samples/torch/build.gradle
new file mode 100644
index 00000000000..1c5b980bb6d
--- /dev/null
+++ b/samples/torch/build.gradle
@@ -0,0 +1,34 @@
+apply plugin: 'konan'
+
+konan.targets = ['macbook', 'linux']
+
+def torchHome = "${System.getProperty("user.home")}/.konan/third-party/torch"
+
+task downloadTorch(type: Exec) {
+ workingDir getProjectDir()
+ commandLine './downloadTorch.sh'
+}
+
+konanArtifacts {
+ interop('TorchInterop') {
+ defFile "src/main/c_interop/torch.def"
+ includeDirs "${torchHome}/include", "${torchHome}/include/TH"
+ dependsOn 'downloadTorch'
+ }
+
+ program('Torch') {
+ libraries {
+ artifact 'TorchInterop'
+ }
+ linkerOpts "-L${torchHome}/lib -lATen"
+ }
+}
+
+run.dependsOn 'warning'
+
+task warning {
+ doLast {
+ println "Note: You may need to specify LD_LIBRARY_PATH or DYLD_LIBRARY_PATH env variables to $torchHome/lib if the ATen dynamic library cannot be found."
+
+ }
+}
\ No newline at end of file
diff --git a/samples/torch/build.sh b/samples/torch/build.sh
new file mode 100755
index 00000000000..917436eeb4d
--- /dev/null
+++ b/samples/torch/build.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+
+DIR=$(cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd )
+
+source "$DIR/../konan.sh"
+
+$DIR/downloadTorch.sh
+
+TH_TARGET_DIRECTORY="$HOME/.konan/third-party/torch"
+
+if [ x$TARGET == x ]; then
+case "$OSTYPE" in
+ darwin*) TARGET=macbook; TF_TARGET=darwin ;;
+ linux*) TARGET=linux; TF_TARGET=linux ;;
+ *) echo "unknown: $OSTYPE" && exit 1;;
+esac
+fi
+
+CFLAGS_macbook="-I${TH_TARGET_DIRECTORY}/include"
+CFLAGS_linux="-I${TH_TARGET_DIRECTORY}/include"
+
+var=CFLAGS_${TARGET}
+CFLAGS=${!var}
+var=LINKER_ARGS_${TARGET}
+LINKER_ARGS=${!var}
+var=COMPILER_ARGS_${TARGET}
+COMPILER_ARGS=${!var} # add -opt for an optimized build.
+
+mkdir -p $DIR/build/c_interop/
+mkdir -p $DIR/build/bin/
+
+cinterop -def $DIR/src/main/c_interop/torch.def -compilerOpts "$CFLAGS" -labels $TARGET \
+ -copt -I$TH_TARGET_DIRECTORY/include/TH -o $DIR/build/c_interop/torch || exit 1
+
+SOURCE_DIR=$DIR/src/main/kotlin
+
+konanc $COMPILER_ARGS -target $TARGET $SOURCE_DIR/ClassifierDemo.kt $SOURCE_DIR/Disposable.kt \
+ $SOURCE_DIR/Tensors.kt $SOURCE_DIR/Modules.kt $SOURCE_DIR/Dataset.kt $SOURCE_DIR/SmallDemos.kt \
+ -library $DIR/build/c_interop/torch \
+ -o $DIR/build/bin/HelloTorch \
+ -linkerOpts "-L$TH_TARGET_DIRECTORY/lib -lATen" || exit 1
+
+echo "Note: You may need to specify LD_LIBRARY_PATH or DYLD_LIBRARY_PATH env variables to $TH_TARGET_DIRECTORY/lib if the ATen dynamic library cannot be found."
+
+echo "Artifact path is $DIR/build/bin/HelloTorch.kexe"
diff --git a/samples/torch/downloadMNIST.sh b/samples/torch/downloadMNIST.sh
new file mode 100644
index 00000000000..5519d38510c
--- /dev/null
+++ b/samples/torch/downloadMNIST.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+# See http://yann.lecun.com/exdb/mnist/
+
+wget http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz
+wget http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz
+wget http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz
+wget http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz
+
+gzip -d train-images-idx3-ubyte.gz
+gzip -d train-labels-idx1-ubyte.gz
+gzip -d t10k-images-idx3-ubyte.gz
+gzip -d t10k-labels-idx1-ubyte.gz
\ No newline at end of file
diff --git a/samples/torch/downloadTorch.sh b/samples/torch/downloadTorch.sh
new file mode 100755
index 00000000000..b8ddee3fd93
--- /dev/null
+++ b/samples/torch/downloadTorch.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+TH_TARGET_DIRECTORY=~/.konan/third-party/torch
+NO_CUDA=true # set to false for GPU support
+
+if [ ! -d $TH_TARGET_DIRECTORY/include/THNN ]; then
+ git clone https://github.com/pytorch/pytorch.git
+
+ mkdir build_torch
+ cd build_torch
+
+ cmake -DNO_CUDA=$NO_CUDA ../pytorch/aten
+ make
+ make DESTDIR=$TH_TARGET_DIRECTORY install
+
+ cd $TH_TARGET_DIRECTORY
+
+ # remove 'usr/local' prefix produced by make:
+ mv usr/local/* .
+ rm -d usr/local usr
+
+ # hack to solve "fatal error: 'generic/THNN.h' file not found" when linking, -I$
/include/THNN did not work
+ cp include/THNN/generic/THNN.h include/TH/generic/THNN.h
+fi
\ No newline at end of file
diff --git a/samples/torch/src/main/c_interop/torch.def b/samples/torch/src/main/c_interop/torch.def
new file mode 100644
index 00000000000..958c948be70
--- /dev/null
+++ b/samples/torch/src/main/c_interop/torch.def
@@ -0,0 +1 @@
+headers = TH/TH.h THNN/THNN.h
diff --git a/samples/torch/src/main/kotlin/ClassifierDemo.kt b/samples/torch/src/main/kotlin/ClassifierDemo.kt
new file mode 100644
index 00000000000..eb63f283a0a
--- /dev/null
+++ b/samples/torch/src/main/kotlin/ClassifierDemo.kt
@@ -0,0 +1,74 @@
+fun Float.toRoundedString(digits: Int = 0): String {
+ var factor = 1
+
+ for (i in 0 until digits) {
+ factor *= 10
+ }
+
+ return (kotlin.math.round(this * factor) / factor).toString()
+}
+
+fun Float.toPercentageString(roundToDigits: Int = 1) = (this * 100).toRoundedString(roundToDigits)
+
+fun List.maxIndex() = withIndex().maxBy { it.value }!!.index
+
+fun accuracy(predictionBatch: FloatMatrix, labelBatch: FloatMatrix): Float {
+ val resultIndexes = predictionBatch.toList().map { it.maxIndex() }
+ val labelBatchIndexes = labelBatch.toList().map { it.maxIndex() }
+ return resultIndexes.zip(labelBatchIndexes).
+ count { (result, label) -> result == label }.toFloat() / resultIndexes.size
+}
+
+fun Backpropagatable.trainClassifier(
+ dataset: Dataset,
+ lossByLabels: (FloatMatrix) -> Backpropagatable,
+ learningRateByProgress: (Float) -> Float = { 5f * kotlin.math.exp(-it * 3) },
+ batchSize: Int = 64,
+ iterations: Int = 500) {
+
+ for (i in 0 until iterations) {
+ disposeScoped {
+ val (inputBatch, labelBatch) = dataset.sampleBatch(batchSize)
+ val errorNetwork = this@trainClassifier before lossByLabels(labelBatch)
+ val forwardResults = use { errorNetwork.forwardPass(inputBatch) }
+ val accuracy = accuracy(forwardResults.hidden, labelBatch)
+ val progress = i.toFloat() / iterations
+ val learningRate = learningRateByProgress(progress)
+ val backpropResults = use { forwardResults.backpropagate(outputGradient = tensor(learningRate)) }
+ val crossEntropy = forwardResults.output[0]
+ backpropResults.descend()
+ println("Iteration ${i + 1}/$iterations: " +
+ "${accuracy.toPercentageString()}% training batch accuracy, " +
+ "cross entropy loss = ${crossEntropy.toRoundedString(4)}, " +
+ "learning rate = ${learningRate.toRoundedString(4)}")
+ }
+ }
+}
+
+fun Backpropagatable.testClassifier(dataset: Dataset, batchSize: Int = 100): Float {
+ val testBatches = dataset.testBatches(batchSize)
+ return testBatches.withIndex().map { (i, batchPair) ->
+ val (inputBatch, outputBatch) = batchPair
+ val accuracy = accuracy(this.forwardPass(inputBatch).output, outputBatch)
+ println("Test batch ${i + 1}/${testBatches.size}: ${accuracy.toPercentageString()}% accuracy")
+ accuracy * inputBatch.shape[0]
+ }.sum() / dataset.inputs.size
+}
+
+fun randomInit(size: Int) = random(-.01, .01, size)
+fun randomInit(size0: Int, size1: Int) = random(-.1, .1, size0, size1)
+
+fun linear(inputSize: Int, outputSize: Int) = Linear(randomInit(outputSize, inputSize), randomInit(outputSize))
+fun twoLayerClassifier(dataset: Dataset, hiddenSize: Int = 64) =
+ linear(dataset.inputs[0].size, hiddenSize) before Relu before
+ linear(hiddenSize, dataset.labels[0].size) before Softmax
+
+fun main(args: Array) {
+ val trainingDataset = MNIST.labeledTrainingImages()
+ val predictionNetwork = twoLayerClassifier(trainingDataset)
+ predictionNetwork.trainClassifier(trainingDataset, lossByLabels = { CrossEntropyLoss(labels = it) })
+
+ val testDataset = MNIST.labeledTestImages()
+ val averageAccuracy = predictionNetwork.testClassifier(testDataset)
+ println("Accuracy on the test set: ${averageAccuracy.toPercentageString()}")
+}
\ No newline at end of file
diff --git a/samples/torch/src/main/kotlin/Dataset.kt b/samples/torch/src/main/kotlin/Dataset.kt
new file mode 100644
index 00000000000..2f97b25d793
--- /dev/null
+++ b/samples/torch/src/main/kotlin/Dataset.kt
@@ -0,0 +1,96 @@
+import kotlinx.cinterop.*
+import platform.posix.*
+
+data class Dataset(val inputs: List, val labels: List) {
+ fun batch(indices: List): Pair {
+ val inputBatch = tensor(*(indices.map { inputs[it].toTypedArray() }.toTypedArray()))
+ val labelBatch = tensor(*(indices.map { labels[it].toTypedArray() }.toTypedArray()))
+ return inputBatch to labelBatch
+ }
+
+ fun sampleBatch(batchSize: Int) = batch((0 until batchSize).map { randomInt(inputs.size) })
+ private fun batchAt(batchIndex: Int, batchSize: Int) =
+ batch((0 until inputs.size).drop(batchSize + batchIndex).take(batchSize))
+
+ fun testBatches(batchSize: Int) = (0 until inputs.size / batchSize).map { batchAt(it, batchSize = batchSize) }
+}
+
+/**
+ * Provides the MNIST labeled handwritten digit dataset, described at http://yann.lecun.com/exdb/mnist/
+ */
+object MNIST {
+ private fun readFileData(path: String) = memScoped {
+ fun fail(): Nothing = throw Error("Cannot read input file $path")
+
+ val size = alloc().also { if (stat(path, it.ptr) != 0) fail() }.st_size.toInt()
+
+ println("Reading $size bytes from $path...")
+
+ val file = fopen(path, "rb") ?: fail()
+ try {
+ ByteArray(size).also { fread(it.refTo(0), 1, size.signExtend(), file) }
+ } finally {
+ fclose(file)
+ }
+ }
+
+ private fun Byte.reinterpretAsUnsigned() = this.toInt().let { it + if (it < 0) 256 else 0 }
+
+ private fun unsignedBytesToInt(bytes: List) =
+ bytes.withIndex().map { (i, value) -> value.reinterpretAsUnsigned().shl(8 * (3 - i)) }.sum()
+
+ private val intSize = 4
+ private fun ByteArray.getIntAt(index: Int) =
+ unsignedBytesToInt((index until (index + intSize)).map { this[it] })
+
+ private val imageLength = 28
+ private val imageSize = imageLength * imageLength
+
+ private fun ByteArray.getImageAt(index: Int) =
+ FloatArray(imageSize) { this[index + it].reinterpretAsUnsigned().toFloat() / 255 }
+
+ private fun oneHot(size: Int, index: Int) = FloatArray(size) { if (it == index) 1f else 0f }
+
+ private fun readLabels(filePath: String, totalLabels: Int = 10): List {
+ val data = readFileData(filePath)
+ val check = data.getIntAt(0)
+ val expectedCheck = 2049
+ if (check != 2049) throw Error("File should start with int $expectedCheck, but was $check.")
+
+ val count = data.getIntAt(4)
+
+ val offset = 8
+
+ if (count + offset != data.size) throw Error("Unexpected file size: ${data.size}.")
+
+ return (0 until count).map { oneHot(totalLabels, index = data[offset + it].reinterpretAsUnsigned()) }
+ }
+
+ private fun readImages(filePath: String): List {
+ val data = readFileData(filePath)
+ val check = data.getIntAt(0)
+ val expectedCheck = 2051
+ if (check != expectedCheck) throw Error("File should start with int $expectedCheck, but was $check.")
+
+ val count = data.getIntAt(4)
+ val width = data.getIntAt(8)
+ val height = data.getIntAt(12)
+
+ val offset = 16
+
+ if (width != imageLength) throw Error()
+ if (height != imageLength) throw Error()
+
+ if (count * imageSize + offset != data.size) throw Error("Unexpected file size: ${data.size}.")
+
+ return (0 until count).map { data.getImageAt(offset + imageSize * it) }
+ }
+
+ fun labeledTrainingImages() = Dataset(
+ inputs = readImages("train-images-idx3-ubyte"),
+ labels = readLabels("train-labels-idx1-ubyte"))
+
+ fun labeledTestImages() = Dataset(
+ inputs = readImages("t10k-images-idx3-ubyte"),
+ labels = readLabels("t10k-labels-idx1-ubyte"))
+}
\ No newline at end of file
diff --git a/samples/torch/src/main/kotlin/Disposable.kt b/samples/torch/src/main/kotlin/Disposable.kt
new file mode 100644
index 00000000000..e29e66b1238
--- /dev/null
+++ b/samples/torch/src/main/kotlin/Disposable.kt
@@ -0,0 +1,26 @@
+interface Disposable {
+ fun dispose()
+}
+
+open class DisposableContainer(private val disposables: MutableList = ArrayList()) : Disposable {
+ /**
+ * Creates the object and schedules its disposal for the end of the scope.
+ */
+ fun use(create: () -> T) = create().also { disposables.add(it) }
+
+ override fun dispose() {
+ for (disposable in disposables) {
+ disposable.dispose()
+ }
+ }
+}
+
+fun disposeScoped(action: DisposableContainer.() -> T): T {
+ val scope = DisposableContainer()
+
+ try {
+ return scope.action()
+ } finally {
+ scope.dispose()
+ }
+}
\ No newline at end of file
diff --git a/samples/torch/src/main/kotlin/Modules.kt b/samples/torch/src/main/kotlin/Modules.kt
new file mode 100644
index 00000000000..949e8d2a66b
--- /dev/null
+++ b/samples/torch/src/main/kotlin/Modules.kt
@@ -0,0 +1,205 @@
+import kotlinx.cinterop.*
+import torch.*
+
+// Defines network modules with the ability for backpropagation using both TH.h and THNN.h from the ATen library
+
+abstract class Backpropagatable {
+ abstract inner class ForwardResults(val input: Input) : DisposableContainer() {
+ init {
+ use { input }
+ }
+
+ abstract val output: Output
+ abstract fun backpropagate(outputGradient: Output): BackpropagationResults
+ }
+
+ abstract inner class BackpropagationResults(
+ val input: Input,
+ val output: Output,
+ val outputGradient: Output
+ ) : DisposableContainer() {
+ init {
+ use { input }
+ use { output }
+ use { outputGradient }
+ }
+
+ abstract val inputGradient: Input
+ abstract fun descend()
+ }
+
+ abstract fun forwardPass(input: Input): ForwardResults
+}
+
+abstract class Module : Backpropagatable() {
+ abstract var parameters: Parameters
+ abstract fun parametersToList(parameters: Parameters): List
+ abstract fun parametersFromList(list: List): Parameters
+ private val parameterList get() = parametersToList(parameters)
+
+ abstract operator fun invoke(input: Input): Output
+ abstract fun inputGradient(input: Input, outputGradient: Output, output: Output): Input
+ abstract fun parameterGradient(input: Input, outputGradient: Output, inputGradient: Input): Parameters
+
+ override fun forwardPass(input: Input) = object : ForwardResults(input) {
+ override val output = use { this@Module(input) }
+ override fun backpropagate(outputGradient: Output) =
+ object : Backpropagatable.BackpropagationResults(input, output, outputGradient) {
+ override val inputGradient = use { this@Module.inputGradient(input, outputGradient, output) }
+ val parameterGradient = this@Module.parameterGradient(input,
+ outputGradient = outputGradient, inputGradient = inputGradient)
+
+ override fun descend() = this@Module.descend(parameterGradient)
+ }
+ }
+
+ open fun descend(parameterGradient: Parameters) {
+ parameters = parametersFromList(parameterList.zip(
+ parametersToList(parameterGradient)) { parameter, gradient -> parameter - gradient })
+ }
+}
+
+abstract class ParameterFreeModule : Module() {
+ override var parameters = Unit
+ override fun parametersToList(parameters: Unit) = emptyList()
+ override fun parametersFromList(list: List) = Unit
+ override fun parameterGradient(input: Input, outputGradient: Output, inputGradient: Input) = Unit
+ override fun descend(parameterGradient: Unit) {}
+}
+
+class Chain(
+ val module1: Backpropagatable,
+ val module2: Backpropagatable
+) : Backpropagatable() {
+ override fun forwardPass(input: Input) = ChainForwardResults(input)
+
+ inner class ChainForwardResults(input: Input) : ForwardResults(input) {
+ val result1 = use { module1.forwardPass(input) }
+ val hidden = result1.output
+ val result2 = use { module2.forwardPass(result1.output) }
+ override val output = result2.output
+ override fun backpropagate(outputGradient: Output) =
+ object : Backpropagatable.BackpropagationResults(input, output, outputGradient) {
+ val backpropResults2 = use { result2.backpropagate(outputGradient) }
+ val hiddenGradient = backpropResults2.inputGradient
+ val backpropResults1 = use { result1.backpropagate(hiddenGradient) }
+
+ override val inputGradient = backpropResults1.inputGradient
+
+ override fun descend() {
+ backpropResults1.descend()
+ backpropResults2.descend()
+ }
+ }
+ }
+
+ override fun toString() = "$module1 before $module2"
+}
+
+infix fun Backpropagatable.before(
+ other: Backpropagatable) = Chain(this, other)
+
+object Abs : ParameterFreeModule() {
+ override operator fun invoke(input: FloatMatrix) = initializedTensor(input.shape[0], input.shape[1]) {
+ THNN_FloatAbs_updateOutput(cValuesOf(), input.raw, it.raw)
+ }
+
+ override fun inputGradient(input: FloatMatrix, outputGradient: FloatMatrix, output: FloatMatrix) =
+ initializedTensor(input.shape[0], input.shape[1]) {
+ THNN_FloatAbs_updateGradInput(null, input.raw, outputGradient.raw, it.raw)
+ }
+}
+
+object Relu : ParameterFreeModule() {
+ override operator fun invoke(input: FloatMatrix) = initializedTensor(input.shape[0], input.shape[1]) {
+ THNN_FloatLeakyReLU_updateOutput(null, input.raw, it.raw, 0.0, false)
+ }
+
+ override fun inputGradient(input: FloatMatrix, outputGradient: FloatMatrix, output: FloatMatrix) =
+ initializedTensor(input.shape[0], input.shape[1]) {
+ THNN_FloatLeakyReLU_updateGradInput(null, input.raw, outputGradient.raw, it.raw, 0.0, false)
+ }
+}
+
+object Softmax : ParameterFreeModule() {
+ override operator fun invoke(input: FloatMatrix) = initializedTensor(input.shape[0], input.shape[1]) {
+ THNN_FloatSoftMax_updateOutput(null, input.raw, it.raw, 1)
+ }
+
+ override fun inputGradient(input: FloatMatrix, outputGradient: FloatMatrix, output: FloatMatrix) =
+ initializedTensor(input.shape[0], input.shape[1]) {
+ THNN_FloatSoftMax_updateGradInput(null, input.raw, outputGradient.raw, it.raw, output.raw, 1)
+ }
+}
+
+class MeanSquaredError(val labels: FloatMatrix) : ParameterFreeModule() {
+ override operator fun invoke(input: FloatMatrix) = initializedTensor(1) {
+ THNN_FloatMSECriterion_updateOutput(null, input.raw, labels.raw, it.raw,
+ sizeAverage = true, reduce = true)
+ }
+
+ override fun inputGradient(
+ input: FloatMatrix,
+ outputGradient: FloatVector,
+ output: FloatVector
+ ) = initializedTensor(input.shape[0], input.shape[1]) {
+ THNN_FloatMSECriterion_updateGradInput(null, input.raw, labels.raw,
+ outputGradient.raw, it.raw, sizeAverage = true, reduce = true)
+ }
+}
+
+class CrossEntropyLoss(val labels: FloatMatrix) : ParameterFreeModule() {
+ override operator fun invoke(input: FloatMatrix) = initializedTensor(1) {
+ THNN_FloatBCECriterion_updateOutput(null, input.raw, labels.raw, it.raw,
+ sizeAverage = true, reduce = true, weights = null)
+ }
+
+ override fun inputGradient(input: FloatMatrix, outputGradient: FloatVector, output: FloatVector) =
+ initializedTensor(input.shape[0], input.shape[1]) {
+ THNN_FloatBCECriterion_updateGradInput(null, input.raw, labels.raw, outputGradient.raw,
+ it.raw, sizeAverage = true, reduce = true, weights = null)
+ }
+}
+
+data class Linear(
+ var weight: FloatMatrix,
+ var bias: FloatVector) : Module>() {
+ val inputSize = weight.shape[1]
+ val outputSize = weight.shape[0]
+ val addBuffer = uninitializedTensor(outputSize)
+
+ override operator fun invoke(input: FloatMatrix) = initializedTensor(input.shape[0], outputSize) {
+ THNN_FloatLinear_updateOutput(null, input.raw, it.raw, weight.raw, bias.raw, addBuffer.raw)
+ }
+
+ override fun inputGradient(input: FloatMatrix, outputGradient: FloatMatrix, output: FloatMatrix) =
+ initializedTensor(input.shape[0], inputSize) {
+ THNN_FloatLinear_updateGradInput(null, input.raw, outputGradient.raw, it.raw, weight.raw)
+ }
+
+ override fun parameterGradient(
+ input: FloatMatrix,
+ outputGradient: FloatMatrix,
+ inputGradient: FloatMatrix
+ ): Pair {
+ val biasGradient = zeros(outputSize)
+ val weightGradient = zeros(weight.shape[0], weight.shape[1]).also {
+ THNN_FloatLinear_accGradParameters(null, input.raw, outputGradient.raw, inputGradient.raw, weight.raw,
+ bias.raw, it.raw, biasGradient.raw, addBuffer.raw, 1.0)
+ }
+
+ return weightGradient to biasGradient
+ }
+
+ override var parameters: Pair
+ get() = weight to bias
+ set(value) {
+ weight = value.first
+ bias = value.second
+ }
+
+ override fun parametersToList(parameters: Pair) =
+ listOf(parameters.first, parameters.second)
+
+ override fun parametersFromList(list: List) = list.first().asMatrix() to list.last().asVector()
+}
\ No newline at end of file
diff --git a/samples/torch/src/main/kotlin/SmallDemos.kt b/samples/torch/src/main/kotlin/SmallDemos.kt
new file mode 100644
index 00000000000..9240c71e7e8
--- /dev/null
+++ b/samples/torch/src/main/kotlin/SmallDemos.kt
@@ -0,0 +1,55 @@
+// If you are curious you can also try one of these
+
+private fun demonstrateTensors() {
+ disposeScoped {
+ val x = use { tensor(0f, 1f, 2f) }
+ val y = use { tensor(0f, -1f, -2f) }
+ val m = use {
+ tensor(
+ arrayOf(1f, -1f, 0f),
+ arrayOf(0f, -1f, 0f),
+ arrayOf(0f, 0f, -.5f))
+ }
+
+ println("Hello, Torch!\nx = $x\ny = $y\n" +
+ "|x| = ${x.abs()}\n|y| = ${y.abs()}\n" +
+ "2x=${use { x * 2f }}\nx+y = ${use { x + y }}\nx-y = ${use { x - y }}\nxy = ${x * y}\n" +
+ "m=\n${use { m }}\nm·y = ${use { m * y }}\nm+m =\n${use { m + m }}\nm·m =\n${use { m * m }}")
+ }
+}
+
+private fun demonstrateModules() {
+ val input = tensor(arrayOf(-1f))
+ val abs = Abs(input)
+ println("abs of $input is $abs, gradient is ${Abs.inputGradient(input, tensor(arrayOf(1f)), abs)}")
+ val relu = Relu(input)
+ println("relu of $input is $relu, gradient is ${Relu.inputGradient(input, tensor(arrayOf(1f)), relu)}")
+}
+
+private fun demonstrateManualBackpropagationFor1LinearLayer(
+ inputs: FloatMatrix = tensor(arrayOf(1f, -1f), arrayOf(1f, -1f)),
+ labels: FloatMatrix = tensor(arrayOf(5f), arrayOf(5f)),
+ learningRate: Float = .1f) {
+ val linear = Linear(weight = randomInit(1, 2), bias = randomInit(1))
+ val error = MeanSquaredError(labels)
+
+ for (i in 0 until 100) {
+ disposeScoped {
+ val output = use { linear(inputs) }
+ val loss = use { error(output) }
+ val outputGradient = use { error.inputGradient(output, tensor(learningRate), loss) }
+ val inputGradient = use { linear.inputGradient(inputs, outputGradient, output) }
+ val parameterGradient = linear.parameterGradient(inputs, outputGradient, inputGradient).
+ also { use { it.first } }.also { use { it.second } }
+ println("input: $inputs, \n" +
+ "output: $output, \n" +
+ "labels: $labels, \n" +
+ "mean squared error: $loss, \n" +
+ "output gradient: $outputGradient, \n" +
+ "input gradient: $inputGradient, \n" +
+ "parameter gradient: $parameterGradient")
+ linear.weight -= parameterGradient.first
+ linear.bias -= parameterGradient.second
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/torch/src/main/kotlin/Tensors.kt b/samples/torch/src/main/kotlin/Tensors.kt
new file mode 100644
index 00000000000..6b8d2084c3f
--- /dev/null
+++ b/samples/torch/src/main/kotlin/Tensors.kt
@@ -0,0 +1,153 @@
+import kotlinx.cinterop.*
+import torch.*
+
+// Defines tensor classes and operations using TH.h from the ATen library
+
+abstract class FloatTensor(val raw: CPointer) : Disposable {
+ private val storage: CPointer get() = raw.pointed.storage!!
+ private val elements get() = storage.pointed
+ private val data: CPointer get() = elements.data!!
+ private val size: CPointer get() = raw.pointed.size!!
+ protected val nDimension: Int get() = raw.pointed.nDimension
+
+ val shape: List get() = (0 until nDimension).map { size[it].toInt() }
+
+ operator fun plus(other: FloatTensor) = initializedTensor(shape) {
+ THFloatTensor_cadd(it.raw, raw, 1f, other.raw)
+ }
+
+ operator fun minus(other: FloatTensor) = initializedTensor(shape) {
+ THFloatTensor_cadd(it.raw, raw, -1f, other.raw)
+ }
+
+ open operator fun times(factor: Float) = initializedTensor(shape) {
+ THFloatTensor_mul(it.raw, raw, factor)
+ }
+
+ fun sum() = THFloatTensor_sumall(raw)
+ fun flatten() = (0 until elements.size).map { data[it] }.toTypedArray()
+
+ override fun dispose() {
+ THFloatTensor_free(raw)
+ }
+
+ fun asVector() = FloatVector(raw)
+ fun asMatrix() = FloatMatrix(raw)
+ inline fun asTensor(): T = when (T::class) {
+ FloatVector::class -> asVector() as T
+ FloatMatrix::class -> asMatrix() as T
+ FloatTensor::class -> this as T
+ else -> throw Error("Unexpected class ${T::class}")
+ }
+
+ abstract override fun toString(): String
+}
+
+class FloatVector(raw: CPointer) : FloatTensor(raw) {
+ init {
+ if (super.nDimension != 1)
+ throw Error("A vector must have exactly 1 dimension.")
+ }
+
+ operator fun get(i: Int) = THFloatTensor_get1d(raw, i.signExtend())
+ operator fun set(i: Int, value: Float) = THFloatTensor_set1d(raw, i.signExtend(), value)
+ fun toArray() = (0 until shape[0]).map { i0 -> this[i0] }.toTypedArray()
+
+ operator fun plus(other: FloatVector) = super.plus(other).asVector()
+ operator fun minus(other: FloatVector) = super.minus(other).asVector()
+ override operator fun times(factor: Float) = super.times(factor).asVector()
+ operator fun times(other: FloatVector) = THFloatTensor_dot(raw, other.raw)
+
+ fun abs() = kotlin.math.sqrt(this * this)
+
+ override fun toString() = "[${toArray().joinToString { it.toString() }}]"
+}
+
+class FloatMatrix(raw: CPointer) : FloatTensor(raw) {
+ init {
+ if (super.nDimension != 2)
+ throw Error("A matrix must have exactly 2 dimensions.")
+ }
+
+ fun getRow(i0: Int) = (0 until shape[1]).map { i1 -> this[i0, i1] }
+ operator fun get(i0: Int, i1: Int) = THFloatTensor_get2d(raw, i0.signExtend(), i1.signExtend())
+ operator fun set(i0: Int, i1: Int, value: Float) = THFloatTensor_set2d(raw, i0.signExtend(), i1.signExtend(), value)
+ fun toList() = (0 until shape[0]).map { getRow(it) }
+
+ operator fun plus(other: FloatMatrix) = super.plus(other).asMatrix()
+ operator fun minus(other: FloatMatrix) = super.minus(other).asMatrix()
+ override operator fun times(factor: Float) = super.times(factor).asMatrix()
+
+ operator fun times(vector: FloatVector) = initializedTensor(shape[0]) {
+ THFloatTensor_addmv(it.raw, 0f, it.raw, 1f, raw, vector.raw)
+ }
+
+ operator fun times(matrix: FloatMatrix) = initializedTensor(shape[0], matrix.shape[1]) {
+ THFloatTensor_addmm(it.raw, 0f, it.raw, 1f, raw, matrix.raw)
+ }
+
+ override fun toString() = "[${toList().joinToString(",\n") { "[${it.joinToString { it.toString() }}]" }}]"
+}
+
+fun uninitializedTensor(size: Int) =
+ FloatVector(THFloatTensor_newWithSize1d(size.signExtend())!!)
+
+fun uninitializedTensor(size0: Int, size1: Int) =
+ FloatMatrix(THFloatTensor_newWithSize2d(size0.signExtend(), size1.signExtend())!!)
+
+fun uninitializedTensor(shape: List) = when (shape.size) {
+ 1 -> uninitializedTensor(shape.single())
+ 2 -> uninitializedTensor(shape[0], shape[1])
+ else -> throw Error("Tensors with ${shape.size} dimensions are not supported yet.")
+}
+
+fun initializedTensor(size: Int, initializer: (FloatVector) -> T) =
+ uninitializedTensor(size).apply { initializer(this) }
+
+fun initializedTensor(size0: Int, size1: Int, initializer: (FloatMatrix) -> T) =
+ uninitializedTensor(size0, size1).apply { initializer(this) }
+
+fun initializedTensor(shape: List, initializer: (FloatTensor) -> T) =
+ uninitializedTensor(shape).apply { initializer(this) }
+
+fun tensor(size: Int, initializer: (Int) -> Float) = initializedTensor(size) {
+ for (i in 0 until size) {
+ it[i] = initializer(i)
+ }
+}
+
+fun tensor(size0: Int, size1: Int, initializer: (Int, Int) -> Float) = initializedTensor(size0, size1) {
+ for (i0 in 0 until size0) {
+ for (i1 in 0 until size1) {
+ it[i0, i1] = initializer(i0, i1)
+ }
+ }
+}
+
+fun tensor(vararg values: Float) = tensor(values.size) { values[it] }
+fun tensor(vararg values: Array) = tensor(values.size, values.first().size) { i0, i1 -> values[i0][i1] }
+
+fun full(constant: Float, size: Int) = tensor(size) { constant }
+fun full(constant: Float, size0: Int, size1: Int) = tensor(size0, size1) { _, _ -> constant }
+fun full(constant: Float, shape: List) = when (shape.size) {
+ 1 -> full(constant, shape.single())
+ 2 -> full(constant, shape[0], shape[1])
+ else -> throw Error("Tensors with ${shape.size} dimensions are not supported yet.")
+}
+
+val randomGenerator = THGenerator_new()
+fun random(min: Float, max: Float) = THRandom_uniformFloat(randomGenerator, min, max)
+fun randomInt(count: Int, min: Int = 0) = random(min.toFloat(), count.toFloat()).toInt()
+fun random(min: Double, max: Double, size: Int) =
+ initializedTensor(size) { THFloatTensor_uniform(it.raw, randomGenerator, min, max) }
+
+fun random(min: Double, max: Double, size0: Int, size1: Int) =
+ initializedTensor(size0, size1) { THFloatTensor_uniform(it.raw, randomGenerator, min, max) }
+
+fun zeros(size: Int) = full(0f, size)
+fun zeros(size0: Int, size1: Int) = full(0f, size0, size1)
+fun zeros(shape: List) = full(0f, shape)
+
+fun ones(size: Int) = full(1f, size)
+fun ones(size0: Int, size1: Int) = full(1f, size0, size1)
+fun ones(shape: List) = full(1f, shape)
\ No newline at end of file