나는이 사실을 인정하기 위해 약간 당황 스럽지만 간단한 프로그래밍 문제가 무엇인지에 의해 꽤 난처한 것처럼 보입니다. 의사 결정 트리 구현을 구축하고 있으며 재귀를 사용하여 레이블이 지정된 샘플의 목록을 가져 왔으며 반복적으로 목록을 반으로 분할하고이를 트리로 전환했습니다.while 루프 + 스택을 사용한 재귀 트리 작성
불행히도 깊은 나무가 쌓여 스택 오버플로 오류 (하!)가 발생합니다. 따라서 첫 번째 생각은 연속을 사용하여 꼬리 재귀로 바꾸는 것입니다. 불행히도 스칼라는 이러한 종류의 TCO를 지원하지 않으므로 유일한 해결책은 트램펄린을 사용하는 것입니다. 트램펄린은 다소 비효율적 인 것처럼 보이고이 문제에 대한 간단한 스택 기반 명령형 솔루션이 필요할 것으로 기대했지만 문제를 찾는 데 많은 어려움을 겪고 있습니다. 재귀 버전은 같은 종류의 보이는
(간체) :
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)
}
}
을 그래서 기본적으로 내가 재귀 적 데이터의 일부 기능에 따라 두 가지로 목록을 세분하는 등 사용 기능의 목록을 통과하고있어 나는 반복하지 않는다 - 그것은 "getSplittingFeature"함수에서 모두 처리되므로 무시할 수있다. 코드는 정말 간단합니다! 여전히 클로저를 사용하지 않고 효과적으로 트램펄린이되는 스택 기반 솔루션을 찾는 데 문제가 있습니다. 우리는 적어도 스택의 인수 "프레임"주위에 보관해야 할 것이다 알고 있지만 폐쇄 호출을 피하기 위해 싶습니다.
나는 callstack과 프로그램 카운터가 내게 재귀 적 솔루션에 대해 명시 적으로 처리해야한다는 것을 알게되었다. 그러나 나는 계속적으로 문제가있다. 이 시점에서 효율성은 거의 없습니다. 단지 궁금합니다. 조기 최적화가 모든 악의 뿌리이고 트램펄린 기반 솔루션이 잘 작동 할 것이라는 점을 상기시켜주십시오. 아마도 그럴 것입니다. 이것은 기본적으로 자신을위한 퍼즐입니다.
누군가 이런 종류의 표준적인 while-loop-and-stack-based 솔루션이 무엇인지 말해 줄 수 있습니까?
업데이트 : Thipor Kong의 탁월한 솔루션을 기반으로, 재귀 버전의 직접 변환이어야하는 알고리즘의 while-loops/stacks/hashtable 기반 구현을 작성했습니다. 이것은 정확히 내가 뭘 찾고 있었는지 :
최종 업데이트 : 필자는 순차적 인 정수 인덱스를 사용했으며, 성능 향상을 위해 맵 대신 배열로 모든 것을 다시 넣었고, maxDepth 지원을 추가했으며, 마지막으로 동일한 솔루션을 사용했습니다 Wikipedia에 설명
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)
}
스택 기반 구현 (스택이 힙에있는 경우)에서 트램 폴링과 개념적으로 오프로드되지 않습니까? – ron
일종의,하지만 trampolining 의미 스택을 가득 차있는 폐쇄, 어디에만 전체 데이터 스택을 사용하는 솔루션을 바라고 있어요. 아마도 StepOne (a, b, c), StepTwo (a, b, c) 또는 다중 스택 또는 뭔가와 같은 데이터로 표시되지만 함수 호출은 포함되지 않습니다. – lvilnis
내 코드를 또 변경했습니다. 노드 ID의 이름 공간은보다 경제적으로 사용되며 노드 ID (또는 원하는 경우 BigInt)의 유형을 사용자가 직접 플러그인 할 수 있습니다. –