來到實驗室正好有一個月了,趁著端午假期稍微輕鬆一些,在大改程式體系之前,想將自己在這30天中工作之一Markov回顧一下,將從真實的寫程式中學習到的知識、思想記錄下來。希望能和大家積極討論!
本文會以用C#實現Markov Model為主線,分享自己的感悟。
一、簡介Markov
Markov是一種概率模型,最簡單的理解是通過大量資料的學習後,可以通過個數為n的詞推斷出第n+1個詞的出現可能。能理解Markov整體思想就可以了。
所以通常將Markov Model的實現分為兩個部分:
1、Markov Model資料學習部分
2、根據Markov Model生成部分
二、Markov資料學習
很多人說,當Markov的階數(Order)變高的時候,如order=5,資料學習部分根本跑不動,為什麼?因為Markov要求的是全排列。舉個例子,如果學習資料中有5000種詞,階數為5,那麼我們有5000的5次方種全排列可能。在儲存過程中,因為資料量過大,對記憶體的一種挑戰;在搜尋並累積的過程中,如果採用不適當的資料結構,將會消耗大量,超大量時間在搜尋中。而這點也就是程式跑不動的原因。
功能的需求就只是個需求,怎麼寫程式根本是另一回事。很多程式不會按那個需求那麼直接的實現,比如說我們現在要全排列,那我們就把全排列的結果全部都存在計算機中嗎?我們做不到。我們得通過其他方法來實現這個需求,這就可能會稍微拐點彎啦,但能否靈巧地解決程式設計難點,看的就是不同程式設計師的能力高低了。
統計可以得到,全排列中有大量組合是沒有值的,會是個稀疏矩陣。所以我只將學習資料中出現的片語合記錄下來,這個資料量將遠遠小於應有的全排列,記憶體將會有足夠的空間存放。在這樣的前提下,我第一次使用的List做資料結構來儲存,發現程式依然跑的很慢很慢,30mins才跑了50W資料;後來靈機一動用了Dictionary(hash)做資料結構,2mins就跑了300W資料……這真是天壤之別啊!!!這就說明資料結構真是太重要了!!!千萬不要小看資料結構啊!!!一定要重視!!!以前學的時候只知道雜湊查詢效率是O(1),但從沒有這麼震撼的感觸。
話又說回來,為什麼Markov要求是全排列,像我這樣只把出現的學習一樣不可以嗎?生成的時候只生成我們學到的內容不可以嗎?不可以!因為Markov需要平滑,因為Markov需要考慮所有可能的情況,雖然沒學習到的內容出現可能會非常低罷了!
既然有這樣的需求,那我們依然需要稀疏矩陣中稀疏部分的值呀,所以我們在生成部分直接求唄,很快的。這也說明了一句古話,“逃得了初一,逃不過十五”,是在一開始我們就將所有稀疏部分的值求好呢,還是需要的時候臨時再求呢?這有點像ECC和RSA,一個加密快一個解密快,那就合理使用嘛!伺服器若需要做大量解密工作的話,就用RSA做公鑰加密演算法唄,因為解密快,就會少用點資源。要靈活地看程式真正使用場景的需求,是動態還是靜態?是犧牲空間還是犧牲時間?哪有誰就比誰好呢?
這算是Markov學習資料過程中,最大的感觸了,其他的就不說了。
三、根據Markov生成資料
這部分給我帶來的最大的感觸就是,程式碼要一定要和執行環境掛鉤!不要想當然!一切要以程式真正實現為主!必須考慮計算機記憶體資源的合理分配!!!(這點大家可能都沒有遇到過,就不和大家解釋了)
為了能讓程式正常執行,我們必須合理使用記憶體,記憶體的佔用、記憶體的釋放、一次使用多大的記憶體、用什麼資料結構存放?我們都要去考慮!
長度太長的時候,記憶體經常到4個G,程式就報錯了。在沒有找到簡單解決方案後,自己寫了一個記憶體硬碟動態交換資料並支援斷續執行的程式碼,而這個程式碼也是我非常想逼逼的哈哈。這個環節我直接上程式碼了,由於是程式碼片段,所以不是很好理解= =。
//通過迴圈來生成長度為0-maxlength的資料 for (int i = 0; i < maxlength; i++) { //fragnum數決定著長度相同資料 “分存” 在幾個txt中 //隨著長度的增大資料數量會非常非常大 //故我們通過儲存在多個txt中,可以保證txt檔案的正常生成,可以做到 “分段” 跑資料! int fragnum = 0; for (int j = 0; ; j++) { //讀取Length為i,Part為j的檔案,即長度為i的第j個儲存資料的txt檔案 string inpath = FileDirectory + "Markov_Length_" + i.ToString() + "_Part_" + j.ToString() + ".txt"; //ListofBase用來臨時存放從上面檔案中讀取到的length-1的pre資料 //在迴圈內初始化List,可控制記憶體開銷,保證可持續發展 List<string> ListofBase = new List<string>(); Console.WriteLine(i + " " + j); try { //將inpath檔案中的basic資料讀進ListofBase StreamReader sr = new StreamReader(inpath, Encoding.Default); String line; while ((line = sr.ReadLine()) != null) { Console.WriteLine(line); ListofBase.Add(line); } Console.WriteLine("------------------------------"); //準備將生成的資料儲存在長度為length為i+1的資料txt中 //part值由fragnum決定,fragnum在length層面上初始化為0,隨後的值將一直伴隨 string outpath = FileDirectory + "Markov_Length_" + (i + 1).ToString() + "_Part_" + fragnum.ToString() + ".txt"; FileStream fs = new FileStream(outpath, FileMode.Create); StreamWriter sw = new StreamWriter(fs); //modcount的大小決定從ListofBase中抽取多少個用來生成資料 //同一批資料生成的新資料存放在一個part中,用這個來具體實現對同一長度資料的 “碎片化” 儲存 int modcount = 0; //遍歷整個ListofBase foreach (var element in ListofBase) { //將modcount控制在(0,499999)之間 modcount = (modcount + 1) % 500000; //若modcount等於499999,則說明foreach迴圈已經執行了500000次 //通過更改輸出txt檔名來分開儲存 if (modcount == 499999) { //檔案後續處理 sw.Flush(); sw.Close(); fs.Close(); //修改檔名,++fragnum outpath = FileDirectory + "Markov_Length_" + (i + 1).ToString() + "_Part_" + (++fragnum).ToString() + ".txt"; fs = new FileStream(outpath, FileMode.Create); sw = new StreamWriter(fs); } //temp用來儲存ListofBase生成的資料 List<string> temp = new List<string>(); //呼叫ExtendPassword方法,element生成的資料存放在temp中 temp = ExtendPassword(element); //並將temp中資料存放在檔案中 foreach (var every in temp) { sw.WriteLine(every); } } //檔案後續處理 sw.Flush(); sw.Close(); fs.Close(); } //如果沒有讀到(i,j)檔案,那麼判斷Length為i的第j+1個部分是不存在的 //跳出迴圈,讀Length為i+1,Part為0的txt檔案 catch { break; } } }
現在想想,那些安裝程式、補丁程式、下載程式,關閉後還可以找到位置繼續執行的功能是不是就是這段程式碼的特殊版?不過我也不知道那些程式是怎麼寫的,有機會能接觸到就好啦。
不管是因為你自己要寫,或是你的上級、你的客戶下派了任務,還是出於其他原因,我們的重點都應該放在如何用計算機思維解決現實問題,應先了解計算機的功能,是序列計算還是平行計算?準備用過程性的程式設計思想還是物件導向的程式設計思想?既然要寫程式,就要用你用的程式語言的思想、你的計算機的邏輯去解決這個問題。能將現實問題轉換成巧妙的計算機程式這個能力,不是所有程式設計師都有的,一定要好好鍛鍊,路還很長,加油吧!
LIAN
大三下 2017/5/28