!pip install transformers
친절한 영어-한국어 번역기 만들기 A Gentle Introduction to Creating an English-to-Korean translator with Transformers
트랜스포머가 딥러닝 세상을 지배한 지금 과거 CNN, RNN을 모르고 딥러닝을 안다고 할 수 없듯이 이제는 트랜스포머를 이해하지 못하고 딥러닝을 공부한다고 말할 수 없는 시대가 되었다.
트랜스포머가 초기 타겟한 작업이 번역이기 때문에 트랜스포머를 설명하는 글에서 단골로 등장하는 예제가 번역기 예제다. 주로 영어-독일어, 영어-스페인어 예제가 많다. 하지만 쉽게 접할 수 있는 알파벳권 언어 사이의 번역기 예제에 비해 영어-한국어 데이터를 사용해서 번역기를 학습시키는 예제는 이상하리만큼 찾아보기 힘들었다. 왜 그런지 이유는 잘 모르겠지만 어쨌든 거의 없다. 그래서 영어-한국어 문장쌍이 들어있는 원시데이터를 사용해 T5 모델로 영어-한국어 번역기를 데모 수준 정도로 학습하는 예제를 만들어 블로그에 포스팅하면 많은 사람들에게 도움이 되지 않을까 해서 이 글을 적게 되었다.
이 글에서는 트랜스포머에 대한 기초적인 내용은 다루지 않고 오직 데이터를 어떻게 준비하고, 어떻게 데이터를 모델에 입력하여 학습을 시키고, 마지막으로 어떻게 영어로 부터 한국어 번역 문장을 출력시키는가 하는 것에만 초점을 맞추었다. 그리고 코드를 복잡하게 만드는 그 어떤 테크닉도 사용하지 않는다. 오로지 가장 간단하게 한국어 번역기를 구축하는데만 집중할 것이다. 사실 이 글의 대부분 내용은 허깅페이스 도움말에 있는 것을 정리한것에 지나지 않는다. 하지만 입문자나 이제 막 트랜스포머를 이용해서 한국어 번역기를 만들고자 하는 사람들은 허깅페이스 도움말을 보고 이 내용을 모두 정리하기 쉽지 않은 것이 사실이어서 이글이 꽤 도움이 되리라 생각한다.
필요 패키지 설치
시작하기전에 필요한 라이브러리를 설치한다. 본인 컴퓨터에 이미 관련 라이브러리가 설치되어 있다면 설치하지 않아도 된다.
먼저 허깅페이스의 트랜스포머스 라이브러리를 설치한다.
그 다음은 데이터 셋을 다운받는기 위해 다음 명령을 실행해서 허깅페이스 Datasets
라이브러리를 설치한다.
!pip install datasets
허깅페이스 트랜스포머 기반 모델을 학습 시키기 위해 accelerate
라이브러리를 설치한다.
!pip install accelerate
그리고 T5 모델의 토크나이저가 sentencepiece
를 사용하므로 다음을 실행해서 설치한다.
!pip install sentencepiece
또 모델 평가를 위해 허깅페이스 evaluate
라이브러리와 BLEU 점수를 계산하기위해 sacrebleu
를 설치한다.
!pip install evaluate
!pip install sacrebleu
Data Set
모두 설치가 완료되었다면 데이터 셋을 다운받아야 한다. 먼저 허깅페이스 사이트에 접속해서 상단 메뉴에 Datasets를 클릭하고 아래처럼 검색 조건을 맞추면
- 좌측 작은 메뉴에서 Languages를 선택한다.
- Languages 하단에 보이는 여러 언어중에 Korean을 선택한다.
- 다시 우측 검색 필터 창에 en을 적는다.
데이터 셋 네 개가 보이는데 이 중에서 bongsoo/news_talk_en_ko
를 사용하도록 하겠다.
bongsoo/news_talk_en_ko
를 클릭해서 나오는 화면에서 Files and Versions
를 클릭하면 tsv 파일이 보이는데 이 파일에는 영어 문장과 한국어 문장이 한줄에 탭 문자로 구분되어 적혀있다. 로컬 디스크이 이 파일을 다운받고 파일을 읽어보면 다음처럼 확인된다.
[노트] 로컬 또는 코랩 런타임에 파일을 다운 받지 않았다면 굳이 다운받을 필요없고 이 셀은 스킵하자. 그냥 데이터 파일 한줄에 영어 문장과 짝이 되는 한국어 문장이 탭문자로 구분되어 있다는 것만 알면 된다. 실제 데이터를 가져올 때는 허깅페이스를 통해 다운 받게 된다.)
!head -5 news_talk_en_ko_train_130000.tsv
Skinner's reward is mostly eye-watering. 스키너가 말한 보상은 대부분 눈으로 볼 수 있는 현물이다.
Even some problems can be predicted. 심지어 어떤 문제가 발생할 건지도 어느 정도 예측이 가능하다.
Only God will exactly know why. 오직 하나님만이 그 이유를 제대로 알 수 있을 겁니다.
Businesses should not overlook China's dispute. 중국의 논쟁을 보며 간과해선 안 될 게 기업들의 고충이다.
Slow-beating songs often float over time. 박자가 느린 노래는 오랜 시간이 지나 뜨는 경우가 있다.
데이터 파일은 아주 단순한 형태인 것을 알 수 있다. 직접 tsv파일을 다운받아서 사용해도 되나 허깅페이스 허브로 부터 바로 다운받아 사용하는 편이 더 편하다. 다음 명령으로 다운받을 수 있다.
# 데이터 셋을 다운받을 함수를 임포트 한다.
from datasets import load_dataset
# 좀 전에 알아본 체크포인트를 사용해서 데이터를 받아온다.
= load_dataset("bongsoo/news_talk_en_ko") en_ko
Using custom data configuration bongsoo--news_talk_en_ko-e7f00bc8f76f18d5
Found cached dataset csv (/home/metamath/.cache/huggingface/datasets/bongsoo___csv/bongsoo--news_talk_en_ko-e7f00bc8f76f18d5/0.0.0/6b34fb8fcf56f7c8ba51dc895bfa2bfbe43546f190a60fcf74bb5e8afdcc2317)
이제 데이터 객체를 확인해보면 DatasetDict
라는 것을 알 수 있고 안에 train
키만 있는 것이 확인된다.
en_ko
DatasetDict({
train: Dataset({
features: ["Skinner's reward is mostly eye-watering.", '스키너가 말한 보상은 대부분 눈으로 볼 수 있는 현물이다.'],
num_rows: 1299999
})
})
train
키에 Dataset
객체가 하나 있는데 features
가 첫번째 데이터로 되어있고 행수는 1299999개인 것을 보아 데이터 파일에 컬럼명이 적혀있는 헤더라인이 없어서 첫줄을 헤더로 읽은것 같다. 첫줄을 데이터로 다시 집어 넣고 컬럼명은 en
, ko
로 설정하기 위해 데이터 셋을 pandas
로 읽어드린다.
import pandas as pd
# 허깅페이스 데이터셋을 판다스 포맷으로 세팅
type="pandas") en_ko.set_format(
# 'train'키의 모든 행을 DataFrame df에 할당
= en_ko["train"][:]
df
# 잘 담겼는지 확인한다.
df.head()
Skinner's reward is mostly eye-watering. | 스키너가 말한 보상은 대부분 눈으로 볼 수 있는 현물이다. | |
---|---|---|
0 | Even some problems can be predicted. | 심지어 어떤 문제가 발생할 건지도 어느 정도 예측이 가능하다. |
1 | Only God will exactly know why. | 오직 하나님만이 그 이유를 제대로 알 수 있을 겁니다. |
2 | Businesses should not overlook China's dispute. | 중국의 논쟁을 보며 간과해선 안 될 게 기업들의 고충이다. |
3 | Slow-beating songs often float over time. | 박자가 느린 노래는 오랜 시간이 지나 뜨는 경우가 있다. |
4 | I can't even consider uninsured treatments. | 보험 처리가 안 되는 비급여 시술은 엄두도 못 낸다. |
예상처럼 첫 줄이 헤더가 되었으니 이를 수정한 DataFrame
을 만든다.
= list(df.columns)
example_0 example_0
["Skinner's reward is mostly eye-watering.",
'스키너가 말한 보상은 대부분 눈으로 볼 수 있는 현물이다.']
적당히 조작해서 컬럼명이 en
, ko
가 되게 하고 example_0
가 첫 행이 되도록 만든다.
= pd.DataFrame({col: [value] for col, value in zip(('en', 'ko'), example_0)}) example_0_df
= ('en', 'ko') df.columns
= pd.concat([example_0_df, df],).reset_index(drop=True)
en_ko_df en_ko_df.head()
en | ko | |
---|---|---|
0 | Skinner's reward is mostly eye-watering. | 스키너가 말한 보상은 대부분 눈으로 볼 수 있는 현물이다. |
1 | Even some problems can be predicted. | 심지어 어떤 문제가 발생할 건지도 어느 정도 예측이 가능하다. |
2 | Only God will exactly know why. | 오직 하나님만이 그 이유를 제대로 알 수 있을 겁니다. |
3 | Businesses should not overlook China's dispute. | 중국의 논쟁을 보며 간과해선 안 될 게 기업들의 고충이다. |
4 | Slow-beating songs often float over time. | 박자가 느린 노래는 오랜 시간이 지나 뜨는 경우가 있다. |
이렇게 데이터 셋을 DataFrame
으로 만들었다. 이제 이 en_ko_df
로 부터 다시 허깅페이스 데이터 셋을 생성하자.
from datasets import Dataset
= Dataset.from_pandas(en_ko_df) dataset
dataset
Dataset({
features: ['en', 'ko'],
num_rows: 1300000
})
다시 데이터 셋을 확인해보면 features
가 제대로 표시되고 샘플 수도 1300000개 인것을 확인할 수 있다.
이렇게 만들어진 DataFrame
으로 부터 데이터 셋이 잘 초기화되는 것을 확인했으니 en_ko_df
를 세조각으로 쪼개서 tsv파일로 저장하자.
# 각 데이터 셋의 샘플수를 정한다.
= 1200000
num_train = 90000
num_valid = 10000 num_test
설정된 크기만큼 DataFrame
을 자른다.
= en_ko_df.iloc[:num_train] en_ko_df_train
= en_ko_df.iloc[num_train:num_train+num_valid] en_ko_df_valid
= en_ko_df.iloc[-num_test:] en_ko_df_test
다시 tsv
파일로 저장한다.
"train.tsv", sep='\t', index=False) en_ko_df_train.to_csv(
"valid.tsv", sep='\t', index=False) en_ko_df_valid.to_csv(
"test.tsv", sep='\t', index=False) en_ko_df_test.to_csv(
이렇게 tsv
파일 세개로 데이터를 정리했다. 이제 필요할때 이 파일을 읽어 허깅페이스 데이터셋을 만들 수 있다.
아래처럼 스플릿을 정의한 사전을 load_dataset
에 넘기면 된다. 이때 delimiter
를 탭 문자로 지정해야 한다.
= {"train": "train.tsv", "valid": "valid.tsv", "test": "test.tsv"} data_files
= load_dataset("csv", data_files=data_files, delimiter="\t") dataset
Using custom data configuration default-02a3611b1810efcd
Downloading and preparing dataset csv/default to /home/metamath/.cache/huggingface/datasets/csv/default-02a3611b1810efcd/0.0.0/6b34fb8fcf56f7c8ba51dc895bfa2bfbe43546f190a60fcf74bb5e8afdcc2317...
Dataset csv downloaded and prepared to /home/metamath/.cache/huggingface/datasets/csv/default-02a3611b1810efcd/0.0.0/6b34fb8fcf56f7c8ba51dc895bfa2bfbe43546f190a60fcf74bb5e8afdcc2317. Subsequent calls will reuse this data.
제대로 로딩되었는지 dataset
을 확인해보자.
dataset
DatasetDict({
train: Dataset({
features: ['en', 'ko'],
num_rows: 1200000
})
valid: Dataset({
features: ['en', 'ko'],
num_rows: 90000
})
test: Dataset({
features: ['en', 'ko'],
num_rows: 10000
})
})
DatasetDict
에 train
, valid
, test
키로 120만 문장, 9만 문장, 1만 문장이 저장된 것을 확인할 수 있다.
이 데이터 셋에서 개별 샘플에 대한 접근은 [split][feature][row num]
형태로 가능하다.
# train 스플릿에서 영어 3개와 한국어 3개 샘플을 가져온다.
print(dataset['train']['en'][:3], dataset['train']['ko'][:3])
["Skinner's reward is mostly eye-watering.", 'Even some problems can be predicted.', 'Only God will exactly know why.'] ['스키너가 말한 보상은 대부분 눈으로 볼 수 있는 현물이다.', '심지어 어떤 문제가 발생할 건지도 어느 정도 예측이 가능하다.', '오직 하나님만이 그 이유를 제대로 알 수 있을 겁니다.']
그런데 feature
와 row num
은 순서를 바꿔서 사용할 수 도 있다.
print(dataset['train'][:3]['en'], dataset['train'][:3]['ko'])
["Skinner's reward is mostly eye-watering.", 'Even some problems can be predicted.', 'Only God will exactly know why.'] ['스키너가 말한 보상은 대부분 눈으로 볼 수 있는 현물이다.', '심지어 어떤 문제가 발생할 건지도 어느 정도 예측이 가능하다.', '오직 하나님만이 그 이유를 제대로 알 수 있을 겁니다.']
데이터를 어떻게 조회하는지는 데이터 구성 방식에 따라 조금씩 다르므로 데이터 셋을 보고 몇번 해보면 금방 접근법을 알 수 있다.
Hugging face
데이터 셋 준비를 마쳤으니 학습할 차례이다. 허깅페이스에서 제공하는 필요 클래스를 임포트 한다.
먼저 선학습 모델을 사용하기 위한 클래스를 임포트 한다. AutoTokenizer
는 선학습된 모델이 사용한 토크나이저를 읽기 위해 필요하며 AutoModelForSeq2SeqLM
은 시퀀스 투 스퀀스 방식으로 작동하는 선학습된 모델을 불러 올 때 마지막에 분류기 헤드를 붙여서 모델을 로딩하기 위해 사용한다.
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
2023-03-01 16:05:02.191320: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations: AVX2 AVX_VNNI FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
2023-03-01 16:05:02.266647: I tensorflow/core/util/util.cc:169] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.
2023-03-01 16:05:02.281905: E tensorflow/stream_executor/cuda/cuda_blas.cc:2981] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
2023-03-01 16:05:02.592853: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer.so.7'; dlerror: libnvinfer.so.7: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /usr/local/cuda-11.6/lib64:
2023-03-01 16:05:02.592895: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer_plugin.so.7'; dlerror: libnvinfer_plugin.so.7: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /usr/local/cuda-11.6/lib64:
2023-03-01 16:05:02.592898: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Cannot dlopen some TensorRT libraries. If you would like to use Nvidia GPU with TensorRT, please make sure the missing libraries mentioned above are installed properly.
다음은 데이터 콜레이터를 임포트한다. 시쿼스 투 시퀀스 학습 과정은 인코더 입력 시퀀스, 디코더 입력 시퀀스, 디코더 출력 시퀀스를 필요로 하는데 미니배치로 부터 이를 적절히 정리해서 모델에 입력하는 작업이 필요하다. 예를 들면 미니 배치 내에 있는 인코더 입력 시퀀스의 길이를 맞춘다든지 디코더 입력시퀀스를 오른쪽으로 한칸 쉬프트시켜 디코더 출력 시퀀스를 만드는 작업등이 콜레이터에서 일어나는 작업인데 이런 작업을 DataCollatorForSeq2Seq
가 자동으로 처리하게 된다.
from transformers import DataCollatorForSeq2Seq
그리고 학습에 필요한 클래스를 임포트 한다. 학습에 필요한 설정을 Seq2SeqTrainingArguments
에 정의하고 실제 학습은 Seq2SeqTrainer
로 하게 된다. Seq2SeqTrainer
는 generate()
함수를 제공한다.
from transformers import Seq2SeqTrainingArguments, Seq2SeqTrainer
허깅페이스 라이브러리로는 마지막으로 데이터 셋을 로딩하는 함수와 번역 결과를 측정할 함수를 로딩한다.
from datasets import load_dataset, load_metric
그외 필요한 각종 라이브러리를 임포트 한다.
import numpy as np
import torch
import multiprocessing
허깅페이스에서 파이토치 기반 구현을 사용하므로 gpu가 있다면 device
를 세팅한다.
= 'cuda' if torch.cuda.is_available() else 'cpu'
device device
'cuda'
미리 학습된 모델의 체크포인트를 세팅한다. 여기서 사용할 모델은 한국어와 영어에 미리 학습된 KE-T5모델을 사용한다. T5모델은 트랜스포머의 인코더, 디코더 구조를 모두 사용하는 모델로 번역기를 만들 때 사용할 수 있는 모델이다. 아래처럼 모델 체크 포인트와 T5 모델에 입력될 최대 토큰 길이를 설정한다.
= "KETI-AIR/ke-t5-base"
model_ckpt = 64 max_token_length
Tokenizer
먼저 모델 체크 포인트를 사용하여 KE-T5 모델이 학습할때 함께 사용한 토크나이저를 불러온다. 허깅페이스 트랜스포머스 라이브러리를 사용할 때 가장 핵심이 되며 익숙해지기 쉽지 않은 부분이 이 토크나이저라고 개인적으로 생각한다.
= AutoTokenizer.from_pretrained(model_ckpt) tokenizer
토크나이저를 로딩할때 sentencepiece
가 없다고 에러가 나면 위 제시한 라이브러리가 설치 안된 것이므로 설치하고 다시 시도한다.
토크나이저를 불러왔으니 현재 사용하는 데이터셋에서 샘플을 가져와 토크나이징해보는 것이 좋을 것이다. 학습 세트에서 10번 샘플을 가지고 실험해보자. 먼저 10번 샘플을 뿌려보고
'train'][10]['en'], dataset['train'][10]['ko'] dataset[
('Any academic achievement requires constant repetition.',
'어떤 학문이든지 일정의 성취를 이루기 위해서는 끊임없는 반복이 필요하다.')
토크나이저에 각 문장을 입력하고 토큰화된 상태로 돌려 받는다.
= tokenizer(dataset['train'][10]['en'],
tokenized_sample_en =max_token_length,
max_length=True, truncation=True)
padding tokenized_sample_en
{'input_ids': [13941, 10114, 25542, 9361, 20526, 742, 32268, 12520, 3, 1], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
= tokenizer(dataset['train'][10]['ko'],
tokenized_sample_ko =max_token_length,
max_length=True, truncation=True)
padding tokenized_sample_ko
{'input_ids': [404, 12663, 15, 10775, 2334, 6, 15757, 21, 29819, 1736, 26778, 4342, 15, 1701, 3, 1], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
문장에 토큰으로 쪼개지고 각 토큰이 숫자로 변환된 것을 볼 수 있다. 이렇게 숫자화된 토큰을 input_ids
로 반환하고 추가로 트랜스포머 인코더, 디코더에 쓰일 패딩 마스크도 함께 attention_mask
로 돌려준다. 마스크가 모두 1인 이유는 샘플이 하나밖에 없어서 이다. 샘플 몇개를 더 실험해보면
'train'][:3]['en'],
tokenizer(dataset[=max_token_length,
max_length=True, truncation=True) padding
{'input_ids': [[388, 6809, 2952, 17, 8, 32204, 43, 8023, 6687, 28, 9495, 91, 3, 1], [4014, 322, 3170, 147, 67, 23274, 3, 1, 0, 0, 0, 0, 0, 0], [11783, 4412, 96, 6556, 709, 1632, 3, 1, 0, 0, 0, 0, 0, 0]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0]]}
미니배치에 있는 샘플의 최대길이메 맞춰서 패딩되는 모습을 확인할 수 있다. 실제로 어떻게 토큰화 되었는지 확인해보자.
pd.DataFrame(
['input_ids'],
tokenized_sample_en['input_ids'])
tokenizer.convert_ids_to_tokens(tokenized_sample_en[=('ids', 'tokens')
], index )
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
---|---|---|---|---|---|---|---|---|---|---|
ids | 13941 | 10114 | 25542 | 9361 | 20526 | 742 | 32268 | 12520 | 3 | 1 |
tokens | ▁Any | ▁academic | ▁achievement | ▁requires | ▁constant | ▁re | pet | ition | . | </s> |
pd.DataFrame(
['input_ids'],
tokenized_sample_ko['input_ids'])
tokenizer.convert_ids_to_tokens(tokenized_sample_ko[=('ids', 'tokens')
], index )
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ids | 404 | 12663 | 15 | 10775 | 2334 | 6 | 15757 | 21 | 29819 | 1736 | 26778 | 4342 | 15 | 1701 | 3 | 1 |
tokens | ▁어떤 | ▁학문 | 이 | 든지 | ▁일정 | 의 | ▁성취 | 를 | ▁이루기 | ▁위해서는 | ▁끊임없는 | ▁반복 | 이 | ▁필요하다 | . | </s> |
KE-T5를 학습할때 학습된 규칙대로 토큰화가 진행된다. 영어에서 repetition
은 re
, pet
, ition
으로 쪼개진 것을 볼 수 있고, 한국어에서 성취를
은 성취
, 를
로 쪼개지고 학문이든지
는 학문
, 이
, 든지
로 쪼개진것을 볼 수 있다. 토큰 앞에 _표시는 이 토큰 앞에는 공백이 있어야 한다는 의미다. 그리고 마지막에 엔드 토큰인 </s>
가 항상 붙게 되는 것도 확인할 수 있다.
이제 앞서 tsv파일로 부터 로딩한 dataset
내의 문장을 모두 토크나이저를 사용해서 숫자로 바꾸는 작업을 해야 한다. 즉 문자로된 문장을 숫자로 바꿔 특성화 해야 한다. dataset.map()
함수에 각 샘플을 토큰화 하는 함수를 만들어 전달하면 map()
이 모든 샘플에 대해 전달받은 함수를 적용하게 되는데 함수는 이렇게 작성하면 된다.
def convert_examples_to_features(examples):
###########################################################################
# with 쓰는 옛날 방식
# input_encodings = tokenizer(examples['en'],
# max_length=max_token_length, truncation=True)
# Setup the tokenizer for targets
# with tokenizer.as_target_tokenizer():
# target_encodings = tokenizer(text_target=examples['ko'],
# max_length=max_token_length, truncation=True)
#
#
# return {
# "input_ids": input_encodings["input_ids"],
# "attention_mask": input_encodings["attention_mask"],
# "labels": target_encodings["input_ids"]
# }
# 그런데 이렇게 하면 인풋하고 한번에 처리 가능함.
= tokenizer(examples['en'],
model_inputs =examples['ko'],
text_target=max_token_length, truncation=True)
max_length
return model_inputs
convert_examples_to_features()
가 하고 싶은 일은 dataset
에 있는 “어떤 학문이든지 일정의 성취를 이루기 위해서는 끊임없는 반복이 필요하다.”라는 샘플 문장을 [404,12663,15,10775,2334,6,15757,21,29819,1736,26778,4342,15,1701,3,1]
라는 정수로 바꾸는 것이다. convert_examples_to_features()
가 dataset
에 적용될 때 넘겨 받는 examples
는 다음과 같이 넘어 온다.
= {'en':['sent1', 'sent2', ... , 'sent1000'], # 이건 문장 1000개짜리 리스트
examples'ko':['sent1', 'sent2', ... , 'sent1000']}
기본으로 미니 배치 사이즈는 1000으로 세팅되어 있다.(함수 기본인자는 여기서 확인 가능)
미니 배치로 넘어온 문장 샘플을 영어 문장과 한국어 문장을 각각 인풋과 타겟으로 토큰화하고 이로 부터 input_ids
, attention_mask
, labels
로 묶어 리턴하는 방식이 예전에 쓰던 방식으로 함수 위쪽에 주석처리 되어 있다. 타겟 문장을 토큰화 할 때 타겟에서 필요로 하는 특수 토큰을 추가하는 경우 이를 처리하기위해 타겟 토큰 토큰화 때는 with tokenizer.as_target_tokenizer():
라는 컨텍스트 매니저를 사용했는데 최근 업데이트에서는 그냥 tokenizer
에 text_target
인자에 타겟 문장을 넣어서 한번에 다 처리할 수 있다. 이렇게 model_inputs
을 반환하면 dataset
에 있던 각 레코드 마다 en
, ko
특성에 추가로 input_ids
, attention_mask
, labels
특성이 더 추가 되게 된다. 사실 en
, ko
특성은 더이상 필요없기 때문에 convert_examples_to_features()
를 적용할 때 없애라는 인자를 세팅한다.
바로 dataset
에 함수를 적용해보자. 그냥 해도되나 좀 더 빠르게 하기 위해 num_proc
인자에 스레드 개수를 지정한다.
= multiprocessing.cpu_count()
NUM_CPU NUM_CPU
20
그리고 remove_columns
인자에 기존 특성 이름인 en
, ko
를 전달해서 기존 특성은 제거하게 한다. 이 특성이 있으면 이후 콜레이터가 샘플들을 미니 배치로 묶을 때 패딩처리를 못하게 된다.
= dataset.map(convert_examples_to_features,
tokenized_datasets =True,
batched# 이걸 쓰지 않으면 원 데이터 'en', 'ko'가 남아서
# 아래서 콜레이터가 패딩을 못해서 에러남
=dataset["train"].column_names,
remove_columns=NUM_CPU) num_proc
[노트]
dataset.map()
이 실행되면서 출력되는 출력은 생략됨
convert_examples_to_features()
이 dataset
의 모든 샘플에 다 적용되고 나면 tokenized_datasets
는 다음처럼 된다.
tokenized_datasets
DatasetDict({
train: Dataset({
features: ['input_ids', 'attention_mask', 'labels'],
num_rows: 1200000
})
valid: Dataset({
features: ['input_ids', 'attention_mask', 'labels'],
num_rows: 90000
})
test: Dataset({
features: ['input_ids', 'attention_mask', 'labels'],
num_rows: 10000
})
})
기존에 있던 특성 en
, ko
는 사라졌고 en
은 input_ids
와 attention_mask
로 ko
는 labels
로 바뀐것을 확인할 수 있다. 예를 들어 학습 세트에 10번 데이터를 보면 다음처럼 다 숫자라 바뀌게 된것이다.
'train'][10] tokenized_datasets[
{'input_ids': [13941, 10114, 25542, 9361, 20526, 742, 32268, 12520, 3, 1],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
'labels': [404,
12663,
15,
10775,
2334,
6,
15757,
21,
29819,
1736,
26778,
4342,
15,
1701,
3,
1]}
토크나이저를 써서 숫자로 부터 토큰화 해보면 다음과 같다.
print( '원 데이터 :', dataset['train'][10]['en'] )
print( '처리 후 데이터:', tokenized_datasets['train'][10]['input_ids'] )
print( '토큰화 :', tokenizer.convert_ids_to_tokens(tokenized_datasets['train'][10]['input_ids']) )
print('\n')
print( '원 데이터 :', dataset['train'][10]['ko'] )
print( '처리 후 데이터:', tokenizer.convert_ids_to_tokens(tokenized_datasets['train'][10]['labels']) )
print( '토큰화 :', tokenized_datasets['train'][10]['labels'] )
원 데이터 : Any academic achievement requires constant repetition.
처리 후 데이터: [13941, 10114, 25542, 9361, 20526, 742, 32268, 12520, 3, 1]
토큰화 : ['▁Any', '▁academic', '▁achievement', '▁requires', '▁constant', '▁re', 'pet', 'ition', '.', '</s>']
원 데이터 : 어떤 학문이든지 일정의 성취를 이루기 위해서는 끊임없는 반복이 필요하다.
처리 후 데이터: ['▁어떤', '▁학문', '이', '든지', '▁일정', '의', '▁성취', '를', '▁이루기', '▁위해서는', '▁끊임없는', '▁반복', '이', '▁필요하다', '.', '</s>']
토큰화 : [404, 12663, 15, 10775, 2334, 6, 15757, 21, 29819, 1736, 26778, 4342, 15, 1701, 3, 1]
데이터 특성화를 모두 마쳤으므로 이제 모델을 로딩하자. AutoModelForSeq2SeqLM
를 사용해서 선학습 모델을 불러오면 선학습된 T5모델 마지막에 파인튜닝할 수 있는 분류 헤드를 붙인 모델을 반환한다.
Model
= AutoModelForSeq2SeqLM.from_pretrained(model_ckpt).to(device) model
위처럼 모델을 로딩하고 모델 출력 시켜보면 T5 모델 레이어가 매우 길게 출력되는데 제일 마지막 부분에 다음과 같이 분류 헤드가 붙어 있는 것을 확인할 수 있다. 헤드를 보면 모델에서 출력하는 벡터는 768차원이고 이를 단어장 사이즈인 64128로 변환시키고 있는 것을 알 수 있다.
(lm_head): Linear(in_features=768, out_features=64128, bias=False)
이렇게 생성된 model
은 인코더-디코더 구조를 가지는 트랜스포머이므로 이 모델을 포워딩 하려면 인코더 인풋과 디코더 인풋을 넣어줘야 한다. 모델을 만들고 가장 먼저해야되는 작업은 포워딩 테스트라고 개인적으로 생각한다. 임의의 입력을 넣고 출력이 의도대로 나오는지 확인하는 것이다. 이런 작업은 직접 만든 모델이 아닐 수록 중요한데 이렇게 해야지 모델이 제대로 작동하는지 또 어떤 구조로 되어 있는지 쉽게 이해할 수 있기 때문이다. 포워드 테스트를 하기위해 간단한 영어문장으로 예제를 준비한다.
= tokenizer(
encoder_inputs "Studies have been shown that owning a dog is good for you"],
[="pt"
return_tensors'input_ids'].to(device)
)[
= tokenizer(
decoder_targets "개를 키우는 것이 건강에 좋다는 연구 결과가 있습니다."],
[="pt"
return_tensors'input_ids'].to(device) )[
영어 문장은 인코더의 입력이 되고 한국어 문장은 디코더의 타겟이 된다. 아래처럼 모두 숫자로 변환되어 있다.
print( encoder_inputs )
print( decoder_targets )
tensor([[24611, 84, 166, 8135, 38, 847, 91, 16, 8146, 43,
667, 40, 106, 1]], device='cuda:0')
tensor([[15833, 12236, 179, 16120, 28117, 1007, 3883, 327, 3, 1]],
device='cuda:0')
이제 디코더 입력을 만들기위해 model._shift_right
를 사용해 디코더 출력을 오른쪽으로 쉬프트 시킨다.
= model._shift_right(decoder_targets) decoder_inputs
decoder_inputs
와 decoder_targets
이 어떻게 다른지 비교해보면
pd.DataFrame(
[0]),
tokenizer.convert_ids_to_tokens(decoder_targets[0])
tokenizer.convert_ids_to_tokens(decoder_inputs[
],=('decoder target', 'decoder input')
index )
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
---|---|---|---|---|---|---|---|---|---|---|
decoder target | ▁개를 | ▁키우는 | ▁것이 | ▁건강에 | ▁좋다는 | ▁연구 | ▁결과가 | ▁있습니다 | . | </s> |
decoder input | <pad> | ▁개를 | ▁키우는 | ▁것이 | ▁건강에 | ▁좋다는 | ▁연구 | ▁결과가 | ▁있습니다 | . |
위처럼 오른쪽으로 쉬프트된 디코더 입력은 <pad>
토큰이 추가되었다. 이렇게 출력으로 쓰이는 문장을 오른쪽으로 쉬프트시켜 티처포싱Teacher forcing을 진행하게 된다. 다음처럼 model
에 인코더 입력, 디코더 입력, 디코더 타겟을 입력하고 포워드 시킨다.
# forward pass
= model(input_ids=encoder_inputs,
outputs =decoder_inputs,
decoder_input_ids=decoder_targets) labels
model
의 outputs
에는 다음과 같은 키가 있다.
outputs.keys()
odict_keys(['loss', 'logits', 'past_key_values', 'encoder_last_hidden_state'])
손실함수 값을 다음처럼 확인할 수 있고 grad_fn
이 있기 때문에 output.loss
를 백워드 시킬 수 있다.
outputs.loss
tensor(87.8185, device='cuda:0', grad_fn=<NllLossBackward0>)
인코더의 마지막 상태는 (1, 14, 768)이다. 각 숫자는 순서대로 샘플 수, 스탭 수, 모델 사이즈를 나타낸다. 즉 인코더로 들어가는 14개 토큰이 각각 768차원 벡터로 인코딩되었다.
'encoder_last_hidden_state'].shape outputs[
torch.Size([1, 14, 768])
logit
은 디코더 입력 토큰 10개에 대한 그 다음 토큰 예측 10개를 담고있다. 샘플 한개에 대해서 10개 토큰에 대해서 64128개 단어에 대한 확률값이 들어 있다.
'logits'].shape outputs[
torch.Size([1, 10, 64128])
logit
에 argmax
를 씌워서 토큰화시켜보면 다음과 같다.
'logits'][0], axis=1).cpu().numpy() ) tokenizer.convert_ids_to_tokens( torch.argmax(outputs[
['큐브', '큐브', '▁비일비재', '▁비일비재', '▁베네', '▁비일비재', '▁베네', '▁베네', '큐브', '큐브']
마지막 헤더가 학습이 되지 않았기 때문에 적절한 아웃풋이 나오지 않지만 입력과 출력의 텐서 모양을 보면 포워드 패스가 제대로 작동한다는 것을 알 수 있다.
지금까지 데이터 셋, 토크나이저, 모델에 대해서 알아봤다. 이제 학습을 위해 두 단계가 남았는데 하나는 데이터를 미니배치 형태로 모아 주는 콜레이터collator와 나머지 하나는 모델을 평가할 매트릭이다
Collator
파이토치에서 모델을 학습시키기 위해서 DataLoader
를 사용하게 되는데 이 데이터 로더의 역할은 for 루프를 돌면서 데이터 셋으로 부터 샘플을 미니 배치 수만큼 가져오는 것이다. 이때 샘플을 미니 배치 수만큼 무작위로 가져와 어떤 식으로든 각 샘플을 짝맞춤해서 반환해야하는데 크기가 통일된 간단한 이미지 데이터인 경우 특별히 할것이 없지만 서로 크기가 다른 샘플들을 다루는 경우는 반환전 크기 또는 길이를 맞춘다든지 패딩을 한다든지 하는 추가 작업이 필요하게 된다. 이런 작업이 일어나는 곳이 collate_fn
으로 지정되는 함수이다.
시퀀스 투 시퀀스 모델을 학습시킬때 이런 콜레이터 함수가 하는 전형적인 역할은 입력 또는 출력 문자열을 패딩하고 조금 전 모델에서 알아봤듯이 디코더 타겟을 오른쪽으로 한칸 쉬프트 시켜서 디코더 입력으로 만드는 일이다. 앞서 이런 과정을 간단히기 직접 코딩해서 확인했지만 이런 작업을 자동으로 처리해주는 클래스가 DataCollatorForSeq2Seq
이다.
우선 콜레이터를 만들기 위해서는 토크나이저와 모델을 넘겨야 한다.
= DataCollatorForSeq2Seq(tokenizer, model=model) data_collator
앞서 만들어논 tokenized_datasets
에서 샘플 두개를 조회하면 다음처럼 전체 결과는 사전으로 리턴되며 사전의 각 키 아래에 여러 샘플들의 값이 리스트로 들어있게 된다.
# 각 항목아래 샘플들이 리스트 형태로 묶여 반환된다.
"train"][1:3] tokenized_datasets[
{'input_ids': [[4014, 322, 3170, 147, 67, 23274, 3, 1],
[11783, 4412, 96, 6556, 709, 1632, 3, 1]],
'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1]],
'labels': [[6842, 404, 951, 5767, 15387, 27, 831, 800, 4378, 15, 1587, 3, 1],
[9881, 18590, 3837, 70, 4341, 1086, 677, 35, 426, 2255, 3, 1]]}
콜레이터에 샘플을 넘길 때는 개별 샘플이 사전으로 묶이는 형태가 되어야 되므로 아래처럼 한번 가공하게 된다.
# 콜레이터에는 샘플을 개별 {}로 넘겨야 됨
"train"][i] for i in range(1, 3)] [tokenized_datasets[
[{'input_ids': [4014, 322, 3170, 147, 67, 23274, 3, 1],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1],
'labels': [6842, 404, 951, 5767, 15387, 27, 831, 800, 4378, 15, 1587, 3, 1]},
{'input_ids': [11783, 4412, 96, 6556, 709, 1632, 3, 1],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1],
'labels': [9881, 18590, 3837, 70, 4341, 1086, 677, 35, 426, 2255, 3, 1]}]
위에 반환된 결과를 보면 각 샘플이 사전 {}으로 묶이고 샘플 하나에는 input_ids
, attention_mask
, labels
이 존재한다. 각 샘플을 리스트로 묶어서 콜레이터에게 전달하고 반환되는 값을 확인해보자.
# 콜레이터를 돌리면 알아서 패딩하고 쉬프트 시킨다.
= data_collator(
batch "train"][i] for i in range(1, 3)]
[tokenized_datasets[ )
You're using a T5TokenizerFast tokenizer. Please note that with a fast tokenizer, using the `__call__` method is faster than using a method to encode the text followed by a call to the `pad` method to get a padded encoding.
반환된 batch
의 키를 확인해보면 decoder_input_ids
가 생긴것을 확인할 수 있다.
batch.keys()
dict_keys(['input_ids', 'attention_mask', 'labels', 'decoder_input_ids'])
batch
의 각 키에 어떤 값들이 들어있는지 확인해보자.
batch
{'input_ids': tensor([[ 4014, 322, 3170, 147, 67, 23274, 3, 1],
[11783, 4412, 96, 6556, 709, 1632, 3, 1]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1]]), 'labels': tensor([[ 6842, 404, 951, 5767, 15387, 27, 831, 800, 4378, 15,
1587, 3, 1],
[ 9881, 18590, 3837, 70, 4341, 1086, 677, 35, 426, 2255,
3, 1, -100]]), 'decoder_input_ids': tensor([[ 0, 6842, 404, 951, 5767, 15387, 27, 831, 800, 4378,
15, 1587, 3],
[ 0, 9881, 18590, 3837, 70, 4341, 1086, 677, 35, 426,
2255, 3, 1]])}
출력된 batch를 정리하면 아래처럼 된다.
{'input_ids':
4014, 322, 3170, 147, 67, 23274, 3, 1],
tensor([[ 11783, 4412, 96, 6556, 709, 1632, 3, 1]]),
['attention_mask':
1, 1, 1, 1, 1, 1, 1, 1],
tensor([[1, 1, 1, 1, 1, 1, 1, 1]]),
['labels':
tensor(6842, 404, 951, 5767, 15387, 27, 831, 800, 4378, 15, 1587, 3, 1],
[[ 9881,18590,3837,70,4341,1086,677,35,426,2255,3,1,-100]]),
[ 'decoder_input_ids':
tensor(0,6842,404,951,5767,15387, 27, 831, 800, 4378, 15, 1587, 3],
[[ 0,9881,18590,3837,70,4341,1086,677, 35, 426,2255, 3, 1]])
[ }
새로 생긴 decoder_input_ids
는 앞에 0(label
에서 끝에 1이 사라져 label
이 오른쪽으로 쉬프트된 것임을 알 수 있다. 그리고 또 두번째 샘플 labels
에서 마지막에 -100 이 보인다. 이 값은 label
이 패딩된 것을 나타내며 손실 함수값을 계산할 때 -100이 있는 위치는 손실을 계산하지 않게 된다. 이렇게 시퀀스 투 시퀀스 모델을 학습하기 위해 필요한 자잘한 작업을 콜레이터가 알아서 자동으로 처리한다.
Metric
마지막으로 학습한 모델을 측정할 매트릭을 준비해야 한다. 번역 모델에서는 주로 BLEU 점수를 사용한다. BLEU 점수는 번역기가 생성한 문장이 레퍼런스(정답이라는 표현을 사용하지 않는 이유는 제대로 된 번역 문장이 오직 하나가 아니기 때문)문장과 얼마나 비슷한지 측정하는 점수라고 생각하면 된다. 단 같은 단어가 반복된다든지 레퍼런스 문장보다 너무 짧은 문장을 생성한다든지 하면 패널티를 부여 한다. 그렇기 때문에 레퍼런스 문장과 길이가 최대한 비슷하고 다양한 단어를 사용하면서 생성된 문장의 단어가 레퍼런스 단어에 많이 보여야 높은 점수를 얻게 된다.
BLEU를 계산하기 위해 허깅페이스 evaluate 라이브러리와 sacrebleu라이브러리를 제일 처음에 설치했었다.
sacrebleu
라이브러리는 BLEU 구현체에서 사실상 표준 라이브러리이며 각 모델이 다른 토크나이저를 쓰는 경우 이를 BPE로 통일 시켜 BLEU 점수를 계산한다고 한다. 참고링크
evaluate
라이브러리로 이 sacrebleu
를 불러온다.
import evaluate
= evaluate.load("sacrebleu") metric
아래와 같은 예제가 있을 때 두 영어 문장을 번역기가 predictions
처럼 번역했고 데이터 셋에 두 문장의 레퍼런스 번역이 references
처럼 두개씩 있을 때 bleu점수를 계산해보면
= [
predictions "저는 딥러닝을 좋아해요.",
"요즘은 딥러닝 프레임워크가 잘 발달되어 있기 때문에 누구의 도움 없이도 기계 번역 시스템을 구축할 수 있습니다."
]
= [
references "저는 딥러닝을 좋아해요.", "나는 딥러닝을 사랑해요."],
["요즘은 딥러닝 프레임워크가 잘 발달되어 있기 때문에 누구의 도움 없이도 기계 번역 시스템을 구축할 수 있습니다.",
["최근에는 딥러닝 프레임워크가 잘 개발되어 있기 때문에 다른 사람의 도움 없이도 기계 번역 시스템을 개발할 수 있습니다."]
]=predictions, references=references) metric.compute(predictions
{'score': 100.00000000000004,
'counts': [21, 19, 17, 15],
'totals': [21, 19, 17, 15],
'precisions': [100.0, 100.0, 100.0, 100.0],
'bp': 1.0,
'sys_len': 21,
'ref_len': 21}
첫 예에서는 predictions
가 references
의 두 문장 중 하나와 완전히 일치하므로 score
가 100점이 나왔다. 하지만 약간 다른 식으로 번역을 한다면
= [
predictions "저는 딥러닝을 좋아해요.",
"딥러닝 프레임워크가 잘 개발되었기 때문에 요즘은 누군가의 도움 없이 기계번역 시스템을 구축할 수 있다."
]
= [
references "저는 딥러닝을 좋아해요.", "나는 딥러닝을 사랑해요."],
["요즘은 딥러닝 프레임워크가 잘 발달되어 있기 때문에 누구의 도움 없이도 기계 번역 시스템을 구축할 수 있습니다.",
["최근에는 딥러닝 프레임워크가 잘 개발되어 있기 때문에 다른 사람의 도움 없이도 기계 번역 시스템을 개발할 수 있습니다."]
]=predictions, references=references) metric.compute(predictions
{'score': 25.28116160010779,
'counts': [14, 7, 4, 1],
'totals': [19, 17, 15, 13],
'precisions': [73.6842105263158,
41.1764705882353,
26.666666666666668,
7.6923076923076925],
'bp': 0.9000876262522591,
'sys_len': 19,
'ref_len': 21}
점수가 떨어지는 것을 확인할 수 있다. 아래 함수는 모델의 예측과 레이블을 가지고 bleu를 계산하는 헬퍼 함수로 트랜스포머 학습 코스 번역기 매트릭에서 제공하는 코드를 그대로 복사 한 것이다.
def compute_metrics(eval_preds):
= eval_preds
preds, labels
if isinstance(preds, tuple):
= preds[0]
preds
= tokenizer.batch_decode(preds, skip_special_tokens=True)
decoded_preds
# Replace -100 in the labels as we can't decode them.
= np.where(labels != -100, labels, tokenizer.pad_token_id)
labels = tokenizer.batch_decode(labels, skip_special_tokens=True)
decoded_labels
# Some simple post-processing
= [pred.strip() for pred in decoded_preds]
decoded_preds = [[label.strip()] for label in decoded_labels]
decoded_labels
= metric.compute(predictions=decoded_preds, references=decoded_labels)
result = {"bleu": result["score"]}
result
return result
Trainer
학습을 간단히 하기위해 허깅페이스에서 제공하는 Seq2SeqTrainer
클래스를 사용한다. 학습 세부 조건은 Seq2SeqTrainingArguments
를 사용하여 설정한다. 다음 코드로 학습에 필요한 세부 사항을 설정할 수 있다.
= Seq2SeqTrainingArguments(
training_args ="chkpt",
output_dir=0.0005,
learning_rate=0.01,
weight_decay=64,
per_device_train_batch_size=128,
per_device_eval_batch_size=1,
num_train_epochs=500,
save_steps=2,
save_total_limit="epoch",
evaluation_strategy="no",
logging_strategy=True,
predict_with_generate=False,
fp16=2,
gradient_accumulation_steps="none" # Wandb 로그 끄기
report_to )
이런 저런 자잘한 세팅을 해서 training_args
를 만들고 trainer
를 생성한다. 지금까지 준비한 model
, training_args
, tokenized_datasets
, data_collator
, tokenizer
, compute_metrics
를 넘기면 된다.
= Seq2SeqTrainer(
trainer
model,
training_args,=tokenized_datasets["train"],
train_dataset=tokenized_datasets["valid"],
eval_dataset=data_collator,
data_collator=tokenizer,
tokenizer=compute_metrics,
compute_metrics )
이제 아래 코드로 드디어 학습을 할 수 있다!
[주의] 코랩에서 실행한다면
per_device_train_batch_size
를 12정도로 줄여서 학습해야 하는데 학습 시간만 10시간이 넘게 걸린다.
trainer.train()
[노트] 학습 과정에서 출력되는 로그 문장들이 너무 길어서 여기선 생략 되었음
학습이 끝났으면 다음 셀을 실행해서 결과를 저장한다.
"./results") trainer.save_model(
Test
학습과 저장을 성공적으로 마쳤으면 다음 명령으로 모델을 불러올 수 있다.
= "./results"
model_dir = AutoTokenizer.from_pretrained(model_dir)
tokenizer = AutoModelForSeq2SeqLM.from_pretrained(model_dir)
model
; model.cpu()
loading file spiece.model
loading file tokenizer.json
loading file added_tokens.json
loading file special_tokens_map.json
loading file tokenizer_config.json
loading configuration file ./results/config.json
Model config T5Config {
"_name_or_path": "./results",
"architectures": [
"T5ForConditionalGeneration"
],
"d_ff": 2048,
"d_kv": 64,
"d_model": 768,
"decoder_start_token_id": 0,
"dense_act_fn": "gelu_new",
"dropout_rate": 0.1,
"eos_token_id": 1,
"feed_forward_proj": "gated-gelu",
"initializer_factor": 1.0,
"is_encoder_decoder": true,
"is_gated_act": true,
"layer_norm_epsilon": 1e-06,
"model_type": "t5",
"n_positions": 512,
"num_decoder_layers": 12,
"num_heads": 12,
"num_layers": 12,
"pad_token_id": 0,
"relative_attention_max_distance": 128,
"relative_attention_num_buckets": 32,
"torch_dtype": "float32",
"transformers_version": "4.24.0",
"use_cache": true,
"vocab_size": 64128
}
loading weights file ./results/pytorch_model.bin
All model checkpoint weights were used when initializing T5ForConditionalGeneration.
All the weights of T5ForConditionalGeneration were initialized from the model checkpoint at ./results.
If your task is similar to the task the model of the checkpoint was trained on, you can already use T5ForConditionalGeneration for predictions without further training.
로딩된 모델을 테스트하기 위해 다음 두 문장을 준비한다.
= [
input_text "Because deep learning frameworks are well developed, in these days, machine translation system can be built without anyone's help.",
"This system was made by using HuggingFace's T5 model for a one day"
]
모델이 입력하기위해 토크나이저로 토큰화 시킨다.
= tokenizer(input_text, return_tensors="pt",
inputs =True, max_length=max_token_length) padding
/home/metamath/miniconda3/envs/torchflow/lib/python3.9/site-packages/transformers/tokenization_utils_base.py:2322: UserWarning: `max_length` is ignored when `padding`=`True` and there is no truncation strategy. To pad to max length, use `padding='max_length'`.
warnings.warn(
inputs
를 확인해보면 input_ids
와 attention_mask
로 토큰화 된것을 알 수 있다. 첫번째 문장이 더 길기 때문에 두번째 문장의 마스크는 마지막에 0으로 패딩된 것도 확인할 수 있다.
inputs
{'input_ids': tensor([[ 8127, 5859, 5789, 22309, 8, 69, 484, 6560, 4, 20,
572, 1258, 4, 9872, 46301, 1076, 147, 67, 3807, 1215,
3993, 17, 8, 787, 3, 1],
[ 465, 1076, 62, 565, 81, 1676, 992, 60049, 1044, 17400,
17, 8, 745, 466, 3900, 40, 16, 165, 688, 1,
0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0]])}
model.generate()
에 입력을 넣고 출력을 생성한다. 이때 빔서치를 하기 위해 num_beams=5
로 설정한다.
= model.generate(
koreans **inputs,
=max_token_length,
max_length=5,
num_beams
)
koreans.shape
torch.Size([2, 20])
생성된 결과를 디코딩해보면 다음처럼 나쁘지 않게 번역되는 것을 확인할 수 있다.
[
tokenizer.convert_tokens_to_string(for korean in koreans
tokenizer.convert_ids_to_tokens(korean)) ]
['<pad> 딥러닝 틀이 잘 개발되기 때문에 요즘은 누군가의 도움 없이 기계번역 시스템을 구축할 수 있다.</s>',
'<pad> 이 시스템은 HuggingFace의 T5 모델을 하루 동안 사용해 만든 시스템입니다.</s><pad>']
마지막으로 테스트 셋에 대해서 몇개 문장을 가져와 번역해보자. 만들어 놓은 tokenized_datasets
과 data_collator
를 pytorch
DataLoader
에 그대로 전달해서 데이터 로더를 만들 수 있다.
from torch.utils.data import DataLoader
= DataLoader(
test_dataloader "test"], batch_size=32, collate_fn=data_collator
tokenized_datasets[ )
이터레이터로 만들어 한 미니 배치만 가져온다.
= iter(test_dataloader) test_dataloader_iter
= next(test_dataloader_iter) test_batch
콜레이터에 의해 반환된 미니 배치에는 다음처럼 labels
, decoder_input_ids
따위도 가지고 있으므로 모델에 입력하기 위해 input_ids
, attention_mask
만 남긴다.
test_batch.keys()
dict_keys(['input_ids', 'attention_mask', 'labels', 'decoder_input_ids'])
= { key: test_batch[key] for key in ('input_ids', 'attention_mask') } test_input
= model.generate(
koreans **test_input,
=max_token_length,
max_length=5,
num_beams )
이제 입력문장, 정답 그리고 생성된 문장을 비교하기 위해 우선 test_batch.labels
에 -100으로 인코딩된 부분을 패딩 코튼으로 교체 한다.
= np.where(test_batch.labels != -100, test_batch.labels, tokenizer.pad_token_id) labels
= tokenizer.batch_decode(test_batch.input_ids, skip_special_tokens=True)[10:20] eng_sents
= tokenizer.batch_decode(labels, skip_special_tokens=True)[10:20] references
= tokenizer.batch_decode( koreans, skip_special_tokens=True )[10:20] preds
for s in zip(eng_sents, references, preds):
print('English :', s[0])
print('Reference :', s[1])
print('Translated:', s[2])
print('\n')
English : Yes, I'll see you at the parking lot at 3 p.m.
Reference : 네, 오후 3시에 주차장에서 뵙죠.
Translated: 네, 오후 3시에 주차장에서 뵙겠습니다.
English : I'm happy to see Jessica Huh take over my role.
Reference : Jessica Huh가 제 역할을 인계받게 되어서 저는 기니다.
Translated: 제시카 허가 제 역할을 맡아서 기뻐요.
English : I agree with you that she is qualified for the position.
Reference : 저도 부장님 의견대로 그녀가 이 자리의 적임자라고 생각합니다.
Translated: 나는 그녀가 이 직책에 자질이 있다고 당신과 동의합니다.
English : Nick, I was told that your department will be divided into two.
Reference : Nick, 당신 부서가 둘로 나뉜다면서요?
Translated: 닉, 당신의 부서가 두 가지로 나뉘게 될 거라고 들었습니다.
English : Yes, all staff involved in online advertising will have their own office space located on the 2nd floor.
Reference : 네, 다음 달 초부터 온라인 광고와 관련된 직원들은 2층에 위치한 개별 사무실 공간을 사용한다고 합니다.
Translated: 네, 온라인 광고에 관련된 모든 직원들은 2층에 각자의 사무실 공간이 위치해 있습니다.
English : What happens to the remaining staff?
Reference : 남은 직원들은 어떻게 되나요?
Translated: 남은 스태프에게 무슨 일이 일어났나요?
English : The remaining staff will stay in the current space on the 3rd floor, and the division will continue to be called the advertising department.
Reference : 남은 직원들은 3층 현재 자리에 남을 것이고, 부서는 계속 광고 부서로 불린다고 하네요.
Translated: 나머지 직원들은 3층 현 공간에 머물게 되며, 이 부서는 계속 광고부서로 불리게 된다.
English : I have a question about the year-end tax adjustment.
Reference : 이번 연말 정산 관련해서 질문이 있어요.
Translated: 저는 연말정산에 대해 궁금한 점이 있습니다.
English : Is there any problem?
Reference : 무슨 문제라도 있으신가요?
Translated: 혹시 문제가 있나요?
English : I am registering my dependent this time, so do I need to submit any particular documents?
Reference : 제가 이번에 부양가족을 등록하려고 하는데, 따로 제출해야 하는 서류가 있나요?
Translated: 이번에 내 국적 등록을 하고 있으니 특별히 서류 제출을 해야 하나요?
두 시간동안 대충 1 에폭만 학습한 것치고는 꽤 그럴듯 하게 번역을 하는 것을 알 수 있다.
마무리
이상으로 영어-한국어 번역기를 처음부터 학습시키는 방법을 정리했다. 이 글을 글쓴이가 의도한대로 빠르게 읽고 이해하기 위해서는 트랜스포머에 대한 이해가 선행되야 한다. 트랜스포머에 대한 자세한 설명은 진짜로 주석달린 트랜스포머를 참고하자. 하지만 트랜스포머나 스퀀스 투 시퀀스 모델에 대해 잘 모른다 하더라도 한국어 번역기를 만들고자 할때 느끼는 막막함은 어느정도 해소할 수 있으리라 생각한다.
이 글을 읽고 코드를 실행해보고 나서 DataCollatorForSeq2Seq
나 Seq2SeqTrainer
를 쓰지 않고 직접 이 부분을 만들어서 모델을 학습 시켜본다면 트랜스포머를 이용한 번역 작업기 만들기를 훨씬 더 상세히 이해할 수 있을 것이다.