《集体智慧编程》读书笔记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的优势时能够增量的进行训练。经过绘制几率分布,还能够看出输入中是否缺少的必要的因素。

相关文章
相关标签/搜索