1. 程式人生 > >拒絕調包俠,不需要高階演算法和資料結構技巧

拒絕調包俠,不需要高階演算法和資料結構技巧

前言

大多數工科學生或者剛剛入門近年來比較火的“人工智慧”相關演算法的同學,在選擇語言的時候,都會選擇MATLAB、Python、R等等這些高階語言,對自己所學的演算法進行實現和除錯。這些高階語言中,包含了實現複雜演算法的基礎數學演算法、基本統計演算法、基礎資料結構的實現,比如均值(mean)、方差(std)、向量(Vector)等等。藉助於這些高階語言的Built-in Function,我們的一些想法會在較短時間內實現。並且,解釋型的程式設計方式,也方便了大家去除錯程式碼、執行程式碼。但是使用這些語言和它們的函式,會帶來一些效率降低的問題。大多數人首先想到的解決方式,可能是去選擇底層語言來實現演算法,比如C/C++, JAVA等。但其實,我們在運用高階語言進行編碼時,也有大量需要進行優化的內容。我們應當從“調包”和利用Built-in Function的習慣中解放出來。

問題

最近在用C++實現CUSUM時,我參考該演算法的MATLAB的程式碼直接翻譯成了C++的程式碼。本想到演算法應該會非常迅速,但是它的表現的確讓我大失所望。在優化掉輸出(ostream)帶來的時間損耗後,演算法的速度依然沒有達到期望的要求。於是,觀察程式碼細節時發現,在迭代過程中,我們對一段隨迭代次數其長度線性增長的陣列片段(slice)求取均值、方差時,使用了mean()std()函式。

那麼,每一次新的資料新增進一個數組(Array或者Vector),就去呼叫上述這類函式,真的有必要嘛?我們是不是引入了太多的重複計算?

解決方案

我們先來看一下CUSUM演算法的MATLAB實現的一個片段:

%% Global Loop
% w = waitbar(0,'Calculating Cumulative Sums, please wait...');
while k < length(x)
  % current sample
  k = k+1; 
  % mean and variance estimation (from initial to current sample)
  m(k) = mean(x(k0:k));
  v(k) = var(x(k0:k));
複製程式碼

上述程式碼片段裡,在while迴圈中,我們呼叫了length(x)次函式meanstd,這其中包含了大量的重複計算,帶來了大量的計算開銷(計算均值,肯定有大量的加和操作)。假設我們已經計算了實數陣列X_n=[x_1, x_2, \ldots, x_n]

的均值,記為\bar{X}_{n}。當一個新的資料x_{n+1}被取樣到,並加入X_n中,形成X_{n+1}=[x_1, x_2, \ldots, x_n, x_{n+1}]。在計算均值\bar{X}_{n+1}時,可以利用以下公式:

\begin{equation}
\bar{X}_{n+1}=\bar{X}_{n}+\frac{x_{n+1} - \bar{X}_n}{n+1} \tag{1}
\end{equation}

同理,我們可以得到計算新方差的公式:

\begin{equation}
\mathrm{Var}(X_{n+1})=\mathrm{Var}(X_n)+(x_{n+1} - \bar{X}_n)(x_{n+1} - \bar{X}_{n+1}) \tag{2}
\end{equation}

新的編碼方案就變成:

// Global Loop
while (k < len - 1) {
  k++;
  prev_delta = X[k] - m[k - 1];     // online average
  m.emplace_back(m[k - 1] + prev_delta / (k - k0 + 1));
  post_delta = X[k] - m[k];         // online s.t.d
  v.emplace_back(std::sqrt(v[k - 1]*v[k - 1] + prev_delta*post_delta));
複製程式碼

這樣計算速度就快很多了。

思考

如上所述的這種從左至右計算統計量的過程,在很多演算法中出現過,比如著名的決策樹演算法。決策樹在某個節點確定分裂特徵和分裂點的計算過程中,是如何進行計算統計量的呢?著名的決策樹開源框架,如XGBoost中,又是如何編碼,對樣本梯度進行統計的呢?這些留給大家去思考和發現。