快100 倍,Python 為自然語言處理加速度!

2020-12-12 CSDN

利用 spaCy 和一點點 Cython 給 NLP 加速。

自去年發布 Python 的指代消解包(coreference resolution package)之後,很多用戶開始用它來構建許多應用程式,而這些應用與我們最初的對話應用完全不同。

我們發現,儘管在處理對話時這個包的速度完全沒問題,但在處理較大的問題時卻非常慢。

我決定調查一下這個問題,於是就產生了 NeuralCoref v3.0(https://github.com/huggingface/neuralcoref/)這一項目,它比上一個版本快 100 倍(每秒能分析幾千個單詞),同時保持準確度、易用性,並且依然在 Python 庫的生態系統中。

在本文中我想分享一些在這個項目中學習到的經驗,具體來說包括:

怎樣用 Python 設計高速的模塊;怎樣利用 spaCy 的內部數據結構來有效地設計高速的 NLP 函數。所以其實這裡有點耍花招,雖然我們是在討論 Python,但還要用一些 Cython的魔法。但別忘了,Cython 是 Python 的超集(http://cython.org/),所以別被它嚇住了!

你現在的 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):

import cProfileimport pstatsimport my_slow_modulecProfile.run('my_slow_module.run()', 'restats')p = pstats.Stats('restats')p.sort_stats('cumulative').print_stats(30)

也許你會發現有幾個循環比較慢,如果用神經網絡的話,可能有幾個 Numpy 數組操作會很慢(但這裡我不會討論如何加速 NumPy,已經有很多文章討論這個問題了:http://cython.readthedocs.io/en/latest/src/userguide/numpy_tutorial.html)。

那麼,應該如何加快循環的速度?

利用 Cython 實現更快的循環

用個簡單的例子來說明。假設我們一個巨大的集合裡包含許多長方形,保存為 Python 對象(即 Rectangle 類的實例)的列表。模塊的主要功能就是遍歷該列表,數出有多少個長方形超過了某個閾值。

我們的 Python 模塊非常簡單,如下所示:

from random import randomclassRectangle:def__init__(self, w, h):self.w = wself.h = hdefarea(self):returnself.w * self.hdefcheck_rectangles(rectangles, threshold): n_out = 0for rectangle inrectangles:if rectangle.area() > threshold: n_out += 1return n_outdefmain(): n_rectangles = 10000000 rectangles = list(Rectangle(random(), random()) for i in range(n_rectangles)) n_out = check_rectangles(rectangles, threshold=0.25) print(n_out)

這裡 check_rectangles 函數就是瓶頸!它要遍歷大量 Python 對象,而由於每次循環中 Python 解釋器都要在背後進行許多工作(如在類中查找 area 方法、打包解包參數、調用 Python API 等),這段代碼就會非常慢。

這裡 Cython 能幫我們加快循環。

Cython 語言是 Python 的一個超集,它包含兩類對象:

Python 對象是在正常的 Python 中操作的對象,如數字、字符串、列表、類實例等。Cython C 對象是 C 或 C++ 對象,如 dobule、int、float、struct、vectors,這些可以被 Cython 編譯成超級快的底層代碼。高速循環就是 Cython 程序中只訪問 Cython C 對象的循環。

設計這種高速循環最直接的辦法就是,定義一個 C 結構,它包含計算過程需要的一切。在這個例子中,該結構需要包含長方形的長和寬。

然後我們就可以將長方形列表保存在一個 C 數組中,傳遞給 check_rectangle 函數。現在該函數就需要接收一個 C 數組作為輸入,因此它應該用 cdef 關鍵字(而不是 def)定義為 Cython 函數。(注意 cdef 也被用來定義 Cython C 對象。)

下面是 Cython 高速版本的模塊:

from cymem.cymem cimport Poolfrom random import randomcdef struct Rectangle:float wfloat hcdef intcheck_rectangles(Rectangle* rectangles, int n_rectangles, float threshold): cdef int n_out = 0# C arrays contain no size information => we need to give it explicitlyfor rectangle in rectangles[:n_rectangles]:if rectangle[i].w * rectangle[i].h > threshold: n_out += 1returnn_outdef main(): cdef:int n_rectangles = 10000000float threshold = 0.25 Pool mem = Pool() Rectangle* rectangles = <Rectangle*>mem.alloc(n_rectangles, sizeof(Rectangle))for i inrange(n_rectangles): rectangles[i].w = random() rectangles[i].h = random() n_out = check_rectangles(rectangles, n_rectangles, threshold) print(n_out)

這裡用了個 C 指針數組,不過你也可以用別的方式,如 vectors、pairs、queues 等 C++ 結構(http://cython.readthedocs.io/en/latest/src/userguide/wrapping_CPlusPlus.html#standard-library)。在這段代碼中,我還使用了cymem(https://github.com/explosion/cymem)提供的方便的 Pool() 內存管理對象,這樣就不用手動釋放 C 數組了。在 Python 對 Pool 進行垃圾回收時,就會自動釋放所有通過 Pool 分配的內存。

關於在 NLP 中使用 Cython 的指南請參考 spaCy API 的 Cython Conventions:https://spacy.io/api/cython#conventions。

試一下這段代碼

有許多方法可以測試、編譯並發布 Cython 代碼!Cython 甚至可以像 Python 一樣直接用在 Jupyter Notebook 中(http://cython.readthedocs.io/en/latest/src/reference/compilation.html#compiling-notebook)。

首先用 pip install cython 安裝 Cython:

在 Jupyter 中測試

在 Jupyter notebook 中通過 %load_ext Cython 加載 Cython 擴展。

現在,只需使用魔術命令(http://cython.readthedocs.io/en/latest/src/reference/compilation.html#compiling-with-a-jupyter-notebook)%%cython 就可以像寫 Python 代碼一樣寫 Cython 代碼了。

如果在執行 Cython 單元的時候遇到編譯錯誤,可以在 Jupyter 的終端輸出上看到完整的錯誤信息。

一些常見的錯誤:如果要編譯成 C++(比如使用 spaCy Cython API),需要在 %%cython 後面加入 -+ 標記;如果編譯器抱怨 NumPy,需要加入 import numpy 等。

編寫、使用並發布 Cython 代碼

Cython 代碼保存在 .pyx 文件中。這些文件會被 Cython 編譯器編譯成 C 或 C++ 文件,然後再被系統的 C 編譯器編譯成字節碼。這些字節碼可以直接被 Python 解釋器使用。

可以在 Python 中使用 pyximport 直接加載 .pyx 文件:

>>> import pyximport; pyximport.install()>>> import my_cython_module

也可以將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 函數。Cython 函數,用 cdef 關鍵字定義。Python 對象和 Cython 對象都可以作為它的輸入、輸出和內部對象使用。這些函數無法在 Python 空間(即 Python 解釋器,和其他需要導入 Cython 模塊的純 Python 模塊)中直接訪問,但可以被其他 Cython 模塊導入。用 cpdef 定義的 Cython 函數,類似於用 cdef 定義的 Cython 函數,但它們還提供了 Python 封裝,因此可以直接在 Python 空間中調用(用 Python 對象作為輸入和輸出),也可以在其他 Cython 模塊中調用(用 C/C++ 或 Python 對象作為輸入)。cdef 關鍵字還有個用法,就是在代碼中給 Cython C/C++ 對象定義類型。沒有用 cdef 定義類型的對象會被當做 Python 對象處理(因此會降低訪問速度)。

通過 spaCy 使用 Cython 加速 NLP

前面說的這些都很好……但這跟 NLP 還沒關係呢!沒有字符串操作,沒有 Unicode 編碼,自然語言處理中的難點都沒有支持啊!

而且 Cython 的官方文檔甚至還反對使用 C 語言級別的字符串(http://cython.readthedocs.io/en/latest/src/tutorial/strings.html):

一般來說,除非你知道你在做什麼,否則儘量不要使用 C 字符串,而應該使用 Python 字符串對象。

那在處理字符串時怎樣才能設計高速的 Cython 循環?

這就輪到 spaCy 出場了。

spaCy 解決這個問題的辦法特別聰明。

將所有字符串轉換成 64 比特 hash

在 spaCy 中,所有 Unicode 字符串(token 的文本,token 的小寫形式,lemma 形式,詞性標註,依存關係樹的標籤,命名實體標籤……)都保存在名為 StringStore 的單一數據結構中,字符串的索引是 64 比特 hash,也就是 C 語言層次上的 unit64_t。

StringStore 對象實現了在 Python unicode 字符串和 64 比特 hash 之間的查找操作。

StringStore 可以從 spaCy 中的任何地方、任何對象中訪問,例如可以通過 nlp.vocab.string、doc.vocab.strings 或 span.doc.vocab.string 等。

當模塊需要在某些 token 上進行快速處理時,它只會使用 C 語言層次上的 64 比特 hash,而不是使用原始字符串。調用 StringStore 的查找表就會返回與該 hash 關聯的 Python unicode 字符串。

但是 spaCy 還做了更多的事情,我們可以通過它訪問完整的 C 語言層次上的文檔和詞彙表結構,因此可以使用 Cython 循環,不需要再自己構建數據結構。

spaCy 的內部數據結構

spaCy 文檔的主要數據結構是 Doc 對象,它擁有被處理字符串的 token 序列(稱為 words)及所有註解(annotation),這些被保存在一個 C 語言對象 doc.c 中,該對象是個 TokenC 結構的數組。

TokenC(https://github.com/explosion/spaCy/blob/master/spacy/structs.pxd)結構包含關於 token 的所有必要信息。這些信息都保存為 64 比特 hash 的形式,可以通過上面的方法重新構成 unicode 字符串。

看看 spaCy 的 Cython API 文檔,就知道這些 C 結構的好處在哪裡了。

我們通過一個簡單例子看看它在 NLP 處理中的實際應用。

通過 spaCy 和 Cython 進行快速 NPL 處理

假設我們有個文本文檔的數據集需要分析。

import urllib.requestimport spacywith urllib.request.urlopen('https://raw.githubusercontent.com/pytorch/examples/master/word_language_model/data/wikitext-2/valid.txt') as response: text = response.read()nlp = spacy.load('en')doc_list = list(nlp(text[:800000].decode('utf8')) for i in range(10))

上面的腳本建立了一個由10個spaCy解析過的文檔組成的列表,每個文檔大約有17萬個詞。也可以使用17萬個文檔,每個文檔有10個詞(比如對話的數據集),但那樣創建速度就會慢很多,所以還是繼續使用10個文檔好了。

我們要在這個數據集上做一些NLP的處理。比如,我們需要計算「run」這個詞在數據集中作為名詞出現的次數(即被spaCy的詞性分析(Part-Of-Speech)標記為「NN」的詞)。

Python 循環的寫法很直接:

defslow_loop(doc_list, word, tag): n_out = 0for doc in doc_list:for tok in doc:if tok.lower_ == word and tok.tag_ == tag: n_out += 1return n_outdefmain_nlp_slow(doc_list): n_out = slow_loop(doc_list, 'run', 'NN') print(n_out)

但也非常慢!在我的筆記本上這段代碼大概需要1.4秒才能得到結果。如果有100萬個文檔,那就要超過一天的時間。

我們可以使用多任務處理,但在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最精彩的部分,因為GIL鎖已經被釋放,代碼可以全速運行了。基本上,Cython會直接調用OpenMP。這裡不會介紹並行,更多的細節可以參考這裡(https://cython.readthedocs.io/en/latest/src/userguide/parallelism.html)。

現在試著用 spaCy 和一點 Cython 加速 Python 代碼吧。

首先需要考慮下數據結構。我們需要個C層次的數組來保存數據集,其中的指針指向每個文檔的TokenC數組。還需要將測字符串(「run"和「NN」)轉換成64比特hash。

下面是用spaCy編寫的Cython代碼:

%%cython -+import numpy # Sometime we have a fail to import numpy compilation errorif we don't import numpyfrom cymem.cymem cimport Poolfrom spacy.tokens.doc cimport Docfrom spacy.typedefs cimport hash_tfrom spacy.structs cimport TokenCcdef struct DocElement: TokenC* cint lengthcdef intfast_loop(DocElement* docs, int n_docs, hash_t word, hash_t tag): cdef int n_out = 0for doc in docs[:n_docs]:for c in doc.c[:doc.length]:if c.lex.lower == word and c.tag == tag: n_out += 1returnn_outdef main_nlp_fast(doc_list): cdef int i, n_out, n_docs = len(doc_list) cdef Pool mem = Pool() cdef DocElement* docs = <DocElement*>mem.alloc(n_docs, sizeof(DocElement)) cdef Doc docfor i, doc inenumerate(doc_list): # Populate our database structure docs[i].c = doc.c docs[i].length = (<Doc>doc).length word_hash = doc.vocab.strings.add('run') tag_hash = doc.vocab.strings.add('NN') n_out = fast_loop(docs, n_docs, word_hash, tag_hash) print(n_out)

這段代碼有點長,因為得在調用Cython函數之前,在main_nlp_fast中定義並填充C結構。(註:如果在代碼中多次使用低級結構,就不要每次填充C結構,而是設計一段Python代碼,利用Cython擴展類型(http://cython.readthedocs.io/en/latest/src/userguide/extension_types.html)來封裝C語言的低級結構。spaCy的絕大部分數據結構都是這麼做的,能優雅地結合速度、低內存佔用,以及與外部Python庫和函數的接口的簡單性。)

但它也快得多!在我的Jupyter notebook上,這段Cython代碼只需要大約20毫秒,比純Python循環快大約80倍。

要知道它只是Jupyter notebook單元中的一個模塊,還能給其他Python模塊和函數提供原生的接口,考慮到這一點,它的絕對速度也相當出色:20毫秒內掃描1700萬詞,意味著每秒能掃描八千萬詞。

這就是在 NLP 中使用 Cython 的方法,希望你能喜歡。

相關資料

Cython入門教程:http://cython.readthedocs.io/en/latest/src/tutorial/index.htmlspaCy 的 Cython 頁面:https://spacy.io/api/cython英文:100 Times Faster Natural Language Processing in Python連結:https://medium.com/huggingface/100-times-faster-natural-language-processing-in-python-ee32033bdced作者:Thomas Wolf,Huggingface的機器學習,自然語言處理和深度學習科學負責人。譯者:彎月

相關焦點

  • 快 100 倍,Python 為自然語言處理加速度!
    我們發現,儘管在處理對話時這個包的速度完全沒問題,但在處理較大的問題時卻非常慢。我決定調查一下這個問題,於是就產生了 NeuralCoref v3.0(https://github.com/huggingface/neuralcoref/)這一項目,它比上一個版本快 100 倍(每秒能分析幾千個單詞),同時保持準確度、易用性,並且依然在 Python 庫的生態系統中。
  • 韋編 | NLTK——面向英文的python自然語言處理工具
    又快到期末了!!!各種課程論文!!!上周給大家介紹了一款面向中文的自然語言處理工具HanLP,今天給大家介紹另一款面向英文的經典的python自然語言處理工具——NLTK。NLTK全稱「Natural Language Toolkit」,知名的python自然語言處理工具,誕生於賓夕法尼亞大學,以研究和教學為目的而生,因此特別適合入門學習。它提供了易於使用的接口,通過這些接口可以訪問的語料庫和詞彙資源超過50個,還有一套用於分類、標記化、詞幹標記、解析和語義推理的文本處理庫,使用起來高效方便。
  • 基於NLTK的Python自然語言處理-字符串的操作(切分)
    上篇文檔已經介紹了做自然語言處理中我們使用比較的python語言,以及使用的python集成開發環境(IDE,Integrated Development Environment )。從本篇文章將陸續介紹如何使用python進行自然語言處理。
  • 五分鐘入門Python自然語言處理(一)
    blog:https://my.oschina.net/jhao104/blog  github:https://github.com/jhao104本文簡要介紹Python自然語言處理(NLP),使用Python的NLTK庫。
  • python自然語言處理中文翻譯資料分享
    》提供了非常易學的自然語言處理入門介紹,該領域涵蓋從文本和電子郵件預測過濾,到自動總結和翻譯等多種語言處理技術。在《Python自然語言處理》中,你將學會編寫Python程序處理大量非結構化文本。你還將通過使用綜合語言數據結構訪問含有豐富注釋的數據集,理解用於分析書面通信內容和結構的主要算法。
  • PyTorch-Transformers:最先進的自然語言處理庫(附帶python代碼)
    – Sebastian Ruder想像一下我們有能力構建支持谷歌翻譯的自然語言處理(NLP)模型,並且在Python中僅需幾行代碼來完成,這聽起來是不是讓人非常興奮。而現在我們就可以坐在自己的機器前實現這個了!
  • 教你用Python進行自然語言處理(附代碼)
    自然語言處理是數據科學中的一大難題。在這篇文章中,我們會介紹一個工業級的python庫。自然語言處理(NLP)是數據科學中最有趣的子領域之一,越來越多的數據科學家希望能夠開發出涉及非結構化文本數據的解決方案。儘管如此,許多應用數據科學家(均具有STEM和社會科學背景)依然缺乏NLP(自然語言處理)經驗。
  • Python自然語言處理(NLP)入門教程
    (點擊上方公眾號,可快速關注一起學Python)作者:j_hao104   來源:http://www.spiderpy.cn/blog/detail/30本文簡要介紹Python自然語言處理NLTK是Python的自然語言處理工具包,在NLP領域中,最常使用的一個Python庫。什麼是NLP?簡單來說,自然語言處理(NLP)就是開發能夠理解人類語言的應用程式或服務。這裡討論一些自然語言處理(NLP)的實際應用例子,如語音識別、語音翻譯、理解完整的句子、理解匹配詞的同義詞,以及生成語法正確完整句子和段落。
  • 基於 Python 的簡單自然語言處理實踐
    本文是對於基於 Python 進行簡單自然語言處理任務的介紹,本文的所有代碼放置在這裡。
  • 乾貨|長文詳解自然語言處理算法xgboost和python實戰
    xgboost是大規模並行boosted tree的工具,它是目前最快最好的開源boosted tree工具包,比常見的工具包快10倍以上。在數據科學方面,有大量的kaggle選手選用它進行數據挖掘比賽,它也是大家常用的一種處理自然語言的算法,在有些情況下,xgboost甚至能夠比神經網絡的結果要好(小修之前測試過各種模型對一特定文本語言分類的性能)。
  • 數據科學家最喜歡的5個自然語言處理Python庫
    由於其特殊的特性,我們無法以一種簡單的方式處理數據,為了解決這一問題,在大數據和數據科學環境下,出現了許多技術和工具來解決這一問題。自然語言處理是人工智慧領域的前沿技術之一。它研究能實現人與計算機之間用自然語言進行有效通信的各種理論和方法。NLP的最終目標是以一種有價值的方式閱讀、破譯、理解和理解人類語言。
  • Python3自然語言處理——語言處理與Python
    《Python自然語言處理》是美國史丹福大學Steven Bird,Edward Loper和Ewan Klein編著的NLP實用書籍,該書條理清晰,
  • 中文自然語言處理相關資料集合指南
    HanLP (Java)SnowNLP (Python) Python library for processing Chinese textYaYaNLP (Python) 純python編寫的中文自然語言處理包,取名於「牙牙學語」小明NLP (Python) 輕量級中文自然語言處理工具DeepNLP (Python) Deep
  • Awesome-Chinese-NLP:中文自然語言處理相關資料
    推薦Github上一個很棒的中文自然語言處理相關資料的Awesome資源:Awesome-Chinese-NLP ,Github連結地址,點擊文末
  • 一本書精通Python自然語言處理
    NLP主要關注人機互動,它提供了計算機和人類之間的無縫交互,使得計算機在機器學習的幫助下理解人類語言。今天小編就給大家介紹一本《精通Python自然語言處理》,感受用Python是如何開發令人驚訝的NLP項目的。自然語言處理(Natural Language Processing,NLP)關注的是自然語言與計算機之間的交互。
  • 自然語言處理NLP快速入門
    【導讀】自然語言處理已經成為人工智慧領域一個重要的分支,它研究能實現人與計算機之間用自然語言進行有效通信的各種理論和方法。
  • 自然語言處理庫spaCy號稱最快句法分析器
    【IT168 評論】spaCy是Python和Cython中的高級自然語言處理庫,它建立在最新的研究基礎之上,從一開始就設計用於實際產品。spaCy帶有預先訓練的統計模型和單詞向量,目前支持20多種語言的標記。它具有世界上速度最快的句法分析器,用於標籤的卷積神經網絡模型,解析和命名實體識別以及與深度學習整合。它是在MIT許可下發布的商業開源軟體。
  • 自然語言處理背後的數據科學
    大數據文摘出品來源:medium編譯:陸震、夏雅薇自然語言處理(NLP)是計算機科學和人工智慧範疇內的一門學科。NLP是人與機器之間的溝通,使得機器既可以解釋我們的語言,也可以就此作出有效回答。本文將詳細介紹自然語言處理領域的一些算法的基本功能,包含一些Python代碼示例。開始自然語言處理之前,我們看幾個非常簡單的文本解析。標記化是將文本流(如一句話)分解為構成它的最基本的單詞的過程。例如,下面一句話:「紅狐狸跳過月球。」這句話有7個單詞。
  • 一行代碼讓你的Python運行速度提高100倍!Python真強!
    python一直被病垢運行速度太慢,但是實際上python的執行效率並不慢,慢的是python用的解釋器Cpython運行效率太差。「一行代碼讓python的運行速度提高100倍」這絕不是譁眾取寵的論調。我們來看一下這個最簡單的例子,從1一直累加到1億。
  • Python自然語言處理實踐: 在NLTK中使用斯坦福中文分詞器