Skip to content
Snippets Groups Projects
Select Git revision
  • master
  • logging
  • revert-00957b4f
3 results

nlp_ws

  • Clone with SSH
  • Clone with HTTPS
  • Tworzenie usług NLP

    W tym dokumencie opisano proces tworzenia usług NLP (nlpworkers).

    Automatycznie generowana dokumentacja

    Dostępna pod adresem: http://nlpworkers.pages.clarin-pl.eu/nlp_ws

    Informacje podstawowe

    Tworzenie usług NLP w ramach architektury CLARIN polega na stworzeniu klasy dziedziczącej i zdefiniowaniu kilku metod, które były abstrakcyjne. Najważniejszą z nich jest funkcja process, która przyjmuje trzy argumenty: ścieżkę do pliku wejściowego, opcje (klucz-wartość) i ścieżka do pliku wyjściowego (programista jest odpowiedzialny za stworzenie pliku o przekazanej ścieżce). Standardowo pliki wejściowe i wyjściowe znajdują się na dysku sieciowym (aktualnie używamy prot. SMB - katalog jest najczęściej podmontowywany w /samba). Parametry z którymi ma zostać wywołana funkcja process są przesyłane za pośrednictwem systemu kolejkowego Rabbit (jest on również używany do łączenia usług w potoki przetwarzania). Każda usługa NLP powinna mieć swoje repozytorium w https://gitlab.clarin-pl.eu/nlpworkers.

    Aktualnie istnieją trzy wersje wspomnianych bibliotek (zawierających wspomnianą wyżej klasę abstrakcyjną): dla języka Python, Java i C++. Niżej opisano proces tworzenia usługi w każdym z wymienionych języków.

    Tworzenie usługi w języku PYTHON

    Aby stworzyć usługę NLP w języku Python niezbędne jest zainstalowanie paczki nlp_ws (jest ona dostępna z poziomu naszego serwera PYPI: https://pypi.clarin-pl.eu). Aby ją zainstalować należy wprowadzić komendę (najlepiej w wirtualnym środowisku) pip install --index-url https://pypi.clarin-pl.eu/simple/ nlp_ws (lepszym rozwiązaniem jest umieszczenie nazwy biblioteki wraz z innymi używanymi bibliotekami w pliku requirements.txt i użycie komendy pip install --index-url https://pypi.clarin-pl.eu/simple/ -r requirements.txt).

    Niżej przedstawiono prosty przykład usługi, która kopiuje pliki z jednego katalogu do drugiego:

    """Implementetion of dummy worker."""
    
    import nlp_ws
    
    class Worker(nlp_ws.NLPWorker):
        """Class that implements example worker."""
    
        def process(self, input_file: str, task_options: dict, output_file: str) -> None:
            """Implementation of example tasks that copies files."""
            shutil.copy(input_file, output_file)
    
    
    if __name__ == '__main__':
        nlp_ws.NLPService.main(Worker)

    Jak wspomniano wyżej, najważniejszą funkcją, którą trzeba zdefiniować, jest funkcja process - w powyższym przykładzie zajmuje się ona kopiowaniem plików z katalogu input_file do output_file (plik wyjściowy jest tworzony przez komendę shutil.copy). Klasa nlp_ws.NLPWorker posiada więcej przydanych funkcji (funkcje są dobrze udokumentowane w kodzie biblioteki https://gitlab.clarin-pl.eu/nlpworkers/nlp_ws:

    • static_init - funkcja, która pozwala na inicjalizację współdzielonych zasobów - wszystko co przypiszemy do zmiennej cls zostanie spakowane (pickle) i wysłane do każdego procesu (funkcje process są uruchamiane w osobnych procesach)
    • init - inicjalizacja każdego Workera (funkcja działa w sposób zbliżony do standardowego konstruktora init
    • close - funkcja pozwalajaca na zwolnienie zarezerwowanych zasobów przez danego Workera (w związku z istnieniem garbage collectora jest ona rzadko używana
    • ...

    Podzadania nlp (od wersji 2.1)

    Biblioteka umożliwia korzytanie z podzadań nlp (klasa SubTask), które są pełnoprawnymi zapytaniami LPMN wykonywanymi wewnątrz metody process. Aby wykorzytać podzadanie należy:

    1. Zaimportować klase SubTask z biblioteki nlp_ws
    2. Włączyć obsługę podzadań, dodając SubTask.turn_on() przed definicją klasy workera
    3. W metodzie process wywołać obsługę podzadania/podzadań poprzez:
      1. Utworzenie podzadania:
      l_subtask = SubTask(input_path, ["wcrft2", "liner2"])
      1. Uruchomienie:
      l_subtask.run()
      1. Pozyskanie ścieżki do wyniku:
      t_subtask.get_output_path()

    Podzadania mogą być wywoływane:

    • wraz z definicją opcji, zadanie jako słownik, np:
      [{'tab2text':{"files":"B", "texts":"E"}}]
      gdzie:
      • 'tab2text' - nazwa narzędzia
      • {"files":"B", "texts":"E"} - opcje
    • bez definicji opcji, zadanie jako tekst, np:
      ["wcrft2"]
      gdzie:
      • "wcrft2" - nazwa narzedzia
    • po kolei, np:
      ["wcrft2", "liner2"]
      przetwarzanie dokumentu wejściowego kolejno przez narzędzia:
      • wcrft2
      • liner2

    Przykładowe użycie podzadań:

    #!/usr/bin/python
    """Implementation of dummy worker."""
    import shutil
    import logging
    import time
    from nlp_ws import NLPWorker, NLPService, SubTask
    import json
    
    log = logging.getLogger(__name__)
    
    SubTask.turn_on()
    
    
    class DummyWorker(NLPWorker):
        """Class that implements example worker with subtasks."""
    
        def subtasks(self, input_path, task_options, output_path):
            """Worker task with subtasks.
    
            :param input_path: input path
            :type input_path: str
            :param task_options: task options
            :type task_options: dict
            :param output_path: output path
            :type output_path: str
            """
            l_subtask = SubTask(input_path, ["wcrft2", "liner2"])
            l_subtask.run(blocking=False)
            t_subtask = SubTask(input_path, ["textclass"])
            t_subtask.run(blocking=False)
            self.update_progress(0.1)
            time.sleep(0.1)
            log.info("Dummy waits for results")
            l_result = l_subtask.get_output_path()
            self.update_progress(0.3)
            t_result = t_subtask.get_output_path()
            self.update_progress(0.5)
            log.info("Result paths {} {}".format(l_result, t_result))
            with open(t_result, "rt") as f:
                text = json.loads(f.read())
                log.info(text)
            self.update_progress(1)
    
        def process(self, input_path, task_options, output_path):
            """Implementation of process method inherited from NLPworker.
    
            :param input_path: input path
            :type input_path: str
            :param task_options: task options
            :type task_options: dict
            :param output_path: output path
            :type output_path: str
            """
            log.info("Processing!!!")
            self.subtasks(input_path, task_options, output_path)
    
    
    if __name__ == '__main__':
        NLPService.main(DummyWorker)

    Podzadania sent (od wersji 2.2)

    Biblioteka umożliwia korzytanie z podzadań sent (klasa Sentence), które są pełnoprawnymi zapytaniami LPMN wykonywanymi wewnątrz metody process. Aby wykorzytać podzadanie należy:

    1. Zaimportować klase Sentence z biblioteki nlp_ws
    2. Włączyć obsługę podzadań, dodając Sentence.turn_on() przed definicją klasy workera
    3. W metodzie process wywołać obsługę podzadania/podzadań poprzez:
      1. Utworzenie podzadania:
      sentences = ["My name is Jonas E. Smith. Please turn to p. 55.", "My name is Jonas E. Smith. Please turn to p. 55."]
      l_subtask = Sentence(sentences, 'fast_kgr10')
      1. Uruchomienie:
      l_subtask.run()
      1. Pozyskanie wyniku:
      t_subtask.get_results()

    Przykładowe użycie podzadań:

    #!/usr/bin/python
    """Implementation of dummy worker."""
    import shutil
    import logging
    import time
    from nlp_ws import NLPWorker, NLPService, Sentence
    import json
    
    log = logging.getLogger(__name__)
    
    Sentence.turn_on()
    
    
    class DummyWorker(NLPWorker):
        """Class that implements example worker with subtasks."""
    
        def subtasks(self, input_path, task_options, output_path):
            """Worker task with subtasks.
    
            :param input_path: input path
            :type input_path: str
            :param task_options: task options
            :type task_options: dict
            :param output_path: output path
            :type output_path: str
            """
            sentences = ["My name is Jonas E. Smith. Please turn to p. 55.", "My name is Jonas E. Smith. Please turn to p. 55."]
            l_subtask = Sentence(sentences, 'fast_kgr10')
            l_subtask.run(blocking=False)
            self.update_progress(0.1)
            time.sleep(0.1)
            log.info("Dummy waits for results")
            l_result = l_subtask.get_results()
            self.update_progress(0.3)
            log.info("Result {}".format(l_result))
            self.update_progress(1)
    
        def process(self, input_path, task_options, output_path):
            """Implementation of process method inherited from NLPworker.
    
            :param input_path: input path
            :type input_path: str
            :param task_options: task options
            :type task_options: dict
            :param output_path: output path
            :type output_path: str
            """
            log.info("Processing!!!")
            self.subtasks(input_path, task_options, output_path)
    
    
    if __name__ == '__main__':
        NLPService.main(DummyWorker)

    Sugerowana struktura projektu

       src/ - źródła aplikacji
       tests/ - testy
       main.py - skrypt wejściowy
       config.ini - plik konfuguracyjny (opisany niżej)
       Dockerfile - definicja budowanie obrazu Docker
       .gitlab-ci.yml - definicja potoku CI
       .gitignore - pliki mające być ignorowane przez system git
       tox.ini - konfiguracja narzędzia tox
       requirements.txt - wymagane biblioteki

    Plik config.ini

    Każda usługa powinna zawierać plik konfiguracyjny config.ini. Jego podstawowa wersja wygląda w następujący sposób:

    [service]
    tool = nazwa_uslugi
    
    root = /samba/requests/
    rabbit_host = addr
    rabbit_user = test
    rabbit_password = test
    
    [tool]
    workers_number = 1
    
    [logging]
    port = 9981
    local_log_level = INFO
    
    
    [logging levels]
    __main__ == INFO

    Plik ten zawiera informacje nazwie usługi (pod taką nazwą usługa będzie widoczna w systemie), ścieżce z plikami wejściowymi i wyjściowymi, login i hasło pozwalające połączyć się z systemem Rabbit i liczbę procesów, które mają zostać uruchomione w ramach działania usługi (workers_number). Ten plik można rozbudowywać dodając własne sekcje.

    Obsługa zmiennych środowiskowych

    Możliwe jest zdefiniowanie zmiennych środowiskowych które nadpiszą zmienne wprowadzone za pomocą pliku config.ini a także całkowite zrezygnowanie z pliku config.ini i zdefiniowanie wszystkich wymaganych zmiennych jako zmienne środowiskowe.

    Struktura zmiennych środowikowych:

    Zmienna powinna się rozpoczynać od opisu sekcji, jednego z poniższych:

    		CFG_S_SERV - odpowiada sekcji [service]
            CFG_S_TOOL - odpowiada sekcji [tool]
            CFG_S_LOG - odpowiada sekcji [logging]
            CFG_S_LOGLEVELS - odpowiada sekcji [logging_levels]

    Dalsza część zmiennej to _OPT_NAZWA_OPCJI, predefiniowane opcje:

    Dla sekcji service:
        CFG_S_SERV_OPT_ROOT
        CFG_S_SERV_OPT_TOOL
        CFG_S_SERV_OPT_QUEUE_PREFIX
        CFG_S_SERV_OPT_RABBIT_HOST
        CFG_S_SERV_OPT_RABBIT_USER
        CFG_S_SERV_OPT_RABBIT_PASSWORD
        CFG_S_SERV_OPT_IS_PRODUCTION
    
    Dla sekcji tool:
        CFG_S_TOOL_OPT_WORKERS_NUMBER
       
    Dla sekcji log:
        CFG_S_LOG_OPT_PORT
        CFG_S_LOG_OPT_LOGFILE_NAME
        CFG_S_LOG_OPT_LOGFILE_MAXSIZE
        CFG_S_LOG_OPT_LOGFILE_MAXBACKUPS
        CFG_S_LOG_OPT_LOG_FORMAT
        CFG_S_LOG_OPT_LOCAL_LOG_LEVEL

    Dostępne jest także definiowanie własnych zmiennych, takie zmienne powinny mieć strukture taką jak zmienne predefiniowane. Przykładowo zmienna o nazwie my_awesome_variable w sekcji service:

    CFG_S_SERV_OPT_MY_AWESOME_VARIABLE

    Wymagane zmienne środowiskowe (w przypadku braku pliku config.ini):

    CFG_S_SERV_OPT_ROOT
    CFG_S_SERV_OPT_TOOL
    CFG_S_SERV_OPT_RABBIT_HOST
    CFG_S_SERV_OPT_RABBIT_USER
    CFG_S_SERV_OPT_RABBIT_PASSWORD

    Konteneryzacja

    Każda usługa powinna posiadać plik Dockerfile, który pozwala na budowę obrazu Docker. Plik ten powinien być budowany na podstawie jednego z predefiniowanych obrazów (posiadają one informacje między innymi o naszym serwerze PYPI, czy APT (instalacja narzędzi nie wymaga żadnych specjalnych akcji). Lista obrazów bazowych jest dostępna pod adresem https://gitlab.clarin-pl.eu/dockers

    Niżej zamieszczono prosty przykład pliku Dockerfile (obraz clarinpl/cuda-python:3.6 pozwala na używanie karty graficznej - jeżeli ta funkcja jest zbędna należy użyć innego obrazu)

    FROM clarinpl/cuda-python:3.6
    
    WORKDIR /home/worker
    COPY ./src ./src
    COPY ./cmc_service.py .
    COPY ./requirements.txt .
    COPY ./models/CMC ./models/CMC
    COPY ./entrypoint.sh .
    
    RUN apt update && apt install -y g++ gdb
    
    RUN git clone https://github.com/facebookresearch/fastText.git && \
        cd fastText && \
        python3.6 -m pip install . && \
        cd .. && \
        rm -rf fastText
    
    RUN python3.6 -m pip install -r requirements.txt
    
    RUN ["chmod", "+x", "./entrypoint.sh"]
    
    CMD ["./entrypoint.sh"]
    

    Potok CI/CD

    Gitlab pozwala na tworzenie prostych potoków CI/CD (https://docs.gitlab.com/ee/ci/). Każdy nowy projekt powinien zawierać taki plik, który uruchamia sprawdzanie jakości kodu (code style), uruchamia testy, budowanie i wysylanie obrazu Docker (ten krok powinien być uruchamiany jedynie na branchu master). Niżej przedstawiono prosty przykład CI:

    image: clarinpl/python:3.6
    
    cache:
      paths:
        - .tox
    
    stages:
      - check_style
      - build
    
    before_script:
      - pip install tox==2.9.1
    
    pep8:
      stage: check_style
      script:
        - tox -v -e pep8
    
    docstyle:
      stage: check_style
      script:
        - tox -v -e docstyle
    
    build_image:
      stage: build
      image: docker:18.09.7
      only:
        - master
      services:
        - docker:18.09.7-dind
      before_script:
        - ''
      script:
        - docker build -t clarinpl/websim .
        - echo $DOCKER_PASSWORD > pass.txt
        - cat pass.txt | docker login --username $DOCKER_USERNAME --password-stdin
        - rm pass.txt
        - docker push clarinpl/websim