构建价格模型

迄今为止,我们已经考查过了一部分分类器,其中大多数都非常适合于对未知数据的所属分类进行预测。但是,在利用多种不同属性(比如价格)对数值型数据进行预测时,贝叶斯分类器、决策树,以及支持向量机(将在下一章中见到)都不是最佳的算法。本章我们将对一系列算法进行考查:这些算法可以接受训练,根据之前见过的样本数据作出数值类的预测,而且它们还可以显示出预测的概率分布情况,以帮助用户对预测过程加以解释。

本章代码中使用的

MatplotLibAPI	->  2.0.2

并且需要按照 TK 来展示图标

sudo apt install python-tk

构造样本数据集 #

本节中,我们将根据一个人为假设的简单模型来构造一个有关葡萄酒价格的数据集。酒的价格是根据酒的等级及其储藏的年代共同来决定的。该模型假设葡萄酒有“峰值年(peakage)”的现象,即:较之峰值年而言,年代稍早一些的酒的品质会比较好一些,而紧随其后的则品质稍差些。一瓶高等级的葡萄酒将从高价位开始,尔后价格逐渐走高直至其“峰值年”;而一瓶低等级的葡萄酒则会从一个低价位开始,价格一路走低。

def wineprice(rating,age):
  peak_age=rating-50
  
  # 基于等级来计算价格
  price=rating/2
  if age>peak_age:
    # 超过峰值年,价格逐渐下降
    price=price*(5-(age-peak_age)/2)
  else:
    # 接近峰值年,价格会增加到原值的5倍
    price=price*(5*((age+1)/peak_age))
  if price<0: price=0
  return price

然后再生成一批数据,这些数据将用于训练模型,以便预测未知的价格

def wineset1():
  rows=[]
  for i in range(300):
    # 随机生成等级和年代
    rating=random()*50+50
    age=random()*50

    # 得到参数价格
    price=wineprice(rating,age)
    
    # 增加点随机噪音
    price*=(random()*0.2+0.9)

    # 添加到数据集
    rows.append({'input':(rating,age),
                 'result':price})
  return rows

K-近邻算法 #

对于我们的葡萄酒定价问题而言,最简单的做法与人们尝试手工进行定价时所采用的做法是一样的一即,找到几瓶情况最为相近的酒,并假设其价格大体相同。算法通过寻找与当前所关注的商品情况相似的一组商品,对这些商品的价格求均值,进而作出价格预测。这种方法被称为k-最近邻算法(k-nearest neighbors,kNN)。

近邻数 #

KNN 算法中的 K 就代表了最终结果参与求平均的商品数量,理想情况下我们取 1,这样我们只会选择距离最近。但是现实总是没有那么美好

如果选择太小

image

如果选择太大

image

定义相似度 #

对于kNN算法,我们首先要做的一件事情是,寻找一种衡量两件商品之间相似程度的方法。我们已经在本书中学到过各种不同的度量方法。此处,我们将选用欧几里德距离算法,相应的函数我们已经在前几章中介绍过了。

def euclidean(v1,v2):
  d=0.0
  for i in range(len(v1)):
    d+=(v1[i]-v2[i])**2
  return math.sqrt(d)

KNN 算法 #

def getdistances(data,vec1):
  distancelist=[]
  
  # 循环每一个元素
  for i in range(len(data)):
    vec2=data[i]['input']
    
    # 计算距离(相近度)
    distancelist.append((euclidean(vec1,vec2),i))
  
  # 按照相近排序
  distancelist.sort()
  return distancelist


def knnestimate(data,vec1,k=5):
  # 得到排序的结果
  dlist=getdistances(data,vec1)
  avg=0.0
  
  # 取前 K 个元素,求平均值
  for i in range(k):
    idx=dlist[i][1]
    avg+=data[idx]['result']
  avg=avg/k
  return avg

knnestimate(wineset1(),(95.0,3.0))
# 25.2318932697

为近邻分配权重 #

目前我们所用的算法有可能会选择距离太远的近邻,对于这样的情况,一种补偿的办法是根据距离的远近为其赋以相应的权重。这种方法与我们在第2章中所采用的方法是类似的,在那里我们根据某一位寻求推荐的用户与其他人在偏好上的相近程度,为那些人的偏好赋予了相应的权重。 因为需要将距离转为权重,所以我们需要一个函数来将距离转换为权重。

反函数 #

函数最为简单的一种形式是返回距离的倒数。不过有时候,完全一样或非常接近的商品,会使权重值变得非常之大,甚至是无穷大。基于这样的原因,我们有必要在对距离求倒数之前先加上一个小小的常量。

def inverseweight(dist,num=1.0,const=0.1):
  return num/(dist+const)

减法函数 #

除了反函数外,我们的另一个选择是减法函数,这是一个很简单的函数,它用一个常量值减去距离。如果相减的结果大于0,则权重为相减的结果

def subtractweight(dist,const=1.0):
  if dist>const:
    return 0
  else:
    return const-dist

高斯函数 #

def gaussian(dist,sigma=5.0):
  return math.e**(-dist**2/(2*sigma**2))

加权 KNN 算法 #

实现加权kNN算法的代码与普通的kNN函数在执行过程上是相同的,函数首先获得经过排序的距离值,然后取距离最近的k个元素。与普通kNN相比,加权kNN算法最重要的区别在于,它并不是对这些元素简单地求平均,它求的是加权平均。加权平均的结果是通过将每一项的值乘以对应权重,然后将所得结果累加得到的。待求出总和以后,我们再将其除以所有权重值之和。

def weightedknn(data,vec1,k=5,weightf=gaussian):
  # 获得距离值
  dlist=getdistances(data,vec1)
  avg=0.0
  totalweight=0.0

  # 计算权重
  for i in range(k):
    dist=dlist[i][0]
    idx=dlist[i][1]
    weight=weightf(dist)
    avg+=weight*data[idx]['result']
    totalweight+=weight
  if totalweight==0: return 0
  avg=avg/totalweight
  # 返回带权重的平均值
  return avg

Cross Validation 交叉验证 #

交叉验证是将数据拆分成训练集与测试集的一系列技术的统称。我们将训练集传人算法,随着正确答案的得出(在本章的例子中即为价格),我们就得到了一组用以进行预测的数据集。随后,我们要求算法对测试集中的每一项数据都作出预测。其所给出的答案,将与正确答案进行对比,算法会计算出一个整体分值,以评估其所做预测的准确程度。

我们首先将数据集拆分

def dividedata(data,test=0.05):
  trainset=[]
  testset=[]
  for row in data:
    if random()<test:
      testset.append(row)
    else:
      trainset.append(row)
  return trainset,testset

对于测试集中的数据进行测试,获得预测结果的综合评价

def testalgorithm(algf,trainset,testset):
  error=0.0
  for row in testset:
    guess=algf(trainset,row['input'])
    error+=(row['result']-guess)**2
  return error/len(testset)

def crossvalidate(algf,data,trials=100,test=0.1):
  error=0.0
  for i in range(trials):
    trainset,testset=dividedata(data,test)
    error+=testalgorithm(algf,trainset,testset)
  return error/trials

不同类型的变量 #

因为所有变量都位于同一值域范围内,因此利用这些变量一次性算出距离值是有意义的。不过,假设我们引入了一个对价格产生影响的新变量,诸如以毫升为单位的酒瓶尺寸。与我们目前已经使用过的变量不同(那些变量的取值均介于0和100之间),这些变量的值域范围可能会达到1500。看看这种情况是如何对最近邻或加权距离的计算结果构成影响的。

image

这样会导致一个问题,这个属性对价格的影响会变的很大。因此我们需要一个手段能够将其转化。不过首先我们需要增加一个维度信息,参考 numpredict.py

按比例缩放 #

此处,我们所需要的并不是一种根据变量的实际值来计算距离的方法,而是需要一种对数值进行归一化处理的方法,从而使所有变量都位于相同的值域范围之内。这样做也有助于找到减少多余变量的方法,或者至少能够降低其对计算结果的影响。为了达成上述两个目标,一种办法就是在进行任何计算之前先对数据重新按比例进行缩放。

def rescale(data,scale):
  scaleddata=[]
  for row in data:
    scaled=[scale[i]*row['input'][i] for i in range(len(scale))]
    scaleddata.append({'input':scaled,'result':row['result']})
  return scaleddata

这样我们再来进行一次测试

data = wineset2()
print crossvalidate(knnestimate, data)
print crossvalidate(weightedknn, data)
sdata = rescale(wineset2(), [10, 10, 0, 0.5])
print crossvalidate(knnestimate, sdata)
print crossvalidate(weightedknn, sdata)

>>>
2195.72038899
2239.76624339
1694.76468502
1608.60143001

相对来说已经好了很多

对缩放结果进行优化 #

理论上,我们可以尝试大量不同数值的组合,:直到发现一个足够好的结果为止,不过也许会有数以百计的变量须要考查,并且这项工作可能会非常地乏味。所幸的是,假如你通读过第5章,想必已经知道了,如何在有许多输人变量须要考查的情况下,利用优化算法自动寻找最优解的办法。

创建一个成本函数,然后利用之前章节中的算法来优化它。

def createcostfunction(algf,data):
  def costf(scale):
    sdata=rescale(data,scale)
    return crossvalidate(algf,sdata,trials=20)
  return costf

然后利用退火等算法来寻找最优解

不对称分布 #

到目前为止,我们已经假设了,如果你对数据求平均或加权平均,那么就会得到一个有关最终价格的合理估计。在许多场合下,这样做是没有问题的,但有些时候,也可能会存在一些无法测定的变量,它们会对结果产生很大的影响。假设在本章的例子中,葡萄酒购买者分别来自两个彼此独立的群组:一部分人是从小酒馆购得的葡萄酒,而另一部分人则是从折扣店购得,并且后者得到了50%的折扣。不幸的是,这些信息在数据集中并没有被记录下来。

我们准备一个样本

def wineset3():
  rows=wineset1()
  for row in rows:
    if random()<0.5:
      # 购买折扣
      row['result']*=0.6
  return rows

估计概率密度 #

def probguess(data,vec1,low,high,k=5,weightf=gaussian):
  # 计算得到距离
  dlist=getdistances(data,vec1)

  # 符合 low 和 high 之间的的权重和
  nweight=0.0

  # 近邻权重和
  tweight=0.0

  for i in range(k):
    dist=dlist[i][0]
    idx=dlist[i][1]
    weight=weightf(dist)
    v=data[idx]['result']

    # Is this point in the range?
    if v>=low and v<=high:
      nweight+=weight
    tweight+=weight
  if tweight==0: return 0

  # (符合 low 和 high 之间的的权重和) / (近邻权重和)
  return nweight/tweight

函数给出了一个合理的执行结果。位于实际价格以外的区间对应概率为0,而覆盖全部区间的概率则接近于1。通过将区间拆分成更小的区段,我们可以确定出每一瓶葡萄酒倾向于集中分布的实际值域范围。不过,这要求我们不断地猜测范围区间,并将其作为输入,直到对数据的整体结构有了一个清晰的认识为止。

图形化展示 #

最终这里展示了图形化的结果,参考 numpredict.py

comments powered by Disqus