1. 程式人生 > >Linux 桌面玩家指南:14. 數值計算和符號計算

Linux 桌面玩家指南:14. 數值計算和符號計算

特別說明:要在我的隨筆後寫評論的小夥伴們請注意了,我的部落格開啟了 MathJax 數學公式支援,MathJax 使用$標記數學公式的開始和結束。如果某條評論中出現了兩個$,MathJax 會將兩個$之間的內容按照數學公式進行排版,從而導致評論區格式混亂。如果大家的評論中用到了$,但是又不是為了使用數學公式,就請使用\$轉義一下,謝謝。

想從頭閱讀該系列嗎?下面是傳送門:

前言

不知道經常需要做科學計算的朋友們有沒有這樣的好奇:在 Linux 系統下使用什麼工具呢?說到科學計算,首先想到的肯定是 Matlab,如果再說到符號計算,那就非 Mathematica 不可了。可惜,以上兩款軟體都是商業軟體。雖然破解版滿天飛,但是這不符合開源世界的邏輯。在 Linux 系統下,也有非常不錯的科學計算工具,包括符號計算的也有。下面我就來隆重向大家推薦幾款。

Octave

這款軟體是 GNU 出品,在 GNU 的線上文件網站上可以下載到它的完整的幫助文件,我喜歡 pdf 版,可以一口氣從頭讀到尾,很舒服。從語法角度講,Octave 和 matlib 完全相容。下面是其執行效果圖:

它也有 GUI 介面的包裝,那就是 QtOctave,如下圖:

在 Ubuntu 下該軟體的安裝非常簡單,使用如下命令即可:

sudo apt-get install octave
sudo apt-get install qtoctave

Maxima

數值計算使用 Octave,那麼符號計算就少不了 Maxima 了。由於符號計算中,數學公式的顯示也是非常重要的一環,所以我喜歡用它的 GUI 封裝 wxMaxima,該軟體使用如下命令安裝:

sudo apt-get install wxmaxima

下面是它的執行效果圖:

有了 GUI 的封裝,我們的學習曲線都要簡單很多,因為它的功能都在它的選單欄中體現出來了。Maxima 也自帶完善的文件,如下圖:

符號運算不僅能對各種數學公式進行執行、變形、化簡,也可以直接對函式作圖,如下圖:

但是以上介紹的都不是重點。下面的工具才是我這篇隨筆的重量級嘉賓。它就是:

IPython-Notebook

使用 python 進行科學計算最近幾年很火,主要得益於 python 語言和Numpy、SciPy、pandas、matplotlib、SymPy 等庫。另外一個大殺器就是 ipython-notebook,它可以說是提供了在數學方面讀寫算加畫圖一條龍的服務了。Ubuntu 對 Python 的支援真心不錯,先使用下面的命令將以上庫全部安裝:

sudo apt-get install pandas
sudo apt-get install sympy

不是說全部安裝嗎?怎麼只有兩個命令?因為安裝 pandas 時 NumPy、SciPy、matplotlib 都作為依賴項自動安裝了,只有符號計算庫 SymPy 需要另外安裝。然後,使用如下命令安裝 ipython-notebook:

sudo apt-get install ipython-notebook

同理,IPython 也作為依賴項自動安裝了。然後使用如下命令啟動 ipython-notebook:

ipython notebook --pylab=inline

然後 ipython-notebook 就在瀏覽器中啟動了。不錯,這是一個 B/S 應用,我們啟動它時會在我們的機器上建立一個簡單的伺服器,然後用瀏覽器訪問這個伺服器就可以使用 ipython-notebook 了,遠端訪問也行。下面是執行效果:

新建一個筆記後,就會給我們一個輸入程式碼的提示。ipython-notebook 中的內容是由一個一個的輸入區域組成的,稱為 Cell。每一個 Cell 除了可以輸入程式碼,還可以輸入 Markdown、rawtext、heading,如上圖中的選項所示。下面是輸入 Markdown 的效果圖:

按 Shift+Enter 即可結束該區域的輸入,並執行和顯示效果。如果以後要重新編輯裡面的內容,雙擊該區域即可。Markdown 區域也是支援 MathJax 的哦,如下圖:

下面看看使用 NumPy 來進行數值計算和繪圖的效果:

使用 pandas 進行資料分析並繪圖的效果:

最後,看看使用 SymPy 進行符號計算的效果:

從上圖可以看到,SymPy 的 latex 函式可以把輸出的數學公式轉換成 LaTeX 程式碼,不過該程式碼有點問題,它裡面每個反斜槓都變成了雙反斜槓。將該 LaTeX 程式碼複製、修改後,輸入 Markdown 區域就可以看到完美的數學公式了。

我們在 IPython-Notebook 中建立的筆記是可以儲存的,而且儲存的是純文字的 JSON 格式,所以可以非常方便地把它放到 GitHub 進行分享。從 IPython-Notebook 的幫助選單可以很方便地導航到 NumPy、SciPy、matplotlib、pandas、SymPy 的幫助文件。在 matplotlib 的官網中,還專門有一個 gallary 頁面,裡面有各種圖表的縮圖和程式碼,對我們的學習真的是很有幫助哦。

適合數值計算的語言需要具備什麼樣的特色

前面搞了不少工具論,下面再來討論一下程式語言。這段時間,我繼續徜徉在數值計算的世界。為了廣泛學習數值計算方面的知識,我抽空看了 Python 科學計算和數值分析方面的書,也仔細研讀了 Octave 的使用者手冊,甚至連古老的 Fortran、新興的 R 語言我都去逐一瞭解。對於數值計算的庫,我瞭解了一下 Boost 的 uBLAS,以前也用過 OpenCV,當然,瞭解最多的還是 Python 中的 NumPy、SciPy 和 pandas。今天談的內容是我對適合做數值計算的程式語言的一些看法,主要是一些思路方面的東西,不評論具體語言的優劣。另外,我是想到哪兒寫到哪兒,如果有什麼不對的地方歡迎大家指正。

一、元組和陣列

如果數值計算僅僅只是兩個標量之間的加減乘除,那就不需要我在這裡浪費口舌了。向量啊、矩陣啊、多維陣列啊什麼,才是數值計算真正的主角。所以,適合做數值計算的程式語言必須有一個好的方式表示陣列,特別是多維陣列。哪種方式好呢?是這樣:

int a[m][n][k];

還是這樣:

int a[m,n,k];

看似沒有什麼差別,但是如果你想獲取陣列 a 的形狀呢?比如這樣:

? = a.shape();

或者再更進一步,想改變陣列 a 的形狀呢?比如這樣:

a.reshape(?);

在上面的程式碼中,“?”究竟應該用什麼代替呢?

如果讓我給出答案,我會說:要用元組。很多程式語言中都有元組的概念,比如 Python。元組就是用逗號隔開的幾個值,可以加圓括號,也可以不加。我覺得加上圓括號後可讀性更好。比如 (a,b) 是元組,(3,4,5) 也是元組。如果寫成 [3,4,5] 那就是陣列了,在 Python 中,也稱之為列表。不過 Python 的列表功能比陣列要強大,因為陣列只能儲存同一種資料型別的值,而列表可以儲存任何物件。陣列一般情況下不能動態改變長度,而列表可以。Octave 語言中使用 cell array 這個術語來表示可以儲存不同型別物件的容器。Octave 中的陣列和矩陣是可以動態改變長度的。C 語言的陣列沒有動態改變長度這個功能,而如果使用 C++ 的話,則必須使用 vector<> 模板類。

我認為,一個好的程式語言必須要有“元組”這個一個概念,必須能夠用好大括號、中括號和小括號。在有沒有元組這個問題上,很多語言做得不好,C 語言沒有,C++ 也沒有,Java 沒有,C# 這個有很多新功能的語言也沒有,不要告訴我有 Tuple<> 模板類可以用,那個真的沒有語言內建的元組功能好。在能不能用好大中小括號這個問題上,C 語言就做得不好。你看它不管是初始化陣列,還是初始化 struct,都是用大括號。而 Python 和 JSON 就做得很好嘛,初始化陣列用中括號,初始化物件或字典的時候採用大括號。如果加上小括號表示元組,那就齊活兒了。

數值計算可以針對標量、一維陣列、二維陣列以及n維陣列進行。陣列可以如下組織,如下圖:

元組最大的用途就是可以用來表示陣列的形狀了。比如一維陣列的形狀為 (n,),請注意其中的逗號不能省略。二維陣列的形狀 (m,n),三維陣列的形狀 (m,n,k),依次類推。另外,元組可以用來對陣列中的元素進行索引。比如:

a = [ [1,2,3,4], [5,6,7,8], [9,10,11,12], [13,14,15,16] ];
b = a[2,3,3];

元組還有一個很大的用途,那就是可以讓一個函式返回多個值。C 語言在這個方面是做得比較醜陋的,如果一個函式要返回多個值,只能給這個函式傳指標或者多重指標作為引數,C++ 可以傳引用,C# 更加畫蛇添足,專門有一個out關鍵字用來修飾函式的引數。微軟你真是的,你既然能想到out,你就不能想到元組嗎?常見的例子,比如meshgrid()函式可以同時初始化兩個陣列,peak()函式可以同時初始化三個陣列。你看它們用元組多方便:

(xx, yy) = meshgrid(x, y);
(xx, yy, zz) = peak();

另外,元組還可以這樣用,比如交換兩個變數的值:

(a,b) = (b,a);

二、陣列初始化

在數值計算中,陣列的初始化也是非常重要的一環。如果像 C 語言這樣寫:

int a[100] = {1, 2, 3, 4, ... , 100};

估計很多人是要罵孃的。這樣寫:

for(int i=0; i<100; i++){
    a[i] = i+1;
}

也不優雅。我只是想初始化一個數組而已,怎麼就非得要寫一個迴圈呢?如果是二維陣列呢,就得兩層迴圈,三維陣列就得三層。真的是太鬧心了。

另外,如前所述,我也不喜歡在初始化陣列的時候用大括號。我覺得中括號就是為陣列而生。比如這樣:

a = [1, 2, 3, 4];

這就是一個一維陣列,但是如果這樣寫:

a = [ [1, 2, 3, 4] ];

就是一個行向量。如果寫成這樣:

a = [ [1], [2], [3], [4] ];

那麼這就是一個列向量,如下圖:

當然,上面的示例只有四個數字,這麼寫一寫無可厚非。如果是很多數字呢?或者很多維的陣列呢?這時就必須得用到很多初始化函數了,而且這些初始化函式最好能接受元組作為引數來決定陣列的形狀。比如這樣:

a = xrange( 1, 60, (3,4,5) );  //用1到60的數字初始化一個3*4*5的陣列
b = randn ( (3, 4, 5) ); //用隨機數初始化一個3*4*5的陣列

其它的初始化函式還有 linspace()logspace()ones()zeros()eyes() 等等。這些函式還可以配合 reshape()使用,比如這樣:

c = linspace(0, 2*pi, 60).reshape(3, 4, 5);

在所有的這些初始化中,元組都是重要的組成部分。

三、range 和切片

其實,range 除了可以是一個函式,還可以更省點兒事,像這樣寫:

r = 0:10:2;  //0,2,4,6,8,10
s = 11:0:-3; //11,8,5,2

在某些語言中,也把這個功能叫切片。其實就是:的靈活運用,有標點符號可以用當然不能浪費嘛。使用切片,只需要指定起始值、終止值和步長,就可以獲得一個數字序列。

但是,:最大的用途並不是用來對陣列進行初始化,而是對陣列進行索引。比如,a 是一個三維陣列,可以通過切片來獲取其中的一部分資料。見下面的程式碼:

a = range(1, 60).reshape(3, 4, 5); // a是一個三維陣列
b = a[1, 2:3, 1:4]; // b是一個二維陣列,其值為[ [12, 13, 14, 15], [17, 18, 19, 20]]

切片除了可以指定起始值和終止值外,也可以指定步長。當然,也可以只用一個單獨的:,代表取這一整個軸。關於軸的概念,可以看我前面的圖片。見下面這樣的程式碼:

a = range(1, 60).reshape(3, 4, 5); // a是一個三維陣列
b = a[1, :, :]; // b的值為二維陣列[[1,2,3,4,5], [6,7,8,9,10],  [11,12, 13, 14, 15], [16,17, 18, 19, 20]]

四、不寫迴圈

在對多維陣列進行加減乘除的時候,如果使用傳統的像 C 這樣的語言,則避免不了要寫迴圈。比如要計算兩個多維陣列的加法,不得不寫這樣的程式碼:

m = 10;
n = 20;
k = 30;
a = randn(m, n, k); //形狀為(m, n, k)的三維陣列,初始化為隨機值
b = randn(m, n, k); //形狀為(m, n, k)的三維陣列,初始化為隨機值
for(int i=0; i<m; i++){
    for(int j=0; j<n; j++){
        for(int p=0; p<k; p++){
            c[i, j, p] = a[i, j, p] + b[i, j, p];
        }
    }
}

上面的程式碼當然遠不如下面這樣的程式碼簡潔:

C = A + B;

所以不寫迴圈基本上就成了所有數值計算語言的標準配置。Matlab 和 Octave 是這樣,NumPy 是這樣,R 語言也是這樣。C++ 也在追求這樣,因為 C++ 中有運算子過載的功能,所以可以對矩陣類過載加減乘除運算子。但是 C++ 中運算子的基礎設施有缺陷,比如它沒有乘方運算子(冪運算子),在 Octave 和 NumPy 中,都可以這樣計算 $ x^y $:x**y。但是在 C++ 中,只有使用函式 power(x, y)。不要想 ^ 運算子,它是一個位運算子,所以取冪只有使用 ** 了。另外,多維陣列運算還有特例,比如二維陣列之間加減乘除,既可以是逐元素的加減乘除,也可以是矩陣的加減乘除。向量計算也有特例,既可以是逐元素加減乘除,也可能是向量內積(點乘)。如果正好是長度為 3 的向量,還可以計算叉乘。這些運算子都需要重新定義,所以雖然 C++ 有過載運算子的機制,但是因為這些運算子完全超越了 C++ 的基礎設施,所以 C++ 也沒有辦法寫得很優雅。

不寫迴圈還有一個優點,那就是可以對運算速度進行優化。優化是編譯器或直譯器的責任,寫數值計算程式的人可以完全不用費心。編譯器或直譯器可採取的優化方式有可能是利用 SSE 等多媒體指令集,也可能是發揮多核 CPU 的多執行緒優勢,甚至是使用 GPGPU 計算都有可能。如果使用者非要寫成 C 語言那樣的迴圈,而他又不會內聯彙編或 OpenMP 的話,那麼就談不上什麼運算速度的優化了。

五、廣播

不寫迴圈,直接把兩個多維陣列進行加減乘除當然省事。但是如果兩個陣列的形狀不一樣呢?比如一個二維陣列加一個行向量,或一個二維陣列加一個列向量,甚至是陣列加減乘除一個標量,會出現什麼情況呢?

不用擔心,在面向數值計算的語言中,一般都有“廣播”這樣一個特性。當兩個陣列的形狀不一樣時,形狀比較小的那個往往可以在長度為 1 的維度上進行廣播。如下圖:

六、奇異索引

Fancy indexing,有的書上翻譯成花式索引,但我認為叫奇異索引比較好。它就是指一個低維的陣列,可以使用高維的陣列進行索引,最後得到的結果是一個高維的陣列。如果索引中含有切片,可能會得到一個更高維度的陣列作為結果。

這個概念理解起來比較難。特別是再配合切片使用,更加增加其複雜性。所謂一圖勝千言,先看普通索引的情況:

前面提到,對多維陣列進行索引的時候需要用到元組,元組的長度等同於陣列的維數。對於普通索引而言,元組的各個部分要麼是整數,要麼是切片。而對於奇異索引而言,索引元組的各個組成部分都可能是多維陣列或者切片。如果是多維陣列,則最後得到的陣列的形狀和索引陣列的形狀相同,如果配合切片,則可能得到更高維的陣列。如下圖:

七、函式呼叫

程式語言發展這麼多年,一直在進化,也一直在相互靠攏。對於一個程式語言來說,是應該面向過程還是面向物件?是靜態型別還是動態型別?這些都是值得思考的地方。但是在函式呼叫方面,有一些思想倒是可以學習。

在 C 語言這樣比較古老的語言中,對於函式的引數來說,只有位置引數一種。也就是說,像一個函式傳遞引數的時候,只能正確的引數放到正確的位置,而且引數的個數必須和函式定義的相同。這是最原始的函式呼叫思想。

緊接著,在某些程式語言如 Java、C# 中,有了可選引數這個概念。但是可選引數要放到引數列表的最後面,而且必須提供預設值。當呼叫函式時如果指定了這個引數,則使用呼叫時指定的值,否則使用預設值。

但是我覺得適合數值計算的語言必須還得更進一步,提供關鍵字引數的功能。什麼是關鍵字引數呢?比如對資料進行繪圖的時候,需要指定線型、標籤、標題等各種屬性,可以這樣呼叫函式:

plot(x, y, marker="*", color="r", linestyle="-", title="...", legend="...", xlabel="...", ylabel="...");

每一個引數呼叫的時候都可以指定它的名字,這樣我們就不用去死記各個引數的位置,是不是很方便呢?

八、生態環境

對於一門程式語言而言,生態壞境很重要。在數值計算領域更是如此。因為很多數值計算的庫都是專業的人士寫給專業人士看的,比如物理專業的寫物理領域的演算法,氣象專業的寫氣象專業的演算法,所以不大可能有一個全面的官方的,像 C 或 C++ 這樣一個由 ANSI 定義的庫。

廣泛接受開源社群的貢獻是一個比較好的辦法。Perl 是這樣,Python 也是這樣,新興的 R 語言也是這樣。Perl 有 CPAN,Python 有 PyPI,R 語言也有 CRAN。至於 Matlab,那更是有各種各樣的工具包。

OK,就寫這麼多吧,還有其它的一些什麼特色,我想到後再隨時更新此文。

另外,本文中所有的圖片都是在 Ubuntu 中使用 Inkscape 向量圖軟體繪製而成。

求打賞

我對這次寫的這個系列要求是非常高的:首先內容要有意義、夠充實,資訊量要足夠豐富;其次是每一個知識點要講透徹,不能模稜兩可含糊不清;最後是包含豐富的截圖,讓那些不想裝 Linux 系統的朋友們也可以領略到 Linux 桌面的風采。如果我的努力得到大家的認可,可以掃下面的二維碼打賞一下:

版權申明

該隨筆由京山遊俠在2018年11月16日釋出於部落格園,引用請註明出處,轉載或出版請聯絡博主。QQ郵箱:[email protected]