《集體智慧編程》讀書筆記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;
}
而Train、Fprob和WeightedProb三個方法可以直接照搬之前的實現。
完成SqliteClassifier后,我們只需將FisherClassifier和NaiveBayes的父類替換為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);
測試代碼中我們通過費舍爾方法的分類器進行訓練,并使用樸素分類器進行分類測試。

浙公網安備 33010602011771號