集體智慧編程——K近鄰分類器預測價格

K最近鄰(k-Nearest Neighbor,KNN)分類算法,是一個理論上比較成熟的方法,也是最簡單的機器學習算法之一。該方法的思路是:如果一個樣本在特徵空間中的k個最相似(即特徵空間中最鄰近)的樣本中的大多數屬於某一個類別,則該樣本也屬於這個類別。KNN算法中,所選擇的鄰居都是已經正確分類的對象。該方法在定類決策上只依據最鄰近的一個或者幾個樣本的類別來決定待分樣本所屬的類別。 KNN方法雖然從原理上也依賴於極限定理,但在類別決策時,只與極少量的相鄰樣本有關。由於KNN方法主要靠周圍有限的鄰近的樣本,而不是靠判別類域的方法來確定所屬類別的,因此對於類域的交叉或重疊較多的待分樣本集來說,KNN方法較其他方法更爲適合。
  KNN算法不僅可以用於分類,還可以用於迴歸。通過找出一個樣本的k個最近鄰居,將這些鄰居的屬性的平均值賦給該樣本,就可以得到該樣本的屬性。更有用的方法是將不同距離的鄰居對該樣本產生的影響給予不同的權值(weight),如權值與距離成正比(組合函數)。
  該算法在分類時有個主要的不足是,當樣本不平衡時,如一個類的樣本容量很大,而其他類樣本容量很小時,有可能導致當輸入一個新樣本時,該樣本的K個鄰居中大容量類的樣本佔多數。 該算法只計算“最近的”鄰居樣本,某一類的樣本數量很大,那麼或者這類樣本並不接近目標樣本,或者這類樣本很靠近目標樣本。無論怎樣,數量並不能影響運行結果。可以採用權值的方法(和該樣本距離小的鄰居權值大)來改進。該方法的另一個不足之處是計算量較大,因爲對每一個待分類的文本都要計算它到全體已知樣本的距離,才能求得它的K個最近鄰點。目前常用的解決方法是事先對已知樣本點進行剪輯,事先去除對分類作用不大的樣本。該算法比較適用於樣本容量比較大的類域的自動分類,而那些樣本容量較小的類域採用這種算法比較容易產生誤分。

在本文的價格預測問題中,應用KNN主要分爲以下幾個步驟:

1. 定義向量之間的相似度爲歐式距離。

要預測某個特徵向量所對應的價格,首先應當計算該特徵向量與所有訓練樣本的距離。並對這些距離進行排序,從而獲得其最近的K個近鄰。
- 選取較大的K會降低準確率
- 選取較小的K會使得計算結果容易受噪聲的影響

2. KNN估計價格(均值法):

直接計算與特徵向量Vec相聚最近的K個近鄰的價格的均值。

3. KNN估計價格(加權法):

權值隨距離的增大而減小,此處的權值衰減函數有三類:
1)反函數:權值 = 1/(距離 + 常數) 其中常數的作用是防止分母爲0.
2)減法函數: 權值 = 常數 - 距離
3)高斯函數: 權值 = exp(- (x ^ 2) / (2 * sigma^2)) 其中均值爲0,表示距離爲0時權值最大,隨着距離的增加,權值按照高斯函數的形式衰減。

4. 交叉驗證

按照一定的比例,將數據集劃分爲訓練集和測試集
- 根據測試集中的每一項,調用KNN計算價格,累計其平方誤差。
- 重複前面兩步,多次劃分,取誤差的平均值作爲交叉驗證的均值。

5. 縮放特徵

消除不同特徵尺度的影響,同時識別干擾變量。

此處可以利用前面的博客中講述的最優化方法,通過模擬退火算法或遺傳算法,尋找縮放因子的最優值。其中,需要優化的代價函數即爲交叉驗證的誤差值。

優化算法博客鏈接

在《集體智慧編程》上所描述的葡萄酒價格預測問題的源代碼實現如下(其中優化算法採用模擬退火算法):

# -*- coding:utf-8 -*-
__author__ = 'Bai Chenjia'

from random import random, randint
import math


# 產生訓練數據集,參數是訓練數據的兩個特徵,根據兩個特徵計算價格
def winprice(rating, age):
    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


# 生成300條訓練數據,價格在winprice函數的基礎上上下變動20%
def wineset1():
    rows = []
    for i in range(300):
        # 隨機生成年代和等級
        rating = random() * 50 + 50
        age = random() * 50
        # 得到一個參考價格
        price = winprice(rating, age)
        # 添加噪聲
        price *= (random() * 0.4 + 0.8)
        # 加入數據集
        rows.append({'input': (rating, age), 'result': price})
    return rows


# 定義兩個向量的相似度爲歐氏距離
def euclidean(v1, v2):
    d = 0.0
    for i in range(len(v1)):
        a = v1[i]
        b = v2[i]
        d += pow(a - b, 2)
    return math.sqrt(d)


# 獲取要預測的向量vec1與數據集data中所有元素的距離
def getdistances(data, vec1):
    distancelist = []
    for i in range(len(data)):
        vec2 = data[i]['input']
        distancelist.append((euclidean(vec1, vec2), i))
    distancelist = sorted(distancelist, key=lambda x: x[0])
    return distancelist


# 對vec1的K個近鄰的訓練數據的價格取平均值作爲對vec1價格的估計
def knnestimate(data, vec1, k=5):
    # 經過得到排序的距離值
    dlist = getdistances(data, vec1)
    avg = 0.0

    # 對K個近鄰的結果去平均
    for i in range(k):
        avg += data[dlist[i][1]]['result']
    avg /= k
    return avg


# 優化knnestimate函數,對不同距離的近鄰進行距離加權
# 其中權值與距離成反比關係,以下是三種權值隨距離衰減的方式

# 1.反函數
def inverseweight(dist, num=1.0, const=0.1):
    return num / (dist + const)


# 2.減法函數
def subtractweight(dist, const=1.0):
    if dist > const:
        return 0
    else:
        return const - dist


# 3.高斯函數
def gaussian(dist, sigma=10.0):
    return math.e**(-dist**2 / sigma**2)


# 加權KNN算法,根據距離對K個近鄰加權,權值乘以對應的價格作累加最後除以權值之和
# 參數weightf是函數,指示使用哪一種權值衰減方式
def weightedknn(data, vec1, k=5, weightf=gaussian):
    dlist = getdistances(data, vec1)
    result = 0.0
    weight = 0.0
    for i in range(k):
        price = data[dlist[i][1]]['result']      # 價格
        result += price * weightf(dlist[i][0])  # 距離加權,累加價格和
        weight += weightf(dlist[i][0])          # 統計權值和
    return result / weight


# 交叉驗證
# 1. 隨機劃分數據集,test指定了測試集所佔的比例
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


# 2. 爲算法提供訓練集,針對測試集中的每一項內容調用算法,返回誤差
#    其中參數algf是一個函數,可以是 knnestimate, weightedknn或者其他根據KNN算法算法計算價格的函數
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)


# 3. 交叉驗證。 多次調用dividedata函數對數據進行隨機劃分,並計算誤差,取所有隨機劃分誤差的均值
def crossvalidate(algf, data, trials=100, test=0.05):
    error = 0.0
    # trials代表隨機劃分的次數
    for i in range(trials):
        trainset, testset = dividedata(data, test)
        error += testalgorithm(algf, 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 = winprice(rating, age)
        price *= (bottlesize / 750)
        price *= random() * 0.9 + 0.2
        rows.append({'input': (rating, age, aisle, bottlesize), 'result': price})
    return rows


# 縮放,參數scale的長度與訓練數據特徵的長度相同. 每個參數乘以訓練數據中的特徵以達到縮放特徵的目的
def rescale(data, scale):
    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 knn3(d, v):
    return knnestimate(d, v, k=3)


def knn1(d, v):
    return knnestimate(d, v, k=1)


# 構造 優化搜索算法 的代價函數
def createcostfunction(algf, data):
    def costf(scale):
        sdata = rescale(data, scale)
        return crossvalidate(algf, data)
    return costf


# 搜索算法4:模擬退火算法
# 參數:T代表原始溫度,cool代表冷卻率,step代表每次選擇臨近解的變化範圍
# 原理:退火算法以一個問題的隨機解開始,用一個變量表示溫度,這一溫度開始時非常高,而後逐步降低
#      在每一次迭代期間,算法會隨機選中題解中的某個數字,然後朝某個方向變化。如果新的成本值更
#      低,則新的題解將會變成當前題解,這與爬山法類似。不過,如果成本值更高的話,則新的題解仍
#      有可能成爲當前題解,這是避免局部極小值問題的一種嘗試。
# 注意:算法總會接受一個更優的解,而且在退火的開始階段會接受較差的解,隨着退火的不斷進行,算法
#      原來越不能接受較差的解,直到最後,它只能接受更優的解。
# 算法接受較差解的概率 P = exp[-(highcost-lowcost)/temperature]
def annealingoptimize(schedulecost, domain, T=10000.0, cool=0.9, step=2):
    # 隨機初始化值
    vec = [randint(domain[i][0], domain[i][1]) for i in range(len(domain))]
    # 循環
    while T > 0.1:
        # 選擇一個索引值
        i = randint(0, len(domain) - 1)
        # 選擇一個改變索引值的方向
        c = randint(-step, step)  # -1 or 0 or 1
        # 構造新的解
        vecb = vec[:]
        vecb[i] += c
        if vecb[i] < domain[i][0]:  # 判斷越界情況
            vecb[i] = domain[i][0]
        if vecb[i] > domain[i][1]:
            vecb[i] = domain[i][1]

        # 計算當前成本和新的成本
        cost1 = schedulecost(vec)
        cost2 = schedulecost(vecb)

        # 判斷新的解是否優於原始解 或者 算法將以一定概率接受較差的解
        if cost2 < cost1 or random() < math.exp(-(cost2 - cost1) / T):
            vec = vecb

        T = T * cool  # 溫度冷卻
        print vecb[:], "代價:", schedulecost(vecb)

    self.printschedule(vec)
    print "模擬退火算法得到的最小代價是:", schedulecost(vec)
    return vec


if __name__ == '__main__':
    """
    data = wineset1()
    price = knnestimate(data, (95.0, 5.0))
    print price
    price = weightedknn(data, (95.0, 5.0))
    print price

    print crossvalidate(knnestimate, data)
    """
    """
    data = wineset2()
    print crossvalidate(weightedknn, data)

    data = rescale(data, [10, 10, 0, 0.5])
    print crossvalidate(weightedknn, data)
    """
    weightdomain = [(0, 20)] * 4
    data = wineset2()
    costf = createcostfunction(knnestimate, data)
    annealingoptimize(costf, weightdomain)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章