转载: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 合并特征以及应用支持向量机。还有非常重要的一点,就是能够建立你自己的推荐系统。
如果你有疑问或关于本文的反馈,请随时通过 Github、LinkedIn 或 Twitter 联系我。