【Scala-ML】使用Scala構建機器學習工做流

引言

在這一小節中。我將介紹基於數據(函數式)的方法來構建數據應用。這裏會介紹monadic設計來建立動態工做流,利用依賴注入這種高級函數式特性來構建輕便的計算工做流。算法

建模過程

在統計學和機率論中,一個模型經過描寫敘述從一個系統中觀察到的數據來表達不論什麼形式的不肯定性。模型使得咱們可以用來判斷規則,進行預測,從數據中學習實用的東西。
對於有經驗的Scala程序猿而言,模型常常和monoid聯繫起來。monoid是一些觀測的集合。當中的操做是實現模型所需的函數。markdown

關於模型的特徵
模型特徵的選擇是從可用變量中發現最小集合來構建模型的過程。數據中常常包括多餘和不相干的特徵,這些多餘特徵並不能提供不論什麼實用信息。因此需要經過特徵選擇將實用的特徵挑選出來。框架


特徵選擇包括兩個詳細步驟dom

  • 搜索新的特徵子集
  • 經過某種評分機制來評估特徵子集

觀測數據是一組隱含特徵(也稱爲隱含變量。latent variables)的間接測量。他們多是噪聲。也可能包括高度的相關性和冗餘。機器學習

直接使用原始觀測進行預測任務常常獲得不許確的結果。使用從觀測數據提取的所有特徵又帶來了計算代價。特徵抽取可以經過去除冗餘或不相關的特徵來下降特徵數量或維度。ide

設計工做流

首先,所選的數學模型是從原始輸入數據中抽取知識的。那麼模型的選擇中需要考慮如下幾個方面:模塊化

  • 業務需求,比方預測結果的精確度
  • 訓練數據和算法的可用性
  • 專業領域的相關知識

而後。從project角度出發。需要選擇一種計算調度框架來處理數據。這需要考慮如下幾個方面:函數

  • 可用資源,如CPU、內存、IO帶寬
  • 實現策略,如迭代和遞歸計算
  • 響應整個過程的需求。如計算時間、中間結果的顯示

如下的圖標給出了計算模型的工做流程:

在這個流程圖中,下游的數據轉換(data transformation)的參數需要依據上游數據轉換的輸出進行配置。Scala的高階函數很適合實現可配置的數據轉換。post

計算框架

建立足夠靈活和可重用的框架的目的是爲了更好地適應不一樣工做流程,支持各類類型的機器學習算法。
Scala經過特質(traits)語法實現了豐富的語言特性,可以經過如下的設計層級來構建複雜的程序框架:
學習

管道操做符(The pipe operator)

數據轉換是對數據進行分類、訓練驗證模型、結果可視化等每個步驟環節的基礎。定義一個符號。表示不一樣類型的數據轉換,而不暴露算法實現的內部狀態。

而管道操做符就是用來表示數據轉換的。

trait PipeOperator[-T, +U] {
  def |>(data: T): Option[U]
}

|>操做符將類型爲T的數據轉換成類型爲U的數據,返回一個Option來處理中間的錯誤和異常。

單子化數據轉換(Monadic data transformation)

接下來需要建立單子化的設計(monadic design)來實現管道操做(pipe operator)。經過單子化設計來包裝類_FCT_FCT類的方法表明了傳統Scala針對集合的高階函數子集。

class _FCT[+T](val _fct: T) {
  def map[U](c: T => U): _FCT[U] = new _FCT[U]( c(_fct))

  def flatMap[U](f: T =>_FCT[U]): _FCT[U] = f(_fct)

  def filter(p: T =>Boolean): _FCT[T] =
  if( p(_fct) ) new _FCT[T](_fct) else zeroFCT(_fct)

  def reduceLeft[U](f: (U,T) => U)(implicit c: T=> U): U =
  f(c(_fct),_fct)

  def foldLeft[U](zero: U)(f: (U, T) => U)(implicit c: T=> U): U =
  f(c(_fct), _fct)

  def foreach(p: T => Unit): Unit = p(_fct)
}

最後。Transform類將PipeOperator實例做爲參數輸入,本身主動調用其操做符。像這樣:

class Transform[-T, +U](val op: PipeOperator[T, U]) extends _FCT[Function[T, Option[U]]](op.|>) {
  def |>(data: T): Option[U] = _fct(data)
}

或許你會對數據轉換Transform的單子化表示背後的緣由表示懷疑。畢竟原本可以經過PipeOperator的實現來建立不論什麼算法。


緣由是Transform含有豐富的方法,使得開發人員可以建立豐富的工做流。


如下的代碼片斷描寫敘述的是使用單子化方法來進行數據轉換組合:

val op = new PipeOperator[Int, Double] {
  def |> (n: Int):Option[Double] =Some(Math.sin(n.toDouble))
}
def g(f: Int =>Option[Double]): (Int=> Long) = {
  (n: Int) => {
    f(n) match {
      case Some(x) => x.toLong
      case None => -1L
    }   
  }
}
val gof = new Transform[Int,Double](op).map(g(_))

這裏使用函數g做爲現有的數據轉換來擴展op。

依賴注入(Dependency injection)

一個由可配置的數據轉換構成的工做流在其不一樣的流程階段都需要動態的模塊化。蛋糕模式(Cake Pattern)是使用混入特質(mix-in traits)來知足可配置計算工做流的一種高級類組合模式。
Scala經過特質這一語法特性使得開發人員可以使用一種靈活的、可重用的方法來建立和管理模塊,特質是可嵌套的、可混入類中的、可堆疊的、可繼承的。

val myApp = new Classification with Validation with PreProcessing {
  val filter = ..
}
val myApp = new Clustering with Validation with PreProcessing {
  val filter = ..
}

對於上面兩個應用來講。都需要數據的預處理和驗證模塊,在代碼中都反覆定義了filter方法,使得代碼反覆、缺少靈活性。當特質在組合中存在依賴性時。這個問題凸現出來。

混入的線性化
在混入的特質中。方法調用遵循從右到左的順序:
- trait B extends A
- trait C extends A
- class M extends N with C with B
Scala編譯器依照M => B => C => A => N的線性順序來實現

trait PreProcessingWithValidation extends PreProcessing {
  self: Validation =>
  val filter = ..
}

val myApp = new Classification with PreProcessingWithValidation {
  val validation: Validation
}

在PreProcessingWithValidation中使用self類型來解決上述問題。
(tips:原書的內容在這裏我沒怎麼搞清楚,不知道是經過自身類型混入了Validation後filter方法詳細是怎麼實現的,以及實例化Classification時混入PreProcessingWithValidation難道不需要混入Validation嗎?我表示疑問)

工做流模塊

由PipeOperator定義的數據轉換動態地嵌入了經過抽象val定義的模塊中。如下咱們定義工做流的三個階段:

trait PreprocModule[-T, +U] { val preProc: PipeOperator[T, U] }
trait ProcModule[-T, +U] { val proc: PipeOperator[T, U] }
trait PostprocModule[-T, +U] { val postProc: PipeOperator[T, U] }

上面的特質(模塊)僅包括一個抽象值,蛋糕模式的一個特色是用模塊內部封裝的類型初始化抽象值來運行嚴格的模塊化:

trait ProcModule[-T, +U] {
  val proc: PipeOperator [T, U]
  class Classification[-T, +U] extends PipeOperator [T,U] { }
}

構建框架的一個目的是贊成開發人員可以從不論什麼工做流中獨立建立數據轉換(繼承自PipeOperator)。

工做流工廠

接下來就是將不一樣的模塊寫入一個工做流中。經過上一小節中的三個特質的堆疊做爲自身引用來實現:

class WorkFlow[T, U, V, W] {
  self: PreprocModule[T,U] with ProcModule[U,V] with PostprocModule[V,W] =>

  def |> (data: T): Option[W] = {
    preProc |> data match {
      case Some(input) => {
        proc |> input match {
          case Some(output) => postProc |> output
          case None => { … }
        }
      }
      case None => { … }
    }
  }
}

如下介紹怎樣詳細地實現一個工做流。


首先經過繼承PipeOperator來定義集中數據轉換:

class Sampler(val samples: Int) extends PipeOperator[Double => Double, DblVector] {
  override def |> (f: Double => Double): Option[DblVector] =
  Some(Array.tabulate(samples)(n => f(n.toDouble/samples)) )
}

class Normalizer extends PipeOperator[DblVector, DblVector] {
  override def |> (data: DblVector): Option[DblVector] =
  Some(Stats[Double](data).normalize)
}

class Reducer extends PipeOperator[DblVector, Int] {
  override def |> (data: DblVector): Option[Int] =
  Range(0, data.size) find(data(_) == 1.0)
}


工做流工廠由這個UML類圖描寫敘述。
終於經過動態地初始化抽象值preProc、proc和postProc來實例化工做流。

val dataflow = new Workflow[Double => Double, DblVector, DblVector, Int]
  with PreprocModule[Double => Double, DblVector]
  with ProcModule[DblVector, DblVector]
  with PostprocModule[DblVector, Int] {
    val preProc: PipeOperator[Double => Double,DblVector] = new Sampler(100) //1
    val proc: PipeOperator[DblVector,DblVector]= new Normalizer //1
    val postProc: PipeOperator[DblVector,Int] = new Reducer//1
}

dataflow |> ((x: Double) => Math.log(x+1.0)+Random.nextDouble) match {
  case Some(index) => …

參考資料

《Scala for Machine Learning》Chapter 2

轉載請註明做者Jason Ding及其出處
jasonding.top
Github博客主頁(http://blog.jasonding.top/)
CSDN博客(http://blog.csdn.net/jasonding1354)
簡書主頁(http://www.jianshu.com/users/2bd9b48f6ea8/latest_articles)
Google搜索jasonding1354進入個人博客主頁

相關文章
相關標籤/搜索