軟工之詞頻統計器及基於sketch在大數據下的詞頻統計設計


Github項目地址

摘要

  • 本詞頻統計器包括行數統計、字符數統計、單詞數統計、詞頻統計功能。基於紅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對文本數據進行處理,多個哈希函數的最小哈希值作為詞頻的估計,實現簡單,空間開銷較少,

參考文獻:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM