在 GitHub 上編輯此頁面

GSOC2011 MocapyExt

BioPython 是生物資訊學和計算生物學中非常流行的函式庫。Mocapy++ 是一個機器學習工具包,用於動態貝氏網路 (DBN) 中的參數學習和推論PaluszewskiHamelryck2010,它編碼了領域中隨機變數之間的機率關係。Mocapy++ 在 GNU 通用公共許可證 (GPL) 下免費提供,可從 SourceForge 取得。該函式庫支援廣泛的 DBN 架構和機率分佈,包括方向統計學的分佈。值得注意的是,球體上的肯特分佈和環面上的雙變數馮·米塞斯分佈,已被證明在制定蛋白質和 RNA 結構的機率模型中很有用。

這樣一個非常有用且功能強大的函式庫,已成功應用於 TorusDBNBMTFKH2008、BasiliskHBPFJH2010、FB5HMMHKK2006PaluszewskiWinter2008 等專案中,是長期努力的成果。Mocapy 的原始實作可以追溯到 2004 年,從那時起,該函式庫已用 C++ 重寫。然而,C++ 是一種靜態類型和編譯的程式語言,不利於快速原型設計。因此,目前的 Mocapy++ 沒有動態載入自訂節點類型的功能,並且需要一個機制來插入新的節點類型,而無需修改和重新編譯函式庫,這是一個值得關注的問題。這樣一個外掛介面將有助於快速原型設計,允許快速實作和測試新的機率分佈,進而可以大幅減少開發時間和精力;使用者將能夠在不修改和後續重新編譯的情況下擴展 Mocapy++。意識到這種需求,T. Hamelryck 提出了該專案 (以下稱為 MocapyEXT),旨在改進目前的 Mocapy++ 節點類型擴充機制。

專案目標

MocapyEXT 專案主要是一個工程努力,旨在為 Mocapy++ 帶來一個透明的 Python 外掛介面,其中內建和動態載入的節點類型可以以統一的方式使用。此外,外部實作和動態載入的節點可以由使用者修改,並且這些變更不需要重新編譯客戶端程式,也不需要重新編譯隨附的 Mocapy++ 函式庫。這將有助於快速原型設計,簡化現有程式碼的調整,並提高軟體互通性,同時對現有的 Mocapy++ 介面進行最小的變更,從而有助於順利接受 MocapyEXT 引入的變更。

作者和指導老師

Justinas V. Daugmaudis (vygis.d@gmail.com)

指導老師

Thomas Hamelryck

Eric Talevich

工作計畫

'''了解 S-EM 和方向統計'''

'''研究 Mocapy++ 的使用案例'''

'''研究 Mocapy++ 內部結構和程式碼'''

'''設計 Mocapy++ 外掛介面'''

'''實作 Mocapy++ 外掛模組'''

'''實驗模組化的 Mocapy++ 架構'''

時程

'''第 1-2 週 [5 月 23 日 – 6 月 5 日]'''

介面策略設計:對於 Mocapy++ 使用者來說,外掛 API 感覺「自然」的重要性再怎麼強調也不為過。大部分時間將用於介面設計。

'''第 3-5 週 [6 月 6 日 – 6 月 19 日]'''

實作外掛模組。

'''第 6-7 週 [6 月 20 日 – 6 月 30 日]'''

實作一些範例,以展示 Mocapy++ 外掛系統的實際應用;例如,如何使用外部實作的邏輯分佈。

'''第 7-8 週(期中)[7 月 1 日 – 7 月 10 日]'''

'''第 9-10 週 [7 月 11 日 – 7 月 24 日]'''

範例應用程式。應著重於外掛模組的類型反映能力。

'''第 11-12 週 [7 月 25 日 – 8 月 7 日]'''

更新文件以反映新功能。同時記錄範例,進行任何程式碼清理工作等。計畫的「停止撰寫」日期。

'''第 13-14 週 [8 月 8 日 – 8 月 21 日]'''

向更廣泛的受眾介紹綁定,收集社群中的意見和評論。

'''第 15 週 [8 月 22 日]'''

專案結束。

原始碼

託管於 gSoC11 Mocapy 分支

進度

定義

對於 Mocapy++ 術語不熟悉的人來說,必須了解什麼是 ESS 電腦和密度,因為這些術語將在本文中通篇使用。

ESSConcept

在下表中,X 表示 ESS 電腦類別,而 uX 的可變值。

表示式 傳回類型 前提條件/後置條件
u.construct(parent_sizes) void 定義了 ESS 的適當形狀
u.estimate(ess) void 將樣本點新增至 ESS

類別 mocapy::BippoESS 是 ESS 電腦的範例模型。

DensitiesConcept

在下表中,X 表示密度電腦類別,vX 的常數值,而 uX 的可變值。請注意,ptv 代表「父節點和此節點的值」:父節點的值以及該方法所屬節點的值。

表示式 傳回類型 前提條件/後置條件  
u.construct(parent_sizes) void 初始化參數(平均值、共變異數、CPD 等)  
u.estimate(ess) void 根據給定的 ESS 估計節點的參數  
u.sample(ptv) std::vector 根據指示的父節點值傳回樣本。請注意,後續呼叫 sample 成員函式可能會傳回不同的值,因此此操作是可變的  
v.get_lik(ptv, log) double 傳回似然值,P(子節點 父節點)
v.get_parameters() std::vector<MDArray > 傳回分佈參數  
v.get_node_size() unsigned int 傳回節點大小  
v.get_output_size() unsigned int 傳回節點的輸出大小  

類別 mocapy::BippoDensities 是密度電腦的範例模型。

MocapyEXT 實作

嵌入 Python 解譯器

為了讓客戶端程式能夠執行 Python 程式碼,必須初始化指令碼環境。這是通過調用 Py_Initialize() 函式來完成的。解譯器通過調用 Py_Finalize() 函式來釋放。

然而,必須注意,Py_Initialize() 和 Py_Finalize() 呼叫之間的任何陳述都可能拋出例外。如果拋出例外,則必須在 try/catch 區塊中處理,否則必須終止程式。考慮到這一點,可以通過將 Py_Initialize() 和 Py_Finalize() 之間的陳述包裝在 try/catch 區塊中,使先前的範例程式更加強大。Py_Finalize() 的安全性再怎麼強調也不為過。目前,Boost.Python 有多個全域(或函式靜態)物件,它們的存在使得引用計數無法降至零,直到卸載 Boost.Python 共用物件為止。這可能會導致崩潰,因為當引用計數歸零時,沒有解譯器。這提出了一個問題,即這種初始化 Python 解譯器的方法是否可以被認為是「易於使用」、「安全」甚至是「非侵入式」的,而這些都是 MocapyEXT 的主要設計宗旨。

這個問題的解決方案是易於使用、安全且非侵入式的,令人驚訝的是它非常優雅,並展示了 RAII(資源獲取即初始化)的慣用法。

Python 解譯器的生命週期

Python 解譯器在進入 main 函式之前初始化,並在退出 main 函式之後釋放。不需要額外的終端使用者努力,只需包含定義必要 Python 外掛封裝函式的標頭即可。

Python 解譯器應連結到客戶端程式(而不是 Mocapy++ 函式庫)。MocapyEXT 的終端使用者應負責額外的包含和/或程式庫路徑,以指向成功編譯和連結客戶端程式所必需的 Python 標頭和程式庫。

MocapyEXT 也以這樣的方式實例化其靜態資料成員,即它們僅實例化一次,並且在程式庫的編譯單元中實例化。C++ 標準中明確指出

「…特別是,除非靜態資料成員本身以需要靜態資料成員定義存在的方式使用,否則不會發生靜態資料成員的初始化(以及任何相關的副作用)。」[cpp_std2003, temp.inst]

使用者不需要管理實例化的靜態資料成員。

執行緒安全性保證

MocapyEXT 外掛具有以下執行緒安全性保證

對傳入的參數容器進行同時可變存取需要同步處理,例如透過讀寫鎖。可變存取包括修改容器內的值、呼叫會使迭代器失效的成員函式,以及透過移動建構函式移動容器。

模組搜尋路徑

當匯入包含 ESS 或密度 (Densities) 定義的模組 foo 時,內嵌的 Python 解譯器會在環境變數 PYTHONPATH 和目前路徑所指定的目錄列表中搜尋名為 foo.py 的檔案。

在繼續匯入模組之前,會將目前路徑明確附加到 sys.path 列表。如果使用者依賴腳本存在於客戶端程式以外的其他目錄中,則應在 PYTHONPATH 環境變數中列出這些路徑。

請注意,由於解譯器也會在與安裝相關的預設路徑中搜尋,因此包含 ESS 或密度定義的檔案名稱不應與標準模組的名稱相同,這一點非常重要。

外掛程式註冊

最小介面匯入範例顯示了三個外掛程式類別的用法:densities_plugin、ess_plugin 和 aggregate_plugin。densities_plugin 和 ess_plugin 的預期用法是在 ESS 和密度類型於不同檔案中實作的情況下,即每個檔案一個類別。每個類別都會在其自己的解譯器環境中載入。aggregate_plugin 的目的是簡化載入類型的管理,並最佳化為內嵌 Python 解譯器配置的資源,因為 aggregate_plugin 只會為 ESS 和密度類型建立一個內嵌解譯器的執行個體。

#include <mocapy/plugin/ess_plugin.hpp> #include <mocapy/plugin/densities_plugin.hpp> #include <mocapy/plugin/aggregate_plugin.hpp> int main() {   using namespace mocapy::ext;   densities_plugin dens_pl("plugin_tests", "DensitiesPython");   ess_plugin ess_pl("plugin_tests", "ESSPython");   aggregate_plugin aggr_pl("plugin_tests", "ESSPython", "DensitiesPython");   return 0; }

基本上,使用者會提供 Python 程式庫的名稱、類別的名稱,以及建構類別模型所需的實際引數清單。然後,MocapyExt 程式庫會傳回新類別的執行個體。

densities_plugin p("test_module", "DensitiesClassName"); densities_adapter n = p.densities(arg1, arg2, ..., argN);

給定的引數列表(最多支援 N = 6 個引數)將參數化密度的物件初始化。參數會被轉送到 Python API,並使用 Boost.Python 程式庫提供的類型轉換功能進行轉換。這個機制是通用的,使用者可以將任何任意類型 T 轉換為 Python 中對應的類型。

參數會透過 const-reference 轉送。這個方法接受並轉送任意類型的引數,但代價是始終將引數視為 const。這個解決方案通常用於建構函式的引數。這種方法的一個深奧問題是,無法形成函式類型的 const reference,但這個缺陷(正在核心問題 295 中處理),實際上對我們有利,因為這可以防止惡意嘗試將函式傳遞給 Python 解譯器。

任何進一步呼叫 ess 和 dens 成員函式的行為,都會自動呼叫 Python 中對應的類別方法。

類別執行個體的生命週期

ESS 計算機和密度物件 (分別為 ess 和 dens) 的生命週期可能會超過其各自的工廠外掛程式的生命週期。物件 ess 和 dens 會保留對 Python 解譯器的參考計數指標,因此,有效的 Python 解譯器執行個體會一直存在,直到物件 ess 和 dens 被銷毀為止。

建立節點

這是一個簡單的範例,展示了兩種不同的方法來初始化外掛程式節點。在這個特定案例中,程式中使用的模組 plugin_tests 實作了一個固定長度為 1 的虛擬離散節點,並且始終傳回 [0,] 作為取樣值。

#include <mocapy/plugin/ess_plugin.hpp> #include <mocapy/plugin/densities_plugin.hpp> #include <mocapy/plugin/aggregate_plugin.hpp> #include <mocapy/plugin/plugin_node.hpp> int main() {   using namespace mocapy::ext;   // 從兩個單獨載入的模組初始化節點   {     ess_plugin ess("plugin_tests", "ESSPython");     densities_plugin dens("plugin_tests", "DensitiesPython");     plugin_node_type node(ess.ess(arg1, arg2, ..., argN), dens.densities(arg1, arg2, ..., argN));   }   // 從單一載入的模組初始化節點   {     aggregate_plugin pl("plugin_tests", "ESSPython", "DensitiesPython");     plugin_node_type node(pl.ess(arg1, arg2, ..., argN), pl.densities(arg1, arg2, ..., argN));   }   return 0; }

plugin_node_type 是 ChildNode 類別範本的 typedef。在此我們也注意到,節點的生命週期與外掛程式工廠的生命週期無關。

節點輸出

外掛程式節點是可串流的,也就是說,它可以透過運算子輸出到 std::ostream:

std::ostream& operator<<(std::ostream&, const plugin\_node\_type&);

輸出的內容與 repr(Z) 相同,其中 Z 是 ESS 計算機或密度物件。

節點序列化

MocapyEXT 保留了舊的 Mocapy++ ChildNode 類別範本序列化行為,這表示對於不需要序列化的 ESS 計算機(包括 kentess、gaussianess、poissoness 等),不會發生序列化。但是,MocapyEXT 必須序列化實作 ESS 計算機的 Python 外掛程式,才能保留模組和類別的名稱,以便稍後可以將 ESS 計算機反序列化。

還必須注意的是,反序列化後,即使 ESS 計算機和密度在同一個模組中實作,它們也不會共用同一個 Python 解譯器執行個體。

效能測量

測量外掛程式介面與 ESS 和密度計算機本身的實作相比,成本有多高,這很有趣。

我們測量名為 N、S 和 A 的測試的相對效能。所有測試都會呼叫 ESS 和密度計算機的成員函式,儘管沒有執行任何特定的計算。

N 測試代表 N(ative),這表示在測試中使用了 ESS 和密度計算機的純 C++ 實作。

S 測試代表 S(eparate);Python 類別會在個別的 Python 解譯器執行個體中載入。

A 測試代表 A(ggregate);ESS 和密度計算機會在同一個 Python 解譯器執行個體中載入。

信賴區間。 4.53e+04 5.03e+04 5.53e+04

基於 Wilcoxon 配對信賴區間的加速百分比。

函式 A vs 函式 B 最小值 中位數 最大值 (快 %)   (快 %)   (快 %) N vs S 717 719 724 N vs A 718 725 730 A vs S -0.673 -0.251 0.0774

測試結果顯示,使用 MocapyEXT 外掛程式介面會產生一些效能損失。但是必須注意的是,測試也執行了重複建構 ESS 和密度執行個體的行為。

信賴區間。 1.94e+04 2.15e+04 2.37e+04

基於 Wilcoxon 配對信賴區間的加速百分比。

函式 A vs 函式 B 最小值 中位數 最大值 (快 %)   (快 %)   (快 %) N vs S 240 242 243 N vs A 239 243 245 A vs S -1.24 -0.51 -0.25

在沒有建構 ESS 和密度物件的情況下重複測試顯示,透過 MocapyEXT 介面呼叫方法只比呼叫本機實作的 ESS 和密度物件的成員函式慢約 2.5 倍。顯然,在實際情況中,與方法本身的呼叫相比,節點建構、取樣、概似計算等更有可能構成執行時間的主要部分。基本上,這些測試顯示,成員函式呼叫所花費的時間,甚至比節點建構這種「輕量級」的操作還要少得多。原則上,現在可以編寫相當通用的演算法。請考慮這個範例

 template inline void do_something_with_densities(Densities dens) // 通用函式 {    std::vector ps(1, 2);    dens.construct(ps);    /* 在此執行某些操作 */  }  // 透過 Python 介面使用 Bippo 密度  densities_plugin dens("mocapy.bippo", "BippoDensities");  do_something_with_densities( dens.densities(lambdas, thetas, ns) );  // 直接使用 Bippo 密度  mocapy::BippoDensities bd(lambdas, thetas, ns);  do_something_with_densities(bd); 我們可以合理地確定,使用 MocapyEXT 的效能損失雖然不能忽略,但並不會超過通用性所帶來的附加價值。而在純 C++ 的情況下,它不會產生任何效能成本。 ### 未來規劃 MocapyEXT 是一個正在進行中的專案。在目前狀態下,它是可用的,但是,必須強調的是,所描述的功能絕非完整,並且未來可能會以不同的方式運作。為了支援 MocapyEXT 基礎結構,Mocapy++ 中進行了許多變更。最值得注意的是,Mocapy++ 程式庫在開發分支中已部分修正為 const 正確。const 正確性的重要性再怎麼強調也不為過,因為它會影響從 Python 程式碼傳遞參數的協定。但是,這些變更是以透明的方式完成的。如果終端使用者不依賴現在不可變運算的副作用,則應該能夠以較小的修改來編譯和執行程式碼。毫無疑問,還有許多其他變更在等待。例如,在某些情況下,如果能夠透過將參數傳遞給建構函式來複製密度,會很有用。` mocapy::BippoDensities bd(lambdas, thetas, ns);` ` mocapy::BippoDensities clone( bd.get_parameters() );` ` // 這些物件必須有相同的狀態!` ` assert( bd == clone );`我認為目前的參數初始化機制設計不良,因為隨機數產生器的指標被儲存在 Densities 中。將隨機數產生器作為參數傳遞會更好。這將能單獨解決 Mocapy++ Python 綁定中的 [物件所有權](https://biopython.dev.org.tw/wiki/GSOC2011_Mocapy#Testing_and_Improving_Mocapy_Bindings) 問題,修正一些殘留的 const-正確性問題等等。然而,這會引入重大變更,而且許多專案都依賴 Mocapy++。在專案期間,一個有趣的技術練習是將 MDArray 類別模板的值轉換為 Python 的 ndarray。轉換常式已經寫好,並且全部都如預期般正常運作,然而,MDArray 類別模板本身存在問題。這是 [關注點分離](http://en.wikipedia.org/wiki/Separation_of_concerns) 失敗的經典例子之一。它不僅充當多維陣列,還以類似瑞士刀的方式使用:MDArray 也充當矩陣、向量(在數學意義上),它有奇怪命名的成員函式,例如 void keep\_eye() (我仍然不知道它做什麼),它可以旋轉、軸置換,以及任何你能想到的功能 - 所有這些都在成員函式中實現。MDArray 介面非常龐大,這顯然是錯誤的。儘管如此,觸碰 MDArray 是一個需要極為謹慎處理的微妙問題;最有可能的解決方案是引入自由函式來執行這些工作,並逐步淘汰成員函式。參考文獻 ----------1. PaluszewskiHamelryck2010 M. Paluszewski 和 T. Hamelryck. Mocapy++ -- 一個用於動態貝氏網路推論和學習的工具包。BMC 生物資訊學, 11(1):126, 2010. 1. BMTFKH2008 W. Boomsma, K.V. Mardia, C.C. Taylor, J. Ferkinghoff-Borg, A. Krogh, 和 T. Hamelryck. 一個關於局部蛋白質結構的生成式機率模型。美國國家科學院院刊, 105(26):8932–8937, 2008. 1. HBPFJH2010 Tim Harder, Wouter Boomsma, Martin Paluszewski, Jes Frellsen, Kristoffer Johansson, 和 Thomas Hamelryck. 超越側鏈旋轉異構體:蛋白質中側鏈的生成式機率模型。BMC 生物資訊學, 11(1):306, 2010. 1. HKK2006 T. Hamelryck, J.T. Kent, 和 A. Krogh. 使用局部結構偏差採樣真實的蛋白質構象。PLoS 計算生物學, 2(9):e131,2006. 1. PaluszewskiWinter2008 M. Paluszewski 和 P. Winter. 使用帶有效邊界的枝界法生成蛋白質誘餌。生物資訊學演算法, 頁碼 382–393, 2008.