Skip to content
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.

Clone this wiki locally