파이썬 멀티프로세싱 겅부중...!

더보기

import multiprocessing as mp
from multiprocessing import Pool
import spacy
import time

nlp = spacy.load("ko_core_news_lg")

def multi(text):

    start = time.time()
    print(mp.cpu_count())
    with Pool(mp.cpu_count()) as pool:
        pool_top_k_data = pool.map(multi_spacy, text)
        pool.close()
        pool.join()
    print('>>>>>>>>>>>>>>> spacy ', '__________', time.time() - start)


def multi_spacy(text):
    doc = nlp(text)
    print(doc.text)
    for token in doc:
        print(token.text, token.pos_, token.ent_type_)


def spacy(text):
    start = time.time()
    for a in text:
        doc = nlp(a)
        print(doc.text)
        for token in doc:
            print(token.text, token.pos_, token.ent_type_)
    print('>>>>>>>>>>>>>>> spacy ', '__________', time.time() - start)

 

'Study' 카테고리의 다른 글

local에서 ssh 서버에 띄운 jupyter notebook 붙기  (0) 2022.02.06
BERT 이해하기  (0) 2021.09.15
Neural Search 관련 공부  (2) 2021.07.07
머신러닝 개념 정리  (0) 2021.05.22

까먹을까봐 남겨두는 local에서 ssh 서버에 띄운 jupyter notebook 붙는 방법!

ssh 서버에 붙어서 jupyter notebook을 실행시킨 후 로컬에서 붙어보자!

터미널 창을 2개 띄운 후에

1. 로컬에서 서버로 붙기 위해 설정

- ssh -N -f -L localhost:8888:localhost:8800 root@x.x.x.x -p xxxx

- localhost:8888은 로컬 주소

- localhost:8800은 서버에서 띄운 jupyter notebook 주소

 

2. jupyter notebook 실행

- cd /'jupyter notebook 실행시킬 경로'

- jupyter notebook --allow-root --no-browser

 

3. 아래 주소 중 하나 사용해서 로컬에서 붙으면 되는줄 알았으나,,

- Or copy and paste one of these URLs:
        http://localhost:8800/?token=7d8d7482ebe6755311743735ea439cb9b570477008f17e6f
     or http://127.0.0.1:8800/?token=7d8d7482ebe6755311743735ea439cb9b570477008f17e6f

띠용? 사이트에 연결할 수 없다고 뜬다.... ㅠㅠㅠ

그러니까 꼭 잊지말고 localhost:8888으로 붙도록 하자!!

명.심.

 

'Study' 카테고리의 다른 글

multiprocessing 겅부..  (0) 2022.09.18
BERT 이해하기  (0) 2021.09.15
Neural Search 관련 공부  (2) 2021.07.07
머신러닝 개념 정리  (0) 2021.05.22

아무것도 모르는 나란인간.. BERT가 뭔지 알아보기로 했다..

BERT란?

  • Bidirectional Encoder Representations from Transformers
  • Transformer를 Bidirection한 Encoder
    • Bidirection : 양방향
    • Encoder : 입력 값을 숫자 형태로 바꾸는 모듈
  • BERT는 문맥을 양방향을 위해 숫자의 형태로 바꿔주는 딥러닝 모델
  • BERT는 Language Model
  • Pre-training 과 Fine-tuning 두 파트로 나뉘어짐
  • GPT1과 동일한 사이즈의 BERT는 GPT1 보다 높은 성능을 가짐

 

BERT Pre-Training

  • 입력 토큰들이 Position Embedding과 Segment Embedding과 더해짐
  • CLS : Classification, 분류 Task에 사용되기 위한 벡터, 문장 전체가 하나의 벡터로 표현된 스페셜 토큰
  • SEP : 두 문장으로 구성된 입력값의 경우에 사용되는 스페셜 토큰, 문장을 구분해줌 
    • WordPiece Embedding
      1. WordPiece Embedding을 사용해서 문장을 토큰 단위로 분리함
      2. WordPiece Embedding는 단순히 띄어쓰기로 토큰을 나누는 것보다 효과적으로 토큰을 분리함
    • Segment Embedding
      1. 두 개의 문장이 입력될 경우에 각각의 문장에 서로 다른 숫자를 더해주는 것
      2. 딥러닝 모델에게 두 개의 다른 문장이 있다는 것을 쉽게 알려주기 위해 사용하는 Embedding
    • Position Embedding
      1. 토큰들의 상대적 위치정보를 알려줌
      2. 딥러닝 모델은 Position Embedding으로 e1 다음에 e2, e2 다음에 e3 토큰이 위치함을 알 수 있음
      3. sin, cos 함수를 이용함
        • sin과 cos 출력 값은 입력 값에 따라 달라짐
        • 따라서 sin, cos 출력 값은 입력 값의 상대적인 위치를 알 수 있는 숫자로 사용 가능함
        • sin과 cos 출력 값은 규칙적으로 증가 또는 감소함
        • 따라서, 딥러닝 모델이 이 규칙을 사용해서 입력 값의 상대적 위치를 쉽게 계산 가능함
        • sin과 cos는 무한대 길이의 입력 값도 상대적인 위치를 출력할 수 있음
        • 어떤 위치의 입력 값이라도 -1 에서 1사이의 값을 출력하게 되어 있음
        • 상대적 위치를 사용하는 이유?
          1. 절대적 위치를 사용할 경우 최장 길이 문장을 세팅해야 함
          2. 따라서 최장 길이 문장보다 긴 학습 데이터는 사용할 수 없음

 

BERT Fine Tuning

  • 두 문장의 관계를 예측하기 위해 모델을 만드는 방법
    1. 두 개의 문장을 SEP 토큰으로 구분해서 BERT에 입력하여
    2. 력 값의 첫번째 CLS 토큰이 두 문장의 관계를 나타내도록 학습시킴

  • 문장을 분류하는 모델
    1. 문장 1개를 입력받고 CLS 토큰이 분류 값 중 하나가 되도록 학습시킴

  • QnA 예제(질의 및 응답 예제)
    1. 입력 값으로 질문과 정답이 포함된 장문을 SEP 토큰으로 구분해서 줌 
    2. BERT에 출력 값의 마지막 토큰들이 장문 속에 위치한 정답의 시작 인덱스와 마지막 인덱스를 출력하도록 학습시킴

  • 문장 속 단어를 태깅하는 예제 
    1. 각각의 입력 토큰에 대한 출력 값이 있기 때문에, 이 출력 값이 원하는 태깅으로 출력되도록 학습시킴

 

Transformer란?

  • 구글에서 공개한 Encoder, Decoder 구조를 지닌 딥러닝 모델
  • 기계번역에서 우수한 성능을 보여준 모델
  • Encoder는 입력 값을 양방향으로 처리하고
  • Decoder는 입력 값을 왼쪽에서 오른쪽으로 단방향으로 처리 함

 

Transformer 작동방식

  1. 입력 값은 먼저 Encoder에 입력됨
  2. 입력 값이 Encoder에 입력되면 각 토큰들은 포지셔널 인코딩과 더해지고
  3. 인코더는 이 값들을 행렬 계산을 통해서 한방에 Attention 벡터를 생성함
    • 벡터는 토큰의 의미를 구하기 위해서 사용됨
    • 단어하나만 보면 그 단어의 의미가 상당히 모호할 때가 많음
    • 예를 들어, 문장 전체를 보지 않고 단순히 '텍스트'라는 단어만 보면 텍스트가 신문지 속에 있는 문장들을 의미하는지, 문자메세지를 보내는 행위를 말하는 지 알기 어려움
    • 마찬가지로 메세지라는 단어만 봤을 경우 누가 말로 전하는 소식인지, 문자인지 헷갈림
  4. Attention  벡터는 fully connected layer로 전송됨
  5. 이와 동일한 과정이 6번 진행되어 그 최종 출력 값을 Transformer Decoder로 입력 값으로 사용함
    • Encoder는 모든 토큰을 한방에 계산함
    • 왼쪽에서 오른쪽으로 하나씩 읽어가는 과정이 없음
  6. Decoder는 Encoder의 출력 값과 최초 스타트 스페셜 토큰으로 작업 시작함
  7. Decoder는 왼쪽부터 오른쪽으로 순차적으로 출력 값을 생성함
  8. Decoder는 이전에 생성된 Decoder의 출력 값과 Encoder의 출력 값을 사용해서 현재의 출력 값을 생성함
  9. Decoder 역시 Attention 벡터를 만들고 fully connected layer로 전송하는 작업을 6번 진행함
  10. Decoder는 end토큰이라는 스페셜 토큰을 출력할 때까지 반복함

 

BERT가 양방향 Encoder를 취하게 된 이유?

  • GPT1으로부터 시작됨
    • GPT1이란?
    • 2018년에 openAI 에서 Transformer Decoder 구조를 사용해서 만든 자연어 처리 모델
    • GPT1은 Generative training으로 학습된 Language 모델이 얼마나 자연어 처리 능력이 우수한지 보여준 우수한 모델
    • 기본적으로 문장을 데이터로 사용함
    • 단어를 하나씩 읽어가면서 다음 단어를 예측하는 방법으로 학습됨
    • 이런 학습 방식은 별도의 라벨링 작업이 필요하지 않아 비지도 학습임
    • 한 문장만 가지고도 여러 학습 데이터를 만드는 것이 가능함
    • 현재 위치의 단어 다음에 위치한 단어를 예측하는 방식으로 학습됨
  • 구글은 질의 및 응답 영역은 문맥이해 능력이 상당히 중요한데, 단순히 왼쪽에서 오른쪽으로 읽어나가는 방식으로는 문맥이해의 약점이 있을 수 있다고 지적함
  • 이에 단순히 왼쪽에서 오른쪽으로 읽어나가는 Decoder보다 양방향으로 문맥을 이해할 수 있는 Encoder를 활용한 Language 모델을 BERT라는 이름으로 발표함

 

BERT vs GPT

  • BERT
    1. 양방향 LM
    2. Fine Tuning을 하도록 만들어짐
  •  GPT
    1. 단방향 LM
    2. Fine Tuning을 하지 않도록 만들어짐

 

글에 문제가 있으면 댓글로 알려주시면 감사하겠습니다!

그럼 이만.!

출처

1. https://www.youtube.com/watch?v=30SvdoA6ApE (허민석님의 [딥러닝 자연어처리]BERT이해하기)

2. Jacob Devlin, Ming-Wei Chang, Kenton Lee, Kristina Toutanova, "BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding"(https://arxiv.org/pdf/1810.04805.pdf)

'Study' 카테고리의 다른 글

multiprocessing 겅부..  (0) 2022.09.18
local에서 ssh 서버에 띄운 jupyter notebook 붙기  (0) 2022.02.06
Neural Search 관련 공부  (2) 2021.07.07
머신러닝 개념 정리  (0) 2021.05.22

LegalQA에 대해 알아가는 중에 공부한 내용을 끄적끄적 기록하려고 한다.

LegalQA : https://github.com/haven-jeon/LegalQA

 

1. SentenceKoBART에 기반한 법적 QA시스템

그렇담 SentenceKoBART는 무엇인지 알아야 한다.

 

* SentenceKoBART란?

- Sentence-BERT에 기반하여 KorSTS, KLUE STS Dataset을 사용하여 Fine tuning한 모델이다.

 

Sentence-BERT란 또 무엇인가. 알아야 할 게 너무나 많다 ㅠㅠ

LegalQA 시스템에 대해 알기 위해 Sentence-BERT에 대해 먼저 알아보자.

 

* Sentence-BERT란?

- cosine-similarity 사용하여 비교할 수 있는 의미론적으로 의미있는 문장 임베딩을 도출하기 위해 Siamese and triplet network 구조를 사용하는 pretained BERT 네트워크의 변형이라고 한다....

 

그렇다고 한다.. 자세하게 알아보기 위해 아래에서 구글과 파파고의 도움으로 논문을 열심히 파헤쳐보겠다.

- 논문 전문: https://arxiv.org/pdf/1908.10084.pdf 

 

Introduction


- Siamese과 triplet network 구조를 적용함으로써 기존에 BERT가 하지 못했던 "large-scale semantic similarity comparison, clustering, and information retrieval via semantic search" 태스크에 사용할 수 있었다.

- BERT에 두 개의 문장이 transformer network에 전달되고 target value가 예측된다.

- clustering 및 semantic search 처리하는 일반적인 방법은 의미가 유사한 각 문장을 벡터 공간상에 가깝도록 맵핑하는 것이다.

- BERT에 개별 문장을 입력하고 고정된 크기의 문장 임베딩을 도축하기 시작했는데, 가장 일반적인 방법은 output layer을 평균을 내거나 첫 번째 토큰([CLS] 토큰)의 출력을 사용하는 것이다. 이 방법은 GloVe 임베딩에 평균을 내는 것보다 안좋다.

- 그래서 나온 것이 SBERT(Sentence-BERT)이다.

- siamese network architecture를 사용하면 입력 문장에 대해 고정 된 크기의 벡터를 유도할 수 있다.

- cosine similarity, Manhatten / Euclidean distance와 같은 유사도 측정 방법을 사용하면 의미적으로 유사한 문장을 찾을 수 있다.

- NLI 데이터로 SBERT를 fine tuning 하여 InferSent 및 Universal Sentence Encoder와 같은 SOTA sentence embedding methods보다 성능을 높였다.

 

 

- 공부하는데 참고한 블로그

1) https://velog.io/@ysn003/%EB%85%BC%EB%AC%B8-Sentence-BERT-Sentence-Embeddings-using-Siamese-BERT-Networks2)

 

[논문] Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks

사단법인 한국인공지능연구소에서 진행하는 오픈랩 프로그램이 7기로 참여했고, 어찌어찌 NLP 분야의 팀에서 활동을 하게 되었다. 여기서 BERT 공부를 다 끝내면 한 번 읽어보라고 논문을 추천받

velog.io

2) http://mlgalaxy.blogspot.com/2020/09/sentence-bert-sentence-embeddings-using.html

 

논문 설명 - Sentence-BERT : Sentence Embeddings using Siamese BERT-Networks

  논문 정보 링크 :  https://arxiv.org/abs/1908.10084 EMNLP 2019 Introduction BERT 네트워크에 siamese, triplet 구조를 적용했다. 이로 인해 기존의 BERT가 하지 못했던 large-sc...

mlgalaxy.blogspot.com

 

 

2. Nerual Search Engine인 "Jina" 사용

* Jina란?

- 신경망 기반 검색 애플리케이션을 설계하고 구현하기 위한 오픈소스 신경 검색 프레임워크(open-source neural search framework)!

* Jina에서 사용하는 Flow(FinBERT-QA에서 사용한 예시)

1. Index Flow

출처 : https://towardsdatascience.com/how-to-build-a-production-ready-financial-question-answering-system-with-jina-and-bert-48335103043f

- Index Flow의 기본 아이디어는 사전 훈련된 BERT 모델을 사용하여 모든 answer 구절을 Embedding하여 인코딩한 다음 Query Flow에서 검색할 수 있도록 Embedding 한 것을 Indexing 하는 것이다.

 

1-1. Define our data

- 사용하고자 하는 데이터를 인덱싱하기 위해서는 Jina data type인 Document로 정의 해야 한다.

- app.py 파일에는 검색 애플리케이션으로 구성한다.

- config 함수에서 데이터 경로를 다음과 같이 설정한다.

def config():
	os.environ['JINA_DATA_PATH'] = 'dataset/test.csv'

 

- config 함수 생성 후에 answer 구절에 해당하는 ID와 텍스트를 포함하도록 Document 정의한다.

from jina import Document

def index_generator():
    import csv
    data_path = os.path.join(os.path.dirname(__file__), os.environ['JINA_DATA_PATH'])

    # Get Document and ID
    with open(data_path) as f:
        reader = csv.reader(f, delimiter='\t')
        for data in enumerate(reader):
            d = Document()
            d.tags['id'] = int(data[0])
            d.text = data[1]
            yield d

 

1-2. Endocing our data to embeddings

- Answer Passage를 인코딩한다.

- Jina는 Drivers를 사용하여 Executor용 데이터를 translate 하므로, 익숙한 데이터 유형(예: 텍스트, 이미지, np, 배열 등)만 사용하면 된다.

- 인코딩 단계에서 Driver는 Document를 Byte 단위로 수신하여 Document를 해석하고, Document의 텍스트를 인코더에 전달한다.

- 인코더가 해당 텍스트에 대한 Embedding을 출력한 후, 동일한 Driver가 Embedding을 다시 해석하여 Document에 추가한다.

 

Indexing에 앞서 Pea와 Pod에 관해 알아보자.

더보기

- Pea : Executor는 데이터를 처리할 수 있는 Driver가 필요하기 때문에 둘 다 Flow에서 마이크로 서비스의 필수 구성 요소이다. 따라서 우리는 Encoder Microservice를 얻기 위해 Pea를 사용하여 Executor와 Driver를 함께 wrapping 한다. Pea는 게이트웨이 또는 Flow의 다른 Peas에서 들어오는 메시지를 지속적으로 수신 대기하고 메시지를 수신할 때 Driver를 호출하는 마이크로 서비스이다.

 

- Pod : 신경 검색 애플리케이션을 최적화하기 위해 Jina는 기본적으로 병렬화를 제공한다. 단일 인코더를 사용하는 대신 여러 프로세스로 분할할 수 있다. multiple Encoder microservice가 하나의 Encoder처럼 기능적으로 작동하도록 하기 위해 Peas 그룹을 Pod에 wrapping 한다. Pod는 load balancing, further control, context management를 담당하는 microservice 그룹이다.

- 이 디자인의 장점은 Pod가 로컬 호스트 또는 네트워크를 통해 서로 다른 컴퓨터에서 실행될 때, 애플리케이션을 분산시키고, 효율적이며 확장 가능하다는 것이다.

- Jina가 제공하는 building block을 사용하여 아래 2가지를 할 수 있다.

   1) design an Index Flow

   2) create an Encoder Pod with two simple YAML files   

   

1. Create a Pod for the Encoder

   - 먼저 pods 폴더 안에 encode.yml 파일을 생성한 후, Jina Hub에서 사용할 Encoder이름을 지정한다.

!사용할 인코더 이름
with:
  pool_strategy: auto
  pretrained_model_name_or_path: model/모델이름
  max_length: 512

 

2. Add the Encoder to the Index Flow

  - 위에서 Encoder가 준비되었으므로, Index Flow를 만들어 본다.

  - flows 폴더 안에 index.yml 파일을 생성한 후, Index Flow에서 첫 번째 Pod를 지정한다.

  - 매개변수를 사용하여 Encoder를 분할할 프로세스의 수를 지정할 수 있다.

!Flow
pods:
  encoder:
    uses: pods/encode.yml
    parallel: $JINA_PARALLEL
    timeout_ready: 600000
    read_only: true

 

1-3. Index our answer collection

- answer에 대한 Embedding을 얻은 후 Query time에 검색할 수 있도록 데이터를 저장하기 위해 Indexer라는 또 다른 Executor를 만든다.

- 이전 단계와 유사하게 Driver는 Document를 수신하고 docid, doc 및 Embedding을 Indexer한테 전달한다.

- Jina Hub의 (1)Vector 와 (2)Key-Value Indexer를 모두 사용하여 단일 Indexer 역할을 하는 복합 Indexer을 사용한다.

 

(1)Vector Indexer : k-nearest neighbors algorithm을 사용하여 가장 가까운 answer Embedding을 검색하기 위해 answer Embedding을 저장하고 question Embedding으로 query된다.(뭐라고 해석해야 할 지 모르겠다ㅠㅠ..)

(2)Key-Value(KV) Indexer : Document Data(텍스트, blob, 메타데이터)를 저장하고, docid(일반적으로 벡터 인덱서에서 추출)로 쿼리하여 answer id 및 텍스트와 같은 데이터 정보를 검색한다.

 

- 검색 애플리케이션에서 인덱스 흐름을 사용하는 방법을 살펴보자. app.py에서 config 함수에서 parallel을 변경하여 각 Pod에 대해 각 마이크로 서비스를 분할하려는 Peas(프로세스) 수를 나타낼 수 있다. 인덱싱 단계에서 parallelization를 나타내기 위해 shard를 변경할 수도 있다. 아래 코드에서는 둘 다 변경되지 않은 상태로 둔다. 이것은 각 Pod에 하나의 Pea만 가질 것임을 의미한다.

- Step1. Define our data에서 추가한 index_generator 함수 이후에,  먼저 Flows/index.yml 에서 생성한 Index Flow를 로드하고 index_generator의 입력 Document를 플로우로 전달하는 index 함수를 추가한다. answer 구절을 임베딩으로 인코딩하기 위해 batch_size=16을 설정한다.

from jina.flow import Flow

def index():
    f = Flow.load_config('flows/index.yml')

    with f:
        f.index(input_fn=index_generator, batch_size=16)

 

- Data를 Index할 준비가 되었으니 실행을 시켜보자.

python app.py index

 

Driver와 Indexer를 Pea 안에서 wrapping하고, Peas를 Pod로 그룹화하고, YAML 파일을 사용하여 정의한다.

너무 길어서 숨겨두었으니 "더보기"를 눌러서 좀 더 확인해보쟈.

더보기

1. Create a Pod for the Indexer

- 먼저 pods 폴더 안에 doc.yml 파일을 생성한 후, NumpyIndexer인 Vector Indexer와 BinaryPbIndexer인 KV Indexer로 구성된 복합 Indexer인 CompoundIndexer 를 정의한다.

- 인덱싱된 데이터는 vec.gz와 doc.gz에 각각 저장된다.

- workspace는 인덱스가 저장될 디렉토리이다.

!CompoundIndexer
components:
  - !NumpyIndexer
    with:
      index_filename: vec.gz
    metas:
      name: vecidx
      workspace: $WORKDIR
  - !BinaryPbIndexer
    with:
      index_filename: doc.gz
    metas:
      name: docidx
      workspace: $WORKDIR
metas:
  name: doc_compound_indexer
  workspace: $WORKDIR

 

2. Add the Indexer to the Index Flow

  - flows/index.yml로 돌아가서 Index Flow에 doc_indexer로 Indexer를 추가한다.

  - 만약 데이터가 크다면, 최적화를 위해 애플리케이션에 sharding을 추가할 수 있다.

  - app.py에서 환경 변수로 사용된다.

!Flow
pods:
  encoder:
    uses: pods/encode.yml
    parallel: $JINA_PARALLEL
    timeout_ready: 600000
    read_only: true
  doc_indexer:
    uses: pods/doc.yml
    shards: $JINA_SHARDS
    separated_workspace: true

 

Jina의 Flow API를 사용하여 Index Flow를 시각화 할 수 있다.

from jina.flow import Flow

f = Flow.load_config('flows/index.yml')
f.plot()

 

2. Query Flow

출처 : https://towardsdatascience.com/how-to-build-a-production-ready-financial-question-answering-system-with-jina-and-bert-48335103043f

- 데이터를 인덱싱한 후 Query Flow를 만들어야 한다. Query Flow의 기본 아이디어는 동일한 BERT 기반 모델을 사용하여 주어진 질문을 임베딩으로 인코딩하고 인덱서를 사용하여 가장 유사한 답변 임베딩을 검색하는 것이다. 검색 결과를 더욱 향상시키기 위해 저자의 순위 재지정 기술을 사용한다. 따라서 FinBERT-QA를 사용하여 Jina가 반환한 답변 일치의 점수를 다시 계산하는 또 다른 순위 변경 단계를 추가해야 한다.

 

2-1. Encode the query to an embedding

- 사용자가 입력한 질문(Question) 텍스트를 Jina의 Document 타입으로 정의한다.

- Query Flow에 Encoder 추가한다. Index Flow와 마찬가지로 동일한 Encoder를 사용하여 질문을 Encoding한다.

- Query Flow에서 pods/encode.yml의 동일한 Encoder를 사용할 수 있다. flows 폴더에 query.yml 파일을 만들고 여기에 Encoder Pod를 추가한다.

!Flow
with:
  read_only: true
pods:
  encoder:
    uses: pods/encode.yml
    parallel: $JINA_PARALLEL
    timeout_ready: 600000
    read_only: true

 

2-2. Find the most similar similar matches from the Index

- 질문을 인코딩한 후 질문 임베딩은 드라이버에 의해 문서에 추가된다.

- 그런 다음 이 문서는 다음 Pod의 인덱서로 전송되고 Driver는 질문 임베딩을 인덱서로 전달한다.

- 그런 다음 인덱서는 k-nearest neighbors algorithm을 사용하여 가장 유사한 임베딩이 포함된 답변을 검색하고 Document에 추가할 Driver에 Top-k answer 일치 목록을 전달한다. 

- 일치 항목에는 docid, doc 및 일치하는 match score와 같은 데이터가 포함된다.

- Index Flow에서 동일한 Indexer를 사용하고 있으므로 Indexer Pod를 flow/query.yml 에 추가하면 된다.

!Flow
with:
  read_only: true
pods:
  encoder:
    uses: pods/encode.yml
    parallel: $JINA_PARALLEL
    timeout_ready: 600000
    read_only: true
  doc_indexer:
    uses: pods/doc.yml
    shards: $JINA_SHARDS
    separated_workspace: true
    polling: all
    uses_reducing: _merge_all
    timeout_ready: 100000

 

2-3. Rerank the scores of the matches

- Indexer가 이 시점에서 Top-k개 answer 일치 항목을 반환하고 더 나은 결과를 얻기 위해 match score를 다시 계산한다고 가정해보자. Jina에는 Rankers라는 Executors class가 있는데, 특히 Match2DocRankers는 new score를 계산하여 쿼리의 match score를 다시 매긴다. Jina Hub의 Rankers를 보면, Levenshtein Ranker는 Levenshtein distance를 사용하여 match score를 다시 계산한다.

- 그러나 disteance matric을 사용하여 점수를 다시 계산하는 대신, Ranker에 fine-tuned(미세 조정된) BERT 모델인 FinBERT-QA를 로드하고 binary classification task에 대한 입력으로 질문과 현재 일치 답변의 연결을 사용하여 점수를 다시 계산한다.

- 여기서 주요 아이디어는 FinBERT-QA에서 계산한 relevancy score를 기준으로 재정렬된 일치 목록을 반환하기 위해 query text와 일치 항목(answer text 및 match score 포함)을 랭커에 전달하는 것이다. Driver는 다시 정렬된 목록을 기반으로 Document의 일치 항목을 업데이트한다.

** Build a Custom Executor("더보기"를 눌러보시오)**

더보기

1) Set up

- Executor type 중 4번 Ranker를 선택한다.

jina hub new

 

2) Fill in the logit for reranking

- __init__.py 파일에 아래와 같은 로직을 구현한다.

 from jina.executors.rankers import BaseRanker
            
 class FinBertQARanker(BaseRanker):
     """
     :class:`FinBertQARanker` recomputes match scores using FinBERT-QA.
     """
        
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         # your customized __init__ below
         raise NotImplementedError
        
     def score(self, *args, **kwargs):
         raise NotImplementedError

 

- Jina는 다양한 Executor 클래스를 포함하고 있으며, 여기서 사용할 기본 Ranker 클래스는 Match2DocRankers라고 하며, 일치 점수를 다시 계산하는 기능이 있다.

- 먼저 BaseRanker의 기본 클래스를 Match2DocRanker로 변경한다. 또한 Jina와 현재 디렉토리를 정의할 뿐만 아니라 필요한 다른 모듈을 사용하여 PyTorch를 가져온다.

 import os
 from typing import Dict
 import numpy as np
    
 from jina.executors.devices import TorchDevice
 from jina.executors.rankers import Match2DocRanker
    
 cur_dir = os.path.dirname(os.path.abspath(__file__))

 

 

- 로직은 Jina의 TorchDevice 및 Match2DocRanker를 사용하는 FinBertQARanker 클래스에서 구현된다. 나중에 Dockerfile에 필요한 모델을 다운로드한다.

- models 폴더에 (1) bert-qa 및 (2) 2_finbert-qa-50_512_16_3e6.pt 라는 두 가지 모델이 있다고 가정해보자.

(1) bert-qa : BERT를 사용한 Passage Re-ranking의 MS 매크로 데이터 세트에서 미세 조정된 bert-base-uncased

(2) 2_finbert-qa-50_512_16_3e6.pt: FinBERT-QA 모델 - FiQA 데이터 세트에서 미세 조정된 bert-qa

- 먼저 초기화에 사용할 사전 훈련된 모델로 bert-qa/를 지정하고

- QA relevancy score를 계산하는 데 사용할 모델로 2_finbert-qa-50_512_16_3e6.pt를 지정하고

- QA의 최대 시퀀스 길이를 지정한다.

class FinBertQARanker(TorchDevice, Match2DocRanker):
     """
     :class:`FinBertQARanker` Compute QA relevancy scores using a fine-tuned BERT model.
     """
    
     required_keys = {"text"}
    
     def __init__(
             self,
             pretrained_model_name_or_path: str = os.path.join(cur_dir, "models/bert-qa"),
             model_path: str = os.path.join(cur_dir, "models/2_finbert-qa-50_512_16_3e6.pt"),
             max_length: int = 512,
             *args, **kwargs):
         """
         :param pretrained_model_name_or_path: the name of the pre-trained model.
         :param model_path: the path of the fine-tuned model.
         :param max_length: the max length to truncate the tokenized sequences to.
         """
    
         super().__init__(*args, **kwargs)
         self.pretrained_model_name_or_path = pretrained_model_name_or_path
         self.model_path = model_path
         self.max_length = max_length

 

- 그런 다음 Hugging Face transformer을 사용하여 binary classification task에 대한 모델을 로드하기 위해 클래스에 post_init 함수를 추가한다. evaluation mode에서 모델을 설정해야 한다.

     def post_init(self):
         super().post_init()
         import torch
         from transformers import BertForSequenceClassification, AutoTokenizer
    
         self.device = torch.device("cpu")
         self.tokenizer = AutoTokenizer.from_pretrained(self.pretrained_model_name_or_path, do_lower_case=True)
         self.model = BertForSequenceClassification.from_pretrained(self.pretrained_model_name_or_path, cache_dir=None, num_labels=2)
         self.model.load_state_dict(torch.load(self.model_path, map_location=self.device), strict=False)
         self.to_device(self.model)
         self.model.eval()

 

-  _get_score 함수를 구현하여 질문의 relevancy score와 Top-k개 매칭된 답변을 각각 계산한다. 먼저 질문과 각 top-k 답변을 연결하고 변환하여 tokenizer를 사용하여 모델에 필요한 입력( input_ids, token_type_ids, att_mask)을 얻기 위해 인코딩한다.

- 그런 다음 input을 모델에 입력하고 QA 쌍이 관련이 있다는 prediction score를 얻는다(label = 1). prediction score를 0과 1 사이의 확률로 변환하기 위해 점수에 softmax 함수를 적용한다. output은 QA 쌍에 대한 확률 형태의 relevancy score가 된다.

     def _get_score(self, query, answer):
         import torch
         from torch.nn.functional import softmax
    
         # Create input embeddings for the model
         encoded_seq = self.tokenizer.encode_plus(query, answer,
                                             max_length=self.max_length,
                                             pad_to_max_length=True,
                                             return_token_type_ids=True,
                                             return_attention_mask=True)
         # Numericalized, padded, clipped seq with special tokens
         input_ids = torch.tensor([encoded_seq['input_ids']]).to(self.device)
         # Specify which position of the embedding is the question or answer
         token_type_ids = torch.tensor([encoded_seq['token_type_ids']]).to(self.device)
         # Specify which position of the embedding is padded
         att_mask = torch.tensor([encoded_seq['attention_mask']]).to(self.device)
         # Don't calculate gradients
         with torch.no_grad():
             # Forward pass, calculate logit predictions for each QA pair
             outputs = self.model(input_ids, token_type_ids=token_type_ids, attention_mask=att_mask)
         # Get the predictions
         logits = outputs[0]
         # Apply activation function to get the relevancy score
         rel_score = softmax(logits, dim=1)
         rel_score = rel_score.numpy()
         # Probability that the QA pair is relevant
         rel_score = rel_score[:, 1][0]
    
         return rel_score

 

- 마지막으로, 사용자와 Jina의 match score를 입력으로 가져와서 _get_scores를 사용하여 new score를 다시 계산하는 scoring function 를 만든다.

     def score(
             self, query_meta: Dict, old_match_scores: Dict, match_meta: Dict
     ) -> "np.ndarray":
    
         new_scores = [
             (
                 match_id,
                 self._get_score(query_meta['text'], match_meta[match_id]['text']),
              )
             for match_id, old_score in old_match_scores.items()
         ]
         return np.array(
             new_scores
             ,
             dtype=[(self.COL_MATCH_ID, np.int64), (self.COL_SCORE, np.float64)],
         )

 

3) Write a Unit Test

- 새로운 Executor를 생성하고 Jina Hub API를 사용하여 Docker 이미지를 빌드하려면 단위 테스트를 작성해야 한다. tests/test_finbertqaranker.py에서 이에 대한 템플릿을 찾을 수 있다. 쿼리가 주어진 두 개의 답변 일치에 대한 관련성 확률을 계산하고 FinBertQARanker가 우리의 예상과 동일한 점수를 계산하는지 확인하기 위해 간단한 검사를 작성한다.

 import copy
 import json
 import numpy as np
 import torch
 from torch.nn.functional import softmax
 from transformers import BertTokenizer, BertForSequenceClassification
    
 from jina.executors.rankers import Match2DocRanker
    
 from .. import FinBertQARanker
    
 def test_finbertqaranker():
     """here is my test code
    
     https://docs.pytest.org/en/stable/getting-started.html#create-your-first-test
     """
     query_meta = {"text": "Why are big companies like Apple or Google not included in the Dow Jones Industrial "
                           "Average (DJIA) index?"}
     query_meta_json = json.dumps(query_meta, sort_keys=True)
     old_match_scores = {1: 5, 2: 7}
     old_match_scores_json = json.dumps(old_match_scores, sort_keys=True)
     match_meta = {1: {"text": "That is a pretty exclusive club and for the most part they are not interested in "
                               "highly volatile companies like Apple and Google. Sure, IBM is part of the DJIA, "
                               "but that is about as stalwart as you can get these days. The typical profile for a "
                               "DJIA stock would be one that pays fairly predictable dividends, has been around since "
                               "money was invented, and are not going anywhere unless the apocalypse really happens "
                               "this year. In summary, DJIA is the boring reliable company index."},
                   2: {"text": "In  most  cases  you  cannot  do  reverse  lookup  on  tax  id  in  the  US.  You  can "
                               " verify ,  but  for  that  you  need  to  have  more  than  just  the  FEIN/SSN.  You  "
                               "should  also  have  a  name ,  and  some  times  address.  Non-profits ,  specifically "
                               ",  have  to  publish  their  EIN  to  donors ,  so  it  may  be  easier  than  others  "
                               "to  identify  those.  Other  businesses  may  not  be  as  easy  to  find  just  by  "
                               "EIN."}}
     match_meta_json = json.dumps(match_meta, sort_keys=True)
    
     pretrained_model = 'models/bert-qa'
     model_path = "models/2_finbert-qa-50_512_16_3e6.pt"
    
     ranker = FinBertQARanker(pretrained_model_name_or_path=pretrained_model, model_path=model_path)
    
     new_scores = ranker.score(
         copy.deepcopy(query_meta),
         copy.deepcopy(old_match_scores),
         copy.deepcopy(match_meta)
     )
    
     # new_scores = [(1, 0.7607551217079163), (2, 0.0001482228108216077)]
     np.testing.assert_approx_equal(new_scores[0][1], 0.7607, significant=4)
     np.testing.assert_approx_equal(round(new_scores[1][1], 4), 0.0001, significant=4)
    
     # Guarantee no side-effects happen
     assert query_meta_json == json.dumps(query_meta, sort_keys=True)
     assert old_match_scores_json == json.dumps(old_match_scores, sort_keys=True)
     assert match_meta_json == json.dumps(match_meta, sort_keys=True)

 

4) Add Requirements

- Jina 외에도 FinBertQARanker용 PyTorch 및 transformer를 사용하고 있으므로 FinBertQARanker/requirements.txt에 추가한다.

torch==1.7.1
transformers==4.0.1

 

** Dockerfile 관련해서는 아래 내용 참고!

5) Prepare Dockerfile

- Dockerfile을 아래 내용으로 변경하면 모델이 models/이라는 폴더에 다운로드된다.

 

6) Build Docker image with Jina Hub API

- FinBertQARanker를 Docker 이미지로 빌드할 준비가 완료된 후, 작업 디렉토리에 아래 내용을 입력한다.

--pull : 로컬이 아닌 경우 Jina 기본 이미지를 다운로드한다.

--test-uses : 빌드된 이미지가 Jina의 Flow API를 통해 성공적으로 테스트 실행될 수 있는지 확인하는 추가 테스트를 추가한다.

--timeout-ready : post_init 함수에 모델을 로드할 시간을 준다.

5) Prepare Dockerfile

- Dockerfile을 아래 내용으로 변경하면 모델이 models/이라는 폴더에 다운로드된다.

FROM jinaai/jina
    
 RUN apt-get update && \
     apt-get -y install wget && \
     apt-get -y install unzip
    
 COPY ./requirements.txt /
 RUN pip install -r requirements.txt
 RUN pip uninstall -y dataclasses
 RUN pip install pytest
    
 RUN wget https://www.dropbox.com/s/sh2h9o5yd7v4ku6/bert-qa.zip
 RUN wget https://www.dropbox.com/s/12uiuumz4vbqvhk/2_finbert-qa-50_512_16_3e6.pt
    
 # setup the workspace
 COPY  . /workspace
 WORKDIR /workspace
    
 RUN if [ ! -d models ]; then \
     mkdir models/ && cd models/ && \
     mv /bert-qa.zip                   . && \
     mv /2_finbert-qa-50_512_16_3e6.pt . && \
     mkdir bert-qa && \
     unzip bert-qa.zip -d bert-qa/; \
 fi
    
 RUN pytest
    
 ENTRYPOINT ["jina", "pod", "--uses", "config.yml"]

 

6) Build Docker image with Jina Hub API

- FinBertQARanker를 Docker 이미지로 빌드할 준비가 완료된 후, 작업 디렉토리에 아래 내용을 입력한다.

jina hub build FinBertQARanker/ --pull --test-uses --timeout-ready 60000

--pull : 로컬이 아닌 경우 Jina 기본 이미지를 다운로드한다.

--test-uses : 빌드된 이미지가 Jina의 Flow API를 통해 성공적으로 테스트 실행될 수 있는지 확인하는 추가 테스트를 추가한다.

--timeout-ready : post_init 함수에 모델을 로드할 시간을 준다.

 

1) Create a custom Ranker Pod

- 사용자 지정 Ranker인 FinBertQARanker를 사용하려면 먼저 Ranker에 대한 새 Pod를 만든다. pods 폴더에 rank.yml 파일을 생성한 후 FinBertQARanker/config.yml의 내용을 pods/rank.yml로 복사한다.

- Executor는  FinBertQARanker/__init__.py에서 구현한 로직을 사용하도록 Query Flow에 지시한다. 이 구현을 위한 코드는 Docker 이미지의 workspace 폴더 안에 로드되므로 __init__.py 앞에 workspace/를 추가한다.

- 지금까지 사용한 Encoder 및 Indexer Executor는 모두 Pod의 기본 Driver를 사용한다. Custom Executor를 생성했으므로 Ranker Pod에 사용할 Driver를 알려야 합니다. 이 경우 Match2DocRanker 기본 Ranker 클래스에 Matches2DocRankDriver를 사용한다.

!FinBertQARanker
with:
    {}
metas:
    py_modules:
    - workspace/__init__.py
    # - You can put more dependencies here
requests:
    on:
    [ SearchRequest ]:
        - !Matches2DocRankDriver { }

 

2) Use Custom Ranker in the Query Flow

- 다른 Executor Pod와 마찬가지로 doc_indexer 뒤에 ranker를 추가하고 태그 이름 앞에 docker:// 접두사를 지정하여 방금 생성한 Docker 이미지와 Ranker Pod를 사용하도록 Query Flow에 지시한다.

!Flow
with:
  read_only: true
pods:
  encoder:
    uses: pods/encode.yml
    parallel: $JINA_PARALLEL
    timeout_ready: 600000
    read_only: true
  doc_indexer:
    uses: pods/doc.yml
    shards: $JINA_SHARDS
    separated_workspace: true
    polling: all
    uses_reducing: _merge_all
    timeout_ready: 100000
  ranker:
    uses: docker://jinahub/pod.ranker.finbertqaranker:0.0.1-0.8.23
    uses_internal: pods/rank.yml
    parallel: $JINA_PARALLEL
    timeout_ready: 600000

 

- Docker 이미지의 태그 이름은 현재 Jina 릴리스에 따라 변경될 수 있기 때문에 그때마다 태그 이름을 변경해야 한다.

- Flow API를 사용하여 쿼리 흐름을 다시 시각화할 수 있다.

from jina.flow import Flow

f = Flow.load_config('flows/query.yml')
f.plot()

 

- 여기에서 Encoder인 encoder 및 Indexer인 doc_indexer 및 Ranker인 ranker를 포함하는 세 개의 Pod가 있는 Query Flow을 볼 수 있다.

- Query Flow이 끝나면 Ranker Pod의 Driver가 Custom Ranker인 FinBertQARanker가 계산한 확률을 기반으로 Document의 일치 항목을 재정렬된 일치 목록으로 변경한다.

 

2-4. Get matches and scores

* Build a Search Application

- 최종 일치 항목과 relevancy 확률이 Document에 저장되어 있기 때문에 app.py에서 사용자 입력의 질문에 대한 응답을 출력하는 함수를 작성한다.

- Document, d.matches에서 일치 항목을 반복하고 점수 값과 일치하는 답변 텍스트를 출력한다.

def print_resp(resp, question):
    for d in resp.search.docs:
        print(f"🔮 Ranked list of answers to the question: {question}: \n")

        for idx, match in enumerate(d.matches):
            score = match.score.value
            answer = match.text.strip()
            print(f'> {idx+1:>2d}. "{answer}"\n Score: ({score:.2f})')

 

- 그런 다음 flows/query.yml의 Query Flow을 사용하는 검색 방법을 작성하고 사용자 입력을 print_resp에 전달한다.

- f.search_lines()에서 input을 사용자 쿼리로 지정하고 output을 출력할 응답으로 지정하며 검색하려는 top-k개 답변을 지정한다.

- f.search_lines()의 멋진 점은 사용자 쿼리에 대한 문서를 자동으로 생성한다는 것이다.

def search():
    f = Flow.load_config('flows/query.yml')

    with f:
        while True:
            text = input("Please type a question: ")
            if not text:
                break

            def ppr(x):
                print_resp(x, text)

            f.search_lines(lines=[text, ], output_fn=ppr, top_k=50)

 

 

- 공부하는데 참조한 곳

1) https://towardsdatascience.com/how-to-build-a-production-ready-financial-question-answering-system-with-jina-and-bert-48335103043f

 

 

내용에 문제가 발견되면 알려주십시오!

신속하게 수정하겠습니다.

 

그럼 이만.

 

 

'Study' 카테고리의 다른 글

multiprocessing 겅부..  (0) 2022.09.18
local에서 ssh 서버에 띄운 jupyter notebook 붙기  (0) 2022.02.06
BERT 이해하기  (0) 2021.09.15
머신러닝 개념 정리  (0) 2021.05.22

"나의 첫 머신러닝/딥러닝" 책으로 공부하며 정리하는 시간!

1. 지도학습과 비지도학습

지도학습(Supervised learning)이란?

  - 정답을 알려주면서 진행되는 학습

  - 학습시 데이터와 함께 레이블(정답)이 항상 제공되어야 함

  - 정답 ≒ 실제값 ≒ 레이블 ≒ 타깃 ≒ 클래스 ≒ y값

  - 주로 주어진 데이터와 레이블을 이용해 새로운 데이터의 레이블을 예측해야 할 때 사용됨

  - 머신러닝 모델을 통해 예측된 값 = 예측값 ≒ 분류값 ≒ y hat

  - 장점 : 테스트할 때 데이터와 함께 레이블을 제동해서 손쉽게 모델의 성능을 평가할 수 있음

  - 단점 : 테이터마다 레이블을 달기 위해 많은 시간을 투자해야 함

  - 대표적인 예 : 분류, 회기

 

비지도학습(Unsupervised learning)이란?

  - 레이블(정답)이 없이 진행되는 학습

  - 학습할 때 레이블 없이 데이터만 필요

  - 데이터 자체에서 패턴을 찾아내야 할 떄 사용

  - 장점 : 레이블을 제공할 필요가 없음

  - 단점 : 레이블이 없기 때문에 모델 성능을 평가하는 데에 다소 어려움이 있음

  - 대표적인 예 : 군집화, 차원축소

 

2. 분류와 회귀

분류란?

  - 데이터가 입력됐을 때 지도학습을 통해 미리 학습된 레이블 중 하나 또는 여러개의 레이블로 예측

  - 분리된 값으로 예측

      1) 이진분류

      - (예, 아니오), (남자, 여자)와 같이 둘 중 하나의 값으로 분류하는 경우

      2) 다중분류

      - (빨강, 녹색, 파랑) 중 하나의 색으로 분류하거나,

        0부터 9까지의 손글씨 숫자 중 하나의 숫자로 분류하기처럼 여러 개의 분류값 중에서 하나의 값으로 예측

      3) 다중 레이블 분류

      - 데이터가 입력됐을 때 두 개 이상의 레이블로 분류

 

회귀란?

  - 입력된 데이터에 대해 연속된 값으로 예측

  - 날씨를 더움, 보통, 추움이라는 3가지로만 예측하는 분류와 달리,

    회귀는 35도, 34.5도, 34도와 같이 정해진 레이블이 아닌 연속성을 가진 수치로 예측

 

[출처]

허민석, 『나의 첫 머신러닝/딥러닝』, 위키북스(2019), p14-15.

'Study' 카테고리의 다른 글

multiprocessing 겅부..  (0) 2022.09.18
local에서 ssh 서버에 띄운 jupyter notebook 붙기  (0) 2022.02.06
BERT 이해하기  (0) 2021.09.15
Neural Search 관련 공부  (2) 2021.07.07

+ Recent posts