DTW method realization #517

Open
EjenY-Poltavchiny wants to merge 19 commits from mrFendel/ejeny_branch_ into dev
Showing only changes of commit 455c9d188d - Show all commits

View File

@ -5,70 +5,35 @@
package space.kscience.kmath.series package space.kscience.kmath.series
import space.kscience.kmath.PerformancePitfall
import space.kscience.kmath.nd.* import space.kscience.kmath.nd.*
import space.kscience.kmath.nd.DoubleBufferND import space.kscience.kmath.nd.DoubleBufferND
import space.kscience.kmath.nd.ShapeND import space.kscience.kmath.nd.ShapeND
import space.kscience.kmath.nd.ndAlgebra
import space.kscience.kmath.operations.DoubleField
import space.kscience.kmath.structures.DoubleBuffer
import kotlin.math.abs import kotlin.math.abs
public const val LEFT_OFFSET : Int = -1
public const val BOTTOM_OFFSET : Int = 1
public const val DIAGONAL_OFFSET : Int = 0
// TODO: Change container for alignMatrix to kmath special ND structure
public data class DynamicTimeWarpingData(val totalCost : Double = 0.0,
val alignMatrix : IntBufferND = IntRingOpsND.structureND(ShapeND(0, 0)) { (i, j) -> 0}) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as DynamicTimeWarpingData
if (totalCost != other.totalCost) return false
return true
}
}
/** /**
* costMatrix calculates special matrix of costs alignment for two series. * Offset constants which will be used later. Added them for avoiding "magical numbers" problem.
* Formula: costMatrix[i, j] = euqlidNorm(series1(i), series2(j)) + min(costMatrix[i - 1, j],
* costMatrix[i, j - 1],
* costMatrix[i - 1, j - 1]).
* There is special cases for i = 0 or j = 0.
*/ */
internal const val LEFT_OFFSET : Int = -1
internal const val BOTTOM_OFFSET : Int = 1
internal const val DIAGONAL_OFFSET : Int = 0
public fun costMatrix(series1 : DoubleBuffer, series2 : DoubleBuffer) : DoubleBufferND {
val dtwMatrix = DoubleField.ndAlgebra.structureND(ShapeND(series1.size, series2.size)) { /**
(row, col) -> abs(series1[row] - series2[col]) * Public class to store result of method. Class contains total penalty cost for series alignment.
} * Also, this class contains align matrix (which point of the first series matches to point of the other series).
for ( (row, col) in dtwMatrix.indices) { */
dtwMatrix[row, col] += when { public data class DynamicTimeWarpingData(val totalCost : Double = 0.0,
row == 0 && col == 0 -> 0.0 val alignMatrix : DoubleBufferND = DoubleFieldOpsND.structureND(ShapeND(0, 0)) { (_, _) -> 0.0})
row == 0 -> dtwMatrix[row, col - 1]
col == 0 -> dtwMatrix[row - 1, col]
else -> minOf(
dtwMatrix[row, col - 1],
dtwMatrix[row - 1, col],
dtwMatrix[row - 1, col - 1]
)
}
}
return dtwMatrix
}
/** /**
* PathIndices class for better code perceptibility. * PathIndices class for better code perceptibility.
* Special fun moveOption represent offset for indices. Arguments of this function * Special fun moveOption represent offset for indices. Arguments of this function
* is flags for bottom, diagonal or left offsets respectively. * is flags for bottom, diagonal or left offsets respectively.
*/ */
internal data class PathIndices (var id_x: Int, var id_y: Int) {
public data class PathIndices (var id_x: Int, var id_y: Int) { fun moveOption (direction: Int) {
public fun moveOption (direction: Int) {
when(direction) { when(direction) {
BOTTOM_OFFSET -> id_x-- BOTTOM_OFFSET -> id_x--
DIAGONAL_OFFSET -> { DIAGONAL_OFFSET -> {
@ -85,16 +50,36 @@ public data class PathIndices (var id_x: Int, var id_y: Int) {
* Final DTW method realization. Returns alignment matrix * Final DTW method realization. Returns alignment matrix
* for two series comparing and penalty for this alignment. * for two series comparing and penalty for this alignment.
*/ */
@OptIn(PerformancePitfall::class)
public fun dynamicTimeWarping(series1 : DoubleBuffer, series2 : DoubleBuffer) : DynamicTimeWarpingData { public fun DoubleFieldOpsND.dynamicTimeWarping(series1 : Series<Double>, series2 : Series<Double>) : DynamicTimeWarpingData {
var cost = 0.0 var cost = 0.0
var pathLength = 0 var pathLength = 0
val costMatrix = costMatrix(series1, series2) // Special matrix of costs alignment for two series.
val alignMatrix: IntBufferND = IntRingOpsND.structureND(ShapeND(series1.size, series2.size)) {(row, col) -> 0} val costMatrix = structureND(ShapeND(series1.size, series2.size)) {
(row, col) -> abs(series1[row] - series2[col])
}
// Formula: costMatrix[i, j] = euqlidNorm(series1(i), series2(j)) +
// min(costMatrix[i - 1, j], costMatrix[i, j - 1], costMatrix[i - 1, j - 1]).
for ( (row, col) in costMatrix.indices) {
costMatrix[row, col] += when {
// There is special cases for i = 0 or j = 0.
row == 0 && col == 0 -> 0.0
row == 0 -> costMatrix[row, col - 1]
col == 0 -> costMatrix[row - 1, col]
else -> minOf(
costMatrix[row, col - 1],
costMatrix[row - 1, col],
costMatrix[row - 1, col - 1]
)
}
}
// alignMatrix contains non-zero values at position where two points from series matches
// Values are penalty for concatenation of current points.
val alignMatrix = structureND(ShapeND(series1.size, series2.size)) {(_, _) -> 0.0}
val indexes = PathIndices(alignMatrix.indices.shape.first() - 1, alignMatrix.indices.shape.last() - 1) val indexes = PathIndices(alignMatrix.indices.shape.first() - 1, alignMatrix.indices.shape.last() - 1)
with(indexes) { with(indexes) {
alignMatrix[id_x, id_y] = 1 alignMatrix[id_x, id_y] = costMatrix[id_x, id_y]
cost += costMatrix[id_x, id_y] cost += costMatrix[id_x, id_y]
pathLength++ pathLength++
while (id_x != 0 || id_y != 0) { while (id_x != 0 || id_y != 0) {
@ -109,7 +94,7 @@ public fun dynamicTimeWarping(series1 : DoubleBuffer, series2 : DoubleBuffer) :
moveOption(DIAGONAL_OFFSET) moveOption(DIAGONAL_OFFSET)
} }
} }
alignMatrix[id_x, id_y] = 1 alignMatrix[id_x, id_y] = costMatrix[id_x, id_y]
cost += costMatrix[id_x, id_y] cost += costMatrix[id_x, id_y]
pathLength++ pathLength++
} }