摘要
- 本詞頻統計器包括行數統計、字符數統計、單詞數統計、詞頻統計功能。基於紅8黑樹算法和穩定排序實現,其中紅黑樹算法為本詞頻統計器提供良好的效率、提供性能下限保證、提供詞頻統計的高性能、提供較小的資源開銷,而穩定排序算法提供了排序的穩定性,保證了詞頻統計結果按照字典序生成。本詞頻統計器基於C++實現,如下圖所示,統計器作為對象,具有五個成員函數,分別實現統計器的五個功能,而功能函數提供了穩定排序等功能。並設計了異常處理函數,解決一定場景下的異常問題。
算法關鍵
紅黑樹
紅黑樹是一種自平衡的二叉查找樹,是一種高效的查找樹。
本詞頻統計器基於紅黑樹算法,具有以下優點:
-
提供良好的效率:可在O(logN)時間內完成查找、增加、刪除等操作,能保證在最壞情況下,基本的動態幾何操作的時間均為O(lgn)。只要求部分地達到平衡要求,降低了對旋轉的要求,任何不平衡都會在三次旋轉之內解決,從而提高了效率。本詞頻統計器設計大量增刪改查操作,紅黑樹算法提供了良好的效率支撐。
-
提供性能下限保證:相比於BST,紅黑樹可以能確保樹的最長路徑不大於兩倍的最短路徑的長度,可見其查找效果的最低保證。最壞的情況下也可以保證O(logN)的復雜度,好於二叉查找樹O(N)復雜度。在大數據量情況下,紅黑樹算法為詞頻統計器提供良好的性能保證。
-
提供詞頻統計的高性能:紅黑樹的算法時間復雜度和AVL樹相同,但統計性能更高。插入 AVL樹和紅黑樹的速度取決於所插入的數據。在數據比較雜亂的情況,則紅黑樹的統計性能優於AVL樹。在詞頻統計時,數據分布較為雜亂,在此應用場景下,紅黑樹算法與詞頻統計器契合。
-
提供較小的資源開銷:與基於哈希的算法相比,基於紅黑樹方法帶來更小的資源開銷,程序消耗內存較少。哈希的算法占用大量資源,需要維護大量的計數器,並且在哈希過程中消耗了大量的計算資源。本詞頻統計器消耗的資源較少。
穩定排序
假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,則稱這種排序算法是穩定的。本詞頻統計器利用了穩定排序。
- 穩定性;詞頻統計后,要求按字典序輸出,而經過紅黑樹處理后,以達到字典序的要求,若使用非穩定排序,雖性能較高,但打亂了原先的字典序。經過穩定排序后,相對次序保持不變,仍為字典序,滿足要求。
- 達到一定性能:本詞頻統計器利用了STL庫中的stable_sort()函數,避免重復制造車輪。其復雜度是O(N (log N)^2)。在有足夠內存時,可以達到O( N log N)。
代碼框架
本程序代碼為如上結構,分為兩個部分:
- .h
- .cpp
.h文件:
定義:
- 宏定義:用於設定存儲空間大小
- 類定義:定義了統計器的類
- 結構體定義:定義了存放詞頻統計的結構體
聲明
- 成員函數聲明:聲明了包括行統計、字符統計、詞數統計、詞頻統計功能的成員函數
- 功能函數聲明:聲明了包括字符類型轉換、結構體比較的功能函數
- 異常處理函數聲明:聲明了三種的異常處理函數
.cpp文件
- 主函數定義:定義了主函數
- 成員函數定義:定義了包括行統計、字符統計、詞數統計、詞頻統計功能的成員函數
- 功能函數定義:定義了包括字符類型轉換、結構體比較的功能函數
- 異常處理函數定義:定義了三種的異常處理函數
頻率統計器的實現
下列過程中,從上到下為詞頻統計的實現大致過程:
- 打開文件
- 異常檢測
- 文件流按行讀取到字符串數組
- 特殊符號處理
- 大寫字母處理
- 行數組單詞化
- 單詞篩選
- 構造紅黑樹
- 提取鍵值對至結構體數組
- 穩定排序
- 重構字符流
接口設計與實現
接口設計
- 設計了一個Counter類,和構造函數。構造函數從外部獲取的源文件名和目的文件名,進行文件流操作。
- 設計了四個具有統計功能的成員函數,通過獲取的源、目的文件名,對文件進行讀寫,統計行數、字符、單詞、頻率。不在函數中直接輸出,或者直接寫入文件,而是返回一個整型值,將輸出與功能解耦,降低了函數之間的耦合度。
- 設計了一個Write()函數直接用於文件讀寫,專門完成該功能。
class Counter
{
private:int Line;
int Ch;
int Words;
string Freq;
string sfn, dfn;
public:Counter(){}
Counter(string sfn,string dfn)
{
this->sfn = sfn;
this->dfn = dfn;
Line = 0;
Ch = 0;
Words = 0;
Freq = "\0";
}
int LineCount();
int CharCount();
int WordCount();
string WordFreq();
void Write();
};
- 定義了宏,便於更改內存空間使用大小:
#define Linethreshold 5000
#define Charthreshold 50000
#define Wordthreshold 20000
- 設計了異常處理函數,便於在需要檢驗之處加入檢驗功能:
- 設計了類型轉換函數,本統計器多處涉及類型轉換,簡化了實現:
int DetectFileOpen(ifstream &infile);
int DetectOutfileOpen(ofstream &outfile);
string Conventor(int src);
核心功能詞頻統計器流程
- 打開文件
- 異常檢測
- 文件流按行讀取到字符串數組
- 特殊符號處理:處理非字母和數字字符。
- 大寫字母處理:使用函數將大寫字母處理為小寫。
- 行數組單詞化:提取單詞。
- 單詞篩選:將單詞篩選進一個字符串數組。
- 構造紅黑樹:將單詞和頻數構成鍵值對,
- 提取鍵值對至結構體數組:將鍵值對提取到自設計的結構圖數組。
- 穩定排序:對數組進行穩定排序,保持字典序。
- 重構字符流:將排序后的前十位輸出到字符流中。
string Counter::WordFreq()
- 特殊符號處理
for (int i = 0; i<line; i++)//特殊符號處理
{
int j = 0;
while (str[i][j] != '\0')
{
if (ispunct(str[i][j]))str[i][j] = ' ';//特殊符號處理為空格
else
{
str[i][j] = tolower(str[i][j]);//化為小寫
}
j++;
}
}
- 單詞提取
for (int i = 0; i<line; i++)//將空格處理后的文檔轉化為單詞
{
if (str[i]!="\0") {
istringstream stream(str[i]);
while (stream)stream >> str1[j];
j++;
}
}
- 單詞篩選
for (int i = 0; i<j; i++)//單詞篩選
{
isword = true;
for (k = 0; k<4; k++)//除去數字開頭
{
if (str1[i][0] == '\0')
{
isword = false;
break;
}
if (str1[i][k] == '\0')break;
else if (!isalpha(str1[i][k])) {
isword = false;
break;
}
}
if (!isword) {
str1[i] = '\0';
}
}
- 構造紅黑樹
map<string, int> mymap;
map<string, int>::iterator it;
for (int i = 0; i<j ; i++)
{
//查找 是否有key 有的話 value++
//否則加入這個key
it = mymap.find(str1[i]);
if (it == mymap.end())
{
mymap.insert(map<string, int> ::value_type(str1[i], 1));
}
else
{
mymap[str1[i]]++;
}
}
it = mymap.begin();
string temps = "\0";
stringstream ss;
int i = 0;
WF a[100];
for (i = 0; it != mymap.end(); it++, i++)
{
a[i].key = it->first;
a[i].value = it->second;
}
stable_sort(&a[0], &a[i+1], cmp);
for (j = 0; j<i; j++)
{
ss.clear();
temps = "\0";
str[j] = "\0";
ss << a[j].value;
ss >> temps;
str[j] = "<" + a[j].key + "" + ">: " + temps;
}
- 重構字符流
for (i = 0; str[i] != "\0"; i++)
{
if (i >= 10)break;
if(str[i][0]=='<')
result += str[i] + "\n";
else break;
}
//cout << result;
infile.close();
return result;
}
效果
- 對一段論文的摘要進行測試:
- 效果如下
單元測試
- 單元測試應該在最低的功能/參數上驗證程序的正確性。
- 單元測試必須由最熟悉代碼的人(程序的作者)來寫。
- 單元測試過后,機器狀態保持不變。
- 單元測試要快(一個測試運行時間是幾秒鍾,而不是幾分鍾)。
- 單元測試應該產生可重復、一致的結果。
- 單元測試應該覆蓋所有代碼路徑,包括錯誤處理路徑,為了保證單元測試的代碼覆蓋率,單元測試必須測試公開的和私有的函數/方法。
基於以上要求,設計了以下單元測試:
序號 | 測試用例 | 測試對象 | 測試結果 |
---|---|---|---|
1 | 有空白字符的行 | LineCount() | 通過 |
2 | 存在各種樣式的字符 | CharCount() | 通過 |
3 | 數字和單詞正確判斷 | WordCount() | 少算一個,更改后通過 |
4 | 處理特殊字符 | WordCount() | 通過 |
5 | 處理大寫字母 | WordFreq() | 通過 |
6 | 單詞種類超過十個 | WordFreq() | 通過 |
7 | 只有字母 | WordCount() | 迭代器崩潰,更改后通過 |
8 | 單詞頻率是否正確排序 | WordFreq() | 符合字典序 |
9 | 無空行 | LineCount() | 通過 |
10 | 空白文本 | LineCount() | 輸出為0,通過 |
- 測試覆蓋率,可以看出,覆蓋率極高,分析后主要未覆蓋部分為異常檢測函數,損失部分覆蓋率。
以下列出序號1的單元測試代碼:
namespace UnitTest1
{
TEST_CLASS(UnitTest1)
{
public:
TEST_METHOD(TestMethod1)
{
// TODO: 在此輸入測試代碼
Counter C("F:\\軟工\\WordCount\\TEST\\1.txt", "F:\\軟工\\WordCount\\TEST\\result.txt");
int re = C.LineCount();
Assert::AreEqual(re, 2);
}
};
}
性能分析
性能分析圖
- 可以看出,紅框內的詞頻統計功能,占用了最多的CPU資源:
問題發現
- 從以上兩圖可以看出,在申請內存空間和返回字符串時,性能開銷最大。所以考慮從內存分配入手,優化性能。
解決方案
法一
- 為了更加靈活高效申請內存空間,設計了宏。
- 可以方便定義內存空間使用,並且節省CPU消耗。
- 修改時,只需要修改宏即可。
#define Linethreshold 5000
#define Charthreshold 50000
#define Wordthreshold 20000
法二
- 當數據量非常大的時候,內存將成為性能瓶頸,提出基於sketch在大數據下的詞頻統計設計,利用算法Count-min sketch解決內存消耗過大的問題。(具體見下文)
異常處理
- 設計了三個應用場景的異常處理,包括:
- 讀文件未正常開啟:
int DetectFileOpen(ifstream &infile)
{
if (!infile.is_open())
{
cout << "Cannot open the file, please input right filename!";
system("pause");
return 0;
}
}
- 寫文件未能正常開啟:
int DetectOutfileOpen(ofstream &outfile)
{
if (outfile.is_open())
{
cout << "Cannot open the file!";
system("pause");
return 0;
}
}
- 輸入錯誤的文件名:
if (argc != 2)
{
cout << "Uncorrect parameters, Please attach .exe and .txt file in order.";
system("pause");
}
- 文件名錯誤時,報錯,並提供解決方案:
PSP表格記錄
PSP2.1 | header 2 | 預估耗時(分鍾) | 實際耗時(分鍾) |
---|---|---|---|
Planning | 計划 | 35 | 30 |
· Estimate | ·估計這個任務需要多少時間 | 15 | 5 |
Development | 開發 | 645 | 1220 |
· Analysis | 需求分析(包括學習新技術) | 40 | 50 |
· Design Spec | · 生成設計文檔 | 40 | 60 |
· Design Review | · 設計復審 | 10 | 10 |
· Coding Standard | · 代碼規范 (為目前的開發制定合適的規范) | 5 | 10 |
· Design | · 具體設計 | 60 | 120 |
· Coding | · 具體編碼 | 240 | 600 |
· Code Review | · 代碼復審 | 10 | 10 |
· Test | ·測試(自我測試,修改代碼,提交修改) | 240 | 360 |
Reporting | 報告 | 245 | 145 |
· Test Repor | · 測試報告 | 240 | 120 |
· Size Measurement | · 計算工作量 | 5 | 5 |
· Postmortem & Process Improvement Plan | · 事后總結, 並提出過程改進計划 | 20 | 20 |
|合計||935|1400
感想
- 軟工作業的量,真是超乎了我的想象,從PSP表中可以看出,我的預估嚴重不准。而且表中的記錄只可能小於實際值,還不包括寫博客的時間。這次統計器的實現,不算難,但也走了不少彎路,例如需求分析沒有做到位,設計的大局觀不足,導致后期代碼反復迭代差錯,浪費大量時間。其中從《構建之法》中,學到了單元測試要求,很好地幫我解決了設計問題,讓我設計出的單元測試更加合理。最有意思的是單元測試還有強大的性能分析工具,在單元測試后,我查出了許多代碼中的不足,並加以修改。在性能分析后,我可以准確找到我設計中性能的不足和導致不足相應的代碼段,進行優化,應用於之后的程序設計及框架設計,將是一大利器。本次也是代碼規范化的一大訓練,可以體會一些,在實際開發中的方法和思想,回想到之前實際大型開源項目代碼的閱讀,對其中代碼的規范化、接口的設計體會便更加深刻了。在設計過程中,也偶有靈感扇動,也一並記錄下來。
基於sketch在大數據下的詞頻統計設計
引言
- 在海量數據的下,要對數據中的單詞的詞頻進行統計分析,需要對數據存儲、處理,並且維護大量的計數器,將消耗極大資源空間。在本設計中,需要對單詞進行分析、處理、穩定排序,最后得出精確的統計結果,而在大數據下,消耗的時間和內存資源是不可接受的。在數據足夠多的情況下,要了解單詞出現的頻率和頻率排序,其實沒有必要了解詞頻的精確值,只需提供單詞出現的估計值和排序即可。對此,通過在網絡測量中相關文獻的閱讀,提出基於sketch在大數據下詞頻統計,以達到**節省資源*的目的。
背景
- 在網絡測量中,常使用基於sketch的方法,在高速條件下對網絡流的數據進行估計,達到節約資源的目的。在網絡中存在各種各樣的包,若按精確的分類方法,對每一種包都分配一個計數器儲存,雖然測量准確,但存放計數器的空間開銷會非常大。故使用哈希的方法,根據哈希值的范圍確定的所需的存儲空間,各種包根據哈希值再次歸類,可以大大減少存儲空間*。這樣使用哈希來估計流的方法稱為Sketch-based方法。
- 相似的,在大數據下,如果對所有單詞都進行准確存儲統計,不僅沒有必要,而且占用大量內存資源。對此,提出基於sketch在大數據下詞頻統計,在達到需求的條件下,達到節省資源目的。
解決方案
- 使用哈希的方法會產生沖突,多個種類的單詞哈希到同一個桶內,那么這個桶的計數值就會偏大,為了減少誤差,可以使用count-min sketch*。
- Count-min sketch:設置多個哈希函數,開辟一個二維地址空間,包經過不同哈希函數的處理,得到對應的哈希值,而這個哈希值就是sketch(概要)。這些哈希值可能產生沖突,多個種類單詞可能有相同的哈希值,根據哈希值來確定單詞出現的次數則會偏大,所以設置多個哈希函數,取最小的哈希值,則最接近實際包數據。
- 得到每個種類包的估計值后,對sketch進行排序,取前十種類的包,即可得到詞頻統計結果。
總結
- Sketch是使用哈希來進行估計網絡流的一種測量方法,可以減少存儲開銷,相似地,應用於大數據下的詞頻統計,可以減少內存開銷。
- 可以使用Count-Min Sketch對文本數據進行處理,多個哈希函數的最小哈希值作為詞頻的估計,實現簡單,空間開銷較少,
參考文獻:
- SketchVisor: Robust Network Measurement for Software Packet Processing
- An improved data stream summary: the count-min sketch and its applications
- https://www.cnblogs.com/fxjwind/p/3289221.html
- https://blog.csdn.net/lzuacm/article/details/52691450
- http://www.runoob.com/cplusplus/cpp-files-streams.html
- https://baike.baidu.com/item/tolower/6389989?fr=aladdin
- https://baike.baidu.com/item/ctype.h/8497240?fr=aladdin
- https://blog.csdn.net/sophia1224/article/details/53054698
- https://bbs.csdn.net/topics/392186748
- http://www.runoob.com/cplusplus/cpp-stl-tutorial.html
- https://blog.csdn.net/ajianyingxiaoqinghan/article/details/78540736
- https://blog.csdn.net/shinetzh/article/details/65660128
- https://blog.csdn.net/beyongwang/article/details/53074704
- https://blog.csdn.net/qq_31828515/article/details/52056779
- http://www.cnblogs.com/xinz/archive/2011/11/20/2255830.html
- https://www.cnblogs.com/SivilTaram/p/software_pretraining_cpp.html
- https://www.cnblogs.com/nullllun/p/8214599.html
- https://www.cnblogs.com/wuchanming/p/4444961.html
- http://blog.sina.com.cn/s/blog_667cddfc0100wet3.html