天天看點

3D曲面可視化

最近需要 visualize 一些三維的曲面資料,于是簡單調查了一下 Python 繪制三維曲面的一些常用的辦法,貼在這裡以免自己将來再需要用到的時候又想不起來了。比如我們想要畫這個樣子的 3D 曲面圖。

3D曲面可視化

友善起見,我們假造一個 user case,假設  和  是兩個 hyper parameter,現在你使用 grid search 的方式嘗試了每個參數組合下的模型效果。

import numpy as np

def eval_params(x, y):
# ...

# Make data.
X = np.arange(-5, 5, 0.25)
Y = np.arange(-5, 5, 0.25)

Z = np.zeros((len(X), len(Y)))
for i in range(len(X)):
for j in range(len(Y)):
Z[i, j] = eval_params(X[i], Y[j])
           

現在我們希望把這個參數搜尋的曲面畫出來,看看最大值最小值、曲面的連續性之類的性質,是以我們需要畫一個 3D surface 圖。比較新版本的 Matplotlib 其實已經有挺好的 3D 繪圖支援。上面的示例圖其實就是在 Matplotlib 裡畫的。

import matplotlib
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from matplotlib import cm

fig = plt.figure()
ax = fig.gca(projection='3d')

XX, YY = np.meshgrid(X, Y)

# Plot the surface.
surf = ax.plot_surface(XX, YY, Z, cmap=cm.coolwarm,
linewidth=0, antialiased=True)

# Add a color bar which maps values to colors.
fig.colorbar(surf, shrink=0.5, aspect=5)

plt.savefig('surface.pdf')
           

其中 

import Axes3D

 那句雖然沒有直接用到,但是如果删掉的話在建立 3D projection 的 axes 的時候就會出錯。

plot_surface

 這個函數負責繪制 3D surface,參數相當 straightforward。因為 

Z

 作為我們 evaluate 參數的結果是一個二維的矩陣,是以 

plot_surface

 需要的前兩個參數也是二維矩陣,并且需要是和 

Z

 大小相同。

簡單來說,對于一組 index 

(i, j)

XX[i, j]

YY[i, j]

 和 

Z[i, j]

 一起給出了一個位于曲面上的點的三維坐标。而 

meshgrid

 這個函數就是用于友善地根據原來的 

X

 和 

Y

 來建立這樣的二維矩陣的。

到這裡我們的目标其實已經達到了。如果把上面的 

savefig

 換成 

plt.show()

的話,會彈出一個對話框來顯示 3D 曲面圖,好處在于可以對曲面進行旋轉、縮放等互動操作,有時候對于看到曲面被擋住的部分來說非常有用。不過 Matplotlib 的 3D 操作互動反應速度相當緩慢,可能是沒有用任何硬體加速之類的簡單實作吧。如果你不能忍受那個緩慢的互動速度,或者需要将生成出來的圖檔發給别人看,而對方又不一定是能運作 Python 代碼的,那還有一個選擇就是生成内嵌 3D 對象的 PDF 檔案。PDF 檔案可以通過 PRC 格式内嵌 3D 圖像模型,不過目前隻有 Adobe 家的閱讀器(比如 Adobe Reader 或者 Adobe Acrobat)可以檢視這種格式的對象。在 Acrobat 裡打開大概長下面這個樣子,可以對 3D 曲面進行各種操作和檢查。

3D曲面可視化

不過 Matplotlib 本身似乎并不支援直接輸出這種内嵌 3D 對象的 PDF 檔案。這裡我們可以借助另外一個叫做 Asymptote 的繪圖工具。它其實是一種繪圖語言,和 LaTeX 結合得比較緊密,但是和 PGF/TikZ 或者是 PSTricks 不一樣的是,它并不是實作為 TeX 的宏包,是以它的文法并不是一些看起來像英語一樣的很飄逸但是一不小心就會文法錯誤的 DSL,而是有點類似于 C 語言文法。很多年前用它畫過一些 3D 的 vector field 之類的圖,感覺比較适合畫一些複雜的 scientific 的 illustration,可以看一下它的示例。

當然我一般不會用它來 plot 資料圖,不過這裡可以借用一下的是它能輸出内嵌 3D 對象的 PDF 檔案的功能。Asymptote 畫圖的簡單流程是建立一個 

foo.asy

 檔案,把指令寫在裡面,然後執行

asy -f pdf -o foo.pdf foo.asy
      

就可以了。Asymptote 裡建構一個 3D surface 可以通過一個叫做 

surface

 的指令來實作,它的第一個參數是一個描述 surface 的函數:輸入是  兩個 index,輸出是一個三維坐标 。這個比較适合畫一些 parametric function,不過這裡我們是直接 plot 資料,其實隻要做一個簡單的矩陣查表函數就可以了。Asymptote 裡最簡單的讀取資料的方法是讀入空白分隔的文本檔案,打開檔案之後隻要對一個數組或者矩陣進行指派就可以讀入一行或者一塊資料:

import graph3;
import palette;

size3(150, IgnoreAspect);

// Open the data file
file in=input("data.txt").line();
// read file by assignment
// this read a line of numbers as an array
real[] x=in; // first line
real[] y=in; // second line

// read the rest of the file as a matrix
// 0, 0 means do not restrict the number of
// elements to read in either dimension
real[][] z=in.dimension(0,0);

triple f(pair t) {
int i = round(t.x);
int j = round(t.y);
return (x[i], y[j], z[i][j]);
}
           

資料檔案格式非常簡單,頭兩行分别是兩個參數  和  的值清單,然後剩下的行是整個資料矩陣  的一個表格。通過 Numpy 的 

savetxt

 函數很容易導出。在 Asymptote 裡讀入資料之後我們就可以構造查表函數  了,從上面可以看到  隻是直接通過下标取得相應的坐标而已。接下來就可以直接構造 surface 并進行繪制了:

surface s = surface(f, (0,0), (x.length-1, y.length-1), x.length-1, y.length-1);
s.colors(palette(s.map(zpart), Rainbow()));

draw(s, meshpen=blue);

triple m=currentpicture.userMin();
triple M=currentpicture.userMax();
triple target=0.5*(m+M);
currentprojection=perspective(camera=target+realmult(dir(60,210),M-m),
target=target);

xaxis3(Label("$\lambda_1$",position=MidPoint,align=-Y-Z),
Bounds(),blue,OutTicks(Step=0.25,step=0.05));
//Bounds(),blue,OutTicks(Label(align=-Y-X)));
yaxis3(Label("$\lambda_2$",position=MidPoint,align=-5X),
Bounds(),red,OutTicks(Step=0.25,step=0.05));
zaxis3("Error",Bounds(),black,OutTicks(Step=0.25,step=0.05));
           

代碼非常直接,唯一需要簡單注意一下的是我們在調用 

surface

 函數的時候,傳遞了參數 

(0,0)

 和 

(x.length-1, y.length-1)

,這裡對應  在查表時的輸入序列。

這樣就大功告成了,當然缺點就是 Adobe 家族以外的 PDF 閱讀器都看不了。第三個對于 viewer 來說比較 user friendly 的選項就是通過 web 頁面。現在在 webGL 之類的技術支援下,其實 web 頁面裡渲染 3D 對象的效果其實已經非常好了。

比如之前在網上看到這個 3D 的卡通畫,簡直太驚豔了,我覺得現在的各種 3D 遊戲之類的都處于嚴重的 Uncanny Valley 之中,讓我非常難以接受,相反若是做成這種風格,應該會(讓我覺得)賞心悅目很多。然而雖然看起來隻是簡單的 line drawing 的結果,但是實際的制作過程還是要進行正兒八經的三維模組化、渲染之類的,作者在這裡給了制作過程介紹,看起來似乎還是相當費力的。另外還有這裡也有一些非常漂亮的 3D 線條漫畫,看起來都非常酷,而且都是直接在浏覽器了渲染出來的,不需要額外安裝什麼很複雜的 viewer 。

就我所知範圍,目前的繪圖工具中似乎 plotly 算是對 webgl 支援比較好并且也簡單易用的。它們的 3D 圖大概長下面這個樣子,這裡有一個線上的例子可以直接進行互動,渲染和互動效果都挺好的。

3D曲面可視化

例如直接用我們剛才的 Python 裡的資料的話,用下面的代碼可以直接生成 plotly 的圖,結果保持成一個 HTML 檔案。檔案裡内嵌的 viewer,應該用任意一個現代一點的浏覽器都能打開預覽和互動。

import plotly
import plotly.graph_objs as go

data = [go.Surface(z=Z, x=XX, y=YY)]
layout = go.Layout(title='Test Plot', autosize=True)
fig = go.Figure(data=data, layout=layout)

# save offline standalone HTML files
plotly.offline.plot(fig, filename='test-plot')
           

唯一的缺點可能就是由于内嵌的 viewer,是以 HTML 檔案還是比較大的,一個簡單的 surface plot 大概也需要 2M 左右。另外 plotly 還能内嵌在 Jupyter Notebook 裡預覽互動圖,它還提供收費的雲服務可以在伺服器上 host 你的 figure。

感覺以後的科技論文格式應該允許更多的互動内容,嵌入視訊、音頻、互動圖表之類的,還有更全的 meta data,圖表、公式、引用的預覽、跳轉等等。然而現實是目前很多地方還會要求 plot 必須是在黑白列印的情況下也能分辨的。