Sono un po 'imbarazzato ad ammetterlo, ma mi sembra di essere piuttosto sconcertato da quello che dovrebbe essere un semplice problema di programmazione. Sto costruendo un'implementazione di un albero decisionale e sto usando la ricorsione per prendere una lista di campioni etichettati, dividere ricorsivamente l'elenco a metà e trasformarlo in un albero.Codifica ricorsiva della creazione di alberi con loop while + stack
Sfortunatamente, con alberi profondi mi imbatto in errori di overflow dello stack (ha!), Quindi il mio primo pensiero è stato quello di utilizzare le continuazioni per trasformarlo in ricorsione di coda. Sfortunatamente Scala non supporta quel tipo di TCO, quindi l'unica soluzione è usare un trampolino. Un trampolino sembra un po 'inefficiente e speravo che ci sarebbe stata una semplice soluzione imperativa basata su stack per questo problema, ma ho molti problemi a trovarlo.
La versione ricorsiva sembra un po 'come (semplificato):
private def trainTree(samples: Seq[Sample], usedFeatures: Set[Int]): DTree = {
if (shouldStop(samples)) {
DTLeaf(makeProportions(samples))
} else {
val featureIdx = getSplittingFeature(samples, usedFeatures)
val (statsWithFeature, statsWithoutFeature) = samples.partition(hasFeature(featureIdx, _))
DTBranch(
trainTree(statsWithFeature, usedFeatures + featureIdx),
trainTree(statsWithoutFeature, usedFeatures + featureIdx),
featureIdx)
}
}
Quindi, fondamentalmente sto ricorsivamente suddividere la lista in due secondo alcuni caratteristica dei dati, e passando attraverso un elenco di funzioni utilizzate in modo Non ripeto: tutto questo viene gestito nella funzione "getSplittingFeature" in modo che possiamo ignorarlo. Il codice è davvero semplice! Tuttavia, ho difficoltà a trovare una soluzione basata su stack che non usi solo chiusure e diventi effettivamente un trampolino. So che dovremo almeno tenere un po 'di "frame" di argomenti nello stack, ma vorrei evitare le chiamate di chiusura.
Ho capito che dovrei scrivere esplicitamente ciò che il callstack e il contatore del programma gestiscono implicitamente nella soluzione ricorsiva, ma ho difficoltà a farlo senza continuazioni. A questo punto non si parla nemmeno di efficienza, sono solo curioso. Quindi, per favore, non c'è bisogno di ricordarmi che l'ottimizzazione prematura è la radice di tutto il male e la soluzione basata sul trampolino probabilmente funzionerà perfettamente. So che probabilmente lo farò - questo è fondamentalmente un enigma per il proprio bene.
Qualcuno può dirmi qual è la soluzione canonica basata su loop e stack per questo genere di cose?
AGGIORNAMENTO: Sulla base dell'eccellente soluzione di Thipor Kong, ho codificato l'implementazione di un algoritmo while-loops/stack/hashtable che dovrebbe essere una traduzione diretta della versione ricorsiva. Questo è esattamente quello che stavo cercando:
AGGIORNAMENTO FINALE: ho usato indici interi sequenziali, oltre a rimettere tutto in array invece di mappe per prestazioni, aggiunto supporto maxDepth e infine avere una soluzione con lo stesso prestazioni della versione ricorsiva (non sono sicuro circa l'utilizzo della memoria, ma direi meno):
private def trainTreeNoMaxDepth(startingSamples: Seq[Sample], startingMaxDepth: Int): DTree = {
// Use arraybuffer as dense mutable int-indexed map - no IndexOutOfBoundsException, just expand to fit
type DenseIntMap[T] = ArrayBuffer[T]
def updateIntMap[@specialized T](ab: DenseIntMap[T], idx: Int, item: T, dfault: T = null.asInstanceOf[T]) = {
if (ab.length <= idx) {ab.insertAll(ab.length, Iterable.fill(idx - ab.length + 1)(dfault)) }
ab.update(idx, item)
}
var currentChildId = 0 // get childIdx or create one if it's not there already
def child(childMap: DenseIntMap[Int], heapIdx: Int) =
if (childMap.length > heapIdx && childMap(heapIdx) != -1) childMap(heapIdx)
else {currentChildId += 1; updateIntMap(childMap, heapIdx, currentChildId, -1); currentChildId }
// go down
val leftChildren, rightChildren = new DenseIntMap[Int]() // heapIdx -> childHeapIdx
val todo = Stack((startingSamples, Set.empty[Int], startingMaxDepth, 0)) // samples, usedFeatures, maxDepth, heapIdx
val branches = new Stack[(Int, Int)]() // heapIdx, featureIdx
val nodes = new DenseIntMap[DTree]() // heapIdx -> node
while (!todo.isEmpty) {
val (samples, usedFeatures, maxDepth, heapIdx) = todo.pop()
if (shouldStop(samples) || maxDepth == 0) {
updateIntMap(nodes, heapIdx, DTLeaf(makeProportions(samples)))
} else {
val featureIdx = getSplittingFeature(samples, usedFeatures)
val (statsWithFeature, statsWithoutFeature) = samples.partition(hasFeature(featureIdx, _))
todo.push((statsWithFeature, usedFeatures + featureIdx, maxDepth - 1, child(leftChildren, heapIdx)))
todo.push((statsWithoutFeature, usedFeatures + featureIdx, maxDepth - 1, child(rightChildren, heapIdx)))
branches.push((heapIdx, featureIdx))
}
}
// go up
while (!branches.isEmpty) {
val (heapIdx, featureIdx) = branches.pop()
updateIntMap(nodes, heapIdx, DTBranch(nodes(child(leftChildren, heapIdx)), nodes(child(rightChildren, heapIdx)), featureIdx))
}
nodes(0)
}
L'offload non è un'implementazione basata su stack (dove lo stack è nell'heap) concettualmente come il trampolino? – ron
Un po ', ma il trampolino significa che si sta mantenendo uno stack pieno di chiusure, dove spero che ci sia una soluzione che utilizza solo uno stack pieno di dati. Forse dati etichettati come StepOne (a, b, c), StepTwo (a, b, c) o stack multipli o qualcosa del genere, ma nessuna chiamata di funzione sarebbe coinvolta. – lvilnis
Apporta un'altra modifica al mio codice. Lo spazio dei nomi degli id dei nodi viene utilizzato in modo più economico ed è possibile collegare il proprio tipo di id dei nodi (o BigInt, se lo si desidera). –