做者:LogMnode
本文原載於 https://segmentfault.com/u/logm/articles ,不容許轉載~git
comoody 大佬的源碼:https://github.com/comoody/TextRank.gitgithub
本文對應的源碼版本:Commits on Oct 23, 2018, 9736be10593b99adc1ea614c5d83f1bfeca17b94
web
lostfish 大佬的源碼:https://github.com/lostfish/textrank.git算法
本文對應的源碼版本:Commits on Sep 29, 2016, e89084374ae0e08490c9cc0fa79f8ae4bb10ad5b
segmentfault
TextRank 論文地址:https://www.aclweb.org/anthology/W04-3252dom
C++ 版本的 TextRank 尚未發現點贊超級多的代碼,這裏我找了兩個不一樣的實現來分析。ide
在上一篇博客:TextRank Python 版本,咱們知道,看 TextRank 的源碼有兩個重點須要看,重點1:句子與句子的類似度是如何計算的;重點2:PageRank的實現。函數
這裏,考慮到篇幅,我直接給出對應的函數所在的位置。優化
先看看大佬是怎麼計算句子與句子之間的類似度的。配合我寫的那幾行中文註釋,應該很容易看懂。大體和論文裏的公式是一致的,可是分母和論文的公式不同。具體爲何用這個分母,我就不得而知了。
// 文件:src/TextRanker.cpp // 行數:153 float TextRanker::getSimilarity(std::string a, std::string b) const { // no two equivalent sentences should ever be compared, but this logic is included just in case if(a == b) return 0.f; // 大小寫轉換 -> 分詞 -> 把詞放到 set 裏 std::transform(a.begin(), a.end(), a.begin(), ::tolower); std::vector<std::string> aWords = stringSplit(a, ' '); std::set<std::string> aWordSet; for(auto word : aWords) aWordSet.insert(word); std::transform(b.begin(), b.end(), b.begin(), ::tolower); std::vector<std::string> bWords = stringSplit(b, ' '); std::set<std::string> bWordSet; for(auto word : bWords) bWordSet.insert(word); // 求兩個 set 的交集,至關於兩個句子共有的詞語 std::vector<std::string> commonWords; std::set_intersection ( aWordSet.begin(), aWordSet.end(), bWordSet.begin(), bWordSet.end(), std::back_inserter(commonWords) ); // 這個分母和論文的公式不同,論文裏是帶 log 的 float avgWords = (aWords.size() + bWords.size()) / 2; return commonWords.size() / avgWords; }
而後,咱們來看看 PageRank 的實現。哦!看來 comoody 大佬沒有使用 PageRank 的公式,而是在圖中隨機遊走,某個點遊走通過的次數越多,這個點得分越高。
講道理,這種隨機遊走的效果和用 PageRank 的公式求得的效果是差很少的。瞭解 PageRank 的同窗應該清楚,PageRank 這個算法的本質,就是模擬用戶在網頁間的隨機遊走。
// 文件:src/TextRanker.cpp // 行數:77 // does a single walk that visits sentences around the graph according to probabilities defined in the similarity matrix // after each iteration inside the walk, there is a 1 - kNewWalkThreshold probability that the walk will end and this // method will return // during the walk, the visits map is updated accoridingly void TextRanker::doSentenceGraphWalk ( const FloatMatrix& similarityMatrix, const std::vector<std::string>& sentences, std::map<std::string, int>& visits ) const { const int kDim = sentences.size(); bool continueWalk = true; // start walk at a random node in the sentence graph int curSentenceIndex = rand() % kDim; while (continueWalk) { // visit the curSentence std::string curSentence = sentences[curSentenceIndex]; visits[curSentence]++; // the row of the similarity matrix corresponding to the current sentence represents the probabilites // of transferring to all ofther sentences from the current sentence std::vector<float> probabilites; std::copy_if ( similarityMatrix[curSentenceIndex].begin(), similarityMatrix[curSentenceIndex].end(), std::back_inserter(probabilites), [](float f) { return f != 0.f; } ); if (probabilites.size() == 0) break; // no possible neighbor to visit // normalize probabilites float sum = std::accumulate(probabilites.begin(), probabilites.end(), 0.f); std::transform(probabilites.begin(), probabilites.end(), probabilites.begin(), [sum](float probability) { return probability / sum; }); // stack probabilites std::vector<float> probabilityDistribution; for(std::vector<float>::iterator j = probabilites.begin(); j < probabilites.end(); j++) probabilityDistribution.push_back(std::accumulate(probabilites.begin(), j+1, 0.f)); // get a random number betweeon 0 and 1 float selector = (rand() % 1000) / 1000.f; int selectedIndex = 0; while(probabilityDistribution[selectedIndex] <= selector) selectedIndex++; // the selected index maps to a probability from a distribution with all 0 entries removed // iterate through the original probabilites to map back the selected index to its true index from the list int trueIndex = 0; int nonZeroCount = 0; do { if(similarityMatrix[curSentenceIndex][trueIndex] != 0) nonZeroCount++; } while((nonZeroCount < selectedIndex + 1) && ++trueIndex); // update the curSentence index so that it can be visited in the next iter of the current walk curSentenceIndex = trueIndex; // randomly test for the end of a walk, if the random number is above kNkNewWalkThreshold, start a new walk float newWalkSelector = (rand() % 1000) / 1000.f; if(newWalkSelector > kNewWalkThreshold) continueWalk = false; } }
咱們再來看看 lostfish 大佬的源碼。一樣的,先看看是如何計算句子與句子之間類似度的。
emmm. 也是和論文公式有些不同:統計兩個句子共同出現的詞語時,沒有對每一個句子先作一次去重。
// 文件:src/sentence_rank.cpp // 行數:47 double SentenceRank::CalcDist(const vector<string> &token_vec1, const vector<string> &token_vec2) { size_t both_num = 0; size_t n1 = token_vec1.size(); size_t n2 = token_vec2.size(); if (n1 < 2 || n2 < 2) return 0; // 統計兩句話共同出現的詞語數 for (size_t i = 0; i < n1; ++i) { const string &token = token_vec1[i]; for(size_t j = 0; j < n2; ++j) { if (token == token_vec2[j]) { both_num++; break; } } } double dist = both_num / ( log(n1) + log(n2) ); return dist; }
再來看看 PageRank 的實現吧。和論文裏給出的公式同樣。我寫了一些註釋,應該仍是容易看懂的。
void SentenceRank::CalcSentenceScore(map<size_t, double> &score_map) { score_map.clear(); // initialize size_t n = m_sentence_vec.size(); for (size_t id = 0; id < n; ++id) score_map.insert(make_pair(id, 1.0)); // iterate for (size_t i = 0; i < m_max_iter_num; ++i) { double max_delta = 0; map<size_t, double> new_score_map; // 這裏記錄了每一個句子的得分 for (size_t id1 = 0; id1 < n; ++id1) { double new_score = 1 - m_d; // 當前迭代輪次中,每一個句子的得分,這裏先把論文公式的左邊部分加上 // 計算論文公式的右邊部分 double sum_weight = 0.0; for (size_t id2 = 0; id2 < n; ++id2) { if (id1 == id2 || m_out_sum_map[id2] < 1e-6) continue; double weight = GetWeight(id2, id1); // 節點2 -> 節點1 的權重 sum_weight += weight/m_out_sum_map.at(id2)*score_map[id2]; } new_score += m_d * sum_weight; // 把論文公式的右邊部分加上 new_score_map.insert(make_pair(id1, new_score)); // 監測每兩輪迭代之間score的差值,差值足夠小就不用繼續迭代 double delta = fabs(new_score - score_map[id1]); max_delta = max(max_delta, delta); } score_map = new_score_map; if (max_delta < m_least_delta) { break; } } }
兩位大佬實現的 TextRank C++ 版本,仍是有必定的改進空間:
comoody 大佬使用的節點隨機遊走
的方式可行,用在我的項目上是能夠的;可是工程上來講,rand()
函數致使每次結果都是隨機的,沒法復現,是比較致命的。
lostfish 大佬的 PageRank 部分,和論文是一致的,可是運算過程還有一些優化空間。
另外,兩位大佬計算句子與句子之間類似度的函數與論文有點不一樣,固然這個計算類似度的方法不是定死的,是能夠本身創做的,好比用word2vec等等。
我打算本身也實現一個 C++ 的版本。