《集体智慧编程》读书笔记5

最近重读《集体智慧编程》,这本当年出版的介绍推荐系统的书,在当时看来很引领潮流,放眼现在已经成了各互联网公司必备的技术。
这次边阅读边尝试将书中的一些Python语言例子用C#来实现,利于自己理解,代码贴在文中方便各位园友学习。

由于本文可能涉及到的与原书版权问题,请第三方不要以任何形式转载,谢谢合作。

第五部分 分类

本文介绍的内容是关于如何根据内容来对文档分类。这其中的典型应用如垃圾邮件过滤,当然根据邮件的标题或正文自动将邮件归类到工作、生活或社交等不同文件夹中也是一个很普遍的需求。

过滤垃圾信息

这一部分将实现一个不断收集信息,并根据人们对这些信息的判断进行不断学习从而识别垃圾邮件的邮件分类器。比起传统的基于规则的分类器,前者更容易提供正确的结果。

文档特征

当对文档进行分类时,我们选取的特征就是文档中的单词。而使用单词作为辨别垃圾邮件的假设是:某些单词相对更容易出现在垃圾信息中。除了单词外,词组或短语甚至是文档中缺失的东西都可能是特征。
我们边编写代码边逐渐展开这个话题,首先创建一个名为DocClass的类,并在其中添加用于从文本中提取特征的GetWords函数:

public class DocClass
{
    public Dictionary<string, int> GetWords(string doc)
    {
        // 根据非字母字符进行单词拆分
        var words = Regex.Split(doc, "\\W")
                .Where(s => s.Length > 2 && s.Length < 20)
                .Select(s => s.ToLower());
        // 只返回一组不重复的单词
        return words.Distinct().ToDictionary(w => w, w => 1);
    }
}

这个函数以非字母字符将文本划分为单个单词。类似方法在第三篇文章设计搜索引擎时用到过。

特征的选择的重要性体现在如果选择的特征比较大,则除非碰到两个非常近似的邮件才能将其归为一类,而如果特征选的比较小,则所有邮件都可能包含相同的特征,而无法对其分类。另外像上面代码的实现中我们将所有单词都转为小写,这可能就会使过滤邮件的效果大打折扣,因为在邮件中大小写可能是一个非常重要的特征(尤其是某些垃圾邮件喜欢大写特定单词)。

训练分类器

我们介绍的分类器也是通过接受训练的方式来学习如何对文档进行分类。这与第三章介绍的神经网络类似,都是通过读取正确答案的样本进行学习,样本越多,预测的效果就越好。这个过程可以总结为:从最开始不确定的状态,随着分类器不断了解哪些特征对分类更重要,逐渐增加确定性。
我们建立一个表示分类器的类,而这个类中保存了我们训练所得到的成果。

public class Classifier
{
    // 统计特征/分类组合的数量
    private Dictionary<string, Dictionary<Cat,int>> _fc = new Dictionary<string, Dictionary<Cat, int>>();

    // 统计每个分类中的文档数量
    public Dictionary<Cat, int> _cc = new Dictionary<Cat, int>();

    protected Func<string, Dictionary<string, int>> _getFeatures;

    public Classifier(Func<string, Dictionary<string,int>> getFeatures, string filename = null)
    {
        _getFeatures = getFeatures;
    }
}

代码中的枚举Cat正是用来表示不同分类的,这个枚举定义如下:

public enum Cat
{
    Good, Bad,
    None
}

而成员变量_fc用于记录各分类中不同特征(即单词)的数量。
如果用json来展示_fc中所存储的内容,看起来会类似如下这样:

{
    'python':
    {
        'bad':0,
        'good':6
    },
    'the':
    {
        'bad':3,
        'good':3
    }
}

如上,在被划为'bad'和'good'的邮件中'the'各出现了3次。
而变量_cc记录了各个分类被使用的次数,这是后面介绍的概率计算所需的。
最后一个成员变量_getFeatures是表示由内容中提取特征的方法,本例中即最开始定义的GetWords函数。

之后我们在分类器中添加一些辅助方法来方便对_fc_cc的读取和查询。

// 增加对特征/分类组合的计数
public void Incf(string f, Cat cat)
{
    if(!_fc.ContainsKey(f))
        _fc.Add(f,new Dictionary<Cat, int>());
    if(!_fc[f].ContainsKey(cat))
        _fc[f].Add(cat, 0);
    _fc[f][cat] += 1;
}

// 增加对某一分类的计数值
public void Incc(Cat cat)
{
    if(!_cc.ContainsKey(cat))
        _cc.Add(cat,0);
    _cc[cat] += 1;
}

// 某一特征出现于某一分类中的次数
public int Fcount(string f, Cat cat)
{
    if (_fc.ContainsKey(f) && _fc[f].ContainsKey(cat))
        return _fc[f][cat];
    return 0;
}

// 属于某一分类的内容项数量
public int CatCount(Cat cat)
{
    if (_cc.ContainsKey(cat))
        return _cc[cat];
    return 0;
}

// 所有内容项的数量
public int TotalCount()
{
    return _cc.Values.Sum();
}

// 所有分类的列表
public List<Cat> Categories()
{
    return _cc.Keys.ToList();
}

之后我们在Classifier添加一个Train方法,故名思意这个方法的作用就是对分类器进行训练。其接收一段内容和一个分类,利用_getFeatures定义的方法提取特征,并调用Incf方法增加这些特征的对应的参数指定的分类(cat)的值,并增加指定分类的总计数值:

public void Train(string item, Cat cat)
{
    var features = _getFeatures(item);
    //针对该分类为每个特征增加计数值
    foreach (var f in features)
    {
        Incf(f.Key,cat);
    }

    //增加针对该分类的计数值
    Incc(cat);
}

可以通过如下的代码验证上面的分类器是否正常工作:

var docclass = new DocClass();
var cl = new Classifier(docclass.GetWords);    
cl.Train("the quick brown fox jumps over the lazy dog",Cat.Good);
cl.Train("make quick money in the online casino",Cat.Bad);
var fc = cl.Fcount("quick", Cat.Good);
Console.WriteLine(fc);
fc = cl.Fcount("quick", Cat.Bad);
Console.WriteLine(fc);

为了不用在构造分类器实例时进行训练,我们把训练代码提取到一个函数中并把这个函数是现在DocClass类中:

public void SampleTrain(Classifier cl)
{
    cl.Train("Nobody owns the water.", Cat.Good);
    cl.Train("the quick rabbit jumps fences", Cat.Good);
    cl.Train("buy pharmaceuticals now", Cat.Bad);
    cl.Train("make quick money at the online casino", Cat.Bad);
    cl.Train("the quick brown fox jumps", Cat.Good);
}

这个函数也模拟了统计每一封电子邮件在每个分类中出现的次数。下面我们将这个出现次数转换为概率,即用一个0到1之间的数字来表示一件事情发生的可能性。
对于本例,我们使用一个单词在属于某个分类的邮件(可能不只一封)中出现的次数,除以该分类的文档总数,得到单词在分类中出现的概率。

我们在Classifier类中实现下面的方法完成上面描述的工作:

public float Fprob(string f, Cat cat)
{
    if (CatCount(cat) == 0) return 0;
    // 特征在分类中出现的总次数,除以分类中包含的内容项总数
    return Fcount(f, cat)/(float)CatCount(cat);
}

这个方法得到的概率被称为条件概率,记为Pr(A|B),读作在给定条件B下A的概率。对于本例,我们求得的值表示:对于一个给定的分类,某个单词出现的概率。
用如下代码测试一下这个条件概率的计算:

var docclass = new DocClass();
var cl = new Classifier(docclass.GetWords);  
docclass.SampleTrain(cl);
var prob = cl.Fprob("quick", Cat.Good);
Console.WriteLine(prob);

对于我们SampleTrain所填充的测试数据,这段代码返回0.666667,表示一篇Good分类的邮件包含该词的概率,即Pr(quick|good),为0.66667(2/3)。

接着要做的处理是为了避免在训练数据较少时,一个比较罕见的词,又仅出现在一个分类中,而导致计算本词在其他分类中出现概率时会得到0这种不太客观的结果。
在遇到上面描述的情况时,我们以一个假设的概率(assumedprob)做出判断,如设一个初始概率值0.5。另外需要给假设的概率定一个权重(weight),如权重设为1,表示假设的概率的权重与一个单词实际出现的概率相当。我们将单词真正出现的概率与假设的概率的加权平均值作为最终的概率。这个加权平均值计算公式为:

(weight*assumedprob + count*fprob)/(count+weight)

注意,公式中的count是单词在所有分类中出现的次数。

有了公式,实现方法就很简单。我们将计算加权平均概率的方法WeightedProb加入Classifier中:

public float WeightedProb(string f, Cat cat, Func<string,Cat,float> prf, float weight = 1.0f, float ap = 0.5f)
{
    // 计算当前概率
    var basicprob = prf(f, cat);

    // 特征(即单词)在所有分类中出现的次数
    var totals = Categories().Select(c=>Fcount(f,c)).Sum();

    // 计算加权平均
    var bp = (weight*ap + totals*basicprob)/(weight + totals);
    return bp;
}

通过下面的代码来测试这个权重计算函数:

var docclass = new DocClass();
var cl = new Classifier(docclass.GetWords);
docclass.SampleTrain(cl);
var prob = cl.WeightedProb("money", Cat.Good,cl.Fprob);
Console.WriteLine(prob);
docclass.SampleTrain(cl);
prob = cl.WeightedProb("money", Cat.Good, cl.Fprob);
Console.WriteLine(prob);

经过这样的改进,过滤器可以有更强的能力处理极少出现的单词。

朴素分类器

当求出指定单词在属于某个分类的文档出现的概率,就需要通过另一种方法将各单词的概率组合,从而得到整篇文章属于该分类的概率。
有两种方法可以实现这个目的,本节要介绍的是朴素贝叶斯分类器
朴素二字的含义是,这种方法假设将要被组合的各个概率是彼此独立的。对于本例就是说,一个单词在属于某个指定分类的文档中出现的概率与其他单词出现在该分类的概率是不相关的。
事实上这个假设不成立,现实中,常常会见到三个单词中总有两个单词更可能出现在一篇文章中。若不考虑假设的缺陷,朴素贝叶斯分类器还是一种很有效的文档分类法。也可以作为一个基准对其它分类器的结果进行评价。

使用朴素分类器的第一步是计算整篇文档属于给定分类的概率。在假设每个单词概率彼此独立的情况下,可以将所有概率相乘计算总的概率值。
假设有20%的Bad类文档出现单词"Python",用公式表示即
Pr(Python|Bad)=0.2,又
Pr(Casino|Bad)=0.8,则
两个单词同出现一篇Bad类文档中的独立概率(相互独立的事件同时发生的概率)为:
Pr(Python&Casino|Bad)=0.2*0.8=0.16
即计算整篇文章的概率只需将文章中出现单词的概率相乘即可。

在项目中添加一个NaiveBayes类用于朴素分类器,然后在其中添加DocProb方法用于实现上面描述的通过累乘单词概率值以求文档整体概率的方法:

public class NaiveBayes:Classifier
{
    public NaiveBayes(Func<string, Dictionary<string, int>> getFeatures, string filename = null)
        :base(getFeatures,filename)
    {
    }

    public float DocProb(string item, Cat cat)
    {
        var features = _getFeatures(item);
        //将所有特征的概率相乘
        return features.Select(f => f.Key).Aggregate(1f, (tp, f) => tp*WeightedProb(f, cat, Fprob));
    }
}

经过上面方法的计算,可以得到一个分类下出现一篇文档的概率,即
Pr(Document|Category)
而我们最终需要知道的是一篇文档属于一个分类的概率,即
Pr(Category|Document)
解决这个问题的方法就是贝叶斯定理

贝叶斯定理

贝叶斯定理是一种对条件概率调换求解的方法(这个调换说的很形象,如上面的Document和Category在两个公式中正好是不同的位置)。
贝叶斯定理的公式化表示为:

Pr(A|B)=Pr(B|A)*Pr(A)/Pr(B)

对于本例来说即:

Pr(Category|Document)=Pr(Document|Category)*Pr(Category)/Pr(Document)

Pr(Document|Category)的计算上一小节有描述。而Pr(Category)表示选择某一篇文章属于某分类的概率,即属于某一分类的文章的文档数除以文档的总数。
另外由于我们的目的是对Category的概率进行比较,而不是计算Pr(Category|Document)的准确值,而Pr(Document)对所有Category概率产生的影响是相同的,所以Pr(Document)的计算是不必要的。

下面在NaiveBayes中实现Prob方法用来计算Pr(Document|Category)Pr(Category)的乘积。

public float Prob(string item, Cat cat)
{
    var catProb = CatCount(cat) / (float)TotalCount();
    var docProb = DocProb(item, cat);
    return docProb * catProb;
}

我们来测试下上面分类器的概率计算结果:

var docclass = new DocClass();
var cl = new NaiveBayes(docclass.GetWords);
docclass.SampleTrain(cl);
var prob = cl.Prob("quick rabbit", Cat.Good);
Console.WriteLine(prob);
prob = cl.Prob("quick rabbit", Cat.Bad);
Console.WriteLine(prob);
确定文档所属分类

有了上面的结果,最后一步中我们需要确定一个文档所属的分类。由于现实世界中一些约束的存在,我们不能按照文档分类的概率武断的把其归为一个分类。
比如,在本例垃圾邮件过滤这个场景下,避免把普通邮件当垃圾邮件比截获一封垃圾邮件更为重要。即我们不能按照概率就轻易的把一封邮件归为Bad类。
这里解决这一问题的方法是,为每个分类定义一个最小阈值。比如我们将Bad分类的阈值定义为3,则一个邮件归为Bad的概率至少是Good分类的概率的3倍才能将其归为Bad类;将Good分类的阈值定义为1,则一个邮件归为Good的概率只要大于Bad概率,邮件就会被归为Good。而不满足上面的两种条件的邮件可以被归为“未知”分类。
我们用一个字典来保存不同分类的阈值,并将其添加到NaiveBayes中:

public Dictionary<Cat, float> Thresholds { get; } = new Dictionary<Cat, float>()
{
    [Cat.Bad] = 1f,
    [Cat.Good] = 1f
};

最后,可以实现Classify方法了。方法基本就是按照上文描述进行阈值计算并判断。

public Cat Classify(string item, Cat defaultc = Cat.None)
{
    var probs = new Dictionary<Cat, float>();
    //寻找概率最大的分类
    var max = 0f;
    var best = defaultc;
    foreach (var cat in Categories())
    {
        probs.Add(cat, Prob(item, cat));
        if (probs[cat] > max)
        {
            max = probs[cat];
            best = cat;
        }
    }
    //确保概率值超出阈值*次大概率值
    foreach (var cat in probs.Keys)
    {
        if (cat == best) continue;
        if (probs[cat] * Thresholds[best] > probs[best]) return defaultc;
    }
    return best;
}

最后我们来测试下这个朴素分类器:

var docclass = new DocClass();
var cl = new NaiveBayes(docclass.GetWords);
docclass.SampleTrain(cl);
var cat = cl.Classify("quick rabbit");
Console.WriteLine(cat);
cat = cl.Classify("quick money");
Console.WriteLine(cat);
cl.Thresholds[Cat.Bad] = 3f;
cat = cl.Classify("quick money");
Console.WriteLine(cat);
for (int i = 0; i < 10; i++)
{
    docclass.SampleTrain(cl);
}
cat = cl.Classify("quick money");
Console.WriteLine(cat);

我们可以像测试代码所示的那样调整下阈值进行测试。

费舍尔方法

费舍尔方法为文档中每个特征都求得分类的概率,然后将这些概率组合起来,并判断其是否有可能构成一个随机集合。该方法会返回每个分类的概率,这些概率彼此间是可以比较的。

不同于朴素分类法,这里我们直接计算一篇文档出现某个特征时,文档属于某个分类的概率,即Pr(Category|Feature)
举例来说,如果单词"casino"出现在500篇文档中,其中499篇属于"Bad",则"casino"属于"Bad"的概率将非常接近1。
计算Pr(Category|Feature)的常用公式如下:

Pr(Category|Feature)=(具有指定特征的属于某分类的文档数)/(具有指定特征的文档总数)

这个公式在属于不同分类的文档数量相当时表现很好,当如果其中一个分类的文档数远多于另一个分类,则少量的属于较少分类的特征就会使这个特征表示这个较少的分类的概率大大提高。

为了计算上面的公式,还要进行归一化处理。
按照归一化的思想,上面的公式可以表述为:

Pr(Category|Feature)=(具有指定特征的属于某分类的概率)/(具有指定特征属于所有分类的概率和)

clf=Pr(Feature|Category)表示具有指定特征的属于某分类的概率
freqsum=∑(Pr(Feature|Category))表示具有指定特征属于所有分类的概率和
则有:

Pr(Category|Feature)=clf/freqsum

我们添加一个名为FisherClassifier的子类表示费舍尔分类器,并在其中实现计算上面描述的概率的Cprob方法。

public class FisherClassifier : Classifier
{
    public FisherClassifier(Func<string, Dictionary<string, int>> getFeatures, string filename = null)
        : base(getFeatures, filename)
    {
    }

    public float Cprob(string f, Cat cat)
    {
        //特征在该分类中出现的频率
        var clf = Fprob(f, cat);
        if (clf == 0) return 0;

        //特征在所有分类中出现的频率
        var freqsum = Categories().Select(c => Fprob(f, c)).Sum();

        //概率等于特征在该分类中出现的频率除以总体频率
        var p = clf / freqsum;
        return p;
    }
}

这个函数是基于各分类中所包含的内容项数量相当的假设。返回值表示指定特征的内容属于指定分类的可能性。
下面的代码用来测试这个概率的计算:

var docclass = new DocClass();
var cl = new FisherClassifier(docclass.GetWords);
docclass.SampleTrain(cl);
var prob = cl.Cprob("quick",Cat.Good);
Console.WriteLine(prob);
prob = cl.Cprob("money",Cat.Bad);
Console.WriteLine(prob);

也可以像前文介绍的那样对概率进行加权处理来应对由于训练数据过少导致的对某些概率估计过高。
之前实现的WeightedProb方法以0.5作为概率初始值,通过不断的训练使概率像应有的方向去变化。

var docclass = new DocClass();
var cl = new FisherClassifier(docclass.GetWords);
docclass.SampleTrain(cl);
var prob = cl.WeightedProb("money",Cat.Bad,cl.Cprob);
Console.WriteLine(prob);

下面将各个特征的概率值组合起来,形成总的概率值。这就要用到费舍尔方法,其计算过程是将所有概率相乘起来并取自然对数(以e为底的对数),再将结果乘以-2。下面的FisherProb方法实现这个计算过程,将其加入FisherClassifier中。FisherProb中还用到一个倒置对数卡方函数Invchi2。通过将费舍尔方法的计算结果传给倒置对数卡方函数,可以得到一组随机概率中的最大值。

理论根据在于,如果概率彼此独立且随机分布,则其满足对数卡方分布。
如不属于某个分类的内容项可能随机包含针对该分类的不同特征概率的单词;而一个属于该分类的内容项会包含许多概率值很高的特征。

倒置对数卡方函数如下:

public float Invchi2(float chi, int df)
{
    var m = chi / 2;
    float sum, term;
    sum = term = (float)Math.Exp(-m);
    for (int i = 1; i < df/2; i++)
    {
        term *= m / i;
        sum += term;
    }
    return Math.Min(sum, 1f);
}

费舍尔概率计算:

public float FisherProb(string item, Cat cat)
{
    //将所有概率值相乘
    var features = _getFeatures(item).Keys;
    var p = features.Aggregate(1f, (current, f) => current * WeightedProb(f, cat, Cprob));

    //取自然对数,并乘以-2
    var fscore = (float)(-2 * Math.Log(p));

    //利用倒置对数卡方函数
    return Invchi2(fscore, features.Count * 2);
}

然后就可以测试费舍尔方法计算的概率了

var docclass = new DocClass();
var cl = new FisherClassifier(docclass.GetWords);
docclass.SampleTrain(cl);
var prob = cl.Cprob("quick",Cat.Good);
Console.WriteLine(prob);
prob = cl.FisherProb("quick rabbit", Cat.Good);
Console.WriteLine(prob);
prob = cl.FisherProb("quick rabbit", Cat.Bad);
Console.WriteLine(prob);

费舍尔方法计算的概率都是介于0到1之间,非常适合分类器。
下面就来看如果利用这个概率对文档进行分类。同样我们使用一些手段保证正常邮件不会被错误的归类为垃圾邮件。这里我们将垃圾邮件(Bad类)的概率下限设为0.6,而将Good类下限设为0.2。当文档属于Good类的概率大于0.2就会被归为正常邮件(Bad类概率小于0.6),当文档Bad类概率大于0.6时会被归为垃圾邮件,而两类概率都不满足最低概率时,邮件将被归为未知邮件,这样就基本保证了正常邮件不会被错误的归为垃圾邮件(同样可能有一部分垃圾邮件被当作正常邮件)。
我们按这个原则来实现分类方法Classify,同时还需要给FisherClassifier添加保存最小概率的字典Minimum

public Dictionary<Cat, float> Minimum { get; } = new Dictionary<Cat, float>()
{
    [Cat.Bad] = 0.6f,
    [Cat.Good] = 0.2f
};

public Cat Classify(string item, Cat defaultc = Cat.None)
{
    // 循环遍历并寻找最佳结果
    var best = defaultc;
    var max = 0f;
    foreach (var c in Categories())
    {
        var p = FisherProb(item, c);
        // 确保其超过下限值
        if (p > Minimum[c] && p > max)
        {
            best = c;
            max = p;
        }
    }
    return best;
}

最后测试费舍尔方法分类器的效果:

var docclass = new DocClass();
var cl = new FisherClassifier(docclass.GetWords);
docclass.SampleTrain(cl);
var cat = cl.Classify("quick rabbit");
Console.WriteLine(cat);
cat = cl.Classify("quick money");
Console.WriteLine(cat);
cl.Minimum[Cat.Bad] = 0.8f;
cat = cl.Classify("quick money");
Console.WriteLine(cat);
cl.Minimum[Cat.Good] = 0.4f;
cat = cl.Classify("quick money");
Console.WriteLine(cat);

持久化训练过的分类器

我们可以讲训练结果存储起来用于未来的分类处理。而不是像之前的示例代码那样每次分类前先进行训练。
作为例子这里使用SQLite保存,这里实现一个SqliteClassifier类用于实现通过SQLite存储概率方式的分类器。
为了快速实现这个类,我们通过VS由Classifier提取一个接口IClassifier

public interface IClassifier
{
    int CatCount(Cat cat);
    List<Cat> Categories();
    int Fcount(string f, Cat cat);
    float Fprob(string f, Cat cat);
    void Incc(Cat cat);
    void Incf(string f, Cat cat);
    int TotalCount();
    void Train(string item, Cat cat);
    float WeightedProb(string f, Cat cat, Func<string, Cat, float> prf, float weight = 1, float ap = 0.5F);
}

然后通过“实现IClassifier接口”快速创建SqliteClassifier类的结构。
关于SQLite数据库的使用,参考第二篇文章。这里直接引用那篇文章中的代码。
添加SQLite支持后的SqliteClassifier类如下:

public class SqliteClassifier : IClassifier
{
    private IDbConnection _connection;

    public SqliteClassifier(Func<string, Dictionary<string, int>> getFeatures, string dbname)
    {
        _getFeatures = getFeatures;
        _connection = GetConn(dbname);
        SetDb();
    }

    public void SetDb()
    {
        _connection.Execute("create table if not exists fc(feature, category, count)");
        _connection.Execute("create table if not exists cc(category,count)");
    }

    public IDbConnection GetConn(string dbname)
    {
        DbProviderFactory fact = DbProviderFactories.GetFactory("System.Data.SQLite");
        DbConnection cnn = fact.CreateConnection();
        cnn.ConnectionString = $"Data Source={dbname}";
        cnn.Open();
        return cnn;
    }

    protected Func<string, Dictionary<string, int>> _getFeatures;
}

然后我们需要重新实现其中的一部分方法,这些方法主要和概率的存取有关:

public void Incf(string f, Cat cat)
{
    var count = Fcount(f, cat);
    _connection.Execute(count == 0
        ? $"insert into fc values ('{f}','{cat}', 1)"
        : $"update fc set count={count + 1} where feature='{f}' and category='{cat}'");
}

public int Fcount(string f, Cat cat)
{
    var res = _connection.ExecuteScalar<int>($"select count from fc where feature='{f}' and category='{cat}'");
    return res;
}

public void Incc(Cat cat)
{
    var count = CatCount(cat);
    if (count == 0) _connection.Execute($"insert into cc values ('{cat}', 1)");
    else _connection.Execute($"update cc set count={count + 1} where category='{cat}'");
}

public int CatCount(Cat cat)
{
    var res = _connection.ExecuteScalar<int>($"select count from cc where category='{cat}'");
    return res;    
}

public List<Cat> Categories()
{
    return _connection.Query<string>("select category from cc")
        .ToList()
        .Select(cs => Enum.Parse(typeof(Cat), cs))
        .Cast<Cat>()
        .ToList();
}

public int TotalCount()
{
    var res = _connection.ExecuteScalar<int>("select sum(count) from cc");
    return res;
}

TrainFprobWeightedProb三个方法可以直接照搬之前的实现。
完成SqliteClassifier后,我们只需将FisherClassifierNaiveBayes的父类替换为SqliteClassifier

public class NaiveBayes:SqliteClassifier
{
    //... 略
}

public class FisherClassifier : SqliteClassifier
{
    //... 略
}

另外还要把DocClass类的SampleTrain方法的参数类型改为IClassifier

public void SampleTrain(IClassifier cl)
{
    // ... 略
}

最后我们可以测试这个通过Sqlite保存概率的分类器。

var docclass = new DocClass();
var cl = new FisherClassifier(docclass.GetWords, "test1.db");
docclass.SampleTrain(cl);
var cl2 = new NaiveBayes(docclass.GetWords, "test1.db");
var cat = cl2.Classify("quick money");
Console.WriteLine(cat);

测试代码中我们通过费舍尔方法的分类器进行训练,并使用朴素分类器进行分类测试。

 

posted @ 2017-02-04 13:24  hystar  阅读(722)  评论(0编辑  收藏  举报