因此我決定好好處理這個問題,最後開發出了比以前版本(每秒幾千單詞)性能提高百倍的 NeuralCoref v3.0(https://github.com/huggingface/neuralcoref) ,同時還保持了一樣水準的準確性和易用性。html
本文中,我會分享在這個項目上總結的一些經驗,重點包括:python
怎樣在 Python 中 設計一個高效率的模塊。git
怎樣 充分利用 spaCy 的內部數據結構來快速設計極高性能的 NLP函數。**github
這裏我耍了點小花招,由於咱們既要談論 Python,同時還會涉及一些 Cython 內容——不過 Cython 是 Python 的一個超集(http://cython.org/) ,因此不要擔憂!api
你如今寫的 Python 程序已是一個 Cython 程序了。數組
下面的一些場景可能對速度有很高的要求:微信
你正在使用 Python爲 NLP 開發一個 生產模塊;網絡
你正在使用 Python 對一個大型 NLP 數據集進行 計算分析;數據結構
你正在爲諸如 pyTorch/TensorFlow 這類深度學習框架 預處理大型訓練集,或者深度學習模型採用的 批處理加載器加載了太多複雜邏輯,嚴重拖慢了訓練速度。多線程
開始以前再提一句,我還發布了一個 Jupyter notebook(https://github.com/huggingface/100-times-faster-nlp),其中包含了本文中討論的全部示例,去試試吧!
首先你要知道,你的大部分代碼在純 Python 環境下可能都運行良好,可是其中存在一些 瓶頸函數,若是好好處理它們,運行速度就能提高一個數量級。
因此,應該首先檢查你的 Python 代碼,找出那些影響性能的部分。其中一種方法就是使用 cProfile(https://docs.python.org/3/library/profile.html) ,像這樣:
你可能會發現影響性能的是一些循環或者使用神經網絡時引入的 Numpy 數組操做。
那麼該如何加速這些循環?
讓咱們經過一個簡單的例子來解決這個問題。假設有一堆矩形,咱們將它們存儲成一個由 Python 對象(例如 Rectangle類實例)構成的列表。咱們的模塊的主要功能是對該列表進行迭代運算,從而統計出有多少個矩形的面積是大於所設定閾值的。
咱們的 Python 模塊很是簡單,看起來像這樣:
這個 check_rectangles 函數就是咱們的瓶頸所在!它對大量 Python 對象進行循環檢查,而由於 Python 解釋器在每次迭代中都要作不少工做(好比在類中查找 area 方法、打包和解包參數、調用 Python API 等),這個循環就會很是影響性能。
這時就該引入 Cython 來幫助咱們加速循環了。
Cython 語言是 Python 的一個超集,包含兩種類型的對象:
Python 對象就是咱們在常規 Python 中使用到的那些對象,諸如數值、字符串、列表和類實例等;
Cython C 對象是 C 或 C++ 對象,諸如雙精度、整型、浮點、結構、向量,它們可以用 Cython 的高性能底層語言代碼進行編譯。
所謂快速循環,就是在 Cython程序中只訪問 Cython C 對象的循環。
設計這種循環最直接的辦法就是,定義一個 C結構,其中包含計算過程當中須要的全部內容:本例中就是矩形的長度和寬度。
而後咱們能夠將矩形對象的列表存儲到這種 C 結構數組中,再將數組傳遞給 check_rectangle 函數。這個函數如今須要接收一個 C 數組做爲輸入,由此使用 cdef 關鍵字取代了 def(注意 cdef 也能夠用於定義 Cython C 對象),將函數定義爲一個 Cython 函數。
這是咱們的 Python模塊用更快的 Cython 版本重寫後的樣子:
這裏咱們使用了 C 指針的原始數組,但你也能夠選擇其它方案,特別是諸如向量、二元組、隊列之類的 C++結構(http://cython.readthedocs.io/en/latest/src/userguide/wrapping_CPlusPlus.html#standard-library) 。在這段代碼中,我還使用了 cymem(https://github.com/explosion/cymem) 的 Pool() 內存管理對象,以自動釋放分配的 C 數組。當 Pool觸發 Python的垃圾回收時,它會自動釋放所分配對象使用的內存。
spaCy API 的 Cython 約定(https://spacy.io/api/cython#conventions)能夠做爲在實際應用中使用 Cython 執行 NLP任務的參考。
有不少辦法可用於測試、編譯和發佈 Cython 代碼!Cython 甚至能夠像 Python 同樣直接用在 Jupyter Notebook 內(http://cython.readthedocs.io/en/latest/src/reference/compilation.html#compiling-notebook )。
首先使用 pip install cython 命令安裝 Cython。
使用 %load_ext Cython 在 Jupyter notebook 中加載 Cython 擴展。
如今就可使用神奇的命令(http://cython.readthedocs.io/en/latest/src/reference/compilation.html#compiling-with-a-jupyter-notebook ) %%cython 來寫 Cython代碼了,就像寫 Python代碼同樣。
若是在執行 Cython 單元時遇到了編譯錯誤,必定要檢查 Jupyter 終端輸出的完整信息。
大多數狀況下,多是忘記在 %%cython以後加上 -+標籤(好比當你使用 spaCy Cython API 時)。若是編譯器報出了 Numpy相關的錯誤,那就是忘加 import numpy了。
正如我在一開始就提到的,請仔細查看這個 Jupyter notebook(https://github.com/huggingface/100-times-faster-nlp),它包含了咱們討論到的全部示例。
Cython 代碼的文件後綴是 .pyx,這些文件被 Cython 編譯器編譯成 C 或 C++ 文件,再被系統的 C 編譯器編譯成字節碼。以後 Python 解釋器就能使用這些字節碼文件。
可使用 pyximport將一個 .pyx 文件直接加載到 Python 裏:
還能夠將 Cython 代碼打包成 Python,而後像正常的 Python 包同樣導入或發佈,細節見此(http://cython.readthedocs.io/en/latest/src/tutorial/cython_tutorial.html) 。這種作法須要花費更多的時間,尤爲是須要進行全平臺發佈的時候。若是須要參考,能夠看看 spaCy 的安裝腳本(https://github.com/explosion/spaCy/blob/master/setup.py)。
在開始討論 NLP以前,仍是先快速過一遍 def、cdef和 cpdef這三個關鍵字,由於它們是使用 Cython 時須要掌握的基礎內容。
你能夠在 Cython 程序中使用三種類型的函數:
Python 函數,使用 def關鍵字來定義,它們是能夠做爲輸入和輸出的 Python對象。在函數內可使用 Python 和 C/C++ 對象,而且能夠調用 Cython 和 Python 函數。
使用 cdef關鍵字定義的 Cython 函數,它們是能夠做爲輸入(在內部使用)或輸出的 Python 和 C/C++ 對象。這些函數不能從 Python 中訪問(也就是 Python 解釋器和其它能夠導入 Cython 模塊的純 Python 模塊),可是能夠由其它 Cython 模塊導入。
使用 cpdef關鍵字定義的 Cython 函數很像 cdef定義的 Cython 函數,但前者同時還帶有 Python 包裝器,因此能從 Python 中直接調用(用 Python 對象做爲輸入和輸出),也能夠從其它 Cython 模塊中調用(用 C/C++ 或 Python 對象做爲輸入)。
cdef關鍵字的另外一個用途是在代碼中聲明 Cython C/C++ 對象。除非你在代碼中使用 這個關鍵字聲明對象,不然它們都會被當作 Python 對象(結果致使訪問速度變慢)。
這樣看上去又快又好,但還沒到 NLP這一步。好比沒有字符串操做,沒有 unicode 編碼,咱們在 NLP中用到的技巧一個都沒涉及。
此外 Cython 的官方文檔甚至建議不要使用 C 類型的字符串:
通常而言,除非你知道本身在作什麼,不然就應該儘量避免使用 C 字符串,而要使用 Python 的字符串對象。
那麼咱們在處理字符串時,要如何在 Cython 中設計高性能的循環呢?
spaCy 能解決這個問題。
spaCy 處理該問題的作法就很是明智。
spaCy 中全部的 unicode 字符串(一個節點文本、它的小寫文本、它的引理形式、POS 標記標籤、解析樹依賴標籤、命名實體標籤等)都被存儲在一個稱爲 StringStore的數據結構中,用一個 64 位哈希值進行索引,也就是 C 類型的 uint64_t(https://www.badprog.com/c-type-what-are-uint8-t-uint16-t-uint32-t-and-uint64-t)。
StringStore對象實現了 Python unicode 字符串與 64 位哈希值之間的映射。
咱們能夠從 spaCy 的任意位置和任意對象訪問它,例如 npl.vocab.strings、doc.vocab.strings或 span.doc.vocab.string。
當一個模塊須要在某些節點上得到更高的性能時,只要使用 C 類型的 64 位哈希值代替字符串便可。調用 StringStore映射表將返回與該哈希值相關聯的 Python unicode 字符串。
可是 spaCy 還能作更多事情,它還能讓咱們訪問到文檔和詞彙表的完整 C 類型結構,咱們能夠在 Cython 循環中使用這些結構,這樣就不用本身從頭構建了。
與 spaCy 文檔關聯的主要數據結構是 Doc(https://spacy.io/api/cython-classes#section-doc) 對象,它包含通過處理的字符串節點序列(「words」)以及它們在 C 類型對象中的全部註解,稱爲 doc.c(https://spacy.io/api/cython-classes#token_attributes) ,它是一個 TokenC 結構數組。
TokenC(https://spacy.io/api/cython-structs#section-tokenc) 結構包含了咱們須要的每一個節點的全部信息。這些信息被存儲爲 64 位哈希值,它能夠與以前的 unicode 字符串從新關聯。
若是想要準確地瞭解這些 C 結構中的內容,能夠查看最近剛發佈的的 spaCy 的 Cython API 文檔(https://spacy.io/api/cython)。
接下來看一個簡單的 NLP示例。
假設有一個文本文檔的數據集須要分析。
我寫了一個腳本,建立一個包含 10 個文檔(通過 spaCy處理)的列表,每一個文檔有大約 17 萬個單詞。固然,咱們也能夠作 17 萬個文檔(每一個文檔包含 10 個單詞),可是建立這麼多文檔會很慢,因此咱們仍是選擇 10 個文檔。
咱們想要在這個數據集上執行一些 NLP任務。例如,咱們想要統計數據集中單詞「run」做爲名詞出現的次數(也就是被 spaCy 標記爲「NN」)。
用 Python 循環來處理很是簡單和直觀:
但它也很是慢!這段代碼在個人筆記本上須要運行 1.4 秒才能得到結果。若是咱們的數據集中包含數以百萬計的文檔,咱們也許要花費 一天以上才能看到結果。
咱們可使用多線程來提速,但在 Python 中這每每不是最佳方案(https://youtu.be/yJR3qCUB27I?t=19m29s) ,由於你還須要處理全局解釋器鎖(GIL https://wiki.python.org/moin/GlobalInterpreterLock )。須要注意的是, Cython 也可使用多線程(https://cython.readthedocs.io/en/latest/src/userguide/parallelism.html) !Cython 在底層能夠直接調用 OpenMP。這裏我沒時間更加深刻探討並行處理,能夠參考這裏(https://cython.readthedocs.io/en/latest/src/userguide/parallelism.html)獲取更多信息。
如今咱們嘗試使用 spaCy 和 Cython 來加速 Python 代碼。
首先,咱們要肯定使用哪一種數據結構。咱們須要一個 C 類型的數組存放數據集,其中用指針指向每一個文檔的 TokenC 數組。還要將測試字符(「run」和「NN」)轉成 64 位哈希值。
當全部須要處理的數據都變成了 C 類型對象,咱們就能以純 C 語言的速度迭代數據集。
下面展現這個例子如何寫成 Cython 和 spaCy 的形式:
代碼有點長,由於咱們必須在調用 Cython 函數 [*](https://medium.com/huggingface/100-times-faster-natural-language-processing-in-python-ee32033bdced#a220) 以前在 main_nlp_fast中聲明和計算 C 結構。
但它的性能獲得大幅提高!在個人 Jupyter notebook中,這部分 Cython 代碼大概只用 20 毫秒就運行完畢,比以前的純 Python 循環快了 大概 80 倍。
使用 Jupyter notebook 單元編寫模塊的速度很驚人,它能夠與其餘 Python 模塊和函數發生交互:在 20 毫秒內掃描大約 170 萬個單詞,這意味着咱們每秒可以處理高達 8 千萬個單詞。
對使用 Cython 加速 NLP的介紹到此爲止,但願你們喜歡。
關於 Cython 還有不少其它的東西能夠介紹,可是已經大大超出了這篇文章的範圍。接下來最好的參考資料也許是這份 Cython 教程(http://cython.readthedocs.io/en/latest/src/tutorial/index.html),它提供了綜述內容,以及 spaCy 的 Cython 頁面(https://spacy.io/api/cython),它提供了 NLP相關的內容。
若是你在代碼中須要屢次使用底層結構,比每次計算 C 結構更優雅的作法是,在 Python代碼的底層使用 Cython 擴展類型(http://cython.readthedocs.io/en/latest/src/userguide/extension_types.html) 來包裝 C 類型結構。這就是大多數 spaCy 代碼所採用的結構,它很是優雅,兼具高效、低內存開銷和易於交互的特性。
英文原文:
https://medium.com/huggingface/100-times-faster-natural-language-processing-in-python-ee32033bdced