Ranking emails based on their content(Recommendation system)
这个例子将会完全建立你自己的推荐系统。我们将根据以下特征对电子邮件进行排名:“发件人”,“主题”,“主题中的常用术语”和“电子邮件正文中的常用术语”。 稍后在示例中,我们将解释这些特征。 请注意,这些特征是在您制作自己的推荐系统时定义的。当建立自己的推荐系统时,这是最难的部分之一。 达到良好的特征并不简单,当您最终选择这些特征时,数据可能无法直接用于这些特征。
此示例背后的主要想法是向您展示如何执行特征选择,以及如何解决您在使用自己的数据时,开始执行此操作时会出现的问题。
我们将使用我们在电子邮件分类为垃圾邮件或ham的示例中使用的电子邮件数据的子集。这个子集可以在这里下载。此外,你需要停止词文件。 请注意,这个数据是一组接收到的电子邮件,因此我们缺少一半的数据,即此邮箱的外发电子邮件。然而,即使没有这些信息,我们也可以做一些相当不错的排名。
在我们操作排名系统之前,我们首先需要从我们的电子邮件集中提取尽可能多的数据。由于数据格式有点乏味,我们使用代码来解决这个。 内嵌的注释解释了为什么程序怎么完成的。 注意,应用程序是一个带有GUI的swing应用程序。我们这样做是因为我们将需要绘制数据以便稍后获得直观的了解。还要注意,我们直接对测试和训练数据进行分割,方便接下来可以测试我们的模型。
代码语言:javascript复制import java.awt.{Rectangle}
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import smile.plot.BarPlot
import scala.swing.{MainFrame, SimpleSwingApplication}
import scala.util.Try
object RecommendationSystem extendsSimpleSwingApplication {
case classEmailData(emailDate : Date, sender : String, subject : String, body : String)
def top = newMainFrame {
title ="Recommendation System Example"
val basePath ="/Users/../data"
val easyHamPath= basePath "/easy_ham"
val mails =getFilesFromDir(easyHamPath).map(x => getFullEmail(x))
valtimeSortedMails = mails
.map (x => EmailData ( getDateFromEmail(x),
getSenderFromEmail(x),
getSubjectFromEmail(x),
getMessageBodyFromEmail(x)
)
)
.sortBy(x=> x.emailDate)
val(trainingData, testingData) = timeSortedMails
.splitAt(timeSortedMails.length / 2)
}
defgetFilesFromDir(path: String): List[File] = {
val d = newFile(path)
if (d.exists&& d.isDirectory) {
//Remove themac os basic storage file,
//andalternatively for unix systems "cmds"
d.listFiles.filter(x => x.isFile &&
!x.toString.contains(".DS_Store") &&
!x.toString.contains("cmds")).toList
} else {
List[File]()
}
}
defgetFullEmail(file: File): String = {
//Note that theencoding of the example files is latin1,
//thus thisshould be passed to the from file method.
val source =scala.io.Source.fromFile(file)("latin1")
val fullEmail =source.getLines mkString "n"
source.close()
fullEmail
}
defgetSubjectFromEmail(email: String): String = {
//Find theindex of the end of the subject line
val subjectIndex =email.indexOf("Subject:")
valendOfSubjectIndex = email
.substring(subjectIndex) .indexOf('n') subjectIndex
//Extract thesubject: start of subject 7
// (length ofSubject:) until the end of the line.
val subject =email
.substring(subjectIndex 8, endOfSubjectIndex)
.trim
.toLowerCase
//Additionally,we check whether the email was a response and
//remove the're: ' tag, to make grouping on topic easier:
subject.replace("re: ", "")
}
defgetMessageBodyFromEmail(email: String): String = {
valfirstLineBreak = email.indexOf("nn")
//Return themessage body filtered by only text
//from a-z andto lower case
email.substring(firstLineBreak)
.replace("n", " ")
.replaceAll("[^a-zA-Z ]", "")
.toLowerCase
}
defgetSenderFromEmail(email: String): String = {
//Find theindex of the From: line
valfromLineIndex = email
.indexOf("From:")
val endOfLine =email
.substring(fromLineIndex)
.indexOf('n') fromLineIndex
//Search forthe <> tags in this line, as if they are there,
// the emailaddress is contained inside these tags
valmailAddressStartIndex = email
.substring(fromLineIndex, endOfLine)
.indexOf('<') fromLineIndex 1
valmailAddressEndIndex = email
.substring(fromLineIndex, endOfLine)
.indexOf('>') fromLineIndex
if(mailAddressStartIndex > mailAddressEndIndex) {
//The emailaddress was not embedded in <> tags,
// extractthe substring without extra spacing and to lower case
varemailString = email
.substring(fromLineIndex 5, endOfLine)
.trim
.toLowerCase
//Remove apossible name embedded in () at the end of the line,
//for examplein test@test.com (tester) the name would be removed here
valadditionalNameStartIndex = emailString.indexOf('(')
if(additionalNameStartIndex == -1) {
emailString
.toLowerCase
}
else {
emailString
.substring(0, additionalNameStartIndex)
.trim
.toLowerCase
}
}
else {
//Extract theemail address from the tags.
//If these<> tags are there, there is no () with a name in
// the From:string in our data
email
.substring(mailAddressStartIndex, mailAddressEndIndex)
.trim
.toLowerCase
}
}
defgetDateFromEmail(email: String): Date = {
//Find theindex of the Date: line in the complete email
valdateLineIndex = email
.indexOf("Date:")
valendOfDateLine = email
.substring(dateLineIndex)
.indexOf('n') dateLineIndex
//All possibledate patterns in the emails.
valdatePatterns = Array( "EEE MMM ddHH:mm:ss yyyy",
"EEE, ddMMM yyyy HH:mm",
"dd MMMyyyy HH:mm:ss",
"EEE MMMdd yyyy HH:mm")
datePatterns.foreach { x =>
//Try todirectly return a date from the formatting.
//when itfails on a pattern it continues with the next one
// until oneworks
Try(returnnew SimpleDateFormat(x)
.parse(email
.substring(dateLineIndex 5, endOfDateLine)
.trim.substring(0,x.length)))
}
//Finally, ifall failed return null
//(this willnot happen with our example data but without
//this returnthe code will not compile)
null
}
}
对数据做这样的预处理是非常常见的,并且当您的数据非标准化时,例如这些电子邮件的日期和发件人。然而,执行完这个代码块,我们现在可以使用我们的示例数据的下面这些属性了:完整电子邮件,接收日期,发件人,主题和正文。 这允许我们可以在推荐系统中继续使用这些实际特征。
我们将根据电子邮件的发件人制作第一个推荐特征。那些收到更多电子邮件的人应该排名高于收到较少电子邮件的人。这是一个强假设,但是直觉上你会同意,但事实是垃圾邮件被遗漏了。让我们来看看发送者在整个电子邮件集中的分布情况。
代码语言:javascript复制//Add to the top body:
//First we group the emails by Sender, then we extractonly the sender address
//and amount of emails, and finally we sort them onamounts ascending
val mailsGroupedBySender = trainingData
.groupBy(x => x.sender)
.map(x => (x._1, x._2.length))
.toArray
.sortBy(x => x._2)
//In order to plot the data we split the values from theaddresses as
//this is how the plotting library accepts the data.
val senderDescriptions = mailsGroupedBySender
.map(x =>x._1)
val senderValues = mailsGroupedBySender
.map(x =>x._2.toDouble)
val barPlot = BarPlot.plot("", senderValues,senderDescriptions)
//Rotate the email addresses by -80 degrees such that wecan read them
barPlot.getAxis(0).setRotation(-1.3962634)
barPlot.setAxisLabel(0, "")
barPlot.setAxisLabel(1, "Amount of emails received")
peer.setContentPane(barPlot)
bounds = new Rectangle(800, 600)
在这里,您可以看到经常发送邮件的发件人发送了45封电子邮件,其次是37封电子邮件,然后迅速下降。 由于这些“巨大”异常值,直接使用这些数据将导致最高的1或2个发送者被评定为非常重要,而剩下的在推荐系统中将不被考虑。 为了防止这种事情,我们将通过取log1p重新缩放数据。 log1p函数取值的对数,但事先将值加1。 添加1是为了防止在发送仅发送1封电子邮件的发件人的对数值出现问题。在获取数据的对数后,数据看起来像这样。
代码语言:javascript复制//Code changes:
val mailsGroupedBySender = trainingData
.groupBy(x=> x.sender)
.map(x =>(x._1, Math.log1p(x._2.length)))
.toArray
.sortBy(x =>x._2)
barPlot.setAxisLabel(1, "Amount of emails receivedon log Scale ")
一定程度上,数据仍然相同,但是却以不同的尺度表示。请注意,现在的数值范围在0.69和3.83之间。 这范围要小得多,使得异常值不会偏离剩下的数据。这种数据操作技巧在机器学习领域是非常常见的。 找到正确的尺度需要一些洞察力。
我们将要研究的下一个特征是主题发生的频率和时间范围。如果主题出现得更多,它可能具有更高的重要性。 此外,我们考虑线程的时间间隔。 因此,主题的频率将使用该主题的电子邮件的时间范围进行正则化。 这使得高度活跃的电子邮件线程会出现在顶部。同样,我们做的这个假设会决定哪些电子邮件应该排名较高。
代码语言:javascript复制//Add to 'def top'
val mailsGroupedByThread = trainingData
.groupBy(x=> x.subject)
//Create a list of tuples with (subject, list of emails)
val threadBarPlotData = mailsGroupedByThread
.map(x =>(x._1, x._2.length))
.toArray
.sortBy(x =>x._2)
val threadDescriptions = threadBarPlotData
.map(x =>x._1)
val threadValues = threadBarPlotData
.map(x =>x._2.toDouble)
//Code changes in 'def top'
val barPlot = BarPlot.plot(threadValues,threadDescriptions)
barPlot.setAxisLabel(1, "Amount of emails persubject")
再次使用log1p函数吧!
代码语言:javascript复制//Code change:
val threadBarPlotData = mailsGroupedByThread
.map(x =>(x._1, Math.log1p(x._2.length)))
.toArray
.sortBy(x => x._2)
现在值的范围在0.69和3.41之间了,这比之前推荐系统的1到29的范围好多了。 然而,我们没有纳入时间框架,因此我们回到正常频率,并应用接下来的转换。为了能够做到这一点,我们首先需要得到第一个和最后一个线程之间的时间:
代码语言:javascript复制//Create a list of tuples with (subject, list of emails,
//time difference between first and last email)
val mailGroupsWithMinMaxDates = mailsGroupedByThread
.map(x =>(x._1, x._2,
(x._2
.maxBy(x => x.emailDate)
.emailDate.getTime -
x._2
.minBy(x => x.emailDate)
.emailDate.getTime
) / 1000
)
)
//turn into a list of tuples with (topic, list of emails,
// time difference, and weight) filtered that onlythreads occur
val threadGroupedWithWeights = mailGroupsWithMinMaxDates
.filter(x =>x._3 != 0)
.map(x =>(x._1, x._2, x._3, 10
Math.log10(x._2.length.toDouble / x._3)))
.toArray
.sortBy(x =>x._4)
val threadGroupValues = threadGroupedWithWeights
.map(x => x._4)
val threadGroupDescriptions = threadGroupedWithWeights
.map(x => x._1)
//Change the bar plot code to plot this data:
val barPlot = BarPlot.plot(threadGroupValues,threadGroupDescriptions)
barPlot.setAxisLabel(1, "Weighted amount of emailsper subject")
这个值非常小,我们想重新缩放一点,这是通过采取10log。 然而,单纯的log会导致我们的值变为负,这就是为什么我们添加一个基本值10,使每个值为正。该加权的最终结果如下:
可以看到我们的值大致在(4.4和8.6)之间,这表明异常值不会在很大程度上影响特征。
---未完待续