此外,我们将看看前10个和后10个权重,以便更多地了解发生了什么。
Top 10 weights:
Subject | Frequency | Time frame (seconds) | Weight |
---|---|---|---|
[ilug] what howtos for soho system | 2 | 60 | 8.52 |
[zzzzteana] the new steve earle | 2 | 120 | 8.22 |
[ilug] looking for a file / directory in zip file | 2 | 240 | 7.92 |
ouch... [bebergflame] | 2 | 300 | 7.82 |
[ilug] serial number in hosts file | 2 | 420 | 7.685 |
[ilug] email list management howto | 3 | 720 | 7.62 |
should mplayer be build with win32 codecs? | 2 | 660 | 7.482 |
[spambayes] all cap or cap word subjects | 2 | 670 | 7.48 |
[zzzzteana] save the planet, kill the people | 3 | 1020 | 7.47 |
[ilug] got me a crappy laptop | 2 | 720 | 7.44 |
Bottom 10 weights:
Subject | Frequency | Time frame (seconds) | Weight |
---|---|---|---|
secure sofware key | 14 | 1822200 | 4.89 |
[satalk] help with postfix + spamassassin | 2 | 264480 | 4.88 |
the war prayer | 2 | 301800 | 4.82 |
gecko adhesion finally sussed. | 5 | 767287 | 4.81 |
the mime information you requested (last changed 3154 feb 14) | 3 | 504420 | 4.776 |
use of base image / delta image for automated recovery from | 5 | 1405800 | 4.55 |
sprint delivers the next big thing?? | 5 | 1415280 | 4.55 |
[razor-users] collision of hashes? | 4 | 1230420 | 4.51 |
[ilug] modem problems | 2 | 709500 | 4.45 |
tech's major decline | 2 | 747660 | 4.43 |
正如你所看到的,最高的权重给予了几乎立即得到电子邮件回复的电子邮件,而最低权重给予具有非常长的时间范围的电子邮件。这允许具有非常低频率的电子邮件仍然基于它们被发送的时间帧被评定为非常重要。有了这个,我们有了2个特征:来自发件人mailsGroupedBySender的电子邮件的数量,以及属于现有线程threadGroupedWithWeights的电子邮件的权重。
让我们继续下一个特征,因为我们希望基于我们的排名值有尽可能多的特征。下一个特征将基于我们刚刚计算的权重排名。这个想法是,不同主题的新电子邮件会送达。然而,很可能它们包含与之前收到的重要主题相似的关键字。 这允许我们在线程(具有相同主题的多个消息)开始之前将电子邮件排序为重要。 为此,我们将关键字的权重指定为该术语出现的主题的权重。如果这个术语出现在多个线程中,我们采用最高的权重作为前导。
这个特征有一个问题,那就是它是停止词。 幸运的是,我们有一个停止词文件,允许我们删除(大多数)英语停止词。 但是,在设计自己的系统时,您应该考虑到可能会看到多种语言,因此您应该删除系统中可能发生的所有语言的停止词。此外,您可能需要小心的从不同的语言中删除停止词,因为一些词在不同的语言之间可能有不同的含义。 至于现在我们坚持删除英语停止词。 这个特征的代码如下:
def getStopWords: List[String] = {
val source =scala.io.Source
.fromFile(newFile("/Users/../stopwords.txt"))("latin1")
val lines =source.mkString.split("\n")
source.close()
lines.toList
}
//Add to top:
val stopWords = getStopWords
val threadTermWeights = threadGroupedWithWeights
.toArray
.sortBy(x =>x._4)
.flatMap(x=> x._1
.replaceAll("[^a-zA-Z]", "")
.toLowerCase.split(" ")
.filter(_.nonEmpty)
.map(y=> (y,x._4)))
val filteredThreadTermWeights = threadTermWeights
.groupBy(x=> x._1)
.map(x =>(x._1, x._2.maxBy(y => y._2)._2))
.toArray.sortBy(x=> x._1)
.filter(x =>!stopWords.contains(x._1))
给定这个代码,我们现在有一个关于filteredThreadTermWeights的术语的列表,包括基于现有线程的权重。这些权重可以用于计算新电子邮件主题的权重,即使电子邮件不是对现有线程的回复。
val tdm = trainingData
.flatMap(x=> x.body.split(" "))
.filter(x=> x.nonEmpty && !stopWords.contains(x))
.groupBy(x=> x)
.map(x =>(x._1, Math.log10(x._2.length + 1)))
.filter(x=> x._2 != 0)
tdm列表允许我们基于历史数据计算新电子邮件的电子邮件正文的重要性权重。通过这4个特征的准备,我们可以对训练数据进行实际的排序计算。 为此,我们需要找到senderWeight(表示发件人的权重),termWeight(表示主题中的术语的权重),threadGroupWeight(表示线程权重)和commonTermsWeight(表示电子邮件正文的权重) 与每个电子邮件相乘以获得最终排名。 由于我们是相乘而不添加值,我们需要处理小于1的样本。例如,有人发送了1封电子邮件,那么senderWeight将是0.69,这使得那些之前没有发送任何电子邮件的人很不公平,因为他/她会得到一个senderWeight 1。这就是为什么我们拿每个特征的Math.max(值,1)会产生低于1的值。让我们看看代码:
val trainingRanks = trainingData.map(mail => {
//Determinethe weight of the sender, if it is lower than 1, pick 1 instead
//This isdone to prevent the feature from having a negative impact
valsenderWeight = mailsGroupedBySender
.collectFirst { case (mail.sender, x) => Math.max(x,1)}
.getOrElse(1.0)
//Determinethe weight of the subject
valtermsInSubject = mail.subject
.replaceAll("[^a-zA-Z ]", "")
.toLowerCase.split(" ")
.filter(x=> x.nonEmpty &&
!stopWords.contains(x)
)
valtermWeight = if (termsInSubject.size > 0)
Math.max(termsInSubject
.map(x=> {
tdm.collectFirst { case (y, z) if y == x => z}
.getOrElse(1.0)
})
.sum /termsInSubject.length,1)
else 1.0
//Determineif the email is from a thread,
//and if itis the weight from this thread:
valthreadGroupWeight: Double = threadGroupedWithWeights
.collectFirst { case (mail.subject, _, _, weight) => weight}
.getOrElse(1.0)
//Determinethe commonly used terms in the email and the weight belonging to it:
valtermsInMailBody = mail.body
.replaceAll("[^a-zA-Z ]", "")
.toLowerCase.split(" ")
.filter(x=> x.nonEmpty &&
!stopWords.contains(x)
)
valcommonTermsWeight = if (termsInMailBody.size > 0)
Math.max(termsInMailBody
.map(x=> {
tdm.collectFirst { case (y, z) if y == x => z}
.getOrElse(1.0)
})
.sum /termsInMailBody.length,1)
else 1.0
val rank= termWeight *
threadGroupWeight *
commonTermsWeight*
senderWeight
(mail, rank)
})
valsortedTrainingRanks = trainingRanks
.sortBy(x=> x._2)
val median =sortedTrainingRanks(sortedTrainingRanks.length / 2)._2
val mean = sortedTrainingRanks
.map(x =>x._2).sum /
sortedTrainingRanks.length
正如你所看到d额,我们计算了所有用于训练的电子邮件的排名,并排序它们,得到中位数和平均值。我们采用这个中间值和平均值的原因是要将其变为电子邮件评为优先级或非优先级的决策边界。 在实践中,这通常没有用。实践中获得决策边界的最佳方法是让用户将一组电子邮件标记为优先级与非优先级。然后,您可以使用这些等级来计算决策边界,并另外查看排名功能是否正确。 如果用户最终将电子邮件标记为具有比算法标记为优先级的电子邮件更高排名的优先级,则可能需要重新评估您的特征了。
我们计算决策边界的原因,不是因为排序用户的电子邮件排名,而是时间。 如果你纯粹基于排名排序电子邮件,人们会发现这很讨厌,因为一般人喜欢他们自己的电子邮件排序方式。然而,利用这个决策边界,我们可以先将各个电子邮件标记优先级,如果我们将排名纳入电子邮件客户端,可以将其显示在单独的列表中。
现在让我们来看看测试集中的电子邮件数量是多少。 为此,我们首先要添加以下代码:
val testingRanks = trainingData.map(mail => {
//mailcontains (full content, date, sender, subject, body)
//Determinethe weight of the sender
valsenderWeight = mailsGroupedBySender
.collectFirst { case (mail.sender, x) => Math.max(x,1)}
.getOrElse(1.0)
//Determine the weight of the subject
valtermsInSubject = mail.subject
.replaceAll("[^a-zA-Z ]", "")
.toLowerCase.split(" ")
.filter(x=> x.nonEmpty &&
!stopWords.contains(x)
)
valtermWeight = if (termsInSubject.size > 0)
Math.max(termsInSubject
.map(x=> {
tdm.collectFirst { case (y, z) if y == x => z}
.getOrElse(1.0)
})
.sum /termsInSubject.length,1)
else 1.0
//Determineif the email is from a thread,
//and if itis the weight from this thread:
valthreadGroupWeight: Double = threadGroupedWithWeights
.collectFirst { case (mail.subject, _, _, weight) => weight}
.getOrElse(1.0)
//Determinethe commonly used terms in the email and the weight belonging to it:
valtermsInMailBody = mail.body
.replaceAll("[^a-zA-Z ]", "")
.toLowerCase.split(" ")
.filter(x=> x.nonEmpty &&
!stopWords.contains(x)
)
valcommonTermsWeight = if (termsInMailBody.size > 0)
Math.max(termsInMailBody
.map(x=> {
tdm.collectFirst { case (y, z) if y == x => z}
.getOrElse(1.0)
})
.sum /termsInMailBody.length,1)
else 1.0
val rank= termWeight *
threadGroupWeight *
commonTermsWeight *
senderWeight
(mail, rank)
})
valpriorityEmails = testingRanks
.filter(x=> x._2 >= mean)
println(priorityEmails.length + " ranked as priority")
在实际运行测试代码后,您将看到从测试集中根据优先级排序的电子邮件数量实际上是563.这是测试电子邮件集的45%。 这是一个相当高的值,所以我们可以调整一点决策边界。 然而,此时不应该选择实际中的平均值,我们不会进一步探究到底是多少百分比。相反,我们将查看前10个优先级电子邮件的排名。
请注意,我已移除了部分电子邮件地址,以防止垃圾邮件漫游器检索这些邮件地址。 我们在下表中看到,这十大优先级电子邮件中的大多数是组合在一起的线程,其具有非常高的活动性。例如,排名最高的电子邮件。 此电子邮件是对9分钟前的电子邮件的跟进。 这表明电子邮件线程很重要。
Date | Sender | Subject | Rank |
---|---|---|---|
Sat Sep 07 06:45:23 CEST 2002 | skip@... | [spambayes] can't write to cvs... | 81.11 |
Sat Sep 07 21:11:36 CEST 2002 | tim.one@... | [spambayes] test sets? | 79.59 |
Mon Sep 09 17:46:55 CEST 2002 | tim.one@... | [spambayes] deleting "duplicate" spam before training? good idea | 79.54 |
Mon Aug 26 14:43:00 CEST 2002 | tomwhore@... | how unlucky can you get? | 77.87 |
Sat Sep 07 06:36:41 CEST 2002 | tim.one@... | [spambayes] can't write to cvs... | 77.77 |
Sun Sep 08 21:13:40 CEST 2002 | tim.one@... | [spambayes] test sets? | 76.3 |
Thu Sep 05 20:53:00 CEST 2002 | felicity@... | [razor-users] problem with razor 2.14 and spamassassin 2.41 | 75.44 |
Fri Sep 06 07:09:11 CEST 2002 | tim.one@... | [spambayes] all but one testing | 72.77 |
Sat Sep 07 06:40:45 CEST 2002 | tim.one@... | [spambayes] maybe change x-spam-disposition to something else... | 72.62 |
Sat Sep 07 05:05:45 CEST 2002 | skip@... | [spambayes] maybe change x-spam-disposition to something else... | 72.27 |
另外,我们可以看到tim.one ...在这个表中出现了很多次。 这表明他的所有电子邮件都很重要,或者他发送了如此多的电子邮件,那么排名者会自动将它们列为优先。作为这个例子的最后一步,我们将进一步研究一下:
val timsEmails = testingRanks
.filter(x =>x._1.sender == "tim.one@...")
.sortBy(x =>-x._2)
timsEmails
.foreach(x=> println( "| " +
x._1.emailDate +
" |" +
x._1.subject +
" | " +
df.format(x._2) +
" |")
)
运行此代码后,我们将看到45封电子邮件和最顶的十封:
Date | Subject | Rank |
---|---|---|
Sun Sep 08 21:36:15 CEST 2002 | [spambayes] ditching wordinfo | 42.89 |
Tue Sep 10 18:12:51 CEST 2002 | [spambayes] timtest broke? | 41.73 |
Thu Sep 12 04:06:24 CEST 2002 | [spambayes] current histograms | 41.73 |
Sun Sep 08 21:46:47 CEST 2002 | [spambayes] hammie.py vs. gbayes.py | 41.68 |
Tue Sep 10 04:18:25 CEST 2002 | [spambayes] current histograms | 40.67 |
Wed Sep 11 04:46:15 CEST 2002 | [spambayes] xtreme training | 39.83 |
Tue Sep 10 19:26:05 CEST 2002 | [spambayes] timtest broke? | 39.73 |
Thu Sep 12 01:37:13 CEST 2002 | [spambayes] stack.pop() ate my multipart message | 38.89 |
Sat Sep 07 01:06:56 CEST 2002 | [spambayes] ditching wordinfo | 38.34 |
Sat Sep 07 00:21:15 CEST 2002 | [spambayes] [ann] trained classifier available | 8.71 |
如果我们还能回忆起我们的决策边界是平均值,即25.06,那么我们看到的只有1封电子邮件没有被评为优先级。 这表明一方面我们的决策边界太低了,但另一方面,Tim可能实际上发送了很多重要的电子邮件,所以排名可以低于决策边界。
不幸的是,我们不能提供确切的答案,因为我们不是这个测试数据的所有者。当你没有确切的真相时,验证这样的ranker是相当困难的。 验证和改进它的最常见的方法之一是实际呈现给用户并让他/她自己标记错误。然后可以使用这些校正来改进系统。
总而言之,我们看到了如何从原始数据获取特征,即使数据有“巨大”异常值,以及如何将这些特征组合成最终排名值。 此外,我们尝试了评估这些特征,但由于缺乏数据集的知识,我们不能得出明确的结论。然而,如果你对你所知道的数据做同样的事情,那么这应该让你开始建立自己的排名系统。
Predicting weight based on height (usingOrdinary Least Squares)
在本节中,我们将介绍普通最小二乘法,它是线性回归的一种形式。由于这种技术非常强大,在开始这个例子之前,了解一下回归和常见的陷阱很重要。 我们将在本节中讨论这些问题中的一些,而其他一些在欠拟合和过拟合的部分中写出。
线性回归的思想是在你的训练数据点上设置一个“最优”的回归线。注意,这只能是当你的数据是线性的,并没有巨大的离群值时。 如果不是这种情况,您可以尝试操作您的数据,直到出现这种情况,例如通过采取开方或求数据对数。和往常一样,项目设置时要做的第一件事是导入数据集。为此,我们为您提供以下csv文件和读取此文件的代码:
def getDataFromCSV(file: File): (Array[Array[Double]],Array[Double]) = {
val source =scala.io.Source.fromFile(file)
val data =source.getLines().drop(1).map(x => getDataFromString(x)).toArray
source.close()
var inputData =data.map(x => x._1)
var resultData= data.map(x => x._2)
return(inputData,resultData)
}
defgetDataFromString(dataString: String): (Array[Double], Double) = {
//Split thecomma separated value string into an array of strings
val dataArray:Array[String] = dataString.split(',')
var person =1.0
if(dataArray(0) == "\"Male\"") {
person = 0.0
}
//Extract thevalues from the strings
//Since the data is in US metrics
//inch andpounds we will recalculate this to cm and kilo's
val data :Array[Double] = Array(person,dataArray(1).toDouble * 2.54)
val weight:Double = dataArray(2).toDouble * 0.45359237
//And return theresult in a format that can later easily be used to feed to Smile
return (data,weight)
}
请注意,数据读取器会将值从英制转换为公制。这对OLS实现没有大的影响,但由于公制更常用,所以我们更倾向在该系统中提供数据。使用这些方法,我们将数据作为表示数据点的Array [Array [Double]]和表示男性或女性的分类的Array[Double]。 这些格式对于绘制数据和反馈到机器学习算法是有好处的。让我们先看看数据是什么样子。 为此,我们使用以下代码绘制数据。
object LinearRegressionExample extendsSimpleSwingApplication {
def top = newMainFrame {
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))
valmaleFemaleLabels = 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 = newDimension(400, 400)
}
如果你执行上面的代码,会弹出一个窗口,如下图所示。请注意,当您运行代码时,您可以滚动以放大和缩小绘图。
在这个图中,绿色是女性,蓝色是男性,你可以看到他们的重量和身高有很大的重叠。因此,如果我们忽略男性和女性之间的差异,它仍然会看起来像数据中有一个线性函数(可以在左图中看到)。 然而,当忽略这种差异时,函数将不如当我们结合关于男性和女性的信息时那样精确。在这个例子中,找到这种区别是微不足道的,但是你可能会遇到这些集合不那么明显的数据集。让您意识到这种可能性可能有助于您找到数据中的集合,这可以提高机器学习应用程序的性能。现在我们已经看到了数据,看到确实我们可以想出一个线性回归线来拟合这些数据。smile为我们提供了普通最小二乘算法,我们可以很容易地使用如下:
al olsModel = new OLS(testData._1,testData._2)
With this olsModel we can now predict someone's weight based on length andgender as follows:
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())
and this will give the following results:
Prediction for Male of 1.7M: 79.14538559840447
Prediction forFemale of 1.7M:70.35580395758966
ModelError:4.5423150758157185
如果你还能想起分类算法,这里面有一个先验值,解释了一些关于你所建立的模型的性能。由于回归是一种更强的统计方法,所以现在有一个实际的错误值。 该值表示拟合回归线的平均值的范围,因此您可以说,对于这个模型,1.70m男性的预测为79.15kg有 +/- 4.54kg的误差。 注意,如果你删除男性和女性之间的区别,这个错误将增加到5.5428。 换句话说,增加男性和女性之间的区别,在其预测中将模型精度增加+/-1kg。
最后Smile还提供了一些关于你建立的模型的统计信息。RSquared方法给出了来自模型的均方误差(RMSE)除以平均值的RMSE。 该值始终在0和1之间。如果您的模型完全预测了每个数据点,则RSquared将为1,如果模型的性能并不优于平均值函数,则该值将为0.在该字段中,此度量常常乘以100,然后用作表示模型的准确程度。 因为这是一个归一化值,所以它可以用来比较不同模型的性能。