《集體智慧編程》讀書筆記7

最近重讀《集體智慧編程》,這本當年出版的介紹推薦系統的書,在當時看來很引領潮流,放眼如今已經成了各互聯網公司必備的技術。
此次邊閱讀邊嘗試將書中的一些Python語言例子用C#來實現,利於本身理解,代碼貼在文中方便各位園友學習。python

因爲本文可能涉及到的與原書版權問題,請第三方不要以任何形式轉載,謝謝合做。git

第七部分 kNN算法

上一節的最後,給出了一個用方差來做爲結果爲數值的決策樹評價的方法。這一部分咱們針對結果爲數值的數據集給出一種更好的預測算法。
針對數值型結果預測的算法最關鍵的工做就是肯定哪些變量對結果的影響最大,這個能夠經過前面文章介紹的「優化技術「(如模擬退火和遺傳算法)來自動肯定變量的權重。
這一部分將以商品價格預測做爲例子,這是多個特徵決定一個數值的結果的典型例子。github

構造數據集

這個例子中咱們模擬構造了一個關於葡萄酒價格的數據集。價格模型的肯定方式是:酒的價格根據酒的等級及其儲藏的年代共同決定,另外假設葡萄酒有」峯值年「概念,較之峯值年,年代早的葡萄酒品質更高,而峯值年以後的則品質稍差。高等級的葡萄酒的價格將從高位隨着越接近峯值年價格越高。而低等級的葡萄酒價格從低位逐漸走低。
咱們建立一個名爲NumPredict的類並在其中加入WinePrice來模擬生成葡萄酒價格:算法

public double WinePrice(double rating, double age)
{
    var peakAge = rating - 50;
    //根據等級計算價格
    var price = rating / 2;
    if (age > peakAge)
    {
        //通過「峯值年」,後繼5年裏其品質將會變差
        price = price * (5 - (age - peakAge) / 2); //原書配書源碼有/2,印刷版中沒有是個錯誤,會致使爲0的商品過多
    }
    else
    {
        //價格在接近「峯值年」時會增長到原值的5倍
        price = price * (5 * (age + 1) / peakAge);
    }
    if (price < 0)
        price = 0;
    return price;
}

而後再添加一個名爲WineSet1用於模擬生產一批葡萄酒,並使用上面的方法制定葡萄酒的價格。最終的價格會在上面函數肯定的價格基礎上隨機加減20%,這一是模擬稅收,市場供應的客觀狀況對價格的影響,二來可使數據更真實增長數值型預測的難度。編程

public List<PriceStructure> WineSet1()
{
    var rows = new List<PriceStructure>(300);
    var rnd = new Random();
    for (int i = 0; i < 300; i++)
    {
        //隨機生成年代和等級
        var rating = rnd.NextDouble() * 50 + 50;
        var age = rnd.NextDouble() * 50;
        //獲得參考價格
        var price = WinePrice(rating, age);
        //增長「噪聲」
        price *= rnd.NextDouble() * 0.9 + 0.2; //配書代碼的實現
        //加入數據集
        rows.Add(new PriceStructure()
        {
            Input = new[] { rating, age },
            Result = price
        });
    }
    return rows;
}

上面代碼中咱們還添加了一個內部類PriceStructure用於表示一瓶酒的價格造成結構。
接着咱們測試下上面的代碼,保證能夠生成葡萄酒的價格數據集以用於後續的預測:flask

var numPredict = new NumPredict();
var price = numPredict.WinePrice(95, 3);
Console.WriteLine(price);
price = numPredict.WinePrice(95, 8);
Console.WriteLine(price);
price = numPredict.WinePrice(99, 1);
Console.WriteLine(price);
var data = numPredict.WineSet1();
Console.WriteLine(JsonConvert.SerializeObject(data[0]));
Console.WriteLine(JsonConvert.SerializeObject(data[1]));

因爲是隨機生成,每次構造的價格數據集都是不同的。數組

k-最鄰近算法

k-最鄰近算法(k-nearest neighbors),簡稱kNN,的思想很簡單,找到與所預測商品最近似的一組商品,對這些近似商品價格求均值來做爲價格預測。服務器

近鄰數 - k

kNN中的k表示所查找的最近似的一組商品的數量,理想情況下,設置k爲1會查找與待預測商品最近似的商品價格做爲預測結果。
但實際狀況中,會有如本例中故意加入的」噪聲「這種干擾狀況,使得最爲進行的一個商品的價格不能最準確的反應待預測商品的價格。因此就須要經過選取k(k>1)個近似的商品並取其價格的均值來減小」噪聲「影響。
固然若是選擇過多的類似商品(較大的k值),也會致使均值產生不該有的誤差。dom

定義類似度

要使用kNN算法,第一個要作的就是肯定判斷兩個商品類似度的方法。咱們使用以前文章介紹過的歐幾里德距離算法。
咱們將算法函數Euclidean加入NumPredictide

public double Euclidean(double[] v1, double[] v2)
{
    var d = v1.Select((t, i) => (double)Math.Pow(t - v2[i], 2)).Sum();
    return (double)Math.Sqrt(d);
}

接着來測試下歐幾里德距離算法計算到的類似度:

var numPredict = new NumPredict();
var data = numPredict.WineSet1();
var similar = numPredict.Euclidean(data[0].Input, data[1].Input);
Console.WriteLine(similar);

目前的kNN算法所使用的類似度算法存在的問題,就是對不一樣因素的一樣量度差異是同等看待的,而現實狀況是一種因素每每產生的影響比另外一種更大(一樣量度下),後文將會介紹解決此問題的方法。

實現kNN算法

kNN的實現很簡單,並且雖然其計算量較大,但能夠進行增量訓練。
首先,咱們在NumPredict中添加計算距離列表的方法GeetDistances

private SortedDictionary<double, int> GetDistances(List<PriceStructure> data, double[] vec1)
{
    var distancelist = new SortedDictionary<double, int>(new RankComparer());
    for (int i = 0; i < data.Count; i++)
    {
        var vec2 = data[i].Input;
        distancelist.Add(Euclidean(vec1, vec2), i);
    }
    return distancelist;
}

class RankComparer : IComparer<double>
{
    public int Compare(double x, double y)
    {
        if (x == y) //這樣可讓SortedList保存重複的key
            return 1;
        return x.CompareTo(y); //從小到大排序
    }
}

方法中咱們使用SortedDictionary按距離進行了排序方便後面取前k個最近的項。
接着是knnestimate函數,其取上面列表的前k項並求平均值。

public double KnnEstimate(List<PriceStructure> data, double[] vec1, int k = 5)
{
    //獲得通過排序的距離值
    var dlist = GetDistances(data, vec1);
    return dlist.Values.Take(k).Average(dv => data[dv].Result);
}

有了這些方法就能夠對商品進行估價了。

var numPredict = new NumPredict();
var data = numPredict.WineSet1();
var estimate = numPredict.KnnEstimate(data, new [] { 95d, 3});
Console.WriteLine(estimate);
estimate = numPredict.KnnEstimate(data, new[] { 99d, 3});
Console.WriteLine(estimate);
estimate = numPredict.KnnEstimate(data, new[] { 99d, 5});
Console.WriteLine(estimate);
var real = numPredict.WinePrice(99, 5);
Console.WriteLine(real);
estimate = numPredict.KnnEstimate(data, new[] { 99d, 5}, 1);
Console.WriteLine(estimate);

上面的方法對比了預測價格與真實價格,並能夠看到不一樣的k值對結果的影響。

爲近鄰分配權重

上面的算法中計算預測價格時採用k個近似的商品的均價而沒有考慮這些商品的近似度不一樣。這一部分咱們對這個問題進行一些修正,咱們按照近似程度對這些商品的價格賦予必定的權重。將「距離」(類似度指標)轉爲權重有以下三種方法。

反函數

反函數即取距離的倒數做爲權重。因爲當距離極小(類似度極高)時,權重會極具變大(分母減少過快,會使倒數明顯增大)。咱們在計算權重前,給距離加上一個初始常量來避免這個問題。

將反函數的實現方法InverseWeight加入NumPredict中:

public double InverseWeight(double dist, double num = 1, double @const = 0.1f)
{
    return num / (dist + @const);
}

反函數計算速度很快,但其明顯的問題是,隨着類似度下降,權重衰減很快,有些狀況下這可能會帶來問題。

減法函數

減法函數是一個更簡單的函數,其用一個常量值減去距離,若是結果大於0,則將結果做爲權重,不然權重爲0。
NumPredict中的SubtractWeight方法是減法函數的實現:

public double SubtractWeight(double dist, double @const = 1)
{
    if (dist > @const) return 0;
    return @const - dist;
}

這個方法的缺陷是,若是權重值都降爲0,可能沒法找到足夠的近似商品來提供預測數據。

高斯函數

當距離爲1時,高斯函數計算的權重爲1;權重值隨着距離增長而減少,但始終不會減少到0。這就避免了減法函數中那樣出現沒法預測的問題。
將高斯函數的實現方法Gaussian加入NumPredict中:

public double Gaussian(double dist, double sigma = 10)
{
    var exp = (double)Math.Pow(Math.E, -dist * dist / (2 * sigma * sigma));
    return exp;
}

經過代碼也能夠看出,高斯函數的速度不像以前的方法那樣快。

在實現加權kNN前先來測試下這些權值計算函數:

var numPredict = new NumPredict();
var weight = numPredict.SubtractWeight(0.1f);
Console.WriteLine(weight);
weight = numPredict.InverseWeight(0.1f);
Console.WriteLine(weight);
weight = numPredict.Gaussian(0.1f);
Console.WriteLine(weight);
weight = numPredict.Gaussian(1);
Console.WriteLine(weight);
weight = numPredict.SubtractWeight(1);
Console.WriteLine(weight);
weight = numPredict.InverseWeight(1);
Console.WriteLine(weight);
weight = numPredict.Gaussian(3);
Console.WriteLine(weight);

三個函數都符合距離越遠,權重越小這一要求。

加權kNN

加權kNN與以前的普通kNN就在於對於最相似的k個商品的價格是求加權評價,而非普通的求平均。
加權平均的作法就是把每一個商品的價格乘以其權重,累加全部加權價格後再除以權重的和。
NumPredict中加入加權kNN計算方法Weightedknn

public double Weightedknn(List<PriceStructure> data, double[] vec1, int k = 5,
    Func<double, double, double> weightf = null)
{
    if (weightf == null) weightf = Gaussian;
    //獲得通過排序的距離值
    var dlist = GetDistances(data, vec1);
    var avg = 0d;
    var totalweight = 0d;

    //獲得加權平均
    foreach (var kvp in dlist.Take(k))
    {
        var dist = kvp.Key;
        var idx = kvp.Value;
        var weight = weightf(dist, 10);

        avg += weight * data[idx].Result;
        totalweight += weight;
    }
    if (totalweight == 0) return 0;
    avg /= totalweight;
    return avg;
}

咱們用下面的代碼測試下加權kNN計算的結果:

var numPredict = new NumPredict();
var data = numPredict.WineSet1();
var price = numPredict.Weightedknn(data, new []{99d, 5});
Console.WriteLine(price);

能夠看到WeightedknnKnnEstimate有更好的預測效果。
下面會介紹怎麼去驗證這些不一樣預測方法的優劣。

交叉驗證

交叉驗證是將數據集拆分爲訓練集和測試集。使用訓練集來訓練算法,並將測試集的每一項傳入算法獲得一個預測結果,將這個預測結果與真實值進行對比,最後獲得一個偏差分值。經過這個分值能夠評估預測的準確程度。
一般交叉驗證會進行多測,每次使用不一樣的數據集劃分方式,結果分值也是幾回交叉驗證分值的平均值。在劃分上通常訓練集數據佔數據集的95%,剩下的是測試集。
首先在NumPredict中實現數據劃分方法DivideData

public Tuple<List<PriceStructure>, List<PriceStructure>> DivideData(List<PriceStructure> data,
    double test = 0.05f)
{
    var trainSet = new List<PriceStructure>();
    var testSet = new List<PriceStructure>();
    var rnd = new Random();
    foreach (var row in data)
    {
        if (rnd.NextDouble() < test)
            testSet.Add(row);
        else
            trainSet.Add(row);
    }
    return Tuple.Create(trainSet, testSet);
}

接着是一次測試的方法TestAlgorithm,仍然是放在NumPredict中:

public double TestAlgorithm(Func<List<PriceStructure>, double[], double> algf,
    List<PriceStructure> trainSet, List<PriceStructure> testSet)
{
    var error = 0d;
    foreach (var row in testSet)
    {
        var guess = algf(trainSet, row.Input);
        error += Math.Pow(row.Result - guess, 2);
    }
    return error / testSet.Count;
}

這個方法使用訓練集訓練,並在測試集上進行測試,使用預測結果與真實結果進行比較。咱們在計算差別值時選擇了平方差。方差給傾向於給全部測結果和真實結果都比較接近的算法更高的分值。若是不在乎個別預測結果與真實值有較大起伏,可使用差值絕對值的平均值代替方差。

最後是交叉測試的主方法CrossValidate,這個方法實際上就是將上面兩個方法反覆調用多並給出結果的平均值。

public double CrossValidate(Func<List<PriceStructure>, double[], double> algf,
    List<PriceStructure> data, int trials = 100, double test = 0.05f)
{
    var error = 0d;
    for (int i = 0; i < trials; i++)
    {
        var setDiv = DivideData(data, test);
        error += TestAlgorithm(algf, setDiv.Item1, setDiv.Item2);
    }
    return error / trials;
}

有了這些方法就能夠測試不一樣的算法,或者是同一個算法給予不一樣的參數的預測效果。

var numPredict = new NumPredict();
var data = numPredict.WineSet1();
Func<List<NumPredict.PriceStructure>, double[], double> knnEstiDefault =
    (dataset, vec) => numPredict.KnnEstimate(dataset, vec);
var score = numPredict.CrossValidate(knnEstiDefault, data);
Console.WriteLine(score);
Func<List<NumPredict.PriceStructure>, double[], double> knnEsti3 =
    (dataset, vec) => numPredict.KnnEstimate(dataset, vec, 3);
score = numPredict.CrossValidate(knnEsti3, data);
Console.WriteLine(score);
Func<List<NumPredict.PriceStructure>, double[], double> knnEsti1 =
    (dataset, vec) => numPredict.KnnEstimate(dataset, vec, 1);
score = numPredict.CrossValidate(knnEsti1, data);
Console.WriteLine(score);

在博主的測試中,默認參數的kNN算法效果最好(分值最低,表示預測和實際偏差最小)。
也能夠試試加權kNN,以及使用非默認權重函數(高斯函數)的加權kNN。

var numPredict = new NumPredict();
var data = numPredict.WineSet1();
Func<List<NumPredict.PriceStructure>, double[], double> weightedKnnDefault =
    (dataset, vec) => numPredict.Weightedknn(dataset, vec);
var score = numPredict.CrossValidate(weightedKnnDefault, data);
Console.WriteLine(score);
Func<double, double, double> inverseWeight = (d, n) => numPredict.InverseWeight(d);
Func<List<NumPredict.PriceStructure>, double[], double> weightedKnnInverse =
    (dataset, vec) => numPredict.Weightedknn(dataset, vec, 5, inverseWeight);
score = numPredict.CrossValidate(weightedKnnInverse, data);
Console.WriteLine(score);

上面的測試數據集只有兩個不一樣的選項 - 等級和年份。當商品有更多選項時,在比較類似度時怎樣自動給予不一樣的選項不一樣的權重是下一部分要討論的話題。

多種輸入變量

以前的例子中咱們的輸入變量只有兩種,並且這兩種變量是通過特地設計。顯示中輸入可能有多種變量,並且這些變量可能有如下問題:

  1. 不一樣變量的值域差別較大,這會致使臨近距離值不能真實反應兩條記錄的差別,即值域較大的變量所帶來的影響會影響其餘變量,即便這個變量自己不是與結果關係最大的變量。
  2. 有些變量與結果幾乎沒有關係,但以前的方法仍然會將其影響計算在內。

新的數據集

咱們實現一個名爲WineSet2的方法生成一個存在上文描述問題的輸入集。

public List<PriceStructure> WineSet2()
{
    var rows = new List<PriceStructure>(300);
    var rnd = new Random();
    for (int i = 0; i < 300; i++)
    {
        //隨機生成年代和等級
        var rating = rnd.NextDouble() * 50 + 50;
        var age = rnd.NextDouble() * 50;
        var aisle = (double)rnd.Next(1, 20);
        var sizeArr = new[] { 375d, 750d, 1500d, 3000d };
        var bottleSize = sizeArr[rnd.Next(0, 3)];
        //獲得參考價格
        var price = WinePrice(rating, age);
        price *= (bottleSize / 750);
        //增長「噪聲」
        price *= (rnd.NextDouble() * 0.9d + 0.2d); //配書代碼的實現
        //加入數據集
        rows.Add(new PriceStructure()
        {
            Input = new[] { rating, age, aisle, bottleSize },
            Result = price
        });
    }
    return rows;
}

新的數據集添加了兩個列,其中第三列模擬葡萄酒桶存放的通道。第四列模擬葡萄酒桶的尺寸。
試試生成一個新的數據集:

var numPredict = new NumPredict();
var data = numPredict.WineSet2();
Console.WriteLine(JsonConvert.SerializeObject(data));

在新的數據集上測試下以前的算法:

var numPredict = new NumPredict();
var data = numPredict.WineSet2();
Func<List<NumPredict.PriceStructure>, double[], double> knnEsti3 =
    (dataset, vec) => numPredict.KnnEstimate(dataset, vec, 3);
var score = numPredict.CrossValidate(knnEsti3, data);
Console.WriteLine(score);
Func<List<NumPredict.PriceStructure>, double[], double> weightedKnnDefault =
    (dataset, vec) => numPredict.Weightedknn(dataset, vec);
score = numPredict.CrossValidate(weightedKnnDefault, data);
Console.WriteLine(score);

經過結果能夠看出交叉驗證結果很不理想,這是由於咱們沒有對不一樣的變量區別對待。

按比例縮放

結果變量值域不一致的簡單作法就是對輸入值進行縮放,即相似歸一化的操做。具體到實現上就是將變量乘以一個縮放比例。
下面的ReScale方法對每一個列的變量進行了縮放:

public List<PriceStructure> ReScale(List<PriceStructure> data, double[] scale)
{
    return (from row in data
            let scaled = scale.Select((s, i) => s * row.Input[i]).ToArray()
            select new PriceStructure()
            {
                Input = scaled,
                Result = row.Result
            }).ToList();
}

數組參數scale保存了每一個列的縮放比例。
咱們能夠試着構造一個縮放比例,如過道信息與價格無關,將其縮放比例設爲0。

var numPredict = new NumPredict();
var data = numPredict.WineSet2();
var sdata = numPredict.ReScale(data, new[] { 10, 10, 0, 0.5 });
Func<List<NumPredict.PriceStructure>, double[], double> knnEsti3 =
    (dataset, vec) => numPredict.KnnEstimate(dataset, vec, 3);
var score = numPredict.CrossValidate(knnEsti3, sdata);
Console.WriteLine(score);
Func<List<NumPredict.PriceStructure>, double[], double> weightedKnnDefault =
    (dataset, vec) => numPredict.Weightedknn(dataset, vec);
score = numPredict.CrossValidate(weightedKnnDefault, sdata);
Console.WriteLine(score);

優化縮放比例

上面的代碼中,因爲輸入集是咱們子集構造的,因此知道如何選擇最佳的縮放比例。現實中肯定縮放比例就須要其餘技巧。
利用以前文章介紹的優化算法能夠幫咱們選擇最佳的縮放比例。
優化算法須要一個值域範圍,即每一個例的縮放比例的範圍以及一個成本函數。
對於值域範圍,以下代碼生成的結果就很適合比例:

public List<Tuple<int, int>> GetWeightDomain(int count)
{
    var domains = new List<Tuple<int, int>>(count);
    for (var i = 0; i < 4; i++)
    {
        domains.Add(Tuple.Create(0, 20));
    }
    return domains;
}

權重最小值0表示此項與結果毫無關係。

對於成本函數,咱們能夠用以下方法包裝下以前實現的CrossValidate就能快速獲得一個很是好用的成本函數。

public Func<double[], double> CreateCostFunction(Func<List<PriceStructure>, double[], double> algf,
    List<PriceStructure> data)
{
    Func<double[], double> Costf = scale =>
    {
        var sdata = ReScale(data, scale);
        return CrossValidate(algf, sdata, 10);
    };
    return Costf;
}

另外咱們還須要以前文章實現的優化算法,因爲參數類型的問題,咱們不能直接使用以前的代碼,而須要對參數類型稍做調整:

public List<double> AnnealingOptimize(List<Tuple<int, int>> domain, Func<double[], double> costf,
    float T = 10000.0f, float cool = 0.95f, int step = 1)
{
    //隨機初始化值
    var random = new Random();
    var vec = domain.Select(t => (double)random.Next(t.Item1, t.Item2)).ToArray();

    while (T > 0.1)
    {
        //選擇一個索引值
        var i = random.Next(0, domain.Count - 1);
        //選擇一個改變索引值的方向
        var dir = random.Next(-step, step);
        //建立一個表明題解的新列表,改變其中一個值
        var vecb = vec.ToArray();
        vecb[i] += dir;
        if (vecb[i] < domain[i].Item1) vecb[i] = domain[i].Item1;
        else if (vecb[i] > domain[i].Item2) vecb[i] = domain[i].Item2;
        //計算當前成本和新成本
        var ea = costf(vec);
        var eb = costf(vecb);
        //是更好的解?或是退火過程當中可能的波動的臨界值上限?
        if (eb < ea || random.NextDouble() < Math.Pow(Math.E, -(eb - ea) / T))
            vec = vecb;
        //下降溫度
        T *= cool;
    }
    return vec.ToList();
}

public List<double> GeneticOptimize(List<Tuple<int, int>> domain, Func<double[], double> costf,
    int popsize = 50, int step = 1, float mutprob = 0.2f, float elite = 0.2f, int maxiter = 100)
{
    var random = new Random();
    //變異操做
    Func<double[], double[]> mutate = vec =>
    {
        var i = random.Next(0, domain.Count - 1);
        if (random.NextDouble() < 0.5 && vec[i] > domain[i].Item1)
            return vec.Take(i).Concat(new[] { vec[i] - step }).Concat(vec.Skip(i + 1)).ToArray();
        else if (vec[i] < domain[i].Item2)
            return vec.Take(i).Concat(new[] { vec[i] + step }).Concat(vec.Skip(i + 1)).ToArray();
        return vec;
    };
    //配對操做
    Func<double[], double[], double[]> crossover = (r1, r2) =>
    {
        var i = random.Next(1, domain.Count - 2);
        return r1.Take(i).Concat(r2.Skip(i)).ToArray();
    };
    //構造初始種羣
    var pop = new List<double[]>();
    for (int i = 0; i < popsize; i++)
    {
        var vec = domain.Select(t => (double)random.Next(t.Item1, t.Item2)).ToArray();
        pop.Add(vec);
    }
    //每一代中有多少勝出者?
    var topelite = (int) (elite*popsize);
    Func<double, double, int> cf = (x, y) => x == y ? 1 : x.CompareTo(y);
    var scores = new SortedList<double, double[]>(cf.AsComparer());
    //主循環
    for (int i = 0; i < maxiter; i++)
    {
        foreach (var v in pop)
           scores.Add(costf(v),v);
        var ranked = scores.Values;
        //從勝出者開始
        pop = ranked.Take(topelite).ToList();

        //添加變異和配對後的勝出者
        while (pop.Count<popsize)
        {
            if (random.NextDouble() < mutprob)
            {
                //變異
                var c = random.Next(0, topelite);
                pop.Add(mutate(ranked[c]));
            }
            else
            {
                //配對
                var c1 = random.Next(0, topelite);
                var c2 = random.Next(0, topelite);
                pop.Add(crossover(ranked[c1],ranked[c2]));
            }
        }

        //打印當前最優值
        //Console.WriteLine(scores.First().Key);
    }
    return scores.First().Value.ToList();
}

好了,如今能夠試着用優化算法獲得縮放比例了:
首先來試一下模擬退火算法:

var numPredict = new NumPredict();
var data = numPredict.WineSet2();
Func<List<NumPredict.PriceStructure>, double[], double> knnEstimate =
    (dataset, vec) => numPredict.KnnEstimate(dataset, vec);
var costf = numPredict.CreateCostFunction(knnEstimate, data);
var optimization = new Travel();
var optDomain = optimization.AnnealingOptimize(numPredict.GetWeightDomain(4), costf, step: 2);
Console.WriteLine(JsonConvert.SerializeObject(optDomain));

接着能夠試試速度慢但效果更好的遺傳算法:

var numPredict = new NumPredict();
var data = numPredict.WineSet2();
Func<List<NumPredict.PriceStructure>, double[], double> knnEstimate =
    (dataset, vec) => numPredict.KnnEstimate(dataset, vec);
var costf = numPredict.CreateCostFunction(knnEstimate, data);
var optimization = new Travel();
var optDomain = optimization.GeneticOptimize(numPredict.GetWeightDomain(4), costf, popsize: 5);
Console.WriteLine(JsonConvert.SerializeObject(optDomain));

這種自動肯定縮放比例的方法也可讓咱們瞭解到哪些因素與結果關係密切,從而採起更好的市場策略。

不對稱分佈

在以前的例子中,預測價格經過取類似商品價格的平均值或加權平均值來進行。可是對於以下假設這種處理方法就會有問題。
假設一部分葡萄酒是從折扣店購買,其價格只有正常價格的50%,可是這個折扣沒有做爲變量出如今輸入數據中。
咱們用方法WineSet3模擬這樣一個數據集:

public List<PriceStructure> WineSet3()
{
    var rows = WineSet1();
    var rnd = new Random();
    foreach (var row in rows)
    {
        if (rnd.NextDouble() < 0.5)
            // 模擬從折扣店購得的葡萄酒
            row.Result *= 0.5;
    }
    return rows;
}

這個數據集是在WineSet1生成的數據基礎上改造而來。
咱們仍然能夠用以前的算法處理這個數據集:

var numPredict = new NumPredict();
var data = numPredict.WineSet3();
var price = numPredict.WinePrice(99, 20);
Console.WriteLine(price);
price = numPredict.Weightedknn(data, new[] { 99d, 20 });
Console.WriteLine(price);

能夠看到真實價格和預測價格之間可能有較大差別。並且因爲平均咱們平均處理預測價格可能被進行了25%的折扣。

估計密度機率

咱們須要實現一個方法來肯定價格位於一個區間的機率。咱們首先找出必定數量的類似商品,而後用價格處於區間內的類似商品的權重和處於全部類似商品的權重和獲得商品處於這個價格區間的機率。
咱們在ProbGuess方法中實現這個機率估計過程:

public double ProbGuess(List<PriceStructure> data, double[] vec1, double low,
    double high, int k = 5, Func<double, double, double> weightf = null)
{
    if (weightf == null) weightf = Gaussian;
    var dlist = GetDistances(data, vec1);
    var nweight = 0d;
    var tweight = 0d;
    for (int i = 0; i < k; i++)
    {
        var dlistCurr = dlist.Skip(i).First();
        var dist = dlistCurr.Key;
        var idx = dlistCurr.Value;
        var weight = weightf(dist, 10);
        var v = data[idx].Result;
        // 當前數據點在指定範圍嗎?
        if (v >= low && v <= high)
            nweight += weight;
        tweight += weight;
    }
    if (tweight == 0) return 0;
    //機率等於位於指定範圍內的權重值除以全部權重值
    return nweight / tweight;
}

參數low和high就是要判斷的價格區間。嘗試下這個機率預測函數:

var numPredict = new NumPredict();
var data = numPredict.WineSet3();
var prob = numPredict.ProbGuess(data, new[] { 99d, 20 }, 40, 80);
Console.WriteLine(prob);
prob = numPredict.ProbGuess(data, new[] { 99d, 20 }, 80, 120);
Console.WriteLine(prob);
prob = numPredict.ProbGuess(data, new[] { 99d, 20 }, 120, 1000);
Console.WriteLine(prob);
prob = numPredict.ProbGuess(data, new[] { 99d, 20 }, 30, 120);
Console.WriteLine(prob);

經過一小段一小段的傳入價格區間進行機率計算,能夠肯定整個數據集的價格分佈狀況。
下節將經過一種直觀的方法來展現機率分佈狀況:

繪製機率分佈

經過繪製機率分佈圖,能夠避免上節逐段猜想價格區間的作法。

關於C#函數圖的繪製,通過一番尋找在GitHub發現了這個名爲MatplotlibCS的項目。
MatplotlibCS採用了一種很特殊的方式對matplotlib進行封裝,關於這個項目的起源,能夠看這篇博文

這裏簡單介紹下MatplotlibCS的工做方式,MatplotlibCS的代碼分兩部分,C#編寫的客戶端,以及Python編寫的服務器端。C#端將Python庫matplotlib所須要的數據經過http傳輸到基於Python庫Flask編寫的服務器端。Flask接收客戶端傳來的數據並在內部調用matplotlib完成實際的座標圖像繪製。
這種方式能夠擴展到其餘有C#調用Python庫需求的常見。

下面來講說博主配置MatplotlibCS的曲折經歷。博主電腦上安裝有官方2.7.4版本的Python,因而直接使用以下命令安裝matplotlib:

python -m pip install matplotlib

固然這會毫無心外的報錯(博主我也是折騰到後來才知道),基本上常見的錯誤如:

The following required packages can not be built:
freetype, png

去matplotlib官網看看,文檔中說Windows平臺建議使用如WinPython等第三方發行版,這些版本中通常都集成了matplotlib及一些經常使用的庫。因而下載了一個基於3.6.0的64位WinPython。
試了下集成的matplotlib可使用。開始進行下一步。

可使用下面的代碼測試matplotlib是否可用:
from pylab import *
a=[1,2,3,4]
b=[2,3,4,1]
plot(a,b)
show()

MatplotlibCS的服務器端部分,使用

python.exe matplotlib_cs.py

這樣的格式來啓動基於Python的Web服務。其中,文件matplotlib_cs.py位於MatplotlibCS-master/MatplotlibCS/Python內。
運行上述命令,報錯說找不到名爲task的庫。使用pip安裝後,繼續報錯沒法由task導入Task。非常無解,百思不得姐後,放棄這種方法。

就在這山窮水盡之時,靈機一動想到了WSL(好多地方直接稱其Windows Bash)。既然是Python部分只是做爲一個服務端,咱們徹底能夠把其獨立運行。而WSL就是一個很好的運行環境。

MatplotlibCS的代碼也作了判斷,若是檢測到服務端在運行(無論是何種方式啓動的),C#代碼就不會再去啓動Python服務端了。
使用WSL的一個很是妙的地方時,WSL中發佈的Python服務也是在127.0.0.1,也就是本機,這個域名下。因爲這個服務端地址在MatplotlibCS代碼中是寫死的,使用WSL就不須要咱們從新修改、編譯MatplotlibCS代碼。而是能夠直接使用MatplotlibCS的Nuget包。
使用Docker for Windows能夠實現和WSL一致的效果,它們兩個也是各有所長。

WSL中有2.7.6和3.4.3兩個版本的Python,命令分別爲python和python3,對應的pip分別爲pip和pip3。博主使用Python3運行MatplotlibCS的Python服務成功。下面是步驟:
在WSL中使用pip3安裝matplotlib

sudo pip3 install matplotlib #注意須要root權限

通常來講會報以下錯誤:

The following required packages can not be built:
freetype, png

須要在WSL單獨安裝這個包:

sudo apt-get install libfreetype6-dev

這個包及其依賴包能夠知足安裝matplotlib的須要。安裝完成後,重試matplotlib安裝,通常都會成功。

順便安裝後面須要用到的flask

sudo pip3 install flask

環境裝好,下面就能夠啓動服務了。
首先進入MatplotlibCS源碼中包含matplotlib_cs.py這個文件的目錄。執行:

python3 matplotlib_cs.py

不出意外服務能夠正常啓動。能夠看到以下提示:

Running on http://127.0.0.1:57123/ (Press CTRL+C to quit)

不得不說,能夠和Windows共用同一套文件是WSL最大特點。若是使用Docker for Windows免不了要掛載主機目錄。

服務端完成,開始編寫客戶端代碼,首先在項目中安裝MatplotlibCS
能夠直接使用Nuget安裝:

Install-Package MatplotlibCS

還須要自行安裝下NLog(MatplotlibCS使用了NLog但沒有在Nuget包中聲明這個依賴)

Install-Package NLog

安裝後,咱們寫一段簡單的代碼進行測試(這個代碼繪製的圖像,和以前測試matplotlib的Python代碼是相同的):

public List<Axes> BuildAxes()
{
    return new List<Axes>()
    {
        new Axes(1, "X", "Y")
        {
            Title = "MatplotlibCS Test",
            PlotItems =
            {
                new Line2D("Line 1")
                {
                    X = new List<object>() {1,2,3,4},
                    Y = new List<double>() {2,3,4,1}
                }
            }
        }
    };
}

public void Draw(List<Axes> plots)
{
    // 因爲咱們在外部啓動Python服務,這兩個參數傳空字符串就能夠了
    var matplotlibCs = new MatplotlibCS.MatplotlibCS("", "");

    var figure = new Figure(1, 1)
    {
        FileName = $"/mnt/e/Temp/result{DateTime.Now:ddHHmmss}.png",
        OnlySaveImage = true,
        DPI = 150,
        Subplots = plots
    };
    var t = matplotlibCs.BuildFigure(figure);
    t.Wait();
}

這其中BuildAxes用於構造要繪製的座標數據,Draw用於實際完成調用來生成圖片。
使用下面的代碼測試:

var numPredict = new NumPredict();
var axes = numPredict.BuildAxes();
numPredict.Draw(axes);

執行成功後就會在FileName指定的目錄種看到圖片。
注意這個圖片輸出目錄,咱們寫的是一個WSL樣式的絕對路徑(代碼種表示E盤下的Temp目錄)。這樣可讓WSL中的Python服務直接把輸出的文件寫到本地磁盤中。而若是使用相對路徑,客戶端會把其變成基於Windows文件系統的絕對路徑並傳給WSL,而WSL沒法識別這種路徑,致使Python服務報錯。

書歸正題,咱們開始實現繪製本例的機率分佈圖,咱們將繪製兩種不一樣類型的機率分佈圖:
第一種稱爲累計機率,累計圖顯示的是價格小於給定值的機率,因此隨着給訂價格(x軸)的增長,機率值(y軸)會逐漸升高直到1。
繪製累計機率圖的方法很簡單,只須要逐漸增大價格區段並循環調用ProbGuess方法,將獲得機率值做爲Y軸值便可,具體實現見CumulativeGraph

public void CumulativeGraph(List<PriceStructure> data, double[] vec1,
    double high, int k = 5, Func<double, double, double> weightf = null)
{
    if (weightf == null) weightf = Gaussian;
    var t1 = new List<object>();
    for (var i = 0d; i < high; i += 0.1)
        t1.Add(i);
    var cprob = t1.Select(v => ProbGuess(data, vec1, 0, (double)v, k, weightf)).ToList();

    var axes = new Axes(1, "Price", "Cumulative Probility")
    {
        Title = "Price Cumulative Probility",
        PlotItems =
        {
            new Line2D("")
            {
                X = t1,
                Y = cprob
            }
        }
    };
    Draw(new List<Axes>() { axes });
}

調用這個方法也很簡單:

生成機率累加圖以下:

能夠看到在20-40及60-80區間機率和發生了變更,能夠肯定這兩個價格區間就是商品價格的主要分佈區間,這樣就避免了以前的無緒猜想。
另外一種機率圖繪製方法就是繪製該價格位置處的實際機率。但若是直接繪製,所顯示圖像將是一個個跳躍的小點。因此咱們採起將一個價格對應的機率與左右的機率進行加權平均,這樣可使繪製的函數圖像更連續。
咱們在ProbabilityGraph中實現了機率值的加權平均處理及函數圖的繪製:

public void ProbabilityGraph(List<PriceStructure> data, double[] vec1,
    double high, int k = 5, Func<double, double, double> weightf = null,double ss=5)
{
    if (weightf == null) weightf = Gaussian;
    // 價格值域範圍
    var t1 = new List<object>();
    for (var i = 0d; i < high; i += 0.1)
        t1.Add(i);
    // 整個值域範圍的全部機率
    var probs = t1.Cast<double>()
        .Select(v => ProbGuess(data, vec1, v, v+0.1, k, weightf)).ToList();
    // 經過加上近鄰機率的高斯計算結果,對機率值作平滑處理
    var smoothed = new List<double>();
    for (int i = 0; i < probs.Count; i++)
    {
        var sv = 0d;
        for (int j = 0; j < probs.Count; j++)
        {
            var dist = Math.Abs(i - j)*0.1;
            var weight = Gaussian(dist, sigma: ss);
            sv += weight*probs[j];
        }
        smoothed.Add(sv);
    }
    var axes = new Axes(1, "Price", "Probility")
    {
        Title = "Price Probility",
        PlotItems =
        {
            new Line2D("")
            {
                X = t1,
                Y = smoothed
            }
        }
    };
    Draw(new List<Axes>() { axes });
}

開始繪圖吧

var numPredict = new NumPredict();
var data = numPredict.WineSet3();
numPredict.ProbabilityGraph(data, new[] { 1d, 1 }, 120);

生成的圖像以下:

能夠看到商品所處的價格區間和機率累計圖所反映的價格區間一致,可是這個圖像還能更直觀的反應出落在哪一個價格區間的商品更多。
經過商品價格機率分佈圖,還能看出咱們的輸入數據缺乏了必定的關鍵因素。因此這樣的圖能夠給決策者提供更好的銷售策略,訂價策略支持。

總結

kNN算法的缺點在於要計算每一個點之間的類似度(距離),計算量很大。另外若是須要經過優化算法肯定輸入列的權重,又會增長很大的計算量。在數據集很大時,計算將會很是緩慢。 而kNN的優勢時能夠增量的進行訓練。經過繪製機率分佈,還能夠看出輸入中是否缺少的必要的因素。

相關文章
相關標籤/搜索