面向开发人员的机器学习指南(二)

转载:http://blog.jobbole.com/109702/

 

注意,该数据读取器将数值从英制单位转换为公制单位。这对 OLS 的应用没有什么大影响,不过我们还是采用更为常用的公制单位。

这样操作之后我们得到一个数组 Array[Array[Double]],该数组包含了数据点和 Array[Double] 值,该值代表男性或女性。这种格式既有利于将数据绘图,也有利于将数据导入机器学习算法中。

我们首先看看数据是什么样的。为此,用下列代码将数据绘成图。

 

object LinearRegressionExample extends SimpleSwingApplication {
  def top = new MainFrame {
    title = "Linear Regression Example"
    val basePath = "/Users/.../OLS_Regression_Example_3.csv"

    val testData = getDataFromCSV(new File(basePath))

    val plotData = (testData._1 zip testData._2).map(x => Array(x._1(1) ,x._2))
    val maleFemaleLabels = testData._1.map( x=> x(0).toInt)
    val plot =  ScatterPlot.plot(   plotData,
                                    maleFemaleLabels,
                                    '@',
                                    Array(Color.blue, Color.green)
                                 )
    plot.setTitle("Weight and heights for male and females")
    plot.setAxisLabel(0,"Heights")
    plot.setAxisLabel(1,"Weights")

    peer.setContentPane(plot)
    size = new Dimension(400, 400)
  }

 如果你执行上面这段代码,就会弹出一个窗口显示以下右边那幅图像。注意当代码运行时,你可以滚动鼠标来放大和缩小图像。



 



 在这幅图像中,绿色代表女性,蓝色代表男性,可以看到,男女的身高和体重有很大部分是重叠的。因此,如果我们忽略男女性别,数据看上去依旧是呈线性的(如图所示)。然而,若不考虑男女性别差异,模型就不够精确。

在本例中,找出这种区别(将数据依性别分组)是小事一桩,然而,你可能会碰到一些其中的数据区分不那么明显的数据集。意识到这种可能性对数据分组是有帮助的,从而有助于改善机器学习应用程序的性能。

既然我们已经考察过数据,也知道我们确实可以建立一条回归线来拟合数据,现在就该训练模型了。Smile 库提供了普通最小二乘算法,我们可以用如下代码轻松调用:

 

val olsModel = new OLS(testData._1,testData._2)

 有了这个 OLS 模型,我们现在可以根据某人的身高和性别预测其体重了:

println("Prediction for Male of 1.7M: " +olsModel.predict(Array(0.0,170.0)))
println("Prediction for Female of 1.7M:" + olsModel.predict(Array(1.0,170.0)))
println("Model Error:" + olsModel.error())

 结果如下:

 

Prediction for Male of 1.7M: 79.14538559840447
    Prediction for Female of 1.7M:70.35580395758966
    Model Error:4.5423150758157185

 回顾前文的分类算法,它有一个能够反映模型性能的先验值。回归分析是一种更强大的统计方法,它可以给出一个实际误差。这个值反映了偏离拟合回归线的平均程度,因此可以说,在这个模型中,一个身高1.70米的男性的预测体重是 79.15kg ± 4.54kg,4.54 为误差值。注意,如果不考虑数据的男女差异,这一误差会增加到 5.5428。换言之,考虑了数据的男女差异后,模型在预测时,精确度提高了 ±1kg

最后一点,Smile 库也提供了一些关于模型的统计信息。R平方值是模型的均方根误差(RMSE)与平均函数的 RMSE 之比。这个值介于 0 与 1 之间。假如你的模型能够准确的预测每一个数据点,R平方值就是 1,如果模型的预测效果比平均函数差,则该值为 0。在机器学习领域中,通常将该值乘以 100,代表模型的精确度。它是一个归一化值,所以可以用来比较不同模型的性能。

本部分总结了线性回归分析的过程,如果你还想了解如何将回归分析应用于非线性数据,请随时学习下一个实例“应用文本回归尝试畅销书排行预测”。

应用文本回归尝试预测最畅销书排行

在实例“根据身高预测体重”中,我们介绍了线性回归的概念。然而,有时候需要将回归分析应用到像文本这类的非数字数据中去。

在本例中,我们将通过尝试预测最畅销的 100 本 O’Reilly 公司出版的图书,说明如何应用文本回归。此外,我们还介绍在本例的特殊情况下应用文本回归无法解决问题。原因仅仅是这些数据中不含有可以被我们的测试数据利用的信号。即使如此,本例也并非一无是处,因为在实践中,数据可能会含有实际信号,该信号可以被这里要介绍的文本回归检测到。

本例使用到的数文件可以在这里下载。除了 Smile 库,本例也会使用 Scala-csv 库,因为 csv 中包含带逗号的字符串。我们从获取需要的数据开始:

 

object TextRegression  {

  def main(args: Array[String]): Unit = {

    //Get the example data
    //获取案例数据
      val basePath = "/users/.../TextRegression_Example_4.csv"
      val testData = getDataFromCSV(new File(basePath))
  }

  def getDataFromCSV(file: File) : List[(String,Int,String)]= {
    val reader = CSVReader.open(file)
    val data = reader.all()

    val documents = data.drop(1).map(x => (x(1),x(3)toInt,x(4)))
    return documents
  }
}

 现在我们得到了 O’Reilly 出版社最畅销100部图书的书名、排序和详细说明。然而,当涉及某种回归分析时,我们需要数字数据。这就是问什么我们要建立一个文档词汇矩阵 (DTM)。注意这个 DTM 与我们在垃圾邮件分类实例中建立的词汇文档矩阵 (TDM) 是类似的。区别在于,DTM 存储的是文档记录,包含文档中的词汇,相反,TDM 存储的是词汇记录,包含这些词汇所在的一系列文档。

我们自己用如下代码生成 DTM:

 

import java.io.File
import scala.collection.mutable

class DTM {

  var records: List[DTMRecord] = List[DTMRecord]()
  var wordList: List[String] = List[String]()

  def addDocumentToRecords(documentName: String, rank: Int, documentContent: String) = {
    //Find a record for the document
    //找出一条文档记录
    val record = records.find(x => x.document == documentName)
    if (record.nonEmpty) {
      throw new Exception("Document already exists in the records")
    }

    var wordRecords = mutable.HashMap[String, Int]()
    val individualWords = documentContent.toLowerCase.split(" ")
    individualWords.foreach { x =>
      val wordRecord = wordRecords.find(y => y._1 == x)
      if (wordRecord.nonEmpty) {
        wordRecords += x -> (wordRecord.get._2 + 1)
      }
      else {
        wordRecords += x -> 1
        wordList = x :: wordList
      }
    }
    records = new DTMRecord(documentName, rank, wordRecords) :: records
  }

  def getStopWords(): List[String] = {
    val source = scala.io.Source.fromFile(new File("/Users/.../stopwords.txt"))("latin1")
    val lines = source.mkString.split("n")
    source.close()
    return lines.toList
  }

  def getNumericRepresentationForRecords(): (Array[Array[Double]], Array[Double]) = {
    //First filter out all stop words:
    //首先过滤出所有停用词
    val StopWords = getStopWords()
    wordList = wordList.filter(x => !StopWords.contains(x))

    var dtmNumeric = Array[Array[Double]]()
    var ranks = Array[Double]()

    records.foreach { x =>
      //Add the rank to the array of ranks
      //将评级添加到排序数组中
      ranks = ranks :+ x.rank.toDouble

      //And create an array representing all words and their occurrences 
      //for this document:
      //为该文档创建一个数组,表示所有单词及其出现率 
      var dtmNumericRecord: Array[Double] = Array()
      wordList.foreach { y =>

        val termRecord = x.occurrences.find(z => z._1 == y)
        if (termRecord.nonEmpty) {
          dtmNumericRecord = dtmNumericRecord :+ termRecord.get._2.toDouble
        }
        else {
          dtmNumericRecord = dtmNumericRecord :+ 0.0
        }
      }
      dtmNumeric = dtmNumeric :+ dtmNumericRecord

    }

    return (dtmNumeric, ranks)
  }
}

class DTMRecord(val document : String,
                val rank : Int,
                var occurrences :  mutable.HashMap[String,Int]
                )

 观察这段代码,注意到这里面有一个方法 def getNumericRepresentationForRecords(): (Array[Array[Double]], Array[Double])。这一方法返回一个元组,该元组以一个矩阵作为第一个参数,该矩阵中每一行代表一个文档,每一列代表来自 DTM 文档的完备词汇集中的词汇。注意第一个列表中的浮点数表示词汇出现的次数。

第二个参数是一个数组,包含第一个列表中所有记录的排序值。

现在我们可以按如下方式扩展主程序,这样就可以得到所有文档的数值表示:

 

val documentTermMatrix  = new DTM()
testData.foreach(x => documentTermMatrix.addDocumentToRecords(x._1,x._2,x._3))

 有了这个从文本到数值的转换,现在我们可以利用回归分析工具箱了。我们在“基于身高预测体重”的实例中应用了普通最小二乘法 (OLS),不过这次我们要应用“最小绝对收缩与选择算子”(Lasso) 回归。这是因为我们可以给这种回归方法提供某个 λ 值,它代表一个惩罚值。该惩罚值可以帮助 LASSO 算法选择相关的特征(单词)而丢弃其他一些特征(单词)。

LASSO 执行的这一特征选择功能非常有用,因为在本例中,文档说明包含了大量的单词。LASSO 会设法找出那些单词的一个合适的子集作为特征,而要是应用 OLS,则所有单词都会被使用,那么运行时间将会变得极其漫长。此外,OLS 算法实现会检测非满秩。这是维数灾难的一种情形。

无论如何,我们需要找出一个最佳的 λ 值,因此,我们应该用交叉验证法尝试几个 λ 值,操作过程如下:

 

for (i <- 0 until cv.k) {
      //Split off the training datapoints and classifiers from the dataset
      //从数据集中将用于训练的数据点与分类器分离出来
      val dpForTraining = numericDTM
        ._1
        .zipWithIndex
        .filter(x => cv
                    .test(i)
                    .toList
                    .contains(x._2)
                )
        .map(y => y._1)

      val classifiersForTraining = numericDTM
        ._2
        .zipWithIndex
        .filter(x => cv
                    .test(i)
                    .toList
                    .contains(x._2)
                )
        .map(y => y._1)

      //And the corresponding subset of data points and their classifiers for testing
      //以及对应的用于测试的数据点子集及其分类器
      val dpForTesting = numericDTM
        ._1
        .zipWithIndex
        .filter(x => !cv
                    .test(i)
                    .contains(x._2)
                )
        .map(y => y._1)

      val classifiersForTesting = numericDTM
        ._2
        .zipWithIndex
        .filter(x => !cv
                    .test(i)
                    .contains(x._2)
                )
        .map(y => y._1)

      //These are the lambda values we will verify against
      //这些是我们将要验证的λ值
      val lambdas: Array[Double] = Array(0.1, 0.25, 0.5, 1.0, 2.0, 5.0)

      lambdas.foreach { x =>
        //Define a new model based on the training data and one of the lambda's
        //定义一个基于训练数据和其中一个λ值的新模型
        val model = new LASSO(dpForTraining, classifiersForTraining, x)

        //Compute the RMSE for this model with this lambda
        //计算该模型的RMSE值
        val results = dpForTesting.map(y => model.predict(y)) zip classifiersForTesting
        val RMSE = Math
            .sqrt(results
                    .map(x => Math.pow(x._1 - x._2, 2)).sum /
                                 results.length
                        )
        println("Lambda: " + x + " RMSE: " + RMSE)

      }
    }

 多次运行这段代码会给出一个在 36 和 51 之间变化的 RMSE 值。这表示我们排序的预测值会偏离至少 36 位。鉴于我们要尝试预测最高的 100 位,结果表明这个模型的效果非常差。在本例中,λ 值变化对模型的影响并不明显。然而,在实践中应用这种算法时,要小心地选取 λ 值: λ 值选得越大,算法选取的特征数就越少。 所以,交叉验证法对分析不同 λ 值对算法的影响很重要。

引述 John Tukey 的一句话来总结这个实例:

“数据中未必隐含答案。某些数据和对答案的迫切渴求的结合,无法保证人们从一堆给定数据中提取出一个合理的答案。”

应用无监督学习合并特征(PCA)

主成分分析 (PCA) 的基本思路是减少一个问题的维数。这是一个很好的方法,它可以避免维灾难,也可以帮助合并数据,避开无关数据的干扰,使其中的趋势更明显。

在本例中,我们打算应用 PCA 把 2002-2012 年这段时间内 24 只股票的股价合并为一只股票的股价。这个随时间变化的值就代表一个基于这 24 只股票数据的股票市场指数。把这24种股票价格合并为一种,明显地减少了处理过程中的数据量,并减少了数据维数,对于之后应用其他机器学习算法作预测,如回归分析来说,有很大的好处。为了看出特征数从 24 减少为 1 之后的效果,我们会将结果与同一时期的道琼斯指数 (DJI) 作比较。

随着工程的开始,下一步要做的是加载数据。为此,我们提供了两个文件:Data file 1 和 Data file 2.

 

object PCA extends SimpleSwingApplication{


  def top = new MainFrame {
    title = "PCA Example"
    //Get the example data
    //获取案例数据
    val basePath = "/users/.../Example Data/"
    val exampleDataPath = basePath + "PCA_Example_1.csv"
    val trainData = getStockDataFromCSV(new File(exampleDataPath))

    }
  def getStockDataFromCSV(file: File): (Array[Date],Array[Array[Double]]) = {
    val source = scala.io.Source.fromFile(file)
    //Get all the records (minus the header)
    //获取所有记录(减去标头)
    val data = source
        .getLines()
        .drop(1)
        .map(x => getStockDataFromString(x))
        .toArray

    source.close()
    //group all records by date, and sort the groups on date ascending
    //按日期将所有记录分组,并按日期将组升序排列
    val groupedByDate = data.groupBy(x => x._1).toArray.sortBy(x => x._1)
    //extract the values from the 3-tuple and turn them into
    // an array of tuples: Array[(Date, Array[Double)]
    //抽取这些3元组的值并将它们转换为一个元组数组:Array[(Date,Array[Double])]
    val dateArrayTuples = groupedByDate
        .map(x => (x._1, x
                        ._2
                        .sortBy(x => x._2)
                        .map(y => y._3)
                    )
            )

    //turn the tuples into two separate arrays for easier use later on
    //将这些元组分隔为两个数组以方便之后使用
    val dateArray = dateArrayTuples.map(x => x._1).toArray
    val doubleArray = dateArrayTuples.map(x => x._2).toArray


    (dateArray,doubleArray)
  }

  def getStockDataFromString(dataString: String): (Date,String,Double) = {

    //Split the comma separated value string into an array of strings
    //把用逗号分隔的数值字符串分解为一个字符串数组
    val dataArray: Array[String] = dataString.split(',')

    val format = new SimpleDateFormat("yyyy-MM-dd")
    //Extract the values from the strings
    //从字符串中抽取数值

    val date = format.parse(dataArray(0))
    val stock: String = dataArray(1)
    val close: Double = dataArray(2).toDouble

    //And return the result in a format that can later 
    //easily be used to feed to Smile
    //并以一定格式返回结果,使得该结果之后容易输入到Smile中处理

    (date,stock,close)
  }
}

 有了训练数据,并且我们已经知道要将24个特征合并为一个单独的特征,现在我们可以进行主成分分析,并按如下方式为数据点检索数据。

 

//Add to `def top`
//添加到‘def top’中
val pca = new PCA(trainData._2)
pca.setProjection(1)
val points = pca.project(trainData._2)
val plotData = points
    .zipWithIndex
    .map(x => Array(x._2.toDouble, -x._1(0) ))

val canvas: PlotCanvas = LinePlot.plot("Merged Features Index",
                                         plotData, 
                                         Line.Style.DASH,
                                         Color.RED);

peer.setContentPane(canvas)
size = new Dimension(400, 400)

 这段代码不仅执行了 PCA,还将结果绘成图像,y 轴表示特征值,x 轴表示每日。



 为了能看出 PCA 合并的效果,我们现在通过如下方式调整代码将道琼斯指数加入到图像中:

首先把下列代码添加到 def top 方法中:

 

//Verification against DJI
 //用道琼斯指数验证
    val verificationDataPath = basePath + "PCA_Example_2.csv"
    val verificationData = getDJIFromFile(new File(verificationDataPath))
    val DJIIndex = getDJIFromFile(new File(verificationDataPath))
    canvas.line("Dow Jones Index", DJIIndex._2, Line.Style.DOT_DASH, Color.BLUE)

 然后我们需要引入下列两个方法:

 

 def getDJIRecordFromString(dataString: String): (Date,Double) = {

    //Split the comma separated value string into an array of strings
    //把用逗号分隔的数值字符串分解为一个字符串数组
    val dataArray: Array[String] = dataString.split(',')

    val format = new SimpleDateFormat("yyyy-MM-dd")
    //Extract the values from the strings
    //从字符串中抽取数值

    val date = format.parse(dataArray(0))
    val close: Double = dataArray(4).toDouble

    //And return the result in a format that can later 
    //easily be used to feed to Smile
    //并以一定格式返回结果,使得该结果之后容易输入到Smile中处理
    (date,close)
  }

  def getDJIFromFile(file: File): (Array[Date],Array[Double]) = {
    val source = scala.io.Source.fromFile(file)
    //Get all the records (minus the header)
    //获取所有记录(减去标头)
    val data = source
        .getLines()
        .drop(1)
        .map(x => getDJIRecordFromString(x)).toArray
    source.close()

    //turn the tuples into two separate arrays for easier use later on
    //将这些元组分隔为两个数组以方便之后使用
    val sortedData = data.sortBy(x => x._1)
    val dates = sortedData.map(x => x._1)
    val doubles = sortedData.map(x =>   x._2 )

    (dates, doubles)
  }

 这段代码加载了 DJI 数据,并把它绘成图线添加到我们自己的股票指数图中。然而,当我们执行这段代码时,效果图有点无用。

 



 如你所见,DJI 的取值范围与我们的计算特征的取值范围偏离很远。因此,现在我们要将数据标准化。办法就是根据数据的取值范围将数据进行缩放,这样,两个数据集就会落在同样的比例中。

用下列代码替换 getDJIFromFile 方法:

 

def getDJIFromFile(file: File): (Array[Date],Array[Double]) = {
    val source = scala.io.Source.fromFile(file)
    //Get all the records (minus the header)
    //获取所有记录(减去标头)
    val data = source
        .getLines()
        .drop(1)
        .map(x => getDJIRecordFromString(x))
        .toArray

    source.close()

    //turn the tuples into two separate arrays for easier use later on
    //将这些元组分隔为两个数组以方便之后使用
    val sortedData = data.sortBy(x => x._1)
    val dates = sortedData.map(x => x._1)
    val maxDouble = sortedData.maxBy(x => x._2)._2
    val minDouble = sortedData.minBy(x => x._2)._2
    val rangeValue = maxDouble - minDouble
    val doubles = sortedData.map(x =>   x._2 / rangeValue )

    (dates, doubles)
  }

 用下列代码替换 def top 方法中 plotData 的定义:

 

val maxDataValue = points.maxBy(x => x(0))
val minDataValue = points.minBy(x => x(0))
val rangeValue = maxDataValue(0) - minDataValue(0)
val plotData = points
    .zipWithIndex
    .map(x => Array(x._2.toDouble, -x._1(0) / rangeValue))

 

 现在我们看到,虽然 DJI 的取值范围落在 0.8 与 1.8 之间,而我们的新特征的取值范围落在 -0.5 与 0.5 之间,但两条曲线的趋势符合得很好。学完这个实例,加上段落中对 PCA 的说明,现在你应该学会了 PCA 并能把它应用到你自己的数据中。

应用支持向量机(SVM)

在我们实际开始应用支持向量机 (SVM) 之前,我会稍微介绍一下 SVM。基本的 SVM 是一个二元分类器,它通过挑选出一个代表数据点之间最大距离的超平面,将数据集分为两部分。一个 SVM 就带有一个所谓的“校正率”值。如果不存在理想分割,则该校正率提供了一个误差范围,允许人们在该范围内找出一个仍尽可能合理分割的超平面。因此,即使仍存在一些令人不快的点,在校正率规定的误差范围内,超平面也是合适的。这意味着,我们无法为每种情形提出一个“标准的”校正率。不过,如果数据中没有重叠部分,则较低的校正率要优于较高的校正率。

我刚刚说明了作为一个二元分类器的基本 SVM,但是这些原理也适用于具有更多类别的情形。然而,现在我们要继续完成具有 2 种类别的实例,因为仅说明这种情况已经足够了。

在本例中,我们将完成几个小案例,其中,支持向量机 (SVM) 的表现都胜过其他分离算法如 KNN。这种方法与前几例中的不同,但它能帮你更容易学会怎么使用以及何时使用 SVM。

对于每个小案例,我们会提供代码、图像、不同参数时的 SVM 运行测试以及对测试结果的分析。这应该使你对输入 SVM 算法的参数有所了解。

在第一个小案例中,我们将应用高斯核函数,不过在 Smile 库中还有其他核函数。其他核函数可以在这里找到。紧接着高斯核函数,我们将讲述多项式核函数,因为这个核函数与前者有很大的不同。

我们会在每个小案例中用到下列的基本代码,其中只有构造函数 filePaths 和 svm 随每个小案例而改变。

 

object SupportVectorMachine extends SimpleSwingApplication {


  def top = new MainFrame {
    title = "SVM Examples"
    //File path (this changes per example)
    //文件路径(随案例而改变)
    val trainingPath =  "/users/.../Example Data/SVM_Example_1.csv"
    val testingPath =  "/users/.../Example Data/SVM_Example_1.csv"
    //Loading of the test data and plot generation stays the same
    //加载测试数据,绘图生成代码保持相同
    val trainingData = getDataFromCSV(new File(path))
    val testingData = getDataFromCSV(new File(path))

    val plot = ScatterPlot.plot(    trainingData._1, 
                                    trainingData._2, 
                                    '@', 
                                    Array(Color.blue, Color.green)
                                )
    peer.setContentPane(plot)

    //Here we do our SVM fine tuning with possibly different kernels
    //此处,我们用可能的不同核函数对SVM进行微调
    val svm = new SVM[Array[Double]](new GaussianKernel(0.01), 1.0,2)
    svm.learn(trainingData._1, trainingData._2)
    svm.finish()

    //Calculate how well the SVM predicts on the training set
    //计算SVM对测试集的预测效果
    val predictions = testingData
        ._1
        .map(x => svm.predict(x))
        .zip(testingData._2)

    val falsePredictions = predictions
        .map(x => if (x._1 == x._2) 0 else 1 )

    println(falsePredictions.sum.toDouble / predictions.length  
                    * 100 + " % false predicted")

    size = new Dimension(400, 400)
  }


  def getDataFromCSV(file: File): (Array[Array[Double]], Array[Int]) = {
    val source = scala.io.Source.fromFile(file)
    val data = source
        .getLines()
        .drop(1)
        .map(x => getDataFromString(x))
        .toArray

    source.close()
    val dataPoints = data.map(x => x._1)
    val classifierArray = data.map(x => x._2)
    return (dataPoints, classifierArray)
  }

  def getDataFromString(dataString: String): (Array[Double], Int) = {
    //Split the comma separated value string into an array of strings
    //把用逗号分隔的数值字符串分解为一个字符串数组
    val dataArray: Array[String] = dataString.split(',')

    //Extract the values from the strings
    //从字符串中抽取数值
    val coordinates  = Array( dataArray(0).toDouble, dataArray(1).toDouble)
    val classifier: Int = dataArray(2).toInt

    //And return the result in a format that can later 
    //easily be used to feed to Smile
    //并以一定格式返回结果,使得该结果之后容易输入到Smile中处理
    return (coordinates, classifier)
  }

 

案例1(高斯核函数)

在本案例中,我们介绍了最常用的 SVM 核函数,即高斯核函数。我们的想法是帮助读者寻找该核函数的最佳输入参数。本例中用到的数据可以在这里下载。



 

从该图中可以清楚看出,线性回归线在这里起不了作用。我们要使用一个 SVM 来作预测。在给出的第一段代码中,高斯核函数的 sigma 值为 0.01,边距惩罚系数为 1.0,类别总数为 2,并将其传递给了 SVM。那么,这些都代表什么意思呢?

我们从高斯核函数说起。这个核函数反映了 SVM 如何计算系统中成对数据的相似度。对于高斯核函数,用到了欧氏距离中的方差。我们特意挑选高斯核函数的原因是,数据中并不含有明显的结构如线性函数、多项式函数或者双曲线函数。相反地,数据聚集成了3组。

我们传递到高斯核中构造函数的参数是 sigma。这个 sigma 值反映了核函数的平滑程度。我们会演示改变这一取值如何影响预测效果。我们将边距惩罚系数取 1。这一参数定义了系统中向量的边距,因此,这一值越小,约束向量就越多。我们会执行一组运行测试,通过结果向读者说明这个参数在实践中的作用。注意其中 s: 代表 sigma,c: 代表校正惩罚系数。百分数表示预测效果的误差率, 它只不过是训练之后,对相同数据集的错误预测的百分数。



 不幸的是,并不存在为每个数据集寻找正确 sigma 的黄金法则。不过,可能最好的方法就是计算数据的 sigma 值,即 √(variance),然后在这个值附近取值看看哪一个 sigma 值效果最好。因为本例数据的方差在 0.2 与 0.5 之间,我们把这区间作为中心并在中心的两边都选取一些值,以比较我们的案例中使用高斯核的 SVM 的表现。

看看表格中的结果和错误预测的百分比,它表明产生最佳效果的参数组合是一个非常低的 sigma (0.001) 和一个 1.0 及以上的校正率。不过,如果把这个模型应用到实际中的新数据上,可能会产生过拟合。因此,在用模型本身的训练数据测试模型时,你应该保持谨慎。一个更好的方法是使用交叉验证,或用新数据验证。

案例2(多项式核函数)

高斯核并不总是最佳选择,尽管在应用 SVM 时,它是最常用的核函数。因此,在本例中,我们将演示一个多项式核函数胜过高斯核函数的案例。注意,虽然本案例中的示例数据是构建好的,但在本领域内相似的数据(带有一点噪声)是可以找到的。本案例中的训练数据可以在这里下载,测试数据在这里下载。

对于本例数据,我们用一个三次多项式创建了两个类别,并生成了一个测试数据文件和一个训练数据文件。训练数据包含x轴上的前500个点,而测试数据则包含x轴上500到1000这些点。为了分析多项式核函数的工作原理,我们将数据汇成图。左图是训练数据的,右图是测试数据的。



 

 考虑到本实例开头给出的基本代码,我们作如下的替换:

val trainingPath = "/users/.../Example Data/SVM_Example_2.csv"
val testingPath = "/users/.../Example Data/SVM_Example_2_Test_data.csv"

 然后,如果我们使用高斯核并且运行代码,就可以得到如下结果:



 可以看到,即使是最佳情况,仍然有 27.4% 的测试数据被错误分类。这很有趣,因为当我们观察图像时,可以看到两个类别之间有一个很明显的区分。我们可以对 sigma 和校正率进行微调,但是当预测点很远时(例如 x 是 100000),sigma 和校正率就会一直太高而使模型表现不佳(时间方面与预测效果方面)。

因此,我们将高斯核替换为多项式核,代码如下:

val svm = new SVM[Array[Double]](new PolynomialKernel(2), 1.0,2)

 注意我们给多项式核的构造函数传递 2 的方式。这个 2 代表它要拟合的函数的次数。如果我们不单考虑次数为 2 的情况,我们还考虑次数为2、3、4、5的情况,并且让校正率再一次在 0.001 到 100 之间变化,则得到如下结果:

 



 从中我们可以看到,次数为 3 和 5 的情况得到了100%的准确率,这两种情况中测试数据与训练数据之间没有一个点是重叠的。与高斯核的最佳情况 27.4% 的错误率相比,这种表现令人惊喜。确实要注意本例这些数据是构建好的,因此没有什么噪声数据。所以才能出现所有的“校正率”都为 0% 错误率。如果添加了噪声,则需要对校正率进行微调。

以上就是对支持向量机这一部分的总结。

结论

在了解了机器学习的整体思想之后,你应该可以辨别出哪些情况分别属于分类问题、回归问题或是维数约化问题。此外,你应该理解机器学习的基本概念,什么是模型,并且知道机器学习中的一些常见陷阱。

在学完本文中的实例之后,你应该学会应用 K-NN、朴素贝叶斯算法以及线性回归分析了。此外,你也能够应用文本回归、使用 PCA 合并特征以及应用支持向量机。还有非常重要的一点,就是能够建立你自己的推荐系统。

如果你有疑问或关于本文的反馈,请随时通过 GithubLinkedIn 或 Twitter 联系我。

 

 

猜你喜欢

转载自tongxiaoming520.iteye.com/blog/2353222