机器学习 — 构建价格模型

对数值类数据建模—加权k近邻算法

根据相邻的数据预测出目标的取值情况

算法:

  • 计算给定向量与所有其他数据的距离,并按照距离排序
  • 选出前k位,求前k个数据的加权平均,权重根据距离求得

要点:

  • 计算距离:使用欧几里得距离算法
  • 计算权重算法:
    • 反函数
    • 减法函数
    • 高斯函数
  • 缩放:对于各个变量的取值范围相差较大的情况或者属性对结果的影响程度不同的情况可以进行缩放,通过优化算法找到合适缩放参数
  • 交叉验证:用来评估算法预测的准确性。将给定数据集分成训练集、测试集,先使用训练集训练算法,然后使用算法计算测试集中数据,将结果和测试数据集对比,计算出误差
  • 估计概率密度:给出预测值范围,对于同一情况的取值不确定(可能有其他因素影响,但是没有反映出来,比如同一瓶葡萄酒有的是原价,有的是打折的,打折这个因素并没有考虑在内),这个时候可以给出一个预测值的范围,而不是一个简单的数字

优点:

  1. 无任何计算开销的情况下加入新的测试数据(因为算法使用的是加权值计算的)

缺点:

  1. 预测的计算量较大,因为要计算每个点到其他所有点的距离
  2. 在变量较多(但是各个变量对结果的影响程度是不一样的)的情况下,很难确定合理的权重值,可以通过“优化”解决这个问题

适用场景:
对包含多种数值型属性进行预测

from random import random, randint
import math

def wineprice(rating, age):
    """
    根据等级和年份计算红酒的价格
    
    Args:
        rating:红酒等级
        age:红酒年份
        
    Returns:
        返回红酒的价格
    """
    peak_age = rating - 50
    
    # 根据等级计算价格
    price = rating / 2
    if age > peak_age:
        # 经过峰值年之后,后继5年里品质将会变差
        price = price * (5 - (age - peak_age))
    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 = price * (random() * 0.4 + 0.8)
        
        # 加入数据集
        rows.append({'input':(rating, age), 'result':price})
    return rows

def euclidean(v1, v2):
    """
    利用欧几里得距离算法计算相似度
    
    Args:
        v1:需要计算相似度的数据
        v2:需要计算相似度的数据
    """
    d = 0.0
    for i in range(len(v1)):
        d += (v1[i] - v2[i]) ** 2
    return math.sqrt(d)

def getdistance(data, vec1):
    """
    计算按照与给定数据vec1的距离的大小排序的数据集合
    
    Args:
        data:数据集合
        vec1:需要计算的数据
        
    Returns:
        返回按照距离排序后的数据集合
    """
    distancelist = []
    for i in range(len(data)):
        vec2 = data[i]['input']
        # 记录vec1与对应数据的相似度和该数据的index
        distancelist.append((euclidean(vec1, vec2), i))
    # 按照距离进行排序
    distancelist.sort()
    return distancelist

# KNN 最近邻算法
def knnestimate(data, vec1, k=5):
    """
    计算前k项的平均值
    
    Args:
        data:数据集
        vec1:给定向量
        k:取最相似的那些数据
    
    Returns:
        返回平均值
    """
    # 按照给定向量vec1与数据集data中相似度(使用欧几里得距离算法)大小排序的集合
    dlist = getdistance(data, vec1)
    avg = 0.0
    
    # 对前k项求平均值
    for i in range(k):
        idx = dlist[i][1]
        avg += data[idx]['result']
    avg /= k
    return avg
# 构造数据集
data = wineset1()
print data[0]

# 对红酒的价格进行评估
print knnestimate(data, (95.0, 3.0))
{'input': (81.49658395876126, 26.92679356629202), 'result': 210.39750758496763}
21.364958649

上面的算法中,直接根据近邻的平均值进行评估,但是我们有可能使用距离较远的近邻,这个时候可以使用通过赋予不同的权重来补偿
将距离转换为权重通常有一下3种方法:

  1. 反函数
  2. 减法函数
  3. 高斯函数
# 反函数
def inverseweight(distance, num=1.0, const=0.1):
    """
    通过反函数将距离转化为权重
    
    缺点:
        1. 近邻的权重很大,稍微远一些就变得很小(反函数刚开始比较陡)
    
    Args:
        distance:距离
        num:反函数分子
        const:加的常量
    
    """
    # 加一个常量是为了避免:两个很相近的数求倒数之后会使权值变得非常大
    return num / (distance + 0.1)

# 减法函数
def substractweight(dist, const=1.0):
    """
    通过减法函数将距离转化为权重:如果大于阈值const之外的都丢弃(直接返回0),否则返回和阈值的差值
    
    缺点:
        1. 找不到足够数目的近邻项(因为最后权重值会变为0)
        
    优点:
        1. 解决了反函数的缺点:近邻的权重很大
    """
    if dist > const:
        return 0
    return const - dist
    
# 高斯函数
def gaussian(distance, sigma=10.0):
    """
    通过高斯函数计算权重值
    优点:
        1. 解决了反函数和减法函数的缺点
    缺点:
        1. 计算复杂
    """
    return math.e**(-distance**2 / (2 * sigma**2))

# 加权后的KNN 最近邻算法
def weightedknn(data, vec1, k=5, weightfunc=gaussian):
    """
    计算前k项的加权平均值
    
    Args:
        data:数据集
        vec1:给定向量
        k:取最相似的那些数据
        weightfunc:将距离转化为权重的函数
    
    Returns:
        返回平均值
    """
    # 按照给定向量vec1与数据集data中相似度(使用欧几里得距离算法)大小排序的集合
    dlist = getdistance(data, vec1)
    avg = 0.0
    totalweight = 0.0
    
    # 对前k项求平均值
    for i in range(k):
        distance = dlist[i][0]
        idx = dlist[i][1]
        weight = weightfunc(distance)
        avg += data[idx]['result'] * weight
        totalweight += weight
    avg /= totalweight
    return avg
# 利用加权之后的knn算法对红酒的价格进行评估
print weightedknn(data, (95.0, 5.0))
34.245051125

交叉验证

将数据集拆分成训练集、测试集,先使用训练集训练出的模型对测试集中的数据给出预测值,然后和测试集中的数据进行对比,评估预测准确程度

def dividedata(data, test=0.05):
    """
    拆分数据集为训练集和测试集
    
    Args:
        data:给定数据集
        test:测试集占的比例
        
    Returns:
        返回划分好的训练集和测试集
    """
    trainset = []
    testset = []
    for row in data:
        if random() < test:
            testset.append(row)
        else:
            trainset.append(row)
    return trainset, testset

def testalgorithm(algorithm, trainset, testset):
    """
    评估指定算法的准确程度:使用误差的平方可以突出误差较大的项,因为我们认为一个大多数
    接近准确值,但是偶尔会有较大偏差的算法,要比始终接近于正确值的算法稍差一些
    
    Args:
        algorithm:需要评估的算法
        trainset:训练数据集
        testset:测试数据集
    
    Returns:
        返回所有误差平方和的平均值
    """
    error = 0.0
    for row in testset:
        guess = algorithm(trainset, row['input'])
        error += (row['result']-guess)**2
    return error / len(testset)

def crossvalidate(algorithm, data, trials=100, test=0.5):
    """
    随机划分trials次,计算平均的准确度
    
    Args:
        algorithm:被评估的算法
        data:数据集
        trials:划分的次数
        test:测试数据集所占比例
        
    Returns:
        返回平均的准确度
    """
    error = 0.0
    for i in range(trials):
        trainset, testset = dividedata(data, test)
        error += testalgorithm(algorithm, trainset, testset)
    return error / trials

上面的红酒价格的决定因素是等级和年份,但是还有可能有更多的决定因素(有些因素的取值范围比较大,使用上面的算法的时候这个因素的影响就会比较大),比如酒瓶的尺寸,或者数据集中还有一些无关紧要的特征,比如葡萄酒的通道号

# 加入更多的变量
def wineset2():
    rows = []
    for i in range(300):
        rating = random() * 50 + 50
        age = random() * 50
        aisle = float(randint(1, 20))
        bottlesize = [375.0, 750.0, 1500.0, 3000.0][randint(0, 3)]
        price = wineprice(rating, age)
        price *= bottlesize / 750
        price *= random() * 0.9 + 0.2
        rows.append({'input':(rating, age, aisle, bottlesize), 'result':price})
    return rows

def rescale(data, scale):
    """
    对data列表中的数据按照指定比例scale进行缩放
    
    Args:
        data:数据集
        scale:缩放比例的列表,针对每一项的缩放比例
        
    Returns:
        返回缩放后的数据
    """
    scaledata = []
    for row in data:
        scaled = [scale[i] * row['input'][i] for i in range(len(scale))]
        scaledata.append({'input':scaled, 'result':row['result']})
    return scaledata

def createcostfunction(algorithm, data):
    def costfunc(scale):
        sdata = rescale(data, sclae)
        return crossvalidate(algorithm, sdata, trials=10)
    return costfunc
def knn1(d, v):
    return knnestimate(d, v, k=1)
def knn3(d, v):
    return knnestimate(d, v, k=3)

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

print crossvalidate(knn3, sdata)
print crossvalidate(weightedknn, sdata)
10784.9945206
9758.99785876
11208.4046009
12466.9529663
weightdomain = [(0, 20)] * 4
costfunc = createcostfunction(knnestimate, data)
# 构造不对称分布的数据集
def wineset3():
    rows = wineset1()
    for row in rows:
        if random() < 0.5:
            # 葡萄酒从折扣店购得,也就是不对称分布
            row['result'] *= 0.5
    return rows


def probguess(data, vec, low, high, k=5, weightfunc=gaussian):
    """
    计算价格落在某一个区间的概率:计算出区间low-hight之间总的邻近权重值和,除以所有数据的邻近权重值之和
    
    Args:
        data:数据集
        vec:需要预估的数据向量
        low:区间的下限
        high:区间的上限
        k=:最邻近的数据个数
        weightfunc:权重函数
        
    Returns:
        返回落在某一个区间的概率            
    """
    dlist = getdistance(data, vec)
    nweight = 0.0
    tweight = 0.0
    
    for i in range(k):
        dist = dlist[i][0]
        idx = dlist[i][1]
        weight = weightfunc(dist)
        v =data[idx]['result']
        
        # 判断当前价格是否位于指定区间内
        if v >= low and v <= high:
            nweight += weight
        tweight += weight
    if tweight == 0:
        return 0
    
    # 概率等于位于该区间的权重值之和除以所有的权重值之和
    return nweight / tweight
        
# 不对称的数据集
data = wineset3()
print '价格位于%d-%d之间的概率%f' % (40, 80, probguess(data, [99, 20], 40, 80))
print '价格位于%d-%d之间的概率%f' % (80, 120, probguess(data, [99, 20], 80, 120))
print '价格位于%d-%d之间的概率%f' % (40, 80, probguess(data, [99, 20], 120, 1000))
print '价格位于%d-%d之间的概率%f' % (30, 120, probguess(data, [99, 20], 30, 120))
价格位于40-80之间的概率0.803895
价格位于80-120之间的概率0.196105
价格位于40-80之间的概率0.000000
价格位于30-120之间的概率1.000000
from pylab import *
def cumulativegraph(data, vec, high, k=5, weightfunc=gaussian):
    """
    将得到的概率使用累积概率的方法显示在图形中:尝试所有0-high之间的区间,
        将对应的high和得到的概率绘制在图中
    
    Args:
        data:数据集
        vec:需要预估的数据向量
        high:区间的上限
        k=:最邻近的数据个数
        weightfunc:权重函数
    """
    # 构造一个步长为0.1的从0.0到hight的数组
    t1 = arange(0.0, high, 0.1)
    # 取0-h之间的概率
    cprob = array([probguess(data, vec, 0, h, k, weightfunc) for h in t1])
    plot(t1, cprob)
    show()
    
def allprobgraph(data, vec, high, k=5, weightfunc=gaussian):
    """
    将得到的概率显示在图形中:连续依次计算每个区间(h, h+0.1)的概率
    但是得到的图形不平滑,在预测的价格附近形成突起,但是其他地方几乎为0
    
    Args:
        data:数据集
        vec:需要预估的数据向量
        high:区间的上限
        k=:最邻近的数据个数
        weightfunc:权重函数
    """
    t1 = arange(0.0, high, 0.1)
    # 取h-(h+0.1)之间的概率
    cprob = array([probguess(data, vec, h, h+0.1, k, weightfunc) for h in t1])
    plot(t1, cprob)
    show()

def probabilitygraph(data, vec, high, k=5, weightfunc=gaussian, ss=5.0):
    """
    将得到的概率显示在图形中:连续依次计算每个区间(h, h+0.1)的概率
    每一个价位点的概率等于周围概率的加权平均,类似加权knn算法
    
    Args:
        data:数据集
        vec:需要预估的数据向量
        high:区间的上限
        k=:最邻近的数据个数
        weightfunc:权重函数
        ss:高斯函数的系数
    """
    t1 = arange(0.0, high, 0.1)
    # 取h-(h+0.1)之间的概率
    prob = array([probguess(data, vec, h, h+0.1, k, weightfunc) for h in t1])
    
    # 取周边概率的加权平均,对概率做平滑处理
    smoothed = []
    for i in range(len(prob)):
        sv = 0.0
        for j in range(len(prob)):
            dist = abs(i - j) * 0.1
            weight = gaussian(dist, sigma=ss)
            sv += weight * prob[j]
        smoothed.append(sv)
    smoothed = array(smoothed)
    
    plot(t1, smoothed)
    show()
cumulativegraph(data, (1,1), 120)
allprobgraph(data, (1,1), 120)
probabilitygraph(data, (1, 1), 120)


posted @ 2017-04-01 19:30  lacker  阅读(828)  评论(0编辑  收藏  举报