引言
在這一小節(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)入我的博客主頁