2017-04-22 100 views
3

我想使用spacy作爲NLP的在線服務。 每次用戶發出請求我調用腳本「my_script.py」是否可以在內存中保留空間以減少加載時間?

與開始:

from spacy.en import English 
nlp = English() 

時遇到的問題是,這兩條線需要10秒以上,是它可以保持英語()在公羊或其他一些選項,以減少加載時間不到一秒鐘?

+2

您沒有提供足夠的上下文。這個問題更多的是關於你的在線服務的設計而不是spacy,所以請詳細說明前者。 – Leon

+1

顯示你的代碼_「用我想處理的文本調用腳本作爲參數」_,甚至更好地製作一個** MCVe **。閱讀關於閱讀,如何創建一個最小,完整和可驗證的示例:https://stackoverflow.com/help/mcve – stovfl

回答

1

你的目標應該是隻有一次初始化spacy模型。 使用一個類,並使spacy成爲一個類屬性。每當你使用它時,它就是屬性的同一個實例。

from spacy.en import English 

class Spacy(): 
     nlp = English() 
+0

我打電話給我想處理的文本作爲參數的腳本,我該怎麼做才能保持在後臺等待輸入?我想我會在這裏遇到同樣的問題。 –

+0

@LuisRamonRamirezRodriguez這不是一個理想的做法。建議的替代方案是在uwsgi服務器上運行像gunicorn/uwsgi這樣的空間運行,以及通過休息apis進行交談。或者你可以讓spacy python進程作爲芹菜的工作者運行,你可以推送同步任務並獲得同步響應。 – DhruvPathak

1

所以這裏是一個黑客做到這一點(我個人會修改我的代碼,並沒有做到這一點,但由於您的要求沒有太大闡述我要去暗示這 - )

你必須有一個運行在線服務的守護進程。在守護進程中導入spacy並將其作爲參數傳遞給執行nlp的文件。

我重構我的代碼使用由@dhruv在方案中提到的一類是乾淨多了。

下面的例子是如何去的東西的草圖。 (非常糟糕的編程原理雖然)。

File1.py

def caller(a,np): 
    return np.array(a) 

File2.py

import numpy as np 
from File1 import caller 

z=caller(10,np) 
print z 

上述方法將會對首次啓動守護程序加載時間,之後,它只是一個函數調用。 希望這有助於!

1

你這裏根本的問題是發起爲每個請求一個新的腳本。不要爲每個請求運行腳本,而是在每個請求上從腳本內運行一個函數。

有多種方式來處理用戶請求。最簡單的方法是定期輪詢請求並將其添加到隊列中。異步框架對於這類工作也很有用。

talk by raymond hettinger是一個很好的介紹併發在Python。

0

由於您使用Python可以編程某種工人(我認爲在某些時候,你需要規模也你的應用程序),其中這些初始化只進行一次!我們已經嘗試使用類似用例的Gearman,它運行良好。

乾杯

+0

原理很簡單,你的「my_script.py」將適合工作者,你將不得不編程一個服務器,它將工作負載(客戶端查詢)分配給工作人員並收集工作結果。典型的主從式架構。 –

4

你說,你要啓動一個獨立的腳本(my_script.py)每當一個請求進來,這將使用capabilites從spacy.en不加載spacy.en的開銷。通過這種方法,操作系統將在您啓動腳本時始終創建一個新進程。所以只有一種方法可以避免每次加載spacy.en:有一個單獨的進程已經在運行,加載了spacy.en,並讓腳本與該進程進行通信。下面的代碼顯示了一種方法。但是,正如其他人所說的,您可能會因更改服務器體系結構而受益,因此spacy.en已加載到您的Web服務器中(例如,使用基於Python的Web服務器)。

進程間通信的最常見形式是通過TCP/IP套接字。下面的代碼實現了一個小型服務器,它可以保持spacy.en的加載並處理來自客戶端的請求。它還有一個客戶端,它將請求發送到該服務器並返回結果。這取決於你決定將什麼放入這些傳輸中。

還有第三個腳本。由於客戶端和服務器都需要發送和接收功能,因此這些功能位於名爲comm.py的共享腳本中。 (請注意,在客戶端和服務器的每個加載的comm.py單獨副本;它們不通過加載到共享存儲器的單個模塊進行通信。)

我假定這兩個腳本在同一臺機器上運行。如果不是,則需要在兩臺計算機上放置comm.py的副本,並將comm.server_host更改爲服務器的計算機名稱或IP地址。

運行nlp_server.py作爲後臺進程(或只是在用於測試的不同的終端窗口)。這也是在等待的請求,對其進行處理並將結果發送回:

import comm 
import socket 
from spacy.en import English 
nlp = English() 

def process_connection(sock): 
    print "processing transmission from client..." 
    # receive data from the client 
    data = comm.receive_data(sock) 
    # do something with the data 
    result = {"data received": data} 
    # send the result back to the client 
    comm.send_data(result, sock) 
    # close the socket with this particular client 
    sock.close() 
    print "finished processing transmission from client..." 

server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
# open socket even if it was used recently (e.g., server restart) 
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
server_sock.bind((comm.server_host, comm.server_port)) 
# queue up to 5 connections 
server_sock.listen(5) 
print "listening on port {}...".format(comm.server_port) 
try: 
    while True: 
     # accept connections from clients 
     (client_sock, address) = server_sock.accept() 
     # process this connection 
     # (this could be launched in a separate thread or process) 
     process_connection(client_sock) 
except KeyboardInterrupt: 
    print "Server process terminated." 
finally: 
    server_sock.close() 

負載my_script.py作爲一個快速運行的腳本從NLP服務器(例如,python my_script.py here are some arguments)請求的結果:

import socket, sys 
import comm 

# data can be whatever you want (even just sys.argv) 
data = sys.argv 

print "sending to server:" 
print data 

# send data to the server and receive a result 
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
# disable Nagle algorithm (probably only needed over a network) 
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) 
sock.connect((comm.server_host, comm.server_port)) 
comm.send_data(data, sock) 
result = comm.receive_data(sock) 
sock.close() 

# do something with the result... 
print "result from server:" 
print result 

comm.py包含用於由客戶端和服務器代碼:

import sys, struct 
import cPickle as pickle 

# pick a port that is not used by any other process 
server_port = 17001 
server_host = '127.0.0.1' # localhost 
message_size = 8192 
# code to use with struct.pack to convert transmission size (int) 
# to a byte string 
header_pack_code = '>I' 
# number of bytes used to represent size of each transmission 
# (corresponds to header_pack_code) 
header_size = 4 

def send_data(data_object, sock): 
    # serialize the data so it can be sent through a socket 
    data_string = pickle.dumps(data_object, -1) 
    data_len = len(data_string) 
    # send a header showing the length, packed into 4 bytes 
    sock.sendall(struct.pack(header_pack_code, data_len)) 
    # send the data 
    sock.sendall(data_string) 

def receive_data(sock): 
    """ Receive a transmission via a socket, and convert it back into a binary object. """ 
    # This runs as a loop because the message may be broken into arbitrary-size chunks. 
    # This assumes each transmission starts with a 4-byte binary header showing the size of the transmission. 
    # See https://docs.python.org/3/howto/sockets.html 
    # and http://code.activestate.com/recipes/408859-socketrecv-three-ways-to-turn-it-into-recvall/ 

    header_data = '' 
    header_done = False 
    # set dummy values to start the loop 
    received_len = 0 
    transmission_size = sys.maxint 

    while received_len < transmission_size: 
     sock_data = sock.recv(message_size) 
     if not header_done: 
      # still receiving header info 
      header_data += sock_data 
      if len(header_data) >= header_size: 
       header_done = True 
       # split the already-received data between header and body 
       messages = [header_data[header_size:]] 
       received_len = len(messages[0]) 
       header_data = header_data[:header_size] 
       # find actual size of transmission 
       transmission_size = struct.unpack(header_pack_code, header_data)[0] 
     else: 
      # already receiving data 
      received_len += len(sock_data) 
      messages.append(sock_data) 

    # combine messages into a single string 
    data_string = ''.join(messages) 
    # convert to an object 
    data_object = pickle.loads(data_string) 
    return data_object 

注意:您應該確保從服務器發送的結果僅使用本機數據結構(字符串,列表,字符串等)。如果結果包含spacy.en中定義的對象,則客戶端在解包結果時會自動導入spacy.en,以提供對象的方法。

此設置與HTTP協議非常相似(服務器等待連接,客戶端連接,客戶端發送請求,服務器發送響應,雙方斷開連接)。所以你可能會更好地使用標準的HTTP服務器和客戶端來代替這個自定義代碼。這將是一個「RESTful API」,這是目前流行的術語(有充分的理由)。使用標準HTTP軟件包可以節省管理自己的客戶端/服務器代碼的麻煩,甚至可以直接從現有的Web服務器調用數據處理服務器,而無需啓動my_script.py。但是,您必須將您的請求轉換爲與HTTP兼容的內容,例如GET或POST請求,或者可能只是特殊格式的URL。

另一種選擇是使用標準進程間通信包,例如PyZMQ,redis,mpi4py或者zmq_object_exchanger。看到這個問題的一些想法:Efficient Python IPC

或者您可以使用dill包(https://pypi.python.org/pypi/dill)保存在磁盤上spacy.en對象的副本,然後將其在my_script.py開始恢復。這可能比每次輸入/重建都要快,並且比使用進程間通信更簡單。

+0

好的迴應matthias,這是做它的方式。 RAM本質上是易失性的且以流程爲中心,因此單個進程可以充當您的請求的代理並消除加載時間的開銷。 –

相關問題