【Scala-ML】使用Scala構(gòu)建機(jī)器學(xué)習(xí)工作流

引言

在這一小節(jié)中,我將介紹基于數(shù)據(jù)(函數(shù)式)的方法來構(gòu)建數(shù)據(jù)應(yīng)用。這里會介紹monadic設(shè)計來創(chuàng)建動態(tài)工作流,利用依賴注入這樣的面向?qū)ο蟮募夹g(shù)來構(gòu)建可配置的計算工作流。

建模過程

在統(tǒng)計學(xué)和概率論中,一個模型通過描述從一個系統(tǒng)中觀察到的數(shù)據(jù)來表達(dá)任何形式的不確定性,模型使得我們可以用來推斷規(guī)則,進(jìn)行預(yù)測,從數(shù)據(jù)中學(xué)習(xí)有用的東西。
對于有經(jīng)驗(yàn)的Scala程序員而言,模型常常和monoid聯(lián)系起來。monoid是一些觀測的集合,其中的操作是實(shí)現(xiàn)模型所需的函數(shù)。

關(guān)于模型的特征
模型特征的選擇是從可用變量中發(fā)現(xiàn)最小集合來構(gòu)建模型的過程。數(shù)據(jù)中常常包含多余和不相干的特征,這些多余特征并不能提供任何有用信息,所以需要通過特征選擇將有用的特征挑選出來。
特征選擇包含兩個具體步驟

  • 搜索新的特征子集
  • 通過某種評分機(jī)制來評估特征子集

觀測數(shù)據(jù)是一組隱含特征(也稱為隱含變量,latent variables)的間接測量,他們可能是噪聲,也可能包含高度的相關(guān)性和冗余。直接使用原始觀測進(jìn)行預(yù)測任務(wù)常常得到不準(zhǔn)確的結(jié)果,使用從觀測數(shù)據(jù)提取的所有特征又帶來了計算代價。特征抽取可以通過去除冗余或不相關(guān)的特征來減少特征數(shù)量或維度。

設(shè)計工作流

首先,所選的數(shù)學(xué)模型是從原始輸入數(shù)據(jù)中抽取知識的,那么模型的選擇中需要考慮以下幾個方面:

  • 業(yè)務(wù)需求,比如預(yù)測結(jié)果的準(zhǔn)確度
  • 訓(xùn)練數(shù)據(jù)和算法的可用性
  • 專業(yè)領(lǐng)域的相關(guān)知識

然后,從工程角度出發(fā),需要選擇一種計算調(diào)度框架來處理數(shù)據(jù),這需要考慮以下幾個方面:

  • 可用資源,如CPU、內(nèi)存、IO帶寬
  • 實(shí)現(xiàn)策略,如迭代和遞歸計算
  • 響應(yīng)整個過程的需求,如計算時間、中間結(jié)果的顯示

下面的圖標(biāo)給出了計算模型的工作流程:



在這個流程圖中,下游的數(shù)據(jù)轉(zhuǎn)換(data transformation)的參數(shù)需要根據(jù)上游數(shù)據(jù)轉(zhuǎn)換的輸出進(jìn)行配置,Scala的高階函數(shù)非常適合實(shí)現(xiàn)可配置的數(shù)據(jù)轉(zhuǎn)換。

計算框架

創(chuàng)建足夠靈活和可重用的框架的目的是為了更好地適應(yīng)不同工作流程,支持各種類型的機(jī)器學(xué)習(xí)算法。
Scala通過特質(zhì)(traits)語法實(shí)現(xiàn)了豐富的語言特性,可以通過下面的設(shè)計層級來構(gòu)建復(fù)雜的程序框架:


管道操作符(The pipe operator)

數(shù)據(jù)轉(zhuǎn)換是對數(shù)據(jù)進(jìn)行分類、訓(xùn)練驗(yàn)證模型、結(jié)果可視化等每個步驟環(huán)節(jié)的基礎(chǔ)。定義一個符號,表示不同類型的數(shù)據(jù)轉(zhuǎn)換,而不暴露算法實(shí)現(xiàn)的內(nèi)部狀態(tài)。而管道操作符就是用來表示數(shù)據(jù)轉(zhuǎn)換的。

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

|>操作符將類型為T的數(shù)據(jù)轉(zhuǎn)換成類型為U的數(shù)據(jù),返回一個Option來處理中間的錯誤和異常。

單子化數(shù)據(jù)轉(zhuǎn)換(Monadic data transformation)

接下來需要創(chuàng)建單子化的設(shè)計(monadic design)來實(shí)現(xiàn)管道操作(pipe operator)。通過單子化設(shè)計來包裝類_FCT_FCT類的方法代表了傳統(tǒng)Scala針對集合的高階函數(shù)子集。

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實(shí)例作為參數(shù)輸入,自動調(diào)用其操作符,像這樣:

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

也許你會對數(shù)據(jù)轉(zhuǎn)換Transform的單子化表示背后的原因表示懷疑,畢竟本來可以通過PipeOperator的實(shí)現(xiàn)來創(chuàng)建任何算法。
原因是Transform含有豐富的方法,使得開發(fā)者可以創(chuàng)建豐富的工作流。
下面的代碼片段描述的是使用單子化方法來進(jìn)行數(shù)據(jù)轉(zhuǎn)換組合:

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(_))

這里使用函數(shù)g作為現(xiàn)有的數(shù)據(jù)轉(zhuǎn)換來擴(kuò)展op。

依賴注入(Dependency injection)

一個由可配置的數(shù)據(jù)轉(zhuǎn)換構(gòu)成的工作流在其不同的流程階段都需要動態(tài)的模塊化。蛋糕模式(Cake Pattern)是使用混入特質(zhì)(mix-in traits)來滿足可配置計算工作流的一種高級類組合模式。
Scala通過特質(zhì)這一語法特性使得開發(fā)者能夠使用一種靈活的、可重用的方法來創(chuàng)建和管理模塊,特質(zhì)是可嵌套的、可混入類中的、可堆疊的、可繼承的。

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

對于上面兩個應(yīng)用來說,都需要數(shù)據(jù)的預(yù)處理和驗(yàn)證模塊,在代碼中都重復(fù)定義了filter方法,使得代碼重復(fù)、缺乏靈活性。當(dāng)特質(zhì)在組合中存在依賴性時,這個問題凸現(xiàn)出來。

混入的線性化
在混入的特質(zhì)中,方法調(diào)用遵循從右到左的順序:

  • trait B extends A
  • trait C extends A
  • class M extends N with C with B
    Scala編譯器按照M => B => C => A => N的線性順序來實(shí)現(xiàn)
trait PreProcessingWithValidation extends PreProcessing {
  self: Validation =>
  val filter = ..
}

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

在PreProcessingWithValidation中使用self類型來解決上述問題。
(tips:原書的內(nèi)容在這里我沒怎么搞清楚,不知道是通過自身類型混入了Validation后filter方法具體是怎么實(shí)現(xiàn)的,以及實(shí)例化Classification時混入PreProcessingWithValidation難道不需要混入Validation嗎?我表示疑問)

工作流模塊

由PipeOperator定義的數(shù)據(jù)轉(zhuǎn)換動態(tài)地嵌入了通過抽象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] }

上面的特質(zhì)(模塊)僅包含一個抽象值,蛋糕模式的一個特點(diǎn)是用模塊內(nèi)部封裝的類型初始化抽象值來執(zhí)行嚴(yán)格的模塊化:

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

構(gòu)建框架的一個目的是允許開發(fā)者可以從任何工作流中獨(dú)立創(chuàng)建數(shù)據(jù)轉(zhuǎn)換(繼承自PipeOperator)。

工作流工廠

接下來就是將不同的模塊寫入一個工作流中,通過上一小節(jié)中的三個特質(zhì)的堆疊作為自身引用來實(shí)現(xiàn):

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 => { … }
    }
  }
}

下面介紹如何具體地實(shí)現(xiàn)一個工作流。
首先通過繼承PipeOperator來定義集中數(shù)據(jù)轉(zhuǎn)換:

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類圖描述。
最終通過動態(tài)地初始化抽象值preProc、proc和postProc來實(shí)例化工作流。

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

轉(zhuǎn)載請注明作者Jason Ding及其出處
jasonding.top
Github博客主頁(http://blog.jasonding.top/)
CSDN博客(http://blog.csdn.net/jasonding1354)
簡書主頁(http://www.itdecent.cn/users/2bd9b48f6ea8/latest_articles)
Google搜索jasonding1354進(jìn)入我的博客主頁

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容