From 565c2c291f738c8d992982600243e784c2955879 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 15 Sep 2025 22:10:34 +0200 Subject: [PATCH 1/2] Fix package names and add nn package with basic layer and functions --- settings.gradle.kts | 2 + .../kotlin/sk/ainet/core/tensor/DType.kt | 2 +- .../kotlin/sk/ainet/core/tensor/Shape.kt | 2 +- .../kotlin/sk/ainet/core/tensor/Tensor.kt | 2 +- .../kotlin/sk/ainet/core/tensor/TensorData.kt | 2 +- .../kotlin/sk/ainet/core/tensor/TensorOps.kt | 42 +++++- .../tensor/{ => backend}/ComputeBackend.kt | 9 +- .../ainet/core/tensor/backend/CpuBackend.kt | 136 +++++++++++++++++- .../sk/ainet/core/tensor/PrintingTest.kt | 51 ++++--- .../core/tensor/backend/CpuBackendTest.kt | 2 +- .../tensor/multiplication/EdgeCasesTest.kt | 2 +- .../ElementwiseMultiplicationTest.kt | 2 +- .../tensor/multiplication/MLOperationsTest.kt | 2 +- .../MatrixMultiplicationTest.kt | 2 +- skainet-nn/skainet-nn-api/build.gradle.kts | 61 ++++++++ skainet-nn/skainet-nn-api/gradle.properties | 2 + .../commonMain/kotlin/sk/ainet/nn/Flatten.kt | 22 +++ .../commonMain/kotlin/sk/ainet/nn/Input.kt | 18 +++ .../commonMain/kotlin/sk/ainet/nn/Linear.kt | 49 +++++++ .../commonMain/kotlin/sk/ainet/nn/Module.kt | 20 +++ .../activations/ActivationsWrapperModule.kt | 20 +++ .../kotlin/sk/ainet/nn/activations/Softmax.kt | 16 +++ .../kotlin/sk/ainet/nn/activations/relu.kt | 14 ++ .../kotlin/sk/ainet/nn/topology/MLP.kt | 25 ++++ .../sk/ainet/nn/topology/ModuleParameters.kt | 37 +++++ .../skainet-nn-reflection/build.gradle.kts | 61 ++++++++ .../skainet-nn-reflection/gradle.properties | 2 + .../sk/ainet/nn/reflection/ModelSummary.kt | 85 +++++++++++ .../ainet/nn/reflection/ModuleParameters.kt | 35 +++++ .../sk/ainet/nn/reflection/ModuleTools.kt | 54 +++++++ .../sk/ainet/nn/reflection/ParamTools.kt | 14 ++ .../ainet/nn/reflection/table/TableBuilder.kt | 124 ++++++++++++++++ 32 files changed, 873 insertions(+), 44 deletions(-) rename skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/{ => backend}/ComputeBackend.kt (76%) create mode 100644 skainet-nn/skainet-nn-api/build.gradle.kts create mode 100644 skainet-nn/skainet-nn-api/gradle.properties create mode 100644 skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/Flatten.kt create mode 100644 skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/Input.kt create mode 100644 skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/Linear.kt create mode 100644 skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/Module.kt create mode 100644 skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/activations/ActivationsWrapperModule.kt create mode 100644 skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/activations/Softmax.kt create mode 100644 skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/activations/relu.kt create mode 100644 skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/topology/MLP.kt create mode 100644 skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/topology/ModuleParameters.kt create mode 100644 skainet-nn/skainet-nn-reflection/build.gradle.kts create mode 100644 skainet-nn/skainet-nn-reflection/gradle.properties create mode 100644 skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/ModelSummary.kt create mode 100644 skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/ModuleParameters.kt create mode 100644 skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/ModuleTools.kt create mode 100644 skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/ParamTools.kt create mode 100644 skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/table/TableBuilder.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index f2864d18e..8759d6dc6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,3 +18,5 @@ rootProject.name = "skainet" include("skainet-core:skainet-tensors-api") include("skainet-core:skainet-tensors") include("skainet-core:skainet-performance") +include("skainet-nn:skainet-nn-api") +include("skainet-nn:skainet-nn-relection") diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/DType.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/DType.kt index 96346668c..5e79d12bb 100644 --- a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/DType.kt +++ b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/DType.kt @@ -1,4 +1,4 @@ -package sk.ai.net.core.tensor +package sk.ainet.core.tensor // Base marker interface for all dtypes public sealed interface DType { diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/Shape.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/Shape.kt index 2e50cd52b..948fb7897 100644 --- a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/Shape.kt +++ b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/Shape.kt @@ -1,4 +1,4 @@ -package sk.ai.net.core.tensor +package sk.ainet.core.tensor /** * Data class representing the shape of a multi-dimensional array (tensor). diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/Tensor.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/Tensor.kt index bbff47de7..0f43282f7 100644 --- a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/Tensor.kt +++ b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/Tensor.kt @@ -1,4 +1,4 @@ -package sk.ai.net.core.tensor +package sk.ainet.core.tensor /** * Interface representing a multi-dimensional array of numeric values. diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorData.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorData.kt index 27a4a781b..b8d370889 100644 --- a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorData.kt +++ b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorData.kt @@ -1,4 +1,4 @@ -package sk.ai.net.core.tensor +package sk.ainet.core.tensor public interface TensorData { /** diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorOps.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorOps.kt index fd7fadc19..d4982306b 100644 --- a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorOps.kt +++ b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorOps.kt @@ -1,4 +1,4 @@ -package sk.ai.net.core.tensor +package sk.ainet.core.tensor /** * Interface representing mathematical operations on tensors. @@ -6,7 +6,7 @@ package sk.ai.net.core.tensor * Keeps math separate from storage, so the same math API can work * with multiple backends (dense, sparse, GPU, etc.). */ -public interface TensorOps> { +public interface TensorOps> { /** * Performs matrix multiplication of two tensors. * @@ -100,4 +100,40 @@ public interface TensorOps> { public operator fun Float.minus(t: T): T = this.toDouble() - t public operator fun Float.times(t: T): T = this.toDouble() * t public operator fun Float.div(t: T): T = this.toDouble() / t -} \ No newline at end of file + + public fun T.t(): T // transpose + + public fun T.relu(): T + + /** + * Applies the softmax function along the specified dimension of the tensor. + */ + public fun T.softmax(dimension: Int): T + + /** + * Applies the sigmoid function element-wise to the tensor. + * + * @return A new tensor with the sigmoid function applied to each element. + */ + public fun T.sigmoid(): T + + + /** + * Applies the hyperbolic tangent (tanh) function element-wise to the tensor. + * + * @return A new tensor with the tanh function applied to each element. + */ + public fun T.tanh(): T + + + /** + * Flattens the tensor into a 1D tensor. + * + * @param startDim The first dimension to flatten (inclusive). + * @param endDim The last dimension to flatten (inclusive). + * @return A new flattened tensor. + */ + public fun T.flatten(startDim: Int = 1, endDim: Int = -1): T +} + + diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/ComputeBackend.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/backend/ComputeBackend.kt similarity index 76% rename from skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/ComputeBackend.kt rename to skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/backend/ComputeBackend.kt index 02ca02e8d..661e999ce 100644 --- a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/ComputeBackend.kt +++ b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/backend/ComputeBackend.kt @@ -1,8 +1,9 @@ -package sk.ai.net.core.tensor.backend +package sk.ainet.core.tensor.backend + +import sk.ainet.core.tensor.DType +import sk.ainet.core.tensor.Tensor +import sk.ainet.core.tensor.TensorOps -import sk.ai.net.core.tensor.DType -import sk.ai.net.core.tensor.Tensor -import sk.ai.net.core.tensor.TensorOps /** * Interface representing a computation backend for tensor operations. diff --git a/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ainet/core/tensor/backend/CpuBackend.kt b/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ainet/core/tensor/backend/CpuBackend.kt index 18b242815..c3c5b1395 100644 --- a/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ainet/core/tensor/backend/CpuBackend.kt +++ b/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ainet/core/tensor/backend/CpuBackend.kt @@ -1,7 +1,8 @@ package sk.ainet.core.tensor.backend -import sk.ai.net.core.tensor.* -import sk.ai.net.core.tensor.backend.ComputeBackend +import sk.ainet.core.tensor.* +import sk.ainet.core.tensor.backend.ComputeBackend +import kotlin.math.* /** * Convenient type alias for FP32 tensors with Float values. @@ -149,6 +150,14 @@ public class CpuTensorFP32( override fun Double.times(t: Tensor): Tensor = with(backend) { this@times.times(t) } override fun Double.div(t: Tensor): Tensor = with(backend) { this@div.div(t) } + // Advanced tensor operations - delegate to backend + override fun Tensor.t(): Tensor = with(backend) { this@t.t() } + override fun Tensor.relu(): Tensor = with(backend) { this@relu.relu() } + override fun Tensor.sigmoid(): Tensor = with(backend) { this@sigmoid.sigmoid() } + override fun Tensor.tanh(): Tensor = with(backend) { this@tanh.tanh() } + override fun Tensor.softmax(dimension: Int): Tensor = with(backend) { this@softmax.softmax(dimension) } + override fun Tensor.flatten(startDim: Int, endDim: Int): Tensor = with(backend) { this@flatten.flatten(startDim, endDim) } + public companion object { /** * Creates a tensor from an array with the given shape. @@ -369,4 +378,127 @@ public class CpuBackend : ComputeBackend { val result = t.data.map { this.toFloat() / it }.toFloatArray() return CpuTensorFP32(t.shape, result) } + + // Advanced tensor operations + override fun Tensor.t(): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + require(this.shape.rank == 2) { "Transpose only supported for 2D tensors (matrices)" } + + val rows = this.shape[0] + val cols = this.shape[1] + val result = FloatArray(rows * cols) + + for (i in 0 until rows) { + for (j in 0 until cols) { + result[j * rows + i] = this.data[i * cols + j] + } + } + + return CpuTensorFP32(Shape(cols, rows), result) + } + + override fun Tensor.relu(): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = this.data.map { maxOf(0f, it) }.toFloatArray() + return CpuTensorFP32(this.shape, result) + } + + override fun Tensor.sigmoid(): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = this.data.map { 1f / (1f + exp(-it)) }.toFloatArray() + return CpuTensorFP32(this.shape, result) + } + + override fun Tensor.tanh(): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = this.data.map { tanh(it) }.toFloatArray() + return CpuTensorFP32(this.shape, result) + } + + override fun Tensor.softmax(dimension: Int): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + require(dimension in 0 until this.shape.rank) { "Dimension $dimension is out of bounds for tensor with rank ${this.shape.rank}" } + + when (this.shape.rank) { + 1 -> { + // For 1D tensor, apply softmax across the single dimension + val maxVal = this.data.maxOrNull() ?: 0f + val expValues = this.data.map { exp(it - maxVal) } + val sum = expValues.sum() + val result = expValues.map { it / sum }.toFloatArray() + return CpuTensorFP32(this.shape, result) + } + 2 -> { + // For 2D tensor (matrix), apply softmax along specified dimension + val rows = this.shape[0] + val cols = this.shape[1] + val result = FloatArray(this.data.size) + + if (dimension == 0) { + // Apply softmax along rows (for each column) + for (j in 0 until cols) { + val columnValues = FloatArray(rows) { i -> this.data[i * cols + j] } + val maxVal = columnValues.maxOrNull() ?: 0f + val expValues = columnValues.map { exp(it - maxVal) } + val sum = expValues.sum() + for (i in 0 until rows) { + result[i * cols + j] = expValues[i] / sum + } + } + } else { + // Apply softmax along columns (for each row) + for (i in 0 until rows) { + val rowStart = i * cols + val rowValues = this.data.sliceArray(rowStart until rowStart + cols) + val maxVal = rowValues.maxOrNull() ?: 0f + val expValues = rowValues.map { exp(it - maxVal) } + val sum = expValues.sum() + for (j in 0 until cols) { + result[rowStart + j] = expValues[j] / sum + } + } + } + return CpuTensorFP32(this.shape, result) + } + else -> { + throw UnsupportedOperationException("Softmax not implemented for tensors with rank > 2") + } + } + } + + override fun Tensor.flatten(startDim: Int, endDim: Int): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + + val actualEndDim = if (endDim == -1) this.shape.rank - 1 else endDim + require(startDim >= 0 && startDim < this.shape.rank) { "startDim $startDim is out of bounds" } + require(actualEndDim >= startDim && actualEndDim < this.shape.rank) { "endDim $actualEndDim is out of bounds or less than startDim" } + + if (startDim == actualEndDim) { + // No flattening needed + return CpuTensorFP32(this.shape, this.data.copyOf()) + } + + // Calculate new shape + val newDimensions = mutableListOf() + + // Add dimensions before startDim + for (i in 0 until startDim) { + newDimensions.add(this.shape[i]) + } + + // Calculate flattened dimension size + var flattenedSize = 1 + for (i in startDim..actualEndDim) { + flattenedSize *= this.shape[i] + } + newDimensions.add(flattenedSize) + + // Add dimensions after endDim + for (i in (actualEndDim + 1) until this.shape.rank) { + newDimensions.add(this.shape[i]) + } + + val newShape = Shape(*newDimensions.toIntArray()) + return CpuTensorFP32(newShape, this.data.copyOf()) + } } \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/PrintingTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/PrintingTest.kt index 7623fe630..936f823fc 100644 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/PrintingTest.kt +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/PrintingTest.kt @@ -1,6 +1,5 @@ package sk.ainet.core.tensor -import sk.ai.net.core.tensor.Shape import sk.ainet.core.tensor.backend.CpuTensorFP32 import sk.ainet.core.tensor.backend.print import sk.ainet.core.tensor.backend.printMatrix @@ -9,73 +8,73 @@ import sk.ainet.core.tensor.backend.printVector import kotlin.test.* class PrintingTest { - + @Test fun testScalarPrinting() { // Test scalar (1D tensor with single element) val scalar = CpuTensorFP32.fromArray(Shape(1), floatArrayOf(42.5f)) - + val scalarOutput = scalar.printScalar() assertEquals("42.5", scalarOutput) - + val generalOutput = scalar.print() assertEquals("42.5", generalOutput) } - + @Test fun testVectorPrinting() { // Test vector (1D tensor) val vector = CpuTensorFP32.fromArray(Shape(4), floatArrayOf(1.0f, 2.5f, -3.0f, 4.7f)) - + val vectorOutput = vector.printVector() assertEquals("[1.0, 2.5, -3.0, 4.7]", vectorOutput) - + val generalOutput = vector.print() assertEquals("[1.0, 2.5, -3.0, 4.7]", generalOutput) } - + @Test fun testMatrixPrinting() { // Test matrix (2D tensor) val matrix = CpuTensorFP32.fromArray( - Shape(2, 3), + Shape(2, 3), floatArrayOf(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f) ) - + val matrixOutput = matrix.printMatrix() val expectedMatrix = """[ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ]""" assertEquals(expectedMatrix, matrixOutput) - + val generalOutput = matrix.print() assertEquals(expectedMatrix, generalOutput) } - + @Test fun testSingleElementMatrixPrinting() { // Test 1x1 matrix val singleMatrix = CpuTensorFP32.fromArray(Shape(1, 1), floatArrayOf(7.5f)) - + val matrixOutput = singleMatrix.printMatrix() val expectedMatrix = """[ [7.5] ]""" assertEquals(expectedMatrix, matrixOutput) - + val generalOutput = singleMatrix.print() assertEquals(expectedMatrix, generalOutput) } - + @Test fun testLargerMatrixPrinting() { // Test larger matrix val largerMatrix = CpuTensorFP32.fromArray( - Shape(3, 2), + Shape(3, 2), floatArrayOf(1.1f, 2.2f, 3.3f, 4.4f, 5.5f, 6.6f) ) - + val matrixOutput = largerMatrix.printMatrix() val expectedMatrix = """[ [1.1, 2.2], @@ -84,45 +83,45 @@ class PrintingTest { ]""" assertEquals(expectedMatrix, matrixOutput) } - + @Test fun testWrongRankForScalar() { // Test error handling for wrong tensor rank in printScalar val vector = CpuTensorFP32.fromArray(Shape(3), floatArrayOf(1.0f, 2.0f, 3.0f)) - + assertFailsWith { vector.printScalar() } } - + @Test fun testWrongRankForVector() { // Test error handling for wrong tensor rank in printVector val matrix = CpuTensorFP32.fromArray(Shape(2, 2), floatArrayOf(1.0f, 2.0f, 3.0f, 4.0f)) - + assertFailsWith { matrix.printVector() } } - + @Test fun testWrongRankForMatrix() { // Test error handling for wrong tensor rank in printMatrix val vector = CpuTensorFP32.fromArray(Shape(3), floatArrayOf(1.0f, 2.0f, 3.0f)) - + assertFailsWith { vector.printMatrix() } } - + @Test fun testHigherDimensionTensorPrinting() { // Test that higher-dimensional tensors show appropriate message val tensor3D = CpuTensorFP32.fromArray( - Shape(2, 2, 2), + Shape(2, 2, 2), FloatArray(8) { it.toFloat() } ) - + val output = tensor3D.print() assertTrue(output.contains("printing not supported for tensors with rank > 2")) assertTrue(output.contains("rank=3")) diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/backend/CpuBackendTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/backend/CpuBackendTest.kt index 30e9ee2b9..38ad06ec9 100644 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/backend/CpuBackendTest.kt +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/backend/CpuBackendTest.kt @@ -1,6 +1,6 @@ package sk.ainet.core.tensor.backend -import sk.ai.net.core.tensor.Shape +import sk.ainet.core.tensor.Shape import kotlin.test.* class CpuBackendTest { diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/EdgeCasesTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/EdgeCasesTest.kt index 3ba216383..7a3d1b143 100644 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/EdgeCasesTest.kt +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/EdgeCasesTest.kt @@ -1,6 +1,6 @@ package sk.ainet.core.tensor.multiplication -import sk.ai.net.core.tensor.* +import sk.ainet.core.tensor.* import sk.ainet.core.tensor.backend.CpuBackend import sk.ainet.core.tensor.backend.CpuTensorFP32 import kotlin.test.* diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/ElementwiseMultiplicationTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/ElementwiseMultiplicationTest.kt index afca23bd1..3d2a79a65 100644 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/ElementwiseMultiplicationTest.kt +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/ElementwiseMultiplicationTest.kt @@ -1,6 +1,6 @@ package sk.ainet.core.tensor.multiplication -import sk.ai.net.core.tensor.* +import sk.ainet.core.tensor.* import sk.ainet.core.tensor.backend.CpuBackend import sk.ainet.core.tensor.backend.CpuTensorFP32 import kotlin.test.* diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MLOperationsTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MLOperationsTest.kt index f317653f0..64d124b8f 100644 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MLOperationsTest.kt +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MLOperationsTest.kt @@ -1,6 +1,6 @@ package sk.ainet.core.tensor.multiplication -import sk.ai.net.core.tensor.* +import sk.ainet.core.tensor.* import sk.ainet.core.tensor.backend.CpuBackend import sk.ainet.core.tensor.backend.CpuTensorFP32 import kotlin.test.* diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MatrixMultiplicationTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MatrixMultiplicationTest.kt index e672c67dc..1bdfdb350 100644 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MatrixMultiplicationTest.kt +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MatrixMultiplicationTest.kt @@ -1,6 +1,6 @@ package sk.ainet.core.tensor.multiplication -import sk.ai.net.core.tensor.* +import sk.ainet.core.tensor.* import sk.ainet.core.tensor.backend.CpuBackend import sk.ainet.core.tensor.backend.CpuTensorFP32 import sk.ainet.core.tensor.backend.TensorFP32 diff --git a/skainet-nn/skainet-nn-api/build.gradle.kts b/skainet-nn/skainet-nn-api/build.gradle.kts new file mode 100644 index 000000000..1a50eacdc --- /dev/null +++ b/skainet-nn/skainet-nn-api/build.gradle.kts @@ -0,0 +1,61 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) + alias(libs.plugins.vanniktech.mavenPublish) +} + +kotlin { + explicitApi() + + androidTarget { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + iosArm64() + iosSimulatorArm64() + macosArm64 () + linuxX64 () + linuxArm64 () + + jvm() + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":skainet-core:skainet-tensors-api")) + implementation(project(":skainet-core:skainet-tensors")) + } + } + + commonTest.dependencies { + implementation(libs.kotlin.test) + implementation(project(":skainet-core:skainet-performance")) + } + } +} + +android { + namespace = "sk.ainet.core.api" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} \ No newline at end of file diff --git a/skainet-nn/skainet-nn-api/gradle.properties b/skainet-nn/skainet-nn-api/gradle.properties new file mode 100644 index 000000000..cc090d121 --- /dev/null +++ b/skainet-nn/skainet-nn-api/gradle.properties @@ -0,0 +1,2 @@ +POM_ARTIFACT_ID=nn-api +POM_NAME=skainet neural network API \ No newline at end of file diff --git a/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/Flatten.kt b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/Flatten.kt new file mode 100644 index 000000000..2f846bf47 --- /dev/null +++ b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/Flatten.kt @@ -0,0 +1,22 @@ +package sk.ainet.nn + +import sk.ainet.core.tensor.DType +import sk.ainet.core.tensor.Tensor +import sk.ainet.core.tensor.TensorOps + +/** + * A simple layer that flattens an input tensor into a 1D tensor. + * This layer has no parameters and simply reshapes the input. + */ +public class Flatten( + private val startDim: Int = 1, + private val endDim: Int = -1, + override val name: String = "Flatten" +) : Module() { + override val modules: List> + get() = emptyList() + + override fun TensorOps>.forward(input: Tensor): Tensor { + return input.flatten(startDim, endDim) + } +} diff --git a/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/Input.kt b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/Input.kt new file mode 100644 index 000000000..c0184ed4b --- /dev/null +++ b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/Input.kt @@ -0,0 +1,18 @@ +package sk.ainet.nn + +import sk.ainet.core.tensor.DType +import sk.ainet.core.tensor.Shape +import sk.ainet.core.tensor.Tensor +import sk.ainet.core.tensor.TensorOps + + +public class Input(private val inputShape: Shape, override val name: String = "Input") : Module() { + + override val modules: List> + get() = emptyList() + + + override fun TensorOps>.forward(input: Tensor): Tensor { + return input + } +} \ No newline at end of file diff --git a/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/Linear.kt b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/Linear.kt new file mode 100644 index 000000000..7317110c4 --- /dev/null +++ b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/Linear.kt @@ -0,0 +1,49 @@ +package sk.ainet.nn + +import sk.ainet.core.tensor.DType +import sk.ainet.core.tensor.Tensor +import sk.ainet.core.tensor.TensorOps +import sk.ainet.core.tensor.backend.CpuTensorFP32 +import sk.ainet.core.tensor.backend.CpuBackend +import sk.ainet.nn.topology.ModuleParameter +import sk.ainet.nn.topology.ModuleParameters +import sk.ainet.nn.topology.bias +import sk.ainet.nn.topology.weights + +/** + * Linear layer (a.k.a. fully connected dense layer). This layer applies a linear transformation to the input data. + * The weights and biases are learned during training. + * + * @param inFeatures Number of input features + * @param outFeatures Number of output features + * @param name Name of the module + * @param initWeights Initial weights + * @param initBias Initial bias + */ + +public class Linear( + inFeatures: Int, + outFeatures: Int, + override val name: String = "Linear", + initWeights: Tensor, //= Tensor.randn(shape = intArrayOf(outFeatures, inFeatures)), + initBias: Tensor, // = Tensor.zeros(shape = intArrayOf(outFeatures)), +) : Module(), ModuleParameters { + override val params: List> = listOf( + ModuleParameter.WeightParameter("$name.weight", initWeights), + ModuleParameter.BiasParameter("$name.bias", initBias) + ) + + + override val modules: List> + get() = emptyList() + + override fun TensorOps>.forward(input: Tensor): Tensor { + val weight = params.weights().value + val bias = params.bias().value + + // Use TensorOps context operations + val weightTransposed = weight.t() + val matmulResult = matmul(input, weightTransposed) + return matmulResult + bias + } +} diff --git a/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/Module.kt b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/Module.kt new file mode 100644 index 000000000..e9758371d --- /dev/null +++ b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/Module.kt @@ -0,0 +1,20 @@ +package sk.ainet.nn + +import sk.ainet.core.tensor.DType +import sk.ainet.core.tensor.Tensor +import sk.ainet.core.tensor.TensorOps + + +public abstract class Module { + + public abstract val name: String + + public abstract val modules: List> + + public abstract fun TensorOps>.forward(input: Tensor): Tensor + + public operator fun TensorOps>.invoke(input: Tensor): Tensor { + return forward(input) + } +} + diff --git a/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/activations/ActivationsWrapperModule.kt b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/activations/ActivationsWrapperModule.kt new file mode 100644 index 000000000..f994b19a7 --- /dev/null +++ b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/activations/ActivationsWrapperModule.kt @@ -0,0 +1,20 @@ +package sk.ainet.nn.activations + +import sk.ainet.core.tensor.DType +import sk.ainet.core.tensor.Tensor +import sk.ainet.core.tensor.TensorOps +import sk.ainet.nn.Module + + +public class ActivationsWrapperModule( + private val activationHandler: (Tensor) -> Tensor, + override val name: String +) : + Module() { + override val modules: List> + get() = emptyList() + + override fun TensorOps>.forward(input: Tensor): Tensor { + return activationHandler(input) + } +} \ No newline at end of file diff --git a/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/activations/Softmax.kt b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/activations/Softmax.kt new file mode 100644 index 000000000..1a45dc470 --- /dev/null +++ b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/activations/Softmax.kt @@ -0,0 +1,16 @@ +package sk.ainet.nn.activations + +import sk.ainet.core.tensor.DType +import sk.ainet.core.tensor.Tensor +import sk.ainet.core.tensor.TensorOps +import sk.ainet.nn.Module + +public class Softmax(private val dimension: Int, override val name: String = "Softmax") : Module() { + override val modules: List> + get() = emptyList() + + override fun TensorOps>.forward(input: Tensor): Tensor { + return input.softmax(dimension) + } +} + diff --git a/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/activations/relu.kt b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/activations/relu.kt new file mode 100644 index 000000000..5dbea0f69 --- /dev/null +++ b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/activations/relu.kt @@ -0,0 +1,14 @@ +package sk.ainet.nn.activations + +import sk.ainet.core.tensor.DType +import sk.ainet.core.tensor.Tensor +import sk.ainet.core.tensor.TensorOps +import sk.ainet.nn.Module + +public class ReLU(override val name: String = "ReLU") : Module() { + override val modules: List> + get() = emptyList() + + override fun TensorOps>.forward(input: Tensor): Tensor = input.relu() +} + diff --git a/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/topology/MLP.kt b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/topology/MLP.kt new file mode 100644 index 000000000..9f01b0a52 --- /dev/null +++ b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/topology/MLP.kt @@ -0,0 +1,25 @@ +package sk.ainet.nn.topology + +import sk.ainet.core.tensor.DType +import sk.ainet.core.tensor.Tensor +import sk.ainet.core.tensor.TensorOps +import sk.ainet.nn.Module + + +public class MLP(vararg modules: Module, override val name: String = "FeedForwardNetwork") : + Module(), ModuleParameters { + private val modulesList = modules.toList() + override val modules: List> + get() = modulesList + + override fun TensorOps>.forward(input: Tensor): Tensor { + var tmp = input + modulesList.forEach { module -> + tmp = with(module) { this@forward.forward(tmp) } + } + return tmp + } + + override val params: List> + get() = emptyList() +} \ No newline at end of file diff --git a/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/topology/ModuleParameters.kt b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/topology/ModuleParameters.kt new file mode 100644 index 000000000..9c26934e7 --- /dev/null +++ b/skainet-nn/skainet-nn-api/src/commonMain/kotlin/sk/ainet/nn/topology/ModuleParameters.kt @@ -0,0 +1,37 @@ +package sk.ainet.nn.topology + +import sk.ainet.core.tensor.DType +import sk.ainet.core.tensor.Tensor + + +public sealed class ModuleParameter { + public abstract val name: String + public abstract var value: Tensor + + public data class WeightParameter( + override val name: String, + override var value: Tensor + ) : ModuleParameter() + + public data class BiasParameter( + override val name: String, + override var value: Tensor + ) : ModuleParameter() +} + +public interface ModuleParameters { + public val params: List> +} + +public fun List>.by(name: String): ModuleParameter? = + firstOrNull { namedParameter -> namedParameter.name.uppercase().contains(name.uppercase()) } + +// Returns the first BiasParameter or throws a NoSuchElementException if none is found. +public fun List>.bias(): ModuleParameter.BiasParameter = + this.filterIsInstance>() + .firstOrNull() ?: throw NoSuchElementException("No bias parameter found!") + +// Returns the first WeightParameter or throws a NoSuchElementException if none is found. +public fun List>.weights(): ModuleParameter.WeightParameter = + this.filterIsInstance>() + .firstOrNull() ?: throw NoSuchElementException("No weight parameter found!") diff --git a/skainet-nn/skainet-nn-reflection/build.gradle.kts b/skainet-nn/skainet-nn-reflection/build.gradle.kts new file mode 100644 index 000000000..8d85acccb --- /dev/null +++ b/skainet-nn/skainet-nn-reflection/build.gradle.kts @@ -0,0 +1,61 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) + alias(libs.plugins.vanniktech.mavenPublish) +} + +kotlin { + explicitApi() + + androidTarget { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + iosArm64() + iosSimulatorArm64() + macosArm64 () + linuxX64 () + linuxArm64 () + + jvm() + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":skainet-core:skainet-tensors-api")) + implementation(project(":skainet-core:skainet-tensors-api")) + } + } + + commonTest.dependencies { + implementation(libs.kotlin.test) + implementation(project(":skainet-core:skainet-performance")) + } + } +} + +android { + namespace = "sk.ainet.core.api" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} \ No newline at end of file diff --git a/skainet-nn/skainet-nn-reflection/gradle.properties b/skainet-nn/skainet-nn-reflection/gradle.properties new file mode 100644 index 000000000..cc090d121 --- /dev/null +++ b/skainet-nn/skainet-nn-reflection/gradle.properties @@ -0,0 +1,2 @@ +POM_ARTIFACT_ID=nn-api +POM_NAME=skainet neural network API \ No newline at end of file diff --git a/skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/ModelSummary.kt b/skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/ModelSummary.kt new file mode 100644 index 000000000..6b3d79342 --- /dev/null +++ b/skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/ModelSummary.kt @@ -0,0 +1,85 @@ +package sk.ai.net.nn.reflection + +import sk.ai.net.Shape +import sk.ai.net.Tensor +import sk.ai.net.impl.DoublesTensor +import sk.ai.net.impl.prod +import sk.ai.net.nn.Module +import sk.ainet.nn.reflection.table.table + +data class NodeSummary(val name: String, val input: Shape, val output: Shape, val params: Long) + +class Summary { + + val nodes = mutableListOf() + + private fun nodeSummary(index: Int, module: Module, input: Shape, output: Tensor): NodeSummary { + var params = 0L + + if (module is ModuleParameters) { + + module.params.by("W")?.let { weight -> + val dimension = + DoublesTensor(weight.value.shape, weight.value.shape.dimensions.map { it.toDouble() }.toDoubleArray()) + params += dimension.prod().toLong() + } + + module.params.by("B")?.let { bias -> + val dimension = + DoublesTensor(bias.value.shape, bias.value.shape.dimensions.map { it.toDouble() }.toDoubleArray()) + params += dimension.prod().toLong() + } + } + + + return NodeSummary( + module.name, + input, + output.shape, + params + ) + } + + + fun summary(model: Module, input: Shape, batch_size: Int = -1): List { + var data = DoublesTensor(input, List(input.volume) { 0.0 }.toDoubleArray()) + var count = 1 + model.modules.forEach { module -> + val moduleInput = data + data = module.forward(moduleInput) as DoublesTensor + val nodeSummary = nodeSummary(count, module, moduleInput.shape, data) + if (nodeSummary.params > 0) { + count++ + nodes.add(nodeSummary) + } + } + return nodes + } + + fun printSummary(nodes: List) = + table { + cellStyle { + border = true + } + header { + row { + cell("Layer (type)") + cell("Output Shape") + cell("Param #") + } + } + nodes.forEach { node -> + row { + cell(node.name) + cell(node.output.toString()) + cell(node.params) + } + } + }.toString() +} + +fun Module.summary(input: Shape, batch_size: Int = -1): String { + val summary = Summary() + val nodes = summary.summary(this, input, batch_size) + return summary.printSummary(nodes) +} diff --git a/skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/ModuleParameters.kt b/skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/ModuleParameters.kt new file mode 100644 index 000000000..c4f69bdec --- /dev/null +++ b/skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/ModuleParameters.kt @@ -0,0 +1,35 @@ +package sk.ai.net.nn.reflection + +import sk.ai.net.Tensor + +sealed class ModuleParameter { + abstract val name: String + abstract var value: Tensor + + data class WeightParameter( + override val name: String, + override var value: Tensor + ) : ModuleParameter() + + data class BiasParameter( + override val name: String, + override var value: Tensor + ) : ModuleParameter() +} + +interface ModuleParameters { + val params: List +} + +public fun List.by(name: String): ModuleParameter? = + firstOrNull { namedParameter -> namedParameter.name.uppercase().contains(name.uppercase()) } + +// Returns the first BiasParameter or throws a NoSuchElementException if none is found. +fun List.bias(): ModuleParameter.BiasParameter = + this.filterIsInstance() + .firstOrNull() ?: throw NoSuchElementException("No bias parameter found!") + +// Returns the first WeightParameter or throws a NoSuchElementException if none is found. +fun List.weights(): ModuleParameter.WeightParameter = + this.filterIsInstance() + .firstOrNull() ?: throw NoSuchElementException("No weight parameter found!") diff --git a/skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/ModuleTools.kt b/skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/ModuleTools.kt new file mode 100644 index 000000000..544b550ac --- /dev/null +++ b/skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/ModuleTools.kt @@ -0,0 +1,54 @@ +package sk.ai.net.nn.reflection + +import sk.ai.net.nn.Module + +// Extension function to generate a custom string representation for a Module +fun Module.toCustomString(indent: String = ""): String { + // Build the string for the current module + val builder = StringBuilder() + builder.append("$indent$name") + + // If there are child modules, recursively add their representations with increased indent + if (modules.isNotEmpty()) { + modules.forEach { child -> + builder.append("\n") + builder.append(child.toCustomString("$indent ")) + } + } + return builder.toString() +} + +// Extension function for the root that prints the module tree hierarchy. +fun Module.toVisualString(): String { + val builder = StringBuilder() + // Print the root node (without any branch symbols) + builder.append(name).append("\n") + // For each child, call the helper function with an empty initial prefix. + modules.forEachIndexed { index, module -> + val isLast = index == modules.lastIndex + builder.append(module.toVisualStringHelper("", isLast)) + } + return builder.toString() +} + +// Private helper extension function that handles the branch prefixes. +private fun Module.toVisualStringHelper(prefix: String, isLast: Boolean): String { + val builder = StringBuilder() + // Append the current prefix and the branch symbols: + // "└── " if this node is the last child, otherwise "├── " + builder.append(prefix) + builder.append(if (isLast) "└── " else "├── ") + builder.append(name) + builder.append("\n") + + // Update the prefix for children: + // If this node is the last, add spaces; otherwise add a vertical bar and spaces. + val newPrefix = prefix + if (isLast) " " else "│ " + + // Recursively process all child modules. + modules.forEachIndexed { index, module -> + val childIsLast = index == modules.lastIndex + builder.append(module.toVisualStringHelper(newPrefix, childIsLast)) + } + return builder.toString() +} diff --git a/skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/ParamTools.kt b/skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/ParamTools.kt new file mode 100644 index 000000000..671fbf773 --- /dev/null +++ b/skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/ParamTools.kt @@ -0,0 +1,14 @@ +package sk.ai.net.nn.reflection + +import sk.ai.net.nn.Module + +fun flattenParams(module: Module): List { + val params = mutableListOf() + for (m in module.modules) { + params.addAll(flattenParams(m)) + } + if (module is ModuleParameters) { + params.addAll(module.params) + } + return params +} \ No newline at end of file diff --git a/skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/table/TableBuilder.kt b/skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/table/TableBuilder.kt new file mode 100644 index 000000000..ace937978 --- /dev/null +++ b/skainet-nn/skainet-nn-reflection/src/commonMain/kotlin/sk/ainet/nn/reflection/table/TableBuilder.kt @@ -0,0 +1,124 @@ +package sk.ainet.nn.reflection.table + +// DSL entry point +public fun table(block: Table.() -> Unit): Table { + return Table().apply(block) +} + +// The Table DSL class +public class Table { + // A cell style configuration object + public val cellStyle = CellStyle() + + // Optional header section + public var header: Header? = null + + // List of body rows + public val rows = mutableListOf() + + // DSL function to configure the cell style + public fun cellStyle(block: CellStyle.() -> Unit) { + cellStyle.block() + } + + // DSL function to add a header section + public fun header(block: Header.() -> Unit) { + header = Header().apply(block) + } + + // DSL function to add a body row directly to the table + public fun row(block: Row.() -> Unit) { + rows.add(Row().apply(block)) + } + + // Converts the table to an ASCII string + override fun toString(): String { + // Gather all rows (header and body) to compute column widths. + val allRows = mutableListOf() + header?.let { allRows.addAll(it.rows) } + allRows.addAll(rows) + + // Determine the number of columns (max among all rows). + val colCount = allRows.maxOfOrNull { it.cells.size } ?: 0 + val colWidths = MutableList(colCount) { 0 } + for (row in allRows) { + row.cells.forEachIndexed { index, cell -> + colWidths[index] = maxOf(colWidths[index], cell.content.length) + } + } + + // Build the table line by line. + val sb = StringBuilder() + + // If borders are enabled, print a top border. + if (cellStyle.border) { + sb.appendLine(buildSeparatorLine(colWidths)) + } + + // Print header rows if they exist. + header?.let { + for (row in it.rows) { + sb.appendLine(buildRowLine(row, colWidths, cellStyle.border)) + } + if (cellStyle.border) { + sb.appendLine(buildSeparatorLine(colWidths)) + } + } + + // Print body rows. + for (row in rows) { + sb.appendLine(buildRowLine(row, colWidths, cellStyle.border)) + if (cellStyle.border) { + sb.appendLine(buildSeparatorLine(colWidths)) + } + } + + return sb.toString() + } + + // Helper: builds a border/separator line based on column widths. + private fun buildSeparatorLine(colWidths: List): String { + return colWidths.joinToString(separator = "+", prefix = "+", postfix = "+") { + "-".repeat(it + 2) + } + } + + // Helper: builds a formatted row line. + private fun buildRowLine(row: Row, colWidths: List, border: Boolean): String { + val cells = row.cells.mapIndexed { index, cell -> + " " + cell.content.padEnd(colWidths[index]) + " " + } + return if (border) { + cells.joinToString(separator = "|", prefix = "|", postfix = "|") + } else { + cells.joinToString(separator = " ") + } + } +} + +// A simple header container allowing multiple header rows. +class Header { + val rows = mutableListOf() + + fun row(block: Row.() -> Unit) { + rows.add(Row().apply(block)) + } +} + +// Represents a row in the table. +class Row { + val cells = mutableListOf() + + // Adds a cell to the row. + fun cell(value: Any?) { + cells.add(Cell(value?.toString() ?: "")) + } +} + +// Represents a cell containing text. +class Cell(val content: String) + +// A configuration class for cell style options. +class CellStyle { + var border: Boolean = false +} From 29b1245c5fec6ae3b5b475939e8fad769e99c07b Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Thu, 18 Sep 2025 06:51:50 +0200 Subject: [PATCH 2/2] Implement simple utility for tensors printing --- docs/modules/getting-started/nav.adoc | 2 + .../pages/tensor-operators.adoc | 381 ++++++++++++++++++ gradle.properties | 2 +- settings.gradle.kts | 1 + .../sk/ainet/core/tensor/TensorPrinting.kt | 139 +++++++ .../ainet/core/tensor/backend/CpuBackend.kt | 44 +- 6 files changed, 556 insertions(+), 13 deletions(-) create mode 100644 docs/modules/getting-started/pages/tensor-operators.adoc create mode 100644 skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorPrinting.kt diff --git a/docs/modules/getting-started/nav.adoc b/docs/modules/getting-started/nav.adoc index 7088062a2..2f691f76a 100644 --- a/docs/modules/getting-started/nav.adoc +++ b/docs/modules/getting-started/nav.adoc @@ -3,7 +3,9 @@ * xref:index.adoc[Introduction] * xref:installation.adoc[Installation & Setup] * xref:basic-tensors.adoc[Basic Tensor Operations] +* xref:tensor-operators.adoc[Tensor Operators] * xref:matrix-operations.adoc[Matrix Operations] * xref:neural-network-basics.adoc[Neural Network Basics] +* xref:neural-network-api.adoc[Neural Network API] * xref:data-processing.adoc[Data Processing Use Cases] * xref:performance-tips.adoc[Performance Tips] \ No newline at end of file diff --git a/docs/modules/getting-started/pages/tensor-operators.adoc b/docs/modules/getting-started/pages/tensor-operators.adoc new file mode 100644 index 000000000..fe5bf90c8 --- /dev/null +++ b/docs/modules/getting-started/pages/tensor-operators.adoc @@ -0,0 +1,381 @@ += Tensor Operators +:toc: left +:toclevels: 3 +:sectanchors: +:sectlinks: + +Master the complete set of tensor operations available in SKaiNET's TensorOps API for mathematical computations and neural network operations. + +== Element-wise Operations + +SKaiNET provides comprehensive element-wise operations between tensors and scalars. + +=== Tensor-Tensor Operations + +Perform element-wise operations between tensors of compatible shapes: + +[source,kotlin] +---- +val backend = CpuBackend() + +// Create sample tensors +val tensorA = CpuTensorFP32.fromArray( + Shape(2, 3), + floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f) +) + +val tensorB = CpuTensorFP32.fromArray( + Shape(2, 3), + floatArrayOf(2f, 3f, 4f, 5f, 6f, 7f) +) + +with(backend) { + // Element-wise addition + val sum = tensorA + tensorB + println("A + B = ${sum.print()}") + // Output: [[3, 5, 7], [9, 11, 13]] + + // Element-wise subtraction + val diff = tensorA - tensorB + println("A - B = ${diff.print()}") + // Output: [[-1, -1, -1], [-1, -1, -1]] + + // Element-wise multiplication + val product = tensorA * tensorB + println("A * B = ${product.print()}") + // Output: [[2, 6, 12], [20, 30, 42]] + + // Element-wise division + val quotient = tensorA / tensorB + println("A / B = ${quotient.print()}") + // Output: [[0.5, 0.67, 0.75], [0.8, 0.83, 0.86]] +} +---- + +=== Tensor-Scalar Operations + +Apply scalar operations to entire tensors: + +[source,kotlin] +---- +val backend = CpuBackend() +val tensor = CpuTensorFP32.fromArray( + Shape(2, 2), + floatArrayOf(1f, 2f, 3f, 4f) +) + +with(backend) { + // Scalar addition + val added = tensor + 10f + println("Tensor + 10 = ${added.print()}") + // Output: [[11, 12], [13, 14]] + + // Scalar multiplication + val scaled = tensor * 2.5f + println("Tensor * 2.5 = ${scaled.print()}") + // Output: [[2.5, 5.0], [7.5, 10.0]] + + // Works with Int, Float, and Double + val intAdded = tensor + 5 + val doubleScaled = tensor * 3.14 + + // Scalar-tensor operations (commutative) + val scaledCommutative = 2f * tensor + println("2 * Tensor = ${scaledCommutative.print()}") +} +---- + +== Matrix Operations + +=== Matrix Multiplication + +The fundamental linear algebra operation for neural networks: + +[source,kotlin] +---- +val backend = CpuBackend() + +val A = CpuTensorFP32.fromArray( + Shape(2, 3), + floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f) +) + +val B = CpuTensorFP32.fromArray( + Shape(3, 2), + floatArrayOf(7f, 8f, 9f, 10f, 11f, 12f) +) + +// Matrix multiplication +val C = backend.matmul(A, B) +println("A @ B = ${C.print()}") +// Output: [[58, 64], [139, 154]] +---- + +=== Transpose Operation + +Transpose tensors along their last two dimensions: + +[source,kotlin] +---- +val backend = CpuBackend() +val matrix = CpuTensorFP32.fromArray( + Shape(2, 3), + floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f) +) + +with(backend) { + val transposed = matrix.t() + println("Original: ${matrix.print()}") + // Output: [[1, 2, 3], [4, 5, 6]] + + println("Transposed: ${transposed.print()}") + // Output: [[1, 4], [2, 5], [3, 6]] +} +---- + +=== Dot Product and Scaling + +[source,kotlin] +---- +val backend = CpuBackend() +val vectorA = CpuTensorFP32.fromArray(Shape(3), floatArrayOf(1f, 2f, 3f)) +val vectorB = CpuTensorFP32.fromArray(Shape(3), floatArrayOf(4f, 5f, 6f)) + +// Dot product +val dotResult = backend.dot(vectorA, vectorB) +println("Dot product: $dotResult") // 32.0 (1*4 + 2*5 + 3*6) + +// Scale tensor by scalar +val scaled = backend.scale(vectorA, 2.5) +println("Scaled: ${scaled.print()}") // [2.5, 5.0, 7.5] +---- + +== Activation Functions + +Essential non-linear functions for neural networks. + +=== ReLU Activation + +Rectified Linear Unit - the most common activation function: + +[source,kotlin] +---- +val backend = CpuBackend() +val input = CpuTensorFP32.fromArray( + Shape(4), + floatArrayOf(-2f, -1f, 0f, 1f, 2f) +) + +with(backend) { + val activated = input.relu() + println("Input: ${input.print()}") + println("ReLU: ${activated.print()}") + // Output: [0, 0, 0, 1, 2] +} +---- + +=== Sigmoid Activation + +Sigmoid function for probability outputs: + +[source,kotlin] +---- +val backend = CpuBackend() +val input = CpuTensorFP32.fromArray( + Shape(3), + floatArrayOf(-1f, 0f, 1f) +) + +with(backend) { + val activated = input.sigmoid() + println("Input: ${input.print()}") + println("Sigmoid: ${activated.print()}") + // Output: [0.268, 0.5, 0.732] +} +---- + +=== Tanh Activation + +Hyperbolic tangent activation: + +[source,kotlin] +---- +val backend = CpuBackend() +val input = CpuTensorFP32.fromArray( + Shape(3), + floatArrayOf(-1f, 0f, 1f) +) + +with(backend) { + val activated = input.tanh() + println("Input: ${input.print()}") + println("Tanh: ${activated.print()}") + // Output: [-0.762, 0.0, 0.762] +} +---- + +=== Softmax Activation + +Softmax for multi-class classification: + +[source,kotlin] +---- +val backend = CpuBackend() +val logits = CpuTensorFP32.fromArray( + Shape(2, 3), // Batch size 2, 3 classes + floatArrayOf(1f, 2f, 3f, 0.5f, 1.5f, 2.5f) +) + +with(backend) { + // Apply softmax along dimension 1 (classes) + val probabilities = logits.softmax(dimension = 1) + println("Logits: ${logits.print()}") + println("Softmax: ${probabilities.print()}") + // Each row sums to 1.0 +} +---- + +== Tensor Reshaping + +=== Flatten Operation + +Convert multi-dimensional tensors to 1D or flatten specific dimensions: + +[source,kotlin] +---- +val backend = CpuBackend() +val tensor3D = CpuTensorFP32.fromArray( + Shape(2, 3, 4), + FloatArray(24) { it.toFloat() } +) + +with(backend) { + // Flatten all dimensions + val flattened = tensor3D.flatten() + println("Original shape: ${tensor3D.shape}") // Shape(2, 3, 4) + println("Flattened shape: ${flattened.shape}") // Shape(24) + + // Flatten from dimension 1 onwards (keep batch dimension) + val batchFlattened = tensor3D.flatten(startDim = 1) + println("Batch flattened shape: ${batchFlattened.shape}") // Shape(2, 12) + + // Flatten specific range of dimensions + val partialFlattened = tensor3D.flatten(startDim = 1, endDim = 2) + println("Partial flattened shape: ${partialFlattened.shape}") // Shape(2, 12) +} +---- + +== Practical Examples + +=== Neural Network Forward Pass + +Combine multiple operators for a complete neural network layer: + +[source,kotlin] +---- +val backend = CpuBackend() + +// Input batch: 32 samples, 784 features (28x28 images) +val input = CpuTensorFP32.fromArray( + Shape(32, 784), + FloatArray(32 * 784) { kotlin.random.Random.nextFloat() } +) + +// Layer weights and bias +val weights = CpuTensorFP32.fromArray( + Shape(128, 784), + FloatArray(128 * 784) { kotlin.random.Random.nextGaussian().toFloat() * 0.1f } +) +val bias = CpuTensorFP32.fromArray( + Shape(128), + FloatArray(128) { 0f } +) + +with(backend) { + // Linear transformation: W @ x^T + b + val linearOutput = matmul(input, weights.t()) + bias + + // Apply ReLU activation + val activated = linearOutput.relu() + + // Apply dropout simulation (multiply by 0.8) + val dropped = activated * 0.8f + + println("Input shape: ${input.shape}") + println("Output shape: ${dropped.shape}") +} +---- + +=== Image Processing Pipeline + +[source,kotlin] +---- +val backend = CpuBackend() + +// RGB image: 224x224x3 +val image = CpuTensorFP32.fromArray( + Shape(224, 224, 3), + FloatArray(224 * 224 * 3) { kotlin.random.Random.nextFloat() * 255f } +) + +with(backend) { + // Normalize to [0, 1] + val normalized = image / 255f + + // Apply mean subtraction (ImageNet means) + val meanSubtracted = normalized - CpuTensorFP32.fromArray( + Shape(3), + floatArrayOf(0.485f, 0.456f, 0.406f) + ) + + // Flatten for fully connected layer + val flattened = meanSubtracted.flatten() + + println("Original shape: ${image.shape}") + println("Processed shape: ${flattened.shape}") +} +---- + +== Performance Tips + +=== Operator Chaining + +Chain operations efficiently within the backend context: + +[source,kotlin] +---- +val backend = CpuBackend() +val input = CpuTensorFP32.fromArray(Shape(100, 50), FloatArray(5000) { it.toFloat() }) + +with(backend) { + // Efficient chaining + val result = input + .relu() // Apply activation + .t() // Transpose + .softmax(dimension = 0) // Normalize along first dimension + + // This is more efficient than separate operations +} +---- + +=== Memory Considerations + +Be mindful of tensor shapes and memory usage: + +[source,kotlin] +---- +// Large tensors - be careful with memory +val largeTensor = CpuTensorFP32.fromArray( + Shape(1000, 1000), + FloatArray(1_000_000) { it.toFloat() } +) + +// Operations create new tensors - manage memory accordingly +with(backend) { + val processed = largeTensor + .relu() // Creates new tensor + .softmax(1) // Creates another new tensor + + // Original largeTensor still exists in memory +} +---- \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index ed1cf6139..9706cdfe0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=sk.ainet.core -VERSION_NAME=0.0.8 +VERSION_NAME=0.0.1 mavenCentralPublishing=true mavenCentralAutomaticPublishing=true diff --git a/settings.gradle.kts b/settings.gradle.kts index 8759d6dc6..b2e55038a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,5 +18,6 @@ rootProject.name = "skainet" include("skainet-core:skainet-tensors-api") include("skainet-core:skainet-tensors") include("skainet-core:skainet-performance") +include("skainet-core:skainet-reflection") include("skainet-nn:skainet-nn-api") include("skainet-nn:skainet-nn-relection") diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorPrinting.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorPrinting.kt new file mode 100644 index 000000000..dbb31c311 --- /dev/null +++ b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorPrinting.kt @@ -0,0 +1,139 @@ +package sk.ainet.core.tensor + +/** + * Generic extension function to print scalar tensors (1D with single element). + */ +public fun Tensor.printScalar(): String { + require(shape.rank == 1 && shape[0] == 1) { + "Tensor must be a scalar (1D with single element), got rank ${shape.rank} with shape ${shape.dimensions.contentToString()}" + } + return this[0].toString() +} + +/** + * Generic extension function to print vector tensors (1D). + * Returns a string representation in the format [a, b, c, ...]. + */ +public fun Tensor.printVector(): String { + require(shape.rank == 1) { "Tensor must be a vector (1D), got rank ${shape.rank}" } + val elements = (0 until shape[0]).map { this[it].toString() } + return "[${elements.joinToString(", ")}]" +} + +/** + * Generic extension function to print matrix tensors (2D). + * Returns a string representation with each row on a separate line. + */ +public fun Tensor.printMatrix(): String { + require(shape.rank == 2) { "Tensor must be a matrix (2D), got rank ${shape.rank}" } + val rows = shape[0] + val cols = shape[1] + val result = StringBuilder() + + result.append("[\n") + for (i in 0 until rows) { + result.append(" [") + for (j in 0 until cols) { + result.append(this[i, j].toString()) + if (j < cols - 1) result.append(", ") + } + result.append("]") + if (i < rows - 1) result.append(",") + result.append("\n") + } + result.append("]") + + return result.toString() +} + +/** + * Generic extension function to print 3D tensors. + * Useful for batch processing, RGB images, etc. + */ +public fun Tensor.print3D(): String { + require(shape.rank == 3) { "Tensor must be 3D, got rank ${shape.rank}" } + val dim0 = shape[0] + val dim1 = shape[1] + val dim2 = shape[2] + val result = StringBuilder() + + result.append("[\n") + for (i in 0 until dim0) { + result.append(" [\n") + for (j in 0 until dim1) { + result.append(" [") + for (k in 0 until dim2) { + result.append(this[i, j, k].toString()) + if (k < dim2 - 1) result.append(", ") + } + result.append("]") + if (j < dim1 - 1) result.append(",") + result.append("\n") + } + result.append(" ]") + if (i < dim0 - 1) result.append(",") + result.append("\n") + } + result.append("]") + + return result.toString() +} + +/** + * Generic extension function to iterate over all tensor elements. + * Useful for custom printing formats or processing all elements. + */ +public fun Tensor.forEachIndexed(action: (indices: IntArray, value: V) -> Unit) { + fun recursiveIterate(currentIndices: IntArray, dimensionIndex: Int) { + if (dimensionIndex == shape.rank) { + // Base case: we've built a complete index + val value = when (currentIndices.size) { + 1 -> this@forEachIndexed[currentIndices[0]] + 2 -> this@forEachIndexed[currentIndices[0], currentIndices[1]] + 3 -> this@forEachIndexed[currentIndices[0], currentIndices[1], currentIndices[2]] + 4 -> this@forEachIndexed[currentIndices[0], currentIndices[1], currentIndices[2], currentIndices[3]] + else -> throw UnsupportedOperationException("Tensors with more than 4 dimensions are not supported for iteration") + } + action(currentIndices, value) + return + } + + // Recursive case: iterate through current dimension + for (i in 0 until shape[dimensionIndex]) { + val newIndices = currentIndices + i + recursiveIterate(newIndices, dimensionIndex + 1) + } + } + + recursiveIterate(intArrayOf(), 0) +} + +/** + * General extension function to print tensors of any dimension. + * Automatically selects the appropriate printing method based on tensor rank. + */ +public fun Tensor.print(): String { + return when (shape.rank) { + 1 if shape[0] == 1 -> printScalar() + 1 -> printVector() + 2 -> printMatrix() + 3 -> print3D() + else -> { + // For higher dimensions, provide a summary + val elementCount = minOf(10, shape.volume) // Show first 10 elements + val elements = mutableListOf() + var count = 0 + + forEachIndexed { _, value -> + if (count < elementCount) { + elements.add(value) + count++ + } + } + + val preview = elements.joinToString(", ") { it.toString() } + val more = if (shape.volume > elementCount) ", ..." else "" + "Tensor(shape=${shape.dimensions.contentToString()}, rank=${shape.rank}) [$preview$more]" + } + } +} \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ainet/core/tensor/backend/CpuBackend.kt b/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ainet/core/tensor/backend/CpuBackend.kt index c3c5b1395..6195b2904 100644 --- a/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ainet/core/tensor/backend/CpuBackend.kt +++ b/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ainet/core/tensor/backend/CpuBackend.kt @@ -11,18 +11,28 @@ import kotlin.math.* public typealias TensorFP32 = Tensor /** - * Extension function to print scalar tensors (represented as 1D tensor with single element). - * Returns a string representation of the scalar value. + * @deprecated Use the generic tensor printing extensions from sk.ainet.core.tensor.printScalar() instead. + * This CPU-specific function will be removed in a future version. */ +@Deprecated( + message = "Use the generic tensor printing extensions instead", + replaceWith = ReplaceWith("this.printScalar()", "sk.ainet.core.tensor.printScalar"), + level = DeprecationLevel.WARNING +) public fun TensorFP32.printScalar(): String { require(shape.rank == 1 && shape[0] == 1) { "Tensor must be a scalar (1D with single element), got rank ${shape.rank} with shape ${shape.dimensions.contentToString()}" } return this[0].toString() } /** - * Extension function to print vector tensors (1D). - * Returns a string representation in the format [a, b, c, ...]. + * @deprecated Use the generic tensor printing extensions from sk.ainet.core.tensor.printVector() instead. + * This CPU-specific function will be removed in a future version. */ +@Deprecated( + message = "Use the generic tensor printing extensions instead", + replaceWith = ReplaceWith("this.printVector()", "sk.ainet.core.tensor.printVector"), + level = DeprecationLevel.WARNING +) public fun TensorFP32.printVector(): String { require(shape.rank == 1) { "Tensor must be a vector (1D), got rank ${shape.rank}" } val elements = (0 until shape[0]).map { this[it] } @@ -30,9 +40,14 @@ public fun TensorFP32.printVector(): String { } /** - * Extension function to print matrix tensors (2D). - * Returns a string representation with each row on a separate line. + * @deprecated Use the generic tensor printing extensions from sk.ainet.core.tensor.printMatrix() instead. + * This CPU-specific function will be removed in a future version. */ +@Deprecated( + message = "Use the generic tensor printing extensions instead", + replaceWith = ReplaceWith("this.printMatrix()", "sk.ainet.core.tensor.printMatrix"), + level = DeprecationLevel.WARNING +) public fun TensorFP32.printMatrix(): String { require(shape.rank == 2) { "Tensor must be a matrix (2D), got rank ${shape.rank}" } val rows = shape[0] @@ -56,14 +71,19 @@ public fun TensorFP32.printMatrix(): String { } /** - * General extension function to print tensors of any dimension. - * Automatically selects the appropriate printing method based on tensor rank and shape. + * @deprecated Use the generic tensor printing extensions from sk.ainet.core.tensor.print() instead. + * This CPU-specific function will be removed in a future version. */ +@Deprecated( + message = "Use the generic tensor printing extensions instead", + replaceWith = ReplaceWith("this.print()", "sk.ainet.core.tensor.print"), + level = DeprecationLevel.WARNING +) public fun TensorFP32.print(): String { - return when { - shape.rank == 1 && shape[0] == 1 -> printScalar() - shape.rank == 1 -> printVector() - shape.rank == 2 -> printMatrix() + return when (shape.rank) { + 1 if shape[0] == 1 -> printScalar() + 1 -> printVector() + 2 -> printMatrix() else -> "Tensor(shape=${shape}, rank=${shape.rank}) - printing not supported for tensors with rank > 2" } }