[NLP]. 한국어 T5 모델 문장 임베딩(sentence embedding)으로 사용하기 : ET5(etri)

jongmin-oh·2023년 6월 1일
0

배경

T5 모델은 현재 유행중인 LLM에 비해 가벼운 PLM모델이지만
그 중에서는 최고의 성능을 보이는 모델이다. (작은 놈 중에 제일 좋은 놈??)
T5에 대한 내용은 너무 많고 잘 설명해준 글이 많아 나는 실제 사용기에 더 맞춰서 글을 작성했다.

T5 설명
PLM vs LLM

자연어처리 세미나(LangCon2023)에서도 나왔던 이야기인데, chatGPT 혁명을 보고 놀랍다고 감탄하고 있는 우리지만 비용/서빙등.. 현실적으로 회사에가서 결국 BERT와 같은 PLM을 쓰게된다는 안타까운 상황에 대해 들었고 나 또한 매우 공감하고있다.

현재 우리 서비스는 한국어 넘사벽 Klue/Roberta-base 모델을 사용하고있다.
하지만 한국어 PLM 모델의 학습 corpus는 하나도 빠짐 없이 뉴스, 블로그, 위키 등과 같은 정형화된 텍스트들이다. 물론 kcBERT 같은 뉴스 기사 댓글을 가지고 만든 PLM도 있지만,
vocab을 살펴보면 일상대화보다는 정치적인 단어가 매우 많은 비중을 차지하고있었다.

우리는 일상대화 챗봇을 만들고 있으며, 그에 맞는 일상대화 PLM을 만들고자 시도했지만 데이터 부족 + 시간과 비용때문에 나중으로 미뤘고 그러던 도중 ETRI에서 만든 T5모델을 만나게 된다..


ETRI ET5

https://aiopen.etri.re.kr/et5Model

T5모델은 간단한 인증을 거쳐 회원가입하면 직접 다운로드 받아 사용할 수 있다.
(huggingFace에는 없다.)

내용을 살펴보면

학습 말뭉치의 양이 상당하다는 것을 알 수 있다.

학습 말뭉치로는 약 136 GB (12억9천만 문장, 139억개 단어, 643억 글자)의 Common Crawl, 위키백과, 신문기사, 방송 대본, 영화/드라마 대본 등, 문어/구어를 망라한 대용량 텍스트를 대상으로 학습하였습니다.

ETRI 홈페이지에 보면 위와 같이 표시되어있는데 "방송 대본, 영화/드라마 대본 등"에 데이터가 포함되어있기 때문에 일상대화 즉 구어체/대화체를 더 잘 이해할 수 있다고 생각했다.

드디어.. 대화체를 학습한 한국어 PLM이 나왔구나 ㅠㅠ

모델크기도 600MB로 roberta-base에 비하면 200MB 정도 크다.

이 정도면 바로 적용해도 나쁘지않다!!


T5

T5는 BART와 같은 seq2seq를 기반으로한 대표적인 생성 모델이다.
하지만 우리는 Bi-encoder의 base 모델로 ET5 모델을 사용하고자 했기에 사용성이 많이 달랐다.

#학습
다행히 sentence-transfomer로 학습했을때는 별다른 문제가 발생하지 않아
기존 학습데이터로 학습을 진행했고 확실히 roberta-base 보다 성능이 3%가량 좋아졌다.

정량적인 수치 뿐만아니라 실제 챗봇 검색(retrieval)모델로 사용했을때도 마찬가지로 좋은 성능을 보였다.
50개의 sample 중에서 말이 안되게 답변하는 경우는 단 한 개도 없었다.

문제 발생

하지만 T5는 생성 모델이고, Encoder 구조만 가지고있는 BERT류의 PLM과 달리 decoder를 달고있어서 만나보지 못한 잦은 에러를 발생시켰고, 가장 중요한건 CPU inference 속도가 엄청 오래걸렸다(3초~4초),
양자화(quantization)를 시도했지만 이것도 많은 에러가 발생해서 기존 양자화 코드로는 할 수 없었다.

GPU서버로 서빙하지않는 우리는 4초의 속도로 서빙할 순 없었다.


문제 해결 과정

[사이드 이펙트] Decode input 에러

ValueError: You have to specify either decoder_input_ids or decoder_inputs_embeds

이 에러는 decoder 레이어의 inputs 값을 지정해주지 않아서 발생하는 문제였다.

inputs = tokenizer.encode_plus("안녕", return_tensors="pt")
result = model.forward(inputs['input_ids'], inputs['attention_mask'], decoder_input_ids=inputs['input_ids'])

이런식으로 model.foward() 할때 decoder_input_ids=inputs['input_ids'] 값을 넣어주면 해결되는데.
이렇게 해도 속도는 여전히 느리고, 양자화가 불가능하다.

1. 모델의 Encoder만 사용하자

model = T5Model.from_pretrained(MODEL_PATH)
tok = T5Tokenizer.from_pretrained(MODEL_PATH)
enc = tok("안녕", return_tensors="pt")

output = model.encoder(
    input_ids=enc["input_ids"],
    attention_mask=enc["attention_mask"],
    return_dict=True
)
# get the final hidden states
emb = output.last_hidden_state

T5 모델에서 Encoder만 사용하는 방법을 찾아냈다. 이 방법과 SentenceTransfomer의 결과값을 서로 비교헀는데 값이 똑같았다. 여기서 "sentence-transformer"는 내부적으로 T5 모델의 encoder만 사용한다는 것을 알게 되었다.

2. Encoder만 따로 불러와서 저장하자.

from transformers import T5EncoderModel
T5EncoderModel._keys_to_ignore_on_load_unexpected = ["decoder.*"]
auto_model = T5EncoderModel.from_pretrained(MODEL_PATH)

이런식으로 처음 불러올때 부터 Encoder 레이어만 불러오는 방법이 있다.
하지만 auto_model를 저장했을때 용량이 줄지 않고 계속 유지되었다.
이유는 아직도 잘 모르겠다

이 상태로 auto_model을 양자화하면 된다고 생각했지만,

TypeError: T5EncoderModel.forward() got an unexpected keyword argument 'return_tensors'

위 에러가 발생했다.

3. Transformer Pipeline 사용

양자화를 하기 가장 적합한 방법은 Transformer에 pipeline을 사용하여 모델을 만들고
그 모델을 양자화하는 방법이다.

from transformers import pipeline

encoder = pipeline(
    "feature-extraction",
    model=auto_model,
    tokenizer=tokenizer,
    return_tensors=True
)

여기서 return_tensors=True 옵션을 사용하면 위 에러가 발생하지 않는다.


양자화(quantization)

양자화는 ONNX를 사용했다.

import transformers
import transformers.convert_graph_to_onnx as onnx_convert
from onnxruntime.quantization import quantize_dynamic, QuantType

model = model.to('cpu')
onnx_convert.convert_pytorch(encoder, opset=17, output=Path("encoder.onnx"),
	use_external_format=False)

여기서 opset은 13이상으로 해야 에러가 발생하지 않는다.

Using framework PyTorch: 2.0.1+cu117
Found input input_ids with shape: {0: 'batch', 1: 'sequence'}
Found input attention_mask with shape: {0: 'batch', 1: 'sequence'}
Found output output_0 with shape: {0: 'batch', 1: 'sequence'}
Ensuring inputs are in correct order
head_mask is not present in the generated input list.
Generated inputs order: ['input_ids', 'attention_mask']
============= Diagnostic Run torch.onnx.export version 2.0.1+cu117 =============
verbose: False, log level: Level.ERROR
======================= 0 NONE 0 NOTE 0 WARNING 0 ERROR ========================
quantize_dynamic("encoder.onnx", "encoder.onnx_uint8.onnx", 
                 weight_type=QuantType.QUInt8)
Ignore MatMul due to non constant B: /[/encoder/block.0/layer.0/SelfAttention/MatMul]
Ignore MatMul due to non constant B: /[/encoder/block.0/layer.0/SelfAttention/MatMul_1]
Ignore MatMul due to non constant B: /[/encoder/block.1/layer.0/SelfAttention/MatMul]
Ignore MatMul due to non constant B: /[/encoder/block.1/layer.0/SelfAttention/MatMul_1]
Ignore MatMul due to non constant B: /[/encoder/block.2/layer.0/SelfAttention/MatMul]
Ignore MatMul due to non constant B: /[/encoder/block.2/layer.0/SelfAttention/MatMul_1]
Ignore MatMul due to non constant B: /[/encoder/block.3/layer.0/SelfAttention/MatMul]
Ignore MatMul due to non constant B: /[/encoder/block.3/layer.0/SelfAttention/MatMul_1]
Ignore MatMul due to non constant B: /[/encoder/block.4/layer.0/SelfAttention/MatMul]
Ignore MatMul due to non constant B: /[/encoder/block.4/layer.0/SelfAttention/MatMul_1]
Ignore MatMul due to non constant B: /[/encoder/block.5/layer.0/SelfAttention/MatMul]
Ignore MatMul due to non constant B: /[/encoder/block.5/layer.0/SelfAttention/MatMul_1]
Ignore MatMul due to non constant B: /[/encoder/block.6/layer.0/SelfAttention/MatMul]
Ignore MatMul due to non constant B: /[/encoder/block.6/layer.0/SelfAttention/MatMul_1]
Ignore MatMul due to non constant B: /[/encoder/block.7/layer.0/SelfAttention/MatMul]
Ignore MatMul due to non constant B: /[/encoder/block.7/layer.0/SelfAttention/MatMul_1]
Ignore MatMul due to non constant B: /[/encoder/block.8/layer.0/SelfAttention/MatMul]
Ignore MatMul due to non constant B: /[/encoder/block.8/layer.0/SelfAttention/MatMul_1]
Ignore MatMul due to non constant B: /[/encoder/block.9/layer.0/SelfAttention/MatMul]
Ignore MatMul due to non constant B: /[/encoder/block.9/layer.0/SelfAttention/MatMul_1]
Ignore MatMul due to non constant B: /[/encoder/block.10/layer.0/SelfAttention/MatMul]
Ignore MatMul due to non constant B: /[/encoder/block.10/layer.0/SelfAttention/MatMul_1]
Ignore MatMul due to non constant B: /[/encoder/block.11/layer.0/SelfAttention/MatMul]
Ignore MatMul due to non constant B: /[/encoder/block.11/layer.0/SelfAttention/MatMul_1]

결론

속도체크

%%timeit -n 100
sentence_embedding("안녕")

4.77 ms ± 96.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

300ms => 4.7ms 로 임베딩 시간 대폭 감소했고
챗봇의 성능 측면에서도 큰 차이를 보이지 않았다.

(내가 알고있는 한) 유일한 대화체, 구어체 말뭉치를 포함한 한국어 PLM 모델 ET5를 적용해보았다.
중간에 대표님께 보고드렸을때 "속도때문에 사용할 수 없겠다"는 답변을 들었지만 챗봇의 성능이 너무 좋아서 포기하기 너무 아쉬웠다, 결국 해결 방법을 찾았고 챗봇 성능 향상에 큰 도움이 되었다.

요즘 커뮤니티나 NLP 자료들을 보면 챗GPT 글이 대부분이고 T5는 그에 비하면 뒤쳐진 모델이다.
하지만 PLM은 계속해서 속도와 서비스 문제 뿐만아니라 도메인특화에 더 큰 강점을 보이기 때문에 앞으로도 LLM과 계속해서 같이 사용될 것이라 생각한다.

ETRI에서 나온 T5모델은 아주 좋은 모델이고 이 글을 통해 T5로 성능 개선을 이루는데 조금이나마 도움이 되었으면 한다.

전체코드 :
https://colab.research.google.com/drive/1CLvhbz0OCNkioBKsJ1RN_rZoLgz1nw2f?usp=sharing

ETRI-ET5
https://aiopen.etri.re.kr/et5Model

profile
스타트업에서 자연어처리 챗봇을 연구하는 머신러닝 개발자입니다.

10개의 댓글

comment-user-thumbnail
2024년 1월 7일

안녕하세요? 덕분에 이 글 보고 따라해서 코랩에서 구동까지 성공했습니다. 저는 임베딩 결과물을 챗봇에 입력한 문장과 제가 가지고 있는 데이터 간 유사도 값을 내는 데에 쓰려 합니다.

이 과정에서 파인튜닝 관련하여 궁금한 게 생겼습니다.

(1) 임베딩 하려는 문장이 30000여 개 정도 있는데요, 이 문장으로 et5-base 모델을 가지고 파인튜닝을 해야 하는가... 하는 의문이 들었습니다. 막상 파인튜닝을 하려고 ynat-v1.1_dev_sample_10.json 파일을 열어봤더니 이렇게 생겼던데요,

[
{
"guid": "ynat-v1_dev_00000",
"title": "5억원 무이자 융자는 되고 7천만원 이사비는 안된다",
"predefined_news_category": "경제",
"label": "사회",
"annotations": {
"annotators": [
"18",
"03",
"15"
],
"annotations": {
"first-scope": [
"사회",
"사회",
"경제"
],
"second-scope": [
"해당없음",
"해당없음",
"사회"
],
"third-scope": [
"해당없음",
"해당없음",
"생활문화"
]
}
},
"url": "https://news.naver.com/main/read.nhn?mode=LS2D&mid=shm&sid1=101&sid2=260&oid=001&aid=0009563542",
"date": "2017.09.21. 오후 5:09"
},
...이런 게 수십 개 있더군요.

json 형태로 guid, title, label, ..., url, date까지 이 모든 것을 다 입력하고 파인튜닝을 하셨는지 궁금합니다. 제게는 분류가 안 된 문장 정도 밖에 없습니다...ㅠ

(2) 선생님께서 챗봇을 만드셔서 운영한다고 하셨는데요, 이 모델을 챗봇에 어떻게 쓰고 계신지 알 수 있을까요? 임베딩을 해서 사용자가 입력한 물음과 가장 비슷한 데이터셋 속 질의에 대응하는 미리 입력된 답을 출력하는 형태인가요? ^^

(3) 챗봇에서 파인튜닝을 해서 임베딩했을 때 얻을 수 있는 이득이 무엇이길래 챗봇 운영에서 파인튜닝을 했는지도 궁금합니다.

감사합니다~

1개의 답글
comment-user-thumbnail
2024년 4월 20일

안녕하세요 !! 여러 글을 읽고 덕분에 많이 공부하고 있습니다ㅠㅠ 이 글과 이 글에 달린 댓글들을 보고 궁금한 게 있어 질문드립니다..!! 밑의 댓글의 코랩을 보고 궁금한 게 있습니다.
->
https://colab.research.google.com/drive/1UcS70OhNNkwp2v3C9r8sku8uvAsie6cL?usp=sharing
제가 예전에 공부했던 코드를 복제해둔 링크입니다.
코랩에서도 돌아가니까 직접 해보세요!
**지포스 RTX VRAM 24GB 에서도 충분히 돌아갑니다.
(만약에 안될경우 batch_size를 줄여보세요!)

모델이름만 교체해서 쓰시면 됩니다.

->
sts 파인튜닝시 쌍으로 짝지어진 데이터셋이 있어야 하는 거 같은데 그런 데이터셋을 직접 만드는 것은 어려운 거 같고,, 제가 가지고 있는 데이터를 가지고 파인튜닝이 아닌 사전학습된 모델에 그냥 추가로 데이터를 넣어 학습시키고 싶을 때 어떻게 해야하는지 여쭤봐도 괜찮을까요??

답글 달기