-
Notifications
You must be signed in to change notification settings - Fork 0
KNN test
Piotr Sokólski edited this page Aug 25, 2015
·
4 revisions
//miara odległości
val distFn = choose from List(normDistance(1).asRep, normDistance(2).asRep, hammingDistance.asRep)
//k w *k*nn
val k = choose between 1 and 5
//dane - plik csv wczytywany przez drilla z dodaną kolumną folds do cross-validacji
val ds = load("/Users/Pietras/Downloads/train-vowels.csv")
//funkcja (non-staged) pobierająca wiersze ze zbioru danych, filtrująca je po kolumnie fold (do cross-validacji) i opakowująca je w Feature Vector
def dataSet(filter: Row[Int] => Rep[Boolean]) = ds.filter(Tuple1("columns[11]".::[Int]))(filter).map({(row: Rep[UntypedTup]) =>
val vec = row.liftMap(rowi => FeatureVec(rowi[Double](0 to 9), rowi.get[Int](10)))
vec as 'features
})
val experiment = optimize (distFn, k) map { case Tup(dist, k) =>
//4-fold cross-validation - dla każdego fold
val avgF1 = List(1, 2, 3, 4).asTable.map { case Tup(Tuple1(fold)) =>
//zbiór testowy - 25% całości
val test = dataSet(row => row(0) === fold)
//zbiór treningowy - 75% całości
val train = dataSet(row => row(0) !== fold)
//M-tree - szybsze wyszukiwanie najbliższych sąsiadów, zbudowane dla zbioru treningowego
val tree = (makeTree _).pure.apply((dist, train))
//wynikiem wykonania algorytmu knn jest confusion matrix dla zbioru testowego
val confMatrix: Rep[DenseMatrix[Double]] = (knn _).pure.apply((k, tree, test))
//obliczam f1 score dla confusion matrix. `.liftMap` służy do wykonania natywnej (skompilowanej przez kompilator scali) funkcji na wartości należącej do języka
confMatrix.liftMap(mx => -f1_accuracy(mx)._1) as 'result
}.avg //oblicz średnią dla całości
(k as 'k, dist as 'distFn, avgF1 as 'result)
} by('result) limit(12) //optymalizuj po kolumnie result, wykonaj 12 iteracji optymalizatora
store(experiment)
dump()
Wynik:
+---+------------------+---------------------+
| k | distFn | result |
+---+------------------+---------------------+
| 1 | 1.0. normDistace | -1.0000000000000000 |
| 2 | 1.0. normDistace | -1.0000000000000000 |
| 3 | 1.0. normDistace | -0.9981027667984192 |
| 4 | 1.0. normDistace | -0.9981027667984192 |
| 5 | 1.0. normDistace | -0.9884042044911612 |
| 1 | 2.0. normDistace | -1.0000000000000000 |
| 2 | 2.0. normDistace | -1.0000000000000000 |
| 3 | 2.0. normDistace | -0.9981027667984192 |
| 4 | 2.0. normDistace | -1.0000000000000000 |
| 5 | 2.0. normDistace | -0.9847355970774945 |
| 1 | hammingDistance | -1.0000000000000000 |
| 2 | hammingDistance | -1.0000000000000000 |
+---+------------------+---------------------+
Niestety akurat te dane na których trenowałem nie wyglądają za ciekawie...
alternatywnie optimize()
można też użyć np w taki sposób
...
//parametry distFn i k aplikowane są "bezpośrednio" (poza pętlą optimize)
val tree = (makeTree _).pure.apply((distFn, train))
//teraz confusion matrix jest sparametryzowane - możemy optymalizować po tej zmiennej
val confMatrix: Rep[DenseMatrix[Double]] = (knn _).pure.apply((k, tree, test))
val experiment = optimize(confMatrix) map { case Tup(Tuple1(m: Rep[DenseMatrix[Double]])) =>
m.liftMap(mx => -f1_accuracy(mx)._1) as 'result
} by 'result
...
Dodatkowe (biblioteczne) funkcje
case class FeatureVec(features: Vector[Double], klass: Int)
def normDistance(n: Double) = new DistanceFunction[FeatureVec] {
override def calculate(data1: FeatureVec, data2: FeatureVec): Double = norm(data1.features - data2.features, n)
}
def hammingDistance = new DistanceFunction[FeatureVec] {
override def calculate(data1: FeatureVec, data2: FeatureVec): Double = {
if (data1.features.length != data2.features.length) throw new Exception("cannot compute hamming distance for vectors of different length")
var distance = 0
zipValues(data1.features, data2.features).foreach {(x1, x2) => if (x1 != x2) distance+=1}
distance
}
}
def makeTree(dist: DistanceFunction[FeatureVec], train: TableImpl) = {
val tree = new MTree(dist, null)
train.toIterator.foreach(row => tree.add(row(0).asInstanceOf[FeatureVec]))
tree
}
def knn(k: Int, tree: MTree[FeatureVec], test: TableImpl) = {
val confMatrix = DenseMatrix.zeros[Double](11, 11)
test.toIterator.foreach { row =>
val fv = row(0).asInstanceOf[FeatureVec]
val query = tree.getNearestByLimit(fv, k)
val votes = query.iterator().asScala
.foldLeft(Map.empty[Int, Int].withDefaultValue(0))((acc, vote) => acc + (vote.data.klass -> (acc(vote.data.klass) + 1)))
val voted = votes.maxBy({ case (klass, count) => count })._1
confMatrix(fv.klass, voted) += 1.0
}
confMatrix
}
Biblioteczne funkcje napisane są bezpośrednio w scali i kompilowane przez scalac, interpreter wywołuje je używając refleksji.